feat : ajout de log pour les cron

This commit is contained in:
2026-03-10 13:34:00 +01:00
parent 53255dba43
commit 84bffc645a
6 changed files with 310 additions and 242 deletions

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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;
} }