Compare commits

..

6 Commits

Author SHA1 Message Date
gitea-actions
10a0ab0809 chore: bump version to v0.1.78
All checks were successful
Build & Push Docker Image / build (push) Successful in 38s
Auto Tag Develop / tag (push) Successful in 6s
2026-04-02 12:34:10 +00:00
055f1187f9 fix : wording
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-04-02 14:34:00 +02:00
gitea-actions
f3ed359d3f chore: bump version to v0.1.77
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
Build & Push Docker Image / build (push) Successful in 57s
2026-04-02 10:05:12 +00:00
906c245451 feat(deploy) : add maintenance mode with automatic toggle during deploy
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-04-02 11:56:13 +02:00
gitea-actions
100ab340d4 chore: bump version to v0.1.76
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build & Push Docker Image / build (push) Successful in 39s
2026-04-02 08:55:05 +00:00
0257e59671 [#SIRH-21] Revoir l'affichage des RTT pour les semaines qui se chevauchent (#13)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

Reviewed-on: #13
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-04-02 08:54:55 +00:00
13 changed files with 226 additions and 45 deletions

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '0.1.75'
app.version: '0.1.78'

View File

@@ -8,6 +8,9 @@ export SIRH_IMAGE_TAG="$TAG"
echo "==> Deploying sirh:${TAG}..."
echo "==> Enabling maintenance mode..."
touch maintenance.on
echo "==> Pulling image..."
docker compose pull
@@ -24,5 +27,8 @@ echo "==> Clearing cache..."
docker compose exec -T -u www-data app php bin/console cache:clear --env=prod
docker compose exec -T -u www-data app php bin/console cache:warmup --env=prod
echo "==> Disabling maintenance mode..."
rm -f maintenance.on
VERSION=$(docker compose exec -T app cat config/version.yaml | grep 'app.version' | awk -F"'" '{print $2}')
echo "==> Deployed v${VERSION}"

50
deploy/maintenance.html Normal file
View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Maintenance en cours</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
color: #1f2937;
}
.container {
text-align: center;
padding: 3rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
max-width: 500px;
width: 90%;
}
.icon {
font-size: 4rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.5rem;
margin-bottom: 0.75rem;
color: #111827;
}
p {
font-size: 1rem;
color: #6b7280;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">&#128736;</div>
<h1>Maintenance en cours</h1>
<p>L'application est temporairement indisponible pour mise a jour. Elle sera de retour dans quelques instants.</p>
</div>
</body>
</html>

View File

@@ -2,6 +2,23 @@ server {
listen 80;
server_name sirh.malio-dev.fr;
root /var/www/sirh/public;
# Maintenance mode : si le fichier maintenance.on existe, renvoyer la page 503
if (-f /var/www/sirh/maintenance.on) {
return 503;
}
error_page 503 @maintenance;
location @maintenance {
rewrite ^(.*)$ /maintenance.html break;
}
location = /maintenance.html {
internal;
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;

View File

@@ -197,6 +197,23 @@ server {
listen 80;
server_name sirh.malio-dev.fr;
root /var/www/sirh/public;
# Maintenance mode
if (-f /var/www/sirh/maintenance.on) {
return 503;
}
error_page 503 @maintenance;
location @maintenance {
rewrite ^(.*)$ /maintenance.html break;
}
location = /maintenance.html {
internal;
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
@@ -207,9 +224,10 @@ server {
}
```
Activer le site :
Copier la page de maintenance et activer le site :
```bash
cp deploy/maintenance.html /var/www/sirh/public/maintenance.html
sudo ln -sf /etc/nginx/sites-available/sirh.conf /etc/nginx/sites-enabled/sirh.conf
sudo nginx -t && sudo systemctl reload nginx
```
@@ -251,6 +269,8 @@ rm /tmp/sirh.sql
├── config/jwt/
│ ├── private.pem
│ └── public.pem
├── public/
│ └── maintenance.html
└── uploads/
```
@@ -266,7 +286,24 @@ cd /var/www/sirh
./deploy.sh v0.1.61 # deploie une version specifique
```
C'est tout. Le script pull l'image, redemarre le conteneur, lance les migrations et vide le cache.
Le script active automatiquement la maintenance pendant le deploy et la desactive a la fin.
---
## Maintenance manuelle
Activer la maintenance (sans deployer) :
```bash
cd /var/www/sirh
touch maintenance.on
```
Desactiver :
```bash
rm maintenance.on
```
---

View File

@@ -245,7 +245,7 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
- pour `FORFAIT`:
- pris: basé sur toutes les absences (demi-journées incluses)
- restants = acquis - pris (borné à 0)
- paiement congés N-1: saisie RH via `PATCH /employees/{id}/paid-leave-days` (body: `paidLeaveDays`, `year`). Stocké dans `employee_leave_balances.paid_leave_days`. Les jours payés sont soustraits du reste à prendre N-1 (`previousYearRemainingDays = max(0, acquis_N-1 - pris_N-1 - payés)`). Uniquement pour les contrats forfait.
- paiement congés N-1: saisie RH via `PATCH /employees/{id}/paid-leave-days` (body: `paidLeaveDays`, `year`). Stocké dans `employee_leave_balances.paid_leave_days`. Les jours payés réduisent le stock N-1 **avant** l'attribution des jours pris : `disponible_N-1 = max(0, acquis_N-1 - payés)`, puis `pris_N-1 = min(disponible_N-1, total_pris)`, surplus pris basculé sur N. Reste à prendre N-1 = `max(0, disponible_N-1 - pris_N-1)`. Uniquement pour les contrats forfait.
- report annuel:
- le reliquat (`restants`) de l'exercice précédent est reporté dans les acquis de l'exercice courant
- pour `CDI`/`CDD` non forfait: report séparé jours + samedis
@@ -275,8 +275,9 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
- total mensuel des minutes de récupération
- compteur global exercice = `report N-1 + acquis N`
- attribution mensuelle des semaines:
- une semaine ISO est affichée une seule fois, dans le mois qui contient le **samedi** de cette semaine
- si le weekend tombe en début de mois suivant, c'est le mois suivant qui porte la semaine
- une semaine ISO qui chevauche deux mois est affichée dans **les deux mois**, avec les valeurs réparties proportionnellement aux minutes travaillées de chaque portion
- le calcul des heures supplémentaires reste hebdomadaire (seuils 35h/39h/43h appliqués sur la semaine entière), seul l'affichage est scindé
- exemple: S14 lundi-mardi en mars, mercredi-dimanche en avril → la S14 apparaît en mars (avec la part des heures de lun-mar) et en avril (avec la part mer-dim)
- logique de calcul:
- base identique aux calculs d'heures supplémentaires de la vue semaine Heures
- minutes de récupération hebdomadaires = `HS totales + bonus 25% + bonus 50%`

View File

@@ -32,9 +32,9 @@ Principe:
## 4) Attribution mensuelle des semaines
- une semaine ISO est affichee une seule fois, dans le mois qui contient le **samedi** de cette semaine
- si le weekend tombe en debut du mois suivant, c'est ce mois qui porte la semaine
- pas de prorata: la totalite des minutes de recuperation de la semaine est comptee dans un seul mois
- une semaine ISO qui chevauche deux mois est affichee dans **les deux mois**, avec les valeurs reparties proportionnellement aux minutes travaillees de chaque portion
- le calcul des heures supplementaires reste hebdomadaire (seuils 35h/39h/43h appliques sur la semaine entiere), seul l'affichage est scinde
- exemple: S14 lundi-mardi en mars, mercredi-dimanche en avril → la S14 apparait en mars (part lun-mar) et en avril (part mer-dim)
## 5) Table cible

View File

@@ -30,7 +30,7 @@
class="rounded-md bg-primary-500 px-8 py-2 font-bold text-white hover:bg-primary-600"
@click="openPaymentDrawer"
>
+ Payer les RRT
+ Payer les RTT
</button>
</div>
</div>

View File

@@ -44,6 +44,7 @@
<option value="contract_suspension">Suspension</option>
<option value="rtt_payment">Paiement RTT</option>
<option value="fractioned_days">Jours fractionnés</option>
<option value="paid_leave_days">Congés N-1 payés</option>
</select>
</div>
<button
@@ -241,6 +242,7 @@ const entityTypeLabel = (type: string): string => {
contract_suspension: 'Suspension',
rtt_payment: 'RTT',
fractioned_days: 'Fract.',
paid_leave_days: 'Congés payés',
}
return map[type] ?? type
}

View File

@@ -6,6 +6,9 @@ namespace App\Dto\Rtt;
final class WeekRecoveryDetail
{
/**
* @param array<string, int> $dailyMinutes date (Y-m-d) => worked minutes
*/
public function __construct(
public int $overtimeMinutes = 0,
public int $base25Minutes = 0,
@@ -13,5 +16,6 @@ final class WeekRecoveryDetail
public int $base50Minutes = 0,
public int $bonus50Minutes = 0,
public int $totalMinutes = 0,
public array $dailyMinutes = [],
) {}
}

View File

@@ -46,7 +46,7 @@ final readonly class RttRecoveryComputationService
}
/**
* @return list<array{month:int,weekNumber:int,start:DateTimeImmutable,end:DateTimeImmutable}>
* @return list<array{weekNumber:int,start:DateTimeImmutable,end:DateTimeImmutable}>
*/
public function buildWeeksForExercise(DateTimeImmutable $from, DateTimeImmutable $to): array
{
@@ -61,10 +61,7 @@ final readonly class RttRecoveryComputationService
$effectiveEnd = $end > $to ? $to : $end;
if ($effectiveEnd >= $effectiveStart) {
$saturday = $start->modify('+5 days');
$monthAnchor = $saturday < $from ? $from : ($saturday > $to ? $to : $saturday);
$weeks[] = [
'month' => (int) $monthAnchor->format('n'),
$weeks[] = [
'weekNumber' => (int) $effectiveStart->format('W'),
'start' => $start,
'end' => $end,
@@ -82,7 +79,6 @@ final readonly class RttRecoveryComputationService
$weeks = $this->buildWeeksForExercise($from, $to);
$weekRanges = array_map(
static fn (array $week): array => [
'month' => (int) $week['month'],
'weekNumber' => (int) $week['weekNumber'],
'start' => $week['start'],
'end' => $week['end'],
@@ -108,7 +104,7 @@ final readonly class RttRecoveryComputationService
}
/**
* @param list<array{month:int,weekNumber:int,start:DateTimeImmutable,end:DateTimeImmutable}> $weeks
* @param list<array{weekNumber:int,start:DateTimeImmutable,end:DateTimeImmutable}> $weeks
*
* @return array<string, WeekRecoveryDetail>
*/
@@ -189,6 +185,7 @@ final readonly class RttRecoveryComputationService
}
$weeklyTotalMinutes = 0;
$dailyWorkedMinutes = [];
$employeeContractsByDate = [];
foreach ($weekDays as $date) {
$employeeContractsByDate[$date] = $contractsByDate[$employeeId][$date] ?? null;
@@ -198,6 +195,7 @@ final readonly class RttRecoveryComputationService
$metrics = $metricsByDate[$date] ?? new WorkMetrics();
$metrics->addCreditedMinutes($creditedByDate[$date] ?? 0);
$weeklyTotalMinutes += $metrics->totalMinutes;
$dailyWorkedMinutes[$date] = $metrics->totalMinutes;
}
if ([] === $weekDays) {
@@ -244,6 +242,7 @@ final readonly class RttRecoveryComputationService
base50Minutes: $base50,
bonus50Minutes: $bonus50,
totalMinutes: $totalMinutes,
dailyMinutes: $dailyWorkedMinutes,
);
}

View File

@@ -95,6 +95,15 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
$fractionedDays = $this->resolveFractionedDays($employee, $yearSummary['ruleCode'], $year);
$paidLeaveDays = $this->resolvePaidLeaveDays($employee, $yearSummary['ruleCode'], $year);
// For forfait contracts, paid days reduce N-1 stock before taken-day attribution.
// Recompute with paidLeaveDays so taken days shift from N-1 to N when N-1 is consumed by payment.
if ($paidLeaveDays > 0.0) {
$yearSummary = $this->computeYearSummary($employee, $year, $paidLeaveDays);
if (null === $yearSummary) {
return $summary;
}
}
$summary->isSupported = true;
$summary->ruleCode = $yearSummary['ruleCode'];
$summary->acquiredDays = $yearSummary['acquiredDays'] + $fractionedDays;
@@ -107,7 +116,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
$summary->remainingSaturdays = $yearSummary['remainingSaturdays'];
$summary->previousYearAcquiredDays = $yearSummary['previousYearAcquiredDays'];
$summary->previousYearTakenDays = $yearSummary['previousYearTakenDays'];
$summary->previousYearRemainingDays = max(0.0, $yearSummary['previousYearRemainingDays'] - $paidLeaveDays);
$summary->previousYearRemainingDays = $yearSummary['previousYearRemainingDays'];
$summary->previousYearPaidDays = $paidLeaveDays;
[$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year);
@@ -131,7 +140,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
* previousYearRemainingDays: float
* }
*/
public function computeYearSummary(Employee $employee, int $targetYear): ?array
public function computeYearSummary(Employee $employee, int $targetYear, float $paidLeaveDays = 0.0): ?array
{
$firstYear = max($this->resolveFirstComputationYear($employee), $targetYear - 1);
if ($targetYear < $firstYear) {
@@ -271,13 +280,15 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
} else {
// Forfait: no "en cours d'acquisition" counter, all rights are in acquired.
// Suspensions do not impact forfait 218 leave calculation.
// Taken days are first deducted from N-1 carry, then from current year.
$previousYearAcquired = $carryDays;
$takenFromPrevious = min(max(0.0, $previousYearAcquired), $takenDays);
$previousYearTaken = $takenFromPrevious;
$takenFromCurrent = $takenDays - $takenFromPrevious;
// Paid days reduce N-1 stock first, then taken days are attributed to what remains in N-1.
$previousYearAcquired = $carryDays;
$effectivePaidDays = ($year === $targetYear) ? $paidLeaveDays : 0.0;
$availableAfterPayment = max(0.0, $previousYearAcquired - $effectivePaidDays);
$takenFromPrevious = min($availableAfterPayment, $takenDays);
$previousYearTaken = $takenFromPrevious;
$takenFromCurrent = $takenDays - $takenFromPrevious;
$previousYearRemaining = max(0.0, $previousYearAcquired - $takenFromPrevious);
$previousYearRemaining = max(0.0, $availableAfterPayment - $takenFromPrevious);
$acquiredDays = $leavePolicy['acquiredDays'];
$accruingDays = 0.0;

View File

@@ -71,7 +71,6 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
$weeks = $this->rttRecoveryService->buildWeeksForExercise($periodFrom, $periodTo);
$weekRanges = array_map(
static fn (array $week): array => [
'month' => (int) $week['month'],
'weekNumber' => (int) $week['weekNumber'],
'start' => $week['start'],
'end' => $week['end'],
@@ -118,25 +117,7 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
$summary->rttStartDate = $this->rttStartDate;
}
}
$summary->weeks = array_map(
static function (array $week) use ($currentByWeekStart) {
$detail = $currentByWeekStart[$week['start']->format('Y-m-d')] ?? new WeekRecoveryDetail();
return new EmployeeRttWeekSummary(
month: (int) $week['month'],
weekNumber: (int) $week['weekNumber'],
weekStart: $week['start']->format('Y-m-d'),
weekEnd: $week['end']->format('Y-m-d'),
overtimeMinutes: $detail->overtimeMinutes,
base25Minutes: $detail->base25Minutes,
bonus25Minutes: $detail->bonus25Minutes,
base50Minutes: $detail->base50Minutes,
bonus50Minutes: $detail->bonus50Minutes,
totalMinutes: $detail->totalMinutes,
);
},
$weekRanges
);
$summary->weeks = $this->buildWeekSummaries($weekRanges, $currentByWeekStart, $periodFrom, $periodTo);
// Post-process: distribute deficit weeks across cumulative balance (50% first, then 25%)
$cumulative50 = $carry->base50Minutes + $carry->bonus50Minutes;
@@ -269,4 +250,77 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
return $weekEnd;
}
/**
* Build week summaries, splitting weeks that span two months into two entries
* with values distributed proportionally based on daily worked minutes.
*
* @param list<array{weekNumber:int,start:DateTimeImmutable,end:DateTimeImmutable}> $weekRanges
* @param array<string, WeekRecoveryDetail> $recoveryByWeek
*
* @return list<EmployeeRttWeekSummary>
*/
private function buildWeekSummaries(array $weekRanges, array $recoveryByWeek, DateTimeImmutable $periodFrom, DateTimeImmutable $periodTo): array
{
$result = [];
foreach ($weekRanges as $week) {
$weekStart = $week['start'];
$weekEnd = $week['end'];
$weekKey = $weekStart->format('Y-m-d');
$detail = $recoveryByWeek[$weekKey] ?? new WeekRecoveryDetail();
$effectiveStart = $weekStart < $periodFrom ? $periodFrom : $weekStart;
$effectiveEnd = $weekEnd > $periodTo ? $periodTo : $weekEnd;
$startMonth = (int) $effectiveStart->format('n');
$endMonth = (int) $effectiveEnd->format('n');
if ($startMonth === $endMonth) {
$result[] = new EmployeeRttWeekSummary(
month: $startMonth,
weekNumber: (int) $week['weekNumber'],
weekStart: $weekStart->format('Y-m-d'),
weekEnd: $weekEnd->format('Y-m-d'),
overtimeMinutes: $detail->overtimeMinutes,
base25Minutes: $detail->base25Minutes,
bonus25Minutes: $detail->bonus25Minutes,
base50Minutes: $detail->base50Minutes,
bonus50Minutes: $detail->bonus50Minutes,
totalMinutes: $detail->totalMinutes,
);
continue;
}
// Week spans two months — split proportionally by daily worked minutes
$monthMinutes = [];
foreach ($detail->dailyMinutes as $date => $mins) {
$m = (int) new DateTimeImmutable($date)->format('n');
$monthMinutes[$m] = ($monthMinutes[$m] ?? 0) + $mins;
}
$totalWorked = array_sum($monthMinutes);
foreach ([$startMonth, $endMonth] as $month) {
$portion = $monthMinutes[$month] ?? 0;
$ratio = $totalWorked > 0 ? $portion / $totalWorked : 0.0;
$result[] = new EmployeeRttWeekSummary(
month: $month,
weekNumber: (int) $week['weekNumber'],
weekStart: $weekStart->format('Y-m-d'),
weekEnd: $weekEnd->format('Y-m-d'),
overtimeMinutes: (int) round($detail->overtimeMinutes * $ratio),
base25Minutes: (int) round($detail->base25Minutes * $ratio),
bonus25Minutes: (int) round($detail->bonus25Minutes * $ratio),
base50Minutes: (int) round($detail->base50Minutes * $ratio),
bonus50Minutes: (int) round($detail->bonus50Minutes * $ratio),
totalMinutes: (int) round($detail->totalMinutes * $ratio),
);
}
}
return $result;
}
}