Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96185e2334 | ||
| 7d53000fc2 | |||
|
|
c317a2a026 | ||
| 8846e83df1 |
@@ -1,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.1.33'
|
||||
app.version: '0.1.35'
|
||||
|
||||
@@ -1,35 +1,54 @@
|
||||
<template>
|
||||
<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="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3">
|
||||
<p><strong class="uppercase font-semibold">Année acquis :</strong> {{
|
||||
formatCount(summary?.acquiredDays)
|
||||
}} Jours</p>
|
||||
<p><strong class="uppercase font-semibold">Reste à prendre :</strong>
|
||||
{{ formatCount(summary?.remainingDays) }} 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">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>
|
||||
<div class="grid grid-cols-4 rounded-md bg-tertiary-500 text-primary-500 text-[18px] border border-primary-500">
|
||||
<p class="col-start-1 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">Année acquis :</strong> {{
|
||||
formatCount(summary?.acquiredDays)
|
||||
}} 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>
|
||||
<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"
|
||||
>
|
||||
{{ summary?.fractionedDays === 0 ? '+ Ajouter' : 'Modifier' }}</button>
|
||||
</div>
|
||||
<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>
|
||||
<Icon name="mdi:edit-box" size="24"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 min-h-0 flex-1 overflow-y-auto pr-2">
|
||||
<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">
|
||||
{{ month.label }}
|
||||
</div>
|
||||
@@ -54,7 +73,9 @@
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
@@ -118,7 +139,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const isFractionedDrawerOpen = ref(false)
|
||||
const fractionedForm = reactive({ days: 0 })
|
||||
const fractionedForm = reactive({days: 0})
|
||||
|
||||
const openFractionedDrawer = () => {
|
||||
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 currentYearTakenDays = computed(() => {
|
||||
if (!props.summary) return null
|
||||
return props.summary.takenDays - (props.summary.previousYearTakenDays ?? 0)
|
||||
})
|
||||
|
||||
const displayedYear = computed(() => {
|
||||
if (props.summary?.year) return props.summary.year
|
||||
const today = new Date()
|
||||
@@ -282,15 +308,15 @@ const getDayStyle = (day: { leave: DayLeaveState | null; isHoliday: boolean }) =
|
||||
if (day.leave) {
|
||||
const color = day.leave.colors[0] ?? '#222783'
|
||||
if (day.leave.am && day.leave.pm) {
|
||||
return { backgroundColor: color }
|
||||
return {backgroundColor: color}
|
||||
}
|
||||
const colorFaded = `${color}60`
|
||||
const backgroundImage = day.leave.am
|
||||
? `linear-gradient(180deg, ${color} 0 50%, ${colorFaded} 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
|
||||
}
|
||||
|
||||
|
||||
@@ -138,19 +138,17 @@ export const useHoursPage = () => {
|
||||
return true
|
||||
}
|
||||
|
||||
const canCreateValidationRowFromAbsence = (employeeId: number) => {
|
||||
const canCreateEmptyValidationRow = (employeeId: number) => {
|
||||
const row = rows.value[employeeId]
|
||||
if (row?.workHourId) return false
|
||||
if (!hasContractAtSelectedDate(employeeId)) return false
|
||||
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
||||
return !!dayRow?.absenceLabel && hasContractAtSelectedDate(employeeId)
|
||||
return !!dayRow?.absenceLabel || is4hContract(employeeId)
|
||||
}
|
||||
|
||||
const canCreateSiteValidationRowFromAbsence = (employeeId: number) => {
|
||||
const row = rows.value[employeeId]
|
||||
if (row?.workHourId) return false
|
||||
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
||||
return !!dayRow?.absenceLabel && hasContractAtSelectedDate(employeeId)
|
||||
}
|
||||
const canCreateValidationRowFromAbsence = (employeeId: number) => canCreateEmptyValidationRow(employeeId)
|
||||
|
||||
const canCreateSiteValidationRowFromAbsence = (employeeId: number) => canCreateEmptyValidationRow(employeeId)
|
||||
|
||||
const bulkValidatableEmployeeIds = computed(() => {
|
||||
return visibleEmployees.value
|
||||
@@ -347,6 +345,10 @@ export const useHoursPage = () => {
|
||||
|
||||
const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === TRACKING_MODES.PRESENCE
|
||||
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 row = rows.value[employeeId]
|
||||
if (!row) return false
|
||||
@@ -692,13 +694,8 @@ export const useHoursPage = () => {
|
||||
options: { toast?: boolean } = {}
|
||||
) => {
|
||||
const row = rows.value[employeeId]
|
||||
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
||||
if (!row?.workHourId && checked) {
|
||||
const employee = employees.value.find((item) => item.id === employeeId)
|
||||
const hasAbsence = !!dayRow?.absenceLabel
|
||||
const canCreateFromAbsence = !!employee && hasAbsence && hasContractAtSelectedDate(employeeId)
|
||||
|
||||
if (canCreateFromAbsence) {
|
||||
if (canCreateEmptyValidationRow(employeeId)) {
|
||||
await bulkUpsertWorkHours({
|
||||
workDate: selectedDate.value,
|
||||
entries: [{
|
||||
@@ -746,13 +743,8 @@ export const useHoursPage = () => {
|
||||
options: { toast?: boolean } = {}
|
||||
) => {
|
||||
const row = rows.value[employeeId]
|
||||
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
||||
if (!row?.workHourId && checked) {
|
||||
const employee = employees.value.find((item) => item.id === employeeId)
|
||||
const hasAbsence = !!dayRow?.absenceLabel
|
||||
const canCreateFromAbsence = !!employee && hasAbsence && hasContractAtSelectedDate(employeeId)
|
||||
|
||||
if (canCreateFromAbsence) {
|
||||
if (canCreateEmptyValidationRow(employeeId)) {
|
||||
await bulkUpsertWorkHours({
|
||||
workDate: selectedDate.value,
|
||||
entries: [{
|
||||
|
||||
@@ -10,6 +10,9 @@ export type EmployeeLeaveSummary = {
|
||||
takenSaturdays: number
|
||||
fractionedDays: number
|
||||
accruingDays: number
|
||||
previousYearAcquiredDays: number
|
||||
previousYearTakenDays: number
|
||||
previousYearRemainingDays: number
|
||||
presenceDaysByMonth: Record<string, number>
|
||||
}
|
||||
|
||||
|
||||
@@ -20,17 +20,20 @@ use App\State\EmployeeLeaveSummaryProvider;
|
||||
)]
|
||||
final class EmployeeLeaveSummary
|
||||
{
|
||||
public int $year = 0;
|
||||
public bool $isSupported = false;
|
||||
public string $ruleCode = '';
|
||||
public float $acquiredDays = 0.0;
|
||||
public float $remainingDays = 0.0;
|
||||
public float $takenDays = 0.0;
|
||||
public float $acquiredSaturdays = 0.0;
|
||||
public float $remainingSaturdays = 0.0;
|
||||
public float $takenSaturdays = 0.0;
|
||||
public float $fractionedDays = 0.0;
|
||||
public float $accruingDays = 0.0;
|
||||
public int $year = 0;
|
||||
public bool $isSupported = false;
|
||||
public string $ruleCode = '';
|
||||
public float $acquiredDays = 0.0;
|
||||
public float $remainingDays = 0.0;
|
||||
public float $takenDays = 0.0;
|
||||
public float $acquiredSaturdays = 0.0;
|
||||
public float $remainingSaturdays = 0.0;
|
||||
public float $takenSaturdays = 0.0;
|
||||
public float $fractionedDays = 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) */
|
||||
public array $presenceDaysByMonth = [];
|
||||
|
||||
@@ -91,16 +91,19 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
|
||||
$fractionedDays = $this->resolveFractionedDays($employee, $yearSummary['ruleCode'], $year);
|
||||
|
||||
$summary->isSupported = true;
|
||||
$summary->ruleCode = $yearSummary['ruleCode'];
|
||||
$summary->acquiredDays = $yearSummary['acquiredDays'] + $fractionedDays;
|
||||
$summary->acquiredSaturdays = $yearSummary['acquiredSaturdays'];
|
||||
$summary->fractionedDays = $fractionedDays;
|
||||
$summary->accruingDays = $yearSummary['accruingDays'];
|
||||
$summary->takenDays = $yearSummary['takenDays'];
|
||||
$summary->takenSaturdays = $yearSummary['takenSaturdays'];
|
||||
$summary->remainingDays = $yearSummary['remainingDays'] + $fractionedDays;
|
||||
$summary->remainingSaturdays = $yearSummary['remainingSaturdays'];
|
||||
$summary->isSupported = true;
|
||||
$summary->ruleCode = $yearSummary['ruleCode'];
|
||||
$summary->acquiredDays = $yearSummary['acquiredDays'] + $fractionedDays;
|
||||
$summary->acquiredSaturdays = $yearSummary['acquiredSaturdays'];
|
||||
$summary->fractionedDays = $fractionedDays;
|
||||
$summary->accruingDays = $yearSummary['accruingDays'];
|
||||
$summary->takenDays = $yearSummary['takenDays'];
|
||||
$summary->takenSaturdays = $yearSummary['takenSaturdays'];
|
||||
$summary->remainingDays = $yearSummary['remainingDays'] + $fractionedDays;
|
||||
$summary->remainingSaturdays = $yearSummary['remainingSaturdays'];
|
||||
$summary->previousYearAcquiredDays = $yearSummary['previousYearAcquiredDays'];
|
||||
$summary->previousYearTakenDays = $yearSummary['previousYearTakenDays'];
|
||||
$summary->previousYearRemainingDays = $yearSummary['previousYearRemainingDays'];
|
||||
|
||||
[$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year);
|
||||
$summary->presenceDaysByMonth = $this->computePresenceDaysByMonth($employee, $periodFrom, $periodTo);
|
||||
@@ -117,7 +120,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
* takenDays: float,
|
||||
* takenSaturdays: float,
|
||||
* remainingDays: float,
|
||||
* remainingSaturdays: float
|
||||
* remainingSaturdays: float,
|
||||
* previousYearAcquiredDays: float,
|
||||
* previousYearTakenDays: float,
|
||||
* previousYearRemainingDays: float
|
||||
* }
|
||||
*/
|
||||
private function computeYearSummary(Employee $employee, int $targetYear): ?array
|
||||
@@ -214,6 +220,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
$takenDays += $openingBalance->getTakenDays();
|
||||
$takenSaturdays += $openingBalance->getTakenSaturdays();
|
||||
}
|
||||
$previousYearAcquired = 0.0;
|
||||
$previousYearTaken = 0.0;
|
||||
$previousYearRemaining = 0.0;
|
||||
|
||||
if (LeaveRuleCode::CDI_CDD_NON_FORFAIT->value === $leavePolicy['ruleCode']) {
|
||||
$availableAcquired = max(0.0, $carryDays);
|
||||
$takenFromAcquired = min($availableAcquired, $takenDays);
|
||||
@@ -238,26 +248,37 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
} else {
|
||||
// Forfait: no "en cours d'acquisition" counter, all rights are in acquired.
|
||||
// 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;
|
||||
$remainingDays = max(0.0, $acquiredDays - $takenDays);
|
||||
$remainingDays = max(0.0, $acquiredDays - $takenFromCurrent);
|
||||
$acquiredSaturdays = 0.0;
|
||||
$remainingSaturdays = 0.0;
|
||||
|
||||
$previousRemainingDays = $remainingDays;
|
||||
$previousRemainingDays = $previousYearRemaining + $remainingDays;
|
||||
$previousRemainingSaturdays = 0.0;
|
||||
}
|
||||
|
||||
if ($year === $targetYear) {
|
||||
$targetSummary = [
|
||||
'ruleCode' => $leavePolicy['ruleCode'],
|
||||
'acquiredDays' => $acquiredDays,
|
||||
'acquiredSaturdays' => $acquiredSaturdays,
|
||||
'accruingDays' => $accruingDays,
|
||||
'takenDays' => $takenDays,
|
||||
'takenSaturdays' => $takenSaturdays,
|
||||
'remainingDays' => $remainingDays,
|
||||
'remainingSaturdays' => $remainingSaturdays,
|
||||
'ruleCode' => $leavePolicy['ruleCode'],
|
||||
'acquiredDays' => $acquiredDays,
|
||||
'acquiredSaturdays' => $acquiredSaturdays,
|
||||
'accruingDays' => $accruingDays,
|
||||
'takenDays' => $takenDays,
|
||||
'takenSaturdays' => $takenSaturdays,
|
||||
'remainingDays' => $remainingDays,
|
||||
'remainingSaturdays' => $remainingSaturdays,
|
||||
'previousYearAcquiredDays' => $previousYearAcquired,
|
||||
'previousYearTakenDays' => $previousYearTaken,
|
||||
'previousYearRemainingDays' => $previousYearRemaining,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,13 +134,15 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
||||
continue;
|
||||
}
|
||||
|
||||
$is4hContract = 4 === $contract->getWeeklyHours();
|
||||
|
||||
if ($this->isEntryEmpty($normalized)) {
|
||||
// Convention choisie: une ligne vide supprime l'enregistrement existant.
|
||||
if ($existing) {
|
||||
$this->entityManager->remove($existing);
|
||||
++$result->deleted;
|
||||
} elseif (($absenceByEmployeeId[$employeeId] ?? false) === true) {
|
||||
// Si une absence existe ce jour, on garde une ligne technique pour pouvoir valider la journée.
|
||||
} elseif (($absenceByEmployeeId[$employeeId] ?? false) === true || $is4hContract) {
|
||||
// Si une absence existe ce jour ou contrat 4h, on garde une ligne technique pour pouvoir valider la journée.
|
||||
$workHour = new WorkHour()
|
||||
->setEmployee($employee)
|
||||
->setWorkDate($workDate)
|
||||
|
||||
Reference in New Issue
Block a user