pour prod
This commit is contained in:
986
app/Models/PlanningModel.php
Normal file
986
app/Models/PlanningModel.php
Normal file
@@ -0,0 +1,986 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Throwable;
|
||||
|
||||
final class PlanningModel extends BaseModel
|
||||
{
|
||||
public function findByContext(int $structureId, int $agentId, int $year, int $week): ?array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT p.id_planning, p.statut, p.commentaire, s.date_debut, s.date_fin
|
||||
FROM planning p
|
||||
INNER JOIN semaine s ON s.id_semaine = p.id_semaine
|
||||
WHERE p.id_structure = :structure_id
|
||||
AND p.id_agent = :agent_id
|
||||
AND s.annee = :annee
|
||||
AND s.numero_semaine = :semaine
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([
|
||||
'structure_id' => $structureId,
|
||||
'agent_id' => $agentId,
|
||||
'annee' => $year,
|
||||
'semaine' => $week,
|
||||
]);
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
|
||||
public function entries(int $planningId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT c.id_creneau, c.date_jour,
|
||||
TIME_FORMAT(c.heure_debut, '%H:%i') AS heure_debut,
|
||||
TIME_FORMAT(c.heure_fin, '%H:%i') AS heure_fin,
|
||||
m.id_motif, m.code AS motif_code, m.libelle AS motif_libelle,
|
||||
m.compte_dans_quota
|
||||
FROM creneau_horaire c
|
||||
INNER JOIN motif_planning m ON m.id_motif = c.id_motif
|
||||
WHERE c.id_planning = :planning_id
|
||||
ORDER BY c.date_jour, c.heure_debut, c.heure_fin"
|
||||
);
|
||||
$stmt->execute(['planning_id' => $planningId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function otherLocationEntries(int $agentId, int $year, int $week, int $structureId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT p.id_planning, p.statut,
|
||||
st.id_structure, st.code AS structure_code, st.nom AS structure_nom,
|
||||
st.type_affectation AS structure_type_affectation,
|
||||
c.id_creneau, c.date_jour,
|
||||
TIME_FORMAT(c.heure_debut, '%H:%i') AS heure_debut,
|
||||
TIME_FORMAT(c.heure_fin, '%H:%i') AS heure_fin,
|
||||
m.id_motif, m.code AS motif_code, m.libelle AS motif_libelle,
|
||||
m.compte_dans_quota
|
||||
FROM planning p
|
||||
INNER JOIN semaine sem ON sem.id_semaine = p.id_semaine
|
||||
INNER JOIN structure st ON st.id_structure = p.id_structure
|
||||
INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning
|
||||
INNER JOIN motif_planning m ON m.id_motif = c.id_motif
|
||||
WHERE p.id_agent = :agent_id
|
||||
AND sem.annee = :annee
|
||||
AND sem.numero_semaine = :semaine
|
||||
AND p.id_structure <> :structure_id
|
||||
ORDER BY c.date_jour, c.heure_debut, st.nom"
|
||||
);
|
||||
$stmt->execute([
|
||||
'agent_id' => $agentId,
|
||||
'annee' => $year,
|
||||
'semaine' => $week,
|
||||
'structure_id' => $structureId,
|
||||
]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function saveDraft(int $structureId, int $agentId, int $year, int $weekNumber, array $entries): int
|
||||
{
|
||||
$weekStart = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1)->setTime(0, 0);
|
||||
$weekEnd = $weekStart->modify('+6 days');
|
||||
|
||||
try {
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
$weekStmt = $this->pdo->prepare(
|
||||
"INSERT INTO semaine (annee, numero_semaine, date_debut, date_fin, statut)
|
||||
VALUES (:annee, :numero, :date_debut, :date_fin, 'OUVERTE')
|
||||
ON DUPLICATE KEY UPDATE id_semaine = LAST_INSERT_ID(id_semaine)"
|
||||
);
|
||||
$weekStmt->execute([
|
||||
'annee' => $year,
|
||||
'numero' => $weekNumber,
|
||||
'date_debut' => $weekStart->format('Y-m-d'),
|
||||
'date_fin' => $weekEnd->format('Y-m-d'),
|
||||
]);
|
||||
$weekId = (int) $this->pdo->lastInsertId();
|
||||
|
||||
if ($weekId === 0) {
|
||||
$findWeek = $this->pdo->prepare(
|
||||
'SELECT id_semaine FROM semaine WHERE annee = :annee AND numero_semaine = :numero'
|
||||
);
|
||||
$findWeek->execute(['annee' => $year, 'numero' => $weekNumber]);
|
||||
$weekId = (int) $findWeek->fetchColumn();
|
||||
}
|
||||
|
||||
$planningStmt = $this->pdo->prepare(
|
||||
"SELECT id_planning, statut
|
||||
FROM planning
|
||||
WHERE id_agent = :agent_id
|
||||
AND id_semaine = :semaine_id
|
||||
AND id_structure = :structure_id
|
||||
FOR UPDATE"
|
||||
);
|
||||
$planningStmt->execute([
|
||||
'agent_id' => $agentId,
|
||||
'semaine_id' => $weekId,
|
||||
'structure_id' => $structureId,
|
||||
]);
|
||||
$planning = $planningStmt->fetch();
|
||||
|
||||
if ($planning && $planning['statut'] === 'VALIDE') {
|
||||
throw new \DomainException('Ce planning est déjà validé et doit être rouvert avant modification.');
|
||||
}
|
||||
|
||||
if ($planning) {
|
||||
$planningId = (int) $planning['id_planning'];
|
||||
$delete = $this->pdo->prepare('DELETE FROM creneau_horaire WHERE id_planning = :planning_id');
|
||||
$delete->execute(['planning_id' => $planningId]);
|
||||
} else {
|
||||
$insertPlanning = $this->pdo->prepare(
|
||||
"INSERT INTO planning (id_agent, id_semaine, id_structure, statut)
|
||||
VALUES (:agent_id, :semaine_id, :structure_id, 'BROUILLON')"
|
||||
);
|
||||
$insertPlanning->execute([
|
||||
'agent_id' => $agentId,
|
||||
'semaine_id' => $weekId,
|
||||
'structure_id' => $structureId,
|
||||
]);
|
||||
$planningId = (int) $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
$insertEntry = $this->pdo->prepare(
|
||||
"INSERT INTO creneau_horaire (id_planning, id_motif, date_jour, heure_debut, heure_fin)
|
||||
VALUES (:planning_id, :motif_id, :date_jour, :heure_debut, :heure_fin)"
|
||||
);
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$insertEntry->execute([
|
||||
'planning_id' => $planningId,
|
||||
'motif_id' => $entry['motif_id'],
|
||||
'date_jour' => $entry['date'],
|
||||
'heure_debut' => $entry['start'],
|
||||
'heure_fin' => $entry['end'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->pdo->commit();
|
||||
return $planningId;
|
||||
} catch (Throwable $e) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function pendingValidation(): array
|
||||
{
|
||||
$stmt = $this->pdo->query(
|
||||
"SELECT p.id_planning, p.id_agent, p.id_structure, p.statut,
|
||||
p.date_creation, p.date_modification,
|
||||
a.matricule, a.nom AS agent_nom, a.prenom AS agent_prenom,
|
||||
st.nom AS structure_nom, st.type_affectation AS structure_type_affectation,
|
||||
sem.annee, sem.numero_semaine, sem.date_debut, sem.date_fin,
|
||||
DATE_ADD(sem.date_debut, INTERVAL 4 DAY) AS date_fin_ouvrable,
|
||||
COUNT(c.id_creneau) AS nombre_creneaux,
|
||||
COALESCE(SUM(TIMESTAMPDIFF(
|
||||
MINUTE,
|
||||
CONCAT(c.date_jour, ' ', c.heure_debut),
|
||||
CONCAT(c.date_jour, ' ', c.heure_fin)
|
||||
)), 0) AS total_minutes,
|
||||
COALESCE(SUM(CASE WHEN m.compte_dans_quota = 1 THEN TIMESTAMPDIFF(
|
||||
MINUTE,
|
||||
CONCAT(c.date_jour, ' ', c.heure_debut),
|
||||
CONCAT(c.date_jour, ' ', c.heure_fin)
|
||||
) ELSE 0 END), 0) AS minutes_decomptees
|
||||
FROM planning p
|
||||
INNER JOIN agent a ON a.id_agent = p.id_agent
|
||||
INNER JOIN structure st ON st.id_structure = p.id_structure
|
||||
INNER JOIN semaine sem ON sem.id_semaine = p.id_semaine
|
||||
LEFT JOIN creneau_horaire c ON c.id_planning = p.id_planning
|
||||
LEFT JOIN motif_planning m ON m.id_motif = c.id_motif
|
||||
WHERE p.statut = 'BROUILLON'
|
||||
GROUP BY p.id_planning, p.id_agent, p.id_structure, p.statut,
|
||||
p.date_creation, p.date_modification,
|
||||
a.matricule, a.nom, a.prenom,
|
||||
st.nom, st.type_affectation,
|
||||
sem.annee, sem.numero_semaine, sem.date_debut, sem.date_fin
|
||||
HAVING COUNT(c.id_creneau) > 0
|
||||
ORDER BY sem.date_debut ASC, a.nom ASC, a.prenom ASC, st.nom ASC"
|
||||
);
|
||||
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Plannings en brouillon de l'agent qui contiennent au moins un créneau
|
||||
* dans l'année civile demandée.
|
||||
*/
|
||||
public function pendingValidationForAgentYear(int $agentId, int $year): array
|
||||
{
|
||||
$start = sprintf('%04d-01-01', $year);
|
||||
$end = sprintf('%04d-12-31', $year);
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT p.id_planning, p.id_agent, p.id_structure, p.statut,
|
||||
st.nom AS structure_nom,
|
||||
sem.annee, sem.numero_semaine, sem.date_debut, sem.date_fin,
|
||||
COUNT(c.id_creneau) AS nombre_creneaux,
|
||||
COALESCE(SUM(TIMESTAMPDIFF(
|
||||
MINUTE,
|
||||
CONCAT(c.date_jour, ' ', c.heure_debut),
|
||||
CONCAT(c.date_jour, ' ', c.heure_fin)
|
||||
)), 0) AS total_minutes
|
||||
FROM planning p
|
||||
INNER JOIN structure st ON st.id_structure = p.id_structure
|
||||
INNER JOIN semaine sem ON sem.id_semaine = p.id_semaine
|
||||
LEFT JOIN creneau_horaire c ON c.id_planning = p.id_planning
|
||||
WHERE p.id_agent = :agent_id
|
||||
AND p.statut = 'BROUILLON'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM creneau_horaire cy
|
||||
WHERE cy.id_planning = p.id_planning
|
||||
AND cy.date_jour BETWEEN :date_debut AND :date_fin
|
||||
)
|
||||
GROUP BY p.id_planning, p.id_agent, p.id_structure, p.statut,
|
||||
st.nom, sem.annee, sem.numero_semaine, sem.date_debut, sem.date_fin
|
||||
HAVING COUNT(c.id_creneau) > 0
|
||||
ORDER BY sem.date_debut, st.nom"
|
||||
);
|
||||
$stmt->execute([
|
||||
'agent_id' => $agentId,
|
||||
'date_debut' => $start,
|
||||
'date_fin' => $end,
|
||||
]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Chevauchements avec n'importe quel autre planning de l'agent,
|
||||
* qu'il soit déjà validé ou encore en brouillon.
|
||||
*/
|
||||
public function externalOverlaps(int $planningId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT DISTINCT c1.id_creneau AS c1, c2.id_creneau AS c2,
|
||||
c1.date_jour, st.nom AS structure_nom, p2.statut AS autre_statut
|
||||
FROM planning p1
|
||||
INNER JOIN creneau_horaire c1 ON c1.id_planning = p1.id_planning
|
||||
INNER JOIN planning p2
|
||||
ON p2.id_agent = p1.id_agent
|
||||
AND p2.id_planning <> p1.id_planning
|
||||
INNER JOIN structure st ON st.id_structure = p2.id_structure
|
||||
INNER JOIN creneau_horaire c2
|
||||
ON c2.id_planning = p2.id_planning
|
||||
AND c2.date_jour = c1.date_jour
|
||||
AND c1.heure_debut < c2.heure_fin
|
||||
AND c1.heure_fin > c2.heure_debut
|
||||
WHERE p1.id_planning = :planning_id"
|
||||
);
|
||||
$stmt->execute(['planning_id' => $planningId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function findDetailed(int $planningId): ?array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT p.id_planning, p.id_agent, p.id_structure, p.statut,
|
||||
s.annee, s.numero_semaine, s.date_debut, s.date_fin
|
||||
FROM planning p
|
||||
INNER JOIN semaine s ON s.id_semaine = p.id_semaine
|
||||
WHERE p.id_planning = :planning_id
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute(['planning_id' => $planningId]);
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
|
||||
public function countEntries(int $planningId): int
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT COUNT(*) FROM creneau_horaire WHERE id_planning = :planning_id');
|
||||
$stmt->execute(['planning_id' => $planningId]);
|
||||
return (int) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
public function entriesOutsideWeek(int $planningId, string $start, string $end): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT id_creneau, date_jour
|
||||
FROM creneau_horaire
|
||||
WHERE id_planning = :planning_id
|
||||
AND date_jour NOT BETWEEN :date_debut AND :date_fin"
|
||||
);
|
||||
$stmt->execute(['planning_id' => $planningId, 'date_debut' => $start, 'date_fin' => $end]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function internalOverlaps(int $planningId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT c1.id_creneau AS c1, c2.id_creneau AS c2, c1.date_jour
|
||||
FROM creneau_horaire c1
|
||||
INNER JOIN creneau_horaire c2
|
||||
ON c2.id_planning = c1.id_planning
|
||||
AND c2.date_jour = c1.date_jour
|
||||
AND c2.id_creneau > c1.id_creneau
|
||||
AND c1.heure_debut < c2.heure_fin
|
||||
AND c1.heure_fin > c2.heure_debut
|
||||
WHERE c1.id_planning = :planning_id"
|
||||
);
|
||||
$stmt->execute(['planning_id' => $planningId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function validatedExternalOverlaps(int $planningId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT DISTINCT c1.id_creneau AS c1, c2.id_creneau AS c2, c1.date_jour, st.nom AS structure_nom
|
||||
FROM planning p1
|
||||
INNER JOIN creneau_horaire c1 ON c1.id_planning = p1.id_planning
|
||||
INNER JOIN planning p2
|
||||
ON p2.id_agent = p1.id_agent
|
||||
AND p2.id_planning <> p1.id_planning
|
||||
AND p2.statut = 'VALIDE'
|
||||
INNER JOIN structure st ON st.id_structure = p2.id_structure
|
||||
INNER JOIN creneau_horaire c2
|
||||
ON c2.id_planning = p2.id_planning
|
||||
AND c2.date_jour = c1.date_jour
|
||||
AND c1.heure_debut < c2.heure_fin
|
||||
AND c1.heure_fin > c2.heure_debut
|
||||
WHERE p1.id_planning = :planning_id"
|
||||
);
|
||||
$stmt->execute(['planning_id' => $planningId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function findEntryDetailed(int $entryId): ?array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT c.id_creneau, c.id_planning, c.id_motif, c.date_jour,
|
||||
TIME_FORMAT(c.heure_debut, '%H:%i') AS heure_debut,
|
||||
TIME_FORMAT(c.heure_fin, '%H:%i') AS heure_fin,
|
||||
p.id_agent, p.id_structure, p.id_semaine, p.statut,
|
||||
sem.annee, sem.numero_semaine, sem.date_debut, sem.date_fin
|
||||
FROM creneau_horaire c
|
||||
INNER JOIN planning p ON p.id_planning = c.id_planning
|
||||
INNER JOIN semaine sem ON sem.id_semaine = p.id_semaine
|
||||
WHERE c.id_creneau = :entry_id
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute(['entry_id' => $entryId]);
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifie un créneau depuis la vue agent, y compris vers une autre semaine.
|
||||
* Le planning cible est créé si nécessaire et tout planning validé modifié
|
||||
* repasse automatiquement en brouillon.
|
||||
*/
|
||||
public function updateEntryFromAgentView(
|
||||
int $entryId,
|
||||
int $targetStructureId,
|
||||
int $motifId,
|
||||
string $date,
|
||||
string $start,
|
||||
string $end
|
||||
): array {
|
||||
try {
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
$entryStmt = $this->pdo->prepare(
|
||||
"SELECT c.id_creneau, c.id_planning, c.id_motif, c.date_jour, c.heure_debut, c.heure_fin,
|
||||
p.id_agent, p.id_structure, p.id_semaine, p.statut,
|
||||
sem.annee, sem.numero_semaine
|
||||
FROM creneau_horaire c
|
||||
INNER JOIN planning p ON p.id_planning = c.id_planning
|
||||
INNER JOIN semaine sem ON sem.id_semaine = p.id_semaine
|
||||
WHERE c.id_creneau = :entry_id
|
||||
FOR UPDATE"
|
||||
);
|
||||
$entryStmt->execute(['entry_id' => $entryId]);
|
||||
$entry = $entryStmt->fetch();
|
||||
if (!$entry) {
|
||||
throw new \DomainException('Le créneau à modifier est introuvable.');
|
||||
}
|
||||
|
||||
$targetDate = new DateTimeImmutable($date);
|
||||
if ((int) $targetDate->format('N') > 5) {
|
||||
throw new \DomainException('Le créneau doit être positionné entre le lundi et le vendredi.');
|
||||
}
|
||||
|
||||
$conflictStmt = $this->pdo->prepare(
|
||||
"SELECT c.id_creneau, p.statut, st.nom AS structure_nom,
|
||||
TIME_FORMAT(c.heure_debut, '%H:%i') AS heure_debut,
|
||||
TIME_FORMAT(c.heure_fin, '%H:%i') AS heure_fin
|
||||
FROM creneau_horaire c
|
||||
INNER JOIN planning p ON p.id_planning = c.id_planning
|
||||
INNER JOIN structure st ON st.id_structure = p.id_structure
|
||||
WHERE p.id_agent = :agent_id
|
||||
AND c.id_creneau <> :entry_id
|
||||
AND c.date_jour = :date_jour
|
||||
AND c.heure_debut < :heure_fin
|
||||
AND c.heure_fin > :heure_debut
|
||||
LIMIT 1
|
||||
FOR UPDATE"
|
||||
);
|
||||
$conflictStmt->execute([
|
||||
'agent_id' => (int) $entry['id_agent'],
|
||||
'entry_id' => $entryId,
|
||||
'date_jour' => $date,
|
||||
'heure_debut' => $start,
|
||||
'heure_fin' => $end,
|
||||
]);
|
||||
$conflict = $conflictStmt->fetch();
|
||||
if ($conflict) {
|
||||
throw new \DomainException(sprintf(
|
||||
'Double affectation impossible : un autre créneau existe déjà sur le lieu « %s » de %s à %s (%s).',
|
||||
$conflict['structure_nom'],
|
||||
$conflict['heure_debut'],
|
||||
$conflict['heure_fin'],
|
||||
strtolower((string) $conflict['statut'])
|
||||
));
|
||||
}
|
||||
|
||||
$targetYear = (int) $targetDate->format('o');
|
||||
$targetWeekNumber = (int) $targetDate->format('W');
|
||||
$targetWeekStart = $targetDate->modify('-' . ((int) $targetDate->format('N') - 1) . ' days')->setTime(0, 0);
|
||||
$targetWeekId = $this->ensureWeek($targetYear, $targetWeekNumber, $targetWeekStart, $targetWeekStart->modify('+6 days'));
|
||||
|
||||
$sourcePlanningId = (int) $entry['id_planning'];
|
||||
$targetPlanning = $this->findOrCreatePlanning(
|
||||
(int) $entry['id_agent'],
|
||||
$targetWeekId,
|
||||
$targetStructureId
|
||||
);
|
||||
$targetPlanningId = (int) $targetPlanning['id_planning'];
|
||||
|
||||
$updateEntry = $this->pdo->prepare(
|
||||
"UPDATE creneau_horaire
|
||||
SET id_planning = :planning_id,
|
||||
id_motif = :motif_id,
|
||||
date_jour = :date_jour,
|
||||
heure_debut = :heure_debut,
|
||||
heure_fin = :heure_fin
|
||||
WHERE id_creneau = :entry_id"
|
||||
);
|
||||
$updateEntry->execute([
|
||||
'planning_id' => $targetPlanningId,
|
||||
'motif_id' => $motifId,
|
||||
'date_jour' => $date,
|
||||
'heure_debut' => $start,
|
||||
'heure_fin' => $end,
|
||||
'entry_id' => $entryId,
|
||||
]);
|
||||
|
||||
$reopenedPlanningIds = [];
|
||||
if ((string) $entry['statut'] === 'VALIDE') {
|
||||
$reopenedPlanningIds[] = $sourcePlanningId;
|
||||
}
|
||||
if ((string) $targetPlanning['statut'] === 'VALIDE') {
|
||||
$reopenedPlanningIds[] = $targetPlanningId;
|
||||
}
|
||||
$reopenedPlanningIds = $this->reopenPlanningIds($reopenedPlanningIds);
|
||||
|
||||
$this->pdo->commit();
|
||||
|
||||
return [
|
||||
'entry_id' => $entryId,
|
||||
'agent_id' => (int) $entry['id_agent'],
|
||||
'year' => $targetYear,
|
||||
'week' => $targetWeekNumber,
|
||||
'source_planning_id' => $sourcePlanningId,
|
||||
'target_planning_id' => $targetPlanningId,
|
||||
'reopened_planning_ids' => $reopenedPlanningIds,
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un créneau depuis la vue par agent.
|
||||
* Si le planning était validé, il repasse en brouillon afin de signaler
|
||||
* que son contenu a été modifié. Le planning est conservé même s'il ne
|
||||
* contient plus aucun créneau.
|
||||
*/
|
||||
public function deleteEntryFromAgentView(int $entryId): array
|
||||
{
|
||||
try {
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
$entryStmt = $this->pdo->prepare(
|
||||
"SELECT c.id_creneau, c.id_planning, c.date_jour,
|
||||
p.id_agent, p.statut,
|
||||
sem.annee, sem.numero_semaine
|
||||
FROM creneau_horaire c
|
||||
INNER JOIN planning p ON p.id_planning = c.id_planning
|
||||
INNER JOIN semaine sem ON sem.id_semaine = p.id_semaine
|
||||
WHERE c.id_creneau = :entry_id
|
||||
FOR UPDATE"
|
||||
);
|
||||
$entryStmt->execute(['entry_id' => $entryId]);
|
||||
$entry = $entryStmt->fetch();
|
||||
|
||||
if (!$entry) {
|
||||
throw new \DomainException('Le créneau à supprimer est introuvable.');
|
||||
}
|
||||
|
||||
$deleteStmt = $this->pdo->prepare(
|
||||
'DELETE FROM creneau_horaire WHERE id_creneau = :entry_id'
|
||||
);
|
||||
$deleteStmt->execute(['entry_id' => $entryId]);
|
||||
|
||||
if ($deleteStmt->rowCount() !== 1) {
|
||||
throw new \DomainException('Le créneau a déjà été supprimé ou modifié. Rechargez la page.');
|
||||
}
|
||||
|
||||
$reopened = (string) $entry['statut'] === 'VALIDE';
|
||||
if ($reopened) {
|
||||
$reopenStmt = $this->pdo->prepare(
|
||||
"UPDATE planning
|
||||
SET statut = 'BROUILLON'
|
||||
WHERE id_planning = :planning_id"
|
||||
);
|
||||
$reopenStmt->execute(['planning_id' => (int) $entry['id_planning']]);
|
||||
}
|
||||
|
||||
$countStmt = $this->pdo->prepare(
|
||||
'SELECT COUNT(*) FROM creneau_horaire WHERE id_planning = :planning_id'
|
||||
);
|
||||
$countStmt->execute(['planning_id' => (int) $entry['id_planning']]);
|
||||
$remainingEntries = (int) $countStmt->fetchColumn();
|
||||
|
||||
$this->pdo->commit();
|
||||
|
||||
return [
|
||||
'entry_id' => $entryId,
|
||||
'planning_id' => (int) $entry['id_planning'],
|
||||
'agent_id' => (int) $entry['id_agent'],
|
||||
'year' => (int) $entry['annee'],
|
||||
'week' => (int) $entry['numero_semaine'],
|
||||
'date' => (string) $entry['date_jour'],
|
||||
'reopened' => $reopened,
|
||||
'remaining_entries' => $remainingEntries,
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Déplace ou duplique un créneau vers n'importe quel jour ouvré.
|
||||
* Le lieu, le motif et les horaires sont conservés, y compris lors d'un
|
||||
* changement de semaine.
|
||||
*/
|
||||
public function moveOrDuplicateEntry(int $entryId, string $targetDate, bool $duplicate): array
|
||||
{
|
||||
try {
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
$entryStmt = $this->pdo->prepare(
|
||||
"SELECT c.id_creneau, c.id_planning, c.id_motif, c.date_jour, c.heure_debut, c.heure_fin,
|
||||
p.id_agent, p.id_structure, p.id_semaine, p.statut,
|
||||
sem.annee, sem.numero_semaine
|
||||
FROM creneau_horaire c
|
||||
INNER JOIN planning p ON p.id_planning = c.id_planning
|
||||
INNER JOIN semaine sem ON sem.id_semaine = p.id_semaine
|
||||
WHERE c.id_creneau = :entry_id
|
||||
FOR UPDATE"
|
||||
);
|
||||
$entryStmt->execute(['entry_id' => $entryId]);
|
||||
$entry = $entryStmt->fetch();
|
||||
if (!$entry) {
|
||||
throw new \DomainException('Le créneau à déplacer est introuvable.');
|
||||
}
|
||||
|
||||
$targetDateObject = new DateTimeImmutable($targetDate);
|
||||
if ((int) $targetDateObject->format('N') > 5) {
|
||||
throw new \DomainException('Le jour cible doit être compris entre le lundi et le vendredi.');
|
||||
}
|
||||
|
||||
if (!$duplicate && $targetDate === (string) $entry['date_jour']) {
|
||||
$this->pdo->commit();
|
||||
return [
|
||||
'entry_id' => $entryId,
|
||||
'created_entry_id' => null,
|
||||
'agent_id' => (int) $entry['id_agent'],
|
||||
'year' => (int) $entry['annee'],
|
||||
'week' => (int) $entry['numero_semaine'],
|
||||
'planning_id' => (int) $entry['id_planning'],
|
||||
'target_planning_id' => (int) $entry['id_planning'],
|
||||
'reopened_planning_ids' => [],
|
||||
'action' => 'noop',
|
||||
];
|
||||
}
|
||||
|
||||
$excludeEntryId = $duplicate ? 0 : $entryId;
|
||||
$conflictStmt = $this->pdo->prepare(
|
||||
"SELECT c.id_creneau, p.statut, st.nom AS structure_nom,
|
||||
TIME_FORMAT(c.heure_debut, '%H:%i') AS heure_debut,
|
||||
TIME_FORMAT(c.heure_fin, '%H:%i') AS heure_fin
|
||||
FROM creneau_horaire c
|
||||
INNER JOIN planning p ON p.id_planning = c.id_planning
|
||||
INNER JOIN structure st ON st.id_structure = p.id_structure
|
||||
WHERE p.id_agent = :agent_id
|
||||
AND (:exclude_entry_id = 0 OR c.id_creneau <> :exclude_entry_id_compare)
|
||||
AND c.date_jour = :date_jour
|
||||
AND c.heure_debut < :heure_fin
|
||||
AND c.heure_fin > :heure_debut
|
||||
LIMIT 1
|
||||
FOR UPDATE"
|
||||
);
|
||||
$conflictStmt->execute([
|
||||
'agent_id' => (int) $entry['id_agent'],
|
||||
'exclude_entry_id' => $excludeEntryId,
|
||||
'exclude_entry_id_compare' => $excludeEntryId,
|
||||
'date_jour' => $targetDate,
|
||||
'heure_fin' => $entry['heure_fin'],
|
||||
'heure_debut' => $entry['heure_debut'],
|
||||
]);
|
||||
$conflict = $conflictStmt->fetch();
|
||||
if ($conflict) {
|
||||
throw new \DomainException(sprintf(
|
||||
'Action impossible : l’agent possède déjà un créneau sur le lieu « %s » de %s à %s (%s).',
|
||||
$conflict['structure_nom'],
|
||||
$conflict['heure_debut'],
|
||||
$conflict['heure_fin'],
|
||||
strtolower((string) $conflict['statut'])
|
||||
));
|
||||
}
|
||||
|
||||
$targetYear = (int) $targetDateObject->format('o');
|
||||
$targetWeekNumber = (int) $targetDateObject->format('W');
|
||||
$targetMonday = $targetDateObject->modify('-' . ((int) $targetDateObject->format('N') - 1) . ' days')->setTime(0, 0);
|
||||
$targetWeekId = $this->ensureWeek($targetYear, $targetWeekNumber, $targetMonday, $targetMonday->modify('+6 days'));
|
||||
$targetPlanning = $this->findOrCreatePlanning(
|
||||
(int) $entry['id_agent'],
|
||||
$targetWeekId,
|
||||
(int) $entry['id_structure']
|
||||
);
|
||||
$targetPlanningId = (int) $targetPlanning['id_planning'];
|
||||
$sourcePlanningId = (int) $entry['id_planning'];
|
||||
|
||||
$createdEntryId = null;
|
||||
if ($duplicate) {
|
||||
$insertStmt = $this->pdo->prepare(
|
||||
"INSERT INTO creneau_horaire (id_planning, id_motif, date_jour, heure_debut, heure_fin)
|
||||
VALUES (:planning_id, :motif_id, :date_jour, :heure_debut, :heure_fin)"
|
||||
);
|
||||
$insertStmt->execute([
|
||||
'planning_id' => $targetPlanningId,
|
||||
'motif_id' => (int) $entry['id_motif'],
|
||||
'date_jour' => $targetDate,
|
||||
'heure_debut' => $entry['heure_debut'],
|
||||
'heure_fin' => $entry['heure_fin'],
|
||||
]);
|
||||
$createdEntryId = (int) $this->pdo->lastInsertId();
|
||||
} else {
|
||||
$moveStmt = $this->pdo->prepare(
|
||||
"UPDATE creneau_horaire
|
||||
SET id_planning = :target_planning_id,
|
||||
date_jour = :date_jour
|
||||
WHERE id_creneau = :entry_id"
|
||||
);
|
||||
$moveStmt->execute([
|
||||
'target_planning_id' => $targetPlanningId,
|
||||
'date_jour' => $targetDate,
|
||||
'entry_id' => $entryId,
|
||||
]);
|
||||
}
|
||||
|
||||
$reopenedPlanningIds = [];
|
||||
if (!$duplicate && (string) $entry['statut'] === 'VALIDE') {
|
||||
$reopenedPlanningIds[] = $sourcePlanningId;
|
||||
}
|
||||
if ((string) $targetPlanning['statut'] === 'VALIDE') {
|
||||
$reopenedPlanningIds[] = $targetPlanningId;
|
||||
}
|
||||
$reopenedPlanningIds = $this->reopenPlanningIds($reopenedPlanningIds);
|
||||
|
||||
$this->pdo->commit();
|
||||
|
||||
return [
|
||||
'entry_id' => $entryId,
|
||||
'created_entry_id' => $createdEntryId,
|
||||
'agent_id' => (int) $entry['id_agent'],
|
||||
'year' => $targetYear,
|
||||
'week' => $targetWeekNumber,
|
||||
'planning_id' => $sourcePlanningId,
|
||||
'target_planning_id' => $targetPlanningId,
|
||||
'reopened_planning_ids' => $reopenedPlanningIds,
|
||||
'action' => $duplicate ? 'duplicate' : 'move',
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function copyAgentWeek(
|
||||
int $agentId,
|
||||
int $sourceYear,
|
||||
int $sourceWeekNumber,
|
||||
int $targetYear,
|
||||
int $targetWeekNumber,
|
||||
string $mode
|
||||
): array {
|
||||
if (!in_array($mode, ['merge', 'replace'], true)) {
|
||||
throw new \DomainException('Mode de duplication invalide.');
|
||||
}
|
||||
if ($sourceYear === $targetYear && $sourceWeekNumber === $targetWeekNumber) {
|
||||
throw new \DomainException('La semaine cible doit être différente de la semaine source.');
|
||||
}
|
||||
|
||||
$sourceStmt = $this->pdo->prepare(
|
||||
"SELECT WEEKDAY(c.date_jour) + 1 AS jour_semaine,
|
||||
p.id_structure, c.id_motif,
|
||||
TIME_FORMAT(c.heure_debut, '%H:%i:%s') AS heure_debut,
|
||||
TIME_FORMAT(c.heure_fin, '%H:%i:%s') AS heure_fin
|
||||
FROM planning p
|
||||
INNER JOIN semaine sem ON sem.id_semaine = p.id_semaine
|
||||
INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning
|
||||
WHERE p.id_agent = :agent_id
|
||||
AND sem.annee = :annee
|
||||
AND sem.numero_semaine = :semaine
|
||||
AND WEEKDAY(c.date_jour) BETWEEN 0 AND 4
|
||||
ORDER BY c.date_jour, c.heure_debut"
|
||||
);
|
||||
$sourceStmt->execute([
|
||||
'agent_id' => $agentId,
|
||||
'annee' => $sourceYear,
|
||||
'semaine' => $sourceWeekNumber,
|
||||
]);
|
||||
$sourceEntries = $sourceStmt->fetchAll();
|
||||
if (!$sourceEntries) {
|
||||
throw new \DomainException('La semaine source ne contient aucun créneau à dupliquer.');
|
||||
}
|
||||
|
||||
$targetMonday = (new DateTimeImmutable())->setISODate($targetYear, $targetWeekNumber, 1)->setTime(0, 0);
|
||||
$targetEntries = array_map(static function (array $entry) use ($targetMonday): array {
|
||||
return [
|
||||
'date_jour' => $targetMonday->modify('+' . ((int) $entry['jour_semaine'] - 1) . ' days')->format('Y-m-d'),
|
||||
'id_structure' => (int) $entry['id_structure'],
|
||||
'id_motif' => (int) $entry['id_motif'],
|
||||
'heure_debut' => (string) $entry['heure_debut'],
|
||||
'heure_fin' => (string) $entry['heure_fin'],
|
||||
];
|
||||
}, $sourceEntries);
|
||||
|
||||
try {
|
||||
$this->pdo->beginTransaction();
|
||||
$targetWeekId = $this->ensureWeek($targetYear, $targetWeekNumber, $targetMonday, $targetMonday->modify('+6 days'));
|
||||
|
||||
$existingPlanningStmt = $this->pdo->prepare(
|
||||
"SELECT id_planning, id_structure, statut
|
||||
FROM planning
|
||||
WHERE id_agent = :agent_id AND id_semaine = :week_id
|
||||
FOR UPDATE"
|
||||
);
|
||||
$existingPlanningStmt->execute(['agent_id' => $agentId, 'week_id' => $targetWeekId]);
|
||||
$existingPlannings = $existingPlanningStmt->fetchAll();
|
||||
|
||||
$reopened = [];
|
||||
if ($mode === 'replace') {
|
||||
if ($existingPlannings) {
|
||||
$ids = array_map(static fn(array $row): int => (int) $row['id_planning'], $existingPlannings);
|
||||
foreach ($existingPlannings as $planning) {
|
||||
if ((string) $planning['statut'] === 'VALIDE') {
|
||||
$reopened[] = (int) $planning['id_planning'];
|
||||
}
|
||||
}
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
$delete = $this->pdo->prepare("DELETE FROM creneau_horaire WHERE id_planning IN ($placeholders)");
|
||||
$delete->execute($ids);
|
||||
}
|
||||
} else {
|
||||
$existingEntriesStmt = $this->pdo->prepare(
|
||||
"SELECT c.date_jour, c.heure_debut, c.heure_fin, st.nom AS structure_nom
|
||||
FROM planning p
|
||||
INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning
|
||||
INNER JOIN structure st ON st.id_structure = p.id_structure
|
||||
WHERE p.id_agent = :agent_id AND p.id_semaine = :week_id
|
||||
FOR UPDATE"
|
||||
);
|
||||
$existingEntriesStmt->execute(['agent_id' => $agentId, 'week_id' => $targetWeekId]);
|
||||
$existingEntries = $existingEntriesStmt->fetchAll();
|
||||
foreach ($targetEntries as $candidate) {
|
||||
foreach ($existingEntries as $existing) {
|
||||
if ($candidate['date_jour'] === $existing['date_jour']
|
||||
&& $candidate['heure_debut'] < $existing['heure_fin']
|
||||
&& $candidate['heure_fin'] > $existing['heure_debut']) {
|
||||
throw new \DomainException(sprintf(
|
||||
'Duplication impossible : un créneau existe déjà le %s de %s à %s sur « %s ».',
|
||||
$candidate['date_jour'],
|
||||
substr((string) $existing['heure_debut'], 0, 5),
|
||||
substr((string) $existing['heure_fin'], 0, 5),
|
||||
$existing['structure_nom']
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$planningCache = [];
|
||||
foreach ($existingPlannings as $planning) {
|
||||
$planningCache[(int) $planning['id_structure']] = [
|
||||
'id_planning' => (int) $planning['id_planning'],
|
||||
'statut' => (string) $planning['statut'],
|
||||
];
|
||||
}
|
||||
|
||||
$insertEntry = $this->pdo->prepare(
|
||||
"INSERT INTO creneau_horaire (id_planning, id_motif, date_jour, heure_debut, heure_fin)
|
||||
VALUES (:planning_id, :motif_id, :date_jour, :heure_debut, :heure_fin)"
|
||||
);
|
||||
|
||||
foreach ($targetEntries as $candidate) {
|
||||
$structureId = (int) $candidate['id_structure'];
|
||||
if (!isset($planningCache[$structureId])) {
|
||||
$planningCache[$structureId] = $this->findOrCreatePlanning($agentId, $targetWeekId, $structureId);
|
||||
}
|
||||
$planning = $planningCache[$structureId];
|
||||
if ((string) $planning['statut'] === 'VALIDE') {
|
||||
$reopened[] = (int) $planning['id_planning'];
|
||||
}
|
||||
$insertEntry->execute([
|
||||
'planning_id' => (int) $planning['id_planning'],
|
||||
'motif_id' => (int) $candidate['id_motif'],
|
||||
'date_jour' => $candidate['date_jour'],
|
||||
'heure_debut' => $candidate['heure_debut'],
|
||||
'heure_fin' => $candidate['heure_fin'],
|
||||
]);
|
||||
}
|
||||
|
||||
$reopened = $this->reopenPlanningIds($reopened);
|
||||
$this->pdo->commit();
|
||||
|
||||
return [
|
||||
'agent_id' => $agentId,
|
||||
'year' => $targetYear,
|
||||
'week' => $targetWeekNumber,
|
||||
'copied_entries' => count($targetEntries),
|
||||
'reopened_planning_ids' => $reopened,
|
||||
'mode' => $mode,
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureWeek(int $year, int $weekNumber, DateTimeImmutable $weekStart, DateTimeImmutable $weekEnd): int
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"INSERT INTO semaine (annee, numero_semaine, date_debut, date_fin, statut)
|
||||
VALUES (:annee, :numero, :date_debut, :date_fin, 'OUVERTE')
|
||||
ON DUPLICATE KEY UPDATE id_semaine = LAST_INSERT_ID(id_semaine)"
|
||||
);
|
||||
$stmt->execute([
|
||||
'annee' => $year,
|
||||
'numero' => $weekNumber,
|
||||
'date_debut' => $weekStart->format('Y-m-d'),
|
||||
'date_fin' => $weekEnd->format('Y-m-d'),
|
||||
]);
|
||||
$weekId = (int) $this->pdo->lastInsertId();
|
||||
if ($weekId > 0) {
|
||||
return $weekId;
|
||||
}
|
||||
|
||||
$find = $this->pdo->prepare('SELECT id_semaine FROM semaine WHERE annee = :annee AND numero_semaine = :numero');
|
||||
$find->execute(['annee' => $year, 'numero' => $weekNumber]);
|
||||
$weekId = (int) $find->fetchColumn();
|
||||
if ($weekId <= 0) {
|
||||
throw new \RuntimeException('Impossible de créer ou retrouver la semaine cible.');
|
||||
}
|
||||
return $weekId;
|
||||
}
|
||||
|
||||
/** @return array{id_planning:int,statut:string} */
|
||||
private function findOrCreatePlanning(int $agentId, int $weekId, int $structureId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"SELECT id_planning, statut
|
||||
FROM planning
|
||||
WHERE id_agent = :agent_id AND id_semaine = :week_id AND id_structure = :structure_id
|
||||
FOR UPDATE"
|
||||
);
|
||||
$stmt->execute([
|
||||
'agent_id' => $agentId,
|
||||
'week_id' => $weekId,
|
||||
'structure_id' => $structureId,
|
||||
]);
|
||||
$planning = $stmt->fetch();
|
||||
if ($planning) {
|
||||
return ['id_planning' => (int) $planning['id_planning'], 'statut' => (string) $planning['statut']];
|
||||
}
|
||||
|
||||
$insert = $this->pdo->prepare(
|
||||
"INSERT INTO planning (id_agent, id_semaine, id_structure, statut)
|
||||
VALUES (:agent_id, :week_id, :structure_id, 'BROUILLON')"
|
||||
);
|
||||
$insert->execute([
|
||||
'agent_id' => $agentId,
|
||||
'week_id' => $weekId,
|
||||
'structure_id' => $structureId,
|
||||
]);
|
||||
return ['id_planning' => (int) $this->pdo->lastInsertId(), 'statut' => 'BROUILLON'];
|
||||
}
|
||||
|
||||
/** @return int[] */
|
||||
private function reopenPlanningIds(array $planningIds): array
|
||||
{
|
||||
$planningIds = array_values(array_unique(array_map('intval', array_filter($planningIds))));
|
||||
if (!$planningIds) {
|
||||
return [];
|
||||
}
|
||||
$placeholders = implode(',', array_fill(0, count($planningIds), '?'));
|
||||
$stmt = $this->pdo->prepare(
|
||||
"UPDATE planning SET statut = 'BROUILLON', date_validation = NULL WHERE id_planning IN ($placeholders)"
|
||||
);
|
||||
$stmt->execute($planningIds);
|
||||
return $planningIds;
|
||||
}
|
||||
|
||||
|
||||
/** @param int[] $planningIds */
|
||||
public function markValidatedMany(array $planningIds): int
|
||||
{
|
||||
$planningIds = array_values(array_unique(array_filter(array_map('intval', $planningIds), static fn(int $id): bool => $id > 0)));
|
||||
if ($planningIds === []) {
|
||||
return 0;
|
||||
}
|
||||
$placeholders = implode(',', array_fill(0, count($planningIds), '?'));
|
||||
$stmt = $this->pdo->prepare(
|
||||
"UPDATE planning
|
||||
SET statut = 'VALIDE', date_validation = CURRENT_TIMESTAMP
|
||||
WHERE statut = 'BROUILLON' AND id_planning IN ($placeholders)"
|
||||
);
|
||||
$stmt->execute($planningIds);
|
||||
return $stmt->rowCount();
|
||||
}
|
||||
|
||||
public function markValidated(int $planningId): void
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"UPDATE planning SET statut = 'VALIDE', date_validation = CURRENT_TIMESTAMP WHERE id_planning = :planning_id"
|
||||
);
|
||||
$stmt->execute(['planning_id' => $planningId]);
|
||||
}
|
||||
|
||||
public function reopen(int $planningId): bool
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
"UPDATE planning
|
||||
SET statut = 'BROUILLON', date_validation = NULL
|
||||
WHERE id_planning = :planning_id AND statut = 'VALIDE'"
|
||||
);
|
||||
$stmt->execute(['planning_id' => $planningId]);
|
||||
return $stmt->rowCount() === 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user