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 }