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
}} Jours</p> </p>
<p><strong class="uppercase font-semibold">Reste à prendre :</strong> <p class="col-start-2 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">Pris :</strong>
{{ formatCount(summary?.remainingDays) }} Jours</p> {{ formatCount(isForfaitRule ? currentYearTakenDays : summary?.takenDays) }} Jours
</div> </p>
<div class="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3"> <p class="col-start-3 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">Reste à prendre :</strong>
<p><span class="uppercase font-semibold">Samedi acquis :</span> {{ formatCount(summary?.remainingDays) }} Jours
{{ formatCount(summary?.acquiredSaturdays) }} Jours</p> </p>
<p><span class="uppercase font-semibold">Reste à prendre :</span> <p class="col-start-4 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">En cours d'acquisition :</strong>
{{ formatCount(summary?.remainingSaturdays) }} Jours</p> {{ formatCount(summary?.accruingDays) }} Jours
</div> </p>
<div class="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3"> <p v-if="!isForfaitRule" class="col-start-1 p-[10px]"><span class="uppercase font-semibold">Samedi acquis :</span>
<p><span class="uppercase font-semibold">Fractionné acquis : </span>{{ formatCount(summary?.fractionedDays) }} Jours</p> {{ 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>
<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>
@@ -118,7 +139,7 @@ const emit = defineEmits<{
}>() }>()
const isFractionedDrawerOpen = ref(false) const isFractionedDrawerOpen = ref(false)
const fractionedForm = reactive({ days: 0 }) const fractionedForm = reactive({days: 0})
const openFractionedDrawer = () => { const openFractionedDrawer = () => {
fractionedForm.days = props.summary?.fractionedDays ?? 0 fractionedForm.days = props.summary?.fractionedDays ?? 0
@@ -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()
@@ -282,15 +308,15 @@ const getDayStyle = (day: { leave: DayLeaveState | null; isHoliday: boolean }) =
if (day.leave) { if (day.leave) {
const color = day.leave.colors[0] ?? '#222783' const color = day.leave.colors[0] ?? '#222783'
if (day.leave.am && day.leave.pm) { if (day.leave.am && day.leave.pm) {
return { backgroundColor: color } return {backgroundColor: color}
} }
const colorFaded = `${color}60` const colorFaded = `${color}60`
const backgroundImage = day.leave.am const backgroundImage = day.leave.am
? `linear-gradient(180deg, ${color} 0 50%, ${colorFaded} 50% 100%)` ? `linear-gradient(180deg, ${color} 0 50%, ${colorFaded} 50% 100%)`
: `linear-gradient(180deg, ${colorFaded} 0 50%, ${color} 50% 100%)` : `linear-gradient(180deg, ${colorFaded} 0 50%, ${color} 50% 100%)`
return { backgroundImage, backgroundColor: 'transparent' } return {backgroundImage, backgroundColor: 'transparent'}
} }
if (day.isHoliday) return { backgroundColor: 'rgb(179, 229, 252)' } if (day.isHoliday) return {backgroundColor: 'rgb(179, 229, 252)'}
return undefined return undefined
} }

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

@@ -20,17 +20,20 @@ use App\State\EmployeeLeaveSummaryProvider;
)] )]
final class EmployeeLeaveSummary final class EmployeeLeaveSummary
{ {
public int $year = 0; public int $year = 0;
public bool $isSupported = false; public bool $isSupported = false;
public string $ruleCode = ''; public string $ruleCode = '';
public float $acquiredDays = 0.0; public float $acquiredDays = 0.0;
public float $remainingDays = 0.0; public float $remainingDays = 0.0;
public float $takenDays = 0.0; public float $takenDays = 0.0;
public float $acquiredSaturdays = 0.0; public float $acquiredSaturdays = 0.0;
public float $remainingSaturdays = 0.0; public float $remainingSaturdays = 0.0;
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

@@ -91,16 +91,19 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
$fractionedDays = $this->resolveFractionedDays($employee, $yearSummary['ruleCode'], $year); $fractionedDays = $this->resolveFractionedDays($employee, $yearSummary['ruleCode'], $year);
$summary->isSupported = true; $summary->isSupported = true;
$summary->ruleCode = $yearSummary['ruleCode']; $summary->ruleCode = $yearSummary['ruleCode'];
$summary->acquiredDays = $yearSummary['acquiredDays'] + $fractionedDays; $summary->acquiredDays = $yearSummary['acquiredDays'] + $fractionedDays;
$summary->acquiredSaturdays = $yearSummary['acquiredSaturdays']; $summary->acquiredSaturdays = $yearSummary['acquiredSaturdays'];
$summary->fractionedDays = $fractionedDays; $summary->fractionedDays = $fractionedDays;
$summary->accruingDays = $yearSummary['accruingDays']; $summary->accruingDays = $yearSummary['accruingDays'];
$summary->takenDays = $yearSummary['takenDays']; $summary->takenDays = $yearSummary['takenDays'];
$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,26 +248,37 @@ 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;
} }
if ($year === $targetYear) { if ($year === $targetYear) {
$targetSummary = [ $targetSummary = [
'ruleCode' => $leavePolicy['ruleCode'], 'ruleCode' => $leavePolicy['ruleCode'],
'acquiredDays' => $acquiredDays, 'acquiredDays' => $acquiredDays,
'acquiredSaturdays' => $acquiredSaturdays, 'acquiredSaturdays' => $acquiredSaturdays,
'accruingDays' => $accruingDays, 'accruingDays' => $accruingDays,
'takenDays' => $takenDays, 'takenDays' => $takenDays,
'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)