github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/core/datamodel/acces.go (about)

     1  // Ce package ajoute de l'intelligence aux données:
     2  //	- résolutions des liens entre tables
     3  //	- fonctions de recherches
     4  //	- implémentation des interfaces
     5  package datamodel
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"html/template"
    11  	"sort"
    12  	"strings"
    13  
    14  	"github.com/benoitkugler/goACVE/logs"
    15  
    16  	"github.com/benoitkugler/goACVE/server/core/rawdata/composites"
    17  
    18  	rd "github.com/benoitkugler/goACVE/server/core/rawdata"
    19  )
    20  
    21  const (
    22  	CouleurAgeInvalide       = rd.HexColor("#80ea0000")
    23  	CouleurAttenteReponse    = rd.HexColor("#90e0d616")
    24  	CouleurListeAttente      = rd.HexColor("#90FF610F")
    25  	CouleurAnniversaire      = rd.HexColor("#826a1b")
    26  	CouleurPaiementInvalide  = rd.HexColor("#90e0552a")
    27  	CouleurPaiementBordereau = rd.HexColor("#9062872b")
    28  	CouleurResponsable       = rd.HexColor("#4a086d11")
    29  )
    30  
    31  const (
    32  	PubEte OptionExport = iota
    33  	PubHiver
    34  	EchoRocher
    35  )
    36  
    37  const (
    38  	MembresTriAlphabetique OptionExport = iota
    39  	MembresTriCotisation
    40  )
    41  
    42  var (
    43  	CouleurGarcon         = rd.RGBA{R: 19, G: 139, B: 215, A: 50}
    44  	CouleurFille          = rd.RGBA{R: 199, G: 10, B: 126, A: 50}
    45  	CouleurParticipantTmp = rd.RGBA{R: 230, G: 157, B: 51, A: 130}
    46  
    47  	headerFacture = []string{
    48  		"Participant",
    49  		"Camp",
    50  		"Dates",
    51  		"Prix du camp",
    52  		"Aides extérieures",
    53  		"Sous-total",
    54  	}
    55  
    56  	templateRowFacture = template.Must(template.New("rowFacture").Parse(`<tr>
    57  		<td>
    58  			{{ .Personne.NomPrenom }} <br/>
    59  			(né{{ .Personne.Sexe.Accord }} le {{ .Personne.DateNaissance }}
    60  		</td>
    61  		<td>
    62  			{{ .Camp.Label }} <br/>
    63  			N° JS : {{ .Camp.NumeroJS }}
    64  		</td>
    65  		<td>
    66  			du {{ .Camp.DateDebut }} <br />
    67  			au {{ .Camp.DateFin }}
    68  		</td>
    69  		<td>
    70  			{{ .BilanPrix.PrixBase }} {{ .BilanPrix.DescriptionPrix }}
    71  		</td>
    72  		<td>
    73  			{{ range .BilanPrix.Aides }} 
    74  				{{- if .Valide -}}	
    75  					{{ .Description }} <br/>
    76  				{{- else -}}
    77  					(<i>{{ .Description }}</i>) <br/>
    78  				{{- end }}
    79  			{{ end }}
    80  		</td>
    81  		<td>
    82  			{{ .BilanPrix.PrixSansRemises }}
    83  		</td>
    84  	</tr>`))
    85  )
    86  
    87  type dataTemplateFacture struct {
    88  	Personne  rd.Personne
    89  	Camp      rd.Camp
    90  	BilanPrix BilanPrixCamp
    91  }
    92  
    93  // --------------------------------------------------------------------------
    94  // -------------------------------Personne ----------------------------------
    95  // --------------------------------------------------------------------------
    96  
    97  type OptionExport int
    98  
    99  type CacheContacts map[int64][]rd.Organisme // id personnes -> structures contacts
   100  
   101  func (b *BaseLocale) ResoudOrganismes() CacheContacts {
   102  	out := CacheContacts{}
   103  	for _, orga := range b.Organismes {
   104  		if contact := orga.IdContact; contact.IsNotNil() {
   105  			out[contact.Int64] = append(out[contact.Int64], orga)
   106  		}
   107  	}
   108  	return out
   109  }
   110  
   111  func (ac AccesPersonne) GetDocuments(cache LiensPersonneDocuments) []AccesDocumentPersonne {
   112  	if cache == nil {
   113  		cache = ac.Base.ResoudDocumentsPersonnes()
   114  	}
   115  	ids := cache[ac.Id]
   116  	out := make([]AccesDocumentPersonne, len(ids))
   117  	for i, id := range ids {
   118  		out[i] = AccesDocumentPersonne{ac.Base.NewDocument(id)}
   119  	}
   120  	return out
   121  }
   122  
   123  // HasDocument renvoie `true` si la personne possède un document
   124  // dans la catégorie donnée.
   125  func (ac AccesPersonne) HasDocument(cache LiensPersonneDocuments, categorie rd.BuiltinContrainte) bool {
   126  	for _, doc := range ac.GetDocuments(cache) {
   127  		if doc.GetContrainte().Builtin == categorie {
   128  			return true
   129  		}
   130  	}
   131  	return false
   132  }
   133  
   134  // IsSB renvoie `true` si la personne a un document dans la catégorie SB.
   135  func (ac AccesPersonne) IsSB(cache LiensPersonneDocuments) rd.Bool {
   136  	return rd.Bool(ac.HasDocument(cache, rd.CSb))
   137  }
   138  
   139  // --------------------------------------------------------------------------
   140  // ------------------------------- Camp -------------------------------------
   141  // --------------------------------------------------------------------------
   142  
   143  // StatsCamp décrit le nombre de personnes dans différentes catégories.
   144  type StatsCamp struct {
   145  	Inscrits, Attente, Equipe, PlacesRestantes, PlacesReservees, Garcons, Filles int
   146  }
   147  
   148  func (ac AccesCamp) GetGroupes() []rd.Groupe {
   149  	var out []rd.Groupe
   150  	for _, groupe := range ac.Base.Groupes {
   151  		if groupe.IdCamp == ac.Id {
   152  			out = append(out, groupe)
   153  		}
   154  	}
   155  	return out
   156  }
   157  
   158  func (ac AccesCamp) GetDirecteurEquipier() (AccesEquipier, bool) {
   159  	dir, has := ac.getEquipeMap(nil).FindDirecteur()
   160  	return ac.Base.NewEquipier(dir.Id), has
   161  }
   162  
   163  func (ac AccesCamp) GetDirecteur() (AccesPersonne, bool) {
   164  	dir, ok := ac.GetDirecteurEquipier()
   165  	if ok {
   166  		return dir.GetPersonne(), true
   167  	}
   168  	return AccesPersonne{}, false
   169  }
   170  
   171  func (ac AccesCamp) hintsAttente(cache LiensCampParticipants, personne rd.Personne, isCurrentInscrit bool) rd.HintsAttente {
   172  	parts := ac.getParticipants(cache)
   173  	cp := composites.CampParticipants{Camp: ac.RawData(), Participants: parts}
   174  	return cp.HintsAttente(personne.BasePersonne, isCurrentInscrit, ac.Base.Personnes)
   175  }
   176  
   177  // HintsAttente renvoie le statut conseillé pour le "participant" donné par
   178  // `idPersonne`. Pour un participant déjà créé, voir `AccesParticipant.HintsAttente`
   179  func (ac AccesCamp) HintsAttente(idPersonne int64, isCurrrentInscrit bool) rd.HintsAttente {
   180  	personne := ac.Base.Personnes[idPersonne]
   181  	return ac.hintsAttente(nil, personne, isCurrrentInscrit)
   182  }
   183  
   184  // Remplissage renvoi le ratio en % de remplissage du camp
   185  // Si `cache` est nil, parcourt la table des participants.
   186  func (ac AccesCamp) Remplissage(cache1 LiensCampParticipants, cache2 LiensCampParticipantsimples) rd.Pourcent {
   187  	nbInscrits := ac.GetNbInscrits(cache1, cache2)
   188  	nbPlaces := ac.RawData().NbPlaces
   189  	var remp rd.Pourcent
   190  	if nbPlaces > 0 {
   191  		remp = rd.Pourcent(nbInscrits * 100 / int(nbPlaces))
   192  	}
   193  	return remp
   194  }
   195  
   196  // GetNbInscrits renvoie le nombre de participant effectif (sans liste d'attente)
   197  func (ac AccesCamp) GetNbInscrits(cache1 LiensCampParticipants, cache2 LiensCampParticipantsimples) int {
   198  	if ac.RawData().InscriptionSimple {
   199  		return len(ac.GetParticipantsimples(cache2))
   200  	}
   201  	return len(ac.GetInscrits(cache1))
   202  }
   203  
   204  func (ac AccesCamp) getParticipants(cache LiensCampParticipants) (parts []rd.Participant) {
   205  	if cache == nil {
   206  		_, cache = ac.Base.ResoudParticipants()
   207  	}
   208  	ids := cache[ac.Id]
   209  	parts = make([]rd.Participant, len(ids))
   210  	for index, id := range ids {
   211  		parts[index] = ac.Base.Participants[id]
   212  	}
   213  	return parts
   214  }
   215  
   216  // Si `cache` est nil, parcourt la table des participants.
   217  func (ac AccesCamp) GetInscrits(cache LiensCampParticipants) []AccesParticipant {
   218  	var liste []AccesParticipant
   219  	for _, part := range ac.getParticipants(cache) {
   220  		if part.ListeAttente.IsInscrit() {
   221  			liste = append(liste, ac.Base.NewParticipant(part.Id))
   222  		}
   223  	}
   224  	return liste
   225  }
   226  
   227  // Si `cache` est nil, parcourt la table des participants.
   228  func (ac AccesCamp) GetAttente(cache LiensCampParticipants) []AccesParticipant {
   229  	var liste []AccesParticipant
   230  	for _, part := range ac.getParticipants(cache) {
   231  		if !part.ListeAttente.IsInscrit() {
   232  			liste = append(liste, ac.Base.NewParticipant(part.Id))
   233  		}
   234  	}
   235  	return liste
   236  }
   237  
   238  func (ac AccesCamp) GetParticipantsimples(cache LiensCampParticipantsimples) []AccesParticipantsimple {
   239  	if cache == nil {
   240  		cache = ac.Base.ResoudParticipantsimples()
   241  	}
   242  	ids := cache[ac.Id]
   243  	parts := make([]AccesParticipantsimple, len(ids))
   244  	for index, id := range ids {
   245  		parts[index] = ac.Base.NewParticipantsimple(id)
   246  	}
   247  	return parts
   248  }
   249  
   250  func (ac AccesCamp) getEquipeMap(cache LiensCampEquipiers) rd.Equipiers {
   251  	if cache == nil {
   252  		cache = ac.Base.ResoudEquipiers()
   253  	}
   254  	ids := cache[ac.Id]
   255  	out := make(rd.Equipiers, len(ids))
   256  	for _, id := range ids {
   257  		out[id] = ac.Base.Equipiers[id]
   258  	}
   259  	return out
   260  }
   261  
   262  // Si `cache` est nil, parcourt la table des equipier.
   263  func (ac AccesCamp) GetEquipe(cache LiensCampEquipiers) []AccesEquipier {
   264  	var liste []AccesEquipier
   265  	for id := range ac.getEquipeMap(cache) {
   266  		liste = append(liste, ac.Base.NewEquipier(id))
   267  	}
   268  
   269  	sort.Slice(liste, func(i, j int) bool {
   270  		return liste[i].Id < liste[j].Id
   271  	})
   272  	sort.SliceStable(liste, func(i, j int) bool {
   273  		return liste[i].RawData().Roles.Sortable() < liste[j].RawData().Roles.Sortable()
   274  	})
   275  	return liste
   276  }
   277  
   278  // TriParticipants trie `l` sur place par ordre alphabétique, et
   279  // par groupe si demandé.
   280  func TriParticipants(l []AccesParticipant, triGroupe bool) {
   281  	sort.Slice(l, func(i, j int) bool {
   282  		return l[i].GetPersonne().RawData().Prenom.Sortable() < l[j].GetPersonne().RawData().Prenom.Sortable()
   283  	})
   284  	sort.SliceStable(l, func(i, j int) bool {
   285  		return l[i].GetPersonne().RawData().Nom.Sortable() < l[j].GetPersonne().RawData().Nom.Sortable()
   286  	})
   287  	if triGroupe {
   288  		sort.SliceStable(l, func(i, j int) bool {
   289  			gi, _ := l[i].GetGroupe()
   290  			gj, _ := l[j].GetGroupe()
   291  			return gi.Id < gj.Id
   292  		})
   293  	}
   294  }
   295  
   296  // TriParticipantsItems trie `l` sur place par ordre alphabétique, et
   297  // par groupe si demandé.
   298  func TriParticipantsItems(parts rd.Table, triGroupe bool) {
   299  	SortBy(parts, PersonnePrenom, false)
   300  	SortStableBy(parts, PersonneNom, false)
   301  	if triGroupe {
   302  		SortStableBy(parts, ParticipantAnimateur, false)
   303  		SortStableBy(parts, ParticipantGroupe, false)
   304  	}
   305  }
   306  
   307  // GetListes renvoi la liste des inscrits et la liste d'attente.
   308  // Si `critereBus` est non nul, ne renvoi que les participants matchant ce critère.
   309  // Pour un camp simple, ces critères sont ignorés.
   310  func (ac AccesCamp) GetListes(triGroupe bool, critereBus rd.Bus, hideNonValidated, selectPartageOK bool) (inscrits rd.Table, attente rd.Table) {
   311  	if ac.RawData().InscriptionSimple {
   312  		l := ac.GetParticipantsimples(nil)
   313  		inscrits = make(rd.Table, len(l))
   314  		for i, part := range l {
   315  			inscrits[i] = part.AsItem()
   316  		}
   317  	} else {
   318  		_, cache := ac.Base.ResoudParticipants()
   319  		insc, att := ac.GetInscrits(cache), ac.GetAttente(cache)
   320  		inscrits, attente = make(rd.Table, 0, len(insc)), make(rd.Table, 0, len(att))
   321  		for _, part := range insc {
   322  			if hideNonValidated { // ignore les participants en attente de validation
   323  				if f, ok := part.GetFacture(); ok && !f.RawData().IsValidated {
   324  					continue
   325  				}
   326  			}
   327  			if selectPartageOK {
   328  				// si un participant est lié à un dossier qui n'autorise pas
   329  				// le partage, ignore
   330  				if f, ok := part.GetFacture(); ok && !f.RawData().PartageAdressesOK {
   331  					continue
   332  				}
   333  			}
   334  			if part.RawData().Options.Bus.Match(critereBus) {
   335  				inscrits = append(inscrits, part.AsItem(cache))
   336  			}
   337  		}
   338  		for _, part := range att {
   339  			if hideNonValidated { // ignore les participants en attente de validation
   340  				if f, ok := part.GetFacture(); ok && !f.RawData().IsValidated {
   341  					continue
   342  				}
   343  			}
   344  			if part.RawData().Options.Bus.Match(critereBus) {
   345  				attente = append(attente, part.AsItem(cache))
   346  			}
   347  		}
   348  	}
   349  	TriParticipantsItems(inscrits, triGroupe)
   350  	TriParticipantsItems(attente, triGroupe)
   351  	return inscrits, attente
   352  }
   353  
   354  // StatsCamp renvoie un détails du nombre de personnes par catégorie.
   355  func (ac AccesCamp) GetStats(cacheP LiensCampParticipants, cachePs LiensCampParticipantsimples, cacheE LiensCampEquipiers) StatsCamp {
   356  	inscrits, inscritssimples := ac.GetInscrits(cacheP), ac.GetParticipantsimples(cachePs)
   357  	nbInscrits, nbG, nbF := len(inscrits)+len(inscritssimples), 0, 0
   358  	for _, part := range inscrits {
   359  		switch part.GetPersonne().RawData().Sexe {
   360  		case "M":
   361  			nbG += 1
   362  		case "F":
   363  			nbF += 1
   364  		}
   365  	}
   366  	r := ac.RawData()
   367  	return StatsCamp{
   368  		Inscrits:        nbInscrits,
   369  		Attente:         len(ac.GetAttente(cacheP)),
   370  		Equipe:          len(ac.GetEquipe(cacheE)),
   371  		PlacesRestantes: int(r.NbPlaces) - nbInscrits,
   372  		PlacesReservees: int(r.NbPlacesReservees),
   373  		Garcons:         nbG,
   374  		Filles:          nbF,
   375  	}
   376  }
   377  
   378  // TrouveGroupe cherche parmis les groupes possibles celui qui pourrait convenir.
   379  // Normalement, les groupes respectent un invariant de continuité sur les plages,
   380  // imposé par le frontend.
   381  // Si plusieurs peuvent convenir, un seul est renvoyé, de façon arbitraire.
   382  func (ac AccesCamp) TrouveGroupe(dateNaissance rd.Date) (rd.Groupe, bool) {
   383  	cg := composites.CampGroupes{Camp: ac.RawData(), Groupes: ac.GetGroupes()}
   384  	return cg.TrouveGroupe(dateNaissance)
   385  }
   386  
   387  // GetRegisteredDocuments renvoie les documents (enregistrés sur la base)
   388  // liés au camp.
   389  // Si `withLettre` vaut false, seules les pièces jointes additionnelles sont renvoyés.
   390  // Si `withPiecesJointes` vaut false, seules les lettres sont renvoyés.
   391  func (ac AccesCamp) GetRegisteredDocuments(withLettre, withPiecesJointes bool) (out []rd.Document) {
   392  	for _, lien := range ac.Base.DocumentCamps {
   393  		if lien.IdCamp == ac.Id {
   394  			doc := ac.Base.Documents[lien.IdDocument]
   395  			if (withLettre && lien.IsLettre) || (withPiecesJointes && !lien.IsLettre) {
   396  				out = append(out, doc)
   397  			}
   398  		}
   399  	}
   400  	return out
   401  }
   402  
   403  func (ac AccesGroupe) GetCamp() AccesCamp {
   404  	return ac.Base.NewCamp(ac.RawData().IdCamp)
   405  }
   406  
   407  func (ac AccesGroupe) AsGroupeCamp() composites.GroupeCamp {
   408  	return composites.GroupeCamp{
   409  		Groupe: ac.RawData(),
   410  		Camp:   ac.GetCamp().RawData(),
   411  	}
   412  }
   413  
   414  // Renvoie les ids des contraintes imposées au groupe.
   415  func (ac AccesGroupe) GetContraintes() rd.GroupeContraintes {
   416  	return ac.Base.GroupeContraintes[ac.Id]
   417  }
   418  
   419  // Label inclut le nom du camp
   420  func (ac AccesGroupe) Label() rd.String {
   421  	camp := ac.GetCamp()
   422  	out := camp.RawData().Label()
   423  	if len(camp.GetGroupes()) >= 1 {
   424  		out += " - " + ac.RawData().Nom
   425  	}
   426  	return out
   427  }
   428  
   429  // --------------------------------------------------------------------------
   430  // ------------------------------- Participant ------------------------------
   431  // --------------------------------------------------------------------------
   432  
   433  func (ac AccesParticipant) AsParticipantCamp() composites.ParticipantCamp {
   434  	return composites.ParticipantCamp{
   435  		Participant: ac.RawData(),
   436  		Camp:        ac.GetCamp().RawData(),
   437  	}
   438  }
   439  
   440  func (ac AccesParticipant) AsParticipantPersonne() composites.ParticipantPersonne {
   441  	return composites.ParticipantPersonne{
   442  		Participant: ac.RawData(),
   443  		Personne:    ac.GetPersonne().RawData(),
   444  	}
   445  }
   446  
   447  func (ac AccesParticipant) GetCamp() AccesCamp {
   448  	return ac.Base.NewCamp(ac.RawData().IdCamp)
   449  }
   450  
   451  func (ac AccesParticipant) GetPersonne() AccesPersonne {
   452  	return ac.Base.NewPersonne(ac.RawData().IdPersonne)
   453  }
   454  
   455  func (ac AccesParticipant) GetFacture() (AccesFacture, bool) {
   456  	idFac := ac.RawData().IdFacture
   457  	if !idFac.Valid {
   458  		return AccesFacture{}, false
   459  	}
   460  	return ac.Base.NewFacture(idFac.Int64), true
   461  }
   462  
   463  func (ac AccesParticipant) GetGroupe() (rd.Groupe, bool) {
   464  	lien, has := ac.Base.ParticipantGroupe[ac.Id]
   465  	if has {
   466  		return ac.Base.NewGroupe(lien.IdGroupe).RawData(), true
   467  	}
   468  	return rd.Groupe{}, false
   469  }
   470  
   471  func (ac AccesParticipant) GetAnimateur() (composites.EquipierPersonne, bool) {
   472  	lien, has := ac.Base.ParticipantEquipier[ac.Id]
   473  	if has {
   474  		eq := ac.Base.Equipiers[lien.IdEquipier]
   475  		pers := ac.Base.Personnes[eq.IdPersonne]
   476  		return composites.EquipierPersonne{Equipier: eq, Personne: pers}, true
   477  	}
   478  	return composites.EquipierPersonne{}, false
   479  }
   480  
   481  // HintsAttente évalue si le participant devrait être placé
   482  // en liste d'attente ou non.
   483  // Certains critères ne sont pertinents que pour un participant
   484  // actuellement en liste d'attente.
   485  func (ac AccesParticipant) HintsAttente(cache rd.LiensCampParticipants) rd.HintsAttente {
   486  	personne := ac.GetPersonne().RawData()
   487  	isAlreadyInscrit := ac.RawData().ListeAttente.Statut == rd.Inscrit
   488  	return ac.GetCamp().hintsAttente(cache, personne, isAlreadyInscrit)
   489  }
   490  
   491  func (ac AccesParticipant) IsAgeValide() bool {
   492  	statusMin, statusMax := ac.GetCamp().RawData().HasAgeValide(ac.GetPersonne().RawData().BasePersonne)
   493  	return statusMin == rd.Inscrit && statusMax == rd.Inscrit
   494  }
   495  
   496  // GetAides renvoies les aides liées au participant.
   497  // Si `cache` vaut nil, parcourt la table des aides.
   498  func (ac AccesParticipant) GetAides(cache LiensParticipantAides) []AccesAide {
   499  	if cache == nil {
   500  		cache = ac.Base.ResoudAides()
   501  	}
   502  	ids := cache[ac.Id]
   503  	out := make([]AccesAide, len(ids))
   504  	for index, id := range ids {
   505  		out[index] = ac.Base.NewAide(id)
   506  	}
   507  	return out
   508  }
   509  
   510  // StatutPrix renvoi la catégorie de prix du participant
   511  // Une catégorie avec un Id vide équivaut à l'absence de catégorie
   512  func (ac AccesParticipant) StatutPrix() rd.PrixParStatut {
   513  	s := ac.RawData().OptionPrix.Statut
   514  	if s == 0 {
   515  		return rd.PrixParStatut{}
   516  	}
   517  	campOption := ac.GetCamp().RawData().OptionPrix
   518  	if campOption.Active != rd.OptionsPrix.STATUT {
   519  		return rd.PrixParStatut{}
   520  	}
   521  	for _, info := range campOption.Statut {
   522  		if info.Id == s {
   523  			return info
   524  		}
   525  	}
   526  	return rd.PrixParStatut{}
   527  }
   528  
   529  type BilanPrixCamp = composites.BilanPrixCamp
   530  
   531  // EtatFinancier calcule le prix du sejour pour le participant,
   532  // en prenant en compte les options, les remises et les aides.
   533  // Si `withAidesInvalides` vaut `false`, les aides non validées ne sont pas renvoyées.
   534  func (ac AccesParticipant) EtatFinancier(cache LiensParticipantAides, withAidesInvalides bool) BilanPrixCamp {
   535  	// résolution des données
   536  	fac, _ := ac.GetFacture()
   537  	cp := composites.ParticipantComplet{
   538  		ParticipantCamp: ac.AsParticipantCamp(),
   539  		// zero value si le participant n'a pas de facture
   540  		FacturePersonne: fac.AsFacturePersonne(),
   541  	}
   542  	// résolutions des aides
   543  	var aides []composites.AideStructureaide
   544  	for _, aide := range ac.GetAides(cache) {
   545  		aides = append(aides, aide.WithStructure())
   546  	}
   547  	return cp.EtatFinancier(aides, withAidesInvalides)
   548  }
   549  
   550  func (ac AccesParticipant) IsFicheSanitaireUpToDate() rd.OptionnalBool {
   551  	return ac.AsParticipantPersonne().IsFicheSanitaireUpToDate()
   552  }
   553  
   554  func (ac AccesParticipant) GetPresence() rd.Plage {
   555  	plage, _ := ac.AsParticipantCamp().AsOptionParticipantCamp().Presence()
   556  	return plage
   557  }
   558  
   559  // -------------------------------------------------------------------------
   560  
   561  func (ac AccesParticipantsimple) GetCamp() AccesCamp {
   562  	return ac.Base.NewCamp(ac.RawData().IdCamp)
   563  }
   564  
   565  func (ac AccesParticipantsimple) GetPersonne() AccesPersonne {
   566  	return ac.Base.NewPersonne(ac.RawData().IdPersonne)
   567  }
   568  
   569  // GetPresence renvoie la date d'arrivée et départ du participant
   570  func (ac AccesParticipantsimple) GetPresence() rd.Plage {
   571  	camp := ac.GetCamp().RawData()
   572  	arrivee, depart := camp.DateDebut, camp.DateFin
   573  	return rd.Plage{From: arrivee, To: depart}
   574  }
   575  
   576  // AsItemChilds renvoie une stucture hierachique sans enfants
   577  func (ac AccesParticipantsimple) AsItemChilds() rd.ItemChilds {
   578  	item := ac.AsItem()
   579  	return rd.ItemChilds{Item: item}
   580  }
   581  
   582  // --------------------------------------------------------------------------
   583  // ----------------------------- Equipier -----------------------------------
   584  // --------------------------------------------------------------------------
   585  
   586  func (ac AccesEquipier) GetPersonne() AccesPersonne {
   587  	return ac.Base.NewPersonne(ac.RawData().IdPersonne)
   588  }
   589  
   590  func (ac AccesEquipier) GetCamp() AccesCamp {
   591  	return ac.Base.NewCamp(ac.RawData().IdCamp)
   592  }
   593  
   594  func (ac AccesEquipier) GetContraintes() rd.EquipierContraintes {
   595  	return ac.Base.EquipierContraintes[ac.Id]
   596  }
   597  
   598  func safeChange(date, candidat rd.Date) rd.Date {
   599  	if !candidat.Time().IsZero() {
   600  		return candidat
   601  	}
   602  	return date
   603  }
   604  
   605  // GetPresence renvoie la date d'arrivée et départ du participant
   606  // Elles sont déterminés par le champ `Présence`
   607  // (puis par défaut les dates du camp)
   608  func (ac AccesEquipier) GetPresence() rd.Plage {
   609  	camp := ac.GetCamp()
   610  	rawCamp := camp.RawData()
   611  
   612  	// plus faible priorité
   613  	arrivee, depart := rawCamp.DateDebut, rawCamp.DateFin
   614  
   615  	rawPart := ac.RawData()
   616  	if rawPart.Presence.Active {
   617  		arrivee, depart = safeChange(arrivee, rawPart.Presence.From), safeChange(depart, rawPart.Presence.To)
   618  	}
   619  	return rd.Plage{From: arrivee, To: depart}
   620  }
   621  
   622  // --------------------------------------------------------------------------
   623  // ------------------------------- Aide -------------------------------------
   624  // --------------------------------------------------------------------------
   625  
   626  func (ac AccesAide) GetStructureaide() AccesStructureaide {
   627  	return ac.Base.NewStructureaide(ac.RawData().IdStructureaide)
   628  }
   629  
   630  func (ac AccesAide) GetParticipant() AccesParticipant {
   631  	return ac.Base.NewParticipant(ac.RawData().IdParticipant)
   632  }
   633  
   634  func (ac AccesAide) AsAideParticipantCamp() composites.AideParticipantCamp {
   635  	return composites.AideParticipantCamp{
   636  		Aide:            ac.RawData(),
   637  		ParticipantCamp: ac.GetParticipant().AsParticipantCamp(),
   638  	}
   639  }
   640  
   641  // ValeurEffective renvoie :
   642  // 	- le montant dans le cas d'une aide absolue
   643  // 	- le montant fois le nombre de jours (en prenant en compte une éventuelle limite) sinon
   644  // Si `withRemise` vaut true les remises (%) sont appliquées.
   645  func (ac AccesAide) ValeurEffective(withRemise bool) rd.Euros {
   646  	return ac.AsAideParticipantCamp().ValeurEffective(withRemise)
   647  }
   648  
   649  // GetDocuments renvoie la liste des pièces justificatives associées à l'aide.
   650  func (ac AccesAide) GetDocuments() rd.Table {
   651  	aideToDocs := ac.Base.ResoudDocumentsAides()
   652  	ids := aideToDocs[ac.Id]
   653  	out := make(rd.Table, len(ids))
   654  	for i, id := range ids {
   655  		out[i] = AccesDocumentAide{ac.Base.NewDocument(id)}.AsItem()
   656  	}
   657  	return out
   658  }
   659  
   660  func (ac AccesAide) Label() string {
   661  	return ac.GetParticipant().GetPersonne().RawData().NomPrenom().String() + " - " + string(ac.GetStructureaide().RawData().Nom)
   662  }
   663  
   664  func (ac AccesAide) Description() string {
   665  	return fmt.Sprintf("%s : %s", ac.GetStructureaide().RawData().Nom, ac.ValeurEffective(false))
   666  }
   667  
   668  func (ac AccesAide) WithStructure() composites.AideStructureaide {
   669  	return composites.AideStructureaide{
   670  		Aide:          ac.RawData(),
   671  		Structureaide: ac.GetStructureaide().RawData(),
   672  	}
   673  }
   674  
   675  // --------------------------------------------------------------------------
   676  // -------------------------------- Facture ---------------------------------
   677  // --------------------------------------------------------------------------
   678  
   679  func (ac AccesFacture) GetPersonne() AccesPersonne {
   680  	return ac.Base.NewPersonne(ac.RawData().IdPersonne)
   681  }
   682  
   683  func (ac AccesFacture) AsFacturePersonne() composites.FacturePersonne {
   684  	return composites.FacturePersonne{
   685  		Facture:  ac.RawData(),
   686  		Personne: ac.GetPersonne().RawData(),
   687  	}
   688  }
   689  
   690  // GetDossiers renvoie les dossiers associés à la facture.
   691  // Si l'attribut `cache` vaut nil,
   692  // effectue un parcourt de la table des participants.
   693  func (ac AccesFacture) GetDossiers(cache LiensFactureParticipants) []AccesParticipant {
   694  	if cache == nil {
   695  		cache, _ = ac.Base.ResoudParticipants()
   696  	}
   697  	ids := cache[ac.Id]
   698  	out := make([]AccesParticipant, len(ids))
   699  	for index, id := range ids {
   700  		out[index] = ac.Base.NewParticipant(id)
   701  	}
   702  	sort.Slice(out, func(i, j int) bool {
   703  		return out[i].Id < out[j].Id
   704  	})
   705  	return out
   706  }
   707  
   708  func (ac AccesFacture) AsItemChilds(cache1 LiensFactureParticipants, cache2 LiensFactureMessages, cache3 LiensFacturePaiements, cache4 LiensCampParticipants) rd.ItemChilds {
   709  	item := ac.AsItem(cache1, cache2, cache3)
   710  	doss := ac.GetDossiers(cache1)
   711  	childs := make(rd.Table, len(doss))
   712  	for index, d := range doss {
   713  		childs[index] = d.AsItem(cache4)
   714  	}
   715  	return rd.ItemChilds{Item: item, Childs: childs}
   716  }
   717  
   718  // GetPaiements renvoies les paiements liés à la facture.
   719  // Si l'attribut `cache` vaut nil,
   720  // effectue un parcourt de la table des paiements
   721  func (ac AccesFacture) GetPaiements(cache LiensFacturePaiements, withInvalides bool) []AccesPaiement {
   722  	if cache == nil {
   723  		cache = ac.Base.ResoudPaiements()
   724  	}
   725  	ids := cache[ac.Id]
   726  	out := make([]AccesPaiement, 0, len(ids))
   727  	for _, Id := range ids {
   728  		ac := ac.Base.NewPaiement(Id)
   729  		if withInvalides || bool(!ac.RawData().IsInvalide) {
   730  			out = append(out, ac)
   731  		}
   732  	}
   733  	return out
   734  }
   735  
   736  // ChoixDestinataires renvoie les destinataires possibles, y compris
   737  // le responsable, qui est garantit d'être en première position
   738  func (ac AccesFacture) ChoixDestinataires() rd.DestinatairesOptionnels {
   739  	d := ac.GetPersonne().RawData().ToDestinataire()
   740  	others := ac.RawData().DestinatairesOptionnels
   741  	return append(rd.DestinatairesOptionnels{d}, others...)
   742  }
   743  
   744  // BilanFacture résume l'état d'une facture.
   745  // (il peut être utilisé pour éviter de répéter les mêmes calculs )
   746  type BilanFacture struct {
   747  	Paiements     []rd.Paiement
   748  	Participants  []AccesParticipant
   749  	Demande, Recu rd.Euros
   750  }
   751  
   752  func (b BilanFacture) IsAcquitte() bool {
   753  	return b.Demande.IsLess(b.Recu)
   754  }
   755  
   756  func (b BilanFacture) IsExactAcquitte() bool {
   757  	return b.Demande.IsEqual(b.Recu)
   758  }
   759  
   760  func (b BilanFacture) ApresPaiement() rd.Euros {
   761  	return b.Demande - b.Recu
   762  }
   763  
   764  func (b BilanFacture) StatutPaiement() rd.Completion {
   765  	if b.Participants == nil {
   766  		return rd.Invalide
   767  	}
   768  	if b.IsAcquitte() {
   769  		return rd.Complete
   770  	} else if b.Recu > 0 {
   771  		return rd.EnCours
   772  	} else {
   773  		return rd.NonCommencee
   774  	}
   775  }
   776  
   777  type CacheEtatFinancier struct {
   778  	PAides        LiensParticipantAides
   779  	FPaiements    LiensFacturePaiements
   780  	FParticipants LiensFactureParticipants
   781  }
   782  
   783  // NewCacheEtatFinancier parcourts les tables des aides, paiements et participants
   784  func (b *BaseLocale) NewCacheEtatFinancier() CacheEtatFinancier {
   785  	var cache CacheEtatFinancier
   786  	cache.PAides = b.ResoudAides()
   787  	cache.FPaiements = b.ResoudPaiements()
   788  	cache.FParticipants, _ = b.ResoudParticipants()
   789  	return cache
   790  }
   791  
   792  // EtatFinancier additionne le prix pour chaque participant et
   793  // décompte les paiements effectués. Seuls les participants inscrits (en liste principale)
   794  // sont pris en compte.
   795  // Les participants en liste d'attente sont ignorés.
   796  // `withInvalides` contrôle si les aides et paiements invalides sont affichés ou non.
   797  // Les maps de `cache` sont générés si `nil`
   798  func (ac AccesFacture) EtatFinancier(cache CacheEtatFinancier, withInvalides bool) BilanFacture {
   799  	var out BilanFacture
   800  
   801  	if cache.PAides == nil {
   802  		cache.PAides = ac.Base.ResoudAides()
   803  	}
   804  
   805  	for _, participant := range ac.GetDossiers(cache.FParticipants) {
   806  		if !participant.RawData().ListeAttente.IsInscrit() {
   807  			continue
   808  		}
   809  		prixParticipant := participant.EtatFinancier(cache.PAides, withInvalides) //
   810  		out.Demande += prixParticipant.PrixNet()
   811  		out.Participants = append(out.Participants, participant)
   812  	}
   813  
   814  	for _, paiement := range ac.GetPaiements(cache.FPaiements, false) {
   815  		raw := paiement.RawData()
   816  		if raw.IsRemboursement {
   817  			out.Recu -= raw.Valeur
   818  		} else {
   819  			out.Recu += raw.Valeur
   820  		}
   821  		out.Paiements = append(out.Paiements, raw)
   822  	}
   823  	return out
   824  }
   825  
   826  // Camps renvois les camps concernés par la facture, ainsi qu'une description
   827  func (ac AccesFacture) Camps(cache LiensFactureParticipants) (map[int64]bool, string) {
   828  	ids, tmp := make(map[int64]bool), rd.StringSet{}
   829  	for _, doss := range ac.GetDossiers(cache) {
   830  		camp := doss.GetCamp().RawData()
   831  		ids[camp.Id] = true
   832  		tmp[camp.Label().String()] = true
   833  	}
   834  	noms := tmp.ToList()
   835  	sort.Slice(noms, func(i, j int) bool {
   836  		return noms[i] > noms[j]
   837  	})
   838  	return ids, strings.Join(noms, ", ")
   839  }
   840  
   841  // participants renvoit les participants
   842  func (ac AccesFacture) participants(cache LiensFactureParticipants) string {
   843  	idsPers := rd.NewSet()
   844  	for _, doss := range ac.GetDossiers(cache) {
   845  		idsPers.Add(doss.GetPersonne().Id)
   846  	}
   847  	var chunks []string
   848  	for id := range idsPers {
   849  		chunks = append(chunks, ac.Base.NewPersonne(id).RawData().NomPrenom().String())
   850  	}
   851  	sort.Strings(chunks) // déterminisme
   852  	return strings.Join(chunks, ", ")
   853  }
   854  
   855  // Description renvoie un tableau Html résumant les participants et les
   856  // paiements associés à la facture.
   857  // Les paiements et aides invalides sont affichés.
   858  func (ac AccesFacture) Description() string {
   859  	bilan := ac.EtatFinancier(CacheEtatFinancier{}, true)
   860  	s := "<table cellpadding='5'><tr>"
   861  	for _, header := range headerFacture {
   862  		s += "<th>" + header + "</th>"
   863  	}
   864  	s += "</tr>"
   865  	sepTab := "&nbsp;"
   866  	for _, dossier := range bilan.Participants {
   867  		var row bytes.Buffer
   868  		bilanPrix := dossier.EtatFinancier(nil, true)
   869  		err := templateRowFacture.Execute(&row, dataTemplateFacture{
   870  			Personne:  dossier.GetPersonne().RawData(),
   871  			Camp:      dossier.GetCamp().RawData(),
   872  			BilanPrix: bilanPrix,
   873  		})
   874  		if err == nil {
   875  			s += row.String()
   876  		} else {
   877  			s += "<tr>" + err.Error() + "</tr>"
   878  		}
   879  		rem := bilanPrix.Remises.Description(sepTab)
   880  		sousTotal := bilanPrix.PrixNet().String()
   881  		if len(rem) > 0 {
   882  			s += fmt.Sprintf(`<tr><td colspan="%d" style="text-align: right;">%s</td><td color="blue">%s</td></tr>`,
   883  				len(headerFacture)-1, strings.Join(rem, sepTab), sousTotal)
   884  		}
   885  	}
   886  	s += fmt.Sprintf(`<tr><td colspan=%d style="text-align: right;">%s</td><td><i>%s</i></td></tr>`,
   887  		len(headerFacture)-1, "Total (après remises)", bilan.Demande)
   888  	for _, paiement := range ac.GetPaiements(nil, true) {
   889  		desc, montant := paiement.RawData().Description()
   890  		s += fmt.Sprintf(`<tr><td colspan="2"><td colspan="%d" style="text-align: left;"><a href='%d'>%s</a></td>
   891  			<td>%s</td></tr>`,
   892  			len(headerFacture)-3, paiement.Id, desc, montant)
   893  	}
   894  	s += fmt.Sprintf(`<tr><td colspan=%d style="text-align: right;">%s</td><td><b>%s</b></td></tr>`,
   895  		len(headerFacture)-1, "Total (après paiements)", bilan.ApresPaiement())
   896  	s += "</table>"
   897  	return s
   898  }
   899  
   900  // LabelVirement renvoie le label (préfixé) permettant d'identifier le dossier
   901  // lors d'un virement.
   902  func (ac AccesFacture) LabelVirement() string {
   903  	return logs.LabelVirement.Offusc(ac.Id)
   904  }
   905  
   906  // NeedAcompte indique si un acompte est demandé sur l'un des camps du dossier.
   907  func (ac AccesFacture) NeedAcompte() bool {
   908  	for _, part := range ac.GetDossiers(nil) {
   909  		if part.GetCamp().RawData().SchemaPaiement == rd.SPAcompte {
   910  			return true
   911  		}
   912  	}
   913  	return false
   914  }
   915  
   916  // --------------------------------------------------------------------------
   917  // ------------------------------ Paiement ----------------------------------
   918  // --------------------------------------------------------------------------
   919  func (ac AccesPaiement) GetFacture() AccesFacture {
   920  	return ac.Base.NewFacture(ac.RawData().IdFacture)
   921  }
   922  
   923  // --------------------------------------------------------------------------
   924  // ------------------------------ Document ----------------------------------
   925  // --------------------------------------------------------------------------
   926  
   927  type AccesDocumentPersonne struct {
   928  	AccesDocument
   929  }
   930  
   931  func (b *BaseLocale) NewDocumentPersonne(id int64) AccesDocumentPersonne {
   932  	return AccesDocumentPersonne{b.NewDocument(id)}
   933  }
   934  
   935  // GetContrainte renvoie la contrainte pour un document
   936  // attaché à une personne
   937  func (ac AccesDocumentPersonne) GetContrainte() rd.Contrainte {
   938  	lien := ac.Base.DocumentPersonnes[ac.Id]
   939  	return ac.Base.Contraintes[lien.IdContrainte]
   940  }
   941  
   942  func (ac AccesDocumentPersonne) GetProprietaire() AccesPersonne {
   943  	lien := ac.Base.DocumentPersonnes[ac.Id]
   944  	return ac.Base.NewPersonne(lien.IdPersonne)
   945  }
   946  
   947  type AccesDocumentAide struct {
   948  	AccesDocument
   949  }
   950  
   951  func (ac AccesDocumentAide) GetAide() AccesAide {
   952  	lien := ac.Base.DocumentAides[ac.Id]
   953  	return ac.Base.NewAide(lien.IdAide)
   954  }
   955  
   956  // --------------------------------------------------------------------------
   957  // --------------------------------- Don ------------------------------------
   958  // --------------------------------------------------------------------------
   959  
   960  // PersonneOrganisme regroupe les éléments affichés
   961  // dans la liste des personnes : personne ou organisme
   962  type PersonneOrganisme interface {
   963  	// CoordonneesDons renvoie les coordonnées utilisées
   964  	// pour un don.
   965  	CoordonneesDons() rd.Coordonnees
   966  }
   967  
   968  // CoordonneesDons renvoie les coordonnées de la personne
   969  func (ac AccesPersonne) CoordonneesDons() rd.Coordonnees {
   970  	return ac.RawData().Coordonnees()
   971  }
   972  
   973  // CoordonneesDons choisit en priorité le contact spécifique aux dons,
   974  // puis le contact normal (propre ou non).
   975  func (ac AccesOrganisme) CoordonneesDons() rd.Coordonnees {
   976  	raw := ac.RawData()
   977  	if cd := raw.IdContactDon; cd.IsNotNil() {
   978  		return ac.Base.Personnes[cd.Int64].Coordonnees()
   979  	} else if c := raw.IdContact; c.IsNotNil() {
   980  		return ac.Base.Personnes[c.Int64].Coordonnees()
   981  	} else {
   982  		return raw.Contact
   983  	}
   984  }
   985  
   986  // GetDonateur renvoie le donateur, possiblement nil
   987  func (ac AccesDon) GetDonateur() PersonneOrganisme {
   988  	donateur := ac.Base.DonDonateurs[ac.Id]
   989  	switch id := donateur.IId().(type) {
   990  	case rd.IdPersonne:
   991  		return ac.Base.NewPersonne(id.Int64())
   992  	case rd.IdOrganisme:
   993  		return ac.Base.NewOrganisme(id.Int64())
   994  	default:
   995  		return nil
   996  	}
   997  }