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  }