Compare commits

...

7 Commits

Author SHA1 Message Date
gitea-actions
11331da6a1 chore: bump version to v0.1.84
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build & Push Docker Image / build (push) Successful in 33s
2026-04-14 09:25:55 +00:00
399fd7335e fix : exclusion de certain jour férié et affichage différent des jours férié dans la page d'heure
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-04-14 11:25:44 +02:00
gitea-actions
46cb7f1a16 chore: bump version to v0.1.83
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build & Push Docker Image / build (push) Successful in 35s
2026-04-14 06:38:09 +00:00
b934f4d81f Merge remote-tracking branch 'origin/develop' into develop
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-04-14 08:38:01 +02:00
77c1cdcbbd fix : on masque la validation chef site 2026-04-14 08:37:54 +02:00
gitea-actions
de302d9ded chore: bump version to v0.1.82
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build & Push Docker Image / build (push) Successful in 22s
2026-04-14 06:25:17 +00:00
ef18210bf7 fix : export du récap congés
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-04-14 08:24:43 +02:00
16 changed files with 119 additions and 76 deletions

3
.env
View File

@@ -38,6 +38,9 @@ DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&ch
###> app ### ###> app ###
RTT_START_DATE=2026-02-23 RTT_START_DATE=2026-02-23
# Comma-separated list of public holiday labels to exclude from the government API response
# (typically the "journée de solidarité" worked in many companies)
EXCLUDED_PUBLIC_HOLIDAYS="Lundi de Pentecôte"
###< app ### ###< app ###
###> nelmio/cors-bundle ### ###> nelmio/cors-bundle ###

View File

@@ -35,6 +35,13 @@
- Absences with `countAsWorkedHours=true`: credit minutes (TIME) or nothing (PRESENCE) - Absences with `countAsWorkedHours=true`: credit minutes (TIME) or nothing (PRESENCE)
- Driver periods (`isDriver=true` on `EmployeeContractPeriod`): separate screen `/driver-hours`, uses `dayHoursMinutes`/`nightHoursMinutes` + meal/overnight flags on `WorkHour` - Driver periods (`isDriver=true` on `EmployeeContractPeriod`): separate screen `/driver-hours`, uses `dayHoursMinutes`/`nightHoursMinutes` + meal/overnight flags on `WorkHour`
## Fériés
- Source : API gouv via `PublicHolidayService` (cache 30j)
- Exclusions : env `EXCLUDED_PUBLIC_HOLIDAYS` (CSV de libellés), défaut `"Lundi de Pentecôte"`. Le filtre s'applique après le cache, côté service, donc frontend et calculs backend voient la même liste.
- Écrans Heures / Heures Conducteurs (vue jour) : le nom du férié est affiché en badge `#b3e5fc` avec icône `mdi:calendar-star` dans la colonne Absence (distinct du pill absence). Bouton "Modifier" absence masqué sur férié (comme pour les formations).
- Création/édition d'absence bloquée sur un férié
- Saisie d'heures (ou de jours de présence) autorisée sur un férié — nécessaire pour éviter un déficit hebdomadaire (la référence hebdo n'est pas réduite par les fériés)
## Validation Rules ## Validation Rules
- `isValid` (RH): locks line for everyone (admin can only untoggle validation) - `isValid` (RH): locks line for everyone (admin can only untoggle validation)
- `isSiteValid` (site manager): locks for non-admin, admin can still edit - `isSiteValid` (site manager): locks for non-admin, admin can still edit

View File

@@ -25,6 +25,7 @@ services:
App\Service\PublicHolidayService: App\Service\PublicHolidayService:
arguments: arguments:
$holidayUrl: '%env(HOLIDAY_URL)%' $holidayUrl: '%env(HOLIDAY_URL)%'
$excludedLabels: '%env(default::EXCLUDED_PUBLIC_HOLIDAYS)%'
App\Service\Rtt\RttRecoveryComputationService: App\Service\Rtt\RttRecoveryComputationService:
arguments: arguments:

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.1.81' app.version: '0.1.84'

View File

@@ -23,3 +23,4 @@ DEFAULT_URI=https://sirh.malio-dev.fr
APP_SHARE_DIR=var/share APP_SHARE_DIR=var/share
RTT_START_DATE=2026-02-23 RTT_START_DATE=2026-02-23
HOLIDAY_URL="https://calendrier.api.gouv.fr/jours-feries/" HOLIDAY_URL="https://calendrier.api.gouv.fr/jours-feries/"
EXCLUDED_PUBLIC_HOLIDAYS="Lundi de Pentecôte"

View File

@@ -161,10 +161,14 @@ Documents complementaires:
## 7) Fériés ## 7) Fériés
- Les jours fériés sont identifiés et affichés - Les jours fériés sont identifiés et affichés
- Source: API `calendrier.api.gouv.fr/jours-feries/` via `PublicHolidayService` (cache 30j)
- Exclusions configurables: variable d'env `EXCLUDED_PUBLIC_HOLIDAYS` (liste de libellés séparés par virgules). Par défaut `"Lundi de Pentecôte"` — journée de solidarité généralement travaillée. Le filtre s'applique à tous les consommateurs (frontend + calculs backend) en amont du retour du service.
- Onglet congés: jours fériés affichés sur le calendrier avec fond `rgb(179, 229, 252)` et nom au survol - Onglet congés: jours fériés affichés sur le calendrier avec fond `rgb(179, 229, 252)` et nom au survol
- Écran Heures et Heures Conducteurs (vue jour): le nom du férié est affiché dans la colonne Absence sous forme de pill (fond `#b3e5fc`, icône `mdi:calendar-star`), distinct du pill absence
- Règle courante: - Règle courante:
- absences bloquées sur jour férié - absences bloquées sur jour férié (création/édition) — bouton "Modifier" masqué comme pour les formations
- saisie d'heures autorisée - saisie d'heures ou de jours de présence autorisée — les heures saisies comptent normalement dans le total hebdo et le calcul RTT
- la référence hebdomadaire n'est pas réduite par un férié: un salarié qui ne saisit rien sur un férié est en déficit de la journée correspondante
## 8) Impression absences (PDF) ## 8) Impression absences (PDF)

View File

@@ -25,19 +25,7 @@
@change="onBulkValidationChange" @change="onBulkValidationChange"
/> />
</span> </span>
<span v-else-if="isSiteManager" class="inline-flex items-center gap-2"> <span v-else-if="!isSiteManager">Site <Icon name="mdi:check-bold" class="ml-1"/></span>
<span>Site</span>
<input
ref="bulkSiteValidationInput"
:checked="isBulkSiteValidationChecked"
type="checkbox"
class="h-4 w-4"
:class="canBulkToggleSiteValidation ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'"
:disabled="!canBulkToggleSiteValidation"
@change="onBulkSiteValidationChange"
/>
</span>
<span v-else>Site <Icon name="mdi:check-bold" class="ml-1"/></span>
<span v-if="!isAdmin">RH <Icon name="mdi:check-bold" class="ml-1"/></span> <span v-if="!isAdmin">RH <Icon name="mdi:check-bold" class="ml-1"/></span>
</div> </div>
@@ -68,19 +56,31 @@
</p> </p>
</div> </div>
<div class="pl-2 min-w-0 self-stretch flex flex-col gap-1 justify-between py-0.5"> <div class="pl-2 min-w-0 self-stretch flex flex-col gap-1 justify-between py-0.5">
<p <div class="flex flex-col gap-1 min-w-0">
class="w-full min-w-0 rounded-md px-2 py-1 text-xs truncate" <p
:class="getRowAbsenceLabel(employee.id) ? 'text-white' : 'invisible'" class="w-full min-w-0 rounded-md px-2 py-1 text-xs truncate"
:title="getRowAbsenceLabel(employee.id) || ''" :class="getRowAbsenceLabel(employee.id) ? 'text-white' : 'invisible'"
:style="getRowAbsenceStyle(employee.id)" :title="getRowAbsenceLabel(employee.id) || ''"
> :style="getRowAbsenceStyle(employee.id)"
{{ getRowAbsenceLabel(employee.id) || '—' }} >
</p> {{ getRowAbsenceLabel(employee.id) || '—' }}
</p>
<p
v-if="isHoliday"
class="w-full min-w-0 rounded-md px-2 py-1 text-xs truncate text-sky-900 inline-flex items-center gap-1"
style="background-color: #b3e5fc"
:title="holidayLabel || 'Férié'"
>
<Icon name="mdi:calendar-star" size="14" class="shrink-0"/>
<span class="truncate">{{ holidayLabel || 'Férié' }}</span>
</p>
</div>
<button <button
v-if="!isHoliday"
type="button" type="button"
class="self-start text-left text-xs font-semibold underline" class="self-start text-left text-xs font-semibold underline"
:class="isHoliday || isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id) ? 'text-neutral-400 cursor-not-allowed' : 'text-primary-500 cursor-pointer'" :class="isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id) ? 'text-neutral-400 cursor-not-allowed' : 'text-primary-500 cursor-pointer'"
:disabled="isHoliday || isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id)" :disabled="isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id)"
@click="onAbsenceClick(employee.id)" @click="onAbsenceClick(employee.id)"
> >
Modifier Modifier
@@ -147,16 +147,8 @@
@change="onToggleValidation(employee.id, ($event.target as HTMLInputElement).checked)" @change="onToggleValidation(employee.id, ($event.target as HTMLInputElement).checked)"
/> />
</div> </div>
<div v-else class="text-right p-5"> <div v-else-if="!isSiteManager" class="text-right p-5">
<input <span v-if="rows[employee.id]?.isSiteValid" class="text-xs font-semibold text-neutral-700">Validé</span>
v-if="isSiteManager"
:checked="rows[employee.id]?.isSiteValid ?? false"
type="checkbox"
class="h-4 w-4 cursor-pointer"
:disabled="(!canToggleSiteValidation(employee.id) && !canCreateSiteValidationRowFromAbsence(employee.id)) || isSiteValidationPending(employee.id)"
@change="onToggleSiteValidation(employee.id, ($event.target as HTMLInputElement).checked)"
/>
<span v-else-if="rows[employee.id]?.isSiteValid" class="text-xs font-semibold text-neutral-700">Validé</span>
<span v-else class="text-xs text-neutral-500">-</span> <span v-else class="text-xs text-neutral-500">-</span>
</div> </div>
<div v-if="!isAdmin"> <div v-if="!isAdmin">
@@ -184,6 +176,7 @@ const props = defineProps<{
isSiteManager: boolean isSiteManager: boolean
dayGridCols: string dayGridCols: string
isHoliday: boolean isHoliday: boolean
holidayLabel: string
contractLabel: (employee: Employee) => string contractLabel: (employee: Employee) => string
isRowLocked: (employeeId: number) => boolean isRowLocked: (employeeId: number) => boolean
hasContractAtSelectedDate: (employeeId: number) => boolean hasContractAtSelectedDate: (employeeId: number) => boolean

View File

@@ -26,19 +26,7 @@
@change="onBulkValidationChange" @change="onBulkValidationChange"
/> />
</span> </span>
<span v-else-if="isSiteManager" class="inline-flex items-center gap-2"> <span v-else-if="!isSiteManager">Site <Icon name="mdi:check-bold" class="ml-1"/></span>
<span>Site</span>
<input
ref="bulkSiteValidationInput"
:checked="isBulkSiteValidationChecked"
type="checkbox"
class="h-4 w-4"
:class="canBulkToggleSiteValidation ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'"
:disabled="!canBulkToggleSiteValidation"
@change="onBulkSiteValidationChange"
/>
</span>
<span v-else>Site <Icon name="mdi:check-bold" class="ml-1"/></span>
<span v-if="!isAdmin">RH <Icon name="mdi:check-bold" class="ml-1"/></span> <span v-if="!isAdmin">RH <Icon name="mdi:check-bold" class="ml-1"/></span>
</div> </div>
@@ -78,6 +66,15 @@
> >
{{ getRowAbsenceLabel(employee.id) || '—' }} {{ getRowAbsenceLabel(employee.id) || '—' }}
</p> </p>
<p
v-if="isHoliday"
class="w-full min-w-0 rounded-md px-2 py-1 text-xs truncate text-sky-900 inline-flex items-center gap-1"
style="background-color: #b3e5fc"
:title="holidayLabel || 'Férié'"
>
<Icon name="mdi:calendar-star" size="14" class="shrink-0"/>
<span class="truncate">{{ holidayLabel || 'Férié' }}</span>
</p>
<p <p
v-if="hasRowFormation(employee.id)" v-if="hasRowFormation(employee.id)"
class="w-full min-w-0 rounded-md px-2 py-1 text-xs truncate text-white bg-indigo-500 inline-flex items-center gap-1" class="w-full min-w-0 rounded-md px-2 py-1 text-xs truncate text-white bg-indigo-500 inline-flex items-center gap-1"
@@ -88,11 +85,11 @@
</p> </p>
</div> </div>
<button <button
v-if="!hasRowFormation(employee.id)" v-if="!hasRowFormation(employee.id) && !isHoliday"
type="button" type="button"
class="self-start text-left text-xs font-semibold underline" class="self-start text-left text-xs font-semibold underline"
:class="isHoliday || isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id) ? 'text-neutral-400 cursor-not-allowed' : 'text-primary-500 cursor-pointer'" :class="isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id) ? 'text-neutral-400 cursor-not-allowed' : 'text-primary-500 cursor-pointer'"
:disabled="isHoliday || isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id)" :disabled="isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id)"
@click="onAbsenceClick(employee.id)" @click="onAbsenceClick(employee.id)"
> >
Modifier Modifier
@@ -181,16 +178,8 @@
@change="onToggleValidation(employee.id, ($event.target as HTMLInputElement).checked)" @change="onToggleValidation(employee.id, ($event.target as HTMLInputElement).checked)"
/> />
</div> </div>
<div v-else class="text-right p-5"> <div v-else-if="!isSiteManager" class="text-right p-5">
<input <span v-if="rows[employee.id]?.isSiteValid" class="text-xs font-semibold text-neutral-700">Validé</span>
v-if="isSiteManager"
:checked="rows[employee.id]?.isSiteValid ?? false"
type="checkbox"
class="h-4 w-4 cursor-pointer"
:disabled="(!canToggleSiteValidation(employee.id) && !canCreateSiteValidationRowFromAbsence(employee.id)) || isSiteValidationPending(employee.id)"
@change="onToggleSiteValidation(employee.id, ($event.target as HTMLInputElement).checked)"
/>
<span v-else-if="rows[employee.id]?.isSiteValid" class="text-xs font-semibold text-neutral-700">Validé</span>
<span v-else class="text-xs text-neutral-500">-</span> <span v-else class="text-xs text-neutral-500">-</span>
</div> </div>
<div v-if="!isAdmin"> <div v-if="!isAdmin">
@@ -218,6 +207,7 @@ const props = defineProps<{
isSiteManager: boolean isSiteManager: boolean
dayGridCols: string dayGridCols: string
isHoliday: boolean isHoliday: boolean
holidayLabel: string
contractLabel: (employee: Employee) => string contractLabel: (employee: Employee) => string
isTimeTracking: (employee: Employee) => boolean isTimeTracking: (employee: Employee) => boolean
isPresenceTracking: (employee: Employee) => boolean isPresenceTracking: (employee: Employee) => boolean

View File

@@ -71,7 +71,7 @@ export const useDriverHoursPage = () => {
const dayGridCols = computed(() => { const dayGridCols = computed(() => {
const metricCol = '0.4fr' const metricCol = '0.4fr'
const validationCols = isAdmin.value ? `${metricCol}` : `${metricCol} ${metricCol}` const validationCols = isAdmin.value || isSiteManager.value ? `${metricCol}` : `${metricCol} ${metricCol}`
return `1.2fr 0.6fr 0.8fr 0.8fr 0.8fr ${metricCol} ${metricCol} ${metricCol} ${metricCol} ${metricCol} ${validationCols}` return `1.2fr 0.6fr 0.8fr 0.8fr 0.8fr ${metricCol} ${metricCol} ${metricCol} ${metricCol} ${metricCol} ${validationCols}`
}) })
@@ -381,7 +381,6 @@ export const useDriverHoursPage = () => {
if (dayRow && dayRow.hasContractAtDate === false) { if (dayRow && dayRow.hasContractAtDate === false) {
return 'Contrat non démarré' return 'Contrat non démarré'
} }
if (isSelectedDateHoliday.value) return 'Férié'
if (!dayRow?.absenceLabel) return '' if (!dayRow?.absenceLabel) return ''
if (dayRow.absenceHalf === 'AM' || dayRow.absenceHalf === 'PM') { if (dayRow.absenceHalf === 'AM' || dayRow.absenceHalf === 'PM') {
const halfLabel = dayRow.absenceHalf === 'AM' ? 'Matin' : 'ap.-m.' const halfLabel = dayRow.absenceHalf === 'AM' ? 'Matin' : 'ap.-m.'
@@ -941,6 +940,7 @@ export const useDriverHoursPage = () => {
saveButtonClass, saveButtonClass,
formattedSelectedDate, formattedSelectedDate,
isSelectedDateHoliday, isSelectedDateHoliday,
selectedHolidayLabel,
weekDayHeaders, weekDayHeaders,
shortcutButtonClass, shortcutButtonClass,
weekShortcutButtonClass, weekShortcutButtonClass,

View File

@@ -73,7 +73,7 @@ export const useHoursPage = () => {
const dayGridCols = computed(() => { const dayGridCols = computed(() => {
const metricCol = '0.4fr' const metricCol = '0.4fr'
const validationCols = isAdmin.value ? `${metricCol}` : `${metricCol} ${metricCol}` const validationCols = isAdmin.value || isSiteManager.value ? `${metricCol}` : `${metricCol} ${metricCol}`
return `1.2fr 0.6fr repeat(6, 0.8fr) ${metricCol} ${metricCol} ${metricCol} ${validationCols}` return `1.2fr 0.6fr repeat(6, 0.8fr) ${metricCol} ${metricCol} ${metricCol} ${validationCols}`
}) })
@@ -458,7 +458,6 @@ export const useHoursPage = () => {
if (dayRow && dayRow.hasContractAtDate === false) { if (dayRow && dayRow.hasContractAtDate === false) {
return 'Contrat non démarré' return 'Contrat non démarré'
} }
if (isSelectedDateHoliday.value) return 'Férié'
if (!dayRow?.absenceLabel) return '' if (!dayRow?.absenceLabel) return ''
if (dayRow.absenceHalf === 'AM' || dayRow.absenceHalf === 'PM') { if (dayRow.absenceHalf === 'AM' || dayRow.absenceHalf === 'PM') {
const halfLabel = dayRow.absenceHalf === 'AM' ? 'Matin' : 'ap.-m.' const halfLabel = dayRow.absenceHalf === 'AM' ? 'Matin' : 'ap.-m.'
@@ -1127,6 +1126,7 @@ export const useHoursPage = () => {
saveButtonClass, saveButtonClass,
formattedSelectedDate, formattedSelectedDate,
isSelectedDateHoliday, isSelectedDateHoliday,
selectedHolidayLabel,
weekDayHeaders, weekDayHeaders,
shortcutButtonClass, shortcutButtonClass,
weekShortcutButtonClass, weekShortcutButtonClass,

View File

@@ -56,6 +56,7 @@ export const documentationSections: DocSection[] = [
{ type: 'list', content: 'Matin : heure de début et heure de fin\nAprès-midi : heure de début et heure de fin\nSoir : heure de début et heure de fin' }, { type: 'list', content: 'Matin : heure de début et heure de fin\nAprès-midi : heure de début et heure de fin\nSoir : heure de début et heure de fin' },
{ type: 'paragraph', content: 'Le sélecteur de temps fonctionne par tranches de 15 minutes (00, 15, 30, 45). La saisie libre est possible mais sera corrigée automatiquement.' }, { type: 'paragraph', content: 'Le sélecteur de temps fonctionne par tranches de 15 minutes (00, 15, 30, 45). La saisie libre est possible mais sera corrigée automatiquement.' },
{ type: 'note', content: 'Les calculs sont mis à jour automatiquement : heures de jour (06:0021:00), heures de nuit (00:0006:00 et 21:0024:00) et total.' }, { type: 'note', content: 'Les calculs sont mis à jour automatiquement : heures de jour (06:0021:00), heures de nuit (00:0006:00 et 21:0024:00) et total.' },
{ type: 'note', content: 'Jours fériés : le nom du férié apparaît en badge bleu dans la colonne Absence. La saisie d\'heures (ou de jours de présence) reste autorisée — elle est même nécessaire pour ne pas être en déficit sur la semaine concernée. La création d\'une absence sur un férié reste bloquée.' },
], ],
}, },
{ {

View File

@@ -43,6 +43,7 @@
:is-site-manager="isSiteManager" :is-site-manager="isSiteManager"
:day-grid-cols="dayGridCols" :day-grid-cols="dayGridCols"
:is-holiday="isSelectedDateHoliday" :is-holiday="isSelectedDateHoliday"
:holiday-label="selectedHolidayLabel"
:contract-label="contractLabel" :contract-label="contractLabel"
:is-row-locked="isRowLocked" :is-row-locked="isRowLocked"
:has-contract-at-selected-date="hasContractAtSelectedDate" :has-contract-at-selected-date="hasContractAtSelectedDate"
@@ -174,6 +175,7 @@ const {
closeAbsenceDrawer, closeAbsenceDrawer,
formatMinutes, formatMinutes,
isSelectedDateHoliday, isSelectedDateHoliday,
selectedHolidayLabel,
handleSave handleSave
} = useDriverHoursPage() } = useDriverHoursPage()

View File

@@ -43,6 +43,7 @@
:is-site-manager="isSiteManager" :is-site-manager="isSiteManager"
:day-grid-cols="dayGridCols" :day-grid-cols="dayGridCols"
:is-holiday="isSelectedDateHoliday" :is-holiday="isSelectedDateHoliday"
:holiday-label="selectedHolidayLabel"
:contract-label="contractLabel" :contract-label="contractLabel"
:is-time-tracking="isTimeTracking" :is-time-tracking="isTimeTracking"
:is-presence-tracking="isPresenceTracking" :is-presence-tracking="isPresenceTracking"
@@ -141,6 +142,7 @@ const {
isSubmitting, isSubmitting,
dayGridCols, dayGridCols,
isSelectedDateHoliday, isSelectedDateHoliday,
selectedHolidayLabel,
weekGridCols, weekGridCols,
saveButtonClass, saveButtonClass,
formattedSelectedDate, formattedSelectedDate,

View File

@@ -17,11 +17,22 @@ use Throwable;
final readonly class PublicHolidayService implements PublicHolidayServiceInterface final readonly class PublicHolidayService implements PublicHolidayServiceInterface
{ {
/**
* @var list<string>
*/
private array $excludedLabels;
public function __construct( public function __construct(
private HttpClientInterface $client, private HttpClientInterface $client,
private string $holidayUrl, private string $holidayUrl,
private CacheInterface $cache, private CacheInterface $cache,
) {} string $excludedLabels = '',
) {
$this->excludedLabels = array_values(array_filter(
array_map('trim', explode(',', $excludedLabels)),
static fn (string $label): bool => '' !== $label,
));
}
/** /**
* @throws TransportExceptionInterface * @throws TransportExceptionInterface
@@ -35,7 +46,7 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa
$zone = strtolower(trim($zone)); $zone = strtolower(trim($zone));
$key = "public_holidays_{$zone}_all"; $key = "public_holidays_{$zone}_all";
return $this->cache->get($key, function (ItemInterface $item) use ($zone): array { $holidays = $this->cache->get($key, function (ItemInterface $item) use ($zone): array {
$item->expiresAfter(30 * 86400); $item->expiresAfter(30 * 86400);
$url = $this->holidayUrl."{$zone}.json"; $url = $this->holidayUrl."{$zone}.json";
@@ -56,6 +67,8 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa
return json_decode($response->getContent(), true); return json_decode($response->getContent(), true);
}); });
return $this->applyExclusions($holidays);
} }
/** /**
@@ -70,7 +83,7 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa
$years = trim($years); $years = trim($years);
$key = "public_holidays_{$zone}_{$years}"; $key = "public_holidays_{$zone}_{$years}";
return $this->cache->get($key, function (ItemInterface $item) use ($zone, $years): array { $holidays = $this->cache->get($key, function (ItemInterface $item) use ($zone, $years): array {
$item->expiresAfter(30 * 86400); $item->expiresAfter(30 * 86400);
$url = $this->holidayUrl."{$zone}/{$years}.json"; $url = $this->holidayUrl."{$zone}/{$years}.json";
@@ -88,5 +101,24 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa
return json_decode($response->getContent(), true); return json_decode($response->getContent(), true);
}); });
return $this->applyExclusions($holidays);
}
/**
* @param array<string, string> $holidays
*
* @return array<string, string>
*/
private function applyExclusions(array $holidays): array
{
if ([] === $this->excludedLabels) {
return $holidays;
}
return array_filter(
$holidays,
fn (string $label): bool => !in_array($label, $this->excludedLabels, true),
);
} }
} }

View File

@@ -330,6 +330,13 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
return $this->resolveCurrentLeaveYear($today); return $this->resolveCurrentLeaveYear($today);
} }
public function resolvePaidLeaveDays(Employee $employee, string $ruleCode, int $year): float
{
$balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $year);
return null !== $balance ? $balance->getPaidLeaveDays() : 0.0;
}
private function resolveEffectivePeriodStart( private function resolveEffectivePeriodStart(
Employee $employee, Employee $employee,
DateTimeImmutable $from, DateTimeImmutable $from,
@@ -778,13 +785,6 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
return null !== $balance ? $balance->getFractionedDays() : 0.0; return null !== $balance ? $balance->getFractionedDays() : 0.0;
} }
private function resolvePaidLeaveDays(Employee $employee, string $ruleCode, int $year): float
{
$balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $year);
return null !== $balance ? $balance->getPaidLeaveDays() : 0.0;
}
private function resolveCurrentLeaveYear(DateTimeImmutable $today): int private function resolveCurrentLeaveYear(DateTimeImmutable $today): int
{ {
$year = (int) $today->format('Y'); $year = (int) $today->format('Y');

View File

@@ -104,6 +104,13 @@ class LeaveRecapPrintProvider implements ProviderInterface
if (null !== $yearSummary) { if (null !== $yearSummary) {
if ($isForfait) { if ($isForfait) {
$paidLeaveDays = $this->leaveSummaryProvider->resolvePaidLeaveDays($employee, $yearSummary['ruleCode'], $leaveYear);
if ($paidLeaveDays > 0.0) {
$recomputed = $this->leaveSummaryProvider->computeYearSummary($employee, $leaveYear, $paidLeaveDays);
if (null !== $recomputed) {
$yearSummary = $recomputed;
}
}
$cpN1Remaining = round($yearSummary['previousYearRemainingDays'], 2); $cpN1Remaining = round($yearSummary['previousYearRemainingDays'], 2);
$cpN = (string) round($yearSummary['acquiredDays'], 2); $cpN = (string) round($yearSummary['acquiredDays'], 2);
$acquiredSaturdays = '-'; $acquiredSaturdays = '-';