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  }