feat : surlignage des jours fériés sur la vue semaine des heures
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Quand un employé n'a pas d'absence sur un jour férié, la cellule prend le fond bleu clair (#b3e5fc) et affiche le nom du férié au survol — cohérent avec la vue jour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
- 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
|
- É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:
|
- 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
|
- 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
|
- saisie d'heures ou de jours de présence autorisée — les heures saisies comptent normalement dans le total hebdo et le calcul RTT
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
class="text-left leading-4 rounded-md px-2 py-1"
|
class="text-left leading-4 rounded-md px-2 py-1"
|
||||||
:class="daily.hasAbsence ? 'text-white' : ''"
|
:class="daily.hasAbsence ? 'text-white' : ''"
|
||||||
:style="getDailyCellStyle(daily)"
|
:style="getDailyCellStyle(daily)"
|
||||||
:title="daily.absenceLabel ?? ''"
|
:title="cellTitle(daily)"
|
||||||
>
|
>
|
||||||
<div>J {{ formatMinutes(daily.dayMinutes) }}</div>
|
<div>J {{ formatMinutes(daily.dayMinutes) }}</div>
|
||||||
<div>N {{ formatMinutes(daily.nightMinutes) }}</div>
|
<div>N {{ formatMinutes(daily.nightMinutes) }}</div>
|
||||||
@@ -93,12 +93,27 @@
|
|||||||
import type { WeeklyWorkHourSummary } from '~/services/dto/work-hour'
|
import type { WeeklyWorkHourSummary } from '~/services/dto/work-hour'
|
||||||
import { contractNatureLabel } from '~/utils/contract'
|
import { contractNatureLabel } from '~/utils/contract'
|
||||||
|
|
||||||
|
const HOLIDAY_BG_COLOR = '#b3e5fc'
|
||||||
|
|
||||||
const getDailyCellStyle = (daily: {
|
const getDailyCellStyle = (daily: {
|
||||||
hasAbsence?: boolean
|
hasAbsence?: boolean
|
||||||
absenceColor?: string | null
|
absenceColor?: string | null
|
||||||
|
holidayLabel?: string | null
|
||||||
}) => {
|
}) => {
|
||||||
if (!daily.hasAbsence) return undefined
|
if (daily.hasAbsence) return { backgroundColor: daily.absenceColor || '#dc2626' }
|
||||||
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<{
|
defineProps<{
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
class="flex items-center justify-between rounded-md px-2 py-1 text-xs"
|
class="flex items-center justify-between rounded-md px-2 py-1 text-xs"
|
||||||
:class="daily.hasAbsence ? 'text-white' : 'text-primary-500'"
|
:class="daily.hasAbsence ? 'text-white' : 'text-primary-500'"
|
||||||
:style="getDailyCellStyle(daily)"
|
:style="getDailyCellStyle(daily)"
|
||||||
|
:title="cellTitle(daily)"
|
||||||
>
|
>
|
||||||
<span class="font-semibold">{{ weekDayHeaders[i]?.label ?? '' }}</span>
|
<span class="font-semibold">{{ weekDayHeaders[i]?.label ?? '' }}</span>
|
||||||
<span v-if="row.trackingMode === 'PRESENCE'">{{ daily.present ?? 0 }}</span>
|
<span v-if="row.trackingMode === 'PRESENCE'">{{ daily.present ?? 0 }}</span>
|
||||||
@@ -104,7 +105,7 @@
|
|||||||
class="text-left leading-4 rounded-md px-2 py-1"
|
class="text-left leading-4 rounded-md px-2 py-1"
|
||||||
:class="daily.hasAbsence ? 'text-white' : ''"
|
:class="daily.hasAbsence ? 'text-white' : ''"
|
||||||
:style="getDailyCellStyle(daily)"
|
:style="getDailyCellStyle(daily)"
|
||||||
:title="daily.absenceLabel ?? ''"
|
:title="cellTitle(daily)"
|
||||||
>
|
>
|
||||||
<template v-if="row.trackingMode === 'PRESENCE'">{{ daily.present ?? 0 }}</template>
|
<template v-if="row.trackingMode === 'PRESENCE'">{{ daily.present ?? 0 }}</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -153,12 +154,27 @@ const isInterimContract = (contractType?: ContractType | null) => {
|
|||||||
return contractType === CONTRACT_TYPES.INTERIM
|
return contractType === CONTRACT_TYPES.INTERIM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HOLIDAY_BG_COLOR = '#b3e5fc'
|
||||||
|
|
||||||
const getDailyCellStyle = (daily: {
|
const getDailyCellStyle = (daily: {
|
||||||
hasAbsence?: boolean
|
hasAbsence?: boolean
|
||||||
absenceColor?: string | null
|
absenceColor?: string | null
|
||||||
|
holidayLabel?: string | null
|
||||||
}) => {
|
}) => {
|
||||||
if (!daily.hasAbsence) return undefined
|
if (daily.hasAbsence) return { backgroundColor: daily.absenceColor || '#dc2626' }
|
||||||
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<{
|
defineProps<{
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ export const documentationSections: DocSection[] = [
|
|||||||
requiredLevel: 'admin',
|
requiredLevel: 'admin',
|
||||||
blocks: [
|
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: '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' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export type WeeklyWorkHourDailySummary = {
|
|||||||
hasDinner?: boolean
|
hasDinner?: boolean
|
||||||
hasOvernight?: boolean
|
hasOvernight?: boolean
|
||||||
virtualHolidayMinutes?: number
|
virtualHolidayMinutes?: number
|
||||||
|
holidayLabel?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WeeklyWorkHourRowSummary = {
|
export type WeeklyWorkHourRowSummary = {
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ final class WeeklyDaySummary
|
|||||||
public bool $hasDinner = false,
|
public bool $hasDinner = false,
|
||||||
public bool $hasOvernight = false,
|
public bool $hasOvernight = false,
|
||||||
public int $virtualHolidayMinutes = 0,
|
public int $virtualHolidayMinutes = 0,
|
||||||
|
public ?string $holidayLabel = null,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
|||||||
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
||||||
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
||||||
use App\Service\Contracts\EmployeeContractResolver;
|
use App\Service\Contracts\EmployeeContractResolver;
|
||||||
|
use App\Service\PublicHolidayServiceInterface;
|
||||||
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
||||||
use App\Service\WorkHours\DailyReferenceMinutesResolver;
|
use App\Service\WorkHours\DailyReferenceMinutesResolver;
|
||||||
use App\Service\WorkHours\HolidayVirtualHoursResolver;
|
use App\Service\WorkHours\HolidayVirtualHoursResolver;
|
||||||
@@ -31,6 +32,7 @@ use Symfony\Bundle\SecurityBundle\Security;
|
|||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||||
{
|
{
|
||||||
@@ -45,6 +47,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
|||||||
private EmployeeContractResolver $contractResolver,
|
private EmployeeContractResolver $contractResolver,
|
||||||
private DailyReferenceMinutesResolver $dailyReferenceResolver,
|
private DailyReferenceMinutesResolver $dailyReferenceResolver,
|
||||||
private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
|
private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
|
||||||
|
private PublicHolidayServiceInterface $publicHolidayService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourWeeklySummary
|
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);
|
$contractNaturesByEmployeeDate = $this->contractResolver->resolveNaturesForEmployeesAndDays($employees, $days);
|
||||||
$isDriverByEmployeeDate = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
|
$isDriverByEmployeeDate = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
|
||||||
$workDaysByEmployeeDate = $this->contractResolver->resolveWorkDaysMinutesForEmployeesAndDays($employees, $days);
|
$workDaysByEmployeeDate = $this->contractResolver->resolveWorkDaysMinutesForEmployeesAndDays($employees, $days);
|
||||||
|
$holidayLabelsByDate = $this->buildHolidayLabelsForDays($days);
|
||||||
$metricsByEmployeeDate = [];
|
$metricsByEmployeeDate = [];
|
||||||
foreach ($workHours as $workHour) {
|
foreach ($workHours as $workHour) {
|
||||||
$employeeId = $workHour->getEmployee()?->getId();
|
$employeeId = $workHour->getEmployee()?->getId();
|
||||||
@@ -324,6 +328,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
|||||||
hasDinner: $hasDinner,
|
hasDinner: $hasDinner,
|
||||||
hasOvernight: $hasOvernight,
|
hasOvernight: $hasOvernight,
|
||||||
virtualHolidayMinutes: $virtualHolidayMinutes,
|
virtualHolidayMinutes: $virtualHolidayMinutes,
|
||||||
|
holidayLabel: $holidayLabelsByDate[$date] ?? null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,6 +381,38 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
|||||||
return $rows;
|
return $rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $days
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
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
|
private function computeMetrics(WorkHour $workHour): WorkMetrics
|
||||||
{
|
{
|
||||||
$ranges = [
|
$ranges = [
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
|
|||||||
$this->buildResolverStub(),
|
$this->buildResolverStub(),
|
||||||
new DailyReferenceMinutesResolver(),
|
new DailyReferenceMinutesResolver(),
|
||||||
$this->buildHolidayResolver(),
|
$this->buildHolidayResolver(),
|
||||||
|
$this->buildHolidayService(),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->expectException(AccessDeniedHttpException::class);
|
$this->expectException(AccessDeniedHttpException::class);
|
||||||
@@ -128,6 +129,7 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
|
|||||||
$this->buildWeeklyResolverStub($employees),
|
$this->buildWeeklyResolverStub($employees),
|
||||||
new DailyReferenceMinutesResolver(),
|
new DailyReferenceMinutesResolver(),
|
||||||
$this->buildHolidayResolver(),
|
$this->buildHolidayResolver(),
|
||||||
|
$this->buildHolidayService(),
|
||||||
);
|
);
|
||||||
|
|
||||||
$result = $provider->provide(new Get());
|
$result = $provider->provide(new Get());
|
||||||
@@ -179,15 +181,20 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function buildHolidayResolver(array $holidayMap = []): HolidayVirtualHoursResolver
|
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 = $this->createStub(PublicHolidayServiceInterface::class);
|
||||||
$service->method('getHolidaysDayByYears')->willReturn($holidayMap);
|
$service->method('getHolidaysDayByYears')->willReturn($holidayMap);
|
||||||
|
|
||||||
return new HolidayVirtualHoursResolver(
|
return $service;
|
||||||
new DailyReferenceMinutesResolver(),
|
|
||||||
$service,
|
|
||||||
$this->createStub(EmployeeContractResolver::class),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildResolverStub(): EmployeeContractResolver
|
private function buildResolverStub(): EmployeeContractResolver
|
||||||
|
|||||||
Reference in New Issue
Block a user