github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/directeurs/animateurs.go (about) 1 package directeurs 2 3 import ( 4 "fmt" 5 "sort" 6 7 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 8 "github.com/benoitkugler/goACVE/server/core/rawdata/composites" 9 "github.com/benoitkugler/goACVE/server/shared" 10 ) 11 12 // ------------------------------------ Animateurs de références ------------------------------------ 13 14 func (rc DriverCampComplet) loadGroupeAnimateurs(idGroupe int64) (out LoadGroupeAnimateursOut, err error) { 15 out.GroupeEquipiers, err = rd.SelectGroupeEquipiersByIdGroupes(rc.DB, idGroupe) 16 if err != nil { 17 return out, err 18 } 19 out.ParticipantEquipiers, err = rd.SelectParticipantEquipiersByIdGroupes(rc.DB, idGroupe) 20 return out, err 21 } 22 23 func (rc DriverCampComplet) addGroupeAnimateurs(params AddGroupeAnimateursIn) error { 24 if err := rc.checkGroupe(params.IdGroupe); err != nil { 25 return err 26 } 27 ges := make(rd.GroupeEquipiers, len(params.IdEquipiers)) 28 for i, idEquipier := range params.IdEquipiers { // on complète les champs évidents 29 ges[i] = rd.GroupeEquipier{ 30 IdCamp: rc.camp.Id, 31 IdGroupe: params.IdGroupe, 32 IdEquipier: idEquipier, 33 } 34 } 35 tx, err := rc.DB.Begin() 36 if err != nil { 37 return err 38 } 39 err = rd.InsertManyGroupeEquipiers(tx, ges...) 40 if err != nil { 41 return shared.Rollback(tx, err) 42 } 43 err = tx.Commit() 44 return err 45 } 46 47 func (rc DriverCampComplet) deleteGroupeAnimateur(params DeleteGroupeAnimateurIn) error { 48 if err := rc.checkGroupe(params.IdGroupe); err != nil { 49 return err 50 } 51 lien := rd.GroupeEquipier{ 52 IdCamp: rc.camp.Id, 53 IdEquipier: params.IdEquipier, 54 IdGroupe: params.IdGroupe, 55 } 56 err := lien.Delete(rc.DB) 57 return err 58 } 59 60 func (rc DriverCampComplet) updateInscritsAnimateur(params UpdateInscritsAnimateurIn) (rd.ParticipantEquipiers, error) { 61 if err := rc.checkGroupe(params.IdGroupe); err != nil { 62 return nil, err 63 } 64 tx, err := rc.DB.Begin() 65 if err != nil { 66 return nil, err 67 } 68 // on supprime les animateurs éventuels 69 _, err = rd.DeleteParticipantEquipiersByIdParticipants(tx, params.IdInscrits...) 70 if err != nil { 71 return nil, shared.Rollback(tx, err) 72 } 73 if params.IdAnimateur.IsNotNil() { // on attribue un nouveau 74 liens := make(rd.ParticipantEquipiers, len(params.IdInscrits)) 75 for i, idInscrit := range params.IdInscrits { 76 liens[i] = rd.ParticipantEquipier{ 77 IdEquipier: params.IdAnimateur.Int64, 78 IdParticipant: idInscrit, 79 IdGroupe: params.IdGroupe, 80 } 81 } 82 err = rd.InsertManyParticipantEquipiers(tx, liens...) 83 if err != nil { 84 return nil, shared.Rollback(tx, err) 85 } 86 } 87 out, err := rd.SelectParticipantEquipiersByIdGroupes(tx, params.IdGroupe) 88 if err != nil { 89 return nil, shared.Rollback(tx, err) 90 } 91 err = tx.Commit() 92 return out, err 93 } 94 95 // anims ne doit pas être vide 96 func computeAffectations(affectationCourantes rd.ParticipantEquipiers, 97 inscrits composites.ParticipantPersonnes, anims composites.EquipierPersonnes, idGroupe int64) rd.ParticipantEquipiers { 98 animToInsc := affectationCourantes.ByIdEquipier() // déjà affectés 99 100 total := len(inscrits) // inscrits à répartir 101 for _, anim := range anims { // inscrits déjà existants 102 total += len(animToInsc[anim.Equipier.Id]) 103 } 104 // le nombre par groupe est d'environ (arrondi à l'inférieur) 105 targetSize := total / len(anims) 106 restant := total - targetSize*len(anims) 107 108 // on répartit par anim 109 var out rd.ParticipantEquipiers 110 inscritsToAffect := inscrits 111 for _, anim := range anims { 112 if len(inscritsToAffect) == 0 { 113 break // plus rien à faire 114 } 115 tailleCourante := len(animToInsc[anim.Equipier.Id]) 116 nbToAffect := targetSize - tailleCourante 117 if restant > 0 { 118 // on ajoute un inscrit en plus pour compenser l'erreur d'arrondi 119 nbToAffect++ 120 restant-- 121 } 122 if nbToAffect <= 0 { 123 // on dépasse la taille recommandée -> pas d'affectation 124 continue 125 } 126 if nbToAffect > len(inscritsToAffect) { 127 nbToAffect = len(inscritsToAffect) 128 } 129 chunk := inscritsToAffect[0:nbToAffect] // extrait les inscrits 130 inscritsToAffect = inscritsToAffect[nbToAffect:] 131 for _, inscrit := range chunk { 132 affectation := rd.ParticipantEquipier{IdEquipier: anim.Equipier.Id, IdParticipant: inscrit.Participant.Id, IdGroupe: idGroupe} 133 out = append(out, affectation) 134 } 135 } 136 137 return out 138 } 139 140 func (rc DriverCampComplet) autoRepartitionAnimateurs(idGroupe int64) (rd.ParticipantEquipiers, error) { 141 data, err := rc.loadGroupeAnimateurs(idGroupe) 142 if err != nil { 143 return nil, err 144 } 145 // on récupère les participants du groupe 146 rows, err := rc.DB.Query(`SELECT participants.*, personnes.* FROM participants 147 JOIN personnes ON personnes.id = participants.id_personne 148 JOIN groupe_participants ON groupe_participants.id_participant = participants.id 149 WHERE groupe_participants.id_groupe = $1`, idGroupe) 150 if err != nil { 151 return nil, err 152 } 153 participants, err := composites.ScanParticipantPersonnes(rows) 154 if err != nil { 155 return nil, err 156 } 157 // on récupère les équipiers 158 rows, err = rc.DB.Query(`SELECT equipiers.*, personnes.* FROM equipiers 159 JOIN personnes ON personnes.id = equipiers.id_personne 160 JOIN groupe_equipiers ON groupe_equipiers.id_equipier = equipiers.id 161 WHERE groupe_equipiers.id_groupe = $1`, idGroupe) 162 if err != nil { 163 return nil, err 164 } 165 equipiers, err := composites.ScanEquipierPersonnes(rows) 166 if err != nil { 167 return nil, err 168 } 169 170 insctToAnim := data.ParticipantEquipiers.ByIdParticipant() 171 172 // on commence par séparer F/G : participants ... 173 var filles, gars composites.ParticipantPersonnes 174 for _, part := range participants { 175 if _, affected := insctToAnim[part.Participant.Id]; affected { 176 continue 177 } 178 if !part.ListeAttente.IsInscrit() { 179 continue // les participants en liste d'attente sont ignorés 180 } 181 if part.Sexe == rd.SFemme { 182 filles = append(filles, part) 183 } else { 184 gars = append(gars, part) 185 } 186 187 } 188 // ... et animateurs 189 var fillesAnim, garsAnim composites.EquipierPersonnes 190 for _, anim := range equipiers { 191 if anim.Sexe == rd.SFemme { 192 fillesAnim = append(fillesAnim, anim) 193 } else { 194 garsAnim = append(garsAnim, anim) 195 } 196 } 197 // on gère les cas particuliers 198 if len(equipiers) == 0 { 199 return nil, fmt.Errorf("Aucun animateur n'est déclaré.") 200 } 201 if len(garsAnim) == 0 { 202 if len(fillesAnim) >= 2 { // on peut partager 203 cutoff := len(fillesAnim) / 2 204 garsAnim = fillesAnim[0:cutoff] 205 fillesAnim = fillesAnim[cutoff:] 206 } else { // une seule animatrice, on regroupe tout les inscrits 207 filles = append(filles, gars...) 208 } 209 } else if len(fillesAnim) == 0 { // pareil, inversé 210 if len(garsAnim) >= 2 { // on peut partager 211 cutoff := len(garsAnim) / 2 212 fillesAnim = garsAnim[0:cutoff] 213 garsAnim = garsAnim[cutoff:] 214 } else { // un seul animateur, on regroupe tout les inscrits 215 gars = append(gars, filles...) 216 } 217 } 218 219 // on tri par age. Attention 220 // dans le cas d'un groupe mixte (si manque d'anim), on veut regrouper les inscrits de même sexe 221 sortInscrits := func(list composites.ParticipantPersonnes) { 222 sort.Slice(list, func(i, j int) bool { 223 return list[i].Personne.DateNaissance.Time().Before(list[j].Personne.DateNaissance.Time()) 224 }) 225 sort.SliceStable(list, func(i, j int) bool { 226 return list[i].Sexe < list[j].Sexe 227 }) 228 } 229 sortInscrits(filles) 230 sortInscrits(gars) 231 232 // on trie aussi les anims par age pour affecter les inscrits les plus vieux aux anims les plus vieux 233 sort.Slice(garsAnim, func(i, j int) bool { 234 return garsAnim[i].Personne.DateNaissance.Time().Before(garsAnim[j].Personne.DateNaissance.Time()) 235 }) 236 sort.Slice(fillesAnim, func(i, j int) bool { 237 return fillesAnim[i].Personne.DateNaissance.Time().Before(fillesAnim[j].Personne.DateNaissance.Time()) 238 }) 239 240 // on calcule les tailles de chaque petit groupe, en 241 // prenant en compte les affectations existantes 242 var allAffectations rd.ParticipantEquipiers 243 if len(garsAnim) > 0 { 244 allAffectations = append(allAffectations, computeAffectations(data.ParticipantEquipiers, gars, garsAnim, idGroupe)...) 245 } 246 if len(fillesAnim) > 0 { 247 allAffectations = append(allAffectations, computeAffectations(data.ParticipantEquipiers, filles, fillesAnim, idGroupe)...) 248 } 249 250 tx, err := rc.DB.Begin() 251 if err != nil { 252 return nil, err 253 } 254 err = rd.InsertManyParticipantEquipiers(tx, allAffectations...) 255 if err != nil { 256 return nil, shared.Rollback(tx, err) 257 } 258 // on renvoie les affectations mises à jour 259 out, err := rd.SelectParticipantEquipiersByIdGroupes(tx, idGroupe) 260 if err != nil { 261 return nil, shared.Rollback(tx, err) 262 } 263 err = tx.Commit() 264 return out, err 265 }