Compare commits

..

2 Commits

Author SHA1 Message Date
gitea-actions
f0ee489c26 chore: bump version to v0.1.40
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m22s
2026-03-16 08:13:46 +00:00
01f8058f56 fix : redirection après login + écran des heures chauffeurs
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-03-16 09:13:35 +01:00
15 changed files with 256 additions and 78 deletions

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.1.39' app.version: '0.1.40'

View File

@@ -124,18 +124,20 @@ Documents complementaires:
- Colonnes spécifiques (vue jour): - Colonnes spécifiques (vue jour):
- Heure de jour (durée HH:MM via TimeSelect) - Heure de jour (durée HH:MM via TimeSelect)
- Heure de nuit (durée HH:MM via TimeSelect) - Heure de nuit (durée HH:MM via TimeSelect)
- Total (somme jour + nuit, calculé) - Heure atelier (durée HH:MM via TimeSelect)
- Total (somme jour + nuit + atelier, calculé)
- Petit déjeuner (checkbox) - Petit déjeuner (checkbox)
- Déjeuner (checkbox) - Déjeuner (checkbox)
- Dîner (checkbox)
- Nuitée (checkbox) - Nuitée (checkbox)
- Stockage backend: - Stockage backend:
- `dayHoursMinutes` et `nightHoursMinutes` (entiers, minutes) sur `WorkHour` - `dayHoursMinutes`, `nightHoursMinutes` et `workshopHoursMinutes` (entiers, minutes) sur `WorkHour`
- `hasBreakfast`, `hasLunch`, `hasOvernight` (booleans) sur `WorkHour` - `hasBreakfast`, `hasLunch`, `hasDinner`, `hasOvernight` (booleans) sur `WorkHour`
- les champs time classiques (morning/afternoon/evening) sont mis à null pour les chauffeurs - les champs time classiques (morning/afternoon/evening) sont mis à null pour les chauffeurs
- 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 par jour + indicateurs repas/nuitée - jour/nuit/atelier par jour + indicateurs repas/dîner/nuitée
- totaux hebdo: jour, nuit, total, compteurs petit déj/déjeuner/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)
- Exposé en API via un getter virtuel sur `Employee` (`employee:read`) qui résout depuis la période active - Exposé en API via un getter virtuel sur `Employee` (`employee:read`) qui résout depuis la période active

View File

@@ -9,9 +9,11 @@
<span class="pl-2">Absence</span> <span class="pl-2">Absence</span>
<span class="pl-4">Heure de jour</span> <span class="pl-4">Heure de jour</span>
<span class="pl-2">Heure de nuit</span> <span class="pl-2">Heure de nuit</span>
<span class="pl-2">Heure atelier</span>
<span class="pl-2">Total</span> <span class="pl-2">Total</span>
<span>Petit déj.</span> <span>Petit déj.</span>
<span>Déjeuner</span> <span>Déjeuner</span>
<span>Dîner</span>
<span>Nuitée</span> <span>Nuitée</span>
<span v-if="isAdmin" class="flex justify-between items-center"> <span v-if="isAdmin" class="flex justify-between items-center">
<span>Valider</span> <span>Valider</span>
@@ -96,6 +98,12 @@
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id)" :disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id)"
/> />
</div> </div>
<div class="pl-2">
<TimeSelect
v-model="rows[employee.id].workshopHours"
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id)"
/>
</div>
<div class="pl-2 text-sm font-semibold"> <div class="pl-2 text-sm font-semibold">
{{ formatMinutes(getRowMetrics(employee.id).totalMinutes) }} {{ formatMinutes(getRowMetrics(employee.id).totalMinutes) }}
</div> </div>
@@ -115,6 +123,14 @@
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id)" :disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id)"
/> />
</div> </div>
<div class="flex">
<input
v-model="rows[employee.id].hasDinner"
type="checkbox"
class="cursor-pointer h-4 w-4"
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id)"
/>
</div>
<div class="flex"> <div class="flex">
<input <input
v-model="rows[employee.id].hasOvernight" v-model="rows[employee.id].hasOvernight"

View File

@@ -7,8 +7,9 @@
:style="{ gridTemplateColumns: weekGridCols }" :style="{ gridTemplateColumns: weekGridCols }"
> >
<span>Nom</span> <span>Nom</span>
<span v-for="day in weekDayHeaders" :key="day.date" class="text-left">{{ day.label }}</span> <span v-for="day in weekDayHeaders" :key="day.date" class="text-left">{{ day.weekday }}<br>{{ day.dayDate }}</span>
<span>Jour/Nuit <br>sem.</span> <span>Jour/Nuit <br>sem.</span>
<span>Atelier <br>sem.</span>
<span>Total <br>sem.</span> <span>Total <br>sem.</span>
<span>Total <br>h. supp.</span> <span>Total <br>h. supp.</span>
<span>+25%</span> <span>+25%</span>
@@ -16,7 +17,8 @@
<span>Total <br>récup.</span> <span>Total <br>récup.</span>
<span>Petit <br>déj.</span> <span>Petit <br>déj.</span>
<span>Déj.</span> <span>Déj.</span>
<span>Nuitée</span> <span>Dîner</span>
<span>Nuit.</span>
</div> </div>
<div class="border-x border-b border-primary-500 rounded-b-md"> <div class="border-x border-b border-primary-500 rounded-b-md">
@@ -44,9 +46,11 @@
> >
<div>J {{ formatMinutes(daily.dayMinutes) }}</div> <div>J {{ formatMinutes(daily.dayMinutes) }}</div>
<div>N {{ formatMinutes(daily.nightMinutes) }}</div> <div>N {{ formatMinutes(daily.nightMinutes) }}</div>
<div v-if="daily.hasBreakfast || daily.hasLunch || daily.hasOvernight" class="text-[10px] flex gap-1 mt-0.5"> <div v-if="daily.workshopMinutes">A {{ formatMinutes(daily.workshopMinutes) }}</div>
<div v-if="daily.hasBreakfast || daily.hasLunch || daily.hasDinner || daily.hasOvernight" class="text-[10px] flex gap-1 mt-0.5">
<span v-if="daily.hasBreakfast" title="Petit déjeuner">PD</span> <span v-if="daily.hasBreakfast" title="Petit déjeuner">PD</span>
<span v-if="daily.hasLunch" title="Déjeuner">DJ</span> <span v-if="daily.hasLunch" title="Déjeuner">DJ</span>
<span v-if="daily.hasDinner" title="Dîner">DI</span>
<span v-if="daily.hasOvernight" title="Nuitée">NU</span> <span v-if="daily.hasOvernight" title="Nuitée">NU</span>
</div> </div>
</div> </div>
@@ -55,6 +59,9 @@
<div>J {{ formatMinutes(row.weeklyDayMinutes) }}</div> <div>J {{ formatMinutes(row.weeklyDayMinutes) }}</div>
<div>N {{ formatMinutes(row.weeklyNightMinutes) }}</div> <div>N {{ formatMinutes(row.weeklyNightMinutes) }}</div>
</div> </div>
<div class="font-semibold">
{{ formatMinutes(row.weeklyWorkshopMinutes ?? 0) }}
</div>
<div class="font-semibold"> <div class="font-semibold">
{{ formatMinutes(row.weeklyTotalMinutes) }} {{ formatMinutes(row.weeklyTotalMinutes) }}
</div> </div>
@@ -72,6 +79,7 @@
</div> </div>
<div class="font-semibold">{{ row.weeklyBreakfastCount ?? 0 }}</div> <div class="font-semibold">{{ row.weeklyBreakfastCount ?? 0 }}</div>
<div class="font-semibold">{{ row.weeklyLunchCount ?? 0 }}</div> <div class="font-semibold">{{ row.weeklyLunchCount ?? 0 }}</div>
<div class="font-semibold">{{ row.weeklyDinnerCount ?? 0 }}</div>
<div class="font-semibold">{{ row.weeklyOvernightCount ?? 0 }}</div> <div class="font-semibold">{{ row.weeklyOvernightCount ?? 0 }}</div>
</div> </div>
</div> </div>
@@ -94,7 +102,7 @@ defineProps<{
isWeekLoading: boolean isWeekLoading: boolean
weekGridCols: string weekGridCols: string
weeklySummary: WeeklyWorkHourSummary | null weeklySummary: WeeklyWorkHourSummary | null
weekDayHeaders: Array<{ date: string; label: string }> weekDayHeaders: Array<{ date: string; weekday: string; dayDate: string }>
formatMinutes: (minutes: number) => string formatMinutes: (minutes: number) => string
}>() }>()
</script> </script>

View File

@@ -22,7 +22,6 @@ import {
} from '~/services/work-hours' } from '~/services/work-hours'
import { import {
formatDateLongFr, formatDateLongFr,
formatWeekDayHeaderFr,
formatWeekRangeFr, formatWeekRangeFr,
getIsoWeekNumber, getIsoWeekNumber,
getOffsetFromTodayYmd, getOffsetFromTodayYmd,
@@ -73,10 +72,10 @@ 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 ? `${metricCol}` : `${metricCol} ${metricCol}`
return `1.2fr 0.6fr 0.8fr 0.8fr ${metricCol} ${metricCol} ${metricCol} ${metricCol} ${validationCols}` return `1.2fr 0.6fr 0.8fr 0.8fr 0.8fr ${metricCol} ${metricCol} ${metricCol} ${metricCol} ${metricCol} ${validationCols}`
}) })
const weekGridCols = '1.6fr repeat(7, 1fr) repeat(6, 0.6fr) repeat(3, 0.4fr)' const weekGridCols = '1.6fr repeat(7, 0.6fr) repeat(7, 0.6fr) repeat(4, 0.4fr)'
const sites = computed<Site[]>(() => { const sites = computed<Site[]>(() => {
const siteMap = new Map<number, Site>() const siteMap = new Map<number, Site>()
@@ -265,7 +264,13 @@ export const useDriverHoursPage = () => {
const weekDayHeaders = computed(() => { const weekDayHeaders = computed(() => {
const days = weeklySummary.value?.days ?? [] const days = weeklySummary.value?.days ?? []
return days.map((date) => ({ date, label: formatWeekDayHeaderFr(date) })) return days.map((date) => {
const parsed = parseYmd(date)
if (!parsed) return { date, weekday: '', dayDate: '' }
const weekday = new Intl.DateTimeFormat('fr-FR', { weekday: 'short' }).format(parsed)
const dayDate = new Intl.DateTimeFormat('fr-FR', { day: '2-digit', month: '2-digit' }).format(parsed)
return { date, weekday, dayDate }
})
}) })
const shiftDate = (steps: number) => { const shiftDate = (steps: number) => {
@@ -331,8 +336,10 @@ export const useDriverHoursPage = () => {
workHourId: null, workHourId: null,
dayHours: '', dayHours: '',
nightHours: '', nightHours: '',
workshopHours: '',
hasBreakfast: false, hasBreakfast: false,
hasLunch: false, hasLunch: false,
hasDinner: false,
hasOvernight: false, hasOvernight: false,
isSiteValid: false, isSiteValid: false,
isValid: false, isValid: false,
@@ -357,8 +364,9 @@ export const useDriverHoursPage = () => {
const row = rows.value[employeeId] ?? emptyRow() const row = rows.value[employeeId] ?? emptyRow()
const dayMinutes = toMinutes(row.dayHours) const dayMinutes = toMinutes(row.dayHours)
const nightMinutes = toMinutes(row.nightHours) const nightMinutes = toMinutes(row.nightHours)
const totalMinutes = dayMinutes + nightMinutes const workshopMinutes = toMinutes(row.workshopHours)
return { dayMinutes, nightMinutes, totalMinutes } const totalMinutes = dayMinutes + nightMinutes + workshopMinutes
return { dayMinutes, nightMinutes, workshopMinutes, totalMinutes }
} }
const getRowAbsenceLabel = (employeeId: number) => { const getRowAbsenceLabel = (employeeId: number) => {
@@ -412,8 +420,10 @@ export const useDriverHoursPage = () => {
workHourId: workHour?.id ?? null, workHourId: workHour?.id ?? null,
dayHours: minutesToTimeString(workHour?.dayHoursMinutes), dayHours: minutesToTimeString(workHour?.dayHoursMinutes),
nightHours: minutesToTimeString(workHour?.nightHoursMinutes), nightHours: minutesToTimeString(workHour?.nightHoursMinutes),
workshopHours: minutesToTimeString(workHour?.workshopHoursMinutes),
hasBreakfast: workHour?.hasBreakfast ?? false, hasBreakfast: workHour?.hasBreakfast ?? false,
hasLunch: workHour?.hasLunch ?? false, hasLunch: workHour?.hasLunch ?? false,
hasDinner: workHour?.hasDinner ?? false,
hasOvernight: workHour?.hasOvernight ?? false, hasOvernight: workHour?.hasOvernight ?? false,
isSiteValid: workHour?.isSiteValid ?? false, isSiteValid: workHour?.isSiteValid ?? false,
isValid: workHour?.isValid ?? false, isValid: workHour?.isValid ?? false,
@@ -556,8 +566,10 @@ export const useDriverHoursPage = () => {
isPresentAfternoon: false, isPresentAfternoon: false,
dayHoursMinutes: null, dayHoursMinutes: null,
nightHoursMinutes: null, nightHoursMinutes: null,
workshopHoursMinutes: null,
hasBreakfast: false, hasBreakfast: false,
hasLunch: false, hasLunch: false,
hasDinner: false,
hasOvernight: false hasOvernight: false
}) })
@@ -859,6 +871,7 @@ export const useDriverHoursPage = () => {
const row = rows.value[employeeId] ?? emptyRow() const row = rows.value[employeeId] ?? emptyRow()
const dayMin = toMinutes(row.dayHours) const dayMin = toMinutes(row.dayHours)
const nightMin = toMinutes(row.nightHours) const nightMin = toMinutes(row.nightHours)
const workshopMin = toMinutes(row.workshopHours)
return { return {
employeeId, employeeId,
@@ -872,8 +885,10 @@ export const useDriverHoursPage = () => {
isPresentAfternoon: false, isPresentAfternoon: false,
dayHoursMinutes: dayMin || null, dayHoursMinutes: dayMin || null,
nightHoursMinutes: nightMin || null, nightHoursMinutes: nightMin || null,
workshopHoursMinutes: workshopMin || null,
hasBreakfast: row.hasBreakfast, hasBreakfast: row.hasBreakfast,
hasLunch: row.hasLunch, hasLunch: row.hasLunch,
hasDinner: row.hasDinner,
hasOvernight: row.hasOvernight hasOvernight: row.hasOvernight
} }
}) })

View File

@@ -21,14 +21,16 @@
<NuxtLink <NuxtLink
to="/hours" to="/hours"
class="flex items-center gap-2 py-2 text-md text-black hover:bg-tertiary-500 hover:text-primary-500" class="flex items-center gap-2 py-2 text-md text-black hover:bg-tertiary-500 hover:text-primary-500"
:class="route.path.startsWith('/hours') :class="[
? 'bg-tertiary-500 text-primary-500 font-bold' route.path.startsWith('/hours') ? 'bg-tertiary-500 text-primary-500 font-bold' : '',
: ''" !isAdmin ? 'border-t border-secondary-500 pt-3' : ''
]"
> >
<Icon name="mdi:clock-time-four-outline" size="24"/> <Icon name="mdi:clock-time-four-outline" size="24"/>
<p>Heures</p> <p>Heures</p>
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
v-if="isAdmin"
to="/driver-hours" to="/driver-hours"
class="flex items-center gap-2 py-2 text-md text-black hover:bg-tertiary-500 hover:text-primary-500" class="flex items-center gap-2 py-2 text-md text-black hover:bg-tertiary-500 hover:text-primary-500"
:class="route.path.startsWith('/driver-hours') :class="route.path.startsWith('/driver-hours')

View File

@@ -68,7 +68,8 @@ const handleSubmit = async () => {
try { try {
await auth.login(username.value, password.value) await auth.login(username.value, password.value)
await router.push('/calendar') const isAdmin = auth.user?.roles?.includes('ROLE_ADMIN')
await router.push(isAdmin ? '/calendar' : '/hours')
} finally { } finally {
isSubmitting.value = false isSubmitting.value = false
} }

View File

@@ -15,8 +15,10 @@ export type WorkHour = {
isPresentAfternoon?: boolean isPresentAfternoon?: boolean
dayHoursMinutes?: number | null dayHoursMinutes?: number | null
nightHoursMinutes?: number | null nightHoursMinutes?: number | null
workshopHoursMinutes?: number | null
hasBreakfast?: boolean hasBreakfast?: boolean
hasLunch?: boolean hasLunch?: boolean
hasDinner?: boolean
hasOvernight?: boolean hasOvernight?: boolean
isSiteValid?: boolean isSiteValid?: boolean
isValid?: boolean isValid?: boolean
@@ -35,8 +37,10 @@ export type WorkHourEntryPayload = {
isPresentAfternoon?: boolean isPresentAfternoon?: boolean
dayHoursMinutes?: number | null dayHoursMinutes?: number | null
nightHoursMinutes?: number | null nightHoursMinutes?: number | null
workshopHoursMinutes?: number | null
hasBreakfast?: boolean hasBreakfast?: boolean
hasLunch?: boolean hasLunch?: boolean
hasDinner?: boolean
hasOvernight?: boolean hasOvernight?: boolean
} }
@@ -44,6 +48,7 @@ export type WeeklyWorkHourDailySummary = {
date: string date: string
dayMinutes: number dayMinutes: number
nightMinutes: number nightMinutes: number
workshopMinutes?: number
totalMinutes: number totalMinutes: number
present?: number | null present?: number | null
hasAbsence?: boolean hasAbsence?: boolean
@@ -51,6 +56,7 @@ export type WeeklyWorkHourDailySummary = {
absenceColor?: string | null absenceColor?: string | null
hasBreakfast?: boolean hasBreakfast?: boolean
hasLunch?: boolean hasLunch?: boolean
hasDinner?: boolean
hasOvernight?: boolean hasOvernight?: boolean
} }
@@ -65,6 +71,7 @@ export type WeeklyWorkHourRowSummary = {
daily: WeeklyWorkHourDailySummary[] daily: WeeklyWorkHourDailySummary[]
weeklyDayMinutes: number weeklyDayMinutes: number
weeklyNightMinutes: number weeklyNightMinutes: number
weeklyWorkshopMinutes?: number
weeklyTotalMinutes: number weeklyTotalMinutes: number
weeklyPresenceCount?: number weeklyPresenceCount?: number
weeklyOvertimeTotalMinutes?: number weeklyOvertimeTotalMinutes?: number
@@ -74,6 +81,7 @@ export type WeeklyWorkHourRowSummary = {
isDriver?: boolean isDriver?: boolean
weeklyBreakfastCount?: number weeklyBreakfastCount?: number
weeklyLunchCount?: number weeklyLunchCount?: number
weeklyDinnerCount?: number
weeklyOvernightCount?: number weeklyOvernightCount?: number
} }
@@ -106,8 +114,10 @@ export type DriverHourRow = {
workHourId: number | null workHourId: number | null
dayHours: string dayHours: string
nightHours: string nightHours: string
workshopHours: string
hasBreakfast: boolean hasBreakfast: boolean
hasLunch: boolean hasLunch: boolean
hasDinner: boolean
hasOvernight: boolean hasOvernight: boolean
isSiteValid: boolean isSiteValid: boolean
isValid: boolean isValid: boolean

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260316100000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add has_dinner column to work_hours';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE work_hours ADD has_dinner BOOLEAN DEFAULT FALSE NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE work_hours DROP COLUMN has_dinner');
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260316100100 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add workshop_hours_minutes column to work_hours';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE work_hours ADD workshop_hours_minutes INTEGER DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE work_hours DROP COLUMN workshop_hours_minutes');
}
}

View File

@@ -10,6 +10,7 @@ final class WeeklyDaySummary
public string $date, public string $date,
public int $dayMinutes, public int $dayMinutes,
public int $nightMinutes, public int $nightMinutes,
public int $workshopMinutes,
public int $totalMinutes, public int $totalMinutes,
public ?float $present = null, public ?float $present = null,
public bool $hasAbsence = false, public bool $hasAbsence = false,
@@ -17,6 +18,7 @@ final class WeeklyDaySummary
public ?string $absenceColor = null, public ?string $absenceColor = null,
public bool $hasBreakfast = false, public bool $hasBreakfast = false,
public bool $hasLunch = false, public bool $hasLunch = false,
public bool $hasDinner = false,
public bool $hasOvernight = false, public bool $hasOvernight = false,
) {} ) {}
} }

View File

@@ -20,6 +20,7 @@ final class WeeklySummaryRow
public array $daily, public array $daily,
public int $weeklyDayMinutes, public int $weeklyDayMinutes,
public int $weeklyNightMinutes, public int $weeklyNightMinutes,
public int $weeklyWorkshopMinutes,
public int $weeklyTotalMinutes, public int $weeklyTotalMinutes,
public float $weeklyPresenceCount, public float $weeklyPresenceCount,
public int $weeklyOvertimeTotalMinutes, public int $weeklyOvertimeTotalMinutes,
@@ -29,6 +30,7 @@ final class WeeklySummaryRow
public bool $isDriver = false, public bool $isDriver = false,
public int $weeklyBreakfastCount = 0, public int $weeklyBreakfastCount = 0,
public int $weeklyLunchCount = 0, public int $weeklyLunchCount = 0,
public int $weeklyDinnerCount = 0,
public int $weeklyOvernightCount = 0, public int $weeklyOvernightCount = 0,
) {} ) {}
} }

View File

@@ -107,6 +107,10 @@ class WorkHour
#[Groups(['work_hour:read'])] #[Groups(['work_hour:read'])]
private ?int $nightHoursMinutes = null; private ?int $nightHoursMinutes = null;
#[ORM\Column(type: 'integer', nullable: true)]
#[Groups(['work_hour:read'])]
private ?int $workshopHoursMinutes = null;
#[ORM\Column(type: 'boolean', options: ['default' => false])] #[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['work_hour:read'])] #[Groups(['work_hour:read'])]
private bool $hasBreakfast = false; private bool $hasBreakfast = false;
@@ -115,6 +119,10 @@ class WorkHour
#[Groups(['work_hour:read'])] #[Groups(['work_hour:read'])]
private bool $hasLunch = false; private bool $hasLunch = false;
#[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['work_hour:read'])]
private bool $hasDinner = false;
#[ORM\Column(type: 'boolean', options: ['default' => false])] #[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['work_hour:read'])] #[Groups(['work_hour:read'])]
private bool $hasOvernight = false; private bool $hasOvernight = false;
@@ -256,6 +264,18 @@ class WorkHour
return $this; return $this;
} }
public function getWorkshopHoursMinutes(): ?int
{
return $this->workshopHoursMinutes;
}
public function setWorkshopHoursMinutes(?int $workshopHoursMinutes): self
{
$this->workshopHoursMinutes = $workshopHoursMinutes;
return $this;
}
public function getHasBreakfast(): bool public function getHasBreakfast(): bool
{ {
return $this->hasBreakfast; return $this->hasBreakfast;
@@ -280,6 +300,18 @@ class WorkHour
return $this; return $this;
} }
public function getHasDinner(): bool
{
return $this->hasDinner;
}
public function setHasDinner(bool $hasDinner): self
{
$this->hasDinner = $hasDinner;
return $this;
}
public function getHasOvernight(): bool public function getHasOvernight(): bool
{ {
return $this->hasOvernight; return $this->hasOvernight;

View File

@@ -229,8 +229,10 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
* isPresentAfternoon:bool, * isPresentAfternoon:bool,
* dayHoursMinutes:?int, * dayHoursMinutes:?int,
* nightHoursMinutes:?int, * nightHoursMinutes:?int,
* workshopHoursMinutes:?int,
* hasBreakfast:bool, * hasBreakfast:bool,
* hasLunch:bool, * hasLunch:bool,
* hasDinner:bool,
* hasOvernight:bool * hasOvernight:bool
* } * }
*/ */
@@ -238,37 +240,41 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
{ {
if ($isDriver) { if ($isDriver) {
return [ return [
'morningFrom' => null, 'morningFrom' => null,
'morningTo' => null, 'morningTo' => null,
'afternoonFrom' => null, 'afternoonFrom' => null,
'afternoonTo' => null, 'afternoonTo' => null,
'eveningFrom' => null, 'eveningFrom' => null,
'eveningTo' => null, 'eveningTo' => null,
'isPresentMorning' => false, 'isPresentMorning' => false,
'isPresentAfternoon' => false, 'isPresentAfternoon' => false,
'dayHoursMinutes' => $this->normalizeMinutes($entry['dayHoursMinutes'] ?? null, $employeeId, 'dayHoursMinutes'), 'dayHoursMinutes' => $this->normalizeMinutes($entry['dayHoursMinutes'] ?? null, $employeeId, 'dayHoursMinutes'),
'nightHoursMinutes' => $this->normalizeMinutes($entry['nightHoursMinutes'] ?? null, $employeeId, 'nightHoursMinutes'), 'nightHoursMinutes' => $this->normalizeMinutes($entry['nightHoursMinutes'] ?? null, $employeeId, 'nightHoursMinutes'),
'hasBreakfast' => $this->normalizePresence($entry['hasBreakfast'] ?? false, $employeeId, 'hasBreakfast'), 'workshopHoursMinutes' => $this->normalizeMinutes($entry['workshopHoursMinutes'] ?? null, $employeeId, 'workshopHoursMinutes'),
'hasLunch' => $this->normalizePresence($entry['hasLunch'] ?? false, $employeeId, 'hasLunch'), 'hasBreakfast' => $this->normalizePresence($entry['hasBreakfast'] ?? false, $employeeId, 'hasBreakfast'),
'hasOvernight' => $this->normalizePresence($entry['hasOvernight'] ?? false, $employeeId, 'hasOvernight'), 'hasLunch' => $this->normalizePresence($entry['hasLunch'] ?? false, $employeeId, 'hasLunch'),
'hasDinner' => $this->normalizePresence($entry['hasDinner'] ?? false, $employeeId, 'hasDinner'),
'hasOvernight' => $this->normalizePresence($entry['hasOvernight'] ?? false, $employeeId, 'hasOvernight'),
]; ];
} }
if ($isPresenceTracking) { if ($isPresenceTracking) {
return [ return [
'morningFrom' => null, 'morningFrom' => null,
'morningTo' => null, 'morningTo' => null,
'afternoonFrom' => null, 'afternoonFrom' => null,
'afternoonTo' => null, 'afternoonTo' => null,
'eveningFrom' => null, 'eveningFrom' => null,
'eveningTo' => null, 'eveningTo' => null,
'isPresentMorning' => $this->normalizePresence($entry['isPresentMorning'] ?? false, $employeeId, 'isPresentMorning'), 'isPresentMorning' => $this->normalizePresence($entry['isPresentMorning'] ?? false, $employeeId, 'isPresentMorning'),
'isPresentAfternoon' => $this->normalizePresence($entry['isPresentAfternoon'] ?? false, $employeeId, 'isPresentAfternoon'), 'isPresentAfternoon' => $this->normalizePresence($entry['isPresentAfternoon'] ?? false, $employeeId, 'isPresentAfternoon'),
'dayHoursMinutes' => null, 'dayHoursMinutes' => null,
'nightHoursMinutes' => null, 'nightHoursMinutes' => null,
'hasBreakfast' => false, 'workshopHoursMinutes' => null,
'hasLunch' => false, 'hasBreakfast' => false,
'hasOvernight' => false, 'hasLunch' => false,
'hasDinner' => false,
'hasOvernight' => false,
]; ];
} }
@@ -281,13 +287,15 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
'eveningTo' => $this->normalizeTime($entry['eveningTo'] ?? null, $employeeId, 'eveningTo'), 'eveningTo' => $this->normalizeTime($entry['eveningTo'] ?? null, $employeeId, 'eveningTo'),
// On conserve aussi la présence si envoyée (cas forfait affiché côté UI), // On conserve aussi la présence si envoyée (cas forfait affiché côté UI),
// même si le contrat résolu ce jour est en suivi horaire. // même si le contrat résolu ce jour est en suivi horaire.
'isPresentMorning' => $this->normalizePresence($entry['isPresentMorning'] ?? false, $employeeId, 'isPresentMorning'), 'isPresentMorning' => $this->normalizePresence($entry['isPresentMorning'] ?? false, $employeeId, 'isPresentMorning'),
'isPresentAfternoon' => $this->normalizePresence($entry['isPresentAfternoon'] ?? false, $employeeId, 'isPresentAfternoon'), 'isPresentAfternoon' => $this->normalizePresence($entry['isPresentAfternoon'] ?? false, $employeeId, 'isPresentAfternoon'),
'dayHoursMinutes' => null, 'dayHoursMinutes' => null,
'nightHoursMinutes' => null, 'nightHoursMinutes' => null,
'hasBreakfast' => false, 'workshopHoursMinutes' => null,
'hasLunch' => false, 'hasBreakfast' => false,
'hasOvernight' => false, 'hasLunch' => false,
'hasDinner' => false,
'hasOvernight' => false,
]; ];
} }
@@ -368,8 +376,10 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
* isPresentAfternoon:bool, * isPresentAfternoon:bool,
* dayHoursMinutes:?int, * dayHoursMinutes:?int,
* nightHoursMinutes:?int, * nightHoursMinutes:?int,
* workshopHoursMinutes:?int,
* hasBreakfast:bool, * hasBreakfast:bool,
* hasLunch:bool, * hasLunch:bool,
* hasDinner:bool,
* hasOvernight:bool * hasOvernight:bool
* } $entry * } $entry
*/ */
@@ -385,8 +395,10 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
&& false === $entry['isPresentAfternoon'] && false === $entry['isPresentAfternoon']
&& (null === $entry['dayHoursMinutes'] || 0 === $entry['dayHoursMinutes']) && (null === $entry['dayHoursMinutes'] || 0 === $entry['dayHoursMinutes'])
&& (null === $entry['nightHoursMinutes'] || 0 === $entry['nightHoursMinutes']) && (null === $entry['nightHoursMinutes'] || 0 === $entry['nightHoursMinutes'])
&& (null === $entry['workshopHoursMinutes'] || 0 === $entry['workshopHoursMinutes'])
&& false === $entry['hasBreakfast'] && false === $entry['hasBreakfast']
&& false === $entry['hasLunch'] && false === $entry['hasLunch']
&& false === $entry['hasDinner']
&& false === $entry['hasOvernight']; && false === $entry['hasOvernight'];
} }
@@ -402,8 +414,10 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
* isPresentAfternoon:bool, * isPresentAfternoon:bool,
* dayHoursMinutes:?int, * dayHoursMinutes:?int,
* nightHoursMinutes:?int, * nightHoursMinutes:?int,
* workshopHoursMinutes:?int,
* hasBreakfast:bool, * hasBreakfast:bool,
* hasLunch:bool, * hasLunch:bool,
* hasDinner:bool,
* hasOvernight:bool * hasOvernight:bool
* } $entry * } $entry
*/ */
@@ -420,8 +434,10 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
->setIsPresentAfternoon($entry['isPresentAfternoon']) ->setIsPresentAfternoon($entry['isPresentAfternoon'])
->setDayHoursMinutes($entry['dayHoursMinutes']) ->setDayHoursMinutes($entry['dayHoursMinutes'])
->setNightHoursMinutes($entry['nightHoursMinutes']) ->setNightHoursMinutes($entry['nightHoursMinutes'])
->setWorkshopHoursMinutes($entry['workshopHoursMinutes'])
->setHasBreakfast($entry['hasBreakfast']) ->setHasBreakfast($entry['hasBreakfast'])
->setHasLunch($entry['hasLunch']) ->setHasLunch($entry['hasLunch'])
->setHasDinner($entry['hasDinner'])
->setHasOvernight($entry['hasOvernight']) ->setHasOvernight($entry['hasOvernight'])
// Toute modification invalide la validation chef de site. // Toute modification invalide la validation chef de site.
->setIsSiteValid(false) ->setIsSiteValid(false)
@@ -442,8 +458,10 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
* isPresentAfternoon:bool, * isPresentAfternoon:bool,
* dayHoursMinutes:?int, * dayHoursMinutes:?int,
* nightHoursMinutes:?int, * nightHoursMinutes:?int,
* workshopHoursMinutes:?int,
* hasBreakfast:bool, * hasBreakfast:bool,
* hasLunch:bool, * hasLunch:bool,
* hasDinner:bool,
* hasOvernight:bool * hasOvernight:bool
* } $entry * } $entry
*/ */
@@ -459,8 +477,10 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
&& $workHour->getIsPresentAfternoon() === $entry['isPresentAfternoon'] && $workHour->getIsPresentAfternoon() === $entry['isPresentAfternoon']
&& $workHour->getDayHoursMinutes() === $entry['dayHoursMinutes'] && $workHour->getDayHoursMinutes() === $entry['dayHoursMinutes']
&& $workHour->getNightHoursMinutes() === $entry['nightHoursMinutes'] && $workHour->getNightHoursMinutes() === $entry['nightHoursMinutes']
&& $workHour->getWorkshopHoursMinutes() === $entry['workshopHoursMinutes']
&& $workHour->getHasBreakfast() === $entry['hasBreakfast'] && $workHour->getHasBreakfast() === $entry['hasBreakfast']
&& $workHour->getHasLunch() === $entry['hasLunch'] && $workHour->getHasLunch() === $entry['hasLunch']
&& $workHour->getHasDinner() === $entry['hasDinner']
&& $workHour->getHasOvernight() === $entry['hasOvernight']; && $workHour->getHasOvernight() === $entry['hasOvernight'];
} }
} }

View File

@@ -127,14 +127,16 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
// Pré-calcul des métriques par salarié/date pour simplifier l'agrégation finale. // Pré-calcul des métriques par salarié/date pour simplifier l'agrégation finale.
$dateKey = $workHour->getWorkDate()->format('Y-m-d'); $dateKey = $workHour->getWorkDate()->format('Y-m-d');
$metricsByEmployeeDate[$employeeId][$dateKey] = [ $metricsByEmployeeDate[$employeeId][$dateKey] = [
'metrics' => $this->computeMetrics($workHour), 'metrics' => $this->computeMetrics($workHour),
'isPresentMorning' => $workHour->getIsPresentMorning(), 'isPresentMorning' => $workHour->getIsPresentMorning(),
'isPresentAfternoon' => $workHour->getIsPresentAfternoon(), 'isPresentAfternoon' => $workHour->getIsPresentAfternoon(),
'dayHoursMinutes' => $workHour->getDayHoursMinutes(), 'dayHoursMinutes' => $workHour->getDayHoursMinutes(),
'nightHoursMinutes' => $workHour->getNightHoursMinutes(), 'nightHoursMinutes' => $workHour->getNightHoursMinutes(),
'hasBreakfast' => $workHour->getHasBreakfast(), 'workshopHoursMinutes' => $workHour->getWorkshopHoursMinutes(),
'hasLunch' => $workHour->getHasLunch(), 'hasBreakfast' => $workHour->getHasBreakfast(),
'hasOvernight' => $workHour->getHasOvernight(), 'hasLunch' => $workHour->getHasLunch(),
'hasDinner' => $workHour->getHasDinner(),
'hasOvernight' => $workHour->getHasOvernight(),
]; ];
} }
@@ -185,14 +187,16 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
continue; continue;
} }
$weeklyDayMinutes = 0; $weeklyDayMinutes = 0;
$weeklyNightMinutes = 0; $weeklyNightMinutes = 0;
$weeklyTotalMinutes = 0; $weeklyWorkshopMinutes = 0;
$weeklyPresenceCount = 0.0; $weeklyTotalMinutes = 0;
$weeklyBreakfastCount = 0; $weeklyPresenceCount = 0.0;
$weeklyLunchCount = 0; $weeklyBreakfastCount = 0;
$weeklyOvernightCount = 0; $weeklyLunchCount = 0;
$daily = []; $weeklyDinnerCount = 0;
$weeklyOvernightCount = 0;
$daily = [];
// Les contrats au suivi "présence" ne manipulent pas les heures, mais des demi-journées. // Les contrats au suivi "présence" ne manipulent pas les heures, mais des demi-journées.
$weekAnchorContract = $contractsByEmployeeDate[$employeeId][$anchorDateYmd] $weekAnchorContract = $contractsByEmployeeDate[$employeeId][$anchorDateYmd]
?? $contractsByEmployeeDate[$employeeId][$days[0]] ?? $contractsByEmployeeDate[$employeeId][$days[0]]
@@ -217,21 +221,27 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$hasBreakfast = false; $hasBreakfast = false;
$hasLunch = false; $hasLunch = false;
$hasDinner = false;
$hasOvernight = false; $hasOvernight = false;
if ($isDateDriver) { if ($isDateDriver) {
$dayMinutes = ($entry['dayHoursMinutes'] ?? 0); $dayMinutes = ($entry['dayHoursMinutes'] ?? 0);
$nightMinutes = ($entry['nightHoursMinutes'] ?? 0); $nightMinutes = ($entry['nightHoursMinutes'] ?? 0);
$totalMinutes = $dayMinutes + $nightMinutes; $workshopMinutes = ($entry['workshopHoursMinutes'] ?? 0);
$hasBreakfast = $entry['hasBreakfast'] ?? false; $totalMinutes = $dayMinutes + $nightMinutes + $workshopMinutes;
$hasLunch = $entry['hasLunch'] ?? false; $hasBreakfast = $entry['hasBreakfast'] ?? false;
$hasOvernight = $entry['hasOvernight'] ?? false; $hasLunch = $entry['hasLunch'] ?? false;
$hasDinner = $entry['hasDinner'] ?? false;
$hasOvernight = $entry['hasOvernight'] ?? false;
if ($hasBreakfast) { if ($hasBreakfast) {
++$weeklyBreakfastCount; ++$weeklyBreakfastCount;
} }
if ($hasLunch) { if ($hasLunch) {
++$weeklyLunchCount; ++$weeklyLunchCount;
} }
if ($hasDinner) {
++$weeklyDinnerCount;
}
if ($hasOvernight) { if ($hasOvernight) {
++$weeklyOvernightCount; ++$weeklyOvernightCount;
} }
@@ -239,9 +249,10 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$metrics = $entry['metrics'] ?? new WorkMetrics(); $metrics = $entry['metrics'] ?? new WorkMetrics();
// Les absences "comptées comme travaillées" alimentent le total du jour. // Les absences "comptées comme travaillées" alimentent le total du jour.
$metrics->addCreditedMinutes($creditedMinutes); $metrics->addCreditedMinutes($creditedMinutes);
$dayMinutes = $metrics->dayMinutes; $dayMinutes = $metrics->dayMinutes;
$nightMinutes = $metrics->nightMinutes; $nightMinutes = $metrics->nightMinutes;
$totalMinutes = $metrics->totalMinutes; $workshopMinutes = 0;
$totalMinutes = $metrics->totalMinutes;
} }
$present = null; $present = null;
@@ -256,6 +267,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
$weeklyDayMinutes += $dayMinutes; $weeklyDayMinutes += $dayMinutes;
$weeklyNightMinutes += $nightMinutes; $weeklyNightMinutes += $nightMinutes;
$weeklyWorkshopMinutes += $workshopMinutes;
$weeklyTotalMinutes += $totalMinutes; $weeklyTotalMinutes += $totalMinutes;
if (null !== $present) { if (null !== $present) {
$weeklyPresenceCount += $present; $weeklyPresenceCount += $present;
@@ -265,6 +277,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
date: $date, date: $date,
dayMinutes: $dayMinutes, dayMinutes: $dayMinutes,
nightMinutes: $nightMinutes, nightMinutes: $nightMinutes,
workshopMinutes: $workshopMinutes,
totalMinutes: $totalMinutes, totalMinutes: $totalMinutes,
present: $present, present: $present,
hasAbsence: $absenceByEmployeeDate[$employeeId][$date] ?? false, hasAbsence: $absenceByEmployeeDate[$employeeId][$date] ?? false,
@@ -272,6 +285,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
absenceColor: $absenceColorByEmployeeDate[$employeeId][$date] ?? null, absenceColor: $absenceColorByEmployeeDate[$employeeId][$date] ?? null,
hasBreakfast: $hasBreakfast, hasBreakfast: $hasBreakfast,
hasLunch: $hasLunch, hasLunch: $hasLunch,
hasDinner: $hasDinner,
hasOvernight: $hasOvernight, hasOvernight: $hasOvernight,
); );
} }
@@ -304,6 +318,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
daily: $daily, daily: $daily,
weeklyDayMinutes: $weeklyDayMinutes, weeklyDayMinutes: $weeklyDayMinutes,
weeklyNightMinutes: $weeklyNightMinutes, weeklyNightMinutes: $weeklyNightMinutes,
weeklyWorkshopMinutes: $weeklyWorkshopMinutes,
weeklyTotalMinutes: $weeklyTotalMinutes, weeklyTotalMinutes: $weeklyTotalMinutes,
weeklyPresenceCount: $weeklyPresenceCount, weeklyPresenceCount: $weeklyPresenceCount,
weeklyOvertimeTotalMinutes: $weeklyOvertimeTotalMinutes, weeklyOvertimeTotalMinutes: $weeklyOvertimeTotalMinutes,
@@ -313,6 +328,7 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
isDriver: $isDriver, isDriver: $isDriver,
weeklyBreakfastCount: $weeklyBreakfastCount, weeklyBreakfastCount: $weeklyBreakfastCount,
weeklyLunchCount: $weeklyLunchCount, weeklyLunchCount: $weeklyLunchCount,
weeklyDinnerCount: $weeklyDinnerCount,
weeklyOvernightCount: $weeklyOvernightCount, weeklyOvernightCount: $weeklyOvernightCount,
); );
} }