Compare commits

...

9 Commits

Author SHA1 Message Date
gitea-actions
8563ddb08c chore: bump version to v0.1.50
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m12s
2026-03-17 10:54:32 +00:00
353d4d9d2b fix : correction calcule prorata congés avec une suspension de contrat
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-03-17 11:54:23 +01:00
gitea-actions
8745e5e425 chore: bump version to v0.1.49
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m14s
2026-03-17 10:25:15 +00:00
4d8c850a77 Merge remote-tracking branch 'origin/develop' into develop
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-03-17 11:25:05 +01:00
1974ace1f2 fix : correction validation des heures qui bloque la modification 2026-03-17 11:24:56 +01:00
gitea-actions
a99a12a759 chore: bump version to v0.1.48
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m15s
2026-03-17 10:17:34 +00:00
548b5d63a6 fix : correction calcule des RTT
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
2026-03-17 11:17:24 +01:00
gitea-actions
ed9df4e178 chore: bump version to v0.1.47
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m20s
2026-03-17 09:34:55 +00:00
625b4af5ba fix : correction calcule des paniers de nuit
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-03-17 10:34:45 +01:00
8 changed files with 85 additions and 43 deletions

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.1.46' app.version: '0.1.50'

View File

@@ -148,7 +148,7 @@ Documents complementaires:
- Validation: même logique que les heures classiques (`isValid`, `isSiteValid`, bulk) - Validation: même logique que les heures classiques (`isValid`, `isSiteValid`, bulk)
- Vue semaine: - Vue semaine:
- jour/nuit/atelier par jour + indicateurs repas/dîner/nuitée - jour/nuit/atelier par jour + indicateurs repas/dîner/nuitée
- panier de nuit (PN): affiché par jour si nightMinutes > dayMinutes, et total hebdo dans la colonne Jour/Nuit sem. - panier de nuit (PN): affiché par jour si (nightMinutes > dayMinutes) OU (nightMinutes >= 240, soit au moins 4h de travail entre 21h et 6h), et total hebdo dans la colonne Jour/Nuit sem.
- totaux hebdo: jour, nuit, atelier, total, compteurs petit déj/déjeuner/dîner/nuitée - totaux hebdo: jour, nuit, atelier, total, compteurs petit déj/déjeuner/dîner/nuitée
- pas de calcul d'heures supplémentaires pour les conducteurs - pas de calcul d'heures supplémentaires pour les conducteurs
- Le flag `isDriver` est sur `EmployeeContractPeriod` (un employé peut changer de statut chauffeur selon la période) - Le flag `isDriver` est sur `EmployeeContractPeriod` (un employé peut changer de statut chauffeur selon la période)
@@ -212,12 +212,14 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
- en cours d'acquisition jours: `25/12 = 2,08` jours/mois - en cours d'acquisition jours: `25/12 = 2,08` jours/mois
- en cours d'acquisition samedis: `5/12 = 0,42` samedi/mois (non detaille en UI) - en cours d'acquisition samedis: `5/12 = 0,42` samedi/mois (non detaille en UI)
- en cas de début/fin en cours de mois, l'acquisition est proratisée au nombre de jours calendaires couverts dans le mois - en cas de début/fin en cours de mois, l'acquisition est proratisée au nombre de jours calendaires couverts dans le mois
- en cas de suspension en cours de mois, l'acquisition est proratisée en jours ouvrés (lun-ven hors fériés) travaillés / 22 (standard mensuel)
- samedis acquis affiches: uniquement `opening_saturdays` (report N-1) - samedis acquis affiches: uniquement `opening_saturdays` (report N-1)
- contrat `4h`: - contrat `4h`:
- acquis annuel CP: `10` - acquis annuel CP: `10`
- acquis annuel samedi: `0` - acquis annuel samedi: `0`
- en cours d'acquisition: `0.83` jour/mois - en cours d'acquisition: `0.83` jour/mois
- en cas de début/fin en cours de mois, l'acquisition est proratisée au nombre de jours calendaires couverts dans le mois - en cas de début/fin en cours de mois, l'acquisition est proratisée au nombre de jours calendaires couverts dans le mois
- en cas de suspension en cours de mois, l'acquisition est proratisée en jours ouvrés (lun-ven hors fériés) travaillés / 22
- contrat `FORFAIT`: - contrat `FORFAIT`:
- base annuelle: `jours ouvrés de l'exercice (lundi-vendredi, hors jours fériés métropole) - 218` - base annuelle: `jours ouvrés de l'exercice (lundi-vendredi, hors jours fériés métropole) - 218`
- prorata: en cas de démarrage/fin de contrat en cours d'année civile, le calcul ne couvre que l'intervalle actif du contrat dans l'année - prorata: en cas de démarrage/fin de contrat en cours d'année civile, le calcul ne couvre que l'intervalle actif du contrat dans l'année
@@ -325,7 +327,7 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
| Base | Contract.name | Via EmployeeContractResolver pour le mois | | Base | Contract.name | Via EmployeeContractResolver pour le mois |
| Jour de présence Cadre | WorkHour | Uniquement FORFAIT (PRESENCE). Somme isPresentMorning (0.5) + isPresentAfternoon (0.5) | | Jour de présence Cadre | WorkHour | Uniquement FORFAIT (PRESENCE). Somme isPresentMorning (0.5) + isPresentAfternoon (0.5) |
| Heures de nuit | WorkHour | Non-chauffeurs: calcul intervalles nuit (00:00-06:00, 21:00-24:00). Chauffeurs: somme nightHoursMinutes | | Heures de nuit | WorkHour | Non-chauffeurs: calcul intervalles nuit (00:00-06:00, 21:00-24:00). Chauffeurs: somme nightHoursMinutes |
| Panier de nuit | WorkHour | Nombre de jours où nightMinutes > dayMinutes | | Panier de nuit | WorkHour | Nombre de jours où (nightMinutes > dayMinutes) OU (nightMinutes >= 240, soit 4h entre 21h-6h) |
| Heures payés | EmployeeRttPayment | Somme base25Minutes + base50Minutes du mois, convertie en heures | | Heures payés | EmployeeRttPayment | Somme base25Minutes + base50Minutes du mois, convertie en heures |
| Congés - Nombre | Absence code 'C' | Jours (demi-journées = 0.5) | | Congés - Nombre | Absence code 'C' | Jours (demi-journées = 0.5) |
| Congés - Date | Absence code 'C' | Dates formatées dd/mm | | Congés - Date | Absence code 'C' | Dates formatées dd/mm |

View File

@@ -64,7 +64,7 @@
</thead> </thead>
<tbody> <tbody>
<!-- Report N-1 row (RTT rollover carry, June only) --> <!-- Report N-1 row (RTT rollover carry, June only) -->
<tr v-if="showCarryRow"> <tr v-if="showCarryRow" class="bg-tertiary-500">
<td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500">Report</td> <td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500">Report</td>
<td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500 border-r-2">-</td> <td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500 border-r-2">-</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(summary!.carryBase25Minutes) }}</td> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(summary!.carryBase25Minutes) }}</td>
@@ -77,7 +77,7 @@
</tr> </tr>
<!-- Report mois précédent (cumulated balance from previous months, July+) --> <!-- Report mois précédent (cumulated balance from previous months, July+) -->
<tr v-if="showMonthReportRow"> <tr v-if="showMonthReportRow" class="bg-tertiary-500">
<td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500">Report</td> <td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500">Report</td>
<td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500 border-r-2">-</td> <td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500 border-r-2">-</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.base25) }}</td> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.base25) }}</td>
@@ -103,11 +103,11 @@
<span v-else>0 h</span> <span v-else>0 h</span>
</td> </td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500"> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">
<span v-if="week">{{ formatMinutes(week.base25Minutes) }}</span> <span v-if="week">{{ formatMinutes(week.totalMinutes >= 0 ? week.base25Minutes : 0) }}</span>
<span v-else>0 h</span> <span v-else>0 h</span>
</td> </td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500"> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">
<span v-if="week">{{ formatMinutes(week.bonus25Minutes) }}</span> <span v-if="week">{{ formatMinutes(week.totalMinutes >= 0 ? week.bonus25Minutes : 0) }}</span>
<span v-else>0 h</span> <span v-else>0 h</span>
</td> </td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2"> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">
@@ -115,11 +115,11 @@
<span v-else>0 h</span> <span v-else>0 h</span>
</td> </td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500"> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">
<span v-if="week">{{ formatMinutes(week.base50Minutes) }}</span> <span v-if="week">{{ formatMinutes(week.totalMinutes >= 0 ? week.base50Minutes : 0) }}</span>
<span v-else>0 h</span> <span v-else>0 h</span>
</td> </td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500"> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">
<span v-if="week">{{ formatMinutes(week.bonus50Minutes) }}</span> <span v-if="week">{{ formatMinutes(week.totalMinutes >= 0 ? week.bonus50Minutes : 0) }}</span>
<span v-else>0 h</span> <span v-else>0 h</span>
</td> </td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2"> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">
@@ -162,13 +162,13 @@
<tr> <tr>
<td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500">Reste</td> <td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500">Reste</td>
<td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500 border-r-2">-</td> <td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500 border-r-2">-</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.base25 + totals.base25 - (currentPayment?.paidBase25Minutes ?? 0)) }}</td> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(reste.base25) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.bonus25 + totals.bonus25 - (currentPayment?.paidBonus25Minutes ?? 0)) }}</td> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(reste.bonus25) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(monthReport.total25 + totals.total25 - (currentPayment?.paidBase25Minutes ?? 0) - (currentPayment?.paidBonus25Minutes ?? 0)) }}</td> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(reste.total25) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.base50 + totals.base50 - (currentPayment?.paidBase50Minutes ?? 0)) }}</td> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(reste.base50) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.bonus50 + totals.bonus50 - (currentPayment?.paidBonus50Minutes ?? 0)) }}</td> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(reste.bonus50) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(monthReport.total50 + totals.total50 - (currentPayment?.paidBase50Minutes ?? 0) - (currentPayment?.paidBonus50Minutes ?? 0)) }}</td> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(reste.total50) }}</td>
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(resteTotal) }}</td> <td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(reste.total) }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -419,13 +419,14 @@ const showMonthReportRow = computed(() => {
const totals = computed(() => { const totals = computed(() => {
const weeks = weeksForCurrentMonth.value const weeks = weeksForCurrentMonth.value
const positive = weeks.filter((w) => w.totalMinutes >= 0)
return { return {
overtime: weeks.reduce((s, w) => s + w.overtimeMinutes, 0), overtime: weeks.reduce((s, w) => s + w.overtimeMinutes, 0),
base25: weeks.reduce((s, w) => s + w.base25Minutes, 0), base25: positive.reduce((s, w) => s + w.base25Minutes, 0),
bonus25: weeks.reduce((s, w) => s + w.bonus25Minutes, 0), bonus25: positive.reduce((s, w) => s + w.bonus25Minutes, 0),
total25: weeks.reduce((s, w) => s + w.base25Minutes + w.bonus25Minutes, 0), total25: weeks.reduce((s, w) => s + w.base25Minutes + w.bonus25Minutes, 0),
base50: weeks.reduce((s, w) => s + w.base50Minutes, 0), base50: positive.reduce((s, w) => s + w.base50Minutes, 0),
bonus50: weeks.reduce((s, w) => s + w.bonus50Minutes, 0), bonus50: positive.reduce((s, w) => s + w.bonus50Minutes, 0),
total50: weeks.reduce((s, w) => s + w.base50Minutes + w.bonus50Minutes, 0), total50: weeks.reduce((s, w) => s + w.base50Minutes + w.bonus50Minutes, 0),
total: weeks.reduce((s, w) => s + w.totalMinutes, 0), total: weeks.reduce((s, w) => s + w.totalMinutes, 0),
} }
@@ -442,8 +443,19 @@ const paidTotal = computed(() => {
return -(p.paidBase25Minutes + p.paidBonus25Minutes + p.paidBase50Minutes + p.paidBonus50Minutes) return -(p.paidBase25Minutes + p.paidBonus25Minutes + p.paidBase50Minutes + p.paidBonus50Minutes)
}) })
const resteTotal = computed(() => { const reste = computed(() => {
return monthReport.value.total + totals.value.total + paidTotal.value const total25 = monthReport.value.total25 + totals.value.total25
- (currentPayment.value?.paidBase25Minutes ?? 0) - (currentPayment.value?.paidBonus25Minutes ?? 0)
const total50 = monthReport.value.total50 + totals.value.total50
- (currentPayment.value?.paidBase50Minutes ?? 0) - (currentPayment.value?.paidBonus50Minutes ?? 0)
const base25 = Math.round(total25 / 1.25)
const bonus25 = total25 - base25
const base50 = Math.round(total50 / 1.5)
const bonus50 = total50 - base50
const total = monthReport.value.total + totals.value.total + paidTotal.value
return { base25, bonus25, total25, base50, bonus50, total50, total }
}) })
// --- Format --- // --- Format ---

View File

@@ -1045,7 +1045,7 @@ export const useHoursPage = () => {
isSubmitting.value = true isSubmitting.value = true
try { try {
const entries = employees.value const entries = employees.value
.filter((employee) => hasContractAtSelectedDate(employee.id)) .filter((employee) => hasContractAtSelectedDate(employee.id) && !isRowLocked(employee.id))
.map((employee) => { .map((employee) => {
const employeeId = employee.id const employeeId = employee.id
const row = rows.value[employeeId] ?? emptyRow() const row = rows.value[employeeId] ?? emptyRow()

View File

@@ -278,10 +278,11 @@ final readonly class LeaveBalanceComputationService
return 0.0; return 0.0;
} }
$periodStart = $this->normalizeDate($periodStart); $periodStart = $this->normalizeDate($periodStart);
$periodEnd = $this->normalizeDate($periodEnd); $periodEnd = $this->normalizeDate($periodEnd);
$coveredMonths = 0.0; $publicHolidays = [] !== $suspensions ? $this->buildPublicHolidayMap($periodStart, $periodEnd) : [];
$cursor = $periodStart->modify('first day of this month')->setTime(0, 0); $coveredMonths = 0.0;
$cursor = $periodStart->modify('first day of this month')->setTime(0, 0);
while ($cursor <= $periodEnd) { while ($cursor <= $periodEnd) {
$monthStart = $cursor > $periodStart ? $cursor : $periodStart; $monthStart = $cursor > $periodStart ? $cursor : $periodStart;
$monthEnd = $cursor->modify('last day of this month')->setTime(0, 0); $monthEnd = $cursor->modify('last day of this month')->setTime(0, 0);
@@ -289,11 +290,19 @@ final readonly class LeaveBalanceComputationService
$monthEnd = $periodEnd; $monthEnd = $periodEnd;
} }
$coveredDays = ((int) $monthEnd->diff($monthStart)->format('%a')) + 1;
if ([] !== $suspensions) { if ([] !== $suspensions) {
$suspendedDays = $this->suspensionDaysCalculator->countSuspendedDaysInMonth($monthStart, $monthEnd, $suspensions); $suspendedDays = $this->suspensionDaysCalculator->countSuspendedDaysInMonth($monthStart, $monthEnd, $suspensions);
$coveredDays = max(0, $coveredDays - $suspendedDays); if ($suspendedDays > 0) {
$businessDays = $this->countBusinessDaysInRange($monthStart, $monthEnd, $publicHolidays);
$suspendedBusinessDays = $this->suspensionDaysCalculator->countSuspendedBusinessDays($monthStart, $monthEnd, $suspensions, $publicHolidays);
$coveredMonths += max(0, $businessDays - $suspendedBusinessDays) / 22.0;
$cursor = $cursor->modify('first day of next month');
continue;
}
} }
$coveredDays = ((int) $monthEnd->diff($monthStart)->format('%a')) + 1;
$daysInMonth = (int) $cursor->format('t'); $daysInMonth = (int) $cursor->format('t');
$coveredMonths += $coveredDays / $daysInMonth; $coveredMonths += $coveredDays / $daysInMonth;
@@ -317,8 +326,15 @@ final readonly class LeaveBalanceComputationService
private function countBusinessDays(DateTimeImmutable $from, DateTimeImmutable $to): int private function countBusinessDays(DateTimeImmutable $from, DateTimeImmutable $to): int
{ {
$publicHolidays = $this->buildPublicHolidayMap($from, $to); return $this->countBusinessDaysInRange($from, $to, $this->buildPublicHolidayMap($from, $to));
$count = 0; }
/**
* @param array<string, string> $publicHolidays pre-built map
*/
private function countBusinessDaysInRange(DateTimeImmutable $from, DateTimeImmutable $to, array $publicHolidays): int
{
$count = 0;
for ($cursor = $from; $cursor <= $to; $cursor = $cursor->modify('+1 day')) { for ($cursor = $from; $cursor <= $to; $cursor = $cursor->modify('+1 day')) {
$weekDay = (int) $cursor->format('N'); $weekDay = (int) $cursor->format('N');
$dayKey = $cursor->format('Y-m-d'); $dayKey = $cursor->format('Y-m-d');

View File

@@ -390,10 +390,11 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
return 0.0; return 0.0;
} }
$periodStart = $this->normalizeDate($periodStart); $periodStart = $this->normalizeDate($periodStart);
$periodEnd = $this->normalizeDate($periodEnd); $periodEnd = $this->normalizeDate($periodEnd);
$coveredMonths = 0.0; $publicHolidays = [] !== $suspensions ? $this->buildPublicHolidayMap($periodStart, $periodEnd) : [];
$cursor = $periodStart->modify('first day of this month')->setTime(0, 0); $coveredMonths = 0.0;
$cursor = $periodStart->modify('first day of this month')->setTime(0, 0);
while ($cursor <= $periodEnd) { while ($cursor <= $periodEnd) {
$monthStart = $cursor > $periodStart ? $cursor : $periodStart; $monthStart = $cursor > $periodStart ? $cursor : $periodStart;
$monthEnd = $cursor->modify('last day of this month')->setTime(0, 0); $monthEnd = $cursor->modify('last day of this month')->setTime(0, 0);
@@ -401,11 +402,19 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
$monthEnd = $periodEnd; $monthEnd = $periodEnd;
} }
$coveredDays = ((int) $monthEnd->diff($monthStart)->format('%a')) + 1;
if ([] !== $suspensions) { if ([] !== $suspensions) {
$suspendedDays = $this->suspensionDaysCalculator->countSuspendedDaysInMonth($monthStart, $monthEnd, $suspensions); $suspendedDays = $this->suspensionDaysCalculator->countSuspendedDaysInMonth($monthStart, $monthEnd, $suspensions);
$coveredDays = max(0, $coveredDays - $suspendedDays); if ($suspendedDays > 0) {
$businessDays = $this->countBusinessDays($monthStart, $monthEnd, $publicHolidays);
$suspendedBusinessDays = $this->suspensionDaysCalculator->countSuspendedBusinessDays($monthStart, $monthEnd, $suspensions, $publicHolidays);
$coveredMonths += max(0, $businessDays - $suspendedBusinessDays) / 22.0;
$cursor = $cursor->modify('first day of next month');
continue;
}
} }
$coveredDays = ((int) $monthEnd->diff($monthStart)->format('%a')) + 1;
$daysInMonth = (int) $cursor->format('t'); $daysInMonth = (int) $cursor->format('t');
$coveredMonths += $coveredDays / $daysInMonth; $coveredMonths += $coveredDays / $daysInMonth;
@@ -526,10 +535,13 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
]; ];
} }
private function countBusinessDays(DateTimeImmutable $from, DateTimeImmutable $to): int /**
* @param null|array<string, string> $publicHolidays pre-built map (built if null)
*/
private function countBusinessDays(DateTimeImmutable $from, DateTimeImmutable $to, ?array $publicHolidays = null): int
{ {
$publicHolidays = $this->buildPublicHolidayMap($from, $to); $publicHolidays ??= $this->buildPublicHolidayMap($from, $to);
$count = 0; $count = 0;
for ($cursor = $from; $cursor <= $to; $cursor = $cursor->modify('+1 day')) { for ($cursor = $from; $cursor <= $to; $cursor = $cursor->modify('+1 day')) {
$weekDay = (int) $cursor->format('N'); $weekDay = (int) $cursor->format('N');
$dayKey = $cursor->format('Y-m-d'); $dayKey = $cursor->format('Y-m-d');

View File

@@ -298,7 +298,7 @@ class SalaryRecapPrintProvider implements ProviderInterface
$nightMinutesTotal += $wh->getNightHoursMinutes() ?? 0; $nightMinutesTotal += $wh->getNightHoursMinutes() ?? 0;
$dayMin = $wh->getDayHoursMinutes() ?? 0; $dayMin = $wh->getDayHoursMinutes() ?? 0;
$nightMin = $wh->getNightHoursMinutes() ?? 0; $nightMin = $wh->getNightHoursMinutes() ?? 0;
if ($nightMin > $dayMin && $nightMin > 0) { if (($nightMin > $dayMin && $nightMin > 0) || $nightMin >= 240) {
++$nightBasketCount; ++$nightBasketCount;
} }
@@ -322,7 +322,7 @@ class SalaryRecapPrintProvider implements ProviderInterface
} else { } else {
$metrics = $this->computeNightMinutes($wh); $metrics = $this->computeNightMinutes($wh);
$nightMinutesTotal += $metrics['nightMinutes']; $nightMinutesTotal += $metrics['nightMinutes'];
if ($metrics['nightMinutes'] > $metrics['dayMinutes'] && $metrics['nightMinutes'] > 0) { if (($metrics['nightMinutes'] > $metrics['dayMinutes'] && $metrics['nightMinutes'] > 0) || $metrics['nightMinutes'] >= 240) {
++$nightBasketCount; ++$nightBasketCount;
} }

View File

@@ -271,7 +271,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$present = min(1.0, $morning + $afternoon + $creditedPresence); $present = min(1.0, $morning + $afternoon + $creditedPresence);
} }
$hasNightBasket = $nightMinutes > $dayMinutes && $nightMinutes > 0; $hasNightBasket = ($nightMinutes > $dayMinutes && $nightMinutes > 0) || $nightMinutes >= 240;
if ($hasNightBasket) { if ($hasNightBasket) {
++$weeklyNightBasketCount; ++$weeklyNightBasketCount;
} }