All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.vertigo.planning.agenda.services.PlanningServices Maven / Gradle / Ivy

The newest version!
/*
 * vertigo - application development platform
 *
 * Copyright (C) 2013-2024, Vertigo.io, [email protected]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.vertigo.planning.agenda.services;

import java.io.Serializable;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import javax.inject.Inject;

import io.vertigo.account.authorization.VSecurityException;
import io.vertigo.commons.transaction.Transactional;
import io.vertigo.commons.transaction.VTransactionManager;
import io.vertigo.commons.transaction.VTransactionWritable;
import io.vertigo.core.lang.Assertion;
import io.vertigo.core.lang.VSystemException;
import io.vertigo.core.lang.VUserException;
import io.vertigo.core.locale.LocaleMessageText;
import io.vertigo.core.node.component.Component;
import io.vertigo.datamodel.criteria.Criterions;
import io.vertigo.datamodel.data.model.DtList;
import io.vertigo.datamodel.data.model.DtListState;
import io.vertigo.datamodel.data.model.UID;
import io.vertigo.datamodel.data.util.VCollectors;
import io.vertigo.planning.agenda.AgendaPAO;
import io.vertigo.planning.agenda.dao.AgendaDAO;
import io.vertigo.planning.agenda.dao.CreneauDAO;
import io.vertigo.planning.agenda.dao.PlageHoraireDAO;
import io.vertigo.planning.agenda.dao.ReservationCreneauDAO;
import io.vertigo.planning.agenda.dao.TrancheHoraireDAO;
import io.vertigo.planning.agenda.domain.AffectionReservation;
import io.vertigo.planning.agenda.domain.Agenda;
import io.vertigo.planning.agenda.domain.AgendaDisplay;
import io.vertigo.planning.agenda.domain.AgendaDisplayRange;
import io.vertigo.planning.agenda.domain.CreationPlageHoraireForm;
import io.vertigo.planning.agenda.domain.Creneau;
import io.vertigo.planning.agenda.domain.DateDisponibleDisplay;
import io.vertigo.planning.agenda.domain.DefaultPlageHoraire;
import io.vertigo.planning.agenda.domain.DuplicationSemaineForm;
import io.vertigo.planning.agenda.domain.PlageHoraire;
import io.vertigo.planning.agenda.domain.PlageHoraireDisplay;
import io.vertigo.planning.agenda.domain.PublicationTrancheHoraireForm;
import io.vertigo.planning.agenda.domain.ReservationCreneau;
import io.vertigo.planning.agenda.domain.TrancheHoraire;
import io.vertigo.planning.agenda.domain.TrancheHoraireDisplay;
import io.vertigo.planning.domain.DtDefinitions.AgendaFields;
import io.vertigo.planning.domain.DtDefinitions.CreationPlageHoraireFormFields;
import io.vertigo.planning.domain.DtDefinitions.CreneauFields;
import io.vertigo.planning.domain.DtDefinitions.DuplicationSemaineFormFields;
import io.vertigo.planning.domain.DtDefinitions.PlageHoraireFields;
import io.vertigo.planning.domain.DtDefinitions.PublicationTrancheHoraireFormFields;
import io.vertigo.planning.domain.DtDefinitions.TrancheHoraireFields;
import io.vertigo.vega.webservice.validation.UiErrorBuilder;

@Transactional
public class PlanningServices implements Component {

	public static final int MAX_AGENDA_TO_LOAD = 20;

	private static final int CREATE_MIN_DUREE_PLAGE_MINUTE = 60;
	private static final int CREATE_MAX_DUREE_PLAGE_HEURE = 10;
	private static final int CREATE_MAX_NB_GUICHET = 9;
	private static final int CREATE_MAX_DAYS_FROM_NOW = 365; //maximum 1 an par rapport à aujourd'hui

	private static final int PUBLISH_MAX_DAYS_FROM_NOW = 365; //maximum 1 an par rapport à aujourd'hui
	private static final int PUBLISH_MAX_DAYS_PERIODE = 31 * 2; //maximum 2 mois publié à la fois
	private static final int PUBLISH_NOW_DELAY_S = 60; //1 minute de sécurité avant publication effective

	private static final int ORPHAN_RESERVATION_AGG_MINUTES = 30;

	private static final int DUPLICATE_MAX_DAYS_PERIODE = 31 * 3; //3 mois
	private static final int DUPLICATE_MAX_DAYS_FROM_NOW = 365; //maximum 1 an par rapport à aujourd'hui

	@Inject
	private AgendaDAO agendaDAO;
	@Inject
	private PlageHoraireDAO plageHoraireDAO;
	@Inject
	private TrancheHoraireDAO trancheHoraireDAO;
	@Inject
	private CreneauDAO creneauDAO;
	@Inject
	private ReservationCreneauDAO reservationCreneauDAO;
	@Inject
	private AgendaPAO agendaPAO;
	@Inject
	private VTransactionManager transactionManager;

	public Agenda createAgenda(final String name) {
		Assertion.check().isNotBlank(name, "A agenda must have a name");
		//---
		final var agenda = new Agenda();
		agenda.setNom(name);
		final var createdAgenda = agendaDAO.create(agenda);
		return createdAgenda;
	}

	public PlageHoraire createPlageHoraire(final CreationPlageHoraireForm creationPlageHoraireForm, final AgendaDisplayRange agendaDisplayRange) {
		/** Contrôles User */
		final var uiErrorBuilder = new UiErrorBuilder();
		final var decalageJours = ChronoUnit.DAYS.between(LocalDate.now(), creationPlageHoraireForm.getDateLocale());
		if (Math.abs(decalageJours) > CREATE_MAX_DAYS_FROM_NOW) {
			uiErrorBuilder.addError(creationPlageHoraireForm, CreationPlageHoraireFormFields.dateLocale,
					LocaleMessageText.of("La date de la plage horaire doit être à moins d'1 an"));
		}
		if (creationPlageHoraireForm.getNbGuichet() <= 0 || creationPlageHoraireForm.getNbGuichet() > CREATE_MAX_NB_GUICHET) {
			uiErrorBuilder.addError(creationPlageHoraireForm, CreationPlageHoraireFormFields.nbGuichet,
					LocaleMessageText.of("Le nombre de guichets doit être un nombre compris entre 1 et 9"));
		}

		if (creationPlageHoraireForm.getMinutesDebut() < 7 * 60 + 30) {
			uiErrorBuilder.addError(creationPlageHoraireForm, CreationPlageHoraireFormFields.minutesDebut,
					LocaleMessageText.of("La plage horaire doit débuter au plus tôt à 07:30"));
		}
		if (creationPlageHoraireForm.getMinutesFin() > 21 * 60) {
			uiErrorBuilder.addError(creationPlageHoraireForm, CreationPlageHoraireFormFields.minutesFin,
					LocaleMessageText.of("La plage horaire doit finir au plus tard à 21:00"));
		}

		final var dureePlageMinute = creationPlageHoraireForm.getMinutesFin() - creationPlageHoraireForm.getMinutesDebut();
		if (dureePlageMinute <= 0) {
			uiErrorBuilder.addError(creationPlageHoraireForm, CreationPlageHoraireFormFields.minutesFin,
					LocaleMessageText.of("L'heure de fin de la plage horaire doit être postérieur à l'heure de début"));
		} else if (dureePlageMinute < CREATE_MIN_DUREE_PLAGE_MINUTE) {
			uiErrorBuilder.addError(creationPlageHoraireForm, CreationPlageHoraireFormFields.minutesDebut,
					LocaleMessageText.of("La plage horaire doit avoir une durée d'au moins " + CREATE_MIN_DUREE_PLAGE_MINUTE + " minutes"));
		} else if (dureePlageMinute > CREATE_MAX_DUREE_PLAGE_HEURE * 60) {
			uiErrorBuilder.addError(creationPlageHoraireForm, CreationPlageHoraireFormFields.minutesDebut,
					LocaleMessageText.of("La plage horaire doit avoir une durée inférieure à " + CREATE_MAX_DUREE_PLAGE_HEURE + " heures"));
		}

		if (creationPlageHoraireForm.getDateLocale().getDayOfWeek() == DayOfWeek.SUNDAY) {
			uiErrorBuilder.addError(creationPlageHoraireForm, CreationPlageHoraireFormFields.dateLocale,
					LocaleMessageText.of("Il n'est pas possible de créer une plage le dimanche"));
		}
		//---

		final var ageId = checkAuthorizedAgenda(UID.of(Agenda.class, creationPlageHoraireForm.getAgeId()), agendaDisplayRange);
		final var agenda = agendaDAO.readOneForUpdate(ageId); //ForUpdate pour éviter les doublons

		final var firstPlageConflict = plageHoraireDAO.getExistsConflictingPlageHoraire(
				agendaDisplayRange.getAgeIds(),
				creationPlageHoraireForm.getDateLocale(),
				creationPlageHoraireForm.getMinutesDebut(),
				creationPlageHoraireForm.getMinutesFin());
		if (firstPlageConflict.isPresent()) {
			//TODO : libelle de la plage
			uiErrorBuilder.addError(creationPlageHoraireForm, CreationPlageHoraireFormFields.minutesDebut,
					LocaleMessageText.of("La plage horaire est en conflit avec une autre"));
		}

		uiErrorBuilder.throwUserExceptionIfErrors();
		/*****/

		final var plageHoraire = new PlageHoraire();
		plageHoraire.agenda().setUID(agenda.getUID());
		plageHoraire.setDateLocale(creationPlageHoraireForm.getDateLocale());
		plageHoraire.setMinutesDebut(creationPlageHoraireForm.getMinutesDebut());
		plageHoraire.setMinutesFin(creationPlageHoraireForm.getMinutesFin());
		plageHoraire.setNbGuichet(creationPlageHoraireForm.getNbGuichet());
		plageHoraireDAO.save(plageHoraire);

		final var trancheHoraires = createTrancheHoraires(plageHoraire, creationPlageHoraireForm.getDureeCreneau());
		trancheHoraireDAO.batchInsertTrancheHoraire(trancheHoraires);
		// On ne crée pas les créneaux : seulement à la publication

		return plageHoraire;
	}

	private static DtList createTrancheHoraires(final PlageHoraire plageHoraire, final int dureeTrancheMinute) {
		return createTrancheHoraires(plageHoraire, dureeTrancheMinute, Collections.emptyList());
	}

	private static DtList createTrancheHoraires(final PlageHoraire plageHoraire, final int dureeTrancheMinute, final List closedTranchesHoraires) {
		final var trancheHoraires = new DtList<>(TrancheHoraire.class);
		for (int i = plageHoraire.getMinutesDebut(); i < plageHoraire.getMinutesFin(); i += dureeTrancheMinute) {
			boolean isClosed = false;
			for (final TrancheHoraire closedTrancheHoraire : closedTranchesHoraires) {
				if (i < closedTrancheHoraire.getMinutesFin() && i + dureeTrancheMinute > closedTrancheHoraire.getMinutesDebut()) {
					isClosed = true;
					break;
				}
			}
			trancheHoraires.add(createTrancheHoraire(plageHoraire, i, dureeTrancheMinute, isClosed ? 0 : plageHoraire.getNbGuichet()));
		}
		return trancheHoraires;
	}

	private static TrancheHoraire createTrancheHoraire(final PlageHoraire plageHoraire, final int startMinuteOfDay, final int dureeTrancheMinute, final int nbGuichet) {
		final var trancheHoraire = new TrancheHoraire();
		trancheHoraire.plageHoraire().setUID(plageHoraire.getUID());
		trancheHoraire.agenda().setUID(plageHoraire.agenda().getUID());
		trancheHoraire.setDateLocale(plageHoraire.getDateLocale());
		trancheHoraire.setMinutesDebut(startMinuteOfDay);
		trancheHoraire.setMinutesFin(startMinuteOfDay + dureeTrancheMinute);
		trancheHoraire.setNbGuichet(nbGuichet);
		return trancheHoraire;
	}

	public void duplicateSemaine(final List> ageUids, final DuplicationSemaineForm duplicationSemaineForm, final Map, Integer> dureeTranchePerAgenda) {
		//security filter
		final var uiErrorBuilder = new UiErrorBuilder();
		uiErrorBuilder.checkFieldDateAfterOrEquals(duplicationSemaineForm, DuplicationSemaineFormFields.dateLocaleFromDebut, DuplicationSemaineFormFields.dateLocaleFromFin,
				LocaleMessageText.of("La date de fin de la selection à copier doit être postérieur à la date de début"));

		uiErrorBuilder.checkFieldDateAfterOrEquals(duplicationSemaineForm, DuplicationSemaineFormFields.dateLocaleToDebut, DuplicationSemaineFormFields.dateLocaleToFin,
				LocaleMessageText.of("La date de fin de la plage de copie doit être postérieur à la date de début"));

		final var dureeDuplicationJours = ChronoUnit.DAYS.between(duplicationSemaineForm.getDateLocaleToDebut(), duplicationSemaineForm.getDateLocaleToFin());
		if (dureeDuplicationJours > DUPLICATE_MAX_DAYS_PERIODE) {
			uiErrorBuilder.addError(duplicationSemaineForm, DuplicationSemaineFormFields.dateLocaleToFin,
					LocaleMessageText.of("Vous ne pouvez pas dupliquer une semaine sur plus de 3 mois"));
		}

		final var decalageJours = ChronoUnit.DAYS.between(LocalDate.now(), duplicationSemaineForm.getDateLocaleToDebut());
		if (decalageJours > DUPLICATE_MAX_DAYS_FROM_NOW) { //abs : on autorise la publication de creneau passé
			uiErrorBuilder.addError(duplicationSemaineForm, DuplicationSemaineFormFields.dateLocaleToDebut,
					LocaleMessageText.of("Vous ne pouvez pas dupliquer à plus d'1 an"));
		} else if (Math.abs(decalageJours) > DUPLICATE_MAX_DAYS_FROM_NOW) { //abs : on autorise la publication de creneau passé
			uiErrorBuilder.addError(duplicationSemaineForm, DuplicationSemaineFormFields.dateLocaleToDebut,
					LocaleMessageText.of("Vous ne pouvez pas dupliquer à plus d'1 an"));
		}

		if (duplicationSemaineForm.getDateLocaleToDebut().isBefore(LocalDate.now())) {
			uiErrorBuilder.addError(duplicationSemaineForm, DuplicationSemaineFormFields.dateLocaleToDebut,
					LocaleMessageText.of("Vous ne pouvez pas dupliquer avant la date du jour"));
		} else if (duplicationSemaineForm.getDateLocaleToDebut().isBefore(duplicationSemaineForm.getDateLocaleFromFin())) {
			uiErrorBuilder.addError(duplicationSemaineForm, DuplicationSemaineFormFields.dateLocaleToDebut,
					LocaleMessageText.of("Vous ne pouvez pas dupliquer avant la semaine utilisée comme modèle"));
		}

		uiErrorBuilder.throwUserExceptionIfErrors();
		/*****/

		//on lock tous les agendas concernés : Attention ça peut locker la table !!
		ageUids.forEach(ageUid -> agendaDAO.readOneForUpdate(ageUid)); //ForUpdate pour éviter les doublons
		final var ageIds = ageUids.stream().map(UID::getId).map(Long.class::cast).toList();
		final var ageIdsArray = ageIds.toArray(Serializable[]::new);
		final var plageHorairesFrom = plageHoraireDAO.findAll(
				Criterions.in(PlageHoraireFields.ageId, ageIdsArray)
						.and(Criterions.isGreaterThanOrEqualTo(PlageHoraireFields.dateLocale, duplicationSemaineForm.getDateLocaleFromDebut()))
						.and(Criterions.isLessThanOrEqualTo(PlageHoraireFields.dateLocale, duplicationSemaineForm.getDateLocaleFromFin())),
				DtListState.of(null, 0, PlageHoraireFields.dateLocale.name(), false));
		if (plageHorairesFrom.isEmpty()) {
			//erreur bloquante
			throw new VUserException("La semaine que vous souhaitez dupliquer n'a aucune plage horaire");
		}
		final var closedTranchesHorairesFrom = trancheHoraireDAO.getTrancheHorairesFermeesByAgeIds(ageIds,
				duplicationSemaineForm.getDateLocaleFromDebut(), duplicationSemaineForm.getDateLocaleFromFin());

		final var previousPlageHorairesTo = plageHoraireDAO.findAll(
				Criterions.in(PlageHoraireFields.ageId, ageIdsArray)
						.and(Criterions.isGreaterThanOrEqualTo(PlageHoraireFields.dateLocale, duplicationSemaineForm.getDateLocaleToDebut()))
						.and(Criterions.isLessThanOrEqualTo(PlageHoraireFields.dateLocale, duplicationSemaineForm.getDateLocaleToFin())),
				DtListState.of(null, 0, PlageHoraireFields.dateLocale.name(), false));

		final Map> mapPlagesHorairesFromPerDayOfWeek = plageHorairesFrom.stream()
				.collect(Collectors.groupingBy(plh -> plh.getDateLocale().getDayOfWeek()));

		final Map> mapPreviousPlagesHorairesToPerLocalDate = previousPlageHorairesTo.stream()
				.collect(Collectors.groupingBy((Function) PlageHoraire::getDateLocale));

		final Map> mapClosedTranchesHorairesFromPerDayOfWeek = closedTranchesHorairesFrom.stream()
				.collect(Collectors.groupingBy(trh -> trh.getDateLocale().getDayOfWeek()));

		final var plageHorairesToCreate = new DtList<>(PlageHoraire.class);
		final var trancheHoraires = new DtList<>(TrancheHoraire.class);
		//final int dureeTrancheMinute = duplicationSemaineForm.getDureeCreneau();
		for (var d = 0; d < dureeDuplicationJours + 1; ++d) { //+1 => date de fin incluse
			final var currentCopyDate = duplicationSemaineForm.getDateLocaleToDebut().plusDays(d);
			final var plageHorairesCopyFrom = mapPlagesHorairesFromPerDayOfWeek.get(currentCopyDate.getDayOfWeek());
			if (plageHorairesCopyFrom == null || plageHorairesCopyFrom.isEmpty()) {
				continue;
			}

			final var plageHorairesPreviousTo = mapPreviousPlagesHorairesToPerLocalDate.get(currentCopyDate);
			for (final PlageHoraire plageHoraireFrom : plageHorairesCopyFrom) {
				//on vérifie qu'il n'y a pas de conflit
				if (plageHorairesPreviousTo != null
						&& !plageHorairesPreviousTo.isEmpty()
						&& conflictPlageHoraire(plageHoraireFrom, plageHorairesPreviousTo)) {
					continue;
				}
				//sinon on crée la plage
				final var plageHoraire = new PlageHoraire();
				plageHoraire.agenda().setUID(plageHoraireFrom.agenda().getUID());
				plageHoraire.setDateLocale(currentCopyDate);
				plageHoraire.setMinutesDebut(plageHoraireFrom.getMinutesDebut());
				plageHoraire.setMinutesFin(plageHoraireFrom.getMinutesFin());
				plageHoraire.setNbGuichet(plageHoraireFrom.getNbGuichet());
				plageHorairesToCreate.add(plageHoraire);

				//(les tranches seront mis à jour après l'insert pour pouvoir récupérer la PK générée)
			}
		}
		//le batch ne marche pas car l'id n'est pas setté
		//on le fait quand meme en dernier pour locker moins longtemps
		//Creation des tranches horaires associées
		for (final PlageHoraire plageHoraire : plageHorairesToCreate) {
			plageHoraireDAO.save(plageHoraire);
			//cette fois l'id de la plage existe et peut être associée dans la FK des tranches
			//RDV-351 : il faut vérifier les tranches supprimées
			final Integer dureeCreneauAgenda = dureeTranchePerAgenda.get(plageHoraire.agenda().getUID());
			Assertion.check().isNotNull(dureeCreneauAgenda, "La durée de créneau n'est pas définie pour l'agenda {0}", plageHoraire.agenda().getUID());
			trancheHoraires.addAll(createTrancheHoraires(plageHoraire, dureeCreneauAgenda,
					mapClosedTranchesHorairesFromPerDayOfWeek.getOrDefault(plageHoraire.getDateLocale().getDayOfWeek(), Collections.emptyList())));
		}
		trancheHoraireDAO.batchInsertTrancheHoraire(trancheHoraires);
	}

	private static boolean conflictPlageHoraire(final PlageHoraire plageHoraireFrom, final List plhsPreviousTo) {
		for (final PlageHoraire plhPreviousTo : plhsPreviousTo) {
			Assertion.check().isTrue(plageHoraireFrom.getAgeId().equals(plhPreviousTo.getAgeId()), "Les plages comparées ne sont pas sur le même agenda");
			if (plageHoraireFrom.getMinutesDebut() < plhPreviousTo.getMinutesFin()
					&& plageHoraireFrom.getMinutesFin() > plhPreviousTo.getMinutesDebut()) {
				return true;
			}
		}
		return false;
	}

	public void deletePlageHoraireCascade(final UID agendaUid, final UID plageHoraireUid) {
		Assertion.check().isNotNull(agendaUid)
				.isNotNull(plageHoraireUid);
		//--
		agendaDAO.readOneForUpdate(agendaUid); //ForUpdate pour éviter les doublons
		agendaPAO.deletePlageHoraireCascadeByPlhId((Long) plageHoraireUid.getId());
	}

	public void closeTrancheHoraire(final UID agendaUid, final UID trancheHoraireUid) {
		Assertion.check().isNotNull(agendaUid)
				.isNotNull(trancheHoraireUid);
		//--
		agendaDAO.readOneForUpdate(agendaUid); //ForUpdate pour éviter les doublons
		agendaPAO.closeTrancheHoraireByTrhId(List.of((Long) trancheHoraireUid.getId()));
	}

	public Agenda getAgenda(final UID ageUid) {
		Assertion.check().isNotNull(ageUid);
		//---
		return agendaDAO.get(ageUid);
	}

	public DtList getAgendas(final List> ageUids) {
		Assertion.check()
				.isNotNull(ageUids)
				.isTrue(ageUids.size() > 0, "Aucun agenda à charger")
				.isTrue(ageUids.size() <= MAX_AGENDA_TO_LOAD, "Too much agenda to load, max {}", MAX_AGENDA_TO_LOAD);
		//---
		final Serializable[] ageIds = ageUids.stream().map(UID::getId).toArray(Serializable[]::new);
		return agendaDAO.findAll(Criterions.in(AgendaFields.ageId, ageIds), DtListState.defaultOf(Agenda.class));
	}

	public DtList getAgendasDisplay(final List> ageUids) {
		final var agendas = getAgendas(ageUids);
		return agendas.stream()
				.map(agenda -> {
					final var agendaDisplay = new AgendaDisplay();
					agendaDisplay.setAgeId(agenda.getAgeId());
					agendaDisplay.setName(agenda.getNom());
					return agendaDisplay;
				})
				.collect(VCollectors.toDtList(AgendaDisplay.class));
	}

	public DtList getPlageHoraireDisplayByAgendas(final List> ageUids, final LocalDate firstDate, final LocalDate lastDate) {
		Assertion.check().isNotNull(ageUids)
				.isNotNull(firstDate)
				.isNotNull(lastDate)
				.isTrue(ageUids.size() > 0, "Aucun agenda à charger")
				.isTrue(ageUids.size() <= MAX_AGENDA_TO_LOAD, "Too much agenda to load, max {}", MAX_AGENDA_TO_LOAD);
		//--
		final var ageIds = ageUids.stream().map(UID::getId).map(Long.class::cast).toList();
		return agendaPAO.getPlageHoraireDisplayByAgeIds(ageIds, firstDate, lastDate, Instant.now());
	}

	/**
	 * Détermine les plages horaires par défaut d'un agenda.
	 * Pour l'instant en dur, plus tard pourrait analyser l'existant.
	 * @param agendaUid uid de l'agenda
	 * @param minusMonths Nombre de mois à analyser
	 * @param firstDate Première date de la période à simuler
	 * @return Liste des plages horaires par défaut
	 */
	public DtList getDefaultPlageHoraireByAgenda(final List> ageUids, final LocalDate minusMonths, final LocalDate firstDate) {

		//default
		final DtList defaultPlageHoraires = new DtList<>(DefaultPlageHoraire.class);

		//A terme : regarder les creneaux du passé pour reproduire le pattern.
		//pour le moment valeur empirique
		defaultPlageHoraires.add(newDefaultPlageHoraire(1, 8, 30, 12, 0, 1));
		defaultPlageHoraires.add(newDefaultPlageHoraire(1, 14, 0, 16, 30, 1));
		defaultPlageHoraires.add(newDefaultPlageHoraire(2, 8, 30, 12, 0, 1));
		defaultPlageHoraires.add(newDefaultPlageHoraire(2, 14, 0, 16, 30, 1));
		defaultPlageHoraires.add(newDefaultPlageHoraire(3, 8, 30, 12, 0, 1));
		defaultPlageHoraires.add(newDefaultPlageHoraire(3, 14, 0, 16, 30, 1));
		defaultPlageHoraires.add(newDefaultPlageHoraire(4, 8, 30, 12, 0, 1));
		defaultPlageHoraires.add(newDefaultPlageHoraire(4, 14, 0, 16, 30, 1));
		defaultPlageHoraires.add(newDefaultPlageHoraire(5, 8, 30, 12, 0, 1));
		defaultPlageHoraires.add(newDefaultPlageHoraire(5, 14, 0, 16, 30, 1));

		return defaultPlageHoraires;
	}

	private static DefaultPlageHoraire newDefaultPlageHoraire(final int jour, final int hourDebut, final int minDebut, final int hourFin, final int minFin, final int nbGuichet) {
		DefaultPlageHoraire defaultPlageHoraire;
		defaultPlageHoraire = new DefaultPlageHoraire();
		defaultPlageHoraire.setJourDeSemaine(jour);
		defaultPlageHoraire.setMinutesDebut(hourDebut * 60 + minDebut);
		defaultPlageHoraire.setMinutesFin(hourFin * 60 + minFin);
		defaultPlageHoraire.setNbGuichet(nbGuichet);
		return defaultPlageHoraire;
	}

	public PlageHoraireDisplay getPlageHoraireDisplay(final UID plageUid) {
		Assertion.check()
				.isNotNull(plageUid);
		//-----
		return agendaPAO.getPlageHoraireDisplayByPlhId((Long) plageUid.getId(), Instant.now());
	}

	public DtList getTrancheHoraireDisplayByPlage(final UID plageUid) {
		Assertion.check()
				.isNotNull(plageUid);
		//-----
		return agendaPAO.getTrancheHoraireDisplayByPlhId((Long) plageUid.getId(), Instant.now());
	}

	public DtList getReservationOrphelines(final List> ageUids, final LocalDate firstDate, final LocalDate lastDate) {
		final var ageIds = ageUids.stream().map(UID::getId).map(Long.class::cast).toList();
		return agendaPAO.countUnlinkReservationPerXminByAgeId(ageIds, firstDate, lastDate, ORPHAN_RESERVATION_AGG_MINUTES);
	}

	public LocalDate getPremierJourLibrePourDuplication(final AgendaDisplayRange agendaRange) {
		//on prend soit la date du jour soit la date de début de la source + 1 semaine
		final var localDateTo = LocalDate.now().isAfter(agendaRange.getLastDate()) ? LocalDate.now() : agendaRange.getFirstDate().plusWeeks(1);

		final var firsts = agendaPAO.getFirstLocalDatesFreeOfPlageHorairePerDayOfWeek(
				agendaRange.getAgeIds(),
				agendaRange.getFirstDate(),
				agendaRange.getLastDate(),
				localDateTo,
				LocalDate.now().plusYears(1));
		if (firsts.isEmpty()) {
			return localDateTo;
		}
		return firsts.get(0);
	}

	public DtList getAvailableCreneaux(final UID trhUid, final int maxAvailabilities) {
		// Here we don't lock lines we just show the state at that time
		final var availableCreneaux = creneauDAO.findAll(Criterions.isEqualTo(CreneauFields.trhId, trhUid.getId()).and(Criterions.isNull(CreneauFields.recId)), DtListState.of(maxAvailabilities));
		return availableCreneaux;
	}

	public Optional reserverCreneau(final UID trhUid) {
		// Trouver un créneau sur une tranche : on utilise le mecanisme de lock de la BDD (for update skip locks)
		final var creneauOpt = creneauDAO.selectCreneauForUpdateByTrhId((Long) trhUid.getId());
		if (creneauOpt.isEmpty()) {
			//on a pas trouvé de creneau disponible à cette date
			return Optional.empty();

		}

		final var creneau = creneauOpt.get();
		creneau.trancheHoraire().load();
		final var trancheHoraire = creneau.trancheHoraire().get();
		//---

		final var reservationCreneau = prepareReservationCreneau(trancheHoraire);
		reservationCreneauDAO.create(reservationCreneau);

		creneau.reservationCreneau().setUID(reservationCreneau.getUID());
		creneauDAO.save(creneau);
		//---
		return Optional.of(reservationCreneau);
	}

	public Optional reserverCreneau(final List> ageUids, final LocalDate startDate, final LocalDate endDate, final Integer startMinutes, final Integer endMinutes) {
		// Trouver un créneau sur une liste d'agenda avec des critères de sélection : on utilise le mecanisme de lock de la BDD (for update skip locks)
		final var ageIds = ageUids.stream().map(UID::getId).map(Long.class::cast).toList();
		final var creneauOpt = creneauDAO.selectCreneauForUpdateByAgeIds(ageIds, startDate, endDate, startMinutes, endMinutes, Instant.now());
		if (creneauOpt.isEmpty()) {
			//on a pas trouvé de creneau disponible à cette date
			return Optional.empty();
		}

		final var creneau = creneauOpt.get();
		creneau.trancheHoraire().load();
		final var trancheHoraire = creneau.trancheHoraire().get();
		//---

		final var reservationCreneau = prepareReservationCreneau(trancheHoraire);
		reservationCreneauDAO.create(reservationCreneau);

		creneau.reservationCreneau().setUID(reservationCreneau.getUID());
		creneauDAO.save(creneau);
		//---
		return Optional.of(reservationCreneau);
	}

	public ReservationCreneau reserverCreneauWithOverbooking(final UID trhUid) {
		final var reservationCreneauOpt = reserverCreneau(trhUid);
		if (reservationCreneauOpt.isPresent()) {
			return reservationCreneauOpt.get();
		}
		final var trancheHoraire = trancheHoraireDAO.get(trhUid);
		final var reservationCreneau = prepareReservationCreneau(trancheHoraire);
		reservationCreneauDAO.create(reservationCreneau);
		return reservationCreneau;
	}

	public DtList reserverCreneaux(final UID trhUid, final DtList creneaux) {
		Assertion.check()
				.isNotNull(trhUid)
				.isNotNull(creneaux);
		//---
		try (final VTransactionWritable tx = transactionManager.createAutonomousTransaction()) {
			final var trancheHoraire = trancheHoraireDAO.get(trhUid);
			final DtList reservationsCreneau = creneaux.stream()
					// First we ensure that all creneaux are associated with the provided tranche
					.peek(creneau -> {
						if (!trhUid.equals(creneau.trancheHoraire().getUID())) {
							throw new VSystemException("All creneau must be associated with the same tranche, the creneau with creId '{0}' doesn't", creneau.getCreId());
						}
					})
					.map(creneau -> prepareReservationCreneau(trancheHoraire))
					.collect(VCollectors.toDtList(ReservationCreneau.class));

			reservationCreneauDAO.insertReservationsCreneau(reservationsCreneau);
			IntStream.range(0, creneaux.size()).forEach(idx -> {
				final var creneau = creneaux.get(idx);
				creneau.reservationCreneau().setUID(reservationsCreneau.get(idx).getUID());
			});
			final var modifiedRows = agendaPAO.reserverCreneaux(creneaux);
			if (modifiedRows != creneaux.size()) {
				throw new VSystemException("Error reserving the provided creneau, none is reserved");
			}
			tx.commit();
			return reservationsCreneau;
		}

	}

	private ReservationCreneau prepareReservationCreneau(final TrancheHoraire trancheHoraire) {
		final var reservationCreneau = new ReservationCreneau();
		reservationCreneau.agenda().setUID(trancheHoraire.agenda().getUID());
		reservationCreneau.setDateLocale(trancheHoraire.getDateLocale());
		reservationCreneau.setMinutesDebut(trancheHoraire.getMinutesDebut());
		reservationCreneau.setMinutesFin(trancheHoraire.getMinutesFin());
		reservationCreneau.setInstantCreation(Instant.now());

		return reservationCreneau;
	}

	public void publishPlageHorairesAndRelinkReservation(final List> ageUids, final PublicationTrancheHoraireForm publicationTrancheHoraireForm) {

		//On change le status des trancheHoraires non publiées en masse (y compris celles planifiées)
		publishPlageHoraires(ageUids, publicationTrancheHoraireForm);

		//On pose le lien vers une reservation depuis le creneau
		relinkReservationsToCreneaux(ageUids, publicationTrancheHoraireForm.getDateLocaleDebut(), publicationTrancheHoraireForm.getDateLocaleFin());
	}

	private void publishPlageHoraires(final List> ageUids, final PublicationTrancheHoraireForm publicationTrancheHoraireForm) {
		//On change le status des trancheHoraires non publiées en masse (y compris celles planifiées)
		final var uiErrorBuilder = new UiErrorBuilder();
		final var decalageJours = ChronoUnit.DAYS.between(LocalDate.now(), publicationTrancheHoraireForm.getDateLocaleDebut());
		if (Math.abs(decalageJours) > PUBLISH_MAX_DAYS_FROM_NOW) { //abs : on autorise la publication de creneau passé
			uiErrorBuilder.addError(publicationTrancheHoraireForm, PublicationTrancheHoraireFormFields.dateLocaleDebut,
					LocaleMessageText.of("Vous ne pouvez pas publier des créneaux à plus d'1 an"));
		} else if (decalageJours < 0) { //on n'autorise pas la publication de creneau passé RDV-154
			uiErrorBuilder.addError(publicationTrancheHoraireForm, PublicationTrancheHoraireFormFields.dateLocaleDebut,
					LocaleMessageText.of("Vous ne pouvez pas publier des créneaux du passé"));
		}

		final var dureeJours = ChronoUnit.DAYS.between(publicationTrancheHoraireForm.getDateLocaleDebut(), publicationTrancheHoraireForm.getDateLocaleFin());
		if (dureeJours < 0) {//inferieur strictement
			uiErrorBuilder.addError(publicationTrancheHoraireForm, PublicationTrancheHoraireFormFields.dateLocaleFin,
					LocaleMessageText.of("La date de fin de la selection doit être postérieur à la date de début"));
		} else if (dureeJours > PUBLISH_MAX_DAYS_PERIODE) {
			uiErrorBuilder.addError(publicationTrancheHoraireForm, PublicationTrancheHoraireFormFields.dateLocaleFin,
					LocaleMessageText.of("Vous ne pouvez pas publier plus de 2 mois à la fois"));
		}
		if (!publicationTrancheHoraireForm.getPublishNow()) {
			uiErrorBuilder.checkFieldNotNull(publicationTrancheHoraireForm, PublicationTrancheHoraireFormFields.publicationDateLocale,
					LocaleMessageText.of("La date de publication est obligatoire"));
			uiErrorBuilder.checkFieldNotNull(publicationTrancheHoraireForm, PublicationTrancheHoraireFormFields.publicationMinutesDebut,
					LocaleMessageText.of("L'heure de publication est obligatoire"));
			if (publicationTrancheHoraireForm.getPublicationDateLocale() != null) {
				final var decalageJoursPublish = ChronoUnit.DAYS.between(LocalDate.now(), publicationTrancheHoraireForm.getPublicationDateLocale());
				if (decalageJoursPublish < 0) {
					uiErrorBuilder.addError(publicationTrancheHoraireForm, PublicationTrancheHoraireFormFields.publicationDateLocale,
							LocaleMessageText.of("Vous ne pouvez pas planifier la publication à une date dans le passé"));
				}
			}
		}
		uiErrorBuilder.throwUserExceptionIfErrors();

		Instant datePublication;
		if (publicationTrancheHoraireForm.getPublishNow()) {
			datePublication = Instant.now().plusSeconds(PUBLISH_NOW_DELAY_S);
		} else {

			final var localTime = LocalTime.ofSecondOfDay(publicationTrancheHoraireForm.getPublicationMinutesDebut() * 60L);
			final var publishLocalDateTime = LocalDateTime.of(publicationTrancheHoraireForm.getPublicationDateLocale(), localTime);

			//on suppose l'heure exprimée à paris par défaut
			String zonCd = "Europe/Paris";
			if (publicationTrancheHoraireForm.getPublicationZonCd() != null) {
				zonCd = publicationTrancheHoraireForm.getPublicationZonCd();
			}
			datePublication = publishLocalDateTime.atZone(ZoneId.of(zonCd)).toInstant();
		}
		uiErrorBuilder.throwUserExceptionIfErrors();
		/*****/
		//on planifie
		final var ageIds = ageUids.stream().map(UID::getId).map(Long.class::cast).toList();
		agendaPAO.publishTrancheHoraireByAgeIds(ageIds,
				publicationTrancheHoraireForm.getDateLocaleDebut(), publicationTrancheHoraireForm.getDateLocaleFin(),
				Instant.now(), datePublication);

		//on crée les créneaux en masse
		agendaPAO.createCreneauOfPublishedTrancheHoraireByAgeIds(ageIds,
				publicationTrancheHoraireForm.getDateLocaleDebut(), publicationTrancheHoraireForm.getDateLocaleFin(),
				datePublication);
	}

	/**
	 * Reassocie les creneaux publiés aux ReservationCreneaux existant.
	 * Utilisé après une publication de plage horaire pour rattacher les réservations préexistantes.
	 * ou après un import de réservation.
	 * @param ageUid Uid de l'agenda
	 * @param dateLocaleDebut Date de début
	 * @param dateLocaleFin Date de fin
	 */
	public void relinkReservationsToCreneaux(final List> ageUids, final LocalDate dateLocaleDebut, final LocalDate dateLocaleFin) {
		final var ageIds = ageUids.stream().map(UID::getId).map(Long.class::cast).toList();
		final var affectations = agendaPAO.getLinkReservationAfterPublishByAgeIds(ageIds, dateLocaleDebut, dateLocaleFin);
		final Set affectedCreneau = new HashSet<>();
		for (final AffectionReservation affectation : affectations) {
			final var creIdsAsString = affectation.getCreIds().split(";");
			for (final String creIdAsString : creIdsAsString) {
				if (affectedCreneau.add(creIdAsString)) {
					affectation.setCreId(Long.valueOf(creIdAsString));
					break;
				}
			}
		}
		agendaPAO.linkCreneauToReservation(affectations);
	}

	public void supprimerReservationCreneau(final List> recUids) {
		Assertion.check().isNotNull(recUids);
		//---
		final List recIds = recUids.stream()
				.filter(id -> id != null) //protect against bad caller
				.map(UID::getId)
				.map(Long.class::cast).toList();
		agendaPAO.supprimerReservationsCreneau(recIds);
	}

	public Optional getTrancheHoraireIfExists(final UID trhUid) {
		return trancheHoraireDAO.findOptional(Criterions.isEqualTo(TrancheHoraireFields.trhId, trhUid.getId()));
	}

	/**
	 * Récupère les date disponibles d'un mois.
	 * N'affiche pas les disponibilités < date du jour.
	 *
	 * @param ageUid l'id d'agenda
	 * @param yearMonth le mois à afficher.
	 * @return la liste d'affichage
	 */
	public DtList getDateDisponibleDisplayFuturOnly(final List> ageUids, final YearMonth yearMonth) {
		Assertion.check()
				.isNotNull(ageUids)
				.isNotNull(yearMonth);
		//-----
		if (yearMonth.isBefore(YearMonth.now())) { // mois passé, on affiche rien (et s'économise un appel base)
			return new DtList<>(DateDisponibleDisplay.class);
		}

		final var firstDayOfMonth = yearMonth.atDay(1);
		final LocalDate today = LocalDate.now();
		final var startLocaldate = firstDayOfMonth.isBefore(today) ? today : firstDayOfMonth; // on commence pas avant la date du jour

		return getDateDisponibleDisplay(ageUids, startLocaldate, yearMonth.atEndOfMonth());
	}

	public DtList getDateDisponibleDisplay(final List> ageUids, final LocalDate startDate, final LocalDate endDate) {
		Assertion.check()
				.isNotNull(ageUids)
				.isNotNull(startDate)
				.isNotNull(endDate);
		//-----
		final var ageIds = ageUids.stream().map(UID::getId).map(Long.class::cast).toList();
		return agendaPAO.getDateDisponibleDisplayByAgeIds(ageIds, startDate, endDate, Instant.now());
	}

	public DtList getTrancheHoraireDisplayByDate(final List> ageUids, final LocalDate localDate) {
		Assertion.check()
				.isNotNull(ageUids)
				.isNotNull(localDate);
		//-----
		final var ageIds = ageUids.stream().map(UID::getId).map(Long.class::cast).toList();
		return agendaPAO.getTrancheHoraireDisplayByDate(ageIds, localDate, Instant.now());
	}

	public void deleteEmptyAgenda(final UID agendaUid) {
		agendaDAO.delete(agendaUid);
	}

	/**
	 * Purge agenda data linked to plage_horaire older than given date
	 *
	 * @param olderThan
	 */
	public int purgeAgendaOlderThan(final LocalDate olderThan) {
		Assertion.check().isNotNull(olderThan);
		return agendaPAO.purgePlageHoraireByDateLocale(olderThan);
	}

	public UID checkAuthorizedAgendaOfPlh(final UID plhUid, final AgendaDisplayRange agenda) {
		final var plageHoraire = plageHoraireDAO.get(plhUid);
		final var ageUid = UID.of(Agenda.class, plageHoraire.getAgeId());
		return checkAuthorizedAgenda(ageUid, agenda);
	}

	public UID checkAuthorizedAgenda(final UID ageUid, final AgendaDisplayRange agenda) {
		//check ageId in agenda.getAgeIds()
		if (!agenda.getAgeIds().contains(ageUid.getId())) {
			throw new VSecurityException(LocaleMessageText.of("Vous n'avez pas les droits pour agir sur cet agenda"));
		}
		return ageUid;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy