Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d4bdba914 | |||
| 74abecbe03 |
@@ -40,7 +40,7 @@ DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&ch
|
|||||||
RTT_START_DATE=2026-02-23
|
RTT_START_DATE=2026-02-23
|
||||||
# Comma-separated list of public holiday labels to exclude from the government API response
|
# Comma-separated list of public holiday labels to exclude from the government API response
|
||||||
# (typically the "journée de solidarité" worked in many companies)
|
# (typically the "journée de solidarité" worked in many companies)
|
||||||
EXCLUDED_PUBLIC_HOLIDAYS="Lundi de Pentecôte"
|
EXCLUDED_PUBLIC_HOLIDAYS="null"
|
||||||
###< app ###
|
###< app ###
|
||||||
|
|
||||||
###> nelmio/cors-bundle ###
|
###> nelmio/cors-bundle ###
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
- **Écrans Heures / Heures Conducteurs (vue jour)** : le libellé nature (CDI/CDD/Intérim) sous le nom de l'employé est résolu **à la date filtrée** via `WorkHourDayContext.contractNature` (alimenté par `EmployeeContractResolver::resolveNatureForEmployeeAndDate`), pas via `Employee.currentContractNature` (qui est résolu à aujourd'hui). Idem pour le **mode de suivi (TIME/PRESENCE), les heures hebdo et le libellé de contrat** sur la vue Jour : résolus à la date filtrée via `WorkHourDayContext` (`trackingMode`/`weeklyHours`/`contractType`/`contractName`, peuplés depuis `EmployeeContractResolver::resolveForEmployeeAndDate`), pas via `employee.contract` (résolu à aujourd'hui). Côté front, `resolveDayContract()` (`useHoursPage.ts`) pilote l'affichage et `handleSave` (heures vs présence par date). **Jours travaillés (CUSTOM)** : le libellé sous le nom affiche en suffixe les jours du planning `workDaysHours` au format court `LU,MA,ME,JE,VE` (ex. `BUREAU — CDI — LU,JE`). Exposé via `WorkHourDayContext.workDaysHours` (peuplé par `EmployeeContractResolver::resolveWorkDaysMinutesForEmployeeAndDate`, à la date filtrée), formaté front par `formatWorkedDaysShort` (`utils/contract.ts`) et accédé via `getRowWorkedDaysLabel` (`useHoursPage.ts`). Affiché **uniquement écran Heures** (`HoursDayView.vue`, mobile + desktop) ; naturellement limité aux CUSTOM (seuls eux ont `workDaysHours` → null sinon, rien affiché). Pas sur Heures Conducteurs (pas de planning workDaysHours).
|
- **Écrans Heures / Heures Conducteurs (vue jour)** : le libellé nature (CDI/CDD/Intérim) sous le nom de l'employé est résolu **à la date filtrée** via `WorkHourDayContext.contractNature` (alimenté par `EmployeeContractResolver::resolveNatureForEmployeeAndDate`), pas via `Employee.currentContractNature` (qui est résolu à aujourd'hui). Idem pour le **mode de suivi (TIME/PRESENCE), les heures hebdo et le libellé de contrat** sur la vue Jour : résolus à la date filtrée via `WorkHourDayContext` (`trackingMode`/`weeklyHours`/`contractType`/`contractName`, peuplés depuis `EmployeeContractResolver::resolveForEmployeeAndDate`), pas via `employee.contract` (résolu à aujourd'hui). Côté front, `resolveDayContract()` (`useHoursPage.ts`) pilote l'affichage et `handleSave` (heures vs présence par date). **Jours travaillés (CUSTOM)** : le libellé sous le nom affiche en suffixe les jours du planning `workDaysHours` au format court `LU,MA,ME,JE,VE` (ex. `BUREAU — CDI — LU,JE`). Exposé via `WorkHourDayContext.workDaysHours` (peuplé par `EmployeeContractResolver::resolveWorkDaysMinutesForEmployeeAndDate`, à la date filtrée), formaté front par `formatWorkedDaysShort` (`utils/contract.ts`) et accédé via `getRowWorkedDaysLabel` (`useHoursPage.ts`). Affiché **uniquement écran Heures** (`HoursDayView.vue`, mobile + desktop) ; naturellement limité aux CUSTOM (seuls eux ont `workDaysHours` → null sinon, rien affiché). Pas sur Heures Conducteurs (pas de planning workDaysHours).
|
||||||
- **Exports heures annuelles** (par salarié `EmployeeYearlyHoursPrintProvider` + tous `EmployeeYearlyHoursBulkPrintProvider`, via `YearlyHoursExportBuilder`) : **tous les jours sous contrat sont affichés**, même vides ou non saisis (jusqu'à aujourd'hui). Seuls les jours hors contrat sont omis (`buildSegments` : un seul filtre `!$hasData && null === $contract`). Ne pas réintroduire de saut des jours de semaine vides. Samedis/dimanches grisés (`#c0c0c0`) dans les templates `employee-yearly-hours/print*.html.twig`. NB : l'export *tous employés* sur l'année peut dépasser `memory_limit=256M` (Dompdf) — limitation pré-existante, voir avec l'infra si besoin.
|
- **Exports heures annuelles** (par salarié `EmployeeYearlyHoursPrintProvider` + tous `EmployeeYearlyHoursBulkPrintProvider`, via `YearlyHoursExportBuilder`) : **tous les jours sous contrat sont affichés**, même vides ou non saisis (jusqu'à aujourd'hui). Seuls les jours hors contrat sont omis (`buildSegments` : un seul filtre `!$hasData && null === $contract`). Ne pas réintroduire de saut des jours de semaine vides. Samedis/dimanches grisés (`#c0c0c0`) dans les templates `employee-yearly-hours/print*.html.twig`. NB : l'export *tous employés* sur l'année peut dépasser `memory_limit=256M` (Dompdf) — limitation pré-existante, voir avec l'infra si besoin.
|
||||||
- **Export heures vue Jour** (`WorkHourDayExportProvider`, endpoint `GET /work-hours/day-export?workDate=&siteIds=`, `ROLE_USER`) : bouton « Exporter » à droite du titre « Heures », **visible uniquement en vue Jour** (`v-if="(isAdmin || isSiteManager) && viewMode === 'day'"`, masqué en vue Semaine et pour `ROLE_SELF`). **Accessible aux admins ET aux chefs de site** : le périmètre est résolu côté backend via `EmployeeRepository::findScoped($user)` (admin → tous les sites, chef de site → ses sites uniquement, cf. `EmployeeScopeService`), donc un `siteIds` hors périmètre est ignoré ; le drawer front ne propose que les sites visibles (`sites` dérivé des employés scopés). PDF A4 portrait d'**une seule journée**, **regroupé par site**, colonnes de la vue Jour **sans « Valider »** (colonne **Total en gras**). Mêmes employés que l'écran : non-conducteurs, sous contrat à la date, sites cochés et dans le périmètre (lignes vides incluses). **Tri intra-site identique au calendrier** : `displayOrder` (ordre manuel), puis nom, puis prénom (cf. `compareEmployeesInSite` front). Calcul des cellules mutualisé via `YearlyHoursExportBuilder::buildDayRowsForEmployees` (Jour/Nuit/Total incluent crédit absence + crédit virtuel férié). Colonne **Statut = code** du type d'absence (`AbsenceType::getCode`, ex. `AT`) sur sa couleur de fond ; férié sans absence → nom du férié sur `#b3e5fc`. Chaque row porte `statut` (code), `statutLabel` (libellé, pour la légende) et `statutColor`. **Légende** sous le tableau (carré coloré contenant le code + libellé à droite), construite côté provider à partir des codes présents (hors férié, dédupliquée par code, triée). Gabarit `templates/work-hour-day-export/print.html.twig`.
|
- **Export heures vue Jour** (`WorkHourDayExportProvider`, endpoint `GET /work-hours/day-export?workDate=&siteIds=`, `ROLE_USER`) : bouton « Exporter » à droite du titre « Heures », **visible uniquement en vue Jour** (`v-if="(isAdmin || isSiteManager) && viewMode === 'day'"`, masqué en vue Semaine et pour `ROLE_SELF`). **Accessible aux admins ET aux chefs de site** : le périmètre est résolu côté backend via `EmployeeRepository::findScoped($user)` (admin → tous les sites, chef de site → ses sites uniquement, cf. `EmployeeScopeService`), donc un `siteIds` hors périmètre est ignoré ; le drawer front ne propose que les sites visibles (`sites` dérivé des employés scopés). PDF A4 portrait d'**une seule journée**, **regroupé par site**, colonnes de la vue Jour **sans « Valider »** (colonne **Total en gras**). Mêmes employés que l'écran : non-conducteurs, sous contrat à la date, sites cochés et dans le périmètre (lignes vides incluses). **Tri intra-site identique au calendrier** : `displayOrder` (ordre manuel), puis nom, puis prénom (cf. `compareEmployeesInSite` front). Calcul des cellules mutualisé via `YearlyHoursExportBuilder::buildDayRowsForEmployees` (Jour/Nuit/Total incluent crédit absence + crédit virtuel férié). Colonne **Statut = code** du type d'absence (`AbsenceType::getCode`, ex. `AT`) sur sa couleur de fond ; férié sans absence → nom du férié sur `#b3e5fc`. Chaque row porte `statut` (code), `statutLabel` (libellé, pour la légende) et `statutColor`. **Légende** sous le tableau (carré coloré contenant le code + libellé à droite), construite côté provider à partir des codes présents (hors férié, dédupliquée par code, triée). Gabarit `templates/work-hour-day-export/print.html.twig`.
|
||||||
|
- **Calendrier des jours validés (vue Jour)** (`WorkHourValidationStatusProvider`, ressource `WorkHourValidationStatus`, endpoint `GET /work-hours/validation-status?from=&to=[&driver=1]`, `ROLE_USER`) : en **vue Jour**, sur les **deux écrans** (Heures **et** Heures Conducteurs), le sélecteur de date est un `MalioDate` (layer `@malio/layer-ui >= 1.7.x` : prop `markedDates` + event `@month-change`) qui peint **en vert** (`markedDates` → `'success'`) les jours **entièrement validés**. **Définition** : un jour est vert ssi il porte ≥1 ligne `WorkHour` du scope ce jour-là **et** aucune n'est `isValid=false` — on se base sur la **seule** colonne `is_valid` (validation admin ; `isSiteValid` ignoré). Jour **sans aucune ligne** → neutre (jamais vert). **Périmètre complet** via `EmployeeRepository::findScoped` (admin = tous sites, chef de site = ses sites), **indépendant du filtre sites** de l'écran. **Scope conducteur inversé** par `?driver=1` : écran Heures → non-conducteurs (défaut), écran Heures Conducteurs → conducteurs (résolu par date via `EmployeeContractResolver::resolveIsDriverForEmployeeAndDate`, mémoïsé ; garde `if ($isDriver !== $driverOnly) continue`). Provider : une requête `WorkHourReadRepositoryInterface::findByDateRangeAndEmployees`, agrégation par jour (`total`/`pending`), plage bornée à 366 j. **Chargement à la volée par mois** (jamais préchargé) : `@month-change {month,year}` (à l'ouverture + nav) → fetch de la **grille visible** (lundi avant le 1er → dimanche après le dernier) → cache `validatedDaysByMonth` (`useHoursPage` / `useDriverHoursPage`, ce dernier passe `{ driver: true }` au service) → `markedDates` réactif. **Rafraîchissement** du mois en cache (`reloadValidationMonth`) après `toggleValidation`/`toggleValidationBulk`/`handleSave`/`refreshAfterAbsenceChange` (pas la validation site). La **vue Semaine** utilise un `MalioDateWeek` (sélecteur de semaine, v-model ISO week `YYYY-Www`, sans coloration). Le **stepper ‹ › du mode jour est remplacé** par le `MalioDate` (raccourcis Hier/Aujourd'hui/Demain conservés) ; `PeriodStepperPicker` reste un fallback de la vue Jour quand `showValidationCalendar` est absent (aucun appelant actuel). Activation par écran via la prop `showValidationCalendar` de `HoursToolbar` (les deux pages la passent à `true` + `markedDates` + `@month-change`). Alignement vertical de la ligne via `lg:items-center` (les champs Malio font `h-12` vs `h-10` des boutons). Doc complète : `doc/hours-validated-days.md`.
|
||||||
- **Export Contingent heures de nuit** (`NightHoursContingentPrintProvider`, endpoint `GET /night-hours-contingent/print?year=YYYY`, `ROLE_USER`) : option « Contingent H.nuit » du drawer Export de la liste employés. PDF **A4 paysage**, lignes = employés **groupés par site** et triés `displayOrder`/nom/prénom (comme le day-export), colonnes = 12 mois civils, chacun avec 2 sous-colonnes **H.nuit** et **N.jours**. Heures de nuit = minutes dans la fenêtre **21h→6h** via le service partagé `App\Service\WorkHours\NightHoursCalculator` (source unique mutualisée avec `WorkHourWeeklySummaryProvider`, `YearlyHoursExportBuilder`, `RttRecoveryComputationService` et `SalaryRecapPrintProvider`). Conducteurs inclus via `WorkHour.nightHoursMinutes`. **N.jours** = nb de jours où minutes de nuit ≥ 240 (4h). **Aucun crédit** absence/férié. Agrégation : `App\Service\WorkHours\NightContingentExportBuilder`. Gabarit `templates/night-hours-contingent/print.html.twig`.
|
- **Export Contingent heures de nuit** (`NightHoursContingentPrintProvider`, endpoint `GET /night-hours-contingent/print?year=YYYY`, `ROLE_USER`) : option « Contingent H.nuit » du drawer Export de la liste employés. PDF **A4 paysage**, lignes = employés **groupés par site** et triés `displayOrder`/nom/prénom (comme le day-export), colonnes = 12 mois civils, chacun avec 2 sous-colonnes **H.nuit** et **N.jours**. Heures de nuit = minutes dans la fenêtre **21h→6h** via le service partagé `App\Service\WorkHours\NightHoursCalculator` (source unique mutualisée avec `WorkHourWeeklySummaryProvider`, `YearlyHoursExportBuilder`, `RttRecoveryComputationService` et `SalaryRecapPrintProvider`). Conducteurs inclus via `WorkHour.nightHoursMinutes`. **N.jours** = nb de jours où minutes de nuit ≥ 240 (4h). **Aucun crédit** absence/férié. Agrégation : `App\Service\WorkHours\NightContingentExportBuilder`. Gabarit `templates/night-hours-contingent/print.html.twig`.
|
||||||
- **Écran Calendrier** : un employé est affiché uniquement si au moins une de ses périodes de contrat (`employee.contractHistory`) intersecte le mois affiché (`[1er ; dernier jour]`). Filtre côté frontend dans `visibleEmployees` (`pages/calendar.vue`). **L'impression PDF des absences applique le même filtre** côté backend (`AbsencePrintProvider::hasContractInRange` sur la période `[from, to]`) : un salarié parti en avril n'apparaît pas sur une impression de mai. **Le récap salaire applique le même filtre** (`SalaryRecapPrintProvider::hasContractInRange` sur le mois imprimé) : un salarié sans contrat sur le mois (ex. parti en février) n'apparaît pas sur le récap de juin.
|
- **Écran Calendrier** : un employé est affiché uniquement si au moins une de ses périodes de contrat (`employee.contractHistory`) intersecte le mois affiché (`[1er ; dernier jour]`). Filtre côté frontend dans `visibleEmployees` (`pages/calendar.vue`). **L'impression PDF des absences applique le même filtre** côté backend (`AbsencePrintProvider::hasContractInRange` sur la période `[from, to]`) : un salarié parti en avril n'apparaît pas sur une impression de mai. **Le récap salaire applique le même filtre** (`SalaryRecapPrintProvider::hasContractInRange` sur le mois imprimé) : un salarié sans contrat sur le mois (ex. parti en février) n'apparaît pas sur le récap de juin.
|
||||||
- **Planning jours travaillés** (`EmployeeContractPeriod.workDaysHours` : JSON `{iso_day: minutes}`) : obligatoire pour tout contrat TIME **hors 35h/39h/INTERIM** (ex. 4h, 25h, 28h). Somme = `weeklyHours × 60`. Utilisé par `HolidayVirtualHoursResolver` (crédit férié) et `WorkedHoursCreditPolicy` (crédit absence) pour ne créditer que les jours effectivement travaillés. Validation : `EmployeeContractPeriodValidator::assertWorkDaysHours`.
|
- **Planning jours travaillés** (`EmployeeContractPeriod.workDaysHours` : JSON `{iso_day: minutes}`) : obligatoire pour tout contrat TIME **hors 35h/39h/INTERIM** (ex. 4h, 25h, 28h). Somme = `weeklyHours × 60`. Utilisé par `HolidayVirtualHoursResolver` (crédit férié) et `WorkedHoursCreditPolicy` (crédit absence) pour ne créditer que les jours effectivement travaillés. Validation : `EmployeeContractPeriodValidator::assertWorkDaysHours`.
|
||||||
@@ -187,11 +188,15 @@
|
|||||||
- Rows: `grid items-center gap-4 border-b border-primary-500 px-6 py-3 text-md font-bold text-primary-500 last:border-b-0 cursor-pointer hover:bg-tertiary-500`
|
- Rows: `grid items-center gap-4 border-b border-primary-500 px-6 py-3 text-md font-bold text-primary-500 last:border-b-0 cursor-pointer hover:bg-tertiary-500`
|
||||||
- Page wrapper for scroll: `h-full flex flex-col overflow-hidden`, table container: `min-h-0 overflow-auto rounded-md bg-white`
|
- Page wrapper for scroll: `h-full flex flex-col overflow-hidden`, table container: `min-h-0 overflow-auto rounded-md bg-white`
|
||||||
|
|
||||||
### Drawer buttons (AppDrawer)
|
### Drawers (MalioDrawer)
|
||||||
- Edit mode: `grid grid-cols-2 gap-3` → Supprimer (red, left) + Modifier (primary, right)
|
- **Tous les drawers utilisent `MalioDrawer`** (couche Malio, auto-importé). L'ancien composant custom `AppDrawer` a été supprimé — ne pas le réintroduire.
|
||||||
- Create mode: centered `+ Ajouter` button, w-[200px]
|
- **Titre via le slot `#header`** (MalioDrawer n'a PAS de prop `title`) : `<template #header><h2 class="text-[32px] font-semibold text-primary-500">…</h2></template>`.
|
||||||
- Exception: Users drawer has NO delete button
|
- `v-model` = ouverture ; bouton de fermeture + clic overlay/Échap gérés par MalioDrawer (`showClose`/`dismissable`/`closeOnEscape` défaut `true`). Largeur `max-w-md`.
|
||||||
- All "Ajouter" buttons across the app use "+" prefix
|
- **Boutons d'action = `MalioButton`** (dans le slot par défaut ; plus de `<button>` natif). `MalioButton` rend un `type="button"` (ne soumet pas) → câbler `@click="<handler de submit>"` (= la fonction du `@submit.prevent` du form, conservé pour la touche Entrée). Delete → `variant="danger"`, annuler → `variant="tertiary"`.
|
||||||
|
- **Deux boutons côte à côte partagent l'espace** : `<div class="grid grid-cols-2 gap-3 pt-2">` + chaque `MalioButton` en `button-class="w-full"` (edit : Supprimer/Annuler à gauche + Modifier/Enregistrer à droite). Cas conditionnel (un des deux en `v-if`) : `flex gap-3` + `button-class="flex-1"` (1 visible → pleine largeur, 2 → moitiés).
|
||||||
|
- **Un seul bouton** : centré `flex justify-center pt-2` (largeur `w-[200px]` ou défaut). Bouton de création : `label="Ajouter…"` + `icon-name="mdi:plus"` (plus de préfixe texte « + »).
|
||||||
|
- Exception: Users drawer has NO delete button.
|
||||||
|
- NB : quelques `MalioButton` historiques soumettent encore via `type="submit"` (passthrough d'attribut) au lieu de `@click` (sites/users/absence-types) — fonctionnel, à aligner sur `@click` à l'occasion.
|
||||||
|
|
||||||
### API Platform (backend)
|
### API Platform (backend)
|
||||||
- Custom operations use Processor (write) / Provider (read)
|
- Custom operations use Processor (write) / Provider (read)
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.120'
|
app.version: '0.1.121'
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Calendrier des jours validés (écran Heures — vue Jour)
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Sur l'écran **Heures**, en **vue Jour**, le sélecteur de date est un calendrier
|
||||||
|
(composant `MalioDate` du layer `@malio/layer-ui`) qui **peint en vert** les jours
|
||||||
|
entièrement validés par un admin. La RH repère ainsi d'un coup d'œil les jours où
|
||||||
|
il reste de la validation à faire.
|
||||||
|
|
||||||
|
## Définition « jour validé » (vert)
|
||||||
|
Un jour est vert ssi, dans le **périmètre complet** de l'utilisateur :
|
||||||
|
- il porte **au moins une ligne `WorkHour`** dans le scope ciblé ce jour-là, **et**
|
||||||
|
- **aucune** de ces lignes n'est en attente de validation (`isValid = false`).
|
||||||
|
|
||||||
|
La même mécanique sert les **deux écrans**, avec un scope opposé : écran **Heures** →
|
||||||
|
non-conducteurs (défaut) ; écran **Heures Conducteurs** → conducteurs (`?driver=1`).
|
||||||
|
|
||||||
|
Conséquences :
|
||||||
|
- Un jour **sans aucune ligne** (rien saisi, ex. week-end, jour futur) reste **neutre**
|
||||||
|
(jamais vert) — « rien fait » n'est pas « tout validé ».
|
||||||
|
- On se base sur la **seule** colonne `work_hours.is_valid` (validation admin/RH).
|
||||||
|
`isSiteValid` (chef de site) n'entre pas en compte → modifier une validation site
|
||||||
|
ne change pas la couleur.
|
||||||
|
- **Scope conducteur** : écran Heures → conducteurs exclus ; écran Heures Conducteurs →
|
||||||
|
seuls les conducteurs (le filtre est inversé via `?driver=1`).
|
||||||
|
|
||||||
|
## Périmètre
|
||||||
|
- `ROLE_ADMIN` → tous les employés / tous les sites.
|
||||||
|
- Chef de site → ses sites uniquement.
|
||||||
|
- Le **filtre sites de l'écran est volontairement ignoré** : le vert reflète tout le
|
||||||
|
périmètre (objectif : repérer le moindre jour incomplet, où qu'il soit). Changer le
|
||||||
|
filtre sites de la vue Jour ne recalcule pas le calendrier.
|
||||||
|
|
||||||
|
## Chargement des données
|
||||||
|
- Endpoint : `GET /work-hours/validation-status?from=YYYY-MM-DD&to=YYYY-MM-DD[&driver=1]`
|
||||||
|
(`ROLE_USER`). Réponse : `{ from, to, validatedDays: string[] }` (dates `Y-m-d`).
|
||||||
|
- Provider : `App\State\WorkHourValidationStatusProvider`
|
||||||
|
(ressource `App\ApiResource\WorkHourValidationStatus`).
|
||||||
|
- `EmployeeRepository::findScoped($user)` pour le périmètre (ignore tout `siteIds`).
|
||||||
|
- Une requête `WorkHourReadRepositoryInterface::findByDateRangeAndEmployees`.
|
||||||
|
- Filtrage conducteur **par date** via `EmployeeContractResolver::resolveIsDriverForEmployeeAndDate`
|
||||||
|
(mémoïsé par couple employé/jour) : `if ($isDriver !== $driverOnly) continue;`
|
||||||
|
(`driverOnly` = `?driver=1`).
|
||||||
|
- Agrégation par jour : vert ⇔ `total > 0` (lignes du scope) et `pending = 0`
|
||||||
|
(aucune `isValid=false`).
|
||||||
|
- Garde-fou : plage bornée à 366 jours.
|
||||||
|
- Le chargement est **à la volée par mois affiché** (jamais préchargé sur plusieurs
|
||||||
|
années) : `MalioDate` émet `@month-change { month, year }` à l'ouverture du popover et
|
||||||
|
à chaque navigation ; le front fetch la **grille visible** (lundi avant le 1er →
|
||||||
|
dimanche après le dernier jour, pour colorer aussi les jours débordants) et met le
|
||||||
|
résultat en cache par mois (`useHoursPage` / `useDriverHoursPage` → `validatedDaysByMonth` ;
|
||||||
|
ce dernier appelle le service avec `{ driver: true }`). La prop réactive `markedDates`
|
||||||
|
(ISO → `'success'`) recolore la grille.
|
||||||
|
|
||||||
|
## Rafraîchissement
|
||||||
|
Toute action qui touche la validation d'un jour recharge le mois concerné s'il est en
|
||||||
|
cache (`reloadValidationMonth`), donc le calendrier se recolore aussitôt :
|
||||||
|
- validation admin d'une ligne (`toggleValidation`) ou en masse (`toggleValidationBulk`) ;
|
||||||
|
- sauvegarde d'heures (`handleSave`) — toute modification réelle remet `isValid=false` ;
|
||||||
|
- création / suppression d'absence (`refreshAfterAbsenceChange`).
|
||||||
|
La validation **site** ne déclenche pas de rechargement (sans effet sur le vert).
|
||||||
|
|
||||||
|
## Périmètre d'affichage
|
||||||
|
- **Vue Jour uniquement** : le vert (calendrier `MalioDate` + `markedDates`) est à la maille
|
||||||
|
jour, sur les **deux écrans** (Heures et Heures Conducteurs, via `showValidationCalendar`).
|
||||||
|
La **vue Semaine** utilise un `MalioDateWeek` (sélecteur de semaine, sans coloration).
|
||||||
|
Le `PeriodStepperPicker` ne subsiste que comme fallback de la vue Jour quand
|
||||||
|
`showValidationCalendar` est absent (aucun appelant actuel, conservé par flexibilité).
|
||||||
|
- Précédence d'affichage dans la grille (côté layer) : sélection (fond plein primary) >
|
||||||
|
variante marquée ; le **jour courant** (`today`) garde sa bordure **et** reçoit le fond
|
||||||
|
vert s'il est validé.
|
||||||
|
|
||||||
|
## Dépendance layer
|
||||||
|
Nécessite `@malio/layer-ui >= 1.7.x` : prop `markedDates`
|
||||||
|
(`Record<"YYYY-MM-DD", 'success' | 'danger'>`) + event `month-change` sur `MalioDate`
|
||||||
|
(ticket Malio UI MUI-45).
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="drawerOpen" title="Nouvelle absence">
|
<MalioDrawer v-model="drawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Nouvelle absence</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="absenceForm.employeeId === '' ? null : absenceForm.employeeId"
|
:model-value="absenceForm.employeeId === '' ? null : absenceForm.employeeId"
|
||||||
:options="employeeOptions"
|
:options="employeeOptions"
|
||||||
label="Employé *"
|
label="Employé *"
|
||||||
@@ -12,7 +15,7 @@
|
|||||||
@update:model-value="onEmployeeChange"
|
@update:model-value="onEmployeeChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="absenceForm.typeId === '' ? null : absenceForm.typeId"
|
:model-value="absenceForm.typeId === '' ? null : absenceForm.typeId"
|
||||||
:options="typeOptions"
|
:options="typeOptions"
|
||||||
label="Type d'absence *"
|
label="Type d'absence *"
|
||||||
@@ -24,16 +27,16 @@
|
|||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="start-date">Début</label>
|
<label class="text-md font-semibold text-neutral-700">Début</label>
|
||||||
<div class="mt-2 grid grid-cols-2 gap-2">
|
<div class="mt-2 space-y-2">
|
||||||
<input
|
<MalioDate
|
||||||
id="start-date"
|
|
||||||
v-model="absenceForm.startDate"
|
v-model="absenceForm.startDate"
|
||||||
type="date"
|
:clearable="false"
|
||||||
:class="[dateInputBaseClass, absenceForm.startDate ? 'border-black' : 'border-m-muted']"
|
:reserve-message-space="false"
|
||||||
:disabled="props.lockDates"
|
:disabled="props.lockDates"
|
||||||
|
group-class="w-full"
|
||||||
/>
|
/>
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="absenceForm.startHalf"
|
:model-value="absenceForm.startHalf"
|
||||||
:options="halfDayOptions"
|
:options="halfDayOptions"
|
||||||
min-width=""
|
min-width=""
|
||||||
@@ -42,16 +45,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="end-date">Fin</label>
|
<label class="text-md font-semibold text-neutral-700">Fin</label>
|
||||||
<div class="mt-2 grid grid-cols-2 gap-2">
|
<div class="mt-2 space-y-2">
|
||||||
<input
|
<MalioDate
|
||||||
id="end-date"
|
|
||||||
v-model="absenceForm.endDate"
|
v-model="absenceForm.endDate"
|
||||||
type="date"
|
:clearable="false"
|
||||||
:class="[dateInputBaseClass, absenceForm.endDate ? 'border-black' : 'border-m-muted']"
|
:reserve-message-space="false"
|
||||||
:disabled="props.lockDates"
|
:disabled="props.lockDates"
|
||||||
|
group-class="w-full"
|
||||||
/>
|
/>
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="absenceForm.endHalf"
|
:model-value="absenceForm.endHalf"
|
||||||
:options="halfDayOptions"
|
:options="halfDayOptions"
|
||||||
min-width=""
|
min-width=""
|
||||||
@@ -72,31 +75,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="editingAbsence" class="grid grid-cols-2 gap-3 pt-2">
|
<div v-if="editingAbsence" class="grid grid-cols-2 gap-3 pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
label="Supprimer"
|
||||||
class="flex items-center justify-center rounded-md bg-red-500 px-4 py-2 text-md font-semibold text-white hover:bg-red-600"
|
variant="danger"
|
||||||
|
button-class="w-full"
|
||||||
@click="handleDelete"
|
@click="handleDelete"
|
||||||
>
|
/>
|
||||||
Supprimer
|
<MalioButton
|
||||||
</button>
|
label="Modifier"
|
||||||
<button
|
variant="primary"
|
||||||
type="submit"
|
button-class="w-full"
|
||||||
class="flex items-center justify-center rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
:disabled="props.isSubmitting || !isFormValid"
|
||||||
:class="submitButtonClass"
|
@click="handleSubmit"
|
||||||
>
|
/>
|
||||||
Modifier
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex justify-center pt-2">
|
<div v-else class="flex justify-center pt-2">
|
||||||
<MalioButton
|
<MalioButton
|
||||||
type="submit"
|
|
||||||
label="Valider"
|
label="Valider"
|
||||||
button-class="w-[200px]"
|
button-class="w-[200px]"
|
||||||
:disabled="props.isSubmitting || !isFormValid"
|
:disabled="props.isSubmitting || !isFormValid"
|
||||||
|
@click="handleSubmit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -106,7 +108,6 @@ import type { AbsenceType } from '~/services/dto/absence-type'
|
|||||||
import type { Absence } from '~/services/dto/absence'
|
import type { Absence } from '~/services/dto/absence'
|
||||||
import type { HalfDay } from '~/services/dto/half-day'
|
import type { HalfDay } from '~/services/dto/half-day'
|
||||||
import { HALF_DAYS } from '~/services/dto/half-day'
|
import { HALF_DAYS } from '~/services/dto/half-day'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -159,13 +160,6 @@ const showTypeError = computed(
|
|||||||
() => validationTouched.type && !isTypeValid.value
|
() => validationTouched.type && !isTypeValid.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const submitButtonClass = computed(() => {
|
|
||||||
if (props.isSubmitting || !isFormValid.value) {
|
|
||||||
return 'opacity-50 cursor-not-allowed'
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const employeeOptions = computed(() =>
|
const employeeOptions = computed(() =>
|
||||||
props.employees.map((e) => ({ label: `${e.firstName} ${e.lastName}`, value: e.id }))
|
props.employees.map((e) => ({ label: `${e.firstName} ${e.lastName}`, value: e.id }))
|
||||||
)
|
)
|
||||||
@@ -174,9 +168,6 @@ const typeOptions = computed(() =>
|
|||||||
)
|
)
|
||||||
const halfDayOptions = HALF_DAYS.map((h) => ({ label: h.label, value: h.value }))
|
const halfDayOptions = HALF_DAYS.map((h) => ({ label: h.label, value: h.value }))
|
||||||
|
|
||||||
const dateInputBaseClass =
|
|
||||||
'h-10 w-full rounded-md border px-3 text-md text-black outline-none focus:border-2 focus:border-m-primary'
|
|
||||||
|
|
||||||
const onEmployeeChange = (value: string | number | null) => {
|
const onEmployeeChange = (value: string | number | null) => {
|
||||||
absenceForm.value.employeeId = value === null ? '' : Number(value)
|
absenceForm.value.employeeId = value === null ? '' : Number(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="drawerOpen" title="Imprimer les absences">
|
<MalioDrawer v-model="drawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Imprimer les absences</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<MalioDate
|
||||||
<div>
|
v-model="printForm.from"
|
||||||
<label class="text-md font-semibold text-neutral-700" for="print-from">
|
label="Date de début"
|
||||||
Date de début <span class="text-red-600">*</span>
|
required
|
||||||
</label>
|
:clearable="false"
|
||||||
<input
|
:reserve-message-space="false"
|
||||||
id="print-from"
|
:error="showFromError ? 'La date de début est obligatoire.' : ''"
|
||||||
v-model="printForm.from"
|
group-class="w-full"
|
||||||
type="date"
|
/>
|
||||||
:class="fromFieldClass"
|
|
||||||
/>
|
<MalioDate
|
||||||
<p v-if="showFromError" class="mt-1 text-sm text-red-600">
|
v-model="printForm.to"
|
||||||
La date de début est obligatoire.
|
label="Date de fin"
|
||||||
</p>
|
required
|
||||||
</div>
|
:clearable="false"
|
||||||
<div>
|
:reserve-message-space="false"
|
||||||
<label class="text-md font-semibold text-neutral-700" for="print-to">
|
:error="showToError ? 'La date de fin est obligatoire.' : ''"
|
||||||
Date de fin <span class="text-red-600">*</span>
|
group-class="w-full"
|
||||||
</label>
|
/>
|
||||||
<input
|
|
||||||
id="print-to"
|
|
||||||
v-model="printForm.to"
|
|
||||||
type="date"
|
|
||||||
:class="toFieldClass"
|
|
||||||
/>
|
|
||||||
<p v-if="showToError" class="mt-1 text-sm text-red-600">
|
|
||||||
La date de fin est obligatoire.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="text-md font-semibold text-neutral-700">
|
<p class="text-md font-semibold text-neutral-700">
|
||||||
@@ -97,21 +89,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center pt-2">
|
<div class="flex justify-center pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Imprimer"
|
||||||
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
variant="primary"
|
||||||
:class="submitButtonClass"
|
:button-class="submitButtonClass"
|
||||||
>
|
@click="handleSubmit"
|
||||||
Imprimer
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, toRef, watch } from 'vue'
|
import { computed, reactive, toRef, watch } from 'vue'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
|
|
||||||
type SiteOption = {
|
type SiteOption = {
|
||||||
id: number
|
id: number
|
||||||
@@ -190,21 +180,6 @@ const showSitesError = computed(() => validationTouched.sites && !isSitesValid.v
|
|||||||
const showContractNaturesError = computed(() => validationTouched.contractNatures && !isContractNaturesValid.value)
|
const showContractNaturesError = computed(() => validationTouched.contractNatures && !isContractNaturesValid.value)
|
||||||
const showWorkContractsError = computed(() => validationTouched.workContracts && !isWorkContractsValid.value)
|
const showWorkContractsError = computed(() => validationTouched.workContracts && !isWorkContractsValid.value)
|
||||||
|
|
||||||
const baseInputClass =
|
|
||||||
'mt-2 w-full rounded-md border px-3 py-2 text-md text-neutral-900'
|
|
||||||
const fromFieldClass = computed(() => {
|
|
||||||
if (showFromError.value) {
|
|
||||||
return `${baseInputClass} border-red-500`
|
|
||||||
}
|
|
||||||
return `${baseInputClass} border-neutral-300`
|
|
||||||
})
|
|
||||||
const toFieldClass = computed(() => {
|
|
||||||
if (showToError.value) {
|
|
||||||
return `${baseInputClass} border-red-500`
|
|
||||||
}
|
|
||||||
return `${baseInputClass} border-neutral-300`
|
|
||||||
})
|
|
||||||
|
|
||||||
const submitButtonClass = computed(() => {
|
const submitButtonClass = computed(() => {
|
||||||
if (!isFormValid.value) {
|
if (!isFormValid.value) {
|
||||||
return 'opacity-50 cursor-not-allowed'
|
return 'opacity-50 cursor-not-allowed'
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="modelValue" class="fixed inset-0 z-[60]">
|
|
||||||
<Transition name="drawer-backdrop">
|
|
||||||
<div class="absolute inset-0 bg-black/40" @click="close" />
|
|
||||||
</Transition>
|
|
||||||
<Transition name="drawer-panel">
|
|
||||||
<div class="absolute right-0 top-0 h-full w-full max-w-md bg-white shadow-xl flex flex-col">
|
|
||||||
<div class="shrink-0 flex items-center justify-between px-[20px] pt-8 pb-8">
|
|
||||||
<h2 class="text-[32px] font-semibold text-primary-500">
|
|
||||||
{{ title }}
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-md p-1 text-primary-500 hover:text-secondary-500"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:close" size="24"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto px-[20px] pb-4 pt-1">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ modelValue: boolean; title?: string }>()
|
|
||||||
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
|
||||||
|
|
||||||
const close = () => emit('update:modelValue', false)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.drawer-backdrop-enter-active,
|
|
||||||
.drawer-backdrop-leave-active {
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-backdrop-enter-from,
|
|
||||||
.drawer-backdrop-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-panel-enter-active,
|
|
||||||
.drawer-panel-leave-active {
|
|
||||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer-panel-enter-from,
|
|
||||||
.drawer-panel-leave-to {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="drawerOpen" title="Export heures (tous les employés)">
|
<MalioDrawer v-model="drawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Export heures (tous les employés)</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="bulk-yearly-hours-year">
|
<label class="text-md font-semibold text-neutral-700" for="bulk-yearly-hours-year">
|
||||||
@@ -29,26 +32,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center pt-2">
|
<div class="flex justify-center pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
:label="isLoading ? 'Génération en cours...' : 'Imprimer'"
|
||||||
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
button-class="w-[200px]"
|
||||||
:disabled="isLoading || selectedMonth === ''"
|
:disabled="isLoading || selectedMonth === ''"
|
||||||
>
|
@click="handleSubmit"
|
||||||
<template v-if="isLoading">
|
/>
|
||||||
Génération en cours...
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
Imprimer
|
|
||||||
</template>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="drawerOpen" title="Export heures">
|
<MalioDrawer v-model="drawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Export heures</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="yearly-hours-year">
|
<label class="text-md font-semibold text-neutral-700" for="yearly-hours-year">
|
||||||
@@ -29,20 +32,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center pt-2">
|
<div class="flex justify-center pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Imprimer"
|
||||||
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
button-class="w-[200px]"
|
||||||
>
|
@click="handleSubmit"
|
||||||
Imprimer
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="drawerOpen" title="Récapitulatif Salaire">
|
<MalioDrawer v-model="drawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Récapitulatif Salaire</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="salary-recap-month">
|
<label class="text-md font-semibold text-neutral-700" for="salary-recap-month">
|
||||||
@@ -17,21 +20,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center pt-2">
|
<div class="flex justify-center pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Imprimer"
|
||||||
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
button-class="w-[200px]"
|
||||||
:class="submitButtonClass"
|
@click="handleSubmit"
|
||||||
>
|
/>
|
||||||
Imprimer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -63,13 +63,6 @@ const monthFieldClass = computed(() => {
|
|||||||
return `${baseInputClass} border-neutral-300`
|
return `${baseInputClass} border-neutral-300`
|
||||||
})
|
})
|
||||||
|
|
||||||
const submitButtonClass = computed(() => {
|
|
||||||
if (!isMonthValid.value) {
|
|
||||||
return 'opacity-50 cursor-not-allowed'
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
validationTouched.value = true
|
validationTouched.value = true
|
||||||
if (!isMonthValid.value) return
|
if (!isMonthValid.value) return
|
||||||
|
|||||||
@@ -33,7 +33,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppDrawer v-model="isDrawerOpen" :title="isEditing ? 'Modification prime' : 'Nouvelle prime'">
|
<MalioDrawer v-model="isDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">{{ isEditing ? 'Modification prime' : 'Nouvelle prime' }}</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="onSubmit">
|
<form class="space-y-4" @submit.prevent="onSubmit">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="bonus-month">
|
<label class="text-md font-semibold text-neutral-700" for="bonus-month">
|
||||||
@@ -75,38 +78,38 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
|
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
label="Supprimer"
|
||||||
class="flex items-center justify-center rounded-md bg-red-500 px-4 py-2 text-md font-semibold text-white hover:bg-red-600"
|
variant="danger"
|
||||||
|
button-class="w-full"
|
||||||
@click="onDelete"
|
@click="onDelete"
|
||||||
>
|
/>
|
||||||
Supprimer
|
<MalioButton
|
||||||
</button>
|
label="Modifier"
|
||||||
<button
|
variant="primary"
|
||||||
type="submit"
|
button-class="w-full"
|
||||||
class="flex items-center justify-center rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
>
|
@click="onSubmit"
|
||||||
Modifier
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex justify-center pt-2">
|
<div v-else class="flex justify-center pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Ajouter"
|
||||||
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
variant="primary"
|
||||||
|
button-class="w-[200px]"
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
>
|
@click="onSubmit"
|
||||||
+ Ajouter
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Bonus } from '~/services/dto/bonus'
|
import type { Bonus } from '~/services/dto/bonus'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
bonuses: Bonus[]
|
bonuses: Bonus[]
|
||||||
|
|||||||
@@ -43,7 +43,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppDrawer :model-value="isContractDrawerOpen" title="Modifier le contrat" @update:model-value="onUpdateContractDrawerOpen">
|
<MalioDrawer :model-value="isContractDrawerOpen" @update:model-value="onUpdateContractDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Modifier le contrat</h2>
|
||||||
|
</template>
|
||||||
<div class="mb-4 flex border-b border-neutral-200">
|
<div class="mb-4 flex border-b border-neutral-200">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -141,13 +144,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center pt-2">
|
<div class="flex justify-center pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Modifier"
|
||||||
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
button-class="w-[200px]"
|
||||||
:disabled="isContractSubmitting || !isContractEndDateValid"
|
:disabled="isContractSubmitting || !isContractEndDateValid"
|
||||||
>
|
@click="onSubmitCloseContract"
|
||||||
Modifier
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,27 +190,29 @@
|
|||||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
:label="form.id ? 'Modifier' : 'Ajouter'"
|
||||||
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-60"
|
button-class="w-full"
|
||||||
:disabled="!form.startDate || isSuspensionSubmitting"
|
:disabled="!form.startDate || isSuspensionSubmitting"
|
||||||
@click="onSubmitSuspension(index)"
|
@click="onSubmitSuspension(index)"
|
||||||
>
|
/>
|
||||||
{{ form.id ? 'Modifier' : 'Ajouter' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
label="Ajouter une suspension"
|
||||||
class="w-full rounded-md border-2 border-dashed border-primary-500/50 px-4 py-3 text-base font-semibold text-primary-500/50 transition hover:border-primary-500 hover:text-primary-500"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
variant="tertiary"
|
||||||
|
button-class="w-full"
|
||||||
@click="onAddSuspensionForm"
|
@click="onAddSuspensionForm"
|
||||||
>
|
/>
|
||||||
+ Ajouter une suspension
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
|
|
||||||
<AppDrawer :model-value="isCreateContractDrawerOpen" title="Ajouter un contrat" @update:model-value="onUpdateCreateContractDrawerOpen">
|
<MalioDrawer :model-value="isCreateContractDrawerOpen" @update:model-value="onUpdateCreateContractDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Ajouter un contrat</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="onSubmitCreateContract">
|
<form class="space-y-4" @submit.prevent="onSubmitCreateContract">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="create-contract-nature">
|
<label class="text-md font-semibold text-neutral-700" for="create-contract-nature">
|
||||||
@@ -282,16 +286,17 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="sticky bottom-0 -mx-[20px] bg-white px-[20px] py-3 flex justify-center">
|
<div class="sticky bottom-0 -mx-[20px] bg-white px-[20px] py-3 flex justify-center">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Ajouter"
|
||||||
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
button-class="w-[200px]"
|
||||||
:disabled="isCreateContractSubmitting || !isCreateContractFormValid"
|
:disabled="isCreateContractSubmitting || !isCreateContractFormValid"
|
||||||
>
|
@click="onSubmitCreateContract"
|
||||||
+ Ajouter
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppDrawer v-model="isDrawerOpen" title="Formation">
|
<MalioDrawer v-model="isDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Formation</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="onSubmit">
|
<form class="space-y-4" @submit.prevent="onSubmit">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="formation-start-date">
|
<label class="text-md font-semibold text-neutral-700" for="formation-start-date">
|
||||||
@@ -107,39 +110,39 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
|
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
label="Supprimer"
|
||||||
class="flex items-center justify-center rounded-md bg-red-500 px-4 py-2 text-md font-semibold text-white hover:bg-red-600"
|
variant="danger"
|
||||||
|
button-class="w-full"
|
||||||
@click="onDelete"
|
@click="onDelete"
|
||||||
>
|
/>
|
||||||
Supprimer
|
<MalioButton
|
||||||
</button>
|
label="Modifier"
|
||||||
<button
|
variant="primary"
|
||||||
type="submit"
|
button-class="w-full"
|
||||||
class="flex items-center justify-center rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
>
|
@click="onSubmit"
|
||||||
Modifier
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex justify-center pt-2">
|
<div v-else class="flex justify-center pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Ajouter"
|
||||||
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
variant="primary"
|
||||||
|
button-class="w-[200px]"
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
>
|
@click="onSubmit"
|
||||||
+ Ajouter
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {Formation} from '~/services/dto/formation'
|
import type {Formation} from '~/services/dto/formation'
|
||||||
import {getFormationJustificatifUrl} from '~/services/formations'
|
import {getFormationJustificatifUrl} from '~/services/formations'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
formations: Formation[]
|
formations: Formation[]
|
||||||
|
|||||||
@@ -111,7 +111,10 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AppDrawer v-model="isFractionedDrawerOpen" title="Jours fractionnés">
|
<MalioDrawer v-model="isFractionedDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Jours fractionnés</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmitFractioned">
|
<form class="space-y-4" @submit.prevent="handleSubmitFractioned">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="fractioned-days">
|
<label class="text-md font-semibold text-neutral-700" for="fractioned-days">
|
||||||
@@ -127,24 +130,25 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end gap-3 pt-2">
|
<div class="grid grid-cols-2 gap-3 pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
label="Annuler"
|
||||||
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
variant="tertiary"
|
||||||
|
button-class="w-full"
|
||||||
@click="isFractionedDrawerOpen = false"
|
@click="isFractionedDrawerOpen = false"
|
||||||
>
|
/>
|
||||||
Annuler
|
<MalioButton
|
||||||
</button>
|
label="Enregistrer"
|
||||||
<button
|
button-class="w-full"
|
||||||
type="submit"
|
@click="handleSubmitFractioned"
|
||||||
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
/>
|
||||||
>
|
|
||||||
Enregistrer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
<AppDrawer v-model="isPaidLeaveDrawerOpen" title="Congés N-1 payés">
|
<MalioDrawer v-model="isPaidLeaveDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Congés N-1 payés</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmitPaidLeave">
|
<form class="space-y-4" @submit.prevent="handleSubmitPaidLeave">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="paid-leave-days">
|
<label class="text-md font-semibold text-neutral-700" for="paid-leave-days">
|
||||||
@@ -160,23 +164,21 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end gap-3 pt-2">
|
<div class="grid grid-cols-2 gap-3 pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
label="Annuler"
|
||||||
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
variant="tertiary"
|
||||||
|
button-class="w-full"
|
||||||
@click="isPaidLeaveDrawerOpen = false"
|
@click="isPaidLeaveDrawerOpen = false"
|
||||||
>
|
/>
|
||||||
Annuler
|
<MalioButton
|
||||||
</button>
|
label="Enregistrer"
|
||||||
<button
|
button-class="w-full"
|
||||||
type="submit"
|
@click="handleSubmitPaidLeave"
|
||||||
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
/>
|
||||||
>
|
|
||||||
Enregistrer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -184,7 +186,6 @@
|
|||||||
import type {Absence} from '~/services/dto/absence'
|
import type {Absence} from '~/services/dto/absence'
|
||||||
import type {EmployeeLeaveSummary} from '~/services/dto/employee-leave-summary'
|
import type {EmployeeLeaveSummary} from '~/services/dto/employee-leave-summary'
|
||||||
import {normalizeDate, toYmd} from '~/utils/date'
|
import {normalizeDate, toYmd} from '~/utils/date'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
|
|
||||||
type DayLeaveState = {
|
type DayLeaveState = {
|
||||||
am: boolean
|
am: boolean
|
||||||
|
|||||||
@@ -64,7 +64,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<AppDrawer v-model="isDrawerOpen" title="Frais">
|
<MalioDrawer v-model="isDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Frais</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="onSubmit">
|
<form class="space-y-4" @submit.prevent="onSubmit">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="mileage-month">
|
<label class="text-md font-semibold text-neutral-700" for="mileage-month">
|
||||||
@@ -157,39 +160,39 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
|
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
label="Supprimer"
|
||||||
class="flex items-center justify-center rounded-md bg-red-500 px-4 py-2 text-md font-semibold text-white hover:bg-red-600"
|
variant="danger"
|
||||||
|
button-class="w-full"
|
||||||
@click="onDelete"
|
@click="onDelete"
|
||||||
>
|
/>
|
||||||
Supprimer
|
<MalioButton
|
||||||
</button>
|
label="Modifier"
|
||||||
<button
|
variant="primary"
|
||||||
type="submit"
|
button-class="w-full"
|
||||||
class="flex items-center justify-center rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
>
|
@click="onSubmit"
|
||||||
Modifier
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex justify-center pt-2">
|
<div v-else class="flex justify-center pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Ajouter"
|
||||||
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
variant="primary"
|
||||||
|
button-class="w-[200px]"
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
>
|
@click="onSubmit"
|
||||||
+ Ajouter
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {MileageAllowance} from '~/services/dto/mileage-allowance'
|
import type {MileageAllowance} from '~/services/dto/mileage-allowance'
|
||||||
import {getKmReceiptUrl, getAmountReceiptUrl} from '~/services/mileage-allowances'
|
import {getKmReceiptUrl, getAmountReceiptUrl} from '~/services/mileage-allowances'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
allowances: MileageAllowance[]
|
allowances: MileageAllowance[]
|
||||||
|
|||||||
@@ -31,7 +31,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppDrawer v-model="isDrawerOpen" :title="isEditing ? 'Modification observation' : 'Nouvelle observation'">
|
<MalioDrawer v-model="isDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">{{ isEditing ? 'Modification observation' : 'Nouvelle observation' }}</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="onSubmit">
|
<form class="space-y-4" @submit.prevent="onSubmit">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="observation-month">
|
<label class="text-md font-semibold text-neutral-700" for="observation-month">
|
||||||
@@ -59,38 +62,38 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
|
<div v-if="isEditing" class="grid grid-cols-2 gap-3 pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
label="Supprimer"
|
||||||
class="flex items-center justify-center rounded-md bg-red-500 px-4 py-2 text-md font-semibold text-white hover:bg-red-600"
|
variant="danger"
|
||||||
|
button-class="w-full"
|
||||||
@click="onDelete"
|
@click="onDelete"
|
||||||
>
|
/>
|
||||||
Supprimer
|
<MalioButton
|
||||||
</button>
|
label="Modifier"
|
||||||
<button
|
variant="primary"
|
||||||
type="submit"
|
button-class="w-full"
|
||||||
class="flex items-center justify-center rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
>
|
@click="onSubmit"
|
||||||
Modifier
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex justify-center pt-2">
|
<div v-else class="flex justify-center pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
label="Ajouter"
|
||||||
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
icon-name="mdi:plus"
|
||||||
|
icon-position="left"
|
||||||
|
variant="primary"
|
||||||
|
button-class="w-[200px]"
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
>
|
@click="onSubmit"
|
||||||
+ Ajouter
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Observation } from '~/services/dto/observation'
|
import type { Observation } from '~/services/dto/observation'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
observations: Observation[]
|
observations: Observation[]
|
||||||
|
|||||||
@@ -203,7 +203,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment Drawer -->
|
<!-- Payment Drawer -->
|
||||||
<AppDrawer v-model="isPaymentDrawerOpen" title="Payer des RTT">
|
<MalioDrawer v-model="isPaymentDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Payer des RTT</h2>
|
||||||
|
</template>
|
||||||
<form @submit.prevent="onSubmitPayment">
|
<form @submit.prevent="onSubmitPayment">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-neutral-700">Mois</label>
|
<label class="block text-sm font-medium text-neutral-700">Mois</label>
|
||||||
@@ -254,30 +257,27 @@
|
|||||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="grid grid-cols-2 gap-3 pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="button"
|
label="Annuler"
|
||||||
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
|
variant="tertiary"
|
||||||
|
button-class="w-full"
|
||||||
@click="isPaymentDrawerOpen = false"
|
@click="isPaymentDrawerOpen = false"
|
||||||
>
|
/>
|
||||||
Annuler
|
<MalioButton
|
||||||
</button>
|
label="Enregistrer"
|
||||||
<button
|
button-class="w-full"
|
||||||
type="submit"
|
@click="onSubmitPayment"
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-medium text-white hover:bg-primary-600"
|
/>
|
||||||
>
|
|
||||||
Enregistrer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { EmployeeRttSummary, EmployeeRttWeekSummary } from '~/services/dto/employee-rtt-summary'
|
import type { EmployeeRttSummary, EmployeeRttWeekSummary } from '~/services/dto/employee-rtt-summary'
|
||||||
import type { ContractPhase } from '~/services/dto/contract-phase'
|
import type { ContractPhase } from '~/services/dto/contract-phase'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
|
|
||||||
type RttYearOption = {
|
type RttYearOption = {
|
||||||
value: number
|
value: number
|
||||||
|
|||||||
@@ -1,49 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="drawerOpen" title="Export des heures">
|
<MalioDrawer v-model="drawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Export des heures</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<div>
|
<MalioDate
|
||||||
<label class="text-md font-semibold text-neutral-700" for="hours-export-date">
|
v-model="selectedDate"
|
||||||
Date <span class="text-red-600">*</span>
|
label="Date"
|
||||||
</label>
|
required
|
||||||
<input
|
:clearable="false"
|
||||||
id="hours-export-date"
|
:reserve-message-space="false"
|
||||||
v-model="selectedDate"
|
group-class="w-full"
|
||||||
type="date"
|
/>
|
||||||
class="mt-2 w-full rounded-md border border-black px-3 py-2 text-md text-neutral-900"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<MalioSelectCheckbox
|
||||||
<label class="text-md font-semibold text-neutral-700">
|
v-model="selectedSites"
|
||||||
Sites <span class="text-red-600">*</span>
|
:options="siteOptions"
|
||||||
</label>
|
label="Sites"
|
||||||
<MalioSelectCheckbox
|
required
|
||||||
v-model="selectedSites"
|
:reserve-message-space="false"
|
||||||
:options="siteOptions"
|
groupClass="w-full"
|
||||||
groupClass="w-full mt-2"
|
display-select-all
|
||||||
label="Sites"
|
display-tag
|
||||||
display-select-all
|
/>
|
||||||
display-tag
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-center pt-2">
|
<div class="flex justify-center pt-2">
|
||||||
<button
|
<MalioButton
|
||||||
type="submit"
|
:label="isLoading ? 'Génération en cours...' : 'Exporter'"
|
||||||
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
button-class="w-[200px]"
|
||||||
:disabled="isLoading || !selectedDate || selectedSites.length === 0"
|
:disabled="isLoading || !selectedDate || selectedSites.length === 0"
|
||||||
>
|
@click="handleSubmit"
|
||||||
<template v-if="isLoading">Génération en cours...</template>
|
/>
|
||||||
<template v-else>Exporter</template>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
import type { Site } from '~/services/dto/site'
|
import type { Site } from '~/services/dto/site'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- Desktop: filters row -->
|
<!-- Desktop: filters row -->
|
||||||
<div class="hidden lg:flex lg:items-center lg:gap-4">
|
<div class="hidden lg:flex lg:items-center lg:gap-4">
|
||||||
<div v-if="sites.length > 0 && isAdmin" class="relative z-50 w-80">
|
<div v-if="sites.length > 0 && isAdmin" class="relative z-50 w-80">
|
||||||
<MalioSelectCheckbox
|
<MalioSelectCheckbox :reserve-message-space="false"
|
||||||
v-model="selectedSiteIds"
|
v-model="selectedSiteIds"
|
||||||
:options="siteOptions"
|
:options="siteOptions"
|
||||||
groupClass="w-80"
|
groupClass="w-80"
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
display-select-all
|
display-select-all
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isAdmin" class="w-80">
|
<div v-if="isAdmin" class="w-96">
|
||||||
<MalioInputText
|
<MalioInputText :reserve-message-space="false"
|
||||||
v-model="employeeFilter"
|
v-model="employeeFilter"
|
||||||
label="Recherche d'un employé"
|
label="Recherche d'un employé"
|
||||||
icon-name="mdi:magnify"
|
icon-name="mdi:magnify"
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<!-- Mobile: search + filter button -->
|
<!-- Mobile: search + filter button -->
|
||||||
<div v-if="isAdmin" class="flex items-center gap-2 lg:hidden">
|
<div v-if="isAdmin" class="flex items-center gap-2 lg:hidden">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<MalioInputText
|
<MalioInputText :reserve-message-space="false"
|
||||||
v-model="employeeFilter"
|
v-model="employeeFilter"
|
||||||
label="Recherche d'un employé"
|
label="Recherche d'un employé"
|
||||||
icon-name="mdi:magnify"
|
icon-name="mdi:magnify"
|
||||||
@@ -39,12 +39,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile filters drawer -->
|
<!-- Mobile filters drawer -->
|
||||||
<AppDrawer v-model="filtersDrawerOpen" title="Filtres">
|
<MalioDrawer v-model="filtersDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Filtres</h2>
|
||||||
|
</template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div v-if="sites.length > 0 && isAdmin">
|
<div v-if="sites.length > 0 && isAdmin">
|
||||||
<label class="text-md font-semibold text-neutral-700">Sites</label>
|
<label class="text-md font-semibold text-neutral-700">Sites</label>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<MalioSelectCheckbox
|
<MalioSelectCheckbox :reserve-message-space="false"
|
||||||
v-model="selectedSiteIds"
|
v-model="selectedSiteIds"
|
||||||
:options="siteOptions"
|
:options="siteOptions"
|
||||||
groupClass="w-80"
|
groupClass="w-80"
|
||||||
@@ -77,11 +80,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppDrawer>
|
</MalioDrawer>
|
||||||
|
|
||||||
<!-- Date navigation -->
|
<!-- Date navigation -->
|
||||||
<div class="flex flex-col gap-3 lg:flex-row lg:justify-between lg:items-center lg:gap-4">
|
<div class="flex flex-col gap-3 lg:flex-row lg:justify-between lg:items-center lg:gap-4">
|
||||||
<div class="flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:gap-4">
|
<div class="flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:items-center lg:gap-4">
|
||||||
<div
|
<div
|
||||||
v-if="viewMode === 'day'"
|
v-if="viewMode === 'day'"
|
||||||
class="inline-flex h-10 w-full overflow-hidden rounded-md border border-primary-500 bg-white lg:w-[320px]"
|
class="inline-flex h-10 w-full overflow-hidden rounded-md border border-primary-500 bg-white lg:w-[320px]"
|
||||||
@@ -142,10 +145,33 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Vue Jour (opt-in) : calendrier Malio avec jours validés en vert (markedDates). -->
|
||||||
|
<MalioDate
|
||||||
|
v-if="viewMode === 'day' && showValidationCalendar"
|
||||||
|
:model-value="selectedDate"
|
||||||
|
:clearable="false"
|
||||||
|
:reserve-message-space="false"
|
||||||
|
:marked-dates="markedDates"
|
||||||
|
group-class="w-full lg:w-96"
|
||||||
|
label="Date"
|
||||||
|
@update:model-value="onDatePicked"
|
||||||
|
@month-change="(payload) => emit('month-change', payload)"
|
||||||
|
/>
|
||||||
|
<!-- Vue Semaine : sélecteur de semaine Malio. -->
|
||||||
|
<MalioDateWeek
|
||||||
|
v-else-if="viewMode === 'week'"
|
||||||
|
:model-value="pickerValue"
|
||||||
|
:clearable="false"
|
||||||
|
:reserve-message-space="false"
|
||||||
|
group-class="w-full lg:w-96"
|
||||||
|
label="Semaine"
|
||||||
|
@update:model-value="onWeekPicked"
|
||||||
|
/>
|
||||||
<PeriodStepperPicker
|
<PeriodStepperPicker
|
||||||
|
v-else
|
||||||
width-class="w-full lg:w-[320px]"
|
width-class="w-full lg:w-[320px]"
|
||||||
:label="formattedSelectedDate"
|
:label="formattedSelectedDate"
|
||||||
:picker-type="viewMode === 'week' ? 'week' : 'date'"
|
picker-type="date"
|
||||||
:picker-value="pickerValue"
|
:picker-value="pickerValue"
|
||||||
prev-aria-label="Période précédente"
|
prev-aria-label="Période précédente"
|
||||||
next-aria-label="Période suivante"
|
next-aria-label="Période suivante"
|
||||||
@@ -195,7 +221,6 @@
|
|||||||
import type { Site } from '~/services/dto/site'
|
import type { Site } from '~/services/dto/site'
|
||||||
import type { AbsenceType } from '~/services/dto/absence-type'
|
import type { AbsenceType } from '~/services/dto/absence-type'
|
||||||
import PeriodStepperPicker from '~/components/PeriodStepperPicker.vue'
|
import PeriodStepperPicker from '~/components/PeriodStepperPicker.vue'
|
||||||
import AppDrawer from '~/components/AppDrawer.vue'
|
|
||||||
import { weekInputValueToYmd, ymdToWeekInputValue } from '~/utils/date'
|
import { weekInputValueToYmd, ymdToWeekInputValue } from '~/utils/date'
|
||||||
|
|
||||||
const selectedDate = defineModel<string>('selectedDate', { required: true })
|
const selectedDate = defineModel<string>('selectedDate', { required: true })
|
||||||
@@ -208,6 +233,10 @@ const props = defineProps<{
|
|||||||
sites: Site[]
|
sites: Site[]
|
||||||
absenceTypes: AbsenceType[]
|
absenceTypes: AbsenceType[]
|
||||||
formattedSelectedDate: string
|
formattedSelectedDate: string
|
||||||
|
// Calendrier des jours validés (vert) : opt-in, réservé à l'écran Heures.
|
||||||
|
// L'écran Heures Conducteurs ne le passe pas → garde le PeriodStepperPicker.
|
||||||
|
showValidationCalendar?: boolean
|
||||||
|
markedDates?: Record<string, 'success' | 'danger'>
|
||||||
shortcutButtonClass: (target: 'yesterday' | 'today' | 'tomorrow') => string
|
shortcutButtonClass: (target: 'yesterday' | 'today' | 'tomorrow') => string
|
||||||
weekShortcutButtonClass: (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => string
|
weekShortcutButtonClass: (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => string
|
||||||
getWeekShortcutLabel: (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => string
|
getWeekShortcutLabel: (target: 'previousWeek' | 'thisWeek' | 'nextWeek') => string
|
||||||
@@ -223,6 +252,7 @@ const emit = defineEmits<{
|
|||||||
(e: 'set-this-week'): void
|
(e: 'set-this-week'): void
|
||||||
(e: 'set-next-week'): void
|
(e: 'set-next-week'): void
|
||||||
(e: 'shift-date', value: number): void
|
(e: 'shift-date', value: number): void
|
||||||
|
(e: 'month-change', value: { month: number; year: number }): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const filtersDrawerOpen = ref(false)
|
const filtersDrawerOpen = ref(false)
|
||||||
@@ -252,4 +282,20 @@ const onPickerValue = (value: string) => {
|
|||||||
|
|
||||||
selectedDate.value = value
|
selectedDate.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sélection d'un jour dans le calendrier MalioDate (vue Jour). `clearable=false`
|
||||||
|
// → pas de null en pratique, mais on garde la garde par sécurité.
|
||||||
|
const onDatePicked = (value: string | null) => {
|
||||||
|
if (!value) return
|
||||||
|
selectedDate.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sélection d'une semaine dans MalioDateWeek (vue Semaine) : v-model au format ISO
|
||||||
|
// week (YYYY-Www) → on repositionne selectedDate sur le lundi de cette semaine.
|
||||||
|
const onWeekPicked = (value: string | null) => {
|
||||||
|
if (!value) return
|
||||||
|
const ymd = weekInputValueToYmd(value)
|
||||||
|
if (!ymd) return
|
||||||
|
selectedDate.value = ymd
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<MalioDrawer v-model="drawerOpen" title="Commentaire">
|
<MalioDrawer v-model="drawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Commentaire</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="onSave">
|
<form class="space-y-4" @submit.prevent="onSave">
|
||||||
<div v-if="employeeLabel" class="text-md font-semibold text-neutral-700">{{ employeeLabel }}</div>
|
<div v-if="employeeLabel" class="text-md font-semibold text-neutral-700">{{ employeeLabel }}</div>
|
||||||
<div class="text-md font-semibold text-neutral-700">{{ formatWeekRange }}</div>
|
<div class="text-md font-semibold text-neutral-700">{{ formatWeekRange }}</div>
|
||||||
<MalioInputTextArea
|
<MalioInputTextArea :reserve-message-space="false"
|
||||||
v-model="content"
|
v-model="content"
|
||||||
label="Commentaire"
|
label="Commentaire"
|
||||||
:size="8"
|
:size="8"
|
||||||
@@ -11,17 +14,18 @@
|
|||||||
:show-counter="true"
|
:show-counter="true"
|
||||||
resize="vertical"
|
resize="vertical"
|
||||||
/>
|
/>
|
||||||
<div class="sticky bottom-0 -mx-[20px] bg-white px-[20px] py-3 flex justify-between gap-3">
|
<div class="sticky bottom-0 -mx-[20px] bg-white px-[20px] py-3 flex gap-3">
|
||||||
<MalioButton
|
<MalioButton
|
||||||
v-if="commentId"
|
v-if="commentId"
|
||||||
label="Supprimer"
|
label="Supprimer"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
|
button-class="flex-1"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
@click="onDelete"
|
@click="onDelete"
|
||||||
/>
|
/>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
label="Enregistrer"
|
label="Enregistrer"
|
||||||
button-class="ml-auto"
|
button-class="flex-1"
|
||||||
:disabled="isSubmitting || !canSubmit"
|
:disabled="isSubmitting || !canSubmit"
|
||||||
@click="onSave"
|
@click="onSave"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
bulkUpdateWorkHourValidation,
|
bulkUpdateWorkHourValidation,
|
||||||
bulkUpsertWorkHours,
|
bulkUpsertWorkHours,
|
||||||
getWorkHourDayContext,
|
getWorkHourDayContext,
|
||||||
|
getWorkHourValidationStatus,
|
||||||
getWeeklyWorkHourSummary,
|
getWeeklyWorkHourSummary,
|
||||||
listWorkHoursByDate,
|
listWorkHoursByDate,
|
||||||
updateWorkHourSiteValidation,
|
updateWorkHourSiteValidation,
|
||||||
@@ -28,7 +29,8 @@ import {
|
|||||||
getWeekStartDate,
|
getWeekStartDate,
|
||||||
getTodayYmd,
|
getTodayYmd,
|
||||||
parseYmd,
|
parseYmd,
|
||||||
shiftYmd
|
shiftYmd,
|
||||||
|
toYmd
|
||||||
} from '~/utils/date'
|
} from '~/utils/date'
|
||||||
import { sortEmployeesBySiteAndOrder } from '~/utils/employee'
|
import { sortEmployeesBySiteAndOrder } from '~/utils/employee'
|
||||||
|
|
||||||
@@ -68,6 +70,9 @@ export const useDriverHoursPage = () => {
|
|||||||
const isSubmitting = ref(false)
|
const isSubmitting = ref(false)
|
||||||
const validatingRowIds = ref<number[]>([])
|
const validatingRowIds = ref<number[]>([])
|
||||||
const siteValidatingRowIds = ref<number[]>([])
|
const siteValidatingRowIds = ref<number[]>([])
|
||||||
|
// Jours entièrement validés (conducteurs) par mois civil, pour le calendrier de
|
||||||
|
// la vue Jour. Clé 'YYYY-MM' → dates Y-m-d. Chargé à la volée sur @month-change.
|
||||||
|
const validatedDaysByMonth = ref<Record<string, string[]>>({})
|
||||||
|
|
||||||
const dayGridCols = computed(() => {
|
const dayGridCols = computed(() => {
|
||||||
const metricCol = '0.4fr'
|
const metricCol = '0.4fr'
|
||||||
@@ -519,10 +524,11 @@ export const useDriverHoursPage = () => {
|
|||||||
const refreshAfterAbsenceChange = async () => {
|
const refreshAfterAbsenceChange = async () => {
|
||||||
if (isAdmin.value) {
|
if (isAdmin.value) {
|
||||||
await Promise.all([loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
await Promise.all([loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
||||||
return
|
} else {
|
||||||
|
weeklySummary.value = null
|
||||||
|
await Promise.all([loadDayContext(), loadAbsences()])
|
||||||
}
|
}
|
||||||
weeklySummary.value = null
|
await reloadValidationMonth(selectedDate.value)
|
||||||
await Promise.all([loadDayContext(), loadAbsences()])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitAbsence = async () => {
|
const submitAbsence = async () => {
|
||||||
@@ -626,6 +632,7 @@ export const useDriverHoursPage = () => {
|
|||||||
try {
|
try {
|
||||||
await updateWorkHourValidation(updatedRow.workHourId, checked, { toast: options.toast })
|
await updateWorkHourValidation(updatedRow.workHourId, checked, { toast: options.toast })
|
||||||
updatedRow.isValid = checked
|
updatedRow.isValid = checked
|
||||||
|
await reloadValidationMonth(selectedDate.value)
|
||||||
} finally {
|
} finally {
|
||||||
validatingRowIds.value = validatingRowIds.value.filter((id) => id !== employeeId)
|
validatingRowIds.value = validatingRowIds.value.filter((id) => id !== employeeId)
|
||||||
}
|
}
|
||||||
@@ -708,6 +715,7 @@ export const useDriverHoursPage = () => {
|
|||||||
}, { toast: false })
|
}, { toast: false })
|
||||||
|
|
||||||
await loadWorkHours()
|
await loadWorkHours()
|
||||||
|
await reloadValidationMonth(selectedDate.value)
|
||||||
|
|
||||||
if (result.updated === 0) {
|
if (result.updated === 0) {
|
||||||
toast.error({ title: 'Erreur', message: 'Aucune ligne mise à jour.' })
|
toast.error({ title: 'Erreur', message: 'Aucune ligne mise à jour.' })
|
||||||
@@ -825,6 +833,45 @@ export const useDriverHoursPage = () => {
|
|||||||
dayContext.value = await getWorkHourDayContext(selectedDate.value)
|
dayContext.value = await getWorkHourDayContext(selectedDate.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Calendrier vue Jour : jours validés en vert (scope conducteurs) ---------
|
||||||
|
const monthKey = (year: number, monthIndex: number) => `${year}-${String(monthIndex + 1).padStart(2, '0')}`
|
||||||
|
|
||||||
|
const markedDates = computed<Record<string, 'success'>>(() => {
|
||||||
|
const map: Record<string, 'success'> = {}
|
||||||
|
for (const days of Object.values(validatedDaysByMonth.value)) {
|
||||||
|
for (const day of days) map[day] = 'success'
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
// Plage = grille visible complète (lundi avant le 1er → dimanche après le dernier).
|
||||||
|
// driver:true → l'endpoint ne considère que les conducteurs.
|
||||||
|
const loadValidationMonth = async (monthIndex: number, year: number, options: { force?: boolean } = {}) => {
|
||||||
|
const key = monthKey(year, monthIndex)
|
||||||
|
if (!options.force && validatedDaysByMonth.value[key]) return
|
||||||
|
|
||||||
|
const gridStart = getWeekStartDate(new Date(year, monthIndex, 1))
|
||||||
|
const gridEnd = getWeekStartDate(new Date(year, monthIndex + 1, 0))
|
||||||
|
gridEnd.setDate(gridEnd.getDate() + 6)
|
||||||
|
|
||||||
|
const from = toYmd(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate())
|
||||||
|
const to = toYmd(gridEnd.getFullYear(), gridEnd.getMonth(), gridEnd.getDate())
|
||||||
|
const days = await getWorkHourValidationStatus(from, to, { driver: true })
|
||||||
|
validatedDaysByMonth.value = { ...validatedDaysByMonth.value, [key]: days }
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCalendarMonthChange = (payload: { month: number; year: number }) => {
|
||||||
|
void loadValidationMonth(payload.month, payload.year)
|
||||||
|
}
|
||||||
|
|
||||||
|
const reloadValidationMonth = async (dateYmd: string) => {
|
||||||
|
const parsed = parseYmd(dateYmd)
|
||||||
|
if (!parsed) return
|
||||||
|
const key = monthKey(parsed.getFullYear(), parsed.getMonth())
|
||||||
|
if (!validatedDaysByMonth.value[key]) return
|
||||||
|
await loadValidationMonth(parsed.getMonth(), parsed.getFullYear(), { force: true })
|
||||||
|
}
|
||||||
|
|
||||||
const refreshByDate = async () => {
|
const refreshByDate = async () => {
|
||||||
if (isAdmin.value) {
|
if (isAdmin.value) {
|
||||||
await Promise.all([loadWorkHours(), loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
await Promise.all([loadWorkHours(), loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
||||||
@@ -921,6 +968,7 @@ export const useDriverHoursPage = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await refreshByDate()
|
await refreshByDate()
|
||||||
|
await reloadValidationMonth(selectedDate.value)
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false
|
isSubmitting.value = false
|
||||||
}
|
}
|
||||||
@@ -1003,6 +1051,8 @@ export const useDriverHoursPage = () => {
|
|||||||
closeAbsenceDrawer,
|
closeAbsenceDrawer,
|
||||||
formatMinutes,
|
formatMinutes,
|
||||||
handleSave,
|
handleSave,
|
||||||
|
markedDates,
|
||||||
|
onCalendarMonthChange,
|
||||||
isWeekCommentDrawerOpen,
|
isWeekCommentDrawerOpen,
|
||||||
weekCommentContext,
|
weekCommentContext,
|
||||||
openWeekCommentDrawer,
|
openWeekCommentDrawer,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
bulkUpdateWorkHourValidation,
|
bulkUpdateWorkHourValidation,
|
||||||
bulkUpsertWorkHours,
|
bulkUpsertWorkHours,
|
||||||
getWorkHourDayContext,
|
getWorkHourDayContext,
|
||||||
|
getWorkHourValidationStatus,
|
||||||
getWeeklyWorkHourSummary,
|
getWeeklyWorkHourSummary,
|
||||||
listWorkHoursByDate,
|
listWorkHoursByDate,
|
||||||
updateWorkHourSiteValidation,
|
updateWorkHourSiteValidation,
|
||||||
@@ -30,7 +31,8 @@ import {
|
|||||||
getWeekStartDate,
|
getWeekStartDate,
|
||||||
getTodayYmd,
|
getTodayYmd,
|
||||||
parseYmd,
|
parseYmd,
|
||||||
shiftYmd
|
shiftYmd,
|
||||||
|
toYmd
|
||||||
} from '~/utils/date'
|
} from '~/utils/date'
|
||||||
import { sortEmployeesBySiteAndOrder } from '~/utils/employee'
|
import { sortEmployeesBySiteAndOrder } from '~/utils/employee'
|
||||||
|
|
||||||
@@ -70,6 +72,10 @@ export const useHoursPage = () => {
|
|||||||
const isSubmitting = ref(false)
|
const isSubmitting = ref(false)
|
||||||
const validatingRowIds = ref<number[]>([])
|
const validatingRowIds = ref<number[]>([])
|
||||||
const siteValidatingRowIds = ref<number[]>([])
|
const siteValidatingRowIds = ref<number[]>([])
|
||||||
|
// Jours entièrement validés (admin) par mois civil affiché dans le calendrier
|
||||||
|
// de la vue Jour. Clé = 'YYYY-MM', valeur = liste de dates Y-m-d. Chargé à la
|
||||||
|
// volée sur @month-change (jamais préchargé sur plusieurs années).
|
||||||
|
const validatedDaysByMonth = ref<Record<string, string[]>>({})
|
||||||
|
|
||||||
const dayGridCols = computed(() => {
|
const dayGridCols = computed(() => {
|
||||||
const metricCol = '0.4fr'
|
const metricCol = '0.4fr'
|
||||||
@@ -686,11 +692,11 @@ export const useHoursPage = () => {
|
|||||||
const refreshAfterAbsenceChange = async () => {
|
const refreshAfterAbsenceChange = async () => {
|
||||||
if (isAdmin.value) {
|
if (isAdmin.value) {
|
||||||
await Promise.all([loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
await Promise.all([loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
||||||
return
|
} else {
|
||||||
|
weeklySummary.value = null
|
||||||
|
await Promise.all([loadDayContext(), loadAbsences()])
|
||||||
}
|
}
|
||||||
|
await reloadValidationMonth(selectedDate.value)
|
||||||
weeklySummary.value = null
|
|
||||||
await Promise.all([loadDayContext(), loadAbsences()])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitAbsence = async () => {
|
const submitAbsence = async () => {
|
||||||
@@ -787,6 +793,7 @@ export const useHoursPage = () => {
|
|||||||
try {
|
try {
|
||||||
await updateWorkHourValidation(updatedRow.workHourId, checked, { toast: options.toast })
|
await updateWorkHourValidation(updatedRow.workHourId, checked, { toast: options.toast })
|
||||||
updatedRow.isValid = checked
|
updatedRow.isValid = checked
|
||||||
|
await reloadValidationMonth(selectedDate.value)
|
||||||
} finally {
|
} finally {
|
||||||
validatingRowIds.value = validatingRowIds.value.filter((id) => id !== employeeId)
|
validatingRowIds.value = validatingRowIds.value.filter((id) => id !== employeeId)
|
||||||
}
|
}
|
||||||
@@ -891,6 +898,7 @@ export const useHoursPage = () => {
|
|||||||
}, { toast: false })
|
}, { toast: false })
|
||||||
|
|
||||||
await loadWorkHours()
|
await loadWorkHours()
|
||||||
|
await reloadValidationMonth(selectedDate.value)
|
||||||
|
|
||||||
if (result.updated === 0) {
|
if (result.updated === 0) {
|
||||||
toast.error({
|
toast.error({
|
||||||
@@ -1031,6 +1039,50 @@ export const useHoursPage = () => {
|
|||||||
dayContext.value = await getWorkHourDayContext(selectedDate.value)
|
dayContext.value = await getWorkHourDayContext(selectedDate.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Calendrier vue Jour : jours validés en vert ---------------------------
|
||||||
|
const monthKey = (year: number, monthIndex: number) => `${year}-${String(monthIndex + 1).padStart(2, '0')}`
|
||||||
|
|
||||||
|
// Fusionne tous les mois chargés en une seule map ISO → 'success' pour MalioDate.
|
||||||
|
const markedDates = computed<Record<string, 'success'>>(() => {
|
||||||
|
const map: Record<string, 'success'> = {}
|
||||||
|
for (const days of Object.values(validatedDaysByMonth.value)) {
|
||||||
|
for (const day of days) map[day] = 'success'
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
// Charge le statut du mois affiché. La plage couvre toute la grille visible
|
||||||
|
// (lundi avant le 1er → dimanche après le dernier jour) pour colorer aussi les
|
||||||
|
// jours débordants des mois adjacents.
|
||||||
|
const loadValidationMonth = async (monthIndex: number, year: number, options: { force?: boolean } = {}) => {
|
||||||
|
const key = monthKey(year, monthIndex)
|
||||||
|
if (!options.force && validatedDaysByMonth.value[key]) return
|
||||||
|
|
||||||
|
const gridStart = getWeekStartDate(new Date(year, monthIndex, 1))
|
||||||
|
const gridEnd = getWeekStartDate(new Date(year, monthIndex + 1, 0))
|
||||||
|
gridEnd.setDate(gridEnd.getDate() + 6)
|
||||||
|
|
||||||
|
const from = toYmd(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate())
|
||||||
|
const to = toYmd(gridEnd.getFullYear(), gridEnd.getMonth(), gridEnd.getDate())
|
||||||
|
const days = await getWorkHourValidationStatus(from, to)
|
||||||
|
validatedDaysByMonth.value = { ...validatedDaysByMonth.value, [key]: days }
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCalendarMonthChange = (payload: { month: number; year: number }) => {
|
||||||
|
void loadValidationMonth(payload.month, payload.year)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Après une modification qui touche la validation d'un jour (validation,
|
||||||
|
// sauvegarde d'heures, absence), recharge le mois concerné s'il est déjà en
|
||||||
|
// cache → le calendrier se recolore. Sinon no-op (le prochain affichage fetch).
|
||||||
|
const reloadValidationMonth = async (dateYmd: string) => {
|
||||||
|
const parsed = parseYmd(dateYmd)
|
||||||
|
if (!parsed) return
|
||||||
|
const key = monthKey(parsed.getFullYear(), parsed.getMonth())
|
||||||
|
if (!validatedDaysByMonth.value[key]) return
|
||||||
|
await loadValidationMonth(parsed.getMonth(), parsed.getFullYear(), { force: true })
|
||||||
|
}
|
||||||
|
|
||||||
const refreshByDate = async () => {
|
const refreshByDate = async () => {
|
||||||
if (isAdmin.value) {
|
if (isAdmin.value) {
|
||||||
await Promise.all([loadWorkHours(), loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
await Promise.all([loadWorkHours(), loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
||||||
@@ -1131,6 +1183,7 @@ export const useHoursPage = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await refreshByDate()
|
await refreshByDate()
|
||||||
|
await reloadValidationMonth(selectedDate.value)
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false
|
isSubmitting.value = false
|
||||||
}
|
}
|
||||||
@@ -1221,6 +1274,8 @@ export const useHoursPage = () => {
|
|||||||
closeAbsenceDrawer,
|
closeAbsenceDrawer,
|
||||||
formatMinutes,
|
formatMinutes,
|
||||||
handleSave,
|
handleSave,
|
||||||
|
markedDates,
|
||||||
|
onCalendarMonthChange,
|
||||||
isWeekCommentDrawerOpen,
|
isWeekCommentDrawerOpen,
|
||||||
weekCommentContext,
|
weekCommentContext,
|
||||||
openWeekCommentDrawer,
|
openWeekCommentDrawer,
|
||||||
|
|||||||
@@ -159,6 +159,17 @@ export const documentationSections: DocSection[] = [
|
|||||||
{ type: 'note', content: 'Toute vraie modification effectuée par un administrateur remet automatiquement les deux validations à zéro.' },
|
{ type: 'note', content: 'Toute vraie modification effectuée par un administrateur remet automatiquement les deux validations à zéro.' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'calendrier-jours-valides',
|
||||||
|
title: 'Calendrier des jours validés (vue Jour)',
|
||||||
|
requiredLevel: 'site_manager',
|
||||||
|
blocks: [
|
||||||
|
{ type: 'paragraph', content: 'En vue Jour, le sélecteur de date est un calendrier qui colore en vert les jours entièrement validés. Vous repérez ainsi d\'un coup d\'œil les jours où il reste de la validation à faire.' },
|
||||||
|
{ type: 'list', content: 'Vert : le jour porte au moins une ligne et toutes sont validées par un administrateur.\nNeutre (sans couleur) : il reste au moins une ligne à valider, ou aucune ligne n\'a encore été saisie ce jour-là.' },
|
||||||
|
{ type: 'paragraph', content: 'Le vert reflète tout votre périmètre (vos sites), indépendamment du filtre Sites de l\'écran. Les conducteurs ne sont pas pris en compte (écran Heures Conducteurs). Cliquez sur un jour pour vous y rendre.' },
|
||||||
|
{ type: 'note', content: 'La couleur se met à jour automatiquement quand vous validez des lignes, enregistrez des heures ou modifiez une absence. La validation de site (chef de site) ne change pas la couleur : seule la validation RH/admin compte.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Generated
+833
-5
@@ -7,7 +7,7 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@malio/layer-ui": "^1.4.6",
|
"@malio/layer-ui": "^1.7.11",
|
||||||
"@nuxt/icon": "^2.2.1",
|
"@nuxt/icon": "^2.2.1",
|
||||||
"@nuxtjs/i18n": "^10.2.1",
|
"@nuxtjs/i18n": "^10.2.1",
|
||||||
"@pinia/nuxt": "^0.11.3",
|
"@pinia/nuxt": "^0.11.3",
|
||||||
@@ -1196,6 +1196,31 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||||
|
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.5",
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||||
|
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -2222,14 +2247,22 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@malio/layer-ui": {
|
"node_modules/@malio/layer-ui": {
|
||||||
"version": "1.4.6",
|
"version": "1.7.11",
|
||||||
"resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.4.6/layer-ui-1.4.6.tgz",
|
"resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.7.11/layer-ui-1.7.11.tgz",
|
||||||
"integrity": "sha512-stHqUAJ8E6a62Ka7QXlE177GhkIsjtmYNa/tNk1TVpbJ099okfLLivrlofEl7CCAqDeMaIepnW4q0vxJT+EFEA==",
|
"integrity": "sha512-uTISSe0L2T0TcpJShdK8VOEr0GpYzyDFDkLNFRa5APbpnfb8GPchx0xlFA1pgEF7DbnYB/zxYTWZCrGOhmaWOQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/icon": "^2.2.1",
|
"@nuxt/icon": "^2.2.1",
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
"@tiptap/extension-color": "^3.22.5",
|
||||||
|
"@tiptap/extension-highlight": "^3.22.5",
|
||||||
|
"@tiptap/extension-placeholder": "^3.22.5",
|
||||||
|
"@tiptap/extension-text-style": "^3.22.5",
|
||||||
|
"@tiptap/pm": "^3.22.5",
|
||||||
|
"@tiptap/starter-kit": "^3.22.5",
|
||||||
|
"@tiptap/vue-3": "^3.22.5",
|
||||||
"maska": "^3.2.0",
|
"maska": "^3.2.0",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tiptap-markdown": "^0.9.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"nuxt": "^4.0.0"
|
"nuxt": "^4.0.0"
|
||||||
@@ -5323,6 +5356,480 @@
|
|||||||
"integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==",
|
"integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==",
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/core": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-TX9PyPqBoix0qDLjtok/bddtdSy54QhzLVha405C07V+WySOpH3s/pWYkywehZQY0SQtcrcY4MNSCeQjCbA28A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/pm": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-WaKjKmUaadgvZDDBk9JOn/oidlOFr6booqJIWHGL5S0aUUTKHS19oGfKQq/l9Z1y1niaRePk0Y4fy/jxCnfKPA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bold": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-VIlF2sAiV6K009pcIDotfY8mvsPaq90dxeG9Q0ZIqfMD958TUCqjHw4MGYZf0/FgP12xksBfmcR7W312xgUf9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bubble-menu": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-Y3R9wFKP/U9M04JG+0PM/yW3OV+MSbUp6YBKQWZmUu8x6y7TbcNvDsaJ6QEFZt5aRMS6qH1ksYPTOz47JdjcfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1",
|
||||||
|
"@tiptap/pm": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bullet-list": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-JB6bEJJHxXNAXEXTIAN3/j70p1ARHdeMfhzshGZswWKUWtDibTCrspIp7p1VNeiuVtJ/HB6PpFkGi7yWtQ3RTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-t9/VR5k3rGPyhcGau9YvVgaAQ+nP9R9WzS996bQQ7GIrMOTSXb0FWwoQFBiYl83V6VA16Tlj/oScC7SFlA8lvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-NY7SYqcrqDVYTSWyaNGdSfCims6pOHoRQ2Rh4DEFb/rb8gLVkqbLZhcHzQCVfinlPqgV3xWF6cYMORwmnlBkXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1",
|
||||||
|
"@tiptap/pm": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-color": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-f8yy+CBRDqMeaIaQ0UHDbGUqjfGka5O16ja47shatXm49lqLcL06js9tGoiZFVzp9/lcKOLSXjuzxNf0OZ9SbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-text-style": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-document": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-6W2vZjvi0Mv+4xEtwMDGhWwo7FotWR6eKfmntmduvehWevFpMxOKcTtyotjLigfZv738y50YWmvbaPuAPJG3BA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-eVq3BvFIa3YD+pBIlj1i72vYEixlegGVKHnSYiVF2ovkQOSAH9sca7pkq6WgV1sMTCyWCU8e+WznTqtydvHUWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-floating-menu": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-xn0g4m/q2bjG+hULPwp6Aqb/6wpzUtc65jOhgJsG/S3Ey3kLJGUvZBuhozwNFu8FcugxM1fMUpNhkJkodCCGFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "3.26.1",
|
||||||
|
"@tiptap/pm": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-gapcursor": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-BWW1yMQQA4TbEU0LLK+4cd9ebLTuZG5KjHwFMBRD/bGiRW9V1gTWFsCqThBbczcANoQiZK9pn5/4Ad/rGM3HUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-hard-break": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-gzNb1e/fK6HN+ko1axsrasjK7F1q0Bnm0G4ZY/0eq7pV7s1wZuwoCiGbvUx/9LCFKRV6+94FTqlb0A3NbYN36g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-heading": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-eRlv9XxzUL8FobKAiF1WjP35CT2QpbcxxeyYFF7BmGEONvKI7r5g7JGwyGli4Cvclh70h8w6JuoXSmGUVEU65A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-highlight": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-qgx4Eetqkogh3OyomZO0yIMQGHhjatCDAtELkC7NQxnmPsp2c9i6ck/hh7mP5We5ccBwUoYRuNGC9lkflCx75g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-l9lPZYeSmY90y/2GkQcKaICFD5Atr8sx2SzJGkQzpNC9tRxZXyAHnfJE3OjBkspuGzjWIN0DimxBj4ibz58sKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1",
|
||||||
|
"@tiptap/pm": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-italic": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-cLKYvOLToWEkJkAPspgIZ/PYDzAxacLm1VWcAq1tO1QDQCDe2Kw+y/zsGlyYEq/aKsAgpp4JNopBwAXRXxt2/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-link": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-aLLGLgikuhLFHRbjfUC6D4gRg+NUty4uhW7YkyVl8AxxPME47dPbCOX4H6uLCjEZcn3WnfNuCTr6HCTl0KEmGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"linkifyjs": "^4.3.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1",
|
||||||
|
"@tiptap/pm": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-06nOjnyXpzMO8Ys5k3IbYsDsKib1mv2OtaxBYX1/1uvRyOKwUX5tqDLb/qigic0LIANNL73lkNC8Z8XPeG4Tkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1",
|
||||||
|
"@tiptap/pm": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-item": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-5gLXJUiP763NA6i4HgrtcwUDXPP8820hsaBQyF1Y1VsXNi02uW9FVLe3RZK8jF0NZUNh9CqD0gogYJCbKOUU8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-keymap": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-EReSayePO6SIxtRbxx+7KfBQreWHvoZmMb3O/RemfT8W6J0hCG5N/Rh8Z12+YZOnCDRXJ4RzFpAikYka3E54jQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-LeFPeFwb7ylkQVuuaHj+niu7WhWHpjDOi1GKZJE/ohOa2lgt7P221HMqhUzPiDlXOExN72oWTNmXUlT0ymCTkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-paragraph": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-OkBeYUNM3eTzjm3z6IcC3NHryOX8g3eGNI86P/B+tFoFQSRuzLsKZU50ARCfIiLLg812NjcqujeJ1eX3BKDZrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-placeholder": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-oJCEVmaaUY1Jn5v8KbRMdgYLFH9aptLkir+M0ZMnl+8TTmvMdLK2H02X9ofZQwAb12qreQgb890hB3PFen7TDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-strike": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-7hmQ2mBsA+75GRrJIKYxb+10H23mblEQSGGsv9Ptl7JLaGmj+8sv2HGQGSUT9QBiBVprxaYTqyWFXQC9akfLWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-Gocui5WvcCCJJIX17gdOVCSdYi5H4fDwaR0qkMAUZPq5kJCdrfl+vNpt8BTt53Bk+/QumiUW21fhQ184w7RoeQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text-style": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-V8t7qOyLH7IBR2HjjJVZ9rUHTnuFToJx07L9PN9PpgQLhz9q8Jah4gAwmjLBXDRG2YaXImGK0RwKKCU/yHhwOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-underline": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-HUHtQ+DRWDM0opW7Nk3YQwrLzw876hMU7cr1X/ZTG+8Bp+AKHihlwU+bqrPgG5St0mqASyUEhHQ/vK5PlnUYOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extensions": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-PmRaoe6bebTgz/ZQrjmzwZMST1d9js9ZTiKnUXeXl3Fm+V5U/c3TbbKDfqmL63qPQdjtShDMHi9tYuv+c77OFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.26.1",
|
||||||
|
"@tiptap/pm": "3.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/pm": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-48cJQRbvr9Ux0+IgM1BR5vOLU5hkC+n+uerdQy2JjrIRKpYE/huU8fQFm6PoRppoKYfilklzb29elsQ+n2TA+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-changeset": "^2.3.0",
|
||||||
|
"prosemirror-commands": "^1.6.2",
|
||||||
|
"prosemirror-dropcursor": "^1.8.1",
|
||||||
|
"prosemirror-gapcursor": "^1.3.2",
|
||||||
|
"prosemirror-history": "^1.4.1",
|
||||||
|
"prosemirror-inputrules": "^1.4.0",
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.7",
|
||||||
|
"prosemirror-schema-list": "^1.5.0",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-tables": "^1.8.0",
|
||||||
|
"prosemirror-transform": "^1.12.0",
|
||||||
|
"prosemirror-view": "^1.41.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/starter-kit": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-A0zsvwGU9exLND34F8e8KqUXFSfs835tNN+VC+ZT3yNeaO/WXnlh/Cgal1F6pHHbcxy7RV2CRwJU5S3cWLPxrA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/core": "^3.26.1",
|
||||||
|
"@tiptap/extension-blockquote": "^3.26.1",
|
||||||
|
"@tiptap/extension-bold": "^3.26.1",
|
||||||
|
"@tiptap/extension-bullet-list": "^3.26.1",
|
||||||
|
"@tiptap/extension-code": "^3.26.1",
|
||||||
|
"@tiptap/extension-code-block": "^3.26.1",
|
||||||
|
"@tiptap/extension-document": "^3.26.1",
|
||||||
|
"@tiptap/extension-dropcursor": "^3.26.1",
|
||||||
|
"@tiptap/extension-gapcursor": "^3.26.1",
|
||||||
|
"@tiptap/extension-hard-break": "^3.26.1",
|
||||||
|
"@tiptap/extension-heading": "^3.26.1",
|
||||||
|
"@tiptap/extension-horizontal-rule": "^3.26.1",
|
||||||
|
"@tiptap/extension-italic": "^3.26.1",
|
||||||
|
"@tiptap/extension-link": "^3.26.1",
|
||||||
|
"@tiptap/extension-list": "^3.26.1",
|
||||||
|
"@tiptap/extension-list-item": "^3.26.1",
|
||||||
|
"@tiptap/extension-list-keymap": "^3.26.1",
|
||||||
|
"@tiptap/extension-ordered-list": "^3.26.1",
|
||||||
|
"@tiptap/extension-paragraph": "^3.26.1",
|
||||||
|
"@tiptap/extension-strike": "^3.26.1",
|
||||||
|
"@tiptap/extension-text": "^3.26.1",
|
||||||
|
"@tiptap/extension-underline": "^3.26.1",
|
||||||
|
"@tiptap/extensions": "^3.26.1",
|
||||||
|
"@tiptap/pm": "^3.26.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/vue-3": {
|
||||||
|
"version": "3.26.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.26.1.tgz",
|
||||||
|
"integrity": "sha512-ihhAYUeOpAQqtY7NcgBFQoIrB5zaB4rYr81dqsfqoqjbnUv5cfDWLIeMQKuXoisqk312IVpvz6Ut+y9fCyIvhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tiptap/extension-bubble-menu": "^3.26.1",
|
||||||
|
"@tiptap/extension-floating-menu": "^3.26.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "3.26.1",
|
||||||
|
"@tiptap/pm": "3.26.1",
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
@@ -5346,6 +5853,28 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/linkify-it": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/markdown-it": {
|
||||||
|
"version": "13.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.9.tgz",
|
||||||
|
"integrity": "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "^3",
|
||||||
|
"@types/mdurl": "^1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/mdurl": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/parse-path": {
|
"node_modules/@types/parse-path": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz",
|
||||||
@@ -9466,6 +9995,31 @@
|
|||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/markdown-it"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/linkifyjs": {
|
||||||
|
"version": "4.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
|
||||||
|
"integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/listhen": {
|
"node_modules/listhen": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/listhen/-/listhen-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/listhen/-/listhen-1.9.0.tgz",
|
||||||
@@ -9640,6 +10194,51 @@
|
|||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it": {
|
||||||
|
"version": "14.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz",
|
||||||
|
"integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/markdown-it"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1",
|
||||||
|
"entities": "^4.4.0",
|
||||||
|
"linkify-it": "^5.0.1",
|
||||||
|
"mdurl": "^2.0.0",
|
||||||
|
"punycode.js": "^2.3.1",
|
||||||
|
"uc.micro": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/markdown-it-task-lists": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/markdown-it/node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/maska": {
|
"node_modules/maska": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/maska/-/maska-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/maska/-/maska-3.2.0.tgz",
|
||||||
@@ -9661,6 +10260,12 @@
|
|||||||
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
|
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/media-typer": {
|
"node_modules/media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
@@ -10444,6 +11049,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/orderedmap": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/oxc-minify": {
|
"node_modules/oxc-minify": {
|
||||||
"version": "0.110.0",
|
"version": "0.110.0",
|
||||||
"resolved": "https://registry.npmjs.org/oxc-minify/-/oxc-minify-0.110.0.tgz",
|
"resolved": "https://registry.npmjs.org/oxc-minify/-/oxc-minify-0.110.0.tgz",
|
||||||
@@ -11527,6 +12138,178 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-changeset": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-commands": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-dropcursor": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0",
|
||||||
|
"prosemirror-view": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-gapcursor": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.0.0",
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-history": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.2.2",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.31.0",
|
||||||
|
"rope-sequence": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-inputrules": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-keymap": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"w3c-keyname": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/markdown-it": "^14.0.0",
|
||||||
|
"markdown-it": "^14.0.0",
|
||||||
|
"prosemirror-model": "^1.25.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown/node_modules/@types/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown/node_modules/@types/markdown-it": {
|
||||||
|
"version": "14.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||||
|
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "^5",
|
||||||
|
"@types/mdurl": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown/node_modules/@types/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-model": {
|
||||||
|
"version": "1.25.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.9.tgz",
|
||||||
|
"integrity": "sha512-pRTklkDDMMRopyoAcrr9wV/8g/RYgrLHBuJAb5hlEuYZRdm5yqmPjWId83fpBwPpSFqEdja0H7Dfd7z1X/npcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"orderedmap": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-list": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-state": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.27.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-tables": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.4",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-transform": "^1.10.5",
|
||||||
|
"prosemirror-view": "^1.41.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-transform": {
|
||||||
|
"version": "1.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
|
||||||
|
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-view": {
|
||||||
|
"version": "1.41.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.9.tgz",
|
||||||
|
"integrity": "sha512-clTunTX+eaLbr87L1V1QPheRlEQJyTlL3gXe9x3jQIk3rL0RVWxviDGz8tFaydwIVm+hKhYCyr+R/zBtWr9s6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.25.8",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/protocols": {
|
"node_modules/protocols": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz",
|
||||||
@@ -11543,6 +12326,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/punycode.js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/quansync": {
|
"node_modules/quansync": {
|
||||||
"version": "0.2.11",
|
"version": "0.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||||
@@ -11948,6 +12740,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rope-sequence": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rou3": {
|
"node_modules/rou3": {
|
||||||
"version": "0.7.12",
|
"version": "0.7.12",
|
||||||
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz",
|
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz",
|
||||||
@@ -12918,6 +13716,24 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiptap-markdown": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"example"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@types/markdown-it": "^13.0.7",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"markdown-it-task-lists": "^2.1.1",
|
||||||
|
"prosemirror-markdown": "^1.11.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -13077,6 +13893,12 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ufo": {
|
"node_modules/ufo": {
|
||||||
"version": "1.6.3",
|
"version": "1.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
|
||||||
@@ -13950,6 +14772,12 @@
|
|||||||
"vue": "^3.5.0"
|
"vue": "^3.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@malio/layer-ui": "^1.7.11",
|
||||||
"@nuxt/icon": "^2.2.1",
|
"@nuxt/icon": "^2.2.1",
|
||||||
"@nuxtjs/i18n": "^10.2.1",
|
"@nuxtjs/i18n": "^10.2.1",
|
||||||
"@malio/layer-ui": "^1.4.6",
|
|
||||||
"@pinia/nuxt": "^0.11.3",
|
"@pinia/nuxt": "^0.11.3",
|
||||||
"nuxt": "^4.3.0",
|
"nuxt": "^4.3.0",
|
||||||
"nuxt-toast": "^1.4.0",
|
"nuxt-toast": "^1.4.0",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="flex items-center justify-between pb-6">
|
<div class="flex items-center justify-between pb-6">
|
||||||
<h1 class="text-4xl font-bold text-primary-500">Types de statut</h1>
|
<h1 class="text-4xl font-bold text-primary-500">Types de statut</h1>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
label="Ajouter un type"
|
label="Ajouter"
|
||||||
icon-name="mdi:plus"
|
icon-name="mdi:plus"
|
||||||
icon-position="left"
|
icon-position="left"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
@@ -55,16 +55,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MalioDrawer v-model="isDrawerOpen" :title="drawerTitle">
|
<MalioDrawer v-model="isDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">{{ drawerTitle }}</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<MalioInputText
|
<MalioInputText :reserve-message-space="false"
|
||||||
v-model="form.code"
|
v-model="form.code"
|
||||||
label="Code *"
|
label="Code *"
|
||||||
group-class="mt-2"
|
group-class="mt-2"
|
||||||
:max-length="10"
|
:max-length="10"
|
||||||
:error="showCodeError ? 'Le code est obligatoire.' : ''"
|
:error="showCodeError ? 'Le code est obligatoire.' : ''"
|
||||||
/>
|
/>
|
||||||
<MalioInputText
|
<MalioInputText :reserve-message-space="false"
|
||||||
v-model="form.label"
|
v-model="form.label"
|
||||||
label="Libellé *"
|
label="Libellé *"
|
||||||
group-class="mt-2"
|
group-class="mt-2"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-3 py-6">
|
<div class="flex flex-col gap-3 py-6">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<MalioSelectCheckbox
|
<MalioSelectCheckbox :reserve-message-space="false"
|
||||||
v-model="selectedSiteIds"
|
v-model="selectedSiteIds"
|
||||||
:options="siteOptions"
|
:options="siteOptions"
|
||||||
label="Sites"
|
label="Sites"
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<MalioButton
|
<MalioButton
|
||||||
label="Ajouter une absence"
|
label="Ajouter"
|
||||||
icon-name="mdi:plus"
|
icon-name="mdi:plus"
|
||||||
icon-position="left"
|
icon-position="left"
|
||||||
@click="openCreateFromToday"
|
@click="openCreateFromToday"
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="w-80">
|
<div class="w-80">
|
||||||
<MalioInputText
|
<MalioInputText :reserve-message-space="false"
|
||||||
v-model="employeeFilter"
|
v-model="employeeFilter"
|
||||||
label="Recherche d'un employé"
|
label="Recherche d'un employé"
|
||||||
icon-name="mdi:magnify"
|
icon-name="mdi:magnify"
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
:sites="sites"
|
:sites="sites"
|
||||||
:absence-types="absenceTypes"
|
:absence-types="absenceTypes"
|
||||||
:formatted-selected-date="formattedSelectedDate"
|
:formatted-selected-date="formattedSelectedDate"
|
||||||
|
:show-validation-calendar="true"
|
||||||
|
:marked-dates="markedDates"
|
||||||
:shortcut-button-class="shortcutButtonClass"
|
:shortcut-button-class="shortcutButtonClass"
|
||||||
:week-shortcut-button-class="weekShortcutButtonClass"
|
:week-shortcut-button-class="weekShortcutButtonClass"
|
||||||
:get-week-shortcut-label="getWeekShortcutLabel"
|
:get-week-shortcut-label="getWeekShortcutLabel"
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
@set-this-week="setThisWeek"
|
@set-this-week="setThisWeek"
|
||||||
@set-next-week="setNextWeek"
|
@set-next-week="setNextWeek"
|
||||||
@shift-date="shiftDate"
|
@shift-date="shiftDate"
|
||||||
|
@month-change="onCalendarMonthChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="isLoading" class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
<div v-if="isLoading" class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
||||||
@@ -193,6 +196,8 @@ const {
|
|||||||
isSelectedDateHoliday,
|
isSelectedDateHoliday,
|
||||||
selectedHolidayLabel,
|
selectedHolidayLabel,
|
||||||
handleSave,
|
handleSave,
|
||||||
|
markedDates,
|
||||||
|
onCalendarMonthChange,
|
||||||
isWeekCommentDrawerOpen,
|
isWeekCommentDrawerOpen,
|
||||||
weekCommentContext,
|
weekCommentContext,
|
||||||
openWeekCommentDrawer,
|
openWeekCommentDrawer,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showPicker" class="mt-3 flex items-center gap-3">
|
<div v-if="showPicker" class="mt-3 flex items-center gap-3">
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
label="Contrat"
|
label="Contrat"
|
||||||
:model-value="selectedPhase?.id ?? null"
|
:model-value="selectedPhase?.id ?? null"
|
||||||
:options="phaseOptions"
|
:options="phaseOptions"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
@click="openExportDrawer"
|
@click="openExportDrawer"
|
||||||
/>
|
/>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
label="Ajouter un employé"
|
label="Ajouter"
|
||||||
icon-name="mdi:plus"
|
icon-name="mdi:plus"
|
||||||
icon-position="left"
|
icon-position="left"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
@@ -21,14 +21,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 py-7">
|
<div class="flex items-center gap-3 py-7">
|
||||||
<div class="w-80">
|
<div class="w-80">
|
||||||
<MalioInputText
|
<MalioInputText :reserve-message-space="false"
|
||||||
v-model="employeeFilter"
|
v-model="employeeFilter"
|
||||||
label="Recherche d'un employé"
|
label="Recherche d'un employé"
|
||||||
icon-name="mdi:magnify"
|
icon-name="mdi:magnify"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="sites.length > 0" class="relative z-50 w-80">
|
<div v-if="sites.length > 0" class="relative z-50 w-80">
|
||||||
<MalioSelectCheckbox
|
<MalioSelectCheckbox :reserve-message-space="false"
|
||||||
v-model="selectedSiteIds"
|
v-model="selectedSiteIds"
|
||||||
:options="siteOptions"
|
:options="siteOptions"
|
||||||
groupClass="w-80"
|
groupClass="w-80"
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
v-model="contractStatusFilter"
|
v-model="contractStatusFilter"
|
||||||
label="Statut contrat"
|
label="Statut contrat"
|
||||||
:options="contractStatusOptions"
|
:options="contractStatusOptions"
|
||||||
@@ -84,21 +84,24 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MalioDrawer v-model="isDrawerOpen" :title="drawerTitle">
|
<MalioDrawer v-model="isDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">{{ drawerTitle }}</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<MalioInputText
|
<MalioInputText :reserve-message-space="false"
|
||||||
v-model="form.firstName"
|
v-model="form.firstName"
|
||||||
label="Prénom *"
|
label="Prénom *"
|
||||||
group-class="mt-2"
|
group-class="mt-2"
|
||||||
:error="showFirstNameError ? 'Le prénom est obligatoire.' : ''"
|
:error="showFirstNameError ? 'Le prénom est obligatoire.' : ''"
|
||||||
/>
|
/>
|
||||||
<MalioInputText
|
<MalioInputText :reserve-message-space="false"
|
||||||
v-model="form.lastName"
|
v-model="form.lastName"
|
||||||
label="Nom *"
|
label="Nom *"
|
||||||
group-class="mt-2"
|
group-class="mt-2"
|
||||||
:error="showLastNameError ? 'Le nom est obligatoire.' : ''"
|
:error="showLastNameError ? 'Le nom est obligatoire.' : ''"
|
||||||
/>
|
/>
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="form.siteId === '' ? null : form.siteId"
|
:model-value="form.siteId === '' ? null : form.siteId"
|
||||||
:options="formSiteOptions"
|
:options="formSiteOptions"
|
||||||
label="Site *"
|
label="Site *"
|
||||||
@@ -107,7 +110,7 @@
|
|||||||
@update:model-value="(v) => { form.siteId = v === null ? '' : Number(v) }"
|
@update:model-value="(v) => { form.siteId = v === null ? '' : Number(v) }"
|
||||||
/>
|
/>
|
||||||
<template v-if="!editingEmployee">
|
<template v-if="!editingEmployee">
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="form.contractNature"
|
:model-value="form.contractNature"
|
||||||
:options="contractNatureFormOptions"
|
:options="contractNatureFormOptions"
|
||||||
label="Type de contrat *"
|
label="Type de contrat *"
|
||||||
@@ -115,7 +118,7 @@
|
|||||||
:error="showContractNatureError ? 'Le type de contrat est obligatoire.' : ''"
|
:error="showContractNatureError ? 'Le type de contrat est obligatoire.' : ''"
|
||||||
@update:model-value="(v) => { if (v !== null) form.contractNature = v as 'CDI' | 'CDD' | 'INTERIM' }"
|
@update:model-value="(v) => { if (v !== null) form.contractNature = v as 'CDI' | 'CDD' | 'INTERIM' }"
|
||||||
/>
|
/>
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
v-if="form.contractNature === 'INTERIM'"
|
v-if="form.contractNature === 'INTERIM'"
|
||||||
:model-value="form.interimAgencyId === '' ? null : form.interimAgencyId"
|
:model-value="form.interimAgencyId === '' ? null : form.interimAgencyId"
|
||||||
:options="interimAgencyOptions"
|
:options="interimAgencyOptions"
|
||||||
@@ -123,7 +126,7 @@
|
|||||||
min-width=""
|
min-width=""
|
||||||
@update:model-value="(v) => { form.interimAgencyId = v === null ? '' : Number(v) }"
|
@update:model-value="(v) => { form.interimAgencyId = v === null ? '' : Number(v) }"
|
||||||
/>
|
/>
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="form.contractId === '' ? null : form.contractId"
|
:model-value="form.contractId === '' ? null : form.contractId"
|
||||||
:options="contractFormOptions"
|
:options="contractFormOptions"
|
||||||
label="Temps de travail *"
|
label="Temps de travail *"
|
||||||
@@ -131,37 +134,27 @@
|
|||||||
:error="showContractError ? 'Le temps de travail est obligatoire.' : ''"
|
:error="showContractError ? 'Le temps de travail est obligatoire.' : ''"
|
||||||
@update:model-value="(v) => { form.contractId = v === null ? '' : Number(v) }"
|
@update:model-value="(v) => { form.contractId = v === null ? '' : Number(v) }"
|
||||||
/>
|
/>
|
||||||
<div>
|
<MalioDate
|
||||||
<label class="text-md font-semibold text-neutral-700" for="contract-start-date">
|
:model-value="form.contractStartDate"
|
||||||
Début contrat <span class="text-red-600">*</span>
|
label="Début contrat"
|
||||||
</label>
|
required
|
||||||
<input
|
:reserve-message-space="false"
|
||||||
id="contract-start-date"
|
:error="showContractStartDateError ? 'La date de début est obligatoire.' : ''"
|
||||||
v-model="form.contractStartDate"
|
group-class="w-full"
|
||||||
type="date"
|
@update:model-value="(v) => form.contractStartDate = v ?? ''"
|
||||||
:class="[dateInputBaseClass, form.contractStartDate ? 'border-black' : 'border-m-muted', showContractStartDateError ? '!border-m-danger' : '']"
|
/>
|
||||||
/>
|
<MalioDate
|
||||||
<p v-if="showContractStartDateError" class="mt-1 text-sm text-red-600">
|
v-if="showsContractEndDateComputed"
|
||||||
La date de début est obligatoire.
|
:model-value="form.contractEndDate"
|
||||||
</p>
|
label="Fin contrat"
|
||||||
</div>
|
:required="requiresContractEndDateComputed"
|
||||||
<div v-if="showsContractEndDateComputed">
|
:reserve-message-space="false"
|
||||||
<label class="text-md font-semibold text-neutral-700" for="contract-end-date">
|
:error="showContractEndDateError ? 'La date de fin est obligatoire pour un CDD ou un Intérim.' : ''"
|
||||||
Fin contrat
|
group-class="w-full"
|
||||||
<span v-if="requiresContractEndDateComputed" class="text-red-600">*</span>
|
@update:model-value="(v) => form.contractEndDate = v ?? ''"
|
||||||
</label>
|
/>
|
||||||
<input
|
|
||||||
id="contract-end-date"
|
|
||||||
v-model="form.contractEndDate"
|
|
||||||
type="date"
|
|
||||||
:class="[dateInputBaseClass, form.contractEndDate ? 'border-black' : 'border-m-muted', showContractEndDateError ? '!border-m-danger' : '']"
|
|
||||||
/>
|
|
||||||
<p v-if="showContractEndDateError" class="mt-1 text-sm text-red-600">
|
|
||||||
La date de fin est obligatoire pour un CDD ou un Intérim.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex h-10 items-center rounded-md border border-neutral-200 bg-neutral-50 px-3">
|
<div class="flex h-10 items-center rounded-md border border-neutral-200 bg-neutral-50 px-3">
|
||||||
<MalioCheckbox
|
<MalioCheckbox :reserve-message-space="false"
|
||||||
v-model="form.isDriver"
|
v-model="form.isDriver"
|
||||||
label="Chauffeur"
|
label="Chauffeur"
|
||||||
group-class="flex items-center"
|
group-class="flex items-center"
|
||||||
@@ -173,24 +166,29 @@
|
|||||||
:contract-weekly-hours="selectedContract?.weeklyHours ?? null"
|
:contract-weekly-hours="selectedContract?.weeklyHours ?? null"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex justify-end gap-3 pt-2">
|
<div class="grid grid-cols-2 gap-3 pt-2">
|
||||||
<MalioButton
|
<MalioButton
|
||||||
label="Annuler"
|
label="Annuler"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
|
button-class="w-full"
|
||||||
@click="isDrawerOpen = false"
|
@click="isDrawerOpen = false"
|
||||||
/>
|
/>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
type="submit"
|
|
||||||
label="Enregistrer"
|
label="Enregistrer"
|
||||||
|
button-class="w-full"
|
||||||
:disabled="isSubmitting || !isFormValid"
|
:disabled="isSubmitting || !isFormValid"
|
||||||
|
@click="handleSubmit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</MalioDrawer>
|
</MalioDrawer>
|
||||||
|
|
||||||
<MalioDrawer v-model="isExportDrawerOpen" title="Export">
|
<MalioDrawer v-model="isExportDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">Export</h2>
|
||||||
|
</template>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="exportChoice === '' ? null : exportChoice"
|
:model-value="exportChoice === '' ? null : exportChoice"
|
||||||
:options="exportTypeOptions"
|
:options="exportTypeOptions"
|
||||||
label="Type d'export"
|
label="Type d'export"
|
||||||
@@ -213,14 +211,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else-if="exportChoice === 'yearly-hours'">
|
<template v-else-if="exportChoice === 'yearly-hours'">
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="exportYear"
|
:model-value="exportYear"
|
||||||
:options="exportYearOptions"
|
:options="exportYearOptions"
|
||||||
label="Année *"
|
label="Année *"
|
||||||
min-width=""
|
min-width=""
|
||||||
@update:model-value="(v) => { if (v !== null) exportYear = Number(v) }"
|
@update:model-value="(v) => { if (v !== null) exportYear = Number(v) }"
|
||||||
/>
|
/>
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="exportMonth === '' ? null : exportMonth"
|
:model-value="exportMonth === '' ? null : exportMonth"
|
||||||
:options="exportMonthOptions"
|
:options="exportMonthOptions"
|
||||||
label="Mois *"
|
label="Mois *"
|
||||||
@@ -231,7 +229,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-else-if="exportChoice === 'night-contingent'">
|
<div v-else-if="exportChoice === 'night-contingent'">
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="exportYear"
|
:model-value="exportYear"
|
||||||
:options="exportYearOptions"
|
:options="exportYearOptions"
|
||||||
label="Année *"
|
label="Année *"
|
||||||
@@ -241,14 +239,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="exportChoice === 'overtime-contingent'" class="flex flex-col gap-4">
|
<div v-else-if="exportChoice === 'overtime-contingent'" class="flex flex-col gap-4">
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="exportYear"
|
:model-value="exportYear"
|
||||||
:options="exportYearOptions"
|
:options="exportYearOptions"
|
||||||
label="Année *"
|
label="Année *"
|
||||||
min-width=""
|
min-width=""
|
||||||
@update:model-value="(v) => { if (v !== null) exportYear = Number(v) }"
|
@update:model-value="(v) => { if (v !== null) exportYear = Number(v) }"
|
||||||
/>
|
/>
|
||||||
<MalioSelectCheckbox
|
<MalioSelectCheckbox :reserve-message-space="false"
|
||||||
v-model="exportSiteIds"
|
v-model="exportSiteIds"
|
||||||
:options="siteOptions"
|
:options="siteOptions"
|
||||||
label="Sites"
|
label="Sites"
|
||||||
@@ -467,9 +465,6 @@ const showContractEndDateError = computed(
|
|||||||
() => !editingEmployee.value && validationTouched.contractEndDate && !isContractEndDateValid.value
|
() => !editingEmployee.value && validationTouched.contractEndDate && !isContractEndDateValid.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const dateInputBaseClass =
|
|
||||||
'mt-2 h-10 w-full rounded-md border px-3 text-md text-black outline-none focus:border-2 focus:border-m-primary'
|
|
||||||
|
|
||||||
const formSiteOptions = computed(() =>
|
const formSiteOptions = computed(() =>
|
||||||
sites.value.map((site) => ({ label: site.name, value: site.id }))
|
sites.value.map((site) => ({ label: site.name, value: site.id }))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
:sites="sites"
|
:sites="sites"
|
||||||
:absence-types="absenceTypes"
|
:absence-types="absenceTypes"
|
||||||
:formatted-selected-date="formattedSelectedDate"
|
:formatted-selected-date="formattedSelectedDate"
|
||||||
|
:show-validation-calendar="true"
|
||||||
|
:marked-dates="markedDates"
|
||||||
:shortcut-button-class="shortcutButtonClass"
|
:shortcut-button-class="shortcutButtonClass"
|
||||||
:week-shortcut-button-class="weekShortcutButtonClass"
|
:week-shortcut-button-class="weekShortcutButtonClass"
|
||||||
:get-week-shortcut-label="getWeekShortcutLabel"
|
:get-week-shortcut-label="getWeekShortcutLabel"
|
||||||
@@ -39,6 +41,7 @@
|
|||||||
@set-this-week="setThisWeek"
|
@set-this-week="setThisWeek"
|
||||||
@set-next-week="setNextWeek"
|
@set-next-week="setNextWeek"
|
||||||
@shift-date="shiftDate"
|
@shift-date="shiftDate"
|
||||||
|
@month-change="onCalendarMonthChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="isLoading" class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
<div v-if="isLoading" class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
||||||
@@ -225,6 +228,8 @@ const {
|
|||||||
closeAbsenceDrawer,
|
closeAbsenceDrawer,
|
||||||
formatMinutes,
|
formatMinutes,
|
||||||
handleSave,
|
handleSave,
|
||||||
|
markedDates,
|
||||||
|
onCalendarMonthChange,
|
||||||
isWeekCommentDrawerOpen,
|
isWeekCommentDrawerOpen,
|
||||||
weekCommentContext,
|
weekCommentContext,
|
||||||
openWeekCommentDrawer,
|
openWeekCommentDrawer,
|
||||||
|
|||||||
@@ -9,14 +9,14 @@
|
|||||||
class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm"
|
class="mt-8 space-y-6 rounded-lg border border-neutral-200 bg-white p-6 shadow-sm"
|
||||||
@submit.prevent="handleSubmit"
|
@submit.prevent="handleSubmit"
|
||||||
>
|
>
|
||||||
<MalioInputText
|
<MalioInputText :reserve-message-space="false"
|
||||||
v-model="username"
|
v-model="username"
|
||||||
label="Nom d'utilisateur"
|
label="Nom d'utilisateur"
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
group-class="mt-2"
|
group-class="mt-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MalioInputPassword
|
<MalioInputPassword :reserve-message-space="false"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
label="Mot de passe"
|
label="Mot de passe"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="flex items-center justify-between pb-6">
|
<div class="flex items-center justify-between pb-6">
|
||||||
<h1 class="text-4xl font-bold text-primary-500">Sites</h1>
|
<h1 class="text-4xl font-bold text-primary-500">Sites</h1>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
label="Ajouter un site"
|
label="Ajouter"
|
||||||
icon-name="mdi:plus"
|
icon-name="mdi:plus"
|
||||||
icon-position="left"
|
icon-position="left"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
@@ -51,9 +51,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MalioDrawer v-model="isDrawerOpen" :title="drawerTitle">
|
<MalioDrawer v-model="isDrawerOpen">
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">{{ drawerTitle }}</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<MalioInputText
|
<MalioInputText :reserve-message-space="false"
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
label="Nom *"
|
label="Nom *"
|
||||||
group-class="mt-2"
|
group-class="mt-2"
|
||||||
|
|||||||
@@ -94,10 +94,12 @@
|
|||||||
|
|
||||||
<MalioDrawer
|
<MalioDrawer
|
||||||
v-model="isDrawerOpen"
|
v-model="isDrawerOpen"
|
||||||
:title="editingUser ? 'Modifier un utilisateur' : 'Ajouter un utilisateur'"
|
|
||||||
>
|
>
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[32px] font-semibold text-primary-500">{{ editingUser ? 'Modifier un utilisateur' : 'Ajouter un utilisateur' }}</h2>
|
||||||
|
</template>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<MalioInputText
|
<MalioInputText :reserve-message-space="false"
|
||||||
v-model="form.username"
|
v-model="form.username"
|
||||||
:label="editingUser ? `Nom d'utilisateur` : `Nom d'utilisateur *`"
|
:label="editingUser ? `Nom d'utilisateur` : `Nom d'utilisateur *`"
|
||||||
group-class="mt-2"
|
group-class="mt-2"
|
||||||
@@ -105,7 +107,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MalioInputPassword
|
<MalioInputPassword :reserve-message-space="false"
|
||||||
v-model="form.password"
|
v-model="form.password"
|
||||||
:label="editingUser ? 'Mot de passe' : 'Mot de passe *'"
|
:label="editingUser ? 'Mot de passe' : 'Mot de passe *'"
|
||||||
:hint="editingUser ? 'Laisse vide pour ne pas changer le mot de passe.' : ''"
|
:hint="editingUser ? 'Laisse vide pour ne pas changer le mot de passe.' : ''"
|
||||||
@@ -153,7 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="form.accessMode === 'self'">
|
<div v-if="form.accessMode === 'self'">
|
||||||
<MalioSelect
|
<MalioSelect :reserve-message-space="false"
|
||||||
:model-value="form.employeeId === '' ? null : form.employeeId"
|
:model-value="form.employeeId === '' ? null : form.employeeId"
|
||||||
:options="employeeOptions"
|
:options="employeeOptions"
|
||||||
label="Employé lié"
|
label="Employé lié"
|
||||||
@@ -172,7 +174,7 @@
|
|||||||
:key="site.id"
|
:key="site.id"
|
||||||
class="flex h-10 items-center rounded-md border border-neutral-200 px-3"
|
class="flex h-10 items-center rounded-md border border-neutral-200 px-3"
|
||||||
>
|
>
|
||||||
<MalioCheckbox
|
<MalioCheckbox :reserve-message-space="false"
|
||||||
:model-value="form.siteIds.includes(site.id)"
|
:model-value="form.siteIds.includes(site.id)"
|
||||||
:label="site.name"
|
:label="site.name"
|
||||||
group-class="flex items-center"
|
group-class="flex items-center"
|
||||||
@@ -186,7 +188,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MalioCheckbox
|
<MalioCheckbox :reserve-message-space="false"
|
||||||
v-model="form.isLocked"
|
v-model="form.isLocked"
|
||||||
label="Verrouiller le compte"
|
label="Verrouiller le compte"
|
||||||
hint="Un compte verrouillé ne peut plus se connecter."
|
hint="Un compte verrouillé ne peut plus se connecter."
|
||||||
@@ -194,7 +196,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MalioCheckbox
|
<MalioCheckbox :reserve-message-space="false"
|
||||||
v-model="form.hasLeaveRecapAccess"
|
v-model="form.hasLeaveRecapAccess"
|
||||||
label="Accès à l'écran Récap. congés"
|
label="Accès à l'écran Récap. congés"
|
||||||
hint="Affiche l'onglet dans la sidebar et donne accès au tableau récap."
|
hint="Affiche l'onglet dans la sidebar et donne accès au tableau récap."
|
||||||
|
|||||||
@@ -138,3 +138,23 @@ export const getWorkHourDayContext = async (workDate: string) => {
|
|||||||
{ toast: false }
|
{ toast: false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Jours entièrement validés (admin) sur une plage, pour colorer le calendrier de
|
||||||
|
// la vue Jour. `validatedDays` = liste de dates Y-m-d (cf. doc/hours-validated-days).
|
||||||
|
// `driver` : true → écran Heures Conducteurs (seuls les conducteurs), false → écran Heures.
|
||||||
|
export const getWorkHourValidationStatus = async (
|
||||||
|
from: string,
|
||||||
|
to: string,
|
||||||
|
options?: { driver?: boolean }
|
||||||
|
) => {
|
||||||
|
const api = useApi()
|
||||||
|
const query: Record<string, string> = { from, to }
|
||||||
|
if (options?.driver) query.driver = '1'
|
||||||
|
const data = await api.get<{ from: string; to: string; validatedDays: string[] }>(
|
||||||
|
'/work-hours/validation-status',
|
||||||
|
query,
|
||||||
|
{ toast: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
return data?.validatedDays ?? []
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\ApiResource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use App\State\WorkHourValidationStatusProvider;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/work-hours/validation-status',
|
||||||
|
security: "is_granted('ROLE_USER')",
|
||||||
|
provider: WorkHourValidationStatusProvider::class
|
||||||
|
),
|
||||||
|
],
|
||||||
|
paginationEnabled: false
|
||||||
|
)]
|
||||||
|
final class WorkHourValidationStatus
|
||||||
|
{
|
||||||
|
public string $from = '';
|
||||||
|
|
||||||
|
public string $to = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jours entièrement validés (admin) sur la plage, au format Y-m-d.
|
||||||
|
* Un jour est présent ssi il porte au moins une ligne (non-conducteur)
|
||||||
|
* et aucune n'est en attente de validation (isValid=false).
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
public array $validatedDays = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\ApiResource\WorkHourValidationStatus;
|
||||||
|
use App\Entity\Employee;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
||||||
|
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
||||||
|
use App\Service\Contracts\EmployeeContractResolver;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statut de validation par jour pour le calendrier de la vue Jour (écran Heures).
|
||||||
|
*
|
||||||
|
* Un jour est « entièrement validé » (peint en vert côté front) ssi, dans le
|
||||||
|
* périmètre de l'utilisateur (admin = tous les sites, chef de site = ses sites) :
|
||||||
|
* - il porte au moins une ligne WorkHour de non-conducteur ce jour-là, ET
|
||||||
|
* - aucune de ces lignes n'est en attente de validation (isValid=false).
|
||||||
|
*
|
||||||
|
* Par défaut les conducteurs sont exclus (écran Heures). Avec `?driver=1`, le filtre
|
||||||
|
* s'inverse et seuls les conducteurs sont pris en compte (écran Heures Conducteurs).
|
||||||
|
* Le filtre sites de l'écran est volontairement ignoré :
|
||||||
|
* le statut reflète tout le périmètre (objectif RH : repérer le moindre jour
|
||||||
|
* incomplet, où qu'il soit). Un jour sans aucune ligne reste neutre (absent de
|
||||||
|
* la liste).
|
||||||
|
*/
|
||||||
|
final readonly class WorkHourValidationStatusProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
/** Garde-fou : borne la plage demandée pour éviter une requête démesurée. */
|
||||||
|
private const int MAX_RANGE_DAYS = 366;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private Security $security,
|
||||||
|
private RequestStack $requestStack,
|
||||||
|
private EmployeeScopedRepositoryInterface $employeeRepository,
|
||||||
|
private WorkHourReadRepositoryInterface $workHourRepository,
|
||||||
|
private EmployeeContractResolver $contractResolver,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourValidationStatus
|
||||||
|
{
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw new AccessDeniedHttpException('Authentication required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
[$from, $to] = $this->resolveRange();
|
||||||
|
// ?driver=1 → ne garder que les conducteurs (écran Heures Conducteurs) ;
|
||||||
|
// défaut → ne garder que les non-conducteurs (écran Heures).
|
||||||
|
$driverOnly = filter_var(
|
||||||
|
$this->requestStack->getCurrentRequest()?->query->get('driver'),
|
||||||
|
FILTER_VALIDATE_BOOLEAN
|
||||||
|
);
|
||||||
|
|
||||||
|
$employees = $this->employeeRepository->findScoped($user);
|
||||||
|
$workHours = $this->workHourRepository->findByDateRangeAndEmployees($from, $to, $employees);
|
||||||
|
|
||||||
|
// Agrégation par jour : total = lignes non-conducteur, pending = lignes isValid=false.
|
||||||
|
/** @var array<string, array{total:int, pending:int}> $byDate */
|
||||||
|
$byDate = [];
|
||||||
|
// Mémoïsation de la résolution conducteur par (employé, jour) : un même
|
||||||
|
// couple peut revenir et resolveIsDriver... interroge la BDD.
|
||||||
|
$driverCache = [];
|
||||||
|
|
||||||
|
foreach ($workHours as $workHour) {
|
||||||
|
$employee = $workHour->getEmployee();
|
||||||
|
if (!$employee instanceof Employee) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$date = DateTimeImmutable::createFromInterface($workHour->getWorkDate());
|
||||||
|
$dateKey = $date->format('Y-m-d');
|
||||||
|
|
||||||
|
$cacheKey = $employee->getId().'|'.$dateKey;
|
||||||
|
$isDriver = $driverCache[$cacheKey]
|
||||||
|
??= $this->contractResolver->resolveIsDriverForEmployeeAndDate($employee, $date);
|
||||||
|
if ($isDriver !== $driverOnly) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bucket = &$byDate[$dateKey];
|
||||||
|
$bucket ??= ['total' => 0, 'pending' => 0];
|
||||||
|
++$bucket['total'];
|
||||||
|
if (!$workHour->isValid()) {
|
||||||
|
++$bucket['pending'];
|
||||||
|
}
|
||||||
|
unset($bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validatedDays = [];
|
||||||
|
foreach ($byDate as $dateKey => $counts) {
|
||||||
|
if ($counts['total'] > 0 && 0 === $counts['pending']) {
|
||||||
|
$validatedDays[] = $dateKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort($validatedDays);
|
||||||
|
|
||||||
|
$response = new WorkHourValidationStatus();
|
||||||
|
$response->from = $from->format('Y-m-d');
|
||||||
|
$response->to = $to->format('Y-m-d');
|
||||||
|
$response->validatedDays = $validatedDays;
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: DateTimeImmutable, 1: DateTimeImmutable}
|
||||||
|
*/
|
||||||
|
private function resolveRange(): array
|
||||||
|
{
|
||||||
|
$query = $this->requestStack->getCurrentRequest()?->query;
|
||||||
|
$from = $this->parseDate((string) ($query?->get('from') ?? ''), 'from');
|
||||||
|
$to = $this->parseDate((string) ($query?->get('to') ?? ''), 'to');
|
||||||
|
|
||||||
|
if ($from > $to) {
|
||||||
|
throw new UnprocessableEntityHttpException('from must be before or equal to to.');
|
||||||
|
}
|
||||||
|
if ($from->diff($to)->days > self::MAX_RANGE_DAYS) {
|
||||||
|
throw new UnprocessableEntityHttpException(sprintf('Range must not exceed %d days.', self::MAX_RANGE_DAYS));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$from, $to];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseDate(string $raw, string $field): DateTimeImmutable
|
||||||
|
{
|
||||||
|
$date = DateTimeImmutable::createFromFormat('Y-m-d', $raw);
|
||||||
|
if (!$date || $date->format('Y-m-d') !== $raw) {
|
||||||
|
throw new UnprocessableEntityHttpException(sprintf('%s must use Y-m-d format.', $field));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalise à minuit pour comparer des jours, pas des instants.
|
||||||
|
return $date->setTime(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use App\Entity\Employee;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Entity\WorkHour;
|
||||||
|
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
||||||
|
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
||||||
|
use App\Service\Contracts\EmployeeContractResolver;
|
||||||
|
use App\State\WorkHourValidationStatusProvider;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use ReflectionObject;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class WorkHourValidationStatusProviderTest extends TestCase
|
||||||
|
{
|
||||||
|
private Security $security;
|
||||||
|
private EmployeeScopedRepositoryInterface $employeeRepository;
|
||||||
|
private WorkHourReadRepositoryInterface $workHourRepository;
|
||||||
|
private RequestStack $requestStack;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->security = $this->createStub(Security::class);
|
||||||
|
$this->employeeRepository = $this->createStub(EmployeeScopedRepositoryInterface::class);
|
||||||
|
$this->workHourRepository = $this->createStub(WorkHourReadRepositoryInterface::class);
|
||||||
|
$this->requestStack = new RequestStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsWhenAnonymous(): void
|
||||||
|
{
|
||||||
|
$this->security->method('getUser')->willReturn(null);
|
||||||
|
|
||||||
|
$this->expectException(AccessDeniedHttpException::class);
|
||||||
|
$this->buildProvider()->provide(new Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsWhenDateFormatInvalid(): void
|
||||||
|
{
|
||||||
|
$this->security->method('getUser')->willReturn(new User());
|
||||||
|
$this->requestStack->push(new Request(query: ['from' => '01-06-2026', 'to' => '2026-06-30']));
|
||||||
|
|
||||||
|
$this->expectException(UnprocessableEntityHttpException::class);
|
||||||
|
$this->buildProvider()->provide(new Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsWhenFromAfterTo(): void
|
||||||
|
{
|
||||||
|
$this->security->method('getUser')->willReturn(new User());
|
||||||
|
$this->requestStack->push(new Request(query: ['from' => '2026-06-30', 'to' => '2026-06-01']));
|
||||||
|
|
||||||
|
$this->expectException(UnprocessableEntityHttpException::class);
|
||||||
|
$this->buildProvider()->provide(new Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsWhenRangeTooLarge(): void
|
||||||
|
{
|
||||||
|
$this->security->method('getUser')->willReturn(new User());
|
||||||
|
$this->requestStack->push(new Request(query: ['from' => '2024-01-01', 'to' => '2026-01-01']));
|
||||||
|
|
||||||
|
$this->expectException(UnprocessableEntityHttpException::class);
|
||||||
|
$this->buildProvider()->provide(new Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testComputesValidatedDays(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$alice = $this->buildEmployee(1);
|
||||||
|
$bob = $this->buildEmployee(2);
|
||||||
|
$driver = $this->buildEmployee(3);
|
||||||
|
|
||||||
|
// 2026-06-01 : Alice + Bob validés → vert.
|
||||||
|
// 2026-06-02 : Alice validée, Bob en attente → pas vert.
|
||||||
|
// 2026-06-03 : seul un conducteur (validé) → exclu → total non-conducteur 0 → pas vert.
|
||||||
|
$workHours = [
|
||||||
|
$this->buildWorkHour($alice, '2026-06-01', true),
|
||||||
|
$this->buildWorkHour($bob, '2026-06-01', true),
|
||||||
|
$this->buildWorkHour($alice, '2026-06-02', true),
|
||||||
|
$this->buildWorkHour($bob, '2026-06-02', false),
|
||||||
|
$this->buildWorkHour($driver, '2026-06-03', true),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->security->method('getUser')->willReturn($user);
|
||||||
|
$this->requestStack->push(new Request(query: ['from' => '2026-06-01', 'to' => '2026-06-30']));
|
||||||
|
$this->employeeRepository->method('findScoped')->with($user)->willReturn([$alice, $bob, $driver]);
|
||||||
|
$this->workHourRepository->method('findByDateRangeAndEmployees')->willReturn($workHours);
|
||||||
|
|
||||||
|
$resolver = $this->createStub(EmployeeContractResolver::class);
|
||||||
|
$resolver->method('resolveIsDriverForEmployeeAndDate')
|
||||||
|
->willReturnCallback(static fn (Employee $e): bool => 3 === $e->getId())
|
||||||
|
;
|
||||||
|
|
||||||
|
$result = $this->buildProvider($resolver)->provide(new Get());
|
||||||
|
|
||||||
|
self::assertSame('2026-06-01', $result->from);
|
||||||
|
self::assertSame('2026-06-30', $result->to);
|
||||||
|
self::assertSame(['2026-06-01'], $result->validatedDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testComputesValidatedDaysForDriverScope(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$alice = $this->buildEmployee(1); // non-conducteur
|
||||||
|
$driver = $this->buildEmployee(3); // conducteur
|
||||||
|
|
||||||
|
// ?driver=1 : 01/06 conducteur validé → vert ; 02/06 conducteur en attente → non ;
|
||||||
|
// 03/06 seule Alice (non-conducteur) validée → ignorée → non.
|
||||||
|
$workHours = [
|
||||||
|
$this->buildWorkHour($driver, '2026-06-01', true),
|
||||||
|
$this->buildWorkHour($driver, '2026-06-02', false),
|
||||||
|
$this->buildWorkHour($alice, '2026-06-03', true),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->security->method('getUser')->willReturn($user);
|
||||||
|
$this->requestStack->push(new Request(query: ['from' => '2026-06-01', 'to' => '2026-06-30', 'driver' => '1']));
|
||||||
|
$this->employeeRepository->method('findScoped')->willReturn([$alice, $driver]);
|
||||||
|
$this->workHourRepository->method('findByDateRangeAndEmployees')->willReturn($workHours);
|
||||||
|
|
||||||
|
$resolver = $this->createStub(EmployeeContractResolver::class);
|
||||||
|
$resolver->method('resolveIsDriverForEmployeeAndDate')
|
||||||
|
->willReturnCallback(static fn (Employee $e): bool => 3 === $e->getId())
|
||||||
|
;
|
||||||
|
|
||||||
|
$result = $this->buildProvider($resolver)->provide(new Get());
|
||||||
|
|
||||||
|
self::assertSame(['2026-06-01'], $result->validatedDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmptyWhenNoWorkHours(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$this->security->method('getUser')->willReturn($user);
|
||||||
|
$this->requestStack->push(new Request(query: ['from' => '2026-06-01', 'to' => '2026-06-30']));
|
||||||
|
$this->employeeRepository->method('findScoped')->willReturn([]);
|
||||||
|
$this->workHourRepository->method('findByDateRangeAndEmployees')->willReturn([]);
|
||||||
|
|
||||||
|
$result = $this->buildProvider()->provide(new Get());
|
||||||
|
|
||||||
|
self::assertSame([], $result->validatedDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildProvider(?EmployeeContractResolver $resolver = null): WorkHourValidationStatusProvider
|
||||||
|
{
|
||||||
|
$resolver ??= $this->createStub(EmployeeContractResolver::class);
|
||||||
|
|
||||||
|
return new WorkHourValidationStatusProvider(
|
||||||
|
$this->security,
|
||||||
|
$this->requestStack,
|
||||||
|
$this->employeeRepository,
|
||||||
|
$this->workHourRepository,
|
||||||
|
$resolver,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildEmployee(int $id): Employee
|
||||||
|
{
|
||||||
|
$employee = new Employee()
|
||||||
|
->setFirstName('Test')
|
||||||
|
->setLastName('Employee')
|
||||||
|
;
|
||||||
|
$reflection = new ReflectionObject($employee);
|
||||||
|
$property = $reflection->getProperty('id');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue($employee, $id);
|
||||||
|
|
||||||
|
return $employee;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildWorkHour(Employee $employee, string $date, bool $isValid): WorkHour
|
||||||
|
{
|
||||||
|
return new WorkHour()
|
||||||
|
->setEmployee($employee)
|
||||||
|
->setWorkDate(new DateTimeImmutable($date))
|
||||||
|
->setIsValid($isValid)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user