diff --git a/doc/functional-rules.md b/doc/functional-rules.md
index 3e4829d..ce18874 100644
--- a/doc/functional-rules.md
+++ b/doc/functional-rules.md
@@ -231,6 +231,14 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
- s'exécute le `1er juin` (même cron que le rollover congés)
- calcule le total récup N-1 et le persiste en `opening_minutes` du nouvel exercice
- idempotent (ne recrée pas si la ligne existe)
+ - paiement RTT:
+ - saisie RH via `PATCH /employees/{id}/rtt-payments` (body: `month`, `minutes`, `rate`)
+ - stocké dans `employee_rtt_payments` (employee, year, month, minutes, rate)
+ - `rate`: taux de majoration, valeurs `25` ou `50`
+ - les heures payées sont soustraites du disponible RTT (`availableMinutes -= totalPaidMinutes`)
+ - affichage: 2 lignes par mois dans le tableau (25% et 50%)
+ - affichage:
+ - le compteur global RTT est affiché en **heures** (format `Xh00`)
## 10) Notifications
diff --git a/frontend/components/employees/RttTab.vue b/frontend/components/employees/RttTab.vue
index 75196fc..778145e 100644
--- a/frontend/components/employees/RttTab.vue
+++ b/frontend/components/employees/RttTab.vue
@@ -1,8 +1,8 @@
-
RTT à la date du jour : {{ formatDays(summary?.availableMinutes ?? 0) }}
-
+
+
+
diff --git a/frontend/composables/useEmployeeDetailPage.ts b/frontend/composables/useEmployeeDetailPage.ts
index 2c4295e..83fbe2f 100644
--- a/frontend/composables/useEmployeeDetailPage.ts
+++ b/frontend/composables/useEmployeeDetailPage.ts
@@ -7,7 +7,7 @@ import { CONTRACT_TYPES } from '~/services/dto/contract'
import { listAbsences } from '~/services/absences'
import { listContracts } from '~/services/contracts'
import { getEmployeeLeaveSummary, updateFractionedDays } from '~/services/employee-leave-summary'
-import { getEmployeeRttSummary } from '~/services/employee-rtt-summary'
+import { getEmployeeRttSummary, createRttPayment } from '~/services/employee-rtt-summary'
import { getEmployee, updateEmployee } from '~/services/employees'
import { listPublicHolidays } from '~/services/public-holidays'
import { formatNullableYmdToFr, getTodayYmd, shiftYmd } from '~/utils/date'
@@ -307,6 +307,13 @@ export const useEmployeeDetailPage = () => {
await loadEmployee()
}
+ const submitRttPayment = async (month: number, minutes: number, rate: '25' | '50') => {
+ if (!employee.value) return
+ const year = rttSummary.value?.year ?? undefined
+ await createRttPayment(employee.value.id, month, minutes, rate, year)
+ await loadEmployee()
+ }
+
watch(requiresCreateContractEndDate, (required) => {
if (!required) {
createContractForm.endDate = ''
@@ -358,6 +365,7 @@ export const useEmployeeDetailPage = () => {
setCreateContractDrawerOpen,
submitContractUpdate,
submitCreateContract,
- submitFractionedDays
+ submitFractionedDays,
+ submitRttPayment
}
}
diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue
index aca253c..da6556c 100644
--- a/frontend/layouts/default.vue
+++ b/frontend/layouts/default.vue
@@ -9,52 +9,64 @@
Tableau de bord
Calendrier
Heures
Employés
Sites
Types d'absence
Utilisateurs
@@ -80,9 +92,5 @@
const auth = useAuthStore()
const {version} = useAppVersion()
const isAdmin = computed(() => auth.user?.roles?.includes('ROLE_ADMIN') ?? false)
-
-const handleLogout = async () => {
- await auth.logout()
- await navigateTo('/login')
-}
+const route = useRoute()
diff --git a/frontend/pages/employees/[id].vue b/frontend/pages/employees/[id].vue
index accc2af..a05f84a 100644
--- a/frontend/pages/employees/[id].vue
+++ b/frontend/pages/employees/[id].vue
@@ -96,7 +96,7 @@
:public-holidays="publicHolidays"
@update-fractioned-days="submitFractionedDays"
/>
-
+
@@ -143,7 +143,8 @@ const {
setCreateContractDrawerOpen,
submitContractUpdate,
submitCreateContract,
- submitFractionedDays
+ submitFractionedDays,
+ submitRttPayment
} = useEmployeeDetailPage()
useHead(() => ({
diff --git a/frontend/services/dto/employee-rtt-summary.ts b/frontend/services/dto/employee-rtt-summary.ts
index 282b154..f4c5380 100644
--- a/frontend/services/dto/employee-rtt-summary.ts
+++ b/frontend/services/dto/employee-rtt-summary.ts
@@ -6,11 +6,19 @@ export type EmployeeRttWeekSummary = {
recoveryMinutes: number
}
+export type RttMonthPayment = {
+ month: number
+ paidMinutes25: number
+ paidMinutes50: number
+}
+
export type EmployeeRttSummary = {
year: number
carryFromPreviousYearMinutes: number
currentYearRecoveryMinutes: number
+ totalPaidMinutes: number
availableMinutes: number
weeks: EmployeeRttWeekSummary[]
+ monthPayments: RttMonthPayment[]
}
diff --git a/frontend/services/employee-rtt-summary.ts b/frontend/services/employee-rtt-summary.ts
index 8888379..6ec4d12 100644
--- a/frontend/services/employee-rtt-summary.ts
+++ b/frontend/services/employee-rtt-summary.ts
@@ -6,3 +6,10 @@ export const getEmployeeRttSummary = async (employeeId: number, year?: number) =
return api.get(`/employees/${employeeId}/rtt-summary`, query, { toast: false })
}
+export const createRttPayment = async (employeeId: number, month: number, minutes: number, rate: '25' | '50', year?: number) => {
+ const api = useApi()
+ const body: Record = { month, minutes, rate }
+ if (year) body.year = year
+ return api.patch(`/employees/${employeeId}/rtt-payments`, body)
+}
+
diff --git a/migrations/Version20260309140000.php b/migrations/Version20260309140000.php
new file mode 100644
index 0000000..2e0c888
--- /dev/null
+++ b/migrations/Version20260309140000.php
@@ -0,0 +1,41 @@
+addSql(<<<'SQL'
+ CREATE TABLE employee_rtt_payments (
+ id SERIAL PRIMARY KEY,
+ employee_id INT NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
+ year INT NOT NULL,
+ month INT NOT NULL,
+ minutes INT NOT NULL,
+ rate VARCHAR(10) NOT NULL,
+ created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
+ )
+ SQL);
+ $this->addSql('CREATE INDEX idx_rtt_payment_employee_year ON employee_rtt_payments (employee_id, year)');
+ $this->addSql("COMMENT ON TABLE employee_rtt_payments IS 'Paiements RTT par employe, mois et taux de majoration.'");
+ $this->addSql("COMMENT ON COLUMN employee_rtt_payments.rate IS 'Taux de majoration: 25 ou 50.'");
+ $this->addSql("COMMENT ON COLUMN employee_rtt_payments.minutes IS 'Minutes RTT payees pour ce mois et ce taux.'");
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('DROP TABLE employee_rtt_payments');
+ }
+}
diff --git a/migrations/Version20260309160000.php b/migrations/Version20260309160000.php
new file mode 100644
index 0000000..61855a0
--- /dev/null
+++ b/migrations/Version20260309160000.php
@@ -0,0 +1,26 @@
+addSql('CREATE UNIQUE INDEX uniq_rtt_payment_employee_year_month_rate ON employee_rtt_payments (employee_id, year, month, rate)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('DROP INDEX uniq_rtt_payment_employee_year_month_rate');
+ }
+}
diff --git a/src/ApiResource/EmployeeRttPaymentInput.php b/src/ApiResource/EmployeeRttPaymentInput.php
new file mode 100644
index 0000000..057b444
--- /dev/null
+++ b/src/ApiResource/EmployeeRttPaymentInput.php
@@ -0,0 +1,29 @@
+ */
+ public array $monthPayments = [];
/** @var list */
public array $weeks = [];
diff --git a/src/Dto/Rtt/RttMonthPayment.php b/src/Dto/Rtt/RttMonthPayment.php
new file mode 100644
index 0000000..6382937
--- /dev/null
+++ b/src/Dto/Rtt/RttMonthPayment.php
@@ -0,0 +1,14 @@
+ 'Paiements RTT par employe, mois et exercice.'])]
+#[ORM\Index(columns: ['employee_id', 'year'], name: 'idx_rtt_payment_employee_year')]
+class EmployeeRttPayment
+{
+ #[ORM\Id]
+ #[ORM\GeneratedValue]
+ #[ORM\Column(type: 'integer')]
+ private ?int $id = null;
+
+ #[ORM\ManyToOne(targetEntity: Employee::class)]
+ #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
+ private ?Employee $employee = null;
+
+ #[ORM\Column(type: 'integer', options: ['comment' => 'Annee d exercice.'])]
+ private int $year = 0;
+
+ #[ORM\Column(type: 'integer', options: ['comment' => 'Mois du paiement.'])]
+ private int $month = 0;
+
+ #[ORM\Column(type: 'integer', options: ['comment' => 'Duree en minutes.'])]
+ private int $minutes = 0;
+
+ #[ORM\Column(type: 'string', length: 10, options: ['comment' => 'Taux applique.'])]
+ private string $rate = '';
+
+ #[ORM\Column(type: 'datetime_immutable')]
+ private DateTimeImmutable $createdAt;
+
+ #[ORM\Column(type: 'datetime_immutable')]
+ private DateTimeImmutable $updatedAt;
+
+ public function __construct()
+ {
+ $now = new DateTimeImmutable();
+ $this->createdAt = $now;
+ $this->updatedAt = $now;
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getEmployee(): ?Employee
+ {
+ return $this->employee;
+ }
+
+ public function setEmployee(Employee $employee): self
+ {
+ $this->employee = $employee;
+
+ return $this;
+ }
+
+ public function getYear(): int
+ {
+ return $this->year;
+ }
+
+ public function setYear(int $year): self
+ {
+ $this->year = $year;
+
+ return $this;
+ }
+
+ public function getMonth(): int
+ {
+ return $this->month;
+ }
+
+ public function setMonth(int $month): self
+ {
+ $this->month = $month;
+
+ return $this;
+ }
+
+ public function getMinutes(): int
+ {
+ return $this->minutes;
+ }
+
+ public function setMinutes(int $minutes): self
+ {
+ $this->minutes = $minutes;
+
+ return $this;
+ }
+
+ public function getRate(): string
+ {
+ return $this->rate;
+ }
+
+ public function setRate(string $rate): self
+ {
+ $this->rate = $rate;
+
+ return $this;
+ }
+
+ public function getCreatedAt(): DateTimeImmutable
+ {
+ return $this->createdAt;
+ }
+
+ public function getUpdatedAt(): DateTimeImmutable
+ {
+ return $this->updatedAt;
+ }
+
+ public function touch(): self
+ {
+ $this->updatedAt = new DateTimeImmutable();
+
+ return $this;
+ }
+}
diff --git a/src/Repository/EmployeeRttPaymentRepository.php b/src/Repository/EmployeeRttPaymentRepository.php
new file mode 100644
index 0000000..5a2b285
--- /dev/null
+++ b/src/Repository/EmployeeRttPaymentRepository.php
@@ -0,0 +1,47 @@
+
+ */
+final class EmployeeRttPaymentRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, EmployeeRttPayment::class);
+ }
+
+ public function findOneByEmployeeYearMonthRate(Employee $employee, int $year, int $month, string $rate): ?EmployeeRttPayment
+ {
+ return $this->findOneBy([
+ 'employee' => $employee,
+ 'year' => $year,
+ 'month' => $month,
+ 'rate' => $rate,
+ ]);
+ }
+
+ /**
+ * @return EmployeeRttPayment[]
+ */
+ public function findByEmployeeAndYear(Employee $employee, int $year): array
+ {
+ return $this->createQueryBuilder('p')
+ ->andWhere('p.employee = :employee')
+ ->andWhere('p.year = :year')
+ ->setParameter('employee', $employee)
+ ->setParameter('year', $year)
+ ->addOrderBy('p.month', 'ASC')
+ ->getQuery()
+ ->getResult()
+ ;
+ }
+}
diff --git a/src/State/EmployeeRttPaymentProcessor.php b/src/State/EmployeeRttPaymentProcessor.php
new file mode 100644
index 0000000..cb5781c
--- /dev/null
+++ b/src/State/EmployeeRttPaymentProcessor.php
@@ -0,0 +1,86 @@
+employeeRepository->find($employeeId);
+ if (!$employee instanceof Employee) {
+ throw new NotFoundHttpException('Employee not found.');
+ }
+
+ if (!in_array($data->rate, ['25', '50'], true)) {
+ throw new UnprocessableEntityHttpException('rate must be "25" or "50".');
+ }
+
+ if ($data->month < 1 || $data->month > 12) {
+ throw new UnprocessableEntityHttpException('month must be between 1 and 12.');
+ }
+
+ if ($data->minutes < 0) {
+ throw new UnprocessableEntityHttpException('minutes must be >= 0.');
+ }
+
+ $year = $data->year ?? $this->resolveCurrentExerciseYear();
+
+ $payment = $this->rttPaymentRepository->findOneByEmployeeYearMonthRate($employee, $year, $data->month, $data->rate);
+
+ if (null === $payment) {
+ $payment = new EmployeeRttPayment();
+ $payment->setEmployee($employee);
+ $payment->setYear($year);
+ $payment->setMonth($data->month);
+ $payment->setRate($data->rate);
+ $this->entityManager->persist($payment);
+ }
+
+ $payment->setMinutes($data->minutes);
+ $this->entityManager->flush();
+
+ $data->year = $year;
+
+ return $data;
+ }
+
+ private function resolveCurrentExerciseYear(): int
+ {
+ $today = new DateTimeImmutable('today');
+ $year = (int) $today->format('Y');
+ $month = (int) $today->format('n');
+
+ return $month >= 6 ? $year + 1 : $year;
+ }
+}
diff --git a/src/State/EmployeeRttPaymentProvider.php b/src/State/EmployeeRttPaymentProvider.php
new file mode 100644
index 0000000..8960953
--- /dev/null
+++ b/src/State/EmployeeRttPaymentProvider.php
@@ -0,0 +1,17 @@
+rttPaymentRepository->findByEmployeeAndYear($employee, $year);
+ $monthBuckets = [];
+
+ foreach ($payments as $payment) {
+ $m = $payment->getMonth();
+ if (!isset($monthBuckets[$m])) {
+ $monthBuckets[$m] = ['paidMinutes25' => 0, 'paidMinutes50' => 0];
+ }
+ if ('25' === $payment->getRate()) {
+ $monthBuckets[$m]['paidMinutes25'] += $payment->getMinutes();
+ } else {
+ $monthBuckets[$m]['paidMinutes50'] += $payment->getMinutes();
+ }
+ }
+
+ $monthPayments = [];
+ $totalPaidMinutes = 0;
+
+ foreach ($monthBuckets as $m => $bucket) {
+ $monthPayments[] = new RttMonthPayment($m, $bucket['paidMinutes25'], $bucket['paidMinutes50']);
+ $totalPaidMinutes += $bucket['paidMinutes25'] + $bucket['paidMinutes50'];
+ }
+
+ $summary->totalPaidMinutes = $totalPaidMinutes;
+ $summary->monthPayments = $monthPayments;
+ $summary->availableMinutes -= $totalPaidMinutes;
+
return $summary;
}