Compare commits

..

7 Commits

Author SHA1 Message Date
gitea-actions
f9979c9a19 chore: bump version to v0.1.75
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build & Push Docker Image / build (push) Successful in 30s
2026-04-02 06:59:10 +00:00
1091147100 [#SIRH-20] Ajouter pour les forfaits le paiement de congés N-1 (#12)
All checks were successful
Auto Tag Develop / tag (push) Successful in 7s
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

Reviewed-on: #12
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-04-02 06:59:03 +00:00
gitea-actions
fd154a59fb chore: bump version to v0.1.74
All checks were successful
Auto Tag Develop / tag (push) Successful in 7s
Build & Push Docker Image / build (push) Successful in 18s
2026-03-31 14:44:07 +00:00
967e3311e5 docs : update doc deployment-docker.md
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-03-31 16:43:54 +02:00
gitea-actions
04c5279946 chore: bump version to v0.1.73
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build & Push Docker Image / build (push) Successful in 15s
2026-03-31 12:46:03 +00:00
b25d40f3d8 Merge remote-tracking branch 'origin/develop' into develop
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-03-31 14:45:55 +02:00
e654516b82 docs : fix JWT key generation and permissions in deployment doc 2026-03-31 14:32:12 +02:00
16 changed files with 399 additions and 46 deletions

View File

@@ -1,5 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="db-forest-configuration">
<data version="2">.
----------------------------------------
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
5:0:9d8c1ad3-2491-4642-964a-666003c14128
.</data>
</component>
<component name="db-tree-configuration">
<option name="data" value="----------------------------------------&#10;1:0:9cad43df-2147-4989-b7a4-443067034884&#10;2:0:ae622167-c834-4e7b-87a5-c1721036f5dc&#10;3:0:f407a514-c6b4-4b26-9555-445a85892502&#10;4:0:09e221b8-067a-488b-9c1d-4e155a333079&#10;" />
</component>

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '0.1.72'
app.version: '0.1.75'

View File

@@ -22,18 +22,42 @@ Se deconnecter/reconnecter pour que le groupe `docker` prenne effet.
```bash
sudo apt install -y nginx
```
Verifier que Nginx tourne :
```bash
sudo systemctl enable nginx
sudo systemctl start nginx
```
## Premier deploiement
### PostgreSQL
### 1. Creer le dossier de deploiement
PostgreSQL tourne dans un conteneur Docker separe (voir le repo `infra-postgres`).
Il doit etre installe et accessible avant de deployer SIRH.
Creer la base de donnees pour SIRH :
```bash
cd /var/www/postgres
docker compose exec postgres psql -U admin
```
```sql
-- Si le user n'existe pas encore
CREATE USER malio WITH PASSWORD 'motdepasse';
-- Creer la base
CREATE DATABASE sirh_prod OWNER malio;
\q
```
---
## Premiere installation (nouvelle machine)
Guide complet pour mettre en ligne SIRH sur une machine vierge. Inclut les pre-requis, la BDD et l'app.
### 1. Installer les pre-requis
Installer Docker, Nginx et PostgreSQL (voir section Pre-requis ci-dessus).
### 2. Creer le dossier de deploiement
```bash
sudo mkdir -p /var/www/sirh
@@ -41,7 +65,18 @@ sudo chown -R $(whoami):$(whoami) /var/www/sirh
cd /var/www/sirh
```
### 2. Creer les fichiers de deploiement
### 3. Se connecter au registry Docker de Gitea
```bash
docker login gitea.malio.fr
```
- **Username** : le nom d'utilisateur du compte organisation Gitea `MALIO-DEV`
- **Password** : le token REGISTRY_TOKEN dispo dans le bitwarden
Le login est sauvegarde dans `~/.docker/config.json`, pas besoin de le refaire a chaque deploiement.
### 4. Creer les fichiers de deploiement
Creer `docker-compose.yml` :
@@ -84,11 +119,11 @@ echo "==> Waiting for container to be ready..."
sleep 3
echo "==> Running migrations..."
docker compose exec -T app php bin/console doctrine:migrations:migrate --no-interaction
docker compose exec -T -u www-data app php bin/console doctrine:migrations:migrate --no-interaction
echo "==> Clearing cache..."
docker compose exec -T app php bin/console cache:clear --env=prod
docker compose exec -T app php bin/console cache:warmup --env=prod
docker compose exec -T -u www-data app php bin/console cache:clear --env=prod
docker compose exec -T -u www-data app php bin/console cache:warmup --env=prod
VERSION=$(docker compose exec -T app cat config/version.yaml | grep 'app.version' | awk -F"'" '{print $2}')
echo "==> Deployed v${VERSION}"
@@ -100,7 +135,7 @@ Rendre executable :
chmod +x deploy.sh
```
### 3. Configurer l'environnement
### 5. Configurer l'environnement
Creer `.env` avec les variables suivantes :
@@ -110,8 +145,8 @@ APP_ENV=prod
APP_DEBUG=0
APP_SECRET=<generer avec: openssl rand -hex 32>
# Database (host.docker.internal = la machine hote, ou le PG tourne en bare metal)
DATABASE_URL="postgresql://sirh_user:password@host.docker.internal:5432/sirh?serverVersion=16&charset=utf8"
# Database (host.docker.internal = la machine hote, ou le PG tourne en Docker)
DATABASE_URL="postgresql://malio:password@host.docker.internal:5432/sirh_prod?serverVersion=16&charset=utf8"
# JWT
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
@@ -132,37 +167,28 @@ RTT_START_DATE=2026-02-23
HOLIDAY_URL="https://calendrier.api.gouv.fr/jours-feries/"
```
### 4. Generer les cles JWT
### 6. Generer les cles JWT
```bash
mkdir -p config/jwt
docker run --rm -v $(pwd)/config/jwt:/jwt php:8.4-cli bash -c \
"apt-get update -qq && apt-get install -y -qq openssl > /dev/null && \
openssl genpkey -algorithm RSA -out /jwt/private.pem -pkeyopt rsa_keygen_bits:4096 && \
openssl rsa -pubout -in /jwt/private.pem -out /jwt/public.pem"
openssl genpkey -algorithm RSA -out config/jwt/private.pem -pkeyopt rsa_keygen_bits:4096
openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem
```
### 5. Creer le dossier uploads
Rendre les cles lisibles par le conteneur (www-data = uid 33) :
```bash
sudo chown 33:33 config/jwt/private.pem config/jwt/public.pem
sudo chmod 644 config/jwt/private.pem config/jwt/public.pem
```
### 7. Creer le dossier uploads
```bash
mkdir -p uploads
```
### 6. Se connecter au registry Docker de Gitea
Pour que la machine puisse telecharger les images Docker depuis Gitea, il faut se connecter au registry une fois :
```bash
docker login gitea.malio.fr
```
Docker va demander :
- **Username** : le nom d'utilisateur du compte organisation Gitea `MALIO-DEV`
- **Password** : le token REGISTRY_TOKEN dispo dans le bitwarden
Le login est sauvegarde dans `~/.docker/config.json`, pas besoin de le refaire a chaque deploiement.
### 7. Configurer Nginx systeme
### 8. Configurer Nginx systeme
Creer `/etc/nginx/sites-available/sirh.conf` :
@@ -188,13 +214,34 @@ sudo ln -sf /etc/nginx/sites-available/sirh.conf /etc/nginx/sites-enabled/sirh.c
sudo nginx -t && sudo systemctl reload nginx
```
### 8. Deployer
### 9. Deployer
```bash
./deploy.sh
```
## Structure finale du dossier
### 10. Importer les donnees (optionnel)
Si tu as un dump SQL a importer :
```bash
# Depuis ton PC, envoyer le dump vers le serveur
scp sirh.sql user@serveur:/tmp/sirh.sql
# Sur le serveur, vider la base puis importer
cd /var/www/postgres
docker compose exec -T postgres psql -U malio sirh_prod -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
docker compose exec -T postgres psql -U malio sirh_prod < /tmp/sirh.sql
# Creer les tables manquantes (si le dump a des erreurs de syntaxe)
cd /var/www/sirh
docker compose exec -u www-data app php bin/console doctrine:schema:update --force --env=prod
# Nettoyer
rm /tmp/sirh.sql
```
### Structure finale du dossier
```
/var/www/sirh/
@@ -207,7 +254,11 @@ sudo nginx -t && sudo systemctl reload nginx
└── uploads/
```
## Deployer une release
---
## Deployer une nouvelle version
Quand l'app est deja installee, deployer une mise a jour :
```bash
cd /var/www/sirh
@@ -215,6 +266,10 @@ cd /var/www/sirh
./deploy.sh v0.1.61 # deploie une version specifique
```
C'est tout. Le script pull l'image, redemarre le conteneur, lance les migrations et vide le cache.
---
## Rollback
### Image seule (pas de changement de schema BDD)
@@ -227,11 +282,13 @@ cd /var/www/sirh
```bash
# 1. Rollback schema (pendant que la version actuelle tourne encore)
docker compose exec -T app php bin/console doctrine:migrations:migrate prev --no-interaction
docker compose exec -T -u www-data app php bin/console doctrine:migrations:migrate prev --no-interaction
# 2. Deployer l'ancienne version
./deploy.sh v0.1.60
```
---
## Voir les logs
```bash
@@ -240,6 +297,14 @@ docker compose logs -f # tous les logs
docker compose logs -f --tail=100 # 100 dernieres lignes
```
Logs Symfony :
```bash
docker compose exec app cat var/log/prod.log
```
---
## Migration depuis l'ancien deploiement (tar.gz)
Si l'application tourne deja en bare metal :
@@ -252,10 +317,10 @@ Si l'application tourne deja en bare metal :
cp -a /var/www/sirh/config/jwt /var/www/sirh-docker/config/jwt
cp -a /var/www/sirh/var/uploads /var/www/sirh-docker/uploads
```
4. Creer `docker-compose.yml` et `deploy.sh` dans `/var/www/sirh-docker/` (voir etape 2 ci-dessus)
4. Creer `docker-compose.yml` et `deploy.sh` dans `/var/www/sirh-docker/` (voir etape 4 ci-dessus)
5. Editer `/var/www/sirh-docker/.env` : changer `DATABASE_URL` pour utiliser `host.docker.internal` au lieu de `127.0.0.1`
6. Se connecter au registry Gitea (voir etape 6 ci-dessus)
7. Mettre a jour Nginx systeme avec la conf reverse proxy (voir etape 7 ci-dessus)
6. Se connecter au registry Gitea (voir etape 3 ci-dessus)
7. Mettre a jour Nginx systeme avec la conf reverse proxy (voir etape 8 ci-dessus)
8. Arreter l'ancien PHP-FPM : `sudo systemctl stop php8.4-fpm`
9. Deployer : `cd /var/www/sirh-docker && ./deploy.sh`
10. Verifier que tout marche, puis renommer le dossier : `mv /var/www/sirh-docker /var/www/sirh`

View File

@@ -245,6 +245,7 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
- pour `FORFAIT`:
- pris: basé sur toutes les absences (demi-journées incluses)
- restants = acquis - pris (borné à 0)
- paiement congés N-1: saisie RH via `PATCH /employees/{id}/paid-leave-days` (body: `paidLeaveDays`, `year`). Stocké dans `employee_leave_balances.paid_leave_days`. Les jours payés sont soustraits du reste à prendre N-1 (`previousYearRemainingDays = max(0, acquis_N-1 - pris_N-1 - payés)`). Uniquement pour les contrats forfait.
- report annuel:
- le reliquat (`restants`) de l'exercice précédent est reporté dans les acquis de l'exercice courant
- pour `CDI`/`CDD` non forfait: report séparé jours + samedis

View File

@@ -32,6 +32,18 @@
<p v-if="isForfaitRule" class="col-start-3 p-[10px] border-r-primary-500 bg-primary-500 text-white"><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">Année n-1 payés : </span>
<span> {{ formatCount(summary?.previousYearPaidDays) }} Jours</span>
</div>
<button
class="flex items-center"
@click="openPaidLeaveDrawer"
>
<Icon name="mdi:edit-box" size="24"/>
</button>
</div>
<div v-if="!isForfaitRule" class="col-start-4 p-[10px] flex gap-7 items-center">
<div>
<span class="uppercase font-semibold">Fractionné acquis : </span>
@@ -112,6 +124,39 @@
</div>
</form>
</AppDrawer>
<AppDrawer v-model="isPaidLeaveDrawerOpen" title="Congés N-1 payés">
<form class="space-y-4" @submit.prevent="handleSubmitPaidLeave">
<div>
<label class="text-md font-semibold text-neutral-700" for="paid-leave-days">
Nombre de jours <span class="text-red-600">*</span>
</label>
<input
id="paid-leave-days"
v-model="paidLeaveForm.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="isPaidLeaveDrawerOpen = 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>
@@ -136,11 +181,15 @@ const props = defineProps<{
const emit = defineEmits<{
(event: 'update-fractioned-days', days: number): void
(event: 'update-paid-leave-days', days: number): void
}>()
const isFractionedDrawerOpen = ref(false)
const fractionedForm = reactive({days: 0})
const isPaidLeaveDrawerOpen = ref(false)
const paidLeaveForm = reactive({days: 0})
const openFractionedDrawer = () => {
fractionedForm.days = props.summary?.fractionedDays ?? 0
isFractionedDrawerOpen.value = true
@@ -153,6 +202,18 @@ const handleSubmitFractioned = () => {
isFractionedDrawerOpen.value = false
}
const openPaidLeaveDrawer = () => {
paidLeaveForm.days = props.summary?.previousYearPaidDays ?? 0
isPaidLeaveDrawerOpen.value = true
}
const handleSubmitPaidLeave = () => {
const value = Number(paidLeaveForm.days)
if (Number.isNaN(value) || value < 0) return
emit('update-paid-leave-days', value)
isPaidLeaveDrawerOpen.value = false
}
const monthLabels = [
'Janvier',
'Fevrier',

View File

@@ -4,7 +4,7 @@ import type { EmployeeLeaveSummary } from '~/services/dto/employee-leave-summary
import type { Employee } from '~/services/dto/employee'
import { CONTRACT_TYPES } from '~/services/dto/contract'
import { listAbsences } from '~/services/absences'
import { getEmployeeLeaveSummary, updateFractionedDays } from '~/services/employee-leave-summary'
import { getEmployeeLeaveSummary, updateFractionedDays, updatePaidLeaveDays } from '~/services/employee-leave-summary'
import { listPublicHolidays } from '~/services/public-holidays'
export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee: () => Promise<void>) => {
@@ -57,6 +57,13 @@ export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee:
await reloadEmployee()
}
const submitPaidLeaveDays = async (days: number) => {
if (!employee.value) return
const year = leaveSummary.value?.year ?? undefined
await updatePaidLeaveDays(employee.value.id, days, year)
await reloadEmployee()
}
return {
employeeAbsences,
leaveSummary,
@@ -65,6 +72,7 @@ export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee:
leaveDataLoaded,
loadLeaveData,
resetLoaded,
submitFractionedDays
submitFractionedDays,
submitPaidLeaveDays
}
}

View File

@@ -148,6 +148,7 @@
:summary="leaveSummary"
:public-holidays="publicHolidays"
@update-fractioned-days="submitFractionedDays"
@update-paid-leave-days="submitPaidLeaveDays"
/>
</div>
<div v-else-if="showRttTab && activeTab === 'rtt'" class="h-full">
@@ -259,6 +260,7 @@ const {
submitContractUpdate,
submitCreateContract,
submitFractionedDays,
submitPaidLeaveDays,
submitRttPayment,
suspensionForms,
isSuspensionSubmitting,

View File

@@ -13,6 +13,7 @@ export type EmployeeLeaveSummary = {
previousYearAcquiredDays: number
previousYearTakenDays: number
previousYearRemainingDays: number
previousYearPaidDays: number
presenceDaysByMonth: Record<string, number>
}

View File

@@ -16,3 +16,11 @@ export const updateFractionedDays = async (employeeId: number, fractionedDays: n
return api.patch(`/employees/${employeeId}/fractioned-days`, body)
}
export const updatePaidLeaveDays = async (employeeId: number, paidLeaveDays: number, year?: number) => {
const api = useApi()
const body: Record<string, unknown> = { paidLeaveDays }
if (year) body.year = year
return api.patch(`/employees/${employeeId}/paid-leave-days`, body)
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260402064647 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add paid_leave_days column to employee_leave_balances for forfait N-1 leave payment';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE employee_leave_balances ADD paid_leave_days DOUBLE PRECISION DEFAULT 0 NOT NULL');
$this->addSql("COMMENT ON COLUMN employee_leave_balances.paid_leave_days IS 'Jours de conges N-1 payes par la RH (forfait uniquement).'");
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE employee_leave_balances DROP paid_leave_days');
}
}

View File

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

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Patch;
use App\State\EmployeePaidLeaveDaysProcessor;
use App\State\EmployeePaidLeaveDaysProvider;
#[ApiResource(
operations: [
new Patch(
uriTemplate: '/employees/{id}/paid-leave-days',
security: "is_granted('ROLE_ADMIN')",
provider: EmployeePaidLeaveDaysProvider::class,
processor: EmployeePaidLeaveDaysProcessor::class
),
],
paginationEnabled: false
)]
final class EmployeePaidLeaveDaysInput
{
public float $paidLeaveDays = 0.0;
public ?int $year = null;
}

View File

@@ -57,6 +57,9 @@ class EmployeeLeaveBalance
#[ORM\Column(type: 'float', options: ['default' => 0, 'comment' => 'Jours de fractionnement saisis par la RH.'])]
private float $fractionedDays = 0.0;
#[ORM\Column(type: 'float', options: ['default' => 0, 'comment' => 'Jours de conges N-1 payes par la RH (forfait uniquement).'])]
private float $paidLeaveDays = 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;
@@ -222,6 +225,18 @@ class EmployeeLeaveBalance
return $this;
}
public function getPaidLeaveDays(): float
{
return $this->paidLeaveDays;
}
public function setPaidLeaveDays(float $paidLeaveDays): self
{
$this->paidLeaveDays = $paidLeaveDays;
return $this;
}
public function isLocked(): bool
{
return $this->isLocked;

View File

@@ -93,6 +93,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
}
$fractionedDays = $this->resolveFractionedDays($employee, $yearSummary['ruleCode'], $year);
$paidLeaveDays = $this->resolvePaidLeaveDays($employee, $yearSummary['ruleCode'], $year);
$summary->isSupported = true;
$summary->ruleCode = $yearSummary['ruleCode'];
@@ -106,7 +107,8 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
$summary->remainingSaturdays = $yearSummary['remainingSaturdays'];
$summary->previousYearAcquiredDays = $yearSummary['previousYearAcquiredDays'];
$summary->previousYearTakenDays = $yearSummary['previousYearTakenDays'];
$summary->previousYearRemainingDays = $yearSummary['previousYearRemainingDays'];
$summary->previousYearRemainingDays = max(0.0, $yearSummary['previousYearRemainingDays'] - $paidLeaveDays);
$summary->previousYearPaidDays = $paidLeaveDays;
[$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year);
$summary->presenceDaysByMonth = $this->computePresenceDaysByMonth($employee, $periodFrom, $periodTo);
@@ -765,6 +767,13 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
return null !== $balance ? $balance->getFractionedDays() : 0.0;
}
private function resolvePaidLeaveDays(Employee $employee, string $ruleCode, int $year): float
{
$balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $year);
return null !== $balance ? $balance->getPaidLeaveDays() : 0.0;
}
private function resolveCurrentLeaveYear(DateTimeImmutable $today): int
{
$year = (int) $today->format('Y');

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\ApiResource\EmployeePaidLeaveDaysInput;
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 App\Service\AuditLogger;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
final readonly class EmployeePaidLeaveDaysProcessor implements ProcessorInterface
{
public function __construct(
private EmployeeRepository $employeeRepository,
private EmployeeLeaveBalanceRepository $leaveBalanceRepository,
private EntityManagerInterface $entityManager,
private AuditLogger $auditLogger,
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EmployeePaidLeaveDaysInput
{
if (!$data instanceof EmployeePaidLeaveDaysInput) {
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->setPaidLeaveDays($data->paidLeaveDays);
$balance->touch();
$empName = trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? ''));
$this->auditLogger->log(
$employee,
'update',
'paid_leave_days',
$balance->getId(),
sprintf('Congés N-1 payés modifiés pour %s (année %d) : %s', $empName, $year, (string) $data->paidLeaveDays),
['new' => ['paidLeaveDays' => $data->paidLeaveDays, 'year' => $year]],
);
$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');
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\EmployeePaidLeaveDaysInput;
final readonly class EmployeePaidLeaveDaysProvider implements ProviderInterface
{
public function provide(Operation $operation, array $uriVariables = [], array $context = []): EmployeePaidLeaveDaysInput
{
return new EmployeePaidLeaveDaysInput();
}
}