github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/directeurs/equipiers.go (about)

     1  package directeurs
     2  
     3  import (
     4  	"bytes"
     5  	"database/sql"
     6  	"errors"
     7  	"fmt"
     8  	"path"
     9  	"sort"
    10  	"strings"
    11  
    12  	dm "github.com/benoitkugler/goACVE/server/core/datamodel"
    13  	rd "github.com/benoitkugler/goACVE/server/core/rawdata"
    14  	"github.com/benoitkugler/goACVE/server/core/rawdata/matching"
    15  	"github.com/benoitkugler/goACVE/server/core/utils/mails"
    16  	"github.com/benoitkugler/goACVE/server/core/utils/table"
    17  	"github.com/benoitkugler/goACVE/server/documents"
    18  	"github.com/benoitkugler/goACVE/server/shared"
    19  )
    20  
    21  type ResultatRecherche struct {
    22  	Id            int64     `json:"id"`
    23  	Nom           rd.String `json:"nom"`
    24  	Prenom        rd.String `json:"prenom"`
    25  	DateNaissance string    `json:"date_naissance"`
    26  	Pertinence    uint8     `json:"pertinence"` // en %
    27  }
    28  
    29  type EquipierDirecteur struct {
    30  	shared.BaseEquipier
    31  
    32  	Id             int64    `json:"id"`
    33  	LienFormulaire string   `json:"lien_formulaire"`
    34  	Roles          rd.Roles `json:"roles"`
    35  	IsSb           rd.Bool  `json:"is_sb"`
    36  }
    37  
    38  func FromPersonneEquipier(personne rd.Personne, equipier rd.Equipier) EquipierDirecteur {
    39  	out := EquipierDirecteur{BaseEquipier: shared.FromPersonneEquipier(personne, equipier)}
    40  
    41  	out.Id = equipier.Id
    42  	out.Roles = equipier.Roles
    43  
    44  	return out
    45  }
    46  
    47  func (e EquipierDirecteur) ToPersonneEquipier(personne *rd.BasePersonne, equipier *rd.Equipier) {
    48  	e.BaseEquipier.ToPersonneEquipier(personne, equipier)
    49  	equipier.Roles = e.Roles
    50  }
    51  
    52  func (rc DriverCampComplet) scanDataEquipiers() (rd.Personnes, rd.Equipiers, error) {
    53  	equipiers, err := rd.SelectEquipiersByIdCamps(rc.DB, rc.camp.Id)
    54  	if err != nil {
    55  		return nil, nil, err
    56  	}
    57  
    58  	rows, err := rc.DB.Query(`SELECT personnes.* FROM personnes 
    59  		JOIN equipiers ON equipiers.id_personne = personnes.id
    60  		WHERE equipiers.id = ANY($1)`, equipiers.Ids().AsSQL())
    61  	if err != nil {
    62  		return nil, nil, err
    63  	}
    64  	personnes, err := rd.ScanPersonnes(rows)
    65  	return personnes, equipiers, err
    66  }
    67  
    68  // renvoie les données nécessaires à la base locale, pour les équipiers.
    69  func (rc DriverCampComplet) loadDataEquipiers() error {
    70  	personnes, equipiers, err := rc.scanDataEquipiers()
    71  
    72  	// contraintes des équipiers
    73  	equipierContraintes, err := rd.SelectEquipierContraintesByIdEquipiers(rc.DB, equipiers.Ids()...)
    74  	if err != nil {
    75  		return err
    76  	}
    77  
    78  	rc.camp.Base.Personnes = personnes
    79  	rc.camp.Base.Equipiers = equipiers
    80  	rc.camp.Base.EquipierContraintes = equipierContraintes.ByIdEquipier()
    81  	return err
    82  }
    83  
    84  // charge en plus les données nécessaires aux pièces justificatives
    85  func (d DriverCampComplet) loadDataDocumentsEquipiers() error {
    86  	base := d.camp.Base
    87  
    88  	// documents présents
    89  	docs, err := d.LoadDocuments(base.Personnes.Ids())
    90  	if err != nil {
    91  		return err
    92  	}
    93  
    94  	// les contraintes (possibles) sont déjà chargées une fois pour toute
    95  	contraintes := make(rd.Contraintes)
    96  	for _, contrainte := range d.contraintesEquipiers {
    97  		contraintes[contrainte.Id] = contrainte
    98  	}
    99  
   100  	base.Documents = docs.Documents
   101  	base.DocumentPersonnes = docs.Liens
   102  	base.Contraintes = contraintes
   103  	return nil
   104  }
   105  
   106  // getPiecesJustificatives compile les documents et exigences
   107  func (d DriverCampComplet) getPiecesJustificatives(host string) (Pieces, error) {
   108  	err := d.loadDataDocumentsEquipiers()
   109  	if err != nil {
   110  		return Pieces{}, err
   111  	}
   112  
   113  	out := Pieces{Contraintes: d.contraintesEquipiers}
   114  
   115  	cache := d.camp.Base.ResoudDocumentsPersonnes()
   116  	equipiers := d.camp.GetEquipe(nil)
   117  	out.Documents = make([]EquipierDocuments, len(equipiers))
   118  	for index, equipier := range equipiers {
   119  		docs := equipier.GetPersonne().GetDocuments(cache)
   120  		publicDocs, err := d.compileDocs(host, docs)
   121  		if err != nil {
   122  			return Pieces{}, err
   123  		}
   124  		out.Documents[index] = EquipierDocuments{
   125  			Contraintes: equipier.GetContraintes(),
   126  			IdEquipier:  equipier.Id,
   127  			NomPrenom:   equipier.GetPersonne().RawData().NomPrenom().String(),
   128  			Documents:   publicDocs,
   129  		}
   130  	}
   131  	sort.Slice(out.Documents, func(i int, j int) bool {
   132  		return out.Documents[i].NomPrenom < out.Documents[j].NomPrenom
   133  	})
   134  	return out, nil
   135  }
   136  
   137  // setExigenceDocument met à jour une exigence sur un équipier
   138  // la base locale n'est pas mise à jour
   139  func (d DriverCampComplet) setExigenceDocument(params UpdateContrainteEquipierIn) error {
   140  	if _, isKnow := d.contraintesEquipiers[params.IdContrainte]; !isKnow {
   141  		return fmt.Errorf("La catégorie du document à fournir est inconnue (id %d)", params.IdContrainte)
   142  	}
   143  
   144  	tx, err := d.DB.Begin()
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	// on supprime l'éventuelle contrainte actuelle pour l' équipier ...
   150  	ct := rd.EquipierContrainte{IdContrainte: params.IdContrainte, IdEquipier: params.IdEquipier}
   151  	err = ct.Delete(tx)
   152  	if err != nil {
   153  		return shared.Rollback(tx, err)
   154  	}
   155  
   156  	// ... puis on inscrit la nouvelle si nécessaire
   157  	if params.Demande != rd.OBNon {
   158  		ct.Optionnel = params.Demande == rd.OBPeutEtre
   159  		err = rd.InsertManyEquipierContraintes(tx, ct)
   160  		if err != nil {
   161  			return shared.Rollback(tx, err)
   162  		}
   163  	}
   164  
   165  	err = tx.Commit()
   166  	return err
   167  }
   168  
   169  func (d DriverCampComplet) creeDocumentEquipier(host string, idEquipier int64, params documents.ParamsNewDocument) (documents.PublicDocument, error) {
   170  	return documents.CreeDocumentEquipier(d.Signing, d.DB, host, idEquipier, params)
   171  }
   172  
   173  func (d DriverCampComplet) downloadDocumentsEquipiers(onlyRequis bool) (*bytes.Buffer, error) {
   174  	if err := d.loadDataDocumentsEquipiers(); err != nil {
   175  		return nil, err
   176  	}
   177  	idsDocs, prefixes := d.analyseDocsRequis(onlyRequis)
   178  	return d.packageDocs(idsDocs, prefixes)
   179  }
   180  
   181  // requiert d'avoir chargé les données documents
   182  func (d DriverCampComplet) analyseDocsRequis(onlyRequis bool) ([]int64, map[int64]string) {
   183  	criblePers := map[int64]rd.Set{} // id personne -> id contraintes
   184  	for _, equipier := range d.camp.GetEquipe(nil) {
   185  		idPersonne := equipier.GetPersonne().Id
   186  		if onlyRequis {
   187  			criblePers[idPersonne] = equipier.GetContraintes().AsIds().AsSet()
   188  		} else {
   189  			criblePers[idPersonne] = d.contraintesEquipiers.Ids().AsSet()
   190  		}
   191  	}
   192  	var docsIds rd.Ids
   193  	prefixes := map[int64]string{}
   194  	for _, doc := range d.camp.Base.DocumentPersonnes {
   195  		if criblePers[doc.IdPersonne][doc.IdContrainte] {
   196  			docsIds = append(docsIds, doc.IdDocument)
   197  			prefixes[doc.IdDocument] = (d.contraintesEquipiers[doc.IdContrainte].Nom + " " + d.camp.Base.Personnes[doc.IdPersonne].NomPrenom()).String()
   198  		}
   199  	}
   200  	return docsIds, prefixes
   201  }
   202  
   203  // chercheSimilaires effectue le plus efficacement possible
   204  // une recherche contre tous les profils connus
   205  func (ct Controller) chercheSimilaires(in matching.PatternsSimilarite) ([]ResultatRecherche, error) {
   206  	personnes, err := matching.SelectAllPatternSimilaires(ct.DB)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  
   211  	scoreMax, res := matching.ChercheSimilaires(personnes, in)
   212  	out := make([]ResultatRecherche, len(res))
   213  	for index, p := range res {
   214  		out[index] = ResultatRecherche{
   215  			Id:            p.Personne.Id,
   216  			Nom:           p.Personne.Nom,
   217  			Prenom:        p.Personne.Prenom,
   218  			DateNaissance: p.Personne.DateNaissance.String(),
   219  			Pertinence:    uint8(p.Score * 100 / scoreMax),
   220  		}
   221  	}
   222  	return out, nil
   223  }
   224  
   225  func (d DriverCampComplet) getEquipe(host string) ([]EquipierDirecteur, error) {
   226  	pts := d.camp.GetEquipe(nil)
   227  	out := make([]EquipierDirecteur, len(pts))
   228  	cache := d.camp.Base.ResoudDocumentsPersonnes()
   229  	for index, equipier := range pts {
   230  		accesPers := equipier.GetPersonne()
   231  		pers := accesPers.RawData()
   232  		out[index] = FromPersonneEquipier(pers, equipier.RawData())
   233  		out[index].IsSb = accesPers.IsSB(cache)
   234  		s, err := shared.EncodeID(d.Signing, shared.OrEquipier, equipier.Id)
   235  		if err != nil {
   236  			return nil, err
   237  		}
   238  		out[index].LienFormulaire = shared.BuildUrl(host, path.Join(EndPointEquipier, s), nil)
   239  	}
   240  	return out, nil
   241  }
   242  
   243  type errorDirecteur string
   244  
   245  func (e errorDirecteur) Error() string {
   246  	return fmt.Sprintf("Le séjour a déjà un directeur : %s", string(e))
   247  }
   248  
   249  // Ne commit pas ni ne rollback, mais met à jour la base locale.
   250  // Des contraintes de documents par défaut sont ajoutées.
   251  func (d DriverCampComplet) creeEquipier(idPersonne int64, roles rd.Roles, tx *sql.Tx) (rd.Equipier, error) {
   252  	if roles.Is(rd.RDirecteur) {
   253  		// on vérifie qu'un directeur n'est pas déjà présent
   254  		if dir, has := d.camp.GetDirecteur(); has {
   255  			return rd.Equipier{}, errorDirecteur(dir.RawData().NomPrenom())
   256  		}
   257  	}
   258  	equipier := rd.Equipier{
   259  		IdPersonne: idPersonne,
   260  		Roles:      roles,
   261  		IdCamp:     d.camp.Id,
   262  	}
   263  	equipier, err := equipier.Insert(tx)
   264  	if err != nil {
   265  		return equipier, err
   266  	}
   267  	pers, err := rd.SelectPersonne(tx, idPersonne)
   268  	if err != nil {
   269  		return equipier, err
   270  	}
   271  
   272  	equipierContraintes, err := shared.AddEquipierDefautContraintes(tx, d.contraintesEquipiers, equipier)
   273  	if err != nil {
   274  		return equipier, err
   275  	}
   276  	d.camp.Base.Personnes[pers.Id] = pers
   277  	d.camp.Base.Equipiers[equipier.Id] = equipier
   278  	d.camp.Base.EquipierContraintes[equipier.Id] = equipierContraintes
   279  	return equipier, nil
   280  }
   281  
   282  // rattacheEquipier ajoute un participant et met à jour la base locale.
   283  func (d DriverCampComplet) rattacheEquipier(idPersonne int64, roles rd.Roles) (rd.Equipier, error) {
   284  	tx, err := d.DB.Begin()
   285  	if err != nil {
   286  		return rd.Equipier{}, err
   287  	}
   288  	out, err := d.creeEquipier(idPersonne, roles, tx)
   289  	if err != nil {
   290  		return out, shared.Rollback(tx, err)
   291  	}
   292  	err = tx.Commit()
   293  	return out, err
   294  }
   295  
   296  func (d DriverCampComplet) ajouteEquipierTmp(data matching.PatternsSimilarite, roles rd.Roles) (rd.Equipier, error) {
   297  	newPers := rd.Personne{
   298  		BasePersonne: rd.BasePersonne{
   299  			Nom:           data.Nom,
   300  			Prenom:        data.Prenom,
   301  			Sexe:          data.Sexe,
   302  			DateNaissance: data.DateNaissance,
   303  			Mail:          data.Mail,
   304  			NomJeuneFille: data.NomJeuneFille,
   305  		},
   306  		IsTemporaire: true,
   307  	}
   308  	tx, err := d.DB.Begin()
   309  	if err != nil {
   310  		return rd.Equipier{}, err
   311  	}
   312  	newPers, err = newPers.Insert(tx)
   313  	if err != nil {
   314  		return rd.Equipier{}, shared.Rollback(tx, err)
   315  	}
   316  	equipier, err := d.creeEquipier(newPers.Id, roles, tx)
   317  	if err != nil {
   318  		errFinale := shared.Rollback(tx, err)
   319  		if errD, isDirecteur := err.(errorDirecteur); isDirecteur {
   320  			// affiche uniquement l'erreur directeur
   321  			errFinale = errD
   322  		}
   323  		return rd.Equipier{}, errFinale
   324  	}
   325  	err = tx.Commit()
   326  	return equipier, err
   327  }
   328  
   329  func (d DriverCampComplet) modifieEquipier(eq EquipierDirecteur) error {
   330  	if err := eq.Roles.Check(); err != nil {
   331  		return err
   332  	}
   333  	if eq.Roles.Is(rd.RDirecteur) {
   334  		// on vérifie qu'un directeur différent n'est pas déjà présent
   335  		if dir, has := d.camp.GetDirecteurEquipier(); has && dir.Id != eq.Id {
   336  			return errorDirecteur(dir.GetPersonne().RawData().NomPrenom())
   337  		}
   338  	}
   339  
   340  	tx, err := d.DB.Begin()
   341  	if err != nil {
   342  		return err
   343  	}
   344  	equipier, err := rd.SelectEquipier(tx, eq.Id)
   345  	if err != nil {
   346  		return shared.Rollback(tx, err)
   347  	}
   348  	pers, err := rd.SelectPersonne(tx, equipier.IdPersonne)
   349  	if err != nil {
   350  		return shared.Rollback(tx, err)
   351  	}
   352  	eq.ToPersonneEquipier(&pers.BasePersonne, &equipier)
   353  
   354  	equipier, err = equipier.Update(tx)
   355  	if err != nil {
   356  		return shared.Rollback(tx, err)
   357  	}
   358  	pers, err = pers.Update(tx)
   359  	if err != nil {
   360  		return shared.Rollback(tx, err)
   361  	}
   362  	// on commit puis on met à jour la base locale
   363  	err = tx.Commit()
   364  	d.camp.Base.Equipiers[equipier.Id] = equipier
   365  	d.camp.Base.Personnes[pers.Id] = pers
   366  	return err
   367  }
   368  
   369  func (d DriverCampComplet) deleteEquipier(id int64) error {
   370  	tx, err := d.DB.Begin()
   371  	if err != nil {
   372  		return err
   373  	}
   374  	equipier, err := rd.SelectEquipier(tx, id)
   375  	if err != nil {
   376  		return shared.Rollback(tx, err)
   377  	}
   378  
   379  	// on supprime les contraintes
   380  	_, err = rd.DeleteEquipierContraintesByIdEquipiers(tx, id)
   381  	if err != nil {
   382  		return shared.Rollback(tx, err)
   383  	}
   384  
   385  	// puis l'équipier
   386  	_, err = rd.DeleteEquipierById(tx, equipier.Id)
   387  	if err != nil {
   388  		return shared.Rollback(tx, err)
   389  	}
   390  
   391  	// et enfin la personne sous-jacente, si besoin
   392  	isTmp, idDocs, err := shared.DeletePersonne(tx, equipier.IdPersonne)
   393  	if err != nil {
   394  		return shared.Rollback(tx, err)
   395  	}
   396  
   397  	// on commit puis on met à jour la base locale
   398  	if err = tx.Commit(); err != nil {
   399  		return err
   400  	}
   401  
   402  	delete(d.camp.Base.Equipiers, id)
   403  	for _, idDoc := range idDocs {
   404  		delete(d.camp.Base.Documents, idDoc)
   405  		delete(d.camp.Base.DocumentPersonnes, idDoc)
   406  	}
   407  	if isTmp {
   408  		delete(d.camp.Base.Personnes, equipier.IdPersonne)
   409  	}
   410  	return nil
   411  }
   412  
   413  func (d DriverCampComplet) exportListeEquipiers() (*bytes.Buffer, error) {
   414  	e := d.camp.GetEquipe(nil)
   415  	eqs := make(rd.Table, len(e))
   416  	for index, part := range e {
   417  		eqs[index] = part.AsItem()
   418  	}
   419  	return table.GenereListeEquipe(HeaderExportEquipiers, eqs, false)
   420  }
   421  
   422  func (d DriverCampComplet) genereMailInvitationEquipier(host string, equipier dm.AccesEquipier) (to, htmlBody, replyTo string, err error) {
   423  	pers := equipier.GetPersonne().RawData()
   424  	camp := equipier.GetCamp()
   425  	cis, err := shared.EncodeID(d.Signing, shared.OrEquipier, equipier.Id)
   426  	if err != nil {
   427  		err = shared.FormatErr("Une erreur interne empêche le cryptage des informations.", err)
   428  		return
   429  	}
   430  	link := shared.BuildUrl(host, path.Join(EndPointEquipier, cis), nil)
   431  	direc, has := camp.GetDirecteur()
   432  	directeur, replyTo := "", ""
   433  	if has {
   434  		directeur = direc.RawData().FPrenom()
   435  		replyTo = direc.RawData().Mail.String()
   436  	}
   437  	html, err := mails.NewInviteEquipier(camp.RawData(), directeur, pers, link)
   438  	if err != nil {
   439  		err = shared.FormatErr("La création du mail a échoué.", err)
   440  		return
   441  	}
   442  	return pers.Mail.String(), html, replyTo, nil
   443  }
   444  
   445  // inviteOneEquipier envoie un mail et met à jour le champ
   446  // invitation (sur la base locale aussi)
   447  func (d DriverCampComplet) inviteOneEquipier(host string, equipier dm.AccesEquipier, mailer mails.Mailer) error {
   448  	to, html, replyTo, err := d.genereMailInvitationEquipier(host, equipier)
   449  	if err != nil {
   450  		return err
   451  	}
   452  
   453  	subject := fmt.Sprintf("[ACVE] Equipier - %s", d.camp.RawData().Label())
   454  	err = mailer.SendMail(to, subject, html, nil, mails.CustomReplyTo(replyTo))
   455  	if err != nil {
   456  		return shared.FormatErr(fmt.Sprintf("L'envoi d'un mail d'invitation à %s a échoué.", to), err)
   457  	}
   458  
   459  	// si l'équipier a déjà rempli son formulaire, on ne veut pas "revenir en arrière" sur une nouvelle invitation
   460  	newState := rd.Invite
   461  	if equipier.RawData().InvitationEquipier == rd.Verifie {
   462  		newState = rd.Verifie
   463  	}
   464  	row := d.DB.QueryRow("UPDATE equipiers SET invitation_equipier = $1 WHERE id = $2 RETURNING *", newState, equipier.Id)
   465  	out, err := rd.ScanEquipier(row)
   466  	if err != nil {
   467  		return shared.FormatErr("L'invitation a bien été envoyée, mais la mise à jour de la base de données a échoué.", err)
   468  	}
   469  	d.camp.Base.Equipiers[equipier.Id] = out
   470  	return nil
   471  }
   472  
   473  // inviteFormulaireEquipier envoie un mail et met à jour le champ
   474  // invitation (sur la base locale aussi)
   475  func (d DriverCampComplet) inviteFormulaireEquipier(host string, id int64) error {
   476  	eq := d.camp.Base.NewEquipier(id)
   477  	return d.inviteOneEquipier(host, eq, mails.NewMailer(d.SMTP))
   478  }
   479  
   480  // inviteFormulairesEquipiers envoie les mails et met à jour le champ invitation
   481  // (aussi sur la base locale)
   482  func (d DriverCampComplet) inviteFormulairesEquipiers(host string, onlyNew bool) error {
   483  	pool, err := mails.NewPool(d.SMTP, nil)
   484  	if err != nil {
   485  		return err
   486  	}
   487  	defer pool.Close()
   488  	var errsMails []string
   489  	for _, equipier := range d.camp.GetEquipe(nil) {
   490  		inv := equipier.RawData().InvitationEquipier
   491  		if (onlyNew && inv == rd.NonInvite) || (!onlyNew && inv <= rd.Invite) {
   492  			err := d.inviteOneEquipier(host, equipier, pool)
   493  			if err != nil {
   494  				// on enregistre l'erreur et on continue
   495  				errsMails = append(errsMails, err.Error())
   496  			}
   497  		}
   498  	}
   499  	if len(errsMails) > 0 {
   500  		err := "Des erreurs pendant l'envoi des mails ont été rencontrées : <br/>" + strings.Join(errsMails, "<br/>")
   501  		return errors.New(err)
   502  	}
   503  	return nil
   504  }