github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/client/controllers/cont_suivi_dossiers.go (about)

     1  package controllers
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"path/filepath"
     9  	"sort"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/benoitkugler/goACVE/server/core/documents"
    14  	"github.com/benoitkugler/goACVE/server/core/utils/importmail"
    15  
    16  	"github.com/benoitkugler/goACVE/server/core/apiserver"
    17  	dm "github.com/benoitkugler/goACVE/server/core/datamodel"
    18  	rd "github.com/benoitkugler/goACVE/server/core/rawdata"
    19  )
    20  
    21  const (
    22  	Indifferent CritereAttente = ""
    23  	// n'affiche pas les dossiers uniquement composé d'inscrits
    24  	MasquerNoAttente CritereAttente = "need_attente"
    25  	// n'affiche pas les dossiers uniquement composé d'attentes
    26  	MasquerOnlyAttente CritereAttente = "need_inscrit"
    27  )
    28  
    29  // SuiviDossiers implémentes des fonctionnalités de communication avec les familles.
    30  type SuiviDossiers struct {
    31  	Onglet    OngletSuiviDossiers
    32  	main      *MainController
    33  	Base      *dm.BaseLocale
    34  	Etat      EtatSuiviDossiers
    35  	EditRight bool // Droit de suppression/ajout/edition
    36  	Liste     rd.Table
    37  	Header    []rd.Header
    38  }
    39  
    40  type ButtonsSuiviDossiers struct {
    41  	Creer, Supprimer, EnvoiDocuments, Alertes EtatSideButton
    42  }
    43  
    44  type OngletSuiviDossiers interface {
    45  	baseOnglet
    46  
    47  	ShowPreviewFacture(fac dm.AccesFacture)
    48  
    49  	ConfirmeSupprimeFacture(html string) bool
    50  	// PrevisualiseMail(mail mails.MailRenderer, pjs []mails.PieceJointe, to string) (mailHtml string)
    51  	ConfirmeMail(fac dm.AccesFacture, messageKind rd.MessageKind) bool
    52  	ConfirmeSupprimeMessages(nb int) bool
    53  }
    54  
    55  type CritereAttente string
    56  
    57  type EtatSuiviDossiers struct {
    58  	FactureCurrent rd.IId         // uniquement dossier
    59  	MessageCurrent rd.OptionnalId // message affiché
    60  	Recherche      string
    61  	CriteresTri    dm.CriteresTri
    62  	CritereCamp    rd.OptionnalId
    63  	CritereAcquite rd.Completion
    64  	CritereAttente CritereAttente
    65  }
    66  
    67  func NewSuiviDossiers(main *MainController, permission int) *SuiviDossiers {
    68  	s := SuiviDossiers{main: main, Base: main.Base, EditRight: permission >= 2,
    69  		Header: []rd.Header{
    70  			{Field: dm.PersonneNomPrenom, Label: "Responsable"},
    71  			{Field: dm.FactureParticipants, Label: "Participants"},
    72  			{Field: dm.FactureCamps, Label: "Camps"},
    73  		}}
    74  	s.resetData()
    75  	return &s
    76  }
    77  
    78  func (c *SuiviDossiers) resetData() {
    79  	cache := c.Base.NewCacheEtatFinancier()
    80  	cache2 := c.Base.ResoudMessages()
    81  	c.Liste = nil
    82  	for id, rawFac := range c.Base.Factures {
    83  		// on n'affiche uniquement les dossiers validés (et non les nouvelles inscriptions)
    84  		if !rawFac.IsValidated {
    85  			continue
    86  		}
    87  
    88  		acFac := c.Base.NewFacture(id)
    89  		match := true
    90  		if c.Etat.CritereCamp.IsNotNil() {
    91  			camps, _ := acFac.Camps(cache.FParticipants)
    92  			match = match && camps[c.Etat.CritereCamp.Int64]
    93  		}
    94  		if c.Etat.CritereAcquite != 0 {
    95  			bilan := acFac.EtatFinancier(cache, false)
    96  			match = match && (c.Etat.CritereAcquite == bilan.StatutPaiement())
    97  		}
    98  		var hasAtLeastOneAttente, hasAtLeastOneInscrit bool
    99  		for _, part := range acFac.GetDossiers(cache.FParticipants) {
   100  			if part.RawData().ListeAttente.IsInscrit() {
   101  				hasAtLeastOneInscrit = true
   102  			} else {
   103  				hasAtLeastOneAttente = true
   104  			}
   105  		}
   106  		switch c.Etat.CritereAttente {
   107  		case MasquerNoAttente:
   108  			match = match && hasAtLeastOneAttente
   109  		case MasquerOnlyAttente:
   110  			match = match && hasAtLeastOneInscrit
   111  		}
   112  
   113  		if match {
   114  			c.Liste = append(c.Liste, acFac.AsItem(cache.FParticipants, cache2, cache.FPaiements))
   115  		}
   116  	}
   117  
   118  	if strings.TrimSpace(c.Etat.Recherche) != "" {
   119  		// on enlève les camp de la recherche pour éviter les faux positifs
   120  		// (ex Grand)
   121  		c.Liste = dm.RechercheDetaillee(c.Liste, c.Etat.Recherche, c.Header[0:2])
   122  	}
   123  
   124  	// besoin de déselectionner si absent de la Recherche :
   125  	if c.Etat.FactureCurrent != nil && !HasId(c.Liste, c.Etat.FactureCurrent) {
   126  		c.Etat.FactureCurrent = nil
   127  	}
   128  
   129  	dateDernierCamp := func(fac rd.Item) time.Time {
   130  		camps, _ := c.Base.NewFacture(fac.Id.Int64()).Camps(cache.FParticipants)
   131  		var last time.Time
   132  		for idCamp := range camps {
   133  			if dateDebut := c.Base.Camps[idCamp].DateDebut.Time(); last.Before(dateDebut) {
   134  				last = dateDebut
   135  			}
   136  		}
   137  		return last
   138  	}
   139  
   140  	// on tri systématiquement par date de séjour, avant
   141  	// de considérer les critères utilisateurs
   142  	sort.Slice(c.Liste, func(i, j int) bool {
   143  		di := dateDernierCamp(c.Liste[i])
   144  		dj := dateDernierCamp(c.Liste[j])
   145  		return di.After(dj) // plus récent en haut
   146  	})
   147  	// s'il n'y a pas de critère, c'est un tri par Id, à éviter
   148  	if len(c.Etat.CriteresTri.Fields) > 0 {
   149  		dm.SortCriteres(c.Liste, c.Etat.CriteresTri)
   150  	}
   151  }
   152  
   153  func (c *SuiviDossiers) SideButtons() ButtonsSuiviDossiers {
   154  	bs := ButtonsSuiviDossiers{}
   155  	isActif := ButtonActivated
   156  	if c.Etat.FactureCurrent == nil {
   157  		isActif = ButtonPresent
   158  	}
   159  	if c.EditRight {
   160  		bs.Supprimer = isActif
   161  		bs.Creer = ButtonActivated
   162  		bs.EnvoiDocuments = ButtonActivated
   163  	}
   164  	bs.Alertes = ButtonActivated
   165  	return bs
   166  }
   167  
   168  func (c *SuiviDossiers) CreeFacture(idResponsable int64, participants rd.Table) {
   169  	currentsFac := c.Base.Factures
   170  	idParts := make(rd.Ids, 0, len(participants))
   171  	// on vérifie que les participants ne sont pas déjà "attribués"
   172  	for _, partItem := range participants {
   173  		part := c.Base.NewParticipant(partItem.Id.Int64())
   174  		if idF := part.RawData().IdFacture; idF.IsNotNil() {
   175  			if _, isIn := currentsFac[idF.Int64]; isIn {
   176  				c.main.ShowError(fmt.Errorf("Le participant %s (%s) est déjà associé à une facture !",
   177  					part.GetPersonne().RawData().NomPrenom(), part.GetCamp().RawData().Label()))
   178  				return
   179  			}
   180  		}
   181  		idParts = append(idParts, part.Id)
   182  	}
   183  
   184  	params := apiserver.CreateFactureIn{IdResponsable: idResponsable, IdParticipants: idParts}
   185  	c.main.ShowStandard("Création du dossier...", true)
   186  	var out apiserver.CreateFactureOut
   187  	err := requete(apiserver.UrlFactures, http.MethodPut, params, &out)
   188  	if err != nil {
   189  		c.main.ShowError(err)
   190  		return
   191  	}
   192  
   193  	c.Base.Factures[out.Facture.Id] = out.Facture
   194  	for _, part := range out.Participants {
   195  		c.Base.Participants[part.Id] = part
   196  	}
   197  	c.Base.Messages[out.Message.Id] = out.Message
   198  	c.Etat.FactureCurrent = rd.IdFacture(out.Facture.Id)
   199  	c.main.ShowStandard("Dossier créé avec succès.", false)
   200  	c.main.ResetAllControllers()
   201  	c.ResetRender()
   202  }
   203  
   204  // UpdateFacture met à jour de manière synchrone la facture donnée.
   205  func (c *SuiviDossiers) UpdateFacture(fac rd.Facture) *rd.Facture {
   206  	c.main.ShowStandard("Mise à jour du dossier...", true)
   207  	out := new(rd.Facture)
   208  	err := requete(apiserver.UrlFactures, http.MethodPost, fac, out)
   209  	if err != nil {
   210  		c.main.ShowError(err)
   211  		return nil
   212  	}
   213  	c.Base.Factures[out.Id] = *out
   214  	c.main.ShowStandard("Dossier mis à jour avec succès.", false)
   215  	c.Reset()
   216  	return out
   217  }
   218  
   219  // SupprimeFacture supprime la facture courante sur le serveur, après confirmation.
   220  func (c *SuiviDossiers) SupprimeFacture() {
   221  	if c.Etat.FactureCurrent == nil {
   222  		return
   223  	}
   224  	fac := c.Base.NewFacture(c.Etat.FactureCurrent.Int64())
   225  	paiements, participants := fac.GetPaiements(nil, true), fac.GetDossiers(nil)
   226  	msg := fmt.Sprintf("Confirmez-vous la suppression du dossier %s ? <br/>", fac.GetPersonne().RawData().NomPrenom())
   227  	if len(paiements) > 1 {
   228  		msg += fmt.Sprintf("<i>%d</i> paiements associés seront <b>supprimés</b>!<br/>", len(paiements))
   229  	} else if len(paiements) == 1 {
   230  		msg += "un paiement associé sera <b>supprimé</b>!<br/>"
   231  	}
   232  	if len(participants) > 1 {
   233  		msg += fmt.Sprintf("%d participants sont liés à ce dossier et seront déliés (mais non supprimés). <br/>", len(participants))
   234  	} else if len(participants) == 1 {
   235  		msg += "un participant est lié à ce dossier et sera délié (mais non supprimé). <br/>"
   236  
   237  	}
   238  	msg += "Attention, l'espace personnel associé sera <b>détruit</b> !"
   239  
   240  	if !c.Onglet.ConfirmeSupprimeFacture(msg) {
   241  		c.main.ShowStandard("Suppression annulée", false)
   242  		return
   243  	}
   244  
   245  	job := func() (interface{}, error) {
   246  		var out apiserver.DeleteFactureOut
   247  		err := requete(apiserver.UrlFactures, http.MethodDelete, IdAsParams(fac.Id), &out)
   248  		return out, err
   249  	}
   250  	onSuccess := func(_out interface{}) {
   251  		out := _out.(apiserver.DeleteFactureOut)
   252  		for _, idPaiement := range out.DeletedPaiements {
   253  			delete(c.Base.Paiements, idPaiement)
   254  		}
   255  		for id, part := range out.Participants {
   256  			c.Base.Participants[id] = part
   257  		}
   258  		c.Base.DeleteFacture(fac.Id)
   259  		msg = fmt.Sprintf("Facture bien supprimée.")
   260  		if L := len(out.DeletedPaiements); L > 0 {
   261  			msg += fmt.Sprintf(" %d paiement(s) lié(s) aussi supprimé(s).", L)
   262  		}
   263  		c.main.ShowStandard(msg, false)
   264  		c.main.ResetAllControllers()
   265  	}
   266  
   267  	c.Etat.FactureCurrent = nil
   268  	c.main.ShowStandard("Suppression du dossier en cours...", true)
   269  	c.main.Background.Run(job, onSuccess)
   270  }
   271  
   272  // AjouteParticipant ajoute le participant à la facture courante.
   273  func (c *SuiviDossiers) AjouteParticipant(participant dm.AccesParticipant) *dm.AccesParticipant {
   274  	if c.Etat.FactureCurrent == nil {
   275  		return nil
   276  	}
   277  
   278  	raw := participant.RawData()
   279  	if !raw.IdFacture.IsNil() {
   280  		c.main.ShowError(fmt.Errorf("Le participant %s (%s) est déjà lié à un dossier !",
   281  			participant.GetPersonne().RawData().NomPrenom(), participant.GetCamp().RawData().Label()))
   282  		return nil
   283  	}
   284  	idFac := c.Etat.FactureCurrent.Int64()
   285  	raw.IdFacture = rd.NewOptionnalId(idFac)
   286  	return c.main.Controllers.Camps.UpdateParticipant(raw)
   287  }
   288  
   289  // UpdateParticipant est synchrone
   290  func (c *SuiviDossiers) UpdateParticipant(participant rd.Participant) {
   291  	c.main.Controllers.Camps.UpdateParticipant(participant)
   292  }
   293  
   294  // RetireParticipant enlève le participant donné de la facture courante, de manière SYNCHRONE.
   295  // Si `supprime` vaut true, le participant est supprimé.
   296  func (c *SuiviDossiers) RetireParticipant(participant dm.AccesParticipant, supprime bool) {
   297  	if supprime {
   298  		c.main.Controllers.Camps.SupprimeParticipant(participant, true)
   299  		return
   300  	}
   301  	raw := participant.RawData()
   302  	raw.IdFacture = rd.OptionnalId{}
   303  	c.main.Controllers.Camps.UpdateParticipant(raw)
   304  }
   305  
   306  // AjouteAide est synchrone. Gère l'erreur et renvoi nil.
   307  func (c *SuiviDossiers) AjouteAide(aide rd.Aide) *rd.Aide {
   308  	created, err := JobCreeAide(aide)
   309  	if err != nil {
   310  		c.main.ShowError(err)
   311  		return nil
   312  	}
   313  	c.Base.Aides[created.Id] = created
   314  	return &created
   315  }
   316  
   317  // UpdateAide est synchrone
   318  func (c *SuiviDossiers) UpdateAide(aide rd.Aide) {
   319  	c.main.Controllers.Aides.UpdateAide(aide)
   320  }
   321  
   322  // SupprimeAide est asynchrone
   323  func (c *SuiviDossiers) SupprimeAide(aide int64) {
   324  	c.main.Controllers.Aides.SupprimeAide(aide)
   325  }
   326  
   327  // SelectFacture sélectionne la facture donnée. Si nécessaire,
   328  // remet à zéro les critères.
   329  func (c *SuiviDossiers) SelectFacture(fac dm.AccesFacture) {
   330  	if _, isIn := c.Base.Factures[fac.Id]; !isIn {
   331  		c.main.ShowError(fmt.Errorf("Le dossier demandé (id %d) n'est pas présent dans la base !", fac.Id))
   332  		return
   333  	}
   334  
   335  	c.Etat.FactureCurrent = rd.IdFacture(fac.Id)
   336  	if !HasId(c.Liste, c.Etat.FactureCurrent) { // restreint l'affichage pour des raisons de performances
   337  		pers := fac.GetPersonne().RawData()
   338  		c.Etat.Recherche = pers.NomPrenom().String()
   339  		c.Etat.CritereAcquite = 0
   340  		c.Etat.CritereCamp = rd.NullId()
   341  		c.resetData()
   342  	}
   343  	c.Onglet.GrabFocus()
   344  	c.ResetRender()
   345  }
   346  
   347  // GenereExportDocument génère la facture au format .pdf, l'enregistre dans local,
   348  // et renvoie le chemin.
   349  // Les aides et paiements invalides ne sont pas affichés.
   350  func (m *MainController) GenereExportDocument(meta documents.DynamicDocument) string {
   351  	b, err := meta.Generate()
   352  	if err != nil {
   353  		m.ShowError(fmt.Errorf("Erreur pendant la génération du document : %s", err))
   354  		return ""
   355  	}
   356  	path, err := filepath.Abs(filepath.Join(LocalFolder, meta.FileName()))
   357  	if err != nil {
   358  		m.ShowError(fmt.Errorf("Impossible d'enregistrer le document: %s", err))
   359  		return ""
   360  	}
   361  	if err = ioutil.WriteFile(path, b, 0666); err != nil {
   362  		m.ShowError(fmt.Errorf("Impossible d'enregistrer le document: %s", err))
   363  		return ""
   364  	}
   365  	m.ShowStandard(fmt.Sprintf("Document généré dans %s", path), false)
   366  	return path
   367  }
   368  
   369  func (c *SuiviDossiers) RenderAttestation() string {
   370  	id := c.Etat.FactureCurrent
   371  	if id == nil {
   372  		c.main.ShowError(errors.New("Aucun dossier courant !"))
   373  		return ""
   374  	}
   375  	fac := c.Base.NewFacture(id.Int64())
   376  	meta := documents.Presence{
   377  		Destinataire: fac.GetPersonne().RawData().ToDestinataire(),
   378  		// on ne sélectionne pas la liste d'attente
   379  		Participants: fac.EtatFinancier(dm.CacheEtatFinancier{}, false).Participants,
   380  	}
   381  	path := c.Main().GenereExportDocument(meta)
   382  	return path
   383  }
   384  
   385  func (c *SuiviDossiers) HasNotifications() bool {
   386  	a, f := c.Notifications()
   387  	return len(a)+len(f) > 0
   388  }
   389  
   390  // Notifications renvoie les messages non lus et les aides déclarées
   391  // sur les espaces persos.
   392  func (c *SuiviDossiers) Notifications() ([]dm.AccesAide, []dm.AccesFacture) {
   393  	var aides []dm.AccesAide
   394  	for _, aide := range c.Base.Aides {
   395  		if !aide.Valide {
   396  			aides = append(aides, c.Base.NewAide(aide.Id))
   397  		}
   398  	}
   399  	sort.Slice(aides, func(i, j int) bool { // force l'idempotance
   400  		return aides[i].Id < aides[j].Id
   401  	})
   402  	sort.SliceStable(aides, func(i, j int) bool {
   403  		di := aides[i].GetParticipant().GetCamp().RawData().DateDebut.Time()
   404  		dj := aides[j].GetParticipant().GetCamp().RawData().DateDebut.Time()
   405  		return di.After(dj) // camp les plus récent au début
   406  	})
   407  	idsFactures := rd.NewSet()
   408  	factureToMessage := map[int64]rd.Message{} // on garde le message concerné pour trier
   409  	for _, message := range c.Base.Messages {
   410  		if message.Kind == rd.MResponsable && !message.Vu {
   411  			// on ajoute le dossier correspondant
   412  			idsFactures.Add(message.IdFacture)
   413  			// si deux nouveaux messages appartiennent au même dossier,
   414  			// on utilise le plus récent pour trier
   415  			// dans la plupart des cas, currentMessage est zéro, et la condition est vraie
   416  			if currentMessage := factureToMessage[message.Id]; message.Created.Time().After(currentMessage.Created.Time()) {
   417  				factureToMessage[message.Id] = message
   418  			}
   419  		}
   420  	}
   421  	var factures []dm.AccesFacture
   422  	for id := range idsFactures {
   423  		factures = append(factures, c.Base.NewFacture(id))
   424  	}
   425  	sort.Slice(factures, func(i, j int) bool { // force l'idempotance
   426  		return factures[i].Id < factures[j].Id
   427  	})
   428  	sort.SliceStable(factures, func(i, j int) bool {
   429  		di := factureToMessage[factures[i].Id].Created.Time()
   430  		dj := factureToMessage[factures[j].Id].Created.Time()
   431  		return di.After(dj)
   432  	})
   433  	return aides, factures
   434  }
   435  
   436  // ImportMail extrait de la source du mail un Message
   437  // L'expéditeur détermine le dossier
   438  func (c *SuiviDossiers) ImportMail(source string) (importmail.Mail, rd.OptionnalId, error) {
   439  	mail, err := importmail.NewMail(source)
   440  	if err != nil {
   441  		err = fmt.Errorf("Impossible de décoder l'email. Détails : <br/> <i>%v</i>", err)
   442  		return mail, rd.NullId(), err
   443  	}
   444  	candidats := rd.NewSet()
   445  	// on cherche l'adresse mail dans les responsables
   446  	for idFacture := range c.Base.Factures {
   447  		fac := c.Base.NewFacture(idFacture)
   448  		if fac.GetPersonne().RawData().Mail.ToLower() == mail.From {
   449  			candidats.Add(idFacture)
   450  		}
   451  	}
   452  	// puis, si besoin, dans les mails en copie
   453  	if len(candidats) == 0 {
   454  		for _, fac := range c.Base.Factures {
   455  			for _, mailCopie := range fac.CopiesMails {
   456  				if strings.TrimSpace(strings.ToLower(mailCopie)) == mail.From {
   457  					candidats.Add(fac.Id)
   458  				}
   459  			}
   460  		}
   461  	}
   462  	// choix du plus récent
   463  	cache1, cache2 := c.Base.ResoudMessages(), c.Base.ResoudPaiements()
   464  	ids := candidats.Keys()
   465  	sort.Slice(ids, func(i, j int) bool {
   466  		fi, fj := c.Base.NewFacture(ids[i]), c.Base.NewFacture(ids[j])
   467  		lastI, lastJ := fi.GetEtat(cache1, cache2).LastModified(), fj.GetEtat(cache1, cache2).LastModified()
   468  		return lastI.Time().Before(lastJ.Time())
   469  	})
   470  	var idHint rd.OptionnalId
   471  	if L := len(ids); L > 0 {
   472  		idHint = rd.NewOptionnalId(ids[L-1])
   473  	}
   474  	return mail, idHint, nil
   475  }
   476  
   477  func (c *SuiviDossiers) CreateMessageResponsable(content rd.String, idRespo int64, imported importmail.Mail) {
   478  	imported.Content = content.String()
   479  	if imported.Received.IsZero() { // absent du mail -> on utilise la date courante (à modifier ?)
   480  		imported.Received = time.Now()
   481  	}
   482  	message, contenu := imported.AsMessage(idRespo)
   483  	params := apiserver.CreateMessageMessage{Message: message, MessageMessage: contenu}
   484  	job := func() (interface{}, error) {
   485  		var out apiserver.CreateMessageMessage
   486  		err := requete(apiserver.UrlMessages, http.MethodPut, params, &out)
   487  		return out, err
   488  	}
   489  	onSuccess := func(_out interface{}) {
   490  		out := _out.(apiserver.CreateMessageMessage)
   491  		c.Base.Messages[out.Message.Id] = out.Message
   492  		c.Base.MessageMessages[out.MessageMessage.IdMessage] = out.MessageMessage
   493  		c.main.ShowStandard("Message ajouté avec succès.", false)
   494  		c.Reset()
   495  	}
   496  	c.main.ShowStandard("Ajout du message en cours...", true)
   497  	c.main.Background.Run(job, onSuccess)
   498  }
   499  
   500  func (c *SuiviDossiers) FusionneDossiers(params apiserver.FusionneFacturesIn) {
   501  	job := func() (interface{}, error) {
   502  		var out apiserver.FusionneFacturesOut
   503  		err := requete(apiserver.UrlFacturesFusion, http.MethodPost, params, &out)
   504  		return out, err
   505  	}
   506  	onSuccess := func(_out interface{}) {
   507  		out := _out.(apiserver.FusionneFacturesOut)
   508  		c.Base.ApplyFusionneFactures(out)
   509  		c.main.ShowStandard("Dossier fusionné avec succés.", false)
   510  		c.main.ResetAllControllers()
   511  	}
   512  	c.main.ShowStandard("Fusion du dossier...", true)
   513  	c.main.Background.Run(job, onSuccess)
   514  }