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

     1  package controllers
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"path"
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/benoitkugler/goACVE/logs"
    13  
    14  	"github.com/benoitkugler/goACVE/server/core/documents"
    15  
    16  	"github.com/benoitkugler/goACVE/server/core/utils/joomeo"
    17  	"github.com/benoitkugler/goACVE/server/core/utils/table"
    18  
    19  	"github.com/benoitkugler/goACVE/server/core/apiserver"
    20  	dm "github.com/benoitkugler/goACVE/server/core/datamodel"
    21  	rd "github.com/benoitkugler/goACVE/server/core/rawdata"
    22  )
    23  
    24  const (
    25  	EndpointInscription = "/inscription/"
    26  
    27  	complementDefaut = "<i>Indispensable:</i> tout linge doit être <b>marqué</b> (étiquette cousue ou stylo indélébile)."
    28  )
    29  
    30  const (
    31  	bfNom rd.Field = iota
    32  	bfDirecteur
    33  	bfLieu
    34  	bfDuree
    35  	bfAnimation
    36  	bfTechnique
    37  	bfInscrits
    38  	bfPrixBase
    39  	bfRemisesSpeciales
    40  	bfRemisesPourcent
    41  	bfTotalAides
    42  	bfTotalDemande
    43  	bfTotal
    44  )
    45  
    46  var (
    47  	HeadersBilanFinancier = []rd.Header{
    48  		{Field: bfNom, Label: "Camp"},
    49  		{Field: bfDirecteur, Label: "Directeur"},
    50  		{Field: bfLieu, Label: "Lieu"},
    51  		{Field: bfDuree, Label: "Durée (jours)"},
    52  		{Field: bfAnimation, Label: "Animateurs"},
    53  		{Field: bfTechnique, Label: "Equipe technique"},
    54  		{Field: bfInscrits, Label: "Inscrits"},
    55  		{Field: bfPrixBase, Label: "Prix (sans option)"},
    56  		{Field: bfRemisesSpeciales, Label: "Réductions spéciales"},
    57  		{Field: bfRemisesPourcent, Label: "Réductions équipiers / enfants"},
    58  		{Field: bfTotalAides, Label: "Aides extérieures (CAFs, CE, etc...)"},
    59  		{Field: bfTotalDemande, Label: "Montant demandé"},
    60  		{Field: bfTotal, Label: "Budget total"},
    61  	}
    62  )
    63  
    64  type ButtonsSuiviCamps struct {
    65  	Creer, Supprimer, AfficherJoomeo, BilanFinancier, LienSondages EtatSideButton
    66  }
    67  
    68  type OngletSuiviCamps interface {
    69  	baseOnglet
    70  
    71  	ConfirmeAjoutDirecteurJoomeo(msg string) rd.OptionnalBool // 0 : annule, Non : Confirme,  Oui: Confirme + mail
    72  	ConfirmeSetupAlbumJoomeo(alreadyLinked []string) bool
    73  	ConfirmeSupprimeCamp(camp dm.AccesCamp) bool
    74  	ProposePasswordDirecteur(camp dm.AccesCamp, password string) bool
    75  }
    76  
    77  type EtatSuiviCamps struct {
    78  	CampCurrent         rd.IId
    79  	CritereIgnoreClosed bool             // seulement camps ouverts
    80  	CritereTerminated   rd.OptionnalBool // non terminés, les deux, terminés)
    81  }
    82  
    83  // SuiviCamps implémente une gestion des camps comme administrateur.
    84  type SuiviCamps struct {
    85  	Onglet    OngletSuiviCamps
    86  	main      *MainController
    87  	Liste     rd.Table
    88  	Header    []rd.Header
    89  	Base      *dm.BaseLocale
    90  	Etat      EtatSuiviCamps
    91  	apiJoomeo *joomeo.ApiJoomeo
    92  	EditRight bool // Droit d'édition/ajout/suppression de camps
    93  }
    94  
    95  func NewSuiviCamps(main *MainController, permission int) *SuiviCamps {
    96  	s := SuiviCamps{main: main, Base: main.Base, EditRight: permission >= 2,
    97  		Header: []rd.Header{
    98  			{Field: dm.CampNom, Label: "Nom"},
    99  			{Field: dm.CampAnnee, Label: "Année"},
   100  			{Field: dm.CampPeriode, Label: "Période"},
   101  			{Field: dm.CampLieu, Label: "Lieu"},
   102  			{Field: dm.CampRemplissage, Label: "Remplissage"},
   103  			{Field: dm.CampOuvert, Label: "Inscriptions ouvertes"},
   104  			{Field: dm.CampIdJs, Label: "N° Jeunesse et Sport"},
   105  			{Field: dm.CampJoomeoAlbumId, Label: "Identifiant Joomeo"},
   106  		}}
   107  	s.resetData()
   108  	return &s
   109  }
   110  
   111  func (c *SuiviCamps) resetData() {
   112  	c.Liste = make(rd.Table, 0, len(c.Base.Camps))
   113  	_, cache1 := c.Base.ResoudParticipants()
   114  	cache2 := c.Base.ResoudParticipantsimples()
   115  	for _, camp := range c.Base.GetCamps(c.Etat.CritereIgnoreClosed, c.Etat.CritereTerminated) {
   116  		c.Liste = append(c.Liste, camp.AsItem(cache1, cache2))
   117  	}
   118  
   119  	if c.Etat.CampCurrent != nil && !HasId(c.Liste, c.Etat.CampCurrent) {
   120  		c.Etat.CampCurrent = nil
   121  	}
   122  }
   123  
   124  func (c *SuiviCamps) SideButtons() ButtonsSuiviCamps {
   125  	bs := ButtonsSuiviCamps{}
   126  	EtatButton := ButtonPresent
   127  	if c.Etat.CampCurrent != nil {
   128  		EtatButton = ButtonActivated
   129  	}
   130  	if c.EditRight {
   131  		bs.Creer = ButtonActivated
   132  		bs.Supprimer = EtatButton
   133  		bs.AfficherJoomeo = EtatButton
   134  	}
   135  	bs.BilanFinancier = ButtonActivated
   136  	bs.LienSondages = ButtonActivated
   137  	return bs
   138  }
   139  
   140  func bilanFinancierCamp(camp dm.AccesCamp, cache1 dm.LiensCampEquipiers,
   141  	cache2 dm.LiensCampParticipants, cache3 dm.LiensCampParticipantsimples,
   142  	cacheAide dm.LiensParticipantAides) rd.Item {
   143  	nomDirecteur := ""
   144  	if dir, has := camp.GetDirecteur(); has {
   145  		nomDirecteur = dir.RawData().NomPrenom().String()
   146  	}
   147  	equipe, nbAnim := camp.GetEquipe(cache1), 0
   148  	for _, part := range equipe {
   149  		if part.RawData().Roles.IsAuPair() {
   150  			nbAnim += 1
   151  		}
   152  	}
   153  	rawCamp := camp.RawData()
   154  	remisesPourcent := make(map[rd.Pourcent]int)
   155  	var remisesSpeciales, totalAides, totalDemande rd.Euros
   156  	for _, part := range camp.GetInscrits(cache2) {
   157  		detailsPrix := part.EtatFinancier(cacheAide, false)
   158  		rem := part.RawData().Remises
   159  		remisesSpeciales += rem.ReducSpeciale
   160  		remPourcent := rem.Pourcent()
   161  		if remPourcent > 0 {
   162  			remisesPourcent[remPourcent] += 1
   163  		}
   164  		totalAides += detailsPrix.TotalAides()
   165  		totalDemande += detailsPrix.PrixNet()
   166  	}
   167  	var remsStr []string
   168  	for rem, nb := range remisesPourcent {
   169  		remsStr = append(remsStr, fmt.Sprintf("de %d %%: %d", rem, nb))
   170  	}
   171  	fields := rd.F{
   172  		bfNom:              rawCamp.Nom,
   173  		bfLieu:             rawCamp.Lieu,
   174  		bfDirecteur:        rd.String(nomDirecteur),
   175  		bfDuree:            rd.Int(rawCamp.Duree()),
   176  		bfInscrits:         rd.Int(camp.GetNbInscrits(cache2, cache3)),
   177  		bfAnimation:        rd.Int(nbAnim),
   178  		bfTechnique:        rd.Int(len(equipe) - nbAnim),
   179  		bfPrixBase:         rawCamp.Prix,
   180  		bfRemisesSpeciales: remisesSpeciales,
   181  		bfTotalAides:       totalAides,
   182  		bfTotalDemande:     totalDemande,
   183  		bfTotal:            totalDemande + totalAides,
   184  		bfRemisesPourcent:  rd.String(strings.Join(remsStr, "; ")),
   185  	}
   186  	bolds := rd.B{bfNom: true, bfDirecteur: true, bfLieu: true}
   187  	colors := rd.MapColors{bfNom: rd.HexColor("#D9D9D9"), bfDirecteur: rd.HexColor("#D9D9D9"), bfLieu: rd.HexColor("#D9D9D9")}
   188  	return rd.Item{Fields: fields, Bolds: bolds, BackgroundColors: colors}
   189  }
   190  
   191  func (c *SuiviCamps) BilansFinanciers(camps []int64) map[int64]rd.Item {
   192  	out := make(map[int64]rd.Item, len(camps))
   193  	cacheAide := c.Base.ResoudAides()
   194  	cache1 := c.Base.ResoudEquipiers()
   195  	_, cache2 := c.Base.ResoudParticipants()
   196  	cache3 := c.Base.ResoudParticipantsimples()
   197  	for _, campId := range camps {
   198  		camp := c.Base.NewCamp(campId)
   199  		bilan := bilanFinancierCamp(camp, cache1, cache2, cache3, cacheAide)
   200  		out[camp.Id] = bilan
   201  	}
   202  	return out
   203  }
   204  
   205  // CreeCamp ajoute le camp sur le serveur.
   206  // Le champ complément de la liste de vêtements est
   207  // pré-rempli.
   208  func (c *SuiviCamps) CreeCamp(params apiserver.CreateCampIn) {
   209  	// défaut, en respectant un éventuel import
   210  	params.Camp.Envois.Locked = true
   211  	params.Camp.Envois.LettreDirecteur = true
   212  	if params.Camp.ListeVetements.Complement != "" {
   213  		params.Camp.ListeVetements.Complement = complementDefaut
   214  	}
   215  
   216  	job := func() (interface{}, error) {
   217  		var out rd.Camp
   218  		err := requete(apiserver.UrlCamps, http.MethodPut, params, &out)
   219  		return out, err
   220  	}
   221  	onSuccess := func(_out interface{}) {
   222  		out := _out.(rd.Camp)
   223  		c.Base.Camps[out.Id] = out
   224  		c.main.ShowStandard(fmt.Sprintf("Camp %s créé avec succès.", out.Label()), false)
   225  		c.Reset()
   226  	}
   227  
   228  	c.main.ShowStandard("Création du camp...", true)
   229  	c.main.Background.Run(job, onSuccess)
   230  }
   231  
   232  func (c *SuiviCamps) UpdateCamp(camp rd.Camp) {
   233  	c.main.Controllers.Camps.UpdateCamp(camp)
   234  }
   235  
   236  // ImportCamp copie les champs pertinents pour
   237  // un nouveau camp, à partir du camp courant
   238  func (c *SuiviCamps) ImportCampFrom() apiserver.CreateCampIn {
   239  	if c.Etat.CampCurrent == nil {
   240  		return apiserver.CreateCampIn{}
   241  	}
   242  	oldCamp := c.Base.Camps[c.Etat.CampCurrent.Int64()] // copie
   243  	oldCamp.Id, oldCamp.Ouvert = -1, false
   244  	oldCamp.NumeroJS, oldCamp.JoomeoAlbumId = "", ""
   245  
   246  	return apiserver.CreateCampIn{Camp: oldCamp, CopyLettreFrom: rd.NewOptionnalId(c.Etat.CampCurrent.Int64())}
   247  }
   248  
   249  // AjouteDirecteur inscrit une personne avec le rôle de directeur
   250  func (c *SuiviCamps) AjouteDirecteur(camp dm.AccesCamp, idDirecteur int64) {
   251  	if dir, has := camp.GetDirecteur(); has {
   252  		c.main.ShowError(fmt.Errorf("Le camp %s possède déjà un directeur (<i>%s</i>). \n Veuillez d'abord le supprimer.",
   253  			camp.RawData().Label(), dir.RawData().NomPrenom()))
   254  		return
   255  	}
   256  	part := rd.Equipier{Roles: rd.RDirecteur.AsRoles(), IdPersonne: idDirecteur, IdCamp: camp.Id}
   257  	jobLaunched := c.main.Controllers.Equipiers.CreateEquipier(part)
   258  	if !jobLaunched {
   259  		return
   260  	}
   261  	// on propose d'utiliser un mot de passe lié au directeur
   262  	// afin de lui simplifier la vie
   263  	proposition := logs.PasswordDir.Password(idDirecteur)
   264  	changePass := c.Onglet.ProposePasswordDirecteur(camp, proposition)
   265  	if !changePass {
   266  		return
   267  	}
   268  	newCamp := camp.RawData()
   269  	newCamp.Password = rd.String(proposition)
   270  	c.UpdateCamp(newCamp)
   271  }
   272  
   273  func (c *SuiviCamps) ExportBilanCamps(bilan map[int64]rd.Item) string {
   274  	liste := make(rd.Table, 0, len(bilan))
   275  	for _, camp := range bilan {
   276  		liste = append(liste, camp)
   277  	}
   278  	filePath, err := table.EnregistreBilanCamps(HeadersBilanFinancier, liste, LocalFolder)
   279  	if err != nil {
   280  		c.main.ShowError(fmt.Errorf("Impossible d'enregistrer le bilan : %s", err))
   281  		return ""
   282  	}
   283  	c.main.ShowStandard(fmt.Sprintf("Bilan exporté : %s", filePath), false)
   284  	return filePath
   285  }
   286  
   287  // LienInscription renvoie un lien pré-selectionnant le camp
   288  // sur le formulaire d'inscription.
   289  func (c *SuiviCamps) LienInscription(idCamp int64) string {
   290  	idCampS := strconv.FormatInt(idCamp, 10)
   291  	lien := Server.EndpointURL(path.Join(EndpointInscription, idCampS))
   292  	c.main.ShowStandard("Lien : "+lien, false)
   293  	return lien
   294  }
   295  
   296  // ExportSuiviFinancierParticipants génère et enregistre une liste des participants / état facture
   297  func (c *SuiviCamps) ExportSuiviFinancierParticipants(camp dm.AccesCamp) string {
   298  	parts, totalDemande, totalAides := c.Base.SuiviFinancier(camp)
   299  	filePath, err := table.EnregistreSuiviFinancierCamp(documents.HeadersSuiviParticipants, parts, totalDemande,
   300  		totalAides, camp.RawData().Label().String(), LocalFolder)
   301  	if err != nil {
   302  		c.main.ShowError(fmt.Errorf("Impossible d'enregistrer l'export : %s", err))
   303  		return ""
   304  	}
   305  	c.main.ShowStandard(fmt.Sprintf("Suivi financier exporté : %s", filePath), false)
   306  	return filePath
   307  }
   308  
   309  // ---------------------------------------------------------------------------
   310  // ---------------------------------- Joomeo ---------------------------------
   311  // ---------------------------------------------------------------------------
   312  
   313  func (c *SuiviCamps) getJoomeoApi() (*joomeo.ApiJoomeo, error) {
   314  	if c.apiJoomeo == nil {
   315  		apiJoomeo, err := joomeo.InitApi(c.main.logsJoomeo)
   316  		if err != nil {
   317  			return nil, err
   318  		}
   319  		c.apiJoomeo = apiJoomeo
   320  	}
   321  	return c.apiJoomeo, nil
   322  }
   323  
   324  // CloseJoomeoApi termine la connexion.
   325  func (c *SuiviCamps) CloseJoomeoApi() {
   326  	if c.apiJoomeo != nil {
   327  		c.apiJoomeo.Kill()
   328  		c.apiJoomeo = nil
   329  	}
   330  }
   331  
   332  // GetAlbumsContactsJoomeo renvoie les albums et contacts Joomeo
   333  func (c *SuiviCamps) GetAlbumsContactsJoomeo() ([]rd.ItemChilds, map[string]joomeo.Album, map[string]joomeo.Contact) {
   334  	api, err := c.getJoomeoApi()
   335  	if err != nil {
   336  		c.main.ShowError(err)
   337  		return nil, nil, nil
   338  	}
   339  	folders, albums, contacts, err := api.GetAlbumsContacts()
   340  	if err != nil {
   341  		c.main.ShowError(err)
   342  		return nil, nil, nil
   343  	}
   344  	out := make([]rd.ItemChilds, 0, len(folders))
   345  	for _, fol := range folders {
   346  		out = append(out, fol.AsItem())
   347  	}
   348  	sort.Slice(out, func(i, j int) bool {
   349  		return out[i].Fields.Data(joomeo.FLabel).Sortable() < out[j].Fields.Data(joomeo.FLabel).Sortable()
   350  	})
   351  	return out, albums, contacts
   352  }
   353  
   354  // SetupAlbumJoomeo attribue l'album donné au camp courant,
   355  // après vérification. Synchrone
   356  func (c *SuiviCamps) SetupAlbumJoomeo(albumId string) {
   357  	if c.Etat.CampCurrent == nil {
   358  		c.main.ShowError(errors.New("Aucun camp courant !"))
   359  		return
   360  	}
   361  	var already []string
   362  	for _, camp := range c.Base.Camps {
   363  		if string(camp.JoomeoAlbumId) == albumId {
   364  			already = append(already, camp.Label().String())
   365  		}
   366  	}
   367  	if len(already) > 0 {
   368  		if !c.Onglet.ConfirmeSetupAlbumJoomeo(already) {
   369  			return
   370  		}
   371  	}
   372  	current := c.Base.Camps[c.Etat.CampCurrent.Int64()]
   373  	current.JoomeoAlbumId = rd.String(albumId)
   374  	c.main.ShowStandard("Ajout de l'album Joomeo...", true)
   375  	out, err := updateCamp(current)
   376  	if err != nil {
   377  		c.main.ShowError(err)
   378  		return
   379  	}
   380  	c.main.ShowStandard("Album Joomeo ajouté.", false)
   381  	c.Base.Camps[out.Id] = out
   382  	c.Reset()
   383  }
   384  
   385  func (c *SuiviCamps) RemoveAlbumJoomeo() {
   386  	if c.Etat.CampCurrent == nil {
   387  		c.main.ShowError(errors.New("Aucun camp courant !"))
   388  		return
   389  	}
   390  	current := c.Base.Camps[c.Etat.CampCurrent.Int64()]
   391  	current.JoomeoAlbumId = ""
   392  	c.main.ShowStandard("Retrait de l'album Joomeo...", true)
   393  	out, err := updateCamp(current)
   394  	if err != nil {
   395  		c.main.ShowError(err)
   396  		return
   397  	}
   398  	c.main.ShowStandard("Album Joomeo retiré.", false)
   399  	c.Base.Camps[out.Id] = out
   400  	c.Reset()
   401  }
   402  
   403  // AjouteDirecteurJoomeo ajoute le directeur du camp courant comme contact Joomeo
   404  func (c *SuiviCamps) AjouteDirecteurJoomeo(albums map[string]joomeo.Album) {
   405  	if c.Etat.CampCurrent == nil {
   406  		c.main.ShowError(errors.New("Aucun camp courant !"))
   407  		return
   408  	}
   409  	camp := c.Base.NewCamp(c.Etat.CampCurrent.Int64())
   410  	rawCamp := camp.RawData()
   411  	directeur, has := camp.GetDirecteur()
   412  	if !has {
   413  		c.main.ShowError(fmt.Errorf("<i>Aucun directeur</i> n'est enregistré sur le séjour %s!", rawCamp.Label()))
   414  		return
   415  	}
   416  	rawDirecteur := directeur.RawData()
   417  	albumid := string(rawCamp.JoomeoAlbumId)
   418  	if albumid == "" {
   419  		c.main.ShowError(fmt.Errorf("Aucun album Joomeo n'est associé au %s !", rawCamp.Label()))
   420  		return
   421  	}
   422  	msg := fmt.Sprintf("Confirmer l'ajout de <b>%s</b> comme uploader sur l'album <i>%s</i> ?",
   423  		rawDirecteur.Mail, albums[albumid].Label)
   424  	rep := c.Onglet.ConfirmeAjoutDirecteurJoomeo(msg)
   425  	if rep == 0 {
   426  		c.main.ShowStandard("Ajout annulé", false)
   427  		return
   428  	}
   429  	envoi_mail := rep == rd.OBOui
   430  
   431  	api, err := c.getJoomeoApi()
   432  	if err != nil {
   433  		c.main.ShowError(err)
   434  		return
   435  	}
   436  	contact, err := api.AjouteDirecteur(albumid, string(rawDirecteur.Mail), envoi_mail)
   437  	if err != nil {
   438  		c.main.ShowError(err)
   439  		return
   440  	}
   441  	c.main.ShowStandard(fmt.Sprintf("Directeur ajouté. Login : %s, password : %s",
   442  		contact.Login, contact.Password), false)
   443  
   444  	if err = api.EleveSuperContact(contact.ContactId); err != nil {
   445  		c.main.ShowError(fmt.Errorf("Le directeur a bien été ajouté comme <b>contact</b>. "+
   446  			"En revanche, l'attribution du <b>droit de suppression</b> a échoué. <br/> <i>Détails : %s</i>", err))
   447  		return
   448  	}
   449  	c.main.ShowStandard("Le directeur a maintenant le droit de suppression des fichiers pour son album.", false)
   450  }
   451  
   452  // ------------------------------ suppression sécurisée d'un camp ------------------------------
   453  
   454  // renvoie `nil` si le camp peut être supprimé,
   455  // en considérant les tables participants, equipiers, participants simples
   456  func (c *SuiviCamps) checkSupprimeCamp(idCamp int64) error {
   457  	for id := range c.Base.Participants {
   458  		if ac := c.Base.NewParticipant(id); ac.GetCamp().Id == idCamp {
   459  			return fmt.Errorf("Le participant %s est lié au camp à supprimer.", ac.GetPersonne().RawData().NomPrenom())
   460  		}
   461  	}
   462  	for id := range c.Base.Participantsimples {
   463  		if ac := c.Base.NewParticipantsimple(id); ac.GetCamp().Id == idCamp {
   464  			return fmt.Errorf("Le participant %s est lié au camp à supprimer.", ac.GetPersonne().RawData().NomPrenom())
   465  		}
   466  	}
   467  	for id := range c.Base.Equipiers {
   468  		if ac := c.Base.NewEquipier(id); ac.GetCamp().Id == idCamp {
   469  			return fmt.Errorf("L'équipier %s est lié au camp à supprimer.", ac.GetPersonne().RawData().NomPrenom())
   470  		}
   471  	}
   472  	return nil
   473  }
   474  
   475  // SupprimeCamp vérifie que le camp courant n'est pas utilisé et le supprime.
   476  func (c *SuiviCamps) SupprimeCamp() {
   477  	camp := c.Etat.CampCurrent
   478  	if camp == nil {
   479  		return // ne devrait pas arriver si le GUI est correct
   480  	}
   481  	err := c.checkSupprimeCamp(camp.Int64())
   482  	if err != nil {
   483  		c.main.ShowError(err)
   484  		return
   485  	}
   486  	if !c.Onglet.ConfirmeSupprimeCamp(c.Base.NewCamp(camp.Int64())) {
   487  		c.main.ShowStandard("Suppression annulée", false)
   488  		return
   489  	}
   490  
   491  	job := func() (interface{}, error) {
   492  		var out apiserver.DeleteCampOut
   493  		err = requete(apiserver.UrlCamps, http.MethodDelete, IdAsParams(camp.Int64()), &out)
   494  		return out, err
   495  	}
   496  	onSuccess := func(_out interface{}) {
   497  		out := _out.(apiserver.DeleteCampOut)
   498  		delete(c.Base.Camps, camp.Int64())
   499  		for _, idDoc := range out.IdsDocuments {
   500  			delete(c.Base.Documents, idDoc)
   501  			delete(c.Base.DocumentCamps, idDoc)
   502  		}
   503  		for _, idGroupe := range out.IdsGroupes {
   504  			delete(c.Base.Groupes, idGroupe)
   505  			delete(c.Base.GroupeContraintes, idGroupe)
   506  		}
   507  		c.Etat.CampCurrent = nil
   508  		c.main.ShowStandard("Camp supprimé avec succès.", false)
   509  		c.Reset()
   510  	}
   511  
   512  	c.main.ShowStandard("Suppression du camp...", true)
   513  	c.main.Background.Run(job, onSuccess)
   514  }
   515  
   516  // CalculeNuitees renvoie le nombre de nuitées pour le séjour donné,
   517  // en séparant les adultes des enfants.
   518  // Un séjour proposant une option journée renvoie `0` enfants
   519  func (c SuiviCamps) CalculeNuitees(camp dm.AccesCamp) (nbEnfants, nbAdultes int) {
   520  	var all []interface {
   521  		GetPresence() rd.Plage
   522  		AgeDebutCamp() rd.AgeDate
   523  	}
   524  	for _, part := range camp.GetEquipe(nil) {
   525  		all = append(all, part)
   526  	}
   527  	if camp.RawData().OptionPrix.Active != rd.OptionsPrix.JOUR {
   528  		// dans ce cas, il n'y a pas de nuitées
   529  		for _, part := range camp.GetInscrits(nil) {
   530  			all = append(all, part)
   531  		}
   532  	}
   533  	for _, part := range camp.GetParticipantsimples(nil) {
   534  		all = append(all, part)
   535  	}
   536  
   537  	for _, part := range all {
   538  		days := part.GetPresence().NbJours()
   539  		nuitees := 0
   540  		if days >= 1 {
   541  			nuitees = days - 1 // car days inclut le départ et l'arrivée
   542  		}
   543  		age := part.AgeDebutCamp().Age()
   544  		if age >= 13 {
   545  			nbAdultes += nuitees
   546  		} else {
   547  			nbEnfants += nuitees
   548  		}
   549  	}
   550  	return
   551  }
   552  
   553  func (c *SuiviCamps) SelectFacturesByCamp(idCamp int64) rd.Ids {
   554  	var filtredIds rd.Ids
   555  	cache, _ := c.Base.ResoudParticipants()
   556  	for _, fac := range c.Base.Factures {
   557  		camps, _ := c.Base.NewFacture(fac.Id).Camps(cache)
   558  		if camps[idCamp] {
   559  			filtredIds = append(filtredIds, fac.Id)
   560  		}
   561  	}
   562  	return filtredIds
   563  }
   564  
   565  func (s *SuiviCamps) UrlSondages() string {
   566  	endpoint := "/sondages/" + logs.KeyAdminSondages
   567  	return Server.EndpointURL(endpoint)
   568  }