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
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
This commit is contained in:
3
.env
3
.env
@@ -38,6 +38,9 @@ DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&ch
|
||||
|
||||
###> app ###
|
||||
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 ###
|
||||
|
||||
###> nelmio/cors-bundle ###
|
||||
|
||||
@@ -35,6 +35,13 @@
|
||||
- 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`
|
||||
|
||||
## 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
|
||||
- `isValid` (RH): locks line for everyone (admin can only untoggle validation)
|
||||
- `isSiteValid` (site manager): locks for non-admin, admin can still edit
|
||||
|
||||
@@ -25,6 +25,7 @@ services:
|
||||
App\Service\PublicHolidayService:
|
||||
arguments:
|
||||
$holidayUrl: '%env(HOLIDAY_URL)%'
|
||||
$excludedLabels: '%env(default::EXCLUDED_PUBLIC_HOLIDAYS)%'
|
||||
|
||||
App\Service\Rtt\RttRecoveryComputationService:
|
||||
arguments:
|
||||
|
||||
@@ -23,3 +23,4 @@ DEFAULT_URI=https://sirh.malio-dev.fr
|
||||
APP_SHARE_DIR=var/share
|
||||
RTT_START_DATE=2026-02-23
|
||||
HOLIDAY_URL="https://calendrier.api.gouv.fr/jours-feries/"
|
||||
EXCLUDED_PUBLIC_HOLIDAYS="Lundi de Pentecôte"
|
||||
|
||||
@@ -161,10 +161,14 @@ Documents complementaires:
|
||||
## 7) Férié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
|
||||
- É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:
|
||||
- absences bloquées sur jour férié
|
||||
- saisie d'heures autorisée
|
||||
- absences bloquées sur jour férié (création/édition) — bouton "Modifier" masqué comme pour les formations
|
||||
- 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)
|
||||
|
||||
|
||||
@@ -56,19 +56,31 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="pl-2 min-w-0 self-stretch flex flex-col gap-1 justify-between py-0.5">
|
||||
<p
|
||||
class="w-full min-w-0 rounded-md px-2 py-1 text-xs truncate"
|
||||
:class="getRowAbsenceLabel(employee.id) ? 'text-white' : 'invisible'"
|
||||
:title="getRowAbsenceLabel(employee.id) || ''"
|
||||
:style="getRowAbsenceStyle(employee.id)"
|
||||
>
|
||||
{{ getRowAbsenceLabel(employee.id) || '—' }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<p
|
||||
class="w-full min-w-0 rounded-md px-2 py-1 text-xs truncate"
|
||||
:class="getRowAbsenceLabel(employee.id) ? 'text-white' : 'invisible'"
|
||||
:title="getRowAbsenceLabel(employee.id) || ''"
|
||||
:style="getRowAbsenceStyle(employee.id)"
|
||||
>
|
||||
{{ 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
|
||||
v-if="!isHoliday"
|
||||
type="button"
|
||||
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'"
|
||||
:disabled="isHoliday || isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id)"
|
||||
:class="isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id) ? 'text-neutral-400 cursor-not-allowed' : 'text-primary-500 cursor-pointer'"
|
||||
:disabled="isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id)"
|
||||
@click="onAbsenceClick(employee.id)"
|
||||
>
|
||||
Modifier
|
||||
@@ -164,6 +176,7 @@ const props = defineProps<{
|
||||
isSiteManager: boolean
|
||||
dayGridCols: string
|
||||
isHoliday: boolean
|
||||
holidayLabel: string
|
||||
contractLabel: (employee: Employee) => string
|
||||
isRowLocked: (employeeId: number) => boolean
|
||||
hasContractAtSelectedDate: (employeeId: number) => boolean
|
||||
|
||||
@@ -66,6 +66,15 @@
|
||||
>
|
||||
{{ 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>
|
||||
<p
|
||||
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"
|
||||
@@ -76,11 +85,11 @@
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="!hasRowFormation(employee.id)"
|
||||
v-if="!hasRowFormation(employee.id) && !isHoliday"
|
||||
type="button"
|
||||
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'"
|
||||
:disabled="isHoliday || isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id)"
|
||||
:class="isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id) ? 'text-neutral-400 cursor-not-allowed' : 'text-primary-500 cursor-pointer'"
|
||||
:disabled="isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id)"
|
||||
@click="onAbsenceClick(employee.id)"
|
||||
>
|
||||
Modifier
|
||||
@@ -198,6 +207,7 @@ const props = defineProps<{
|
||||
isSiteManager: boolean
|
||||
dayGridCols: string
|
||||
isHoliday: boolean
|
||||
holidayLabel: string
|
||||
contractLabel: (employee: Employee) => string
|
||||
isTimeTracking: (employee: Employee) => boolean
|
||||
isPresenceTracking: (employee: Employee) => boolean
|
||||
|
||||
@@ -381,7 +381,6 @@ export const useDriverHoursPage = () => {
|
||||
if (dayRow && dayRow.hasContractAtDate === false) {
|
||||
return 'Contrat non démarré'
|
||||
}
|
||||
if (isSelectedDateHoliday.value) return 'Férié'
|
||||
if (!dayRow?.absenceLabel) return ''
|
||||
if (dayRow.absenceHalf === 'AM' || dayRow.absenceHalf === 'PM') {
|
||||
const halfLabel = dayRow.absenceHalf === 'AM' ? 'Matin' : 'ap.-m.'
|
||||
@@ -941,6 +940,7 @@ export const useDriverHoursPage = () => {
|
||||
saveButtonClass,
|
||||
formattedSelectedDate,
|
||||
isSelectedDateHoliday,
|
||||
selectedHolidayLabel,
|
||||
weekDayHeaders,
|
||||
shortcutButtonClass,
|
||||
weekShortcutButtonClass,
|
||||
|
||||
@@ -458,7 +458,6 @@ export const useHoursPage = () => {
|
||||
if (dayRow && dayRow.hasContractAtDate === false) {
|
||||
return 'Contrat non démarré'
|
||||
}
|
||||
if (isSelectedDateHoliday.value) return 'Férié'
|
||||
if (!dayRow?.absenceLabel) return ''
|
||||
if (dayRow.absenceHalf === 'AM' || dayRow.absenceHalf === 'PM') {
|
||||
const halfLabel = dayRow.absenceHalf === 'AM' ? 'Matin' : 'ap.-m.'
|
||||
@@ -1127,6 +1126,7 @@ export const useHoursPage = () => {
|
||||
saveButtonClass,
|
||||
formattedSelectedDate,
|
||||
isSelectedDateHoliday,
|
||||
selectedHolidayLabel,
|
||||
weekDayHeaders,
|
||||
shortcutButtonClass,
|
||||
weekShortcutButtonClass,
|
||||
|
||||
@@ -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: '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:00–21:00), heures de nuit (00:00–06:00 et 21:00–24: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.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
:is-site-manager="isSiteManager"
|
||||
:day-grid-cols="dayGridCols"
|
||||
:is-holiday="isSelectedDateHoliday"
|
||||
:holiday-label="selectedHolidayLabel"
|
||||
:contract-label="contractLabel"
|
||||
:is-row-locked="isRowLocked"
|
||||
:has-contract-at-selected-date="hasContractAtSelectedDate"
|
||||
@@ -174,6 +175,7 @@ const {
|
||||
closeAbsenceDrawer,
|
||||
formatMinutes,
|
||||
isSelectedDateHoliday,
|
||||
selectedHolidayLabel,
|
||||
handleSave
|
||||
} = useDriverHoursPage()
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
:is-site-manager="isSiteManager"
|
||||
:day-grid-cols="dayGridCols"
|
||||
:is-holiday="isSelectedDateHoliday"
|
||||
:holiday-label="selectedHolidayLabel"
|
||||
:contract-label="contractLabel"
|
||||
:is-time-tracking="isTimeTracking"
|
||||
:is-presence-tracking="isPresenceTracking"
|
||||
@@ -141,6 +142,7 @@ const {
|
||||
isSubmitting,
|
||||
dayGridCols,
|
||||
isSelectedDateHoliday,
|
||||
selectedHolidayLabel,
|
||||
weekGridCols,
|
||||
saveButtonClass,
|
||||
formattedSelectedDate,
|
||||
|
||||
@@ -17,11 +17,22 @@ use Throwable;
|
||||
|
||||
final readonly class PublicHolidayService implements PublicHolidayServiceInterface
|
||||
{
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private array $excludedLabels;
|
||||
|
||||
public function __construct(
|
||||
private HttpClientInterface $client,
|
||||
private string $holidayUrl,
|
||||
private CacheInterface $cache,
|
||||
) {}
|
||||
string $excludedLabels = '',
|
||||
) {
|
||||
$this->excludedLabels = array_values(array_filter(
|
||||
array_map('trim', explode(',', $excludedLabels)),
|
||||
static fn (string $label): bool => '' !== $label,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
@@ -35,7 +46,7 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa
|
||||
$zone = strtolower(trim($zone));
|
||||
$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);
|
||||
$url = $this->holidayUrl."{$zone}.json";
|
||||
|
||||
@@ -56,6 +67,8 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa
|
||||
|
||||
return json_decode($response->getContent(), true);
|
||||
});
|
||||
|
||||
return $this->applyExclusions($holidays);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +83,7 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa
|
||||
$years = trim($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);
|
||||
$url = $this->holidayUrl."{$zone}/{$years}.json";
|
||||
|
||||
@@ -88,5 +101,24 @@ final readonly class PublicHolidayService implements PublicHolidayServiceInterfa
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user