feat : ajout des jours fractionnés
This commit is contained in:
2
.idea/db-forest-config.xml
generated
2
.idea/db-forest-config.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="db-tree-configuration">
|
||||
<option name="data" value="---------------------------------------- 1:0:9cad43df-2147-4989-b7a4-443067034884 2:0:ae622167-c834-4e7b-87a5-c1721036f5dc 3:0:f407a514-c6b4-4b26-9555-445a85892502 " />
|
||||
<option name="data" value="---------------------------------------- 1:0:9cad43df-2147-4989-b7a4-443067034884 2:0:ae622167-c834-4e7b-87a5-c1721036f5dc 3:0:f407a514-c6b4-4b26-9555-445a85892502 4:0:09e221b8-067a-488b-9c1d-4e155a333079 " />
|
||||
</component>
|
||||
</project>
|
||||
@@ -180,7 +180,7 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
|
||||
- reste à prendre: `acquis - absences` (toutes absences, demi-journées incluses)
|
||||
- pas de samedi (`0`)
|
||||
- pas de jours en cours d'acquisition (`0`)
|
||||
- fractionné: `0` (saisie RH ultérieure, non calculée automatiquement)
|
||||
- fractionné: saisie manuelle par la RH via `PATCH /employees/{id}/fractioned-days`, stocké dans `employee_leave_balances.fractioned_days`. Les jours fractionnés sont ajoutés aux acquis et au reste à prendre.
|
||||
- pour `CDI`/`CDD` non forfait:
|
||||
- pris CP: basé sur absences de type code `C` (CONGÉ), en tenant compte des demi-journées
|
||||
- samedi pris: absences `C` posées le samedi (demi-journée incluse)
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
v-for="day in daysInMonth"
|
||||
:key="day.date"
|
||||
class="sticky top-0 z-20 border-b border-neutral-200 px-2 py-3 text-center text-xs font-semibold transition-colors"
|
||||
:class="isHoveredColumn(day.date) ? 'bg-primary-500 text-white' : 'bg-tertiary-500 text-neutral-700'"
|
||||
:class="isHoveredColumn(day.date) || day.date === today ? 'bg-primary-500 text-white' : 'bg-tertiary-500 text-neutral-700'"
|
||||
>
|
||||
<div>{{ day.label }}</div>
|
||||
<div
|
||||
class="text-[10px]"
|
||||
:class="isHoveredColumn(day.date) ? 'text-white/90' : 'text-neutral-500'"
|
||||
:class="isHoveredColumn(day.date) || day.date === today ? 'text-white/90' : 'text-neutral-500'"
|
||||
>
|
||||
{{ day.weekday }}
|
||||
</div>
|
||||
@@ -91,6 +91,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { Employee } from '~/services/dto/employee'
|
||||
import type { HalfDay } from '~/services/dto/half-day'
|
||||
import { toYmd } from '~/utils/date'
|
||||
|
||||
const now = new Date()
|
||||
const today = toYmd(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
|
||||
type DayInfo = {
|
||||
date: string
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
<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 jutify-center items-center border-r-4 border-white py-3">
|
||||
<p><strong class="uppercase font-semibold">Acquis année :</strong> {{
|
||||
<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 jutify-center items-center border-r-4 border-white py-3">
|
||||
<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 jutify-center items-center border-r-4 border-white py-3">
|
||||
<p><span class="uppercase font-semibold">Acquis fractionné :</span></p>
|
||||
<p>{{ formatCount(summary?.fractionedDays) }} Jours</p>
|
||||
<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
|
||||
class="flex justify-center items-center gap-2 bg-white text-primary-500 font-bold w-[150px] rounded-md py-[1px]"
|
||||
@click="openFractionedDrawer"
|
||||
>
|
||||
<Icon name="mdi:plus-thick" size="16" />
|
||||
{{ summary?.fractionedDays === 0 ? 'Ajouter' : 'Modifier' }}</button>
|
||||
</div>
|
||||
<div class="flex flex-col jutify-center items-center py-3">
|
||||
<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>
|
||||
@@ -53,6 +58,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppDrawer v-model="isFractionedDrawerOpen" title="Jours fractionnés">
|
||||
<form class="space-y-4" @submit.prevent="handleSubmitFractioned">
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="fractioned-days">
|
||||
Nombre de jours <span class="text-red-600">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="fractioned-days"
|
||||
v-model="fractionedForm.days"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||
@click="isFractionedDrawerOpen = false"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</AppDrawer>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -60,6 +98,7 @@
|
||||
import type {Absence} from '~/services/dto/absence'
|
||||
import type {EmployeeLeaveSummary} from '~/services/dto/employee-leave-summary'
|
||||
import {normalizeDate, toYmd} from '~/utils/date'
|
||||
import AppDrawer from '~/components/AppDrawer.vue'
|
||||
|
||||
type DayLeaveState = {
|
||||
am: boolean
|
||||
@@ -74,6 +113,25 @@ const props = defineProps<{
|
||||
publicHolidays: Record<string, string>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update-fractioned-days', days: number): void
|
||||
}>()
|
||||
|
||||
const isFractionedDrawerOpen = ref(false)
|
||||
const fractionedForm = reactive({ days: 0 })
|
||||
|
||||
const openFractionedDrawer = () => {
|
||||
fractionedForm.days = props.summary?.fractionedDays ?? 0
|
||||
isFractionedDrawerOpen.value = true
|
||||
}
|
||||
|
||||
const handleSubmitFractioned = () => {
|
||||
const value = Number(fractionedForm.days)
|
||||
if (Number.isNaN(value) || value < 0) return
|
||||
emit('update-fractioned-days', value)
|
||||
isFractionedDrawerOpen.value = false
|
||||
}
|
||||
|
||||
const monthLabels = [
|
||||
'Janvier',
|
||||
'Fevrier',
|
||||
@@ -223,9 +281,10 @@ const getDayStyle = (day: { leave: DayLeaveState | null; isHoliday: boolean }) =
|
||||
if (day.leave.am && day.leave.pm) {
|
||||
return { backgroundColor: color }
|
||||
}
|
||||
const colorFaded = `${color}60`
|
||||
const backgroundImage = day.leave.am
|
||||
? `linear-gradient(135deg, ${color} 0 50%, transparent 50% 100%)`
|
||||
: `linear-gradient(135deg, transparent 0 50%, ${color} 50% 100%)`
|
||||
? `linear-gradient(180deg, ${color} 0 50%, ${colorFaded} 50% 100%)`
|
||||
: `linear-gradient(180deg, ${colorFaded} 0 50%, ${color} 50% 100%)`
|
||||
return { backgroundImage, backgroundColor: 'transparent' }
|
||||
}
|
||||
if (day.isHoliday) return { backgroundColor: 'rgb(179, 229, 252)' }
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<section class="flex h-full min-h-0 flex-col overflow-hidden pt-8">
|
||||
<div class="flex justify-center items-center bg-primary-500 rounded-md text-white py-5 text-[20px]">
|
||||
<div class="flex gap-10 justify-center items-center bg-primary-500 rounded-md text-white py-5 text-[20px]">
|
||||
<p><span class="font-semibold uppercase">RTT à la date du jour :</span> {{ formatDays(summary?.availableMinutes ?? 0) }}</p>
|
||||
<button class="bg-white rounded-md text-primary-500 font-bold px-6 py-1">
|
||||
<Icon name="mdi:plus-thick" size="16"/>
|
||||
Payer les RTT
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-8 min-h-0 flex-1 overflow-y-auto pr-2">
|
||||
<div class="grid grid-cols-4 gap-10 pb-4">
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ContractHistoryItem, Employee } from '~/services/dto/employee'
|
||||
import { CONTRACT_TYPES } from '~/services/dto/contract'
|
||||
import { listAbsences } from '~/services/absences'
|
||||
import { listContracts } from '~/services/contracts'
|
||||
import { getEmployeeLeaveSummary } from '~/services/employee-leave-summary'
|
||||
import { getEmployeeLeaveSummary, updateFractionedDays } from '~/services/employee-leave-summary'
|
||||
import { getEmployeeRttSummary } from '~/services/employee-rtt-summary'
|
||||
import { getEmployee, updateEmployee } from '~/services/employees'
|
||||
import { listPublicHolidays } from '~/services/public-holidays'
|
||||
@@ -300,6 +300,13 @@ export const useEmployeeDetailPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const submitFractionedDays = async (days: number) => {
|
||||
if (!employee.value) return
|
||||
const year = leaveSummary.value?.year ?? undefined
|
||||
await updateFractionedDays(employee.value.id, days, year)
|
||||
await loadEmployee()
|
||||
}
|
||||
|
||||
watch(requiresCreateContractEndDate, (required) => {
|
||||
if (!required) {
|
||||
createContractForm.endDate = ''
|
||||
@@ -350,6 +357,7 @@ export const useEmployeeDetailPage = () => {
|
||||
setContractDrawerOpen,
|
||||
setCreateContractDrawerOpen,
|
||||
submitContractUpdate,
|
||||
submitCreateContract
|
||||
submitCreateContract,
|
||||
submitFractionedDays
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
:absences="employeeAbsences"
|
||||
:summary="leaveSummary"
|
||||
:public-holidays="publicHolidays"
|
||||
@update-fractioned-days="submitFractionedDays"
|
||||
/>
|
||||
<EmployeesRttTab v-else class="h-full" :summary="rttSummary" />
|
||||
</div>
|
||||
@@ -141,7 +142,8 @@ const {
|
||||
setContractDrawerOpen,
|
||||
setCreateContractDrawerOpen,
|
||||
submitContractUpdate,
|
||||
submitCreateContract
|
||||
submitCreateContract,
|
||||
submitFractionedDays
|
||||
} = useEmployeeDetailPage()
|
||||
|
||||
useHead(() => ({
|
||||
|
||||
@@ -8,3 +8,11 @@ export const getEmployeeLeaveSummary = async (employeeId: number, year?: number)
|
||||
return api.get<EmployeeLeaveSummary>(`/employees/${employeeId}/leave-summary`, query, { toast: false })
|
||||
}
|
||||
|
||||
export const updateFractionedDays = async (employeeId: number, fractionedDays: number, year?: number) => {
|
||||
const api = useApi()
|
||||
const body: Record<string, unknown> = { fractionedDays }
|
||||
if (year) body.year = year
|
||||
|
||||
return api.patch(`/employees/${employeeId}/fractioned-days`, body)
|
||||
}
|
||||
|
||||
|
||||
26
migrations/Version20260309120000.php
Normal file
26
migrations/Version20260309120000.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260309120000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add fractioned_days column to employee_leave_balances table.';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE employee_leave_balances ADD fractioned_days DOUBLE PRECISION NOT NULL DEFAULT 0');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE employee_leave_balances DROP COLUMN fractioned_days');
|
||||
}
|
||||
}
|
||||
27
src/ApiResource/EmployeeFractionedDaysInput.php
Normal file
27
src/ApiResource/EmployeeFractionedDaysInput.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\ApiResource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use App\State\EmployeeFractionedDaysProcessor;
|
||||
use App\State\EmployeeFractionedDaysProvider;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Patch(
|
||||
uriTemplate: '/employees/{id}/fractioned-days',
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
provider: EmployeeFractionedDaysProvider::class,
|
||||
processor: EmployeeFractionedDaysProcessor::class
|
||||
),
|
||||
],
|
||||
paginationEnabled: false
|
||||
)]
|
||||
final class EmployeeFractionedDaysInput
|
||||
{
|
||||
public float $fractionedDays = 0.0;
|
||||
public ?int $year = null;
|
||||
}
|
||||
@@ -165,7 +165,7 @@ final class LeaveRolloverCommand extends Command
|
||||
$previousYear = $targetYear - 1;
|
||||
$previous = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $previousYear);
|
||||
if (null !== $previous) {
|
||||
$carryDays = $previous->getClosingDays();
|
||||
$carryDays = $previous->getClosingDays() + $previous->getFractionedDays();
|
||||
$carrySaturdays = LeaveRuleCode::CDI_CDD_NON_FORFAIT === $ruleCode
|
||||
? $previous->getClosingSaturdays()
|
||||
: 0.0;
|
||||
|
||||
@@ -54,6 +54,9 @@ class EmployeeLeaveBalance
|
||||
#[ORM\Column(type: 'float', options: ['comment' => 'Solde de cloture samedis sur l exercice.'])]
|
||||
private float $closingSaturdays = 0.0;
|
||||
|
||||
#[ORM\Column(type: 'float', options: ['default' => 0, 'comment' => 'Jours de fractionnement saisis par la RH.'])]
|
||||
private float $fractionedDays = 0.0;
|
||||
|
||||
#[ORM\Column(type: 'boolean', options: ['default' => false, 'comment' => 'Indique si le solde de l exercice est fige (verrouille RH).'])]
|
||||
private bool $isLocked = false;
|
||||
|
||||
@@ -207,6 +210,18 @@ class EmployeeLeaveBalance
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFractionedDays(): float
|
||||
{
|
||||
return $this->fractionedDays;
|
||||
}
|
||||
|
||||
public function setFractionedDays(float $fractionedDays): self
|
||||
{
|
||||
$this->fractionedDays = $fractionedDays;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isLocked(): bool
|
||||
{
|
||||
return $this->isLocked;
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Entity\Employee;
|
||||
use App\Enum\LeaveRuleCode;
|
||||
use App\Repository\AbsenceRepository;
|
||||
use App\Repository\EmployeeContractPeriodRepository;
|
||||
use App\Repository\EmployeeLeaveBalanceRepository;
|
||||
use App\Service\PublicHolidayServiceInterface;
|
||||
use DateTimeImmutable;
|
||||
use Throwable;
|
||||
@@ -26,6 +27,7 @@ final readonly class LeaveBalanceComputationService
|
||||
public function __construct(
|
||||
private AbsenceRepository $absenceRepository,
|
||||
private EmployeeContractPeriodRepository $periodRepository,
|
||||
private EmployeeLeaveBalanceRepository $leaveBalanceRepository,
|
||||
private PublicHolidayServiceInterface $publicHolidayService,
|
||||
) {}
|
||||
|
||||
@@ -62,8 +64,10 @@ final readonly class LeaveBalanceComputationService
|
||||
$carrySaturdays = 0.0;
|
||||
}
|
||||
|
||||
$fractionedDays = $this->resolveFractionedDays($employee, $ruleCode, $year);
|
||||
|
||||
if (LeaveRuleCode::FORFAIT_218 === $ruleCode) {
|
||||
$acquiredDays = $carryDays + (float) max(0, $this->countBusinessDays($from, $to) - self::FORFAIT_TARGET_WORKED_DAYS);
|
||||
$acquiredDays = $carryDays + (float) max(0, $this->countBusinessDays($from, $to) - self::FORFAIT_TARGET_WORKED_DAYS) + $fractionedDays;
|
||||
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to);
|
||||
[$takenDays] = $this->computeTakenAbsences($absences, $effectiveFrom, $to, false, false);
|
||||
$previousRemainingDays = max(0.0, $acquiredDays - $takenDays);
|
||||
@@ -88,10 +92,11 @@ final readonly class LeaveBalanceComputationService
|
||||
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $effectiveFrom, $to);
|
||||
[$takenDays, $takenSaturdays] = $this->computeTakenAbsences($absences, $effectiveFrom, $to, true, true);
|
||||
|
||||
$takenFromAcquired = min(max(0.0, $carryDays), $takenDays);
|
||||
$remainingAcquired = $carryDays - $takenFromAcquired;
|
||||
$remainingToImpute = max(0.0, $takenDays - $takenFromAcquired);
|
||||
$remainingGenerated = $generatedDays - $remainingToImpute;
|
||||
$acquiredWithFractioned = $carryDays + $fractionedDays;
|
||||
$takenFromAcquired = min(max(0.0, $acquiredWithFractioned), $takenDays);
|
||||
$remainingAcquired = $acquiredWithFractioned - $takenFromAcquired;
|
||||
$remainingToImpute = max(0.0, $takenDays - $takenFromAcquired);
|
||||
$remainingGenerated = $generatedDays - $remainingToImpute;
|
||||
|
||||
$takenFromAcquiredSaturdays = min(max(0.0, $carrySaturdays), $takenSaturdays);
|
||||
$remainingAcquiredSaturdays = $carrySaturdays - $takenFromAcquiredSaturdays;
|
||||
@@ -218,6 +223,13 @@ final readonly class LeaveBalanceComputationService
|
||||
return $earliest;
|
||||
}
|
||||
|
||||
private function resolveFractionedDays(Employee $employee, LeaveRuleCode $ruleCode, int $year): float
|
||||
{
|
||||
$balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $year);
|
||||
|
||||
return null !== $balance ? $balance->getFractionedDays() : 0.0;
|
||||
}
|
||||
|
||||
private function resolveAnnualDays(Employee $employee): float
|
||||
{
|
||||
return 4 === $employee->getContract()?->getWeeklyHours()
|
||||
|
||||
88
src/State/EmployeeFractionedDaysProcessor.php
Normal file
88
src/State/EmployeeFractionedDaysProcessor.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\ApiResource\EmployeeFractionedDaysInput;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\EmployeeLeaveBalance;
|
||||
use App\Enum\ContractType;
|
||||
use App\Enum\LeaveRuleCode;
|
||||
use App\Repository\EmployeeLeaveBalanceRepository;
|
||||
use App\Repository\EmployeeRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
|
||||
final readonly class EmployeeFractionedDaysProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EmployeeRepository $employeeRepository,
|
||||
private EmployeeLeaveBalanceRepository $leaveBalanceRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EmployeeFractionedDaysInput
|
||||
{
|
||||
if (!$data instanceof EmployeeFractionedDaysInput) {
|
||||
throw new UnprocessableEntityHttpException('Invalid payload.');
|
||||
}
|
||||
|
||||
$employeeId = (int) ($uriVariables['id'] ?? 0);
|
||||
if ($employeeId <= 0) {
|
||||
throw new UnprocessableEntityHttpException('id must be a positive integer.');
|
||||
}
|
||||
|
||||
$employee = $this->employeeRepository->find($employeeId);
|
||||
if (!$employee instanceof Employee) {
|
||||
throw new NotFoundHttpException('Employee not found.');
|
||||
}
|
||||
|
||||
$year = $data->year ?? $this->resolveCurrentYear($employee);
|
||||
$ruleCode = $this->resolveRuleCode($employee);
|
||||
|
||||
$balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $year);
|
||||
|
||||
if (null === $balance) {
|
||||
$balance = new EmployeeLeaveBalance();
|
||||
$balance->setEmployee($employee);
|
||||
$balance->setRuleCode($ruleCode);
|
||||
$balance->setYear($year);
|
||||
$this->entityManager->persist($balance);
|
||||
}
|
||||
|
||||
$balance->setFractionedDays($data->fractionedDays);
|
||||
$balance->touch();
|
||||
$this->entityManager->flush();
|
||||
|
||||
$data->year = $year;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function resolveRuleCode(Employee $employee): LeaveRuleCode
|
||||
{
|
||||
if (ContractType::FORFAIT === $employee->getContract()?->getType()) {
|
||||
return LeaveRuleCode::FORFAIT_218;
|
||||
}
|
||||
|
||||
return LeaveRuleCode::CDI_CDD_NON_FORFAIT;
|
||||
}
|
||||
|
||||
private function resolveCurrentYear(Employee $employee): int
|
||||
{
|
||||
$today = new DateTimeImmutable('today');
|
||||
|
||||
if (ContractType::FORFAIT === $employee->getContract()?->getType()) {
|
||||
return (int) $today->format('Y');
|
||||
}
|
||||
|
||||
$month = (int) $today->format('n');
|
||||
|
||||
return $month >= 6 ? (int) $today->format('Y') + 1 : (int) $today->format('Y');
|
||||
}
|
||||
}
|
||||
17
src/State/EmployeeFractionedDaysProvider.php
Normal file
17
src/State/EmployeeFractionedDaysProvider.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\EmployeeFractionedDaysInput;
|
||||
|
||||
final readonly class EmployeeFractionedDaysProvider implements ProviderInterface
|
||||
{
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): EmployeeFractionedDaysInput
|
||||
{
|
||||
return new EmployeeFractionedDaysInput();
|
||||
}
|
||||
}
|
||||
@@ -84,15 +84,17 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
return $summary;
|
||||
}
|
||||
|
||||
$fractionedDays = $this->resolveFractionedDays($employee, $yearSummary['ruleCode'], $year);
|
||||
|
||||
$summary->isSupported = true;
|
||||
$summary->ruleCode = $yearSummary['ruleCode'];
|
||||
$summary->acquiredDays = $yearSummary['acquiredDays'];
|
||||
$summary->acquiredDays = $yearSummary['acquiredDays'] + $fractionedDays;
|
||||
$summary->acquiredSaturdays = $yearSummary['acquiredSaturdays'];
|
||||
$summary->fractionedDays = 0.0;
|
||||
$summary->fractionedDays = $fractionedDays;
|
||||
$summary->accruingDays = $yearSummary['accruingDays'];
|
||||
$summary->takenDays = $yearSummary['takenDays'];
|
||||
$summary->takenSaturdays = $yearSummary['takenSaturdays'];
|
||||
$summary->remainingDays = $yearSummary['remainingDays'];
|
||||
$summary->remainingDays = $yearSummary['remainingDays'] + $fractionedDays;
|
||||
$summary->remainingSaturdays = $yearSummary['remainingSaturdays'];
|
||||
|
||||
return $summary;
|
||||
@@ -515,6 +517,13 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
return [$from, $to];
|
||||
}
|
||||
|
||||
private function resolveFractionedDays(Employee $employee, string $ruleCode, int $year): float
|
||||
{
|
||||
$balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $year);
|
||||
|
||||
return null !== $balance ? $balance->getFractionedDays() : 0.0;
|
||||
}
|
||||
|
||||
private function resolveCurrentLeaveYear(DateTimeImmutable $today): int
|
||||
{
|
||||
$year = (int) $today->format('Y');
|
||||
|
||||
Reference in New Issue
Block a user