github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/directeurs/inscrits.go (about) 1 package directeurs 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "path" 8 "sort" 9 "time" 10 11 dm "github.com/benoitkugler/goACVE/server/core/datamodel" 12 cd "github.com/benoitkugler/goACVE/server/core/documents" 13 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 14 "github.com/benoitkugler/goACVE/server/core/utils/pdf" 15 "github.com/benoitkugler/goACVE/server/core/utils/table" 16 "github.com/benoitkugler/goACVE/server/documents" 17 "github.com/benoitkugler/goACVE/server/shared" 18 "github.com/labstack/echo" 19 ) 20 21 // ResumeMessage simplifie l'affichage des messages 22 // pour le directeur. 23 type ResumeMessage struct { 24 Label string `json:"label"` 25 Created time.Time `json:"created"` 26 Contenu string `json:"contenu"` 27 Kind rd.MessageKind `json:"kind"` 28 } 29 30 type Responsable struct { 31 Valide bool `json:"valide"` 32 Id int64 `json:"id"` 33 Nom string `json:"nom"` 34 Prenom string `json:"prenom"` 35 Mail string `json:"mail"` 36 MailsCopies []string `json:"mails_copies"` 37 Tels string `json:"tels"` 38 Adresse string `json:"adresse"` 39 CodePostal string `json:"code_postal"` 40 Ville string `json:"ville"` 41 PaiementComplet rd.Completion `json:"paiement_complet"` 42 Messages []ResumeMessage `json:"messages"` 43 InscriptionValide bool `json:"inscription_valide"` 44 } 45 46 type InscritWritable struct { 47 IdGroupe rd.OptionnalId `json:"id_groupe"` 48 Options rd.OptionsParticipant `json:"options"` 49 Mail string `json:"mail"` 50 } 51 52 func (i InscritWritable) ToPersonneParticipant(personne *rd.BasePersonne, participant *rd.Participant) { 53 participant.Options = i.Options 54 55 personne.Mail = rd.String(i.Mail) 56 } 57 58 type Inscrit struct { 59 InscritWritable 60 61 Id int64 `json:"id"` 62 63 IsAttente bool `json:"is_attente"` 64 HasAnniversaire bool `json:"has_anniversaire"` 65 AgeDebutCamp int `json:"age_debut_camp"` 66 67 Responsable Responsable `json:"responsable"` 68 69 Nom string `json:"nom"` 70 Prenom string `json:"prenom"` 71 Sexe rd.Sexe `json:"sexe"` 72 DateNaissance rd.Date `json:"date_naissance"` 73 74 // Fiche sanitaire et vaccins 75 FicheSanitaire rd.FicheSanitaire `json:"fiche_sanitaire"` 76 Vaccins []documents.PublicDocument `json:"vaccins"` 77 IsFicheSanitaireUpToDate rd.OptionnalBool `json:"is_fiche_sanitaire_up_to_date"` 78 LienFicheSanitaire string `json:"lien_fiche_sanitaire"` 79 80 // Pour les participants simples 81 Info rd.String `json:"info"` 82 } 83 84 type optionsExportExcel struct { 85 bus rd.Bus 86 triGroupe bool 87 withAttente bool 88 showColors bool 89 simple bool 90 } 91 92 func newOptionsExportExcel(c echo.Context) optionsExportExcel { 93 return optionsExportExcel{ 94 bus: rd.Bus(c.QueryParam("bus")), 95 triGroupe: c.QueryParam("with_groupe") != "", 96 withAttente: c.QueryParam("with_attente") != "", 97 showColors: c.QueryParam("show_colors") != "", 98 simple: c.QueryParam("simple") != "", 99 } 100 } 101 102 // charge les infos de base et met à jour `base` 103 // (fonctionnalité partagée avec joomeo) 104 func (rc DriverCampComplet) loadInscritsResponsables() error { 105 groupes, campContraintes, groupesContraintes, err := loadGroupes(rc.DB, rc.camp.Id) 106 if err != nil { 107 return err 108 } 109 110 rows, err := rc.DB.Query(`SELECT factures.* FROM factures 111 JOIN participants ON participants.id_facture = factures.id 112 WHERE participants.id_camp = $1`, rc.camp.Id) 113 if err != nil { 114 return err 115 } 116 factures, err := rd.ScanFactures(rows) 117 if err != nil { 118 return err 119 } 120 121 // on charge les participants : concernés directement ou via un dossier 122 rows, err = rc.DB.Query("SELECT * FROM participants WHERE id_camp = $1 OR id_facture = ANY($2)", rc.camp.Id, factures.Ids().AsSQL()) 123 if err != nil { 124 return err 125 } 126 participants, err := rd.ScanParticipants(rows) 127 if err != nil { 128 return err 129 } 130 131 // les personnes sont les responsables et les participants (même indirects) 132 rows, err = rc.DB.Query(`SELECT * FROM personnes WHERE 133 id = ANY(SELECT id_personne FROM factures WHERE id = ANY($1)) OR 134 id = ANY(SELECT id_personne FROM participants WHERE id = ANY($2))`, 135 factures.Ids().AsSQL(), participants.Ids().AsSQL()) 136 if err != nil { 137 return err 138 } 139 personnes, err := rd.ScanPersonnes(rows) 140 if err != nil { 141 return err 142 } 143 144 rc.camp.Base.Groupes = groupes 145 rc.camp.Base.GroupeContraintes = groupesContraintes.ByIdGroupe() 146 rc.camp.Base.CampContraintes = campContraintes.ByIdCamp() 147 rc.camp.Base.Personnes = personnes 148 rc.camp.Base.Participants = participants 149 rc.camp.Base.Factures = factures 150 return nil 151 } 152 153 // renvoie les données nécessaires aux inscrits (dont informations financières et documents) 154 func (d DriverCampComplet) loadDataInscrits() error { 155 err := d.loadInscritsResponsables() 156 if err != nil { 157 return err 158 } 159 160 aides, err := rd.SelectAidesByIdParticipants(d.DB, d.camp.Base.Participants.Ids()...) 161 if err != nil { 162 return err 163 } 164 165 structures, err := rd.SelectAllStructureaides(d.DB) // on peut se permettre de tout renvoyer, la table est petite. 166 if err != nil { 167 return err 168 } 169 170 paiements, err := rd.SelectPaiementsByIdFactures(d.DB, d.camp.Base.Factures.Ids()...) 171 if err != nil { 172 return err 173 } 174 175 docs, err := d.LoadDocuments(d.camp.Base.Personnes.Ids()) 176 if err != nil { 177 return err 178 } 179 180 if err = d.loadMessages(); err != nil { 181 return err 182 } 183 184 // répartitions des groupes 185 groupeParticipants, err := rd.SelectGroupeParticipantsByIdCamps(d.DB, d.camp.Id) 186 if err != nil { 187 return err 188 } 189 190 // ajout des animateurs de référence 191 participantEquipiers, err := rd.SelectParticipantEquipiersByIdParticipants(d.DB, d.camp.Base.Participants.Ids()...) 192 if err != nil { 193 return err 194 } 195 196 d.camp.Base.Contraintes = docs.Contraintes 197 d.camp.Base.Paiements = paiements 198 d.camp.Base.Aides = aides 199 d.camp.Base.Structureaides = structures 200 d.camp.Base.Documents = docs.Documents 201 d.camp.Base.DocumentPersonnes = docs.Liens 202 d.camp.Base.ParticipantGroupe = groupeParticipants.ByIdParticipant() 203 d.camp.Base.ParticipantEquipier = participantEquipiers.ByIdParticipant() 204 return nil 205 } 206 207 func (d DriverCampComplet) loadMessages() error { 208 messages, err := rd.SelectMessagesByIdFactures(d.DB, d.camp.Base.Factures.Ids()...) 209 if err != nil { 210 return err 211 } 212 213 mAttestations, err := rd.SelectMessageAttestationsByIdMessages(d.DB, messages.Ids()...) 214 if err != nil { 215 return err 216 } 217 mDocuments, err := rd.SelectMessageDocumentsByIdMessages(d.DB, messages.Ids()...) 218 if err != nil { 219 return err 220 } 221 mMessages, err := rd.SelectMessageMessagesByIdMessages(d.DB, messages.Ids()...) 222 if err != nil { 223 return err 224 } 225 mSondages, err := rd.SelectMessageSondagesByIdMessages(d.DB, messages.Ids()...) 226 if err != nil { 227 return err 228 } 229 mPlaceliberes, err := rd.SelectMessagePlaceliberesByIdMessages(d.DB, messages.Ids()...) 230 if err != nil { 231 return err 232 } 233 234 d.camp.Base.Messages = messages 235 d.camp.Base.MessageAttestations = mAttestations.ByIdMessage() 236 d.camp.Base.MessageDocuments = mDocuments.ByIdMessage() 237 d.camp.Base.MessageMessages = mMessages.ByIdMessage() 238 d.camp.Base.MessageSondages = mSondages.ByIdMessage() 239 d.camp.Base.MessagePlaceliberes = mPlaceliberes.ByIdMessage() 240 return nil 241 } 242 243 // lien vers l'espace perso, avec un tag pour éviter de marquer la connexion 244 // a maintenir synchronisé avec le frontend espace_perso 245 func getLienFicheSanitaire(host string, fac rd.Facture, nomPrenom string) string { 246 pathFs := path.Join(fac.UrlEspacePerso("espace_perso"), "fiches_sanitaires") 247 return shared.BuildUrl(host, pathFs, map[string]string{ 248 "directeur": "ok", 249 "nom_prenom": nomPrenom, 250 }) 251 } 252 253 func (d DriverCampComplet) getInscrits(host string) ([]Inscrit, error) { 254 insc := append(d.camp.GetInscrits(nil), d.camp.GetAttente(nil)...) 255 out := make([]Inscrit, len(insc)) 256 cache := d.camp.Base.ResoudDocumentsPersonnes() 257 cache2, cache3 := d.camp.Base.ResoudMessages(), d.camp.Base.ResoudPaiements() 258 _, cache4 := d.camp.Base.ResoudParticipants() 259 260 for index, part := range insc { 261 var ( 262 respo Responsable 263 lienFicheSanitaire string 264 ) 265 rawPart, pers := part.RawData(), part.GetPersonne() 266 267 if fac, has := part.GetFacture(); has { 268 persResp := fac.GetPersonne().AsItem(0) 269 respo.Valide = true 270 respo.Id = fac.Id 271 respo.Nom = persResp.Fields.Data(dm.PersonneNom).String() 272 respo.Prenom = persResp.Fields.Data(dm.PersonnePrenom).String() 273 respo.Mail = persResp.Fields.Data(dm.PersonneMail).String() 274 respo.MailsCopies = fac.RawData().CopiesMails 275 respo.Tels = persResp.Fields.Data(dm.PersonneTels).String() 276 respo.Adresse = persResp.Fields.Data(dm.PersonneAdresse).String() 277 respo.CodePostal = persResp.Fields.Data(dm.PersonneCodePostal).String() 278 respo.Ville = persResp.Fields.Data(dm.PersonneVille).String() 279 respo.PaiementComplet = fac.EtatFinancier(dm.CacheEtatFinancier{}, false).StatutPaiement() 280 respo.InscriptionValide = fac.RawData().IsValidated 281 lienFicheSanitaire = getLienFicheSanitaire(host, fac.RawData(), pers.RawData().NomPrenom().String()) 282 283 for _, message := range fac.GetEtat(cache2, cache3).PseudoMessages(dm.POVCentre) { 284 str, _ := message.Contenu.(dm.ContenuPerso) // simple 285 respo.Messages = append(respo.Messages, ResumeMessage{ 286 Kind: message.Kind, 287 Label: message.Label, 288 Contenu: string(str), // souvent zero 289 Created: message.Created.Time(), 290 }) 291 } 292 } 293 294 var vaccins []documents.PublicDocument 295 for _, doc := range pers.GetDocuments(cache) { 296 lien := doc.GetContrainte() 297 if lien.Builtin == rd.CVaccin { 298 publicDoc, err := documents.PublieDocument(d.Signing, host, doc.RawData()) 299 if err != nil { 300 return nil, err 301 } 302 vaccins = append(vaccins, publicDoc) 303 } 304 } 305 partFields := part.AsItem(cache4).Fields 306 groupe, hasGroupe := part.GetGroupe() 307 var idGroupe rd.OptionnalId 308 if hasGroupe { 309 idGroupe = rd.NewOptionnalId(groupe.Id) 310 } 311 out[index] = Inscrit{ 312 Id: part.Id, 313 InscritWritable: InscritWritable{ 314 Options: rawPart.Options, 315 Mail: pers.RawData().Mail.String(), 316 IdGroupe: idGroupe, 317 }, 318 IsAttente: !rawPart.ListeAttente.IsInscrit(), 319 AgeDebutCamp: part.AgeDebutCamp().Age(), 320 HasAnniversaire: part.HasAnniversaire(), 321 Nom: partFields.Data(dm.PersonneNom).String(), 322 Prenom: partFields.Data(dm.PersonnePrenom).String(), 323 Sexe: pers.RawData().Sexe, 324 DateNaissance: pers.RawData().DateNaissance, 325 FicheSanitaire: pers.RawData().FicheSanitaire, 326 Vaccins: vaccins, 327 Responsable: respo, 328 IsFicheSanitaireUpToDate: part.IsFicheSanitaireUpToDate(), 329 LienFicheSanitaire: lienFicheSanitaire, 330 } 331 } 332 sort.Slice(out, func(i, j int) bool { 333 return out[i].Nom+out[i].Prenom < out[j].Nom+out[j].Prenom 334 }) 335 return out, nil 336 } 337 338 // -------------------- Fiches sanitaires ---------------------------------- 339 340 // renvoie les données nécessaires 341 func fetchFicheSanitaire(part dm.AccesParticipant) (personne, responsable rd.Personne, nom string, err error) { 342 personne = part.GetPersonne().RawData() 343 fac, has := part.GetFacture() 344 if !has { 345 err = errors.New(`Le participant n'est pas suivi par un dossier. 346 La fiche sanitaire ne peut pas être modifiée dans ce cas, cette situation ne devrait pas donc se produire. 347 Pouvez-vous le signaler (au centre d'inscriptions ou par mail) ?`) 348 return 349 } 350 responsable = fac.GetPersonne().RawData() 351 nom = reduitNom(personne.NomPrenom().String()) 352 return 353 } 354 355 func (d DriverCampComplet) downloadFicheSanitaire(idParticipant int64) (*bytes.Buffer, string, error) { 356 pers, resp, s, err := fetchFicheSanitaire(d.camp.Base.NewParticipant(idParticipant)) 357 s = fmt.Sprintf("fiche_sanitaire_%s.pdf", s) 358 out := new(bytes.Buffer) 359 if err == nil { 360 err = pdf.FicheSanitaire(pers, resp, out) 361 } 362 return out, s, err 363 } 364 365 func filterPartsFicheSanitaire(camp dm.AccesCamp, onlyMineurs, triGroupe bool) []dm.AccesParticipant { 366 insc, att := camp.GetInscrits(nil), camp.GetAttente(nil) 367 dm.TriParticipants(insc, triGroupe) 368 dm.TriParticipants(att, triGroupe) 369 var final []dm.AccesParticipant 370 for _, part := range append(insc, att...) { 371 if onlyMineurs && part.AgeDebutCamp().Age() >= 18 { 372 continue 373 } 374 if part.GetPersonne().RawData().FicheSanitaire.IsNone() { 375 continue 376 } 377 final = append(final, part) 378 } 379 return final 380 } 381 382 // downloadFichesSanitaires regroupe les fiches sanitaires et les vaccins 383 // (contrairement à `downloadFicheSanitaire`) 384 func (d DriverCampComplet) downloadFichesSanitaires(onlyMineurs bool) (*bytes.Buffer, error) { 385 insc := filterPartsFicheSanitaire(d.camp, onlyMineurs, false) 386 387 archive := cd.NewArchiveZip() 388 389 for _, part := range insc { 390 pers, resp, proprio, err := fetchFicheSanitaire(part) 391 if err != nil { 392 return nil, err 393 } 394 content := new(bytes.Buffer) 395 err = pdf.FicheSanitaire(pers, resp, content) 396 if err != nil { 397 return nil, err 398 } 399 filename := fmt.Sprintf("%s_fiche_sanitaire.pdf", proprio) 400 archive.AddFile(filename, content) 401 var idsVaccins []int64 402 acPers := part.GetPersonne() 403 for _, doc := range acPers.GetDocuments(nil) { 404 if doc.GetContrainte().Builtin == rd.CVaccin { 405 idsVaccins = append(idsVaccins, doc.Id) 406 } 407 } 408 archive.Renamer = func(s string) string { 409 return fmt.Sprintf("%s_vaccin_%s", proprio, s) 410 } 411 if err = documents.LoadDocsAndAdd(d.DB, idsVaccins, archive, nil); err != nil { 412 return nil, err 413 } 414 } 415 return archive.Close() 416 } 417 418 func (d DriverCampComplet) downloadFichesSanitairesOneDocument(onlyMineurs, triGroupe bool) (*bytes.Buffer, error) { 419 insc := filterPartsFicheSanitaire(d.camp, onlyMineurs, triGroupe) 420 var personnes, responsables []rd.Personne 421 for _, part := range insc { 422 pers, resp, _, err := fetchFicheSanitaire(part) 423 if err != nil { 424 return nil, err 425 } 426 personnes, responsables = append(personnes, pers), append(responsables, resp) 427 } 428 buf := new(bytes.Buffer) 429 err := pdf.FicheSanitaires(personnes, responsables, buf) 430 return buf, err 431 } 432 433 func (d DriverCampComplet) modifieInscrit(modif InscritWritable, id int64) error { 434 if modif.IdGroupe.Valid && d.camp.Base.Groupes[modif.IdGroupe.Int64].IdCamp != d.camp.Id { 435 return fmt.Errorf("Le groupe (%d) n'est pas lié au séjour", modif.IdGroupe.Int64) 436 } 437 438 tx, err := d.DB.Begin() 439 if err != nil { 440 return err 441 } 442 participant, err := rd.SelectParticipant(tx, id) 443 if err != nil { 444 return shared.Rollback(tx, err) 445 } 446 personne, err := rd.SelectPersonne(tx, participant.IdPersonne) 447 if err != nil { 448 return shared.Rollback(tx, err) 449 } 450 modif.ToPersonneParticipant(&personne.BasePersonne, &participant) 451 participant, err = participant.Update(tx) 452 if err != nil { 453 return shared.Rollback(tx, err) 454 } 455 personne, err = personne.Update(tx) 456 if err != nil { 457 return shared.Rollback(tx, err) 458 } 459 // mise à jour locale 460 d.camp.Base.Participants[participant.Id] = participant 461 d.camp.Base.Personnes[personne.Id] = personne 462 463 // modification du groupe 464 465 gp, hasGroupe, err := rd.SelectGroupeParticipantByIdParticipant(tx, participant.Id) 466 if err != nil { 467 return shared.Rollback(tx, err) 468 } 469 // dans le cas ou on ne change pas de groupe, on ne supprime pas l'animateur de référence éventuel 470 noChange := hasGroupe && modif.IdGroupe.Valid && (gp.IdGroupe == modif.IdGroupe.Int64) 471 472 if !noChange { // on supprime le groupe et l'animateur de référence 473 _, err = rd.DeleteParticipantEquipiersByIdParticipants(tx, participant.Id) 474 if err != nil { 475 return shared.Rollback(tx, err) 476 } 477 _, err = rd.DeleteGroupeParticipantsByIdParticipants(tx, participant.Id) 478 if err != nil { 479 return shared.Rollback(tx, err) 480 } 481 delete(d.camp.Base.ParticipantGroupe, participant.Id) 482 } 483 484 if idGroupe := modif.IdGroupe; !noChange && idGroupe.Valid { // on ajoute un lien 485 // le mode manuel est activé si la modification est différente du groupe actuel 486 // ou que le mode est déjà manuel 487 manuel := gp.Manuel || gp.IdGroupe != idGroupe.Int64 488 gp = rd.GroupeParticipant{IdCamp: participant.IdCamp, IdParticipant: participant.Id, IdGroupe: idGroupe.Int64, Manuel: manuel} 489 err = rd.InsertManyGroupeParticipants(tx, gp) 490 if err != nil { 491 return shared.Rollback(tx, err) 492 } 493 d.camp.Base.ParticipantGroupe[participant.Id] = gp // mise à jour locale 494 } 495 496 err = tx.Commit() 497 return err 498 } 499 500 func (d DriverCampComplet) exportListeInscrits(options optionsExportExcel) (*bytes.Buffer, error) { 501 // les animateurs nécéssitent les équipiers 502 personnes, equipiers, err := d.scanDataEquipiers() 503 if err != nil { 504 return nil, err 505 } 506 // fusion avec existant 507 for _, personne := range personnes { 508 d.camp.Base.Personnes[personne.Id] = personne 509 } 510 d.camp.Base.Equipiers = equipiers 511 512 inscrits, attente := d.camp.GetListes(options.triGroupe, options.bus, true, false) 513 if !options.withAttente { 514 attente = nil 515 } 516 hideColumns := func(header []rd.Header, ignoreFields ...rd.Field) []rd.Header { 517 var out []rd.Header 518 ignore := map[rd.Field]bool{} 519 for _, field := range ignoreFields { 520 ignore[field] = true 521 } 522 for _, h := range header { 523 if ignore[h.Field] { 524 continue 525 } 526 out = append(out, h) 527 } 528 return out 529 } 530 531 // choisit un mode simplifié 532 headerInscrit := HeaderExportInscrits 533 if options.simple { 534 headerInscrit = HeaderExportInscritsSimple 535 } 536 headerResponsable := HeaderExportResponsables 537 if options.simple { 538 headerResponsable = HeaderExportResponsablesSimple 539 } 540 541 // enlève les champs inutile 542 var ignoreFields []rd.Field 543 if showBus := d.camp.RawData().Options.Bus.Actif; !showBus { 544 ignoreFields = append(ignoreFields, dm.ParticipantBus) 545 } 546 if showSki := d.camp.RawData().Options.MaterielSki.Actif; !showSki { 547 ignoreFields = append(ignoreFields, dm.ParticipantMaterielSki, dm.ParticipantMaterielSkiType) 548 } 549 if showGroupes := len(d.camp.GetGroupes()) > 0; !showGroupes { 550 ignoreFields = append(ignoreFields, dm.ParticipantGroupe, dm.ParticipantAnimateur) 551 } 552 if hasOption := d.camp.RawData().OptionPrix.Active != ""; !hasOption { 553 ignoreFields = append(ignoreFields, dm.ParticipantOptionPrix) 554 } 555 headerInscrit = hideColumns(headerInscrit, ignoreFields...) 556 557 return table.GenereListeParticipants(headerInscrit, headerResponsable, 558 inscrits, attente, options.showColors) 559 } 560 561 func (d DriverCampComplet) exportListeFinances() (*bytes.Buffer, error) { 562 parts, totalDemande, totalAides := d.camp.Base.SuiviFinancier(d.camp) 563 return table.GenereSuiviFinancierCamp(cd.HeadersSuiviParticipants, parts, totalDemande, totalAides) 564 }