feat : ajout de log pour les cron
This commit is contained in:
@@ -1,14 +1,19 @@
|
|||||||
monolog:
|
monolog:
|
||||||
channels: [deprecation]
|
channels: [deprecation, cron]
|
||||||
|
|
||||||
when@dev:
|
when@dev:
|
||||||
monolog:
|
monolog:
|
||||||
handlers:
|
handlers:
|
||||||
|
cron:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/cron.log"
|
||||||
|
level: info
|
||||||
|
channels: [cron]
|
||||||
main:
|
main:
|
||||||
type: stream
|
type: stream
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
level: debug
|
level: debug
|
||||||
channels: ["!event"]
|
channels: ["!event", "!cron"]
|
||||||
console:
|
console:
|
||||||
type: console
|
type: console
|
||||||
process_psr_3_messages: false
|
process_psr_3_messages: false
|
||||||
@@ -17,11 +22,16 @@ when@dev:
|
|||||||
when@prod:
|
when@prod:
|
||||||
monolog:
|
monolog:
|
||||||
handlers:
|
handlers:
|
||||||
|
cron:
|
||||||
|
type: stream
|
||||||
|
path: "%kernel.logs_dir%/cron.log"
|
||||||
|
level: info
|
||||||
|
channels: [cron]
|
||||||
main:
|
main:
|
||||||
type: stream
|
type: stream
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
level: debug
|
level: debug
|
||||||
channels: ["!deprecation"]
|
channels: ["!deprecation", "!cron"]
|
||||||
deprecation:
|
deprecation:
|
||||||
type: stream
|
type: stream
|
||||||
channels: [deprecation]
|
channels: [deprecation]
|
||||||
|
|||||||
@@ -168,11 +168,11 @@ employee_id;rule_code;year;opening_days;opening_saturdays;source_date;comment
|
|||||||
Exemple cron (tous les jours a 02:10):
|
Exemple cron (tous les jours a 02:10):
|
||||||
Dev
|
Dev
|
||||||
```cron
|
```cron
|
||||||
10 2 * * * cd /var/www/html && php bin/console app:leave:rollover --no-interaction >> var/log/leave-rollover.log 2>&1
|
10 2 * * * cd /var/www/html && php bin/console app:leave:rollover --no-interaction 2>&1
|
||||||
```
|
```
|
||||||
Prod
|
Prod
|
||||||
```cron
|
```cron
|
||||||
10 2 * * * cd /var/www/sirh && php bin/console app:leave:rollover --no-interaction >> var/log/leave-rollover.log 2>&1
|
10 2 * * * cd /var/www/sirh && php bin/console app:leave:rollover --no-interaction 2>&1
|
||||||
```
|
```
|
||||||
Explication de la ligne cron:
|
Explication de la ligne cron:
|
||||||
- `10 2 * * *`: planification
|
- `10 2 * * *`: planification
|
||||||
|
|||||||
@@ -133,11 +133,11 @@ Conversion rapide: `1260 minutes = 21h00 = 3.00 jours` (1 jour = 420 min = 7h)
|
|||||||
Exemple cron (tous les jours a 02:15, juste apres le rollover conges):
|
Exemple cron (tous les jours a 02:15, juste apres le rollover conges):
|
||||||
Dev
|
Dev
|
||||||
```cron
|
```cron
|
||||||
15 2 * * * cd /var/www/html && php bin/console app:rtt:rollover --no-interaction >> var/log/rtt-rollover.log 2>&1
|
15 2 * * * cd /var/www/html && php bin/console app:rtt:rollover --no-interaction 2>&1
|
||||||
```
|
```
|
||||||
Prod
|
Prod
|
||||||
```cron
|
```cron
|
||||||
15 2 * * * cd /var/www/sirh && php bin/console app:rtt:rollover --no-interaction >> var/log/rtt-rollover.log 2>&1
|
10 2 * * * cd /var/www/sirh && php bin/console app:rtt:rollover --no-interaction 2>&1
|
||||||
```
|
```
|
||||||
Explication de la ligne cron:
|
Explication de la ligne cron:
|
||||||
- `15 2 * * *`: tous les jours a 02:15
|
- `15 2 * * *`: tous les jours a 02:15
|
||||||
|
|||||||
@@ -1,258 +1,267 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-lg border border-neutral-200 bg-white overflow-hidden flex min-h-0 flex-col">
|
<div class="rounded-lg border border-neutral-200 bg-white overflow-hidden flex min-h-0 flex-col">
|
||||||
<div class="overflow-y-auto min-h-0">
|
<div class="overflow-y-auto min-h-0">
|
||||||
<div
|
<div
|
||||||
class="grid w-full min-w-0 gap-1 border-b border-neutral-200 bg-tertiary-500 px-4 py-3 text-sm font-semibold text-neutral-700 sticky top-0 z-10"
|
class="grid w-full min-w-0 gap-1 border-b border-neutral-200 bg-tertiary-500 px-4 py-3 text-sm font-semibold text-neutral-700 sticky top-0 z-10"
|
||||||
:style="{ gridTemplateColumns: dayGridCols }"
|
:style="{ gridTemplateColumns: dayGridCols }"
|
||||||
>
|
>
|
||||||
<span>Nom</span>
|
<span>Nom</span>
|
||||||
<span class="pl-2">Absence</span>
|
<span class="pl-2">Absence</span>
|
||||||
<span class="pl-4">Début matin</span>
|
<span class="pl-4">Début matin</span>
|
||||||
<span class="pr-2">Fin matin</span>
|
<span class="pr-2">Fin matin</span>
|
||||||
<span class="pl-2">Début après-midi</span>
|
<span class="pl-2">Début après-midi</span>
|
||||||
<span class="pr-2">Fin après-midi</span>
|
<span class="pr-2">Fin après-midi</span>
|
||||||
<span class="pl-2">Début soir</span>
|
<span class="pl-2">Début soir</span>
|
||||||
<span class="pr-2">Fin soir</span>
|
<span class="pr-2">Fin soir</span>
|
||||||
<span class="pl-2">Jour</span>
|
<span class="pl-2">Jour</span>
|
||||||
<span>Nuit</span>
|
<span>Nuit</span>
|
||||||
<span>Total</span>
|
<span>Total</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>
|
||||||
<input
|
<input
|
||||||
ref="bulkValidationInput"
|
ref="bulkValidationInput"
|
||||||
:checked="isBulkValidationChecked"
|
:checked="isBulkValidationChecked"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4 cursor-pointer"
|
class="h-4 w-4 cursor-pointer"
|
||||||
@change="onBulkValidationChange"
|
@change="onBulkValidationChange"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="isSiteManager" class="inline-flex items-center gap-2">
|
<span v-else-if="isSiteManager" class="inline-flex items-center gap-2">
|
||||||
<span>Site</span>
|
<span>Site</span>
|
||||||
<input
|
<input
|
||||||
ref="bulkSiteValidationInput"
|
ref="bulkSiteValidationInput"
|
||||||
:checked="isBulkSiteValidationChecked"
|
:checked="isBulkSiteValidationChecked"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
:class="canBulkToggleSiteValidation ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'"
|
:class="canBulkToggleSiteValidation ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'"
|
||||||
:disabled="!canBulkToggleSiteValidation"
|
:disabled="!canBulkToggleSiteValidation"
|
||||||
@change="onBulkSiteValidationChange"
|
@change="onBulkSiteValidationChange"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>Site <Icon name="mdi:check-bold" class="ml-1"/></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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="employee in employees"
|
v-for="employee in employees"
|
||||||
:key="employee.id"
|
:key="employee.id"
|
||||||
class="grid w-full min-w-0 items-center gap-1 border-b border-neutral-100 px-4 py-2 text-sm last:border-b-0"
|
class="grid w-full min-w-0 items-center gap-1 border-b border-neutral-100 px-4 py-2 text-sm last:border-b-0"
|
||||||
:style="{ gridTemplateColumns: dayGridCols }"
|
:style="{ gridTemplateColumns: dayGridCols }"
|
||||||
>
|
|
||||||
<div class="text-neutral-900 min-w-0">
|
|
||||||
<p class="font-semibold truncate">
|
|
||||||
{{ employee.firstName }} {{ employee.lastName }}
|
|
||||||
<span class="font-normal text-neutral-600">({{ contractLabel(employee) }})</span>
|
|
||||||
</p>
|
|
||||||
<p class="text-neutral-500 truncate inline-flex items-center gap-2">
|
|
||||||
<span>{{ employee.site?.name ?? 'Sans site' }}</span>
|
|
||||||
<span
|
|
||||||
v-if="isAdmin && (rows[employee.id]?.isSiteValid ?? false)"
|
|
||||||
class="rounded-full bg-green-500 flex justify-center item-center text-white p-0.5"
|
|
||||||
title="Validation site"
|
|
||||||
>
|
>
|
||||||
|
<div class="text-neutral-900 min-w-0">
|
||||||
|
<p class="font-semibold truncate">
|
||||||
|
{{ employee.firstName }} {{ employee.lastName }}
|
||||||
|
<span class="font-normal text-neutral-600">({{ contractLabel(employee) }})</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-neutral-500 truncate inline-flex items-center gap-2">
|
||||||
|
<span>{{ employee.site?.name ?? 'Sans site' }}</span>
|
||||||
|
<span
|
||||||
|
v-if="isAdmin && (rows[employee.id]?.isSiteValid ?? false)"
|
||||||
|
class="rounded-full bg-green-500 flex justify-center item-center text-white p-0.5"
|
||||||
|
title="Validation site"
|
||||||
|
>
|
||||||
<Icon name="mdi:check"/>
|
<Icon name="mdi:check"/>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p v-if="isAdmin && getRowUpdatedAt(employee.id)" class="text-neutral-400 text-xs truncate">
|
<p v-if="isAdmin && getRowUpdatedAt(employee.id)" class="text-neutral-400 text-xs truncate">
|
||||||
Modifié le {{ getRowUpdatedAt(employee.id) }}
|
Modifié le {{ getRowUpdatedAt(employee.id) }}
|
||||||
</p>
|
</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>
|
||||||
|
<button
|
||||||
|
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)"
|
||||||
|
@click="onAbsenceClick(employee.id)"
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<TimeSelect
|
||||||
|
v-if="isTimeTracking(employee)"
|
||||||
|
v-model="rows[employee.id].morningFrom"
|
||||||
|
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'AM'))"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else-if="isPresenceTracking(employee)"
|
||||||
|
v-model="rows[employee.id].isPresentMorning"
|
||||||
|
type="checkbox"
|
||||||
|
class="cursor-pointer h-4 w-4"
|
||||||
|
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'AM'))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pr-2">
|
||||||
|
<TimeSelect
|
||||||
|
v-if="isTimeTracking(employee)"
|
||||||
|
v-model="rows[employee.id].morningTo"
|
||||||
|
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'AM'))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pl-2">
|
||||||
|
<TimeSelect
|
||||||
|
v-if="isTimeTracking(employee)"
|
||||||
|
v-model="rows[employee.id].afternoonFrom"
|
||||||
|
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'PM'))"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else-if="isPresenceTracking(employee)"
|
||||||
|
v-model="rows[employee.id].isPresentAfternoon"
|
||||||
|
type="checkbox"
|
||||||
|
class="cursor-pointer h-4 w-4"
|
||||||
|
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'PM'))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pr-2">
|
||||||
|
<TimeSelect
|
||||||
|
v-if="isTimeTracking(employee)"
|
||||||
|
v-model="rows[employee.id].afternoonTo"
|
||||||
|
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'PM'))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pl-2">
|
||||||
|
<TimeSelect
|
||||||
|
v-if="isTimeTracking(employee)"
|
||||||
|
v-model="rows[employee.id].eveningFrom"
|
||||||
|
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isEveningLockedByAbsence(employee.id))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pr-2">
|
||||||
|
<TimeSelect
|
||||||
|
v-if="isTimeTracking(employee)"
|
||||||
|
v-model="rows[employee.id].eveningTo"
|
||||||
|
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isEveningLockedByAbsence(employee.id))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pl-2 text-sm font-semibold text-neutral-700">
|
||||||
|
<div v-if="isTimeTracking(employee)">{{
|
||||||
|
formatMinutes(getRowMetrics(employee.id).dayMinutes)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-neutral-700">
|
||||||
|
<div v-if="isTimeTracking(employee)">{{
|
||||||
|
formatMinutes(getRowMetrics(employee.id).nightMinutes)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-neutral-700">
|
||||||
|
<div v-if="isTimeTracking(employee)">{{
|
||||||
|
formatMinutes(getRowMetrics(employee.id).totalMinutes)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div v-else>{{ getPresenceDayValue(employee.id) }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isAdmin" class="text-right">
|
||||||
|
<input
|
||||||
|
:checked="rows[employee.id]?.isValid ?? false"
|
||||||
|
type="checkbox"
|
||||||
|
class="h-4 w-4 cursor-pointer"
|
||||||
|
@change="onToggleValidation(employee.id, ($event.target as HTMLInputElement).checked)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-right p-5">
|
||||||
|
<input
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
<div v-if="!isAdmin">
|
||||||
|
<span v-if="rows[employee.id]?.isValid" class="text-xs font-semibold text-neutral-700">Validé</span>
|
||||||
|
<span v-else class="text-xs text-neutral-500">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
|
||||||
<button
|
|
||||||
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)"
|
|
||||||
@click="onAbsenceClick(employee.id)"
|
|
||||||
>
|
|
||||||
Modifier
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="pl-4">
|
|
||||||
<TimeSelect
|
|
||||||
v-if="isTimeTracking(employee)"
|
|
||||||
v-model="rows[employee.id].morningFrom"
|
|
||||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'AM'))"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
v-else-if="isPresenceTracking(employee)"
|
|
||||||
v-model="rows[employee.id].isPresentMorning"
|
|
||||||
type="checkbox"
|
|
||||||
class="cursor-pointer h-4 w-4"
|
|
||||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'AM'))"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="pr-2">
|
|
||||||
<TimeSelect
|
|
||||||
v-if="isTimeTracking(employee)"
|
|
||||||
v-model="rows[employee.id].morningTo"
|
|
||||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'AM'))"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="pl-2">
|
|
||||||
<TimeSelect
|
|
||||||
v-if="isTimeTracking(employee)"
|
|
||||||
v-model="rows[employee.id].afternoonFrom"
|
|
||||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'PM'))"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
v-else-if="isPresenceTracking(employee)"
|
|
||||||
v-model="rows[employee.id].isPresentAfternoon"
|
|
||||||
type="checkbox"
|
|
||||||
class="cursor-pointer h-4 w-4"
|
|
||||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'PM'))"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="pr-2">
|
|
||||||
<TimeSelect
|
|
||||||
v-if="isTimeTracking(employee)"
|
|
||||||
v-model="rows[employee.id].afternoonTo"
|
|
||||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'PM'))"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="pl-2">
|
|
||||||
<TimeSelect
|
|
||||||
v-if="isTimeTracking(employee)"
|
|
||||||
v-model="rows[employee.id].eveningFrom"
|
|
||||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isEveningLockedByAbsence(employee.id))"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="pr-2">
|
|
||||||
<TimeSelect
|
|
||||||
v-if="isTimeTracking(employee)"
|
|
||||||
v-model="rows[employee.id].eveningTo"
|
|
||||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isEveningLockedByAbsence(employee.id))"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="pl-2 text-sm font-semibold text-neutral-700">
|
|
||||||
<div v-if="isTimeTracking(employee)">{{ formatMinutes(getRowMetrics(employee.id).dayMinutes) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm font-semibold text-neutral-700">
|
|
||||||
<div v-if="isTimeTracking(employee)">{{ formatMinutes(getRowMetrics(employee.id).nightMinutes) }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm font-semibold text-neutral-700">
|
|
||||||
<div v-if="isTimeTracking(employee)">{{ formatMinutes(getRowMetrics(employee.id).totalMinutes) }}</div>
|
|
||||||
<div v-else>{{ getPresenceDayValue(employee.id) }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="isAdmin" class="text-right">
|
|
||||||
<input
|
|
||||||
:checked="rows[employee.id]?.isValid ?? false"
|
|
||||||
type="checkbox"
|
|
||||||
class="h-4 w-4 cursor-pointer"
|
|
||||||
@change="onToggleValidation(employee.id, ($event.target as HTMLInputElement).checked)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-right p-5">
|
|
||||||
<input
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
<div v-if="!isAdmin">
|
|
||||||
<span v-if="rows[employee.id]?.isValid" class="text-xs font-semibold text-neutral-700">Validé</span>
|
|
||||||
<span v-else class="text-xs text-neutral-500">-</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Employee } from '~/services/dto/employee'
|
import type {Employee} from '~/services/dto/employee'
|
||||||
import TimeSelect from '~/components/ui/TimeSelect.vue'
|
import TimeSelect from '~/components/ui/TimeSelect.vue'
|
||||||
import type { HourRow } from './types'
|
import type {HourRow} from './types'
|
||||||
|
|
||||||
const rows = defineModel<Record<number, HourRow>>('rows', { required: true })
|
const rows = defineModel<Record<number, HourRow>>('rows', {required: true})
|
||||||
const bulkValidationInput = ref<HTMLInputElement | null>(null)
|
const bulkValidationInput = ref<HTMLInputElement | null>(null)
|
||||||
const bulkSiteValidationInput = ref<HTMLInputElement | null>(null)
|
const bulkSiteValidationInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
employees: Employee[]
|
employees: Employee[]
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isSiteManager: boolean
|
isSiteManager: boolean
|
||||||
dayGridCols: string
|
dayGridCols: string
|
||||||
isHoliday: boolean
|
isHoliday: boolean
|
||||||
contractLabel: (employee: Employee) => string
|
contractLabel: (employee: Employee) => string
|
||||||
isTimeTracking: (employee: Employee) => boolean
|
isTimeTracking: (employee: Employee) => boolean
|
||||||
isPresenceTracking: (employee: Employee) => boolean
|
isPresenceTracking: (employee: Employee) => boolean
|
||||||
isRowLocked: (employeeId: number) => boolean
|
isRowLocked: (employeeId: number) => boolean
|
||||||
isHalfLockedByAbsence: (employeeId: number, half: 'AM' | 'PM') => boolean
|
isHalfLockedByAbsence: (employeeId: number, half: 'AM' | 'PM') => boolean
|
||||||
isEveningLockedByAbsence: (employeeId: number) => boolean
|
isEveningLockedByAbsence: (employeeId: number) => boolean
|
||||||
hasContractAtSelectedDate: (employeeId: number) => boolean
|
hasContractAtSelectedDate: (employeeId: number) => boolean
|
||||||
isValidationPending: (employeeId: number) => boolean
|
isValidationPending: (employeeId: number) => boolean
|
||||||
isSiteValidationPending: (employeeId: number) => boolean
|
isSiteValidationPending: (employeeId: number) => boolean
|
||||||
canToggleValidation: (employeeId: number) => boolean
|
canToggleValidation: (employeeId: number) => boolean
|
||||||
canToggleSiteValidation: (employeeId: number) => boolean
|
canToggleSiteValidation: (employeeId: number) => boolean
|
||||||
canCreateSiteValidationRowFromAbsence: (employeeId: number) => boolean
|
canCreateSiteValidationRowFromAbsence: (employeeId: number) => boolean
|
||||||
isBulkValidationChecked: boolean
|
isBulkValidationChecked: boolean
|
||||||
isBulkValidationIndeterminate: boolean
|
isBulkValidationIndeterminate: boolean
|
||||||
isBulkSiteValidationChecked: boolean
|
isBulkSiteValidationChecked: boolean
|
||||||
isBulkSiteValidationIndeterminate: boolean
|
isBulkSiteValidationIndeterminate: boolean
|
||||||
canBulkToggleSiteValidation: boolean
|
canBulkToggleSiteValidation: boolean
|
||||||
onToggleValidation: (employeeId: number, checked: boolean) => void
|
onToggleValidation: (employeeId: number, checked: boolean) => void
|
||||||
onToggleSiteValidation: (employeeId: number, checked: boolean) => void
|
onToggleSiteValidation: (employeeId: number, checked: boolean) => void
|
||||||
onToggleValidationBulk: (checked: boolean) => Promise<void> | void
|
onToggleValidationBulk: (checked: boolean) => Promise<void> | void
|
||||||
onToggleSiteValidationBulk: (checked: boolean) => Promise<void> | void
|
onToggleSiteValidationBulk: (checked: boolean) => Promise<void> | void
|
||||||
getRowMetrics: (employeeId: number) => { dayMinutes: number; nightMinutes: number; totalMinutes: number }
|
getRowMetrics: (employeeId: number) => { dayMinutes: number; nightMinutes: number; totalMinutes: number }
|
||||||
getRowAbsenceLabel: (employeeId: number) => string
|
getRowAbsenceLabel: (employeeId: number) => string
|
||||||
getRowAbsenceStyle: (employeeId: number) => { backgroundColor: string } | undefined
|
getRowAbsenceStyle: (employeeId: number) => { backgroundColor: string } | undefined
|
||||||
getRowUpdatedAt: (employeeId: number) => string
|
getRowUpdatedAt: (employeeId: number) => string
|
||||||
getPresenceDayValue: (employeeId: number) => string
|
getPresenceDayValue: (employeeId: number) => string
|
||||||
onAbsenceClick: (employeeId: number) => void
|
onAbsenceClick: (employeeId: number) => void
|
||||||
formatMinutes: (minutes: number) => string
|
formatMinutes: (minutes: number) => string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const onBulkValidationChange = (event: Event) => {
|
const onBulkValidationChange = (event: Event) => {
|
||||||
props.onToggleValidationBulk((event.target as HTMLInputElement).checked)
|
props.onToggleValidationBulk((event.target as HTMLInputElement).checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onBulkSiteValidationChange = (event: Event) => {
|
const onBulkSiteValidationChange = (event: Event) => {
|
||||||
props.onToggleSiteValidationBulk((event.target as HTMLInputElement).checked)
|
props.onToggleSiteValidationBulk((event.target as HTMLInputElement).checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onToggleSiteValidation = (employeeId: number, checked: boolean) => {
|
const onToggleSiteValidation = (employeeId: number, checked: boolean) => {
|
||||||
props.onToggleSiteValidation(employeeId, checked)
|
props.onToggleSiteValidation(employeeId, checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.isBulkValidationIndeterminate,
|
() => props.isBulkValidationIndeterminate,
|
||||||
(isIndeterminate) => {
|
(isIndeterminate) => {
|
||||||
if (!bulkValidationInput.value) return
|
if (!bulkValidationInput.value) return
|
||||||
bulkValidationInput.value.indeterminate = isIndeterminate
|
bulkValidationInput.value.indeterminate = isIndeterminate
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{immediate: true}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.isBulkSiteValidationIndeterminate,
|
() => props.isBulkSiteValidationIndeterminate,
|
||||||
(isIndeterminate) => {
|
(isIndeterminate) => {
|
||||||
if (!bulkSiteValidationInput.value) return
|
if (!bulkSiteValidationInput.value) return
|
||||||
bulkSiteValidationInput.value.indeterminate = isIndeterminate
|
bulkSiteValidationInput.value.indeterminate = isIndeterminate
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{immediate: true}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ use App\Repository\EmployeeRepository;
|
|||||||
use App\Service\Leave\LeaveBalanceComputationService;
|
use App\Service\Leave\LeaveBalanceComputationService;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
#[AsCommand(
|
#[AsCommand(
|
||||||
name: 'app:leave:rollover',
|
name: 'app:leave:rollover',
|
||||||
@@ -31,6 +34,8 @@ final class LeaveRolloverCommand extends Command
|
|||||||
private readonly EmployeeLeaveBalanceRepository $leaveBalanceRepository,
|
private readonly EmployeeLeaveBalanceRepository $leaveBalanceRepository,
|
||||||
private readonly LeaveBalanceComputationService $leaveBalanceComputationService,
|
private readonly LeaveBalanceComputationService $leaveBalanceComputationService,
|
||||||
private readonly EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
#[Autowire(service: 'monolog.logger.cron')]
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@@ -51,8 +56,12 @@ final class LeaveRolloverCommand extends Command
|
|||||||
$today = new DateTimeImmutable('today');
|
$today = new DateTimeImmutable('today');
|
||||||
$force = (bool) $input->getOption('force');
|
$force = (bool) $input->getOption('force');
|
||||||
|
|
||||||
|
$this->logger->info('app:leave:rollover started.', ['date' => $today->format('Y-m-d'), 'force' => $force]);
|
||||||
|
|
||||||
if (!$force && !$this->isBusinessRolloverDate($today)) {
|
if (!$force && !$this->isBusinessRolloverDate($today)) {
|
||||||
$io->success('No rollover today: business date is neither 01/01 nor 01/06.');
|
$message = 'No rollover today: business date is neither 01/01 nor 01/06.';
|
||||||
|
$this->logger->info($message, ['date' => $today->format('Y-m-d')]);
|
||||||
|
$io->success($message);
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -67,6 +76,7 @@ final class LeaveRolloverCommand extends Command
|
|||||||
|
|
||||||
$ruleCode = $this->resolveRuleCode($employee);
|
$ruleCode = $this->resolveRuleCode($employee);
|
||||||
if (null === $ruleCode) {
|
if (null === $ruleCode) {
|
||||||
|
$this->logger->info('Employee skipped: no eligible rule.', ['employeeId' => $employee->getId()]);
|
||||||
++$skipped;
|
++$skipped;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -80,13 +90,22 @@ final class LeaveRolloverCommand extends Command
|
|||||||
$targetYear = $this->resolveTargetYear($ruleCode, $today);
|
$targetYear = $this->resolveTargetYear($ruleCode, $today);
|
||||||
$existing = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $targetYear);
|
$existing = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $targetYear);
|
||||||
if (null !== $existing) {
|
if (null !== $existing) {
|
||||||
|
$this->logger->info('Employee skipped: balance already exists.', ['employeeId' => $employee->getId(), 'year' => $targetYear, 'rule' => $ruleCode->value]);
|
||||||
++$skipped;
|
++$skipped;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
[$carryDays, $carrySaturdays] = $this->resolveCarry($employee, $ruleCode, $targetYear);
|
try {
|
||||||
$balance = new EmployeeLeaveBalance()
|
[$carryDays, $carrySaturdays] = $this->resolveCarry($employee, $ruleCode, $targetYear);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->logger->error('Error computing carry for employee.', ['employeeId' => $employee->getId(), 'error' => $e->getMessage()]);
|
||||||
|
++$skipped;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$balance = new EmployeeLeaveBalance()
|
||||||
->setEmployee($employee)
|
->setEmployee($employee)
|
||||||
->setRuleCode($ruleCode)
|
->setRuleCode($ruleCode)
|
||||||
->setYear($targetYear)
|
->setYear($targetYear)
|
||||||
@@ -102,16 +121,22 @@ final class LeaveRolloverCommand extends Command
|
|||||||
;
|
;
|
||||||
|
|
||||||
$this->entityManager->persist($balance);
|
$this->entityManager->persist($balance);
|
||||||
|
$this->logger->info('Balance created.', ['employeeId' => $employee->getId(), 'year' => $targetYear, 'rule' => $ruleCode->value, 'carryDays' => $carryDays, 'carrySaturdays' => $carrySaturdays]);
|
||||||
++$created;
|
++$created;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->entityManager->flush();
|
try {
|
||||||
|
$this->entityManager->flush();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->logger->error('Error flushing leave balances.', ['error' => $e->getMessage()]);
|
||||||
|
$io->error('Leave rollover failed: '.$e->getMessage());
|
||||||
|
|
||||||
$io->success(sprintf(
|
return Command::FAILURE;
|
||||||
'Leave rollover done: %d created, %d skipped.',
|
}
|
||||||
$created,
|
|
||||||
$skipped
|
$message = sprintf('Leave rollover done: %d created, %d skipped.', $created, $skipped);
|
||||||
));
|
$this->logger->info($message);
|
||||||
|
$io->success($message);
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ use App\Repository\EmployeeRttBalanceRepository;
|
|||||||
use App\Service\Rtt\RttRecoveryComputationService;
|
use App\Service\Rtt\RttRecoveryComputationService;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
#[AsCommand(
|
#[AsCommand(
|
||||||
name: 'app:rtt:rollover',
|
name: 'app:rtt:rollover',
|
||||||
@@ -31,6 +34,8 @@ final class RttRolloverCommand extends Command
|
|||||||
private readonly EmployeeRttBalanceRepository $rttBalanceRepository,
|
private readonly EmployeeRttBalanceRepository $rttBalanceRepository,
|
||||||
private readonly RttRecoveryComputationService $rttRecoveryService,
|
private readonly RttRecoveryComputationService $rttRecoveryService,
|
||||||
private readonly EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
#[Autowire(service: 'monolog.logger.cron')]
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@@ -51,8 +56,12 @@ final class RttRolloverCommand extends Command
|
|||||||
$today = new DateTimeImmutable('today');
|
$today = new DateTimeImmutable('today');
|
||||||
$force = (bool) $input->getOption('force');
|
$force = (bool) $input->getOption('force');
|
||||||
|
|
||||||
|
$this->logger->info('app:rtt:rollover started.', ['date' => $today->format('Y-m-d'), 'force' => $force]);
|
||||||
|
|
||||||
if (!$force && '06-01' !== $today->format('m-d')) {
|
if (!$force && '06-01' !== $today->format('m-d')) {
|
||||||
$io->success('No RTT rollover today: business date is not 01/06.');
|
$message = 'No RTT rollover today: business date is not 01/06.';
|
||||||
|
$this->logger->info($message, ['date' => $today->format('Y-m-d')]);
|
||||||
|
$io->success($message);
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -67,6 +76,7 @@ final class RttRolloverCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->isEligible($employee)) {
|
if (!$this->isEligible($employee)) {
|
||||||
|
$this->logger->info('Employee skipped: not eligible.', ['employeeId' => $employee->getId()]);
|
||||||
++$skipped;
|
++$skipped;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -74,13 +84,21 @@ final class RttRolloverCommand extends Command
|
|||||||
|
|
||||||
$existing = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $targetYear);
|
$existing = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $targetYear);
|
||||||
if (null !== $existing) {
|
if (null !== $existing) {
|
||||||
|
$this->logger->info('Employee skipped: balance already exists.', ['employeeId' => $employee->getId(), 'year' => $targetYear]);
|
||||||
++$skipped;
|
++$skipped;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$previousYear = $targetYear - 1;
|
try {
|
||||||
$carryMinutes = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $previousYear);
|
$previousYear = $targetYear - 1;
|
||||||
|
$carryMinutes = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $previousYear);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->logger->error('Error computing carry for employee.', ['employeeId' => $employee->getId(), 'error' => $e->getMessage()]);
|
||||||
|
++$skipped;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$balance = new EmployeeRttBalance()
|
$balance = new EmployeeRttBalance()
|
||||||
->setEmployee($employee)
|
->setEmployee($employee)
|
||||||
@@ -90,16 +108,22 @@ final class RttRolloverCommand extends Command
|
|||||||
;
|
;
|
||||||
|
|
||||||
$this->entityManager->persist($balance);
|
$this->entityManager->persist($balance);
|
||||||
|
$this->logger->info('Balance created.', ['employeeId' => $employee->getId(), 'year' => $targetYear, 'carryMinutes' => $carryMinutes]);
|
||||||
++$created;
|
++$created;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->entityManager->flush();
|
try {
|
||||||
|
$this->entityManager->flush();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->logger->error('Error flushing RTT balances.', ['error' => $e->getMessage()]);
|
||||||
|
$io->error('RTT rollover failed: '.$e->getMessage());
|
||||||
|
|
||||||
$io->success(sprintf(
|
return Command::FAILURE;
|
||||||
'RTT rollover done: %d created, %d skipped.',
|
}
|
||||||
$created,
|
|
||||||
$skipped
|
$message = sprintf('RTT rollover done: %d created, %d skipped.', $created, $skipped);
|
||||||
));
|
$this->logger->info($message);
|
||||||
|
$io->success($message);
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user