Compare commits

...

4 Commits

Author SHA1 Message Date
gitea-actions
96185e2334 chore: bump version to v0.1.35
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m17s
2026-03-13 11:11:40 +00:00
7d53000fc2 fix : validation autorisée pour les contrats 4h sans heures ou absence
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
2026-03-13 12:11:31 +01:00
gitea-actions
c317a2a026 chore: bump version to v0.1.34
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build Release Artefact / build (push) Successful in 1m20s
2026-03-13 10:59:58 +00:00
8846e83df1 feat : modification de l'affichage des congés
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
2026-03-13 11:57:02 +01:00
7 changed files with 131 additions and 84 deletions

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.1.33' app.version: '0.1.35'

View File

@@ -1,35 +1,54 @@
<template> <template>
<section class="flex h-full min-h-0 flex-col overflow-hidden pt-8"> <section class="flex h-full min-h-0 flex-col overflow-hidden pt-8">
<div class="grid grid-cols-4 rounded-md bg-primary-500 text-white text-[20]"> <div class="grid grid-cols-4 rounded-md bg-tertiary-500 text-primary-500 text-[18px] border border-primary-500">
<div class="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3"> <p class="col-start-1 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">Année acquis :</strong> {{
<p><strong class="uppercase font-semibold">Année acquis :</strong> {{
formatCount(summary?.acquiredDays) formatCount(summary?.acquiredDays)
}} Jours</p> }} Jours
<p><strong class="uppercase font-semibold">Reste à prendre :</strong> </p>
{{ formatCount(summary?.remainingDays) }} Jours</p> <p class="col-start-2 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">Pris :</strong>
{{ formatCount(isForfaitRule ? currentYearTakenDays : summary?.takenDays) }} Jours
</p>
<p class="col-start-3 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">Reste à prendre :</strong>
{{ formatCount(summary?.remainingDays) }} Jours
</p>
<p class="col-start-4 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">En cours d'acquisition :</strong>
{{ formatCount(summary?.accruingDays) }} Jours
</p>
<p v-if="!isForfaitRule" class="col-start-1 p-[10px]"><span class="uppercase font-semibold">Samedi acquis :</span>
{{ formatCount(summary?.acquiredSaturdays) }} Jours
</p>
<p v-else class="col-start-1 p-[10px]"><span class="uppercase font-semibold">Année N-1 acquis :</span>
{{ formatCount(summary?.previousYearAcquiredDays) }} Jours
</p>
<p v-if="!isForfaitRule" class="col-start-2 p-[10px]"><span class="uppercase font-semibold">Pris :</span>
{{ formatCount(summary?.takenSaturdays) }} Jours
</p>
<p v-if="!isForfaitRule" class="col-start-3 p-[10px]"><span class="uppercase font-semibold">Reste à prendre :</span>
{{ formatCount(summary?.remainingSaturdays) }} Jours
</p>
<p v-else class="col-start-2 p-[10px]"><span class="uppercase font-semibold">Pris :</span>
{{ formatCount(summary?.previousYearTakenDays) }} Jours
</p>
<p v-if="isForfaitRule" class="col-start-3 p-[10px]"><span class="uppercase font-semibold">Reste à prendre :</span>
{{ formatCount(summary?.previousYearRemainingDays) }} Jours
</p>
<div v-if="!isForfaitRule" class="col-start-4 p-[10px] flex gap-7 items-center">
<div>
<span class="uppercase font-semibold">Fractionné acquis : </span>
<span>{{ formatCount(summary?.fractionedDays) }} Jours</span>
</div> </div>
<div class="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3">
<p><span class="uppercase font-semibold">Samedi acquis :</span>
{{ formatCount(summary?.acquiredSaturdays) }} Jours</p>
<p><span class="uppercase font-semibold">Reste à prendre :</span>
{{ formatCount(summary?.remainingSaturdays) }} Jours</p>
</div>
<div class="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3">
<p><span class="uppercase font-semibold">Fractionné acquis : </span>{{ formatCount(summary?.fractionedDays) }} Jours</p>
<button <button
class="flex justify-center items-center gap-2 bg-white text-primary-500 font-bold w-[150px] rounded-md py-[1px]" class="flex items-center"
@click="openFractionedDrawer" @click="openFractionedDrawer"
> >
{{ summary?.fractionedDays === 0 ? '+ Ajouter' : 'Modifier' }}</button> <Icon name="mdi:edit-box" size="24"/>
</div> </button>
<div class="flex flex-col jutify-center gap-2 items-center py-3">
<p><span class="uppercase font-semibold">En cours d'acquisition :</span></p>
<p>{{ formatCount(summary?.accruingDays) }} Jours</p>
</div> </div>
</div> </div>
<div class="mt-8 min-h-0 flex-1 overflow-y-auto pr-2"> <div class="mt-8 min-h-0 flex-1 overflow-y-auto pr-2">
<div class="grid grid-cols-4 gap-10"> <div class="grid grid-cols-4 gap-10">
<div v-for="month in months" :key="month.label" class="rounded-md bg-tertiary-500 text-primary-500 flex flex-col justify-between"> <div v-for="month in months" :key="month.label"
class="rounded-md bg-tertiary-500 text-primary-500 flex flex-col justify-between">
<div class="flex justify-center rounded-t-md bg-primary-500 py-1 font-bold uppercase text-white"> <div class="flex justify-center rounded-t-md bg-primary-500 py-1 font-bold uppercase text-white">
{{ month.label }} {{ month.label }}
</div> </div>
@@ -54,7 +73,9 @@
</div> </div>
</template> </template>
</div> </div>
<div class="px-2 py-2 text-center border-t border-primary-500">Jours de présence : {{ summary?.presenceDaysByMonth?.[month.monthKey] ?? 0 }}</div> <div class="px-2 py-2 text-center border-t border-primary-500">Jours de présence :
{{ summary?.presenceDaysByMonth?.[month.monthKey] ?? 0 }}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -151,6 +172,11 @@ const weekDayLabels = ['L', 'M', 'M', 'J', 'V', 'S', 'D'] as const
const isForfaitRule = computed(() => props.summary?.ruleCode === 'FORFAIT_218') const isForfaitRule = computed(() => props.summary?.ruleCode === 'FORFAIT_218')
const currentYearTakenDays = computed(() => {
if (!props.summary) return null
return props.summary.takenDays - (props.summary.previousYearTakenDays ?? 0)
})
const displayedYear = computed(() => { const displayedYear = computed(() => {
if (props.summary?.year) return props.summary.year if (props.summary?.year) return props.summary.year
const today = new Date() const today = new Date()

View File

@@ -138,19 +138,17 @@ export const useHoursPage = () => {
return true return true
} }
const canCreateValidationRowFromAbsence = (employeeId: number) => { const canCreateEmptyValidationRow = (employeeId: number) => {
const row = rows.value[employeeId] const row = rows.value[employeeId]
if (row?.workHourId) return false if (row?.workHourId) return false
if (!hasContractAtSelectedDate(employeeId)) return false
const dayRow = dayContextByEmployeeId.value.get(employeeId) const dayRow = dayContextByEmployeeId.value.get(employeeId)
return !!dayRow?.absenceLabel && hasContractAtSelectedDate(employeeId) return !!dayRow?.absenceLabel || is4hContract(employeeId)
} }
const canCreateSiteValidationRowFromAbsence = (employeeId: number) => { const canCreateValidationRowFromAbsence = (employeeId: number) => canCreateEmptyValidationRow(employeeId)
const row = rows.value[employeeId]
if (row?.workHourId) return false const canCreateSiteValidationRowFromAbsence = (employeeId: number) => canCreateEmptyValidationRow(employeeId)
const dayRow = dayContextByEmployeeId.value.get(employeeId)
return !!dayRow?.absenceLabel && hasContractAtSelectedDate(employeeId)
}
const bulkValidatableEmployeeIds = computed(() => { const bulkValidatableEmployeeIds = computed(() => {
return visibleEmployees.value return visibleEmployees.value
@@ -347,6 +345,10 @@ export const useHoursPage = () => {
const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === TRACKING_MODES.PRESENCE const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === TRACKING_MODES.PRESENCE
const isTimeTracking = (employee: Employee) => !isPresenceTracking(employee) const isTimeTracking = (employee: Employee) => !isPresenceTracking(employee)
const is4hContract = (employeeId: number) => {
const employee = employees.value.find((e) => e.id === employeeId)
return employee?.contract?.weeklyHours === 4
}
const isRowLocked = (employeeId: number) => { const isRowLocked = (employeeId: number) => {
const row = rows.value[employeeId] const row = rows.value[employeeId]
if (!row) return false if (!row) return false
@@ -692,13 +694,8 @@ export const useHoursPage = () => {
options: { toast?: boolean } = {} options: { toast?: boolean } = {}
) => { ) => {
const row = rows.value[employeeId] const row = rows.value[employeeId]
const dayRow = dayContextByEmployeeId.value.get(employeeId)
if (!row?.workHourId && checked) { if (!row?.workHourId && checked) {
const employee = employees.value.find((item) => item.id === employeeId) if (canCreateEmptyValidationRow(employeeId)) {
const hasAbsence = !!dayRow?.absenceLabel
const canCreateFromAbsence = !!employee && hasAbsence && hasContractAtSelectedDate(employeeId)
if (canCreateFromAbsence) {
await bulkUpsertWorkHours({ await bulkUpsertWorkHours({
workDate: selectedDate.value, workDate: selectedDate.value,
entries: [{ entries: [{
@@ -746,13 +743,8 @@ export const useHoursPage = () => {
options: { toast?: boolean } = {} options: { toast?: boolean } = {}
) => { ) => {
const row = rows.value[employeeId] const row = rows.value[employeeId]
const dayRow = dayContextByEmployeeId.value.get(employeeId)
if (!row?.workHourId && checked) { if (!row?.workHourId && checked) {
const employee = employees.value.find((item) => item.id === employeeId) if (canCreateEmptyValidationRow(employeeId)) {
const hasAbsence = !!dayRow?.absenceLabel
const canCreateFromAbsence = !!employee && hasAbsence && hasContractAtSelectedDate(employeeId)
if (canCreateFromAbsence) {
await bulkUpsertWorkHours({ await bulkUpsertWorkHours({
workDate: selectedDate.value, workDate: selectedDate.value,
entries: [{ entries: [{

View File

@@ -10,6 +10,9 @@ export type EmployeeLeaveSummary = {
takenSaturdays: number takenSaturdays: number
fractionedDays: number fractionedDays: number
accruingDays: number accruingDays: number
previousYearAcquiredDays: number
previousYearTakenDays: number
previousYearRemainingDays: number
presenceDaysByMonth: Record<string, number> presenceDaysByMonth: Record<string, number>
} }

View File

@@ -31,6 +31,9 @@ final class EmployeeLeaveSummary
public float $takenSaturdays = 0.0; public float $takenSaturdays = 0.0;
public float $fractionedDays = 0.0; public float $fractionedDays = 0.0;
public float $accruingDays = 0.0; public float $accruingDays = 0.0;
public float $previousYearAcquiredDays = 0.0;
public float $previousYearTakenDays = 0.0;
public float $previousYearRemainingDays = 0.0;
/** @var array<string, float> YYYY-MM => count (0.5 for half-days) */ /** @var array<string, float> YYYY-MM => count (0.5 for half-days) */
public array $presenceDaysByMonth = []; public array $presenceDaysByMonth = [];

View File

@@ -101,6 +101,9 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
$summary->takenSaturdays = $yearSummary['takenSaturdays']; $summary->takenSaturdays = $yearSummary['takenSaturdays'];
$summary->remainingDays = $yearSummary['remainingDays'] + $fractionedDays; $summary->remainingDays = $yearSummary['remainingDays'] + $fractionedDays;
$summary->remainingSaturdays = $yearSummary['remainingSaturdays']; $summary->remainingSaturdays = $yearSummary['remainingSaturdays'];
$summary->previousYearAcquiredDays = $yearSummary['previousYearAcquiredDays'];
$summary->previousYearTakenDays = $yearSummary['previousYearTakenDays'];
$summary->previousYearRemainingDays = $yearSummary['previousYearRemainingDays'];
[$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year); [$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year);
$summary->presenceDaysByMonth = $this->computePresenceDaysByMonth($employee, $periodFrom, $periodTo); $summary->presenceDaysByMonth = $this->computePresenceDaysByMonth($employee, $periodFrom, $periodTo);
@@ -117,7 +120,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
* takenDays: float, * takenDays: float,
* takenSaturdays: float, * takenSaturdays: float,
* remainingDays: float, * remainingDays: float,
* remainingSaturdays: float * remainingSaturdays: float,
* previousYearAcquiredDays: float,
* previousYearTakenDays: float,
* previousYearRemainingDays: float
* } * }
*/ */
private function computeYearSummary(Employee $employee, int $targetYear): ?array private function computeYearSummary(Employee $employee, int $targetYear): ?array
@@ -214,6 +220,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
$takenDays += $openingBalance->getTakenDays(); $takenDays += $openingBalance->getTakenDays();
$takenSaturdays += $openingBalance->getTakenSaturdays(); $takenSaturdays += $openingBalance->getTakenSaturdays();
} }
$previousYearAcquired = 0.0;
$previousYearTaken = 0.0;
$previousYearRemaining = 0.0;
if (LeaveRuleCode::CDI_CDD_NON_FORFAIT->value === $leavePolicy['ruleCode']) { if (LeaveRuleCode::CDI_CDD_NON_FORFAIT->value === $leavePolicy['ruleCode']) {
$availableAcquired = max(0.0, $carryDays); $availableAcquired = max(0.0, $carryDays);
$takenFromAcquired = min($availableAcquired, $takenDays); $takenFromAcquired = min($availableAcquired, $takenDays);
@@ -238,13 +248,21 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
} else { } else {
// Forfait: no "en cours d'acquisition" counter, all rights are in acquired. // Forfait: no "en cours d'acquisition" counter, all rights are in acquired.
// Suspensions do not impact forfait 218 leave calculation. // Suspensions do not impact forfait 218 leave calculation.
$acquiredDays = $carryDays + $leavePolicy['acquiredDays']; // Taken days are first deducted from N-1 carry, then from current year.
$previousYearAcquired = $carryDays;
$takenFromPrevious = min(max(0.0, $previousYearAcquired), $takenDays);
$previousYearTaken = $takenFromPrevious;
$takenFromCurrent = $takenDays - $takenFromPrevious;
$previousYearRemaining = max(0.0, $previousYearAcquired - $takenFromPrevious);
$acquiredDays = $leavePolicy['acquiredDays'];
$accruingDays = 0.0; $accruingDays = 0.0;
$remainingDays = max(0.0, $acquiredDays - $takenDays); $remainingDays = max(0.0, $acquiredDays - $takenFromCurrent);
$acquiredSaturdays = 0.0; $acquiredSaturdays = 0.0;
$remainingSaturdays = 0.0; $remainingSaturdays = 0.0;
$previousRemainingDays = $remainingDays; $previousRemainingDays = $previousYearRemaining + $remainingDays;
$previousRemainingSaturdays = 0.0; $previousRemainingSaturdays = 0.0;
} }
@@ -258,6 +276,9 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
'takenSaturdays' => $takenSaturdays, 'takenSaturdays' => $takenSaturdays,
'remainingDays' => $remainingDays, 'remainingDays' => $remainingDays,
'remainingSaturdays' => $remainingSaturdays, 'remainingSaturdays' => $remainingSaturdays,
'previousYearAcquiredDays' => $previousYearAcquired,
'previousYearTakenDays' => $previousYearTaken,
'previousYearRemainingDays' => $previousYearRemaining,
]; ];
} }
} }

View File

@@ -134,13 +134,15 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
continue; continue;
} }
$is4hContract = 4 === $contract->getWeeklyHours();
if ($this->isEntryEmpty($normalized)) { if ($this->isEntryEmpty($normalized)) {
// Convention choisie: une ligne vide supprime l'enregistrement existant. // Convention choisie: une ligne vide supprime l'enregistrement existant.
if ($existing) { if ($existing) {
$this->entityManager->remove($existing); $this->entityManager->remove($existing);
++$result->deleted; ++$result->deleted;
} elseif (($absenceByEmployeeId[$employeeId] ?? false) === true) { } elseif (($absenceByEmployeeId[$employeeId] ?? false) === true || $is4hContract) {
// Si une absence existe ce jour, on garde une ligne technique pour pouvoir valider la journée. // Si une absence existe ce jour ou contrat 4h, on garde une ligne technique pour pouvoir valider la journée.
$workHour = new WorkHour() $workHour = new WorkHour()
->setEmployee($employee) ->setEmployee($employee)
->setWorkDate($workDate) ->setWorkDate($workDate)