github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/client/controllers/cont_camps.go (about)

     1  package controllers
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"sort"
     8  	"strconv"
     9  	"time"
    10  
    11  	"github.com/benoitkugler/goACVE/server/core/apiserver"
    12  	dm "github.com/benoitkugler/goACVE/server/core/datamodel"
    13  	"github.com/benoitkugler/goACVE/server/core/documents"
    14  	rd "github.com/benoitkugler/goACVE/server/core/rawdata"
    15  	"github.com/benoitkugler/goACVE/server/core/utils/table"
    16  )
    17  
    18  var HeadersResponsables = []rd.Header{
    19  	{Field: dm.ParticipantRespoNom, Label: "Nom"},
    20  	{Field: dm.ParticipantRespoPrenom, Label: "Prénom"},
    21  	{Field: dm.ParticipantRespoMail, Label: "Mail"},
    22  	{Field: dm.ParticipantRespoTelsLines, Label: "Téléphones"},
    23  	{Field: dm.ParticipantRespoAdresse, Label: "Adresse"},
    24  	{Field: dm.ParticipantRespoCodePostal, Label: "Code postal"},
    25  	{Field: dm.ParticipantRespoVille, Label: "Ville"},
    26  }
    27  
    28  type ButtonsCamps struct {
    29  	CreerParticipant, SupprimerParticipant, ChangerParticipant, VerifieAttente, ExporterParticipants EtatSideButton
    30  }
    31  
    32  type OngletCamps interface {
    33  	baseOnglet
    34  
    35  	ConfirmeNotifPlaceLiberee(info string) (keep bool, notifie bool)
    36  	ConfirmeAjoutParticipant(camp dm.AccesCamp, personne rd.Personne, causes []string) bool
    37  	ConfirmeSupprimeParticipant(msg string) bool
    38  }
    39  
    40  type EtatCamps struct {
    41  	CampCurrent         rd.IId
    42  	ShowCampsTerminated bool
    43  
    44  	IdParticipant rd.IId // simple ou complet
    45  }
    46  
    47  type Camps struct {
    48  	Onglet         OngletCamps
    49  	main           *MainController
    50  	ListeCamps     rd.Table
    51  	ListeInscrits  rd.Table
    52  	ListeAttente   rd.Table
    53  	HeaderInscrits []rd.Header
    54  	HeaderAttente  []rd.Header
    55  	Base           *dm.BaseLocale
    56  	Etat           EtatCamps
    57  
    58  	// Droit d'édition/ajout participant,
    59  	// Droit de modification partielle des infos du camp
    60  	ParticipantsRight, CampsRight bool
    61  }
    62  
    63  func NewCamps(main *MainController, permission int) *Camps {
    64  	c := Camps{main: main, Base: main.Base, ParticipantsRight: permission >= 2,
    65  		CampsRight: permission >= 3,
    66  		HeaderAttente: []rd.Header{
    67  			{Field: dm.PersonneNom, Label: "Nom"},
    68  			{Field: dm.PersonnePrenom, Label: "Prénom"},
    69  			{Field: dm.PersonneSexe, Label: "Sexe"},
    70  			{Field: dm.ParticipantAgeDebutCamp, Label: "Age au début du camp"},
    71  			{Field: dm.PersonneDateNaissance, Label: "Date de naissance"},
    72  			{Field: dm.ParticipantRaisonAttente, Label: "Statut"},
    73  		},
    74  		HeaderInscrits: []rd.Header{
    75  			{Field: dm.PersonneNom, Label: "Nom"},
    76  			{Field: dm.PersonnePrenom, Label: "Prénom"},
    77  			{Field: dm.PersonneSexe, Label: "Sexe"},
    78  			{Field: dm.ParticipantAgeDebutCamp, Label: "Age au début du camp"},
    79  			{Field: dm.PersonneDateNaissance, Label: "Date de naissance"},
    80  			{Field: dm.ParticipantGroupe, Label: "Groupe"},
    81  			{Field: dm.ParticipantBus, Label: "Navette"},
    82  			{Field: dm.ParticipantMaterielSki, Label: "Matériel de ski"},
    83  		},
    84  	}
    85  	c.resetData()
    86  	return &c
    87  }
    88  
    89  func (c *Camps) resetData() {
    90  	isTerminated := rd.OBNon
    91  	if c.Etat.ShowCampsTerminated {
    92  		isTerminated = 0
    93  	}
    94  	tmp := c.Base.GetCamps(false, isTerminated)
    95  	sort.Slice(tmp, func(i, j int) bool { // ordre déterministique
    96  		return tmp[i].Id < tmp[j].Id
    97  	})
    98  	sort.SliceStable(tmp, func(i, j int) bool {
    99  		return tmp[i].RawData().DateDebut.Time().Before(tmp[j].RawData().DateDebut.Time())
   100  	})
   101  	sort.SliceStable(tmp, func(i, j int) bool {
   102  		return bool(tmp[i].RawData().Ouvert || !tmp[j].RawData().Ouvert)
   103  	})
   104  
   105  	_, cache1 := c.Base.ResoudParticipants()
   106  	cache2 := c.Base.ResoudParticipantsimples()
   107  	c.ListeCamps = make(rd.Table, len(tmp))
   108  	for index, camp := range tmp {
   109  		c.ListeCamps[index] = camp.AsItem(cache1, cache2)
   110  	}
   111  
   112  	// besoin de déselectionner si absent des camps affichés
   113  	if c.Etat.CampCurrent != nil && !HasId(c.ListeCamps, c.Etat.CampCurrent) {
   114  		c.Etat.CampCurrent = nil
   115  	}
   116  
   117  	partCurrentInListe := false
   118  	if idCamp := c.Etat.CampCurrent; idCamp != nil {
   119  		camp := c.Base.NewCamp(idCamp.Int64())
   120  		c.ListeInscrits, c.ListeAttente = camp.GetListes(false, "", false, false)
   121  		partCurrent := c.Etat.IdParticipant
   122  		if partCurrent != nil && (HasId(c.ListeInscrits, partCurrent) || HasId(c.ListeAttente, partCurrent)) {
   123  			partCurrentInListe = true
   124  		}
   125  	}
   126  	// besoin de déselectionner si absent des participants affichés
   127  	if !partCurrentInListe {
   128  		c.Etat.IdParticipant = nil
   129  	}
   130  }
   131  
   132  func (c *Camps) SideButtons() ButtonsCamps {
   133  	bs := ButtonsCamps{}
   134  	hasCamp := ButtonPresent
   135  	if c.Etat.CampCurrent != nil {
   136  		hasCamp = ButtonActivated
   137  	}
   138  	if c.ParticipantsRight {
   139  		bs.CreerParticipant = hasCamp
   140  
   141  		bs.SupprimerParticipant = ButtonPresent
   142  		bs.ChangerParticipant = ButtonPresent
   143  		if c.Etat.IdParticipant != nil {
   144  			bs.SupprimerParticipant = ButtonActivated
   145  			bs.ChangerParticipant = ButtonActivated
   146  		}
   147  	}
   148  	bs.ExporterParticipants = hasCamp
   149  	bs.VerifieAttente = ButtonActivated
   150  	return bs
   151  }
   152  
   153  func (c *Camps) GetStats() (out []Stat) {
   154  	if c.Etat.CampCurrent == nil {
   155  		return
   156  	}
   157  	camp := c.Base.NewCamp(c.Etat.CampCurrent.Int64())
   158  	stats := camp.GetStats(nil, nil, nil)
   159  	out = []Stat{
   160  		{Label: "Nombre d'inscrits", Value: strconv.Itoa(stats.Inscrits)},
   161  		{Label: "Nombre de places restantes", Value: strconv.Itoa(stats.PlacesRestantes)},
   162  		{Label: "Taille de la liste d'attente", Value: strconv.Itoa(stats.Attente)},
   163  		{Label: "Taille de l'équipe", Value: strconv.Itoa(stats.Equipe)},
   164  		{Label: "Nombre de places réservées", Value: strconv.Itoa(stats.PlacesReservees)},
   165  	}
   166  	if camp.RawData().NeedEquilibreGf {
   167  		v := "-"
   168  		if stats.Inscrits != 0 {
   169  			pG := stats.Garcons * 100 / stats.Inscrits
   170  			v = fmt.Sprintf("%d/%d (%%)", pG, 100-pG)
   171  		}
   172  		out = append(out, Stat{Label: "Equilibre Garçons/Filles", Value: v})
   173  	}
   174  	return
   175  }
   176  
   177  func updateCamp(camp rd.Camp) (rd.Camp, error) {
   178  	var out rd.Camp
   179  	err := requete(apiserver.UrlCamps, http.MethodPost, camp, &out)
   180  	return out, err
   181  }
   182  
   183  // UpdateCamp met à jour le profil transmis sur le serveur
   184  func (c *Camps) UpdateCamp(camp rd.Camp) {
   185  	job := func() (interface{}, error) {
   186  		return updateCamp(camp)
   187  	}
   188  	onSuccess := func(_out interface{}) {
   189  		out := _out.(rd.Camp)
   190  		c.Base.Camps[out.Id] = out
   191  		c.main.ShowStandard(fmt.Sprintf("Camp %s mis à jour avec succès.", out.Label()), false)
   192  		c.main.ResetAllControllers()
   193  	}
   194  	c.main.ShowStandard("Mise à jour du camp...", true)
   195  	c.main.Background.Run(job, onSuccess)
   196  }
   197  
   198  // UpdateParticipant met à jour de profil transmis sur le serveur, de manière SYNCHRONE.
   199  // Gère l'erreur et la modification de la base locale.
   200  func (c *Camps) UpdateParticipant(part rd.Participant) *dm.AccesParticipant {
   201  	c.main.ShowStandard("Mise à jour du participant...", true)
   202  	var out apiserver.CreateParticipantOut
   203  	if err := requete(apiserver.UrlParticipants, http.MethodPost, part, &out); err != nil {
   204  		c.main.ShowError(err)
   205  		return nil
   206  	}
   207  
   208  	c.Base.ApplyCreateParticipant(out)
   209  	c.main.ShowStandard("Participant mis à jour avec succès.", false)
   210  	c.main.ResetAllControllers()
   211  	ac := c.Base.NewParticipant(out.Participant.Id)
   212  	return &ac
   213  }
   214  
   215  // UpdateParticipantsimple met à jour de profil transmis sur le serveur, de manière SYNCHRONE.
   216  // Gère l'erreur et la modification de la base locale.
   217  func (c *Camps) UpdateParticipantsimple(part rd.Participantsimple) *dm.AccesParticipantsimple {
   218  	c.main.ShowStandard("Mise à jour du participant...", true)
   219  	var out rd.Participantsimple
   220  	if err := requete(apiserver.UrlParticipantsimples, http.MethodPost, part, &out); err != nil {
   221  		c.main.ShowError(err)
   222  		return nil
   223  	}
   224  
   225  	c.Base.Participantsimples[out.Id] = out
   226  	c.main.ShowStandard("Participant mis à jour avec succès.", false)
   227  	c.main.ResetAllControllers()
   228  	ac := c.Base.NewParticipantsimple(out.Id)
   229  	return &ac
   230  }
   231  
   232  func (c *Camps) SupprimeParticipantCourant() {
   233  	switch idPart := c.Etat.IdParticipant.(type) {
   234  	case rd.IdParticipant:
   235  		c.SupprimeParticipant(c.Base.NewParticipant(idPart.Int64()), false)
   236  	case rd.IdParticipantsimple:
   237  		c.supprimeParticipantsimple(c.Base.NewParticipantsimple(idPart.Int64()))
   238  	}
   239  }
   240  
   241  // SupprimeParticipant demande la suppression au serveur,
   242  // après avoir averti l'utilisateur.
   243  // Effectue un reset général.
   244  func (c *Camps) SupprimeParticipant(part dm.AccesParticipant, synchrone bool) {
   245  	message := fmt.Sprintf("Suppression de <b>%s</b> du camp %s <br/><br/>", part.GetPersonne().RawData().NomPrenom(),
   246  		part.GetCamp().RawData().Label())
   247  
   248  	fac, hasFacture := part.GetFacture()
   249  	if hasFacture {
   250  		message += "Le participant sera <b>supprimé et retiré du dossier</b> associé. <br/>"
   251  		if len(fac.GetDossiers(nil)) == 1 { // dernier dossier
   252  			message += "Attention, le dossier sera vide.  Si vous pensez ne plus" +
   253  				" travailler avec ce dossier, pensez à le supprimer (Onglet Suivi des dossiers). <br/>"
   254  		}
   255  	} else {
   256  		message += "Le participant sera <b>supprimé</b>. <br/>"
   257  	}
   258  	aides := part.GetAides(nil)
   259  	if len(aides) == 1 {
   260  		message += "<i>Une aide liée</i> au participant sera aussi supprimée."
   261  	} else if len(aides) > 1 {
   262  		message += fmt.Sprintf("<br/> <i>%d aides liées</i> au participant seront aussi supprimées.", len(aides))
   263  	}
   264  
   265  	if !c.Onglet.ConfirmeSupprimeParticipant(message) {
   266  		c.main.ShowStandard("Suppression annulée.", false)
   267  		return
   268  	}
   269  
   270  	job := func() (interface{}, error) {
   271  		var out apiserver.DeleteParticipantOut
   272  		err := requete(apiserver.UrlParticipants, http.MethodDelete, IdAsParams(part.Id), &out)
   273  		return out, err
   274  	}
   275  	onSuccess := func(_out interface{}) {
   276  		out := _out.(apiserver.DeleteParticipantOut)
   277  		c.Base.CleanupParticipant(out)
   278  		msg := "Participant supprimé avec succès."
   279  		if len(out.IdsAides) > 0 {
   280  			msg += fmt.Sprintf(" %d aide(s) associées également supprimées.", len(out.IdsAides))
   281  		}
   282  		if out.IdPersonne >= 0 {
   283  			msg += " Personne temporaire supprimée."
   284  		}
   285  		c.main.ShowStandard(msg, false)
   286  		c.main.ResetAllControllers()
   287  	}
   288  	c.main.ShowStandard("Suppression du participant...", true)
   289  	background := c.main.Background
   290  	if synchrone {
   291  		background = SequentialBackground{OnError: c.main.ShowError}
   292  	}
   293  	background.Run(job, onSuccess)
   294  }
   295  
   296  // supprimeParticipantsimple demande la suppression au serveur (asynchrone),
   297  // après avoir averti l'utilisateur.
   298  // Effectue un reset général.
   299  func (c *Camps) supprimeParticipantsimple(part dm.AccesParticipantsimple) bool {
   300  	message := fmt.Sprintf("Suppression de <b>%s</b> du camp %s <br/><br/>", part.GetPersonne().RawData().NomPrenom(),
   301  		part.GetCamp().RawData().Label())
   302  
   303  	if !c.Onglet.ConfirmeSupprimeParticipant(message) {
   304  		c.main.ShowStandard("Suppression annulée.", false)
   305  		return false
   306  	}
   307  
   308  	job := func() (interface{}, error) {
   309  		var out rd.Participantsimple
   310  		err := requete(apiserver.UrlParticipantsimples, http.MethodDelete, IdAsParams(part.Id), &out)
   311  		return out, err
   312  	}
   313  	onSuccess := func(_out interface{}) {
   314  		out := _out.(rd.Participantsimple)
   315  		delete(c.Base.Participantsimples, out.Id)
   316  		c.main.ShowStandard("Participant supprimé avec succès.", false)
   317  		c.main.ResetAllControllers()
   318  
   319  	}
   320  	c.main.ShowStandard("Suppression du participant...", true)
   321  	c.main.Background.Run(job, onSuccess)
   322  	return true
   323  }
   324  
   325  func (c *Camps) checkParticipant(participant rd.Participant) (keep bool) {
   326  	personne := c.Base.Personnes[participant.IdPersonne]
   327  	camp := c.Base.NewCamp(participant.IdCamp)
   328  	if c.Base.IsParticipantAlreadyHere(personne.Id, camp.Id) {
   329  		c.main.ShowError(fmt.Errorf("%s est déjà présent(e) au camp %s", personne.NomPrenom(),
   330  			camp.RawData().Label()))
   331  		return false
   332  	}
   333  	hints := camp.HintsAttente(participant.IdPersonne, participant.ListeAttente.Statut == rd.Inscrit)
   334  	if hints.Hint() != rd.Inscrit && !c.Onglet.ConfirmeAjoutParticipant(camp, personne, hints.Causes()) {
   335  		c.main.ShowStandard("Ajout annulé.", false)
   336  		return false
   337  	}
   338  	return true
   339  }
   340  
   341  // CreeParticipant ajoute le participant donné, après vérification
   342  func (c *Camps) CreeParticipant(participant rd.Participant) {
   343  	if !c.checkParticipant(participant) {
   344  		return
   345  	}
   346  
   347  	participant.DateHeure = rd.Time(time.Now())
   348  	job := func() (interface{}, error) {
   349  		var out apiserver.CreateParticipantOut
   350  		err := requete(apiserver.UrlParticipants, http.MethodPut, participant, &out)
   351  		return out, err
   352  	}
   353  	onSuccess := func(_out interface{}) {
   354  		out := _out.(apiserver.CreateParticipantOut)
   355  		c.Base.ApplyCreateParticipant(out)
   356  		c.main.ShowStandard("Participant ajouté avec succès.", false)
   357  		c.main.ResetAllControllers()
   358  
   359  	}
   360  	c.main.ShowStandard("Ajout du participant en cours...", true)
   361  	c.main.Background.Run(job, onSuccess)
   362  }
   363  
   364  // CreeParticipantsimple ajoute un participant au camp donné.
   365  func (c *Camps) CreeParticipantsimple(participant rd.Participantsimple) {
   366  	if c.Base.IsParticipantAlreadyHere(participant.IdPersonne, participant.IdCamp) {
   367  		personne, camp := c.Base.Personnes[participant.IdPersonne], c.Base.Camps[participant.IdCamp]
   368  		c.main.ShowError(fmt.Errorf("%s est déjà présent(e) au camp %s", personne.NomPrenom(),
   369  			camp.Label()))
   370  		return
   371  	}
   372  
   373  	participant.DateHeure = rd.Time(time.Now())
   374  
   375  	job := func() (interface{}, error) {
   376  		var out rd.Participantsimple
   377  		err := requete(apiserver.UrlParticipantsimples, http.MethodPut, participant, &out)
   378  		return out, err
   379  	}
   380  	onSuccess := func(_out interface{}) {
   381  		out := _out.(rd.Participantsimple)
   382  		c.Base.Participantsimples[out.Id] = out
   383  		c.main.ShowStandard("Participant ajouté avec succès.", false)
   384  		c.main.ResetAllControllers()
   385  
   386  	}
   387  	c.main.ShowStandard("Ajout du participant en cours...", true)
   388  	c.main.Background.Run(job, onSuccess)
   389  }
   390  
   391  // MoveToInscrits effectue les vérifications, puis demande propose d'envoyer un mail de notification.
   392  // La rotation `_attente` -> `_attente_reponse` -> `_campeur` est appliquée.
   393  // Si `notifie` vaut `false` ou si le participant n'a pas de dossier,
   394  // le rôle `_attente_reponse` est sauté.
   395  // La participant peut être placé manuellement en `_attente_reponse` sans envoyer de mail
   396  // en modifiant son rôle directement.
   397  func (c *Camps) MoveToInscrits(id rd.IId) {
   398  	idParticipant, ok := id.(rd.IdParticipant)
   399  	if !ok {
   400  		c.main.ShowError(errors.New("Fonctionnalité réservé aux séjours normaux."))
   401  		return
   402  	}
   403  	participant := c.Base.NewParticipant(idParticipant.Int64())
   404  	personne, camp := participant.GetPersonne(), participant.GetCamp()
   405  
   406  	hints := camp.HintsAttente(personne.Id, true)
   407  	if hints.Hint() != rd.Inscrit && !c.Onglet.ConfirmeAjoutParticipant(camp, personne.RawData(), hints.Causes()) {
   408  		c.main.ShowStandard("Passage dans les inscrits annulé.", false)
   409  		return
   410  	}
   411  
   412  	raw := participant.RawData()
   413  	fac, HasFac := participant.GetFacture()
   414  	if !HasFac {
   415  		raw.ListeAttente = rd.ListeAttente{}
   416  		c.UpdateParticipant(raw)
   417  		return
   418  	}
   419  
   420  	var newRole = rd.AttenteReponse
   421  	info := "Voulez-vous envoyer un message indiquant qu'une place s'est libérée et demandant une réponse ?"
   422  	if raw.ListeAttente.Statut == rd.AttenteReponse {
   423  		newRole = rd.Inscrit
   424  		info = "Voulez-vous envoyer un message confirmant l'inscription ?"
   425  	}
   426  
   427  	keep, notifie := c.Onglet.ConfirmeNotifPlaceLiberee(info)
   428  	if !keep {
   429  		c.main.ShowStandard("Passage dans les inscrits annulé.", false)
   430  		return
   431  	}
   432  	if !notifie { // simple modification, équivalente à changer le rôle directement
   433  		raw.ListeAttente = rd.ListeAttente{}
   434  		c.UpdateParticipant(raw)
   435  		return
   436  	}
   437  	c.main.Controllers.SuiviDossiers.EnvoiPlaceLiberee(fac, participant.Id, newRole)
   438  }
   439  
   440  func (c *Camps) ExportParticipants(bus rd.Bus, triGroupe, showAttente bool) (filepath string) {
   441  	if c.Etat.CampCurrent == nil {
   442  		c.main.ShowError(errors.New("Aucun camp actuel !"))
   443  		return ""
   444  	}
   445  	camp := c.Base.NewCamp(c.Etat.CampCurrent.Int64())
   446  	inscrits, attente := camp.GetListes(triGroupe, bus, true, false)
   447  	if !showAttente {
   448  		attente = nil
   449  	}
   450  	if len(inscrits)+len(attente) == 0 {
   451  		c.main.ShowError(errors.New("Aucun participant à exporter."))
   452  		return
   453  	}
   454  	filepath, err := table.EnregistreListeParticipants(c.HeaderInscrits, HeadersResponsables, inscrits, attente,
   455  		true, string(camp.RawData().Nom), LocalFolder)
   456  	if err != nil {
   457  		c.main.ShowError(fmt.Errorf("Impossible de générer la liste : %s", err))
   458  		return ""
   459  	}
   460  	c.main.ShowStandard(fmt.Sprintf("Liste des participants générée dans %s", filepath), false)
   461  	return filepath
   462  }
   463  
   464  // ChangeCamp effectue les vérifications puis change le participant courant de camp,
   465  // de manière SYNCHRONE.
   466  func (c *Camps) ChangeCamp(idCamp int64) {
   467  	if c.Etat.CampCurrent == nil || c.Etat.IdParticipant == nil {
   468  		c.main.ShowError(errors.New("Aucun camp courant."))
   469  		return
   470  	}
   471  	isSimple := c.Base.Camps[c.Etat.CampCurrent.Int64()].InscriptionSimple
   472  	if isSimple {
   473  		c.main.ShowError(errors.New("Le camp courant ne supporte pas cette fonction."))
   474  		return
   475  	}
   476  	isTargetSimple := c.Base.Camps[idCamp].InscriptionSimple
   477  	if isTargetSimple {
   478  		c.main.ShowError(errors.New("Le champ choisi propose une inscription simplifiée."))
   479  		return
   480  	}
   481  
   482  	rawPart := c.Base.NewParticipant(c.Etat.IdParticipant.Int64()).RawData()
   483  	rawPart.IdCamp = idCamp // on passe sur le camp cible
   484  	if !c.checkParticipant(rawPart) {
   485  		return
   486  	}
   487  	rawPart.DateHeure = rd.Time(time.Now())
   488  	c.UpdateParticipant(rawPart)
   489  }
   490  
   491  // SelectCamp sélectionne le camp donné
   492  func (c *Camps) SelectCamp(camp int64) {
   493  	c.Etat.CampCurrent = rd.Id(camp)
   494  	if c.Base.Camps[camp].IsTerminated() { // besoin d'afficher les camps terminés
   495  		c.Etat.ShowCampsTerminated = true
   496  	}
   497  	c.Onglet.GrabFocus()
   498  	c.Reset() // le camp courant a changé, besoin des listes, etc..
   499  }
   500  
   501  // SelectParticipant sélectionne le participant donné, en changeant le camp courant
   502  // si nécessaire.
   503  func (c *Camps) SelectParticipant(part dm.Inscrit) {
   504  	camp := part.GetCamp()
   505  	c.Etat.CampCurrent = rd.Id(camp.Id)
   506  	c.Etat.IdParticipant = part.GetId()
   507  	if camp.RawData().IsTerminated() { // besoin d'afficher les camps terminés
   508  		c.Etat.ShowCampsTerminated = true
   509  	}
   510  	c.Onglet.GrabFocus()
   511  	// le camp courant a changé, besoin des listes, etc..
   512  	c.Reset()
   513  	// si l'onglet Camp n'a pas encore été utilisé, la liste ne scroll pas sans ce deuxième appel
   514  	c.Reset()
   515  }
   516  
   517  func (c *Camps) ExportSuiviFinancier(camp int64) string {
   518  	return c.main.Controllers.SuiviCamps.ExportSuiviFinancierParticipants(c.Base.NewCamp(camp))
   519  }
   520  
   521  // NotifieDirecteur envoie un mail (asynchrone)
   522  func (c *MainController) NotifieDirecteur(to, html string) {
   523  	job := func() (interface{}, error) {
   524  		var out string
   525  		err := requete(apiserver.UrlMailsSimple, http.MethodPost, apiserver.ParamsSimpleMail{
   526  			Html: html, To: to, Subject: "[ACVE] Message privé",
   527  		}, &out)
   528  		return out, err
   529  	}
   530  	onSuccess := func(_out interface{}) {
   531  		out := _out.(string)
   532  		c.ShowStandard(out, false)
   533  	}
   534  	c.ShowStandard("Notification du directeur...", true)
   535  	c.Background.Run(job, onSuccess)
   536  }
   537  
   538  type DiagnosticListeAttente struct {
   539  	Participant dm.AccesParticipant
   540  	Hint        rd.StatutAttente
   541  }
   542  
   543  // CheckListeAttentes compare les listes d'attentes
   544  // réelles avec celles calculées automatiquement et émet un diagnostic
   545  // en camp de différence :
   546  //	- optionnel, pour les participants déjà en liste principal
   547  //	- impératif, pour les participants encore en liste d'attente
   548  func (c *Camps) CheckListeAttentes() (inscrits, attentes []DiagnosticListeAttente) {
   549  	_, cache := c.Base.ResoudParticipants()
   550  	for idParticipant, part := range c.Base.Participants {
   551  		acPart := c.Base.NewParticipant(idParticipant)
   552  		hint := acPart.HintsAttente(cache).Hint()
   553  		actuel := part.ListeAttente.Statut
   554  		if actuel == rd.AttenteReponse { // situation temporaire, on ne dit rien
   555  			continue
   556  		}
   557  		if hint == actuel { // tout va bien
   558  			continue
   559  		}
   560  		if part.ListeAttente.Raison != "" {
   561  			// on ignore les participants avec une raison spéciale
   562  			continue
   563  		}
   564  		diag := DiagnosticListeAttente{Participant: acPart, Hint: hint}
   565  		if actuel == rd.Inscrit {
   566  			inscrits = append(inscrits, diag)
   567  		} else {
   568  			attentes = append(attentes, diag)
   569  		}
   570  	}
   571  	sortDiags := func(l []DiagnosticListeAttente) {
   572  		sort.Slice(l, func(i, j int) bool {
   573  			return l[i].Participant.Id < l[j].Participant.Id
   574  		})
   575  		sort.SliceStable(l, func(i, j int) bool {
   576  			return l[i].Participant.GetCamp().Id < l[j].Participant.GetCamp().Id
   577  		})
   578  	}
   579  	sortDiags(inscrits)
   580  	sortDiags(attentes)
   581  	return
   582  }
   583  
   584  // DocumentCamp est soit `DynamicDocument` soit `RegisteredDocument`
   585  type DocumentCamp interface {
   586  	FileName() string
   587  	isDocCamp()
   588  }
   589  
   590  func (DynamicDocument) isDocCamp()    {}
   591  func (RegisteredDocument) isDocCamp() {}
   592  
   593  type DynamicDocument struct {
   594  	documents.DynamicDocument
   595  }
   596  
   597  type RegisteredDocument rd.Document
   598  
   599  func (doc RegisteredDocument) FileName() string { return doc.NomClient.String() }
   600  
   601  // GetDocuments renvoie les metas de chaque document disponible.
   602  // Si plusieurs lettres sont présentes dans la base, elles sont toutes renvoyées.
   603  // Renvois systématiquement les pièces jointes additionnelles.
   604  // L'attribut `Locked` est ignoré
   605  func GetDocuments(ac dm.AccesCamp) (out []DocumentCamp) {
   606  	lp := documents.NewListeParticipants(ac)
   607  	lv := documents.NewListeVetements(ac.RawData())
   608  
   609  	if len(lp.Liste) > 0 {
   610  		out = append(out, DynamicDocument{DynamicDocument: lp})
   611  	}
   612  	if len(lv.Liste) > 0 {
   613  		out = append(out, DynamicDocument{DynamicDocument: lv})
   614  	}
   615  	for _, doc := range ac.GetRegisteredDocuments(true, true) {
   616  		out = append(out, RegisteredDocument(doc))
   617  	}
   618  	return out
   619  }