github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/client/controllers/cont_personnes_export.go (about) 1 package controllers 2 3 import ( 4 "errors" 5 "fmt" 6 "path/filepath" 7 "regexp" 8 "sort" 9 "strings" 10 11 dm "github.com/benoitkugler/goACVE/server/core/datamodel" 12 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 13 "github.com/benoitkugler/goACVE/server/core/utils/table" 14 ) 15 16 const ( 17 ExportDEFAULT ModeExport = iota 18 ExportPUBLIPOSTAGE 19 ExportMAILCHIMP 20 ExportMAILS 21 ExportPORTABLES 22 ExportCOTISATIONS 23 ) 24 const ( 25 RoleEquipe = "_E" 26 RoleInscrit = "_I" 27 RoleAttente = "_A" 28 29 LienResposEtParticipants = "part_resp" // participants + respos 30 LienRespos = "resp" // respos uniquement 31 ) 32 33 const ( 34 publiNbExemplaires = dm.LastField + 1 + iota // pour éviter le clash avec les champs Personnes 35 publiAdresse 36 ) 37 38 var ( 39 sizeAdresse = 38 40 reLinesBreak = regexp.MustCompile("[\n\t\r\f\v]") 41 reMailValide = regexp.MustCompile(`\s*\S+@\S+\.\S+\s*`) 42 reTelPortable = regexp.MustCompile(`^(\+33[67](\d{8})|33[67](\d{8})|0[67](\d{8}))\s*$`) 43 44 HeadersPUBLIPOSTAGE = []rd.Header{ 45 {Label: "Num Client"}, 46 {Field: dm.PersonneSexe, Label: "Civilité"}, 47 {Field: dm.PersonneNom, Label: "nom"}, 48 {Field: dm.PersonnePrenom, Label: "Prénom"}, 49 {Label: "Société"}, 50 {Field: publiAdresse, Label: "Adresse 1"}, 51 {Field: publiAdresse + 1, Label: "Adresse 2"}, 52 {Field: publiAdresse + 2, Label: "Adresse 3"}, 53 {Field: dm.PersonneCodePostal, Label: "Code postal"}, 54 {Field: dm.PersonneVille, Label: "Ville"}, 55 {Field: publiNbExemplaires, Label: "Nombre d'exemplaires"}, 56 {Field: dm.PersonnePays, Label: "Pays"}, 57 {Label: "Message 1"}, 58 {Label: "Message 2"}, 59 } 60 HeadersMAILCHIMP = rd.HeadersMails 61 HeadersMAILS = []rd.Header{ 62 {}, 63 } 64 HeadersPORTABLES = []rd.Header{ 65 {}, 66 } 67 HeadersCOTISATIONS = []rd.Header{ 68 {Field: 0, Label: "Personne"}, 69 {Field: 1, Label: "Membre ?"}, 70 } 71 ) 72 73 func init() { 74 for i, an := range rd.AnneesCotisations { 75 HeadersCOTISATIONS = append(HeadersCOTISATIONS, rd.Header{ 76 Field: rd.Field(2 + i), 77 Label: fmt.Sprintf("Cotisation %d", an), 78 }) 79 } 80 } 81 82 type ModeExport int 83 84 // Description renvoie une explication détaillée. 85 func (m ModeExport) Description() string { 86 switch m { 87 case ExportDEFAULT: 88 return "Les personnes et organismes sont séparées, sans traitement particulier." 89 case ExportPUBLIPOSTAGE, ExportMAILCHIMP, ExportMAILS, ExportPORTABLES: 90 return "Les contacts des organismes sont recoupés avec les personnes." 91 case ExportCOTISATIONS: 92 return "Les organismes sont ignorés." 93 default: 94 return "" 95 } 96 } 97 98 type CritereRecherche struct { 99 Personne CriteresRecherchePersonne 100 Participant CriteresRechercheParticipant 101 } 102 103 // CriteresRecherchePersonne représente une recherche détaillée 104 // Un critère zero est ignoré 105 type CriteresRecherchePersonne struct { 106 Age *[2]int // min max 107 Departement rd.Departement 108 MembreAsso rd.RangMembreAsso 109 EchoRocher rd.OptionnalBool 110 Eonews rd.OptionnalBool 111 PubEte rd.OptionnalBool 112 PubHiver rd.OptionnalBool 113 VersionPapier rd.OptionnalBool 114 HasDocument rd.BuiltinContrainte 115 } 116 117 type CriteresRechercheParticipant struct { 118 Annee map[int]bool 119 Periode string 120 Role string // rd.Role or special value 121 LiensResponsable string // valide pour un inscrit/attente 122 } 123 124 type CriteresRechercheDonateur struct { 125 AnneeDon int 126 IsRemercie rd.OptionnalBool 127 } 128 129 // RecherchePersonnesByCriteres cherche d'abord dans les participants, puis dans les respos correspondants. 130 // Si demandé, les organismes sont aussi ajoutés. 131 func (c *Personnes) RecherchePersonnesByCriteres(criteres CritereRecherche) rd.Table { 132 var finalRes rd.Table 133 134 camps, personnes := c.Base.Camps, c.Base.Personnes 135 cpa, cpe := criteres.Participant, criteres.Personne 136 idsPersonnes := rd.NewSet() // besoin unicité des personnes 137 138 critereCamp := func(idCamp int64) bool { 139 match := true 140 if len(cpa.Annee) > 0 { 141 an := int(camps[idCamp].Annee()) 142 match = match && cpa.Annee[an] 143 } 144 if cpa.Periode != "" { 145 match = match && cpa.Periode == camps[idCamp].Periode().String() 146 } 147 return match 148 } 149 150 // les personnes temporaires sont filtrées plus bas 151 switch { 152 case cpa.Role == RoleInscrit || cpa.Role == RoleAttente: 153 // on cherche parmi les participants, on ignore les participants simples 154 for _, part := range c.Base.Participants { 155 isInscrit := part.ListeAttente.IsInscrit() 156 matchRole := (cpa.Role == RoleInscrit) == isInscrit 157 if matchRole && critereCamp(c.Base.NewParticipant(part.Id).GetCamp().Id) { 158 if cpa.LiensResponsable != "" && part.IdFacture.IsNotNil() { 159 // LiensResponsable demande forcément le reponsable 160 idsPersonnes.Add(part.IdFacture.Int64) 161 } 162 if cpa.LiensResponsable != LienRespos { 163 // le seul cas ou on ne met pas le participant 164 idsPersonnes.Add(part.IdPersonne) 165 } 166 } 167 } 168 case cpa.Role != "": 169 // on cherche parmi les équipiers 170 for _, part := range c.Base.Equipiers { 171 matchRole := cpa.Role == RoleEquipe || part.Roles.Is(rd.Role(cpa.Role)) 172 if matchRole && critereCamp(part.IdCamp) { 173 idsPersonnes.Add(part.IdPersonne) 174 } 175 } 176 default: 177 // toutes les personnes, pas seulement les participants 178 idsPersonnes = c.Base.Personnes.Ids().AsSet() 179 } 180 181 reDep, err := regexp.Compile("^" + string(cpe.Departement)) 182 cache := c.Base.ResoudDocumentsPersonnes() 183 for idPers := range idsPersonnes { // on applique maintent le critère sur la personne 184 pers, match := personnes[idPers], true 185 if pers.IsTemporaire { // on ignore les profils non référencés 186 continue 187 } 188 accesPers := c.Base.NewPersonne(pers.Id) 189 if cpe.Age != nil { 190 age := pers.Age() 191 match = cpe.Age[0] <= age && age <= cpe.Age[1] 192 } 193 if err == nil && cpe.Departement != "" { 194 cp := strings.ReplaceAll(string(pers.CodePostal), " ", "") 195 match = match && reDep.MatchString(cp) 196 } 197 if cpe.MembreAsso != rd.RMANonMembre { 198 match = match && pers.RangMembreAsso.AtLeast(cpe.MembreAsso) 199 } 200 if cpe.EchoRocher != 0 { 201 match = match && cpe.EchoRocher.Bool() == bool(pers.EchoRocher) 202 } 203 if cpe.PubEte != 0 { 204 match = match && cpe.PubEte.Bool() == bool(pers.PubEte) 205 } 206 if cpe.PubHiver != 0 { 207 match = match && cpe.PubHiver.Bool() == bool(pers.PubHiver) 208 } 209 if cpe.Eonews != 0 { 210 match = match && cpe.Eonews.Bool() == bool(pers.Eonews) 211 } 212 if cpe.VersionPapier != 0 { 213 match = match && cpe.VersionPapier.Bool() == bool(pers.VersionPapier) 214 } 215 if cpe.HasDocument != "" { 216 match = match && accesPers.HasDocument(cache, cpe.HasDocument) 217 } 218 if match { 219 finalRes = append(finalRes, accesPers.AsItem(0)) 220 } 221 } 222 223 return finalRes 224 } 225 226 func (c *Personnes) RechercheDonateursByCriteres(cd CriteresRechercheDonateur) []dm.AccesPersonne { 227 idsPersonnes := rd.NewSet() 228 for _, don := range c.Base.Dons { 229 match := true 230 if cd.AnneeDon != 0 { 231 match = cd.AnneeDon == don.DateReception.Time().Year() 232 } 233 if cd.IsRemercie != rd.OBPeutEtre { 234 match = match && bool(don.Remercie) == cd.IsRemercie.Bool() 235 } 236 donateur := c.Base.DonDonateurs[don.Id] 237 if match && donateur.IdPersonne.IsNotNil() { 238 idsPersonnes[donateur.IdPersonne.Int64] = true 239 } 240 } 241 res := make([]dm.AccesPersonne, 0, len(idsPersonnes)) 242 for id := range idsPersonnes { 243 res = append(res, c.Base.NewPersonne(id)) 244 } 245 return res 246 } 247 248 // EffaceListe remet à zéro l'état et enlève toutes les personnes 249 // de la liste courante. 250 func (c *Personnes) EffaceListe() { 251 c.Etat = EtatPersonnes{} 252 c.Liste = rd.Table{} 253 c.ResetRender() 254 } 255 256 // AjouteToListe remet à zéro l'état et ajoute les personnes données 257 // à la liste courante. 258 func (c *Personnes) AjouteToListe(personnes rd.Table) { 259 c.Etat = EtatPersonnes{} 260 ids := map[rd.IId]rd.Item{} // unicité 261 for _, item := range append(c.Liste, personnes...) { 262 ids[item.Id] = item 263 } 264 c.Liste = make(rd.Table, 0, len(ids)) 265 for _, item := range ids { 266 c.Liste = append(c.Liste, item) 267 } 268 c.ResetRender() 269 } 270 271 func (c *Personnes) AjouteOrganismesToListe() { 272 t := make(rd.Table, 0, len(c.Base.Organismes)) 273 for id := range c.Base.Organismes { 274 t = append(t, c.Base.NewOrganisme(id).AsItem()) 275 } 276 c.AjouteToListe(t) 277 } 278 279 type itemExemplaires struct { 280 item rd.Item // une personne ou un organisme avec un contact propre 281 exs rd.Exemplaires // au moins 1 282 } 283 284 // identifie les personnes et les contacts d'organismes 285 func resoudContactOrganismes(liste rd.Table, base *dm.BaseLocale) []itemExemplaires { 286 pers := make(map[rd.IdPersonne]itemExemplaires) 287 var out []itemExemplaires 288 for _, item := range liste { 289 switch itemId := item.Id.(type) { 290 case rd.IdPersonne: 291 itemEx := pers[itemId] 292 // on ajoute la personne propre aux exemplaires 293 itemEx.item = item 294 itemEx.exs = rd.Exemplaires{ 295 EchoRocher: itemEx.exs.EchoRocher + 1, 296 PubEte: itemEx.exs.PubEte + 1, 297 PubHiver: itemEx.exs.PubHiver + 1, 298 } 299 pers[itemId] = itemEx 300 case rd.IdOrganisme: 301 // on différencie suivant ContactPropre 302 org := base.NewOrganisme(itemId.Int64()).RawData() 303 if org.ContactPropre { // ligne propre 304 out = append(out, itemExemplaires{item: item, exs: org.Exemplaires}) 305 } else { // on aggrège au contact 306 idPers := rd.IdPersonne(org.IdContact.Int64) 307 contact := pers[idPers] 308 // les contacts n'étant pas aussi présent comme personne 309 // doivent être ajouté ici 310 contact.item = base.NewPersonne(idPers.Int64()).AsItem(0) 311 contact.exs = rd.Exemplaires{ 312 EchoRocher: contact.exs.EchoRocher + org.Exemplaires.EchoRocher, 313 PubEte: contact.exs.PubEte + org.Exemplaires.PubEte, 314 PubHiver: contact.exs.PubHiver + org.Exemplaires.PubHiver, 315 } 316 pers[idPers] = contact 317 } 318 } 319 } 320 321 // on ajoute les personnes "augmentées" 322 for _, itemEx := range pers { 323 out = append(out, itemEx) 324 } 325 return out 326 } 327 328 // ValideExport vérifie et nettoie la liste actuelle et la renvoie. 329 // Les organismes sont résolus, en utilisant leur contact le cas échéant. 330 // Renvoie aussi le nombre de lignes invalides. 331 func (c *Personnes) ValideExport(mode ModeExport, option dm.OptionExport) (out rd.Table, invalides int) { 332 switch mode { 333 case ExportDEFAULT: 334 out = c.Liste 335 case ExportCOTISATIONS: 336 var liste []dm.AccesPersonne 337 for _, ac := range c.Liste { 338 if id, ok := ac.Id.(rd.IdPersonne); ok { 339 liste = append(liste, c.Base.NewPersonne(id.Int64())) 340 } 341 } 342 if option == dm.MembresTriAlphabetique { 343 sort.Slice(liste, func(i, j int) bool { 344 return liste[i].RawData().Nom < liste[j].RawData().Nom 345 }) 346 } else if option == dm.MembresTriCotisation { 347 sort.Slice(liste, func(i, j int) bool { 348 return liste[i].RawData().Cotisation.Sortable() < liste[j].RawData().Cotisation.Sortable() 349 }) 350 } 351 out = make(rd.Table, len(liste)) 352 for i, pers := range liste { 353 out[i] = itemMembreCotisations(pers) 354 } 355 default: 356 // on commence par recoupper les pers et les orgs. 357 items := resoudContactOrganismes(c.Liste, c.Base) 358 switch mode { 359 case ExportPUBLIPOSTAGE: 360 out = formatPublipostage(items, option) 361 case ExportMAILCHIMP: 362 uniq := make(map[string]rd.Item) 363 for _, pers := range items { 364 mail := pers.item.Fields.Data(dm.PersonneMail).String() 365 if reMailValide.MatchString(mail) { 366 uniq[mail] = pers.item 367 } else { 368 invalides += 1 369 } 370 } 371 for _, mc := range uniq { 372 out = append(out, mc) 373 } 374 case ExportMAILS: 375 uniq := rd.StringSet{} 376 for _, pers := range items { 377 mail := pers.item.Fields.Data(dm.PersonneMail).String() 378 if reMailValide.MatchString(mail) { 379 uniq[mail] = true 380 } else { 381 invalides += 1 382 } 383 } 384 for mail := range uniq { 385 out = append(out, rd.Item{Fields: rd.F{0: rd.String(mail)}}) 386 } 387 case ExportPORTABLES: 388 uniq := rd.StringSet{} 389 for _, pers := range items { 390 tels, _ := pers.item.Fields.Data(dm.PersonneTels).(rd.Tels) 391 for _, tel := range tels { 392 tel = rd.CondenseTel(tel) 393 if reTelPortable.MatchString(tel) { 394 uniq[tel] = true 395 } else { 396 invalides += 1 397 } 398 } 399 } 400 for tel := range uniq { 401 out = append(out, rd.Item{Fields: rd.F{0: rd.String(rd.FormatTel(tel))}}) 402 } 403 } 404 } 405 return out, invalides 406 } 407 408 func splitString(s string, maxSize int) []string { 409 s = reLinesBreak.ReplaceAllString(s, " ") 410 mots := strings.Split(s, " ") 411 var ( 412 currentRow string 413 res []string 414 ) 415 for _, mot := range mots { 416 if mot == "" { 417 continue 418 } 419 nextSize := len(currentRow) + len(mot) + 1 420 if nextSize <= maxSize { 421 currentRow += mot + " " 422 } else { 423 res = append(res, currentRow) 424 currentRow = mot 425 } 426 } 427 res = append(res, currentRow) 428 return res 429 } 430 431 func formatPublipostage(list []itemExemplaires, typeCom dm.OptionExport) rd.Table { 432 out := make(rd.Table, len(list)) 433 for index, itemEx := range list { 434 fields := itemEx.item.Fields 435 // on ajoute les champs spéciaux 436 switch typeCom { 437 case dm.PubEte: 438 fields[publiNbExemplaires] = rd.Int(itemEx.exs.PubEte) 439 case dm.PubHiver: 440 fields[publiNbExemplaires] = rd.Int(itemEx.exs.PubHiver) 441 case dm.EchoRocher: 442 fields[publiNbExemplaires] = rd.Int(itemEx.exs.EchoRocher) 443 } 444 445 ads := splitString(fields.Data(dm.PersonneAdresse).String(), sizeAdresse) 446 for nbAd := 0; nbAd < 3; nbAd++ { 447 var ad string 448 if nbAd < len(ads) { 449 ad = ads[nbAd] 450 } 451 fields[publiAdresse+rd.Field(nbAd)] = rd.String(ad) 452 } 453 454 out[index] = rd.Item{Fields: fields} 455 } 456 return out 457 } 458 459 // affiche le détails des cotisations 460 func itemMembreCotisations(personne dm.AccesPersonne) rd.Item { 461 fields := rd.F{ 462 0: personne.RawData().NomPrenom(), 463 1: personne.RawData().RangMembreAsso, 464 } 465 cots := personne.RawData().Cotisation.Map() 466 for index, annee := range rd.AnneesCotisations { 467 has := cots[annee] 468 var s rd.String = "-" 469 if has { 470 s = "Ok" 471 } 472 fields[rd.Field(index+2)] = s 473 } 474 bolds := rd.B{0: true} 475 return rd.Item{Fields: fields, Bolds: bolds} 476 } 477 478 // SaveExport enregistre l'export et renvoie le chemin. 479 func (c *Personnes) ExportAndSave(mode ModeExport, option dm.OptionExport) string { 480 liste, _ := c.ValideExport(mode, option) 481 var ( 482 path string 483 err error 484 ) 485 switch mode { 486 case ExportDEFAULT: 487 path, err = table.ExportExcel(c.Header[0:9], liste, LocalFolder) 488 case ExportPUBLIPOSTAGE: 489 path, err = table.ExportExcel(HeadersPUBLIPOSTAGE, liste, LocalFolder) 490 case ExportMAILCHIMP: 491 path, err = table.ExportCsv(HeadersMAILCHIMP, liste, LocalFolder) 492 case ExportMAILS: 493 path, err = table.ExportCsv(HeadersMAILS, liste, LocalFolder) 494 case ExportPORTABLES: 495 path, err = table.ExportCsv(HeadersPORTABLES, liste, LocalFolder) 496 case ExportCOTISATIONS: 497 path, err = table.ExportExcel(HeadersCOTISATIONS, liste, LocalFolder) 498 default: 499 err = errors.New("Mode d'export inconnu !") 500 } 501 if err != nil { 502 c.main.ShowError(err) 503 return "" 504 } 505 path, err = filepath.Abs(path) 506 if err != nil { 507 c.main.ShowError(err) 508 return "" 509 } 510 c.main.ShowStandard(fmt.Sprintf("Export généré dans %s", path), false) 511 return path 512 }