diff --git a/doc/functional-rules.md b/doc/functional-rules.md
index f142445..b155bdd 100644
--- a/doc/functional-rules.md
+++ b/doc/functional-rules.md
@@ -173,6 +173,7 @@ Documents complementaires:
- 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
+- Écran Heures et Heures Conducteurs (vue semaine): la cellule du jour férié prend le fond `#b3e5fc` quand l'employé n'a pas d'absence ce jour-là, avec le nom du férié au survol (`title`). Si une absence est posée, la couleur de l'absence prime ; le `title` cumule les deux libellés (`Absence — Férié : Nom`).
- Règle courante:
- absences autorisées sur jour férié (création/édition depuis l'écran Heures et le Calendrier). Quand une absence est posée, le crédit virtuel férié est désactivé — c'est le `countAsWorkedHours` du type d'absence qui pilote
- saisie d'heures ou de jours de présence autorisée — les heures saisies comptent normalement dans le total hebdo et le calcul RTT
diff --git a/frontend/components/driver-hours/DriverHoursWeekView.vue b/frontend/components/driver-hours/DriverHoursWeekView.vue
index 8fc8c16..a750128 100644
--- a/frontend/components/driver-hours/DriverHoursWeekView.vue
+++ b/frontend/components/driver-hours/DriverHoursWeekView.vue
@@ -44,7 +44,7 @@
class="text-left leading-4 rounded-md px-2 py-1"
:class="daily.hasAbsence ? 'text-white' : ''"
:style="getDailyCellStyle(daily)"
- :title="daily.absenceLabel ?? ''"
+ :title="cellTitle(daily)"
>
J {{ formatMinutes(daily.dayMinutes) }}
N {{ formatMinutes(daily.nightMinutes) }}
@@ -93,12 +93,27 @@
import type { WeeklyWorkHourSummary } from '~/services/dto/work-hour'
import { contractNatureLabel } from '~/utils/contract'
+const HOLIDAY_BG_COLOR = '#b3e5fc'
+
const getDailyCellStyle = (daily: {
hasAbsence?: boolean
absenceColor?: string | null
+ holidayLabel?: string | null
}) => {
- if (!daily.hasAbsence) return undefined
- return { backgroundColor: daily.absenceColor || '#dc2626' }
+ if (daily.hasAbsence) return { backgroundColor: daily.absenceColor || '#dc2626' }
+ if (daily.holidayLabel) return { backgroundColor: HOLIDAY_BG_COLOR }
+ return undefined
+}
+
+const cellTitle = (daily: {
+ hasAbsence?: boolean
+ absenceLabel?: string | null
+ holidayLabel?: string | null
+}) => {
+ const parts: string[] = []
+ if (daily.absenceLabel) parts.push(daily.absenceLabel)
+ if (daily.holidayLabel) parts.push(`Férié : ${daily.holidayLabel}`)
+ return parts.join(' — ')
}
defineProps<{
diff --git a/frontend/components/hours/HoursWeekView.vue b/frontend/components/hours/HoursWeekView.vue
index 7e51ab5..d9033dc 100644
--- a/frontend/components/hours/HoursWeekView.vue
+++ b/frontend/components/hours/HoursWeekView.vue
@@ -27,6 +27,7 @@
class="flex items-center justify-between rounded-md px-2 py-1 text-xs"
:class="daily.hasAbsence ? 'text-white' : 'text-primary-500'"
:style="getDailyCellStyle(daily)"
+ :title="cellTitle(daily)"
>
{{ weekDayHeaders[i]?.label ?? '' }}
{{ daily.present ?? 0 }}
@@ -104,7 +105,7 @@
class="text-left leading-4 rounded-md px-2 py-1"
:class="daily.hasAbsence ? 'text-white' : ''"
:style="getDailyCellStyle(daily)"
- :title="daily.absenceLabel ?? ''"
+ :title="cellTitle(daily)"
>
{{ daily.present ?? 0 }}
@@ -153,12 +154,27 @@ const isInterimContract = (contractType?: ContractType | null) => {
return contractType === CONTRACT_TYPES.INTERIM
}
+const HOLIDAY_BG_COLOR = '#b3e5fc'
+
const getDailyCellStyle = (daily: {
hasAbsence?: boolean
absenceColor?: string | null
+ holidayLabel?: string | null
}) => {
- if (!daily.hasAbsence) return undefined
- return { backgroundColor: daily.absenceColor || '#dc2626' }
+ if (daily.hasAbsence) return { backgroundColor: daily.absenceColor || '#dc2626' }
+ if (daily.holidayLabel) return { backgroundColor: HOLIDAY_BG_COLOR }
+ return undefined
+}
+
+const cellTitle = (daily: {
+ hasAbsence?: boolean
+ absenceLabel?: string | null
+ holidayLabel?: string | null
+}) => {
+ const parts: string[] = []
+ if (daily.absenceLabel) parts.push(daily.absenceLabel)
+ if (daily.holidayLabel) parts.push(`Férié : ${daily.holidayLabel}`)
+ return parts.join(' — ')
}
defineProps<{
diff --git a/frontend/data/documentation-content.ts b/frontend/data/documentation-content.ts
index f0a7bce..42d405a 100644
--- a/frontend/data/documentation-content.ts
+++ b/frontend/data/documentation-content.ts
@@ -332,7 +332,7 @@ export const documentationSections: DocSection[] = [
requiredLevel: 'admin',
blocks: [
{ type: 'paragraph', content: 'La vue semaine est réservée aux administrateurs. Elle affiche une synthèse hebdomadaire par employé avec les heures supplémentaires calculées.' },
- { type: 'list', content: 'Filtrage par site et par employé\nDétail par jour avec totaux hebdomadaires\nColonnes de calcul : base, heures sup 25%, heures sup 50%, total récupération' },
+ { type: 'list', content: 'Filtrage par site et par employé\nDétail par jour avec totaux hebdomadaires\nColonnes de calcul : base, heures sup 25%, heures sup 50%, total récupération\nLes jours fériés sont signalés sur la cellule du jour : fond bleu clair quand pas d\'absence, nom du férié au survol' },
],
},
{
diff --git a/frontend/services/dto/work-hour.ts b/frontend/services/dto/work-hour.ts
index bbcfb32..64d0b5b 100644
--- a/frontend/services/dto/work-hour.ts
+++ b/frontend/services/dto/work-hour.ts
@@ -60,6 +60,7 @@ export type WeeklyWorkHourDailySummary = {
hasDinner?: boolean
hasOvernight?: boolean
virtualHolidayMinutes?: number
+ holidayLabel?: string | null
}
export type WeeklyWorkHourRowSummary = {
diff --git a/src/Dto/WorkHours/WeeklyDaySummary.php b/src/Dto/WorkHours/WeeklyDaySummary.php
index 85416db..7d1b3b8 100644
--- a/src/Dto/WorkHours/WeeklyDaySummary.php
+++ b/src/Dto/WorkHours/WeeklyDaySummary.php
@@ -22,5 +22,6 @@ final class WeeklyDaySummary
public bool $hasDinner = false,
public bool $hasOvernight = false,
public int $virtualHolidayMinutes = 0,
+ public ?string $holidayLabel = null,
) {}
}
diff --git a/src/State/WorkHourWeeklySummaryProvider.php b/src/State/WorkHourWeeklySummaryProvider.php
index 4cdde2b..41a196f 100644
--- a/src/State/WorkHourWeeklySummaryProvider.php
+++ b/src/State/WorkHourWeeklySummaryProvider.php
@@ -22,6 +22,7 @@ use App\Repository\Contract\AbsenceReadRepositoryInterface;
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
use App\Repository\Contract\WorkHourReadRepositoryInterface;
use App\Service\Contracts\EmployeeContractResolver;
+use App\Service\PublicHolidayServiceInterface;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\Service\WorkHours\DailyReferenceMinutesResolver;
use App\Service\WorkHours\HolidayVirtualHoursResolver;
@@ -31,6 +32,7 @@ use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+use Throwable;
final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
{
@@ -45,6 +47,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
private EmployeeContractResolver $contractResolver,
private DailyReferenceMinutesResolver $dailyReferenceResolver,
private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
+ private PublicHolidayServiceInterface $publicHolidayService,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourWeeklySummary
@@ -122,6 +125,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$contractNaturesByEmployeeDate = $this->contractResolver->resolveNaturesForEmployeesAndDays($employees, $days);
$isDriverByEmployeeDate = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
$workDaysByEmployeeDate = $this->contractResolver->resolveWorkDaysMinutesForEmployeesAndDays($employees, $days);
+ $holidayLabelsByDate = $this->buildHolidayLabelsForDays($days);
$metricsByEmployeeDate = [];
foreach ($workHours as $workHour) {
$employeeId = $workHour->getEmployee()?->getId();
@@ -324,6 +328,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
hasDinner: $hasDinner,
hasOvernight: $hasOvernight,
virtualHolidayMinutes: $virtualHolidayMinutes,
+ holidayLabel: $holidayLabelsByDate[$date] ?? null,
);
}
@@ -376,6 +381,38 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
return $rows;
}
+ /**
+ * @param list $days
+ *
+ * @return array
+ */
+ private function buildHolidayLabelsForDays(array $days): array
+ {
+ if ([] === $days) {
+ return [];
+ }
+
+ $years = [];
+ foreach ($days as $day) {
+ $years[substr($day, 0, 4)] = true;
+ }
+
+ $map = [];
+
+ try {
+ foreach (array_keys($years) as $year) {
+ $holidays = $this->publicHolidayService->getHolidaysDayByYears('metropole', (string) $year);
+ foreach ($holidays as $date => $label) {
+ $map[(string) $date] = (string) $label;
+ }
+ }
+ } catch (Throwable) {
+ return [];
+ }
+
+ return $map;
+ }
+
private function computeMetrics(WorkHour $workHour): WorkMetrics
{
$ranges = [
diff --git a/tests/State/WorkHourWeeklySummaryProviderTest.php b/tests/State/WorkHourWeeklySummaryProviderTest.php
index 327ada0..8f53d81 100644
--- a/tests/State/WorkHourWeeklySummaryProviderTest.php
+++ b/tests/State/WorkHourWeeklySummaryProviderTest.php
@@ -66,6 +66,7 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
$this->buildResolverStub(),
new DailyReferenceMinutesResolver(),
$this->buildHolidayResolver(),
+ $this->buildHolidayService(),
);
$this->expectException(AccessDeniedHttpException::class);
@@ -128,6 +129,7 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
$this->buildWeeklyResolverStub($employees),
new DailyReferenceMinutesResolver(),
$this->buildHolidayResolver(),
+ $this->buildHolidayService(),
);
$result = $provider->provide(new Get());
@@ -179,15 +181,20 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
}
private function buildHolidayResolver(array $holidayMap = []): HolidayVirtualHoursResolver
+ {
+ return new HolidayVirtualHoursResolver(
+ new DailyReferenceMinutesResolver(),
+ $this->buildHolidayService($holidayMap),
+ $this->createStub(EmployeeContractResolver::class),
+ );
+ }
+
+ private function buildHolidayService(array $holidayMap = []): PublicHolidayServiceInterface
{
$service = $this->createStub(PublicHolidayServiceInterface::class);
$service->method('getHolidaysDayByYears')->willReturn($holidayMap);
- return new HolidayVirtualHoursResolver(
- new DailyReferenceMinutesResolver(),
- $service,
- $this->createStub(EmployeeContractResolver::class),
- );
+ return $service;
}
private function buildResolverStub(): EmployeeContractResolver