Compare commits
1 Commits
v0.1.100
...
bd93c52197
| Author | SHA1 | Date | |
|---|---|---|---|
| bd93c52197 |
@@ -48,12 +48,6 @@
|
|||||||
- Saisie d'heures (ou de jours de présence) autorisée sur un férié
|
- Saisie d'heures (ou de jours de présence) autorisée sur un férié
|
||||||
- **Crédit automatique des heures contractuelles** sur un férié Lun-Ven pour tout contrat hors Forfait, **uniquement en l'absence d'absence déclarée** : le total journalier = `max(saisie + credited_absence, référence_contractuelle)`. Référence : 35h→7h, 39h→8h Lun-Jeu/7h Ven, CUSTOM→weeklyHours/5, INTERIM→idem 35h/39h/custom selon weeklyHours. Aucune ligne BDD créée (crédit virtuel). Drivers : crédité en `dayHoursMinutes`. Impacte directement le total hebdo RTT (tranches 25%/50%). Dès qu'une absence est posée sur le férié, le crédit virtuel saute — c'est le `countAsWorkedHours` du type d'absence qui pilote. Services : `App\Service\WorkHours\HolidayVirtualHoursResolver` + `DailyReferenceMinutesResolver`. Doc complète : `doc/holiday-virtual-hours.md`.
|
- **Crédit automatique des heures contractuelles** sur un férié Lun-Ven pour tout contrat hors Forfait, **uniquement en l'absence d'absence déclarée** : le total journalier = `max(saisie + credited_absence, référence_contractuelle)`. Référence : 35h→7h, 39h→8h Lun-Jeu/7h Ven, CUSTOM→weeklyHours/5, INTERIM→idem 35h/39h/custom selon weeklyHours. Aucune ligne BDD créée (crédit virtuel). Drivers : crédité en `dayHoursMinutes`. Impacte directement le total hebdo RTT (tranches 25%/50%). Dès qu'une absence est posée sur le férié, le crédit virtuel saute — c'est le `countAsWorkedHours` du type d'absence qui pilote. Services : `App\Service\WorkHours\HolidayVirtualHoursResolver` + `DailyReferenceMinutesResolver`. Doc complète : `doc/holiday-virtual-hours.md`.
|
||||||
|
|
||||||
## Commentaires de semaine
|
|
||||||
- Entité `EmployeeWeekComment` : commentaire libre par employé et semaine ISO (unique `(employee_id, week_start_date)`). `week_start_date` = lundi.
|
|
||||||
- CRUD `/employee_week_comments` `ROLE_ADMIN`. Write processor audite via `AuditLogger`.
|
|
||||||
- Picto bulle vue semaine (HoursWeekView + DriverHoursWeekView) : fond bleu/rouge. Intégré dans `WeeklySummaryRow.comment/commentId`.
|
|
||||||
- Doc : `doc/week-comments.md`.
|
|
||||||
|
|
||||||
## Validation Rules
|
## Validation Rules
|
||||||
- `isValid` (RH): locks line for everyone (admin can only untoggle validation)
|
- `isValid` (RH): locks line for everyone (admin can only untoggle validation)
|
||||||
- `isSiteValid` (site manager): locks for non-admin, admin can still edit
|
- `isSiteValid` (site manager): locks for non-admin, admin can still edit
|
||||||
@@ -67,7 +61,6 @@
|
|||||||
- INTERIM: no overtime bonuses, no recovery time
|
- INTERIM: no overtime bonuses, no recovery time
|
||||||
- Driver contracts: RTT uses `dayHoursMinutes + nightHoursMinutes + workshopHoursMinutes` instead of morning/afternoon/evening time ranges
|
- Driver contracts: RTT uses `dayHoursMinutes + nightHoursMinutes + workshopHoursMinutes` instead of morning/afternoon/evening time ranges
|
||||||
- FORFAIT weekend/holiday bonus: each weekend or public holiday day worked gives bonus leave (full day if morning+afternoon, 0.5 if only one). Added to acquired days, no cap. PRESENCE mode only.
|
- FORFAIT weekend/holiday bonus: each weekend or public holiday day worked gives bonus leave (full day if morning+afternoon, 0.5 if only one). Added to acquired days, no cap. PRESENCE mode only.
|
||||||
- **FORFAIT — jours de présence et N-1** : les congés posés et imputés sur le stock N-1 ne décrémentent **pas** les jours de présence affichés (`presenceDaysByMonth` et `presenceDaysToToday`). Implémenté dans `EmployeeLeaveSummaryProvider::computePresenceDaysByMonth` via un budget N-1 (= `previousYearTakenDays`) consommé chronologiquement avant comptage des absences. Pour les non-forfait, ce budget vaut toujours 0 → comportement inchangé.
|
|
||||||
|
|
||||||
## Récap. congés (écran)
|
## Récap. congés (écran)
|
||||||
- Accès via sidebar `Récap. congés`, conditionné au flag `User.hasLeaveRecapAccess` (défaut `false`) — activé au create/edit user. Le flag s'applique à tous les profils, y compris admin.
|
- Accès via sidebar `Récap. congés`, conditionné au flag `User.hasLeaveRecapAccess` (défaut `false`) — activé au create/edit user. Le flag s'applique à tous les profils, y compris admin.
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.100'
|
app.version: '0.1.95'
|
||||||
|
|||||||
@@ -173,7 +173,6 @@ Documents complementaires:
|
|||||||
- Exclusions configurables: variable d'env `EXCLUDED_PUBLIC_HOLIDAYS` (liste de libellés séparés par virgules). Par défaut `"Lundi de Pentecôte"` — journée de solidarité généralement travaillée. Le filtre s'applique à tous les consommateurs (frontend + calculs backend) en amont du retour du service.
|
- Exclusions configurables: variable d'env `EXCLUDED_PUBLIC_HOLIDAYS` (liste de libellés séparés par virgules). Par défaut `"Lundi de Pentecôte"` — journée de solidarité généralement travaillée. Le filtre s'applique à tous les consommateurs (frontend + calculs backend) en amont du retour du service.
|
||||||
- Onglet congés: jours fériés affichés sur le calendrier avec fond `rgb(179, 229, 252)` et nom au survol
|
- Onglet congés: jours fériés affichés sur le calendrier avec fond `rgb(179, 229, 252)` et nom au survol
|
||||||
- Écran Heures et Heures Conducteurs (vue jour): le nom du férié est affiché dans la colonne Absence sous forme de pill (fond `#b3e5fc`, icône `mdi:calendar-star`), distinct du pill absence
|
- Écran Heures et Heures Conducteurs (vue jour): le nom du férié est affiché dans la colonne Absence sous forme de pill (fond `#b3e5fc`, icône `mdi:calendar-star`), distinct du pill absence
|
||||||
- Écran Heures et Heures Conducteurs (vue semaine): la cellule du jour férié prend le fond `#b3e5fc` quand l'employé n'a pas d'absence ce jour-là, avec le nom du férié au survol (`title`). Si une absence est posée, la couleur de l'absence prime ; le `title` cumule les deux libellés (`Absence — Férié : Nom`).
|
|
||||||
- Règle courante:
|
- Règle courante:
|
||||||
- absences autorisées sur jour férié (création/édition depuis l'écran Heures et le Calendrier). Quand une absence est posée, le crédit virtuel férié est désactivé — c'est le `countAsWorkedHours` du type d'absence qui pilote
|
- absences autorisées sur jour férié (création/édition depuis l'écran Heures et le Calendrier). Quand une absence est posée, le crédit virtuel férié est désactivé — c'est le `countAsWorkedHours` du type d'absence qui pilote
|
||||||
- saisie d'heures ou de jours de présence autorisée — les heures saisies comptent normalement dans le total hebdo et le calcul RTT
|
- saisie d'heures ou de jours de présence autorisée — les heures saisies comptent normalement dans le total hebdo et le calcul RTT
|
||||||
@@ -314,7 +313,6 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
|
|||||||
- les heures payées sont soustraites du disponible RTT (`availableMinutes -= totalPaidMinutes`)
|
- les heures payées sont soustraites du disponible RTT (`availableMinutes -= totalPaidMinutes`)
|
||||||
- affichage: 2 lignes par mois dans le tableau (25% et 50%)
|
- affichage: 2 lignes par mois dans le tableau (25% et 50%)
|
||||||
- colonnes Total 25% et Total 50%: somme base + bonus de chaque tranche
|
- colonnes Total 25% et Total 50%: somme base + bonus de chaque tranche
|
||||||
- colonne Cumul (dernière colonne): solde RTT à la fin de chaque semaine = `report N-1 + somme totalMinutes des semaines jusqu'à celle-ci − paiements RTT des mois antérieurs au mois de la semaine`. Le paiement d'un mois M n'est déduit qu'à partir des semaines du mois M+1 (cohérent avec la logique de la ligne "Report mois précédent"). Permet la comparaison ligne à ligne avec un suivi RH externe (Excel)
|
|
||||||
- ligne Report N-1 (carry rollover): affichée en juin uniquement si carry > 0
|
- ligne Report N-1 (carry rollover): affichée en juin uniquement si carry > 0
|
||||||
- ligne Report mois précédent: solde cumulé (carry N-1 + semaines antérieures − paiements antérieurs), affichée à partir de juillet (masquée si nul)
|
- ligne Report mois précédent: solde cumulé (carry N-1 + semaines antérieures − paiements antérieurs), affichée à partir de juillet (masquée si nul)
|
||||||
- Reste = Report cumulé + Total du mois − Payé du mois (balance courante en fin de mois)
|
- Reste = Report cumulé + Total du mois − Payé du mois (balance courante en fin de mois)
|
||||||
@@ -337,7 +335,7 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
|
|||||||
| Contrat | Contract.name |
|
| Contrat | Contract.name |
|
||||||
| CP N-1 restant | CDI/CDD: acquis N-1 − pris sur N-1. Forfait: report N-1 restant |
|
| CP N-1 restant | CDI/CDD: acquis N-1 − pris sur N-1. Forfait: report N-1 restant |
|
||||||
| Samedi restant | CDI/CDD: samedis acquis N-1 − pris. Forfait: `-` |
|
| Samedi restant | CDI/CDD: samedis acquis N-1 − pris. Forfait: `-` |
|
||||||
| CP N | Forfait: restant sur quota année civile (acquis − pris depuis N, sans toucher au stock N-1). Non-forfait: en cours d'acquisition |
|
| CP N | Forfait: jours acquis année civile. Non-forfait: en cours d'acquisition |
|
||||||
| RTT | Minutes disponibles (report N-1 + acquis N - payés). Format `X h Y m`. Forfait et INTERIM: `-` |
|
| RTT | Minutes disponibles (report N-1 + acquis N - payés). Format `X h Y m`. Forfait et INTERIM: `-` |
|
||||||
|
|
||||||
## 10bis) Écran Récap. congés (tableau)
|
## 10bis) Écran Récap. congés (tableau)
|
||||||
@@ -380,7 +378,7 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
|
|||||||
| Maladie - Nombre | Absence code 'M' ou 'AT' | Jours (demi-journées = 0.5) |
|
| Maladie - Nombre | Absence code 'M' ou 'AT' | Jours (demi-journées = 0.5) |
|
||||||
| Maladie - Date | Absence code 'M' ou 'AT' | Dates formatées dd/mm |
|
| Maladie - Date | Absence code 'M' ou 'AT' | Dates formatées dd/mm |
|
||||||
| CHAUFFEUR - PDJ | WorkHour.hasBreakfast | Comptage mois (chauffeurs uniquement) |
|
| CHAUFFEUR - PDJ | WorkHour.hasBreakfast | Comptage mois (chauffeurs uniquement) |
|
||||||
| CHAUFFEUR - REPAS | WorkHour.hasLunch + hasDinner | Somme sur le mois : +1 par déjeuner coché et +1 par dîner coché (un jour avec les deux compte 2 repas, chauffeurs uniquement) |
|
| CHAUFFEUR - REPAS | WorkHour.hasLunch + hasDinner | Comptage mois (chauffeurs uniquement) |
|
||||||
| CHAUFFEUR - NUITEE | WorkHour.hasOvernight | Comptage mois (chauffeurs uniquement) |
|
| CHAUFFEUR - NUITEE | WorkHour.hasOvernight | Comptage mois (chauffeurs uniquement) |
|
||||||
| CHAUFFEUR - samedi | WorkHour (samedi) | Samedis travaillés (chauffeurs uniquement) |
|
| CHAUFFEUR - samedi | WorkHour (samedi) | Samedis travaillés (chauffeurs uniquement) |
|
||||||
| Observations | — | Colonne vide pour saisie manuelle |
|
| Observations | — | Colonne vide pour saisie manuelle |
|
||||||
@@ -444,8 +442,7 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
|
|||||||
- Accessible depuis la fiche employé (bouton imprimante à droite du nom)
|
- Accessible depuis la fiche employé (bouton imprimante à droite du nom)
|
||||||
- Ouvre un drawer pour choisir l'année (civile, Jan-Déc)
|
- Ouvre un drawer pour choisir l'année (civile, Jan-Déc)
|
||||||
- Génère un PDF avec le détail jour par jour des heures de l'employé
|
- Génère un PDF avec le détail jour par jour des heures de l'employé
|
||||||
- Seuls les jours avec heures saisies, absence, week-end ou jour férié sont affichés
|
- Seuls les jours avec heures saisies ou absence sont affichés
|
||||||
- Les jours fériés apparaissent toujours sur une ligne dédiée (fond bleu clair) avec la mention "Férié : {nom}" dans la colonne Absence (même si aucune saisie)
|
|
||||||
|
|
||||||
### Colonnes selon le mode de suivi
|
### Colonnes selon le mode de suivi
|
||||||
|
|
||||||
@@ -463,7 +460,6 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
|
|||||||
- TIME non-chauffeur: somme des créneaux matin + après-midi + soir, plus minutes créditées des absences `countAsWorkedHours`
|
- TIME non-chauffeur: somme des créneaux matin + après-midi + soir, plus minutes créditées des absences `countAsWorkedHours`
|
||||||
- Chauffeur: `dayHoursMinutes + nightHoursMinutes + workshopHoursMinutes` + minutes créditées
|
- Chauffeur: `dayHoursMinutes + nightHoursMinutes + workshopHoursMinutes` + minutes créditées
|
||||||
- PRESENCE: 0.5 par demi-journée présente (matin/après-midi), max 1.0
|
- PRESENCE: 0.5 par demi-journée présente (matin/après-midi), max 1.0
|
||||||
- Jour férié Lun-Ven (hors Forfait, sans absence) : `total = max(saisie + crédit absence, référence contractuelle)` — même règle que l'écran Heures (cf. `HolidayVirtualHoursResolver`). Pour Forfait : pas de crédit virtuel, la ligne férié affiche juste l'éventuelle présence saisie.
|
|
||||||
|
|
||||||
### Nom du fichier
|
### Nom du fichier
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,8 @@
|
|||||||
{{ row.firstName }} {{ row.lastName }}
|
{{ row.firstName }} {{ row.lastName }}
|
||||||
<span class="font-normal text-neutral-600">({{ row.contractName ?? '-' }})</span>
|
<span class="font-normal text-neutral-600">({{ row.contractName ?? '-' }})</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-[11px] text-neutral-500 truncate inline-flex items-center gap-2">
|
<p class="text-[11px] text-neutral-500 truncate">
|
||||||
<span>{{ row.siteName ?? 'Sans site' }}<span v-if="row.contractNature"> — {{ contractNatureLabel(row.contractNature) }}</span></span>
|
{{ row.siteName ?? 'Sans site' }}<span v-if="row.contractNature"> — {{ contractNatureLabel(row.contractNature) }}</span>
|
||||||
<button v-if="isAdmin" type="button" class="inline-flex items-center justify-center rounded-md p-1 text-white transition-colors" :class="row.comment ? 'bg-red-500 hover:bg-red-600' : 'bg-primary-500 hover:bg-secondary-500'" :title="row.comment ?? 'Ajouter un commentaire'" @click="$emit('open-comment', row)">
|
|
||||||
<Icon name="mdi:comment-text-outline" size="12"/>
|
|
||||||
</button>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -47,7 +44,7 @@
|
|||||||
class="text-left leading-4 rounded-md px-2 py-1"
|
class="text-left leading-4 rounded-md px-2 py-1"
|
||||||
:class="daily.hasAbsence ? 'text-white' : ''"
|
:class="daily.hasAbsence ? 'text-white' : ''"
|
||||||
:style="getDailyCellStyle(daily)"
|
:style="getDailyCellStyle(daily)"
|
||||||
:title="cellTitle(daily)"
|
:title="daily.absenceLabel ?? ''"
|
||||||
>
|
>
|
||||||
<div>J {{ formatMinutes(daily.dayMinutes) }}</div>
|
<div>J {{ formatMinutes(daily.dayMinutes) }}</div>
|
||||||
<div>N {{ formatMinutes(daily.nightMinutes) }}</div>
|
<div>N {{ formatMinutes(daily.nightMinutes) }}</div>
|
||||||
@@ -96,37 +93,19 @@
|
|||||||
import type { WeeklyWorkHourSummary } from '~/services/dto/work-hour'
|
import type { WeeklyWorkHourSummary } from '~/services/dto/work-hour'
|
||||||
import { contractNatureLabel } from '~/utils/contract'
|
import { contractNatureLabel } from '~/utils/contract'
|
||||||
|
|
||||||
const HOLIDAY_BG_COLOR = '#b3e5fc'
|
|
||||||
|
|
||||||
const getDailyCellStyle = (daily: {
|
const getDailyCellStyle = (daily: {
|
||||||
hasAbsence?: boolean
|
hasAbsence?: boolean
|
||||||
absenceColor?: string | null
|
absenceColor?: string | null
|
||||||
holidayLabel?: string | null
|
|
||||||
}) => {
|
}) => {
|
||||||
if (daily.hasAbsence) return { backgroundColor: daily.absenceColor || '#dc2626' }
|
if (!daily.hasAbsence) return undefined
|
||||||
if (daily.holidayLabel) return { backgroundColor: HOLIDAY_BG_COLOR }
|
return { backgroundColor: daily.absenceColor || '#dc2626' }
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellTitle = (daily: {
|
|
||||||
hasAbsence?: boolean
|
|
||||||
absenceLabel?: string | null
|
|
||||||
holidayLabel?: string | null
|
|
||||||
}) => {
|
|
||||||
const parts: string[] = []
|
|
||||||
if (daily.absenceLabel) parts.push(daily.absenceLabel)
|
|
||||||
if (daily.holidayLabel) parts.push(`Férié : ${daily.holidayLabel}`)
|
|
||||||
return parts.join(' — ')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
isWeekLoading: boolean
|
isWeekLoading: boolean
|
||||||
isAdmin: boolean
|
|
||||||
weekGridCols: string
|
weekGridCols: string
|
||||||
weeklySummary: WeeklyWorkHourSummary | null
|
weeklySummary: WeeklyWorkHourSummary | null
|
||||||
weekDayHeaders: Array<{ date: string; weekday: string; dayDate: string }>
|
weekDayHeaders: Array<{ date: string; weekday: string; dayDate: string }>
|
||||||
formatMinutes: (minutes: number) => string
|
formatMinutes: (minutes: number) => string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{ (e: 'open-comment', row: WeeklyWorkHourSummary['rows'][number]): void }>()
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -40,15 +40,14 @@
|
|||||||
<table class="w-full table-fixed border-collapse text-[18px]">
|
<table class="w-full table-fixed border-collapse text-[18px]">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col />
|
<col />
|
||||||
<col class="w-[10%]" />
|
<col class="w-[11%]" />
|
||||||
<col class="w-[10%]" />
|
<col class="w-[11%]" />
|
||||||
<col class="w-[10%]" />
|
<col class="w-[11%]" />
|
||||||
<col class="w-[10%]" />
|
<col class="w-[11%]" />
|
||||||
<col class="w-[10%]" />
|
<col class="w-[11%]" />
|
||||||
<col class="w-[10%]" />
|
<col class="w-[11%]" />
|
||||||
<col class="w-[10%]" />
|
<col class="w-[11%]" />
|
||||||
<col class="w-[10%]" />
|
<col class="w-[11%]" />
|
||||||
<col class="w-[10%]" />
|
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -60,8 +59,7 @@
|
|||||||
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">Base</th>
|
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">Base</th>
|
||||||
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">50%</th>
|
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">50%</th>
|
||||||
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">Total 50%</th>
|
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">Total 50%</th>
|
||||||
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">Total</th>
|
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">Total</th>
|
||||||
<th class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">Cumul</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -75,7 +73,6 @@
|
|||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(summary!.carryBase50Minutes) }} <span class="text-neutral-400">/ {{ formatCentiemes(summary!.carryBase50Minutes) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(summary!.carryBase50Minutes) }} <span class="text-neutral-400">/ {{ formatCentiemes(summary!.carryBase50Minutes) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(summary!.carryBonus50Minutes) }} <span class="text-neutral-400">/ {{ formatCentiemes(summary!.carryBonus50Minutes) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(summary!.carryBonus50Minutes) }} <span class="text-neutral-400">/ {{ formatCentiemes(summary!.carryBonus50Minutes) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(summary!.carryBase50Minutes + summary!.carryBonus50Minutes) }} <span class="text-neutral-400">/ {{ formatCentiemes(summary!.carryBase50Minutes + summary!.carryBonus50Minutes) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(summary!.carryBase50Minutes + summary!.carryBonus50Minutes) }} <span class="text-neutral-400">/ {{ formatCentiemes(summary!.carryBase50Minutes + summary!.carryBonus50Minutes) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(summary!.carryFromPreviousYearMinutes) }} <span class="text-neutral-400">/ {{ formatCentiemes(summary!.carryFromPreviousYearMinutes) }}</span></td>
|
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(summary!.carryFromPreviousYearMinutes) }} <span class="text-neutral-400">/ {{ formatCentiemes(summary!.carryFromPreviousYearMinutes) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(summary!.carryFromPreviousYearMinutes) }} <span class="text-neutral-400">/ {{ formatCentiemes(summary!.carryFromPreviousYearMinutes) }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -89,7 +86,6 @@
|
|||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.base50) }} <span class="text-neutral-400">/ {{ formatCentiemes(monthReport.base50) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.base50) }} <span class="text-neutral-400">/ {{ formatCentiemes(monthReport.base50) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.bonus50) }} <span class="text-neutral-400">/ {{ formatCentiemes(monthReport.bonus50) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.bonus50) }} <span class="text-neutral-400">/ {{ formatCentiemes(monthReport.bonus50) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(monthReport.total50) }} <span class="text-neutral-400">/ {{ formatCentiemes(monthReport.total50) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(monthReport.total50) }} <span class="text-neutral-400">/ {{ formatCentiemes(monthReport.total50) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(monthReport.total) }} <span class="text-neutral-400">/ {{ formatCentiemes(monthReport.total) }}</span></td>
|
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.total) }} <span class="text-neutral-400">/ {{ formatCentiemes(monthReport.total) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(monthReport.total) }} <span class="text-neutral-400">/ {{ formatCentiemes(monthReport.total) }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -130,14 +126,10 @@
|
|||||||
<span v-if="week">{{ formatMinutes(week.base50Minutes + week.bonus50Minutes) }}</span>
|
<span v-if="week">{{ formatMinutes(week.base50Minutes + week.bonus50Minutes) }}</span>
|
||||||
<span v-else>0 h</span>
|
<span v-else>0 h</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">
|
||||||
<span v-if="week">{{ formatMinutes(week.totalMinutes) }}</span>
|
<span v-if="week">{{ formatMinutes(week.totalMinutes) }}</span>
|
||||||
<span v-else>0 h</span>
|
<span v-else>0 h</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">
|
|
||||||
<span v-if="week">{{ formatMinutes(week.cumulativeBalanceMinutes) }} <span class="text-neutral-400">/ {{ formatCentiemes(week.cumulativeBalanceMinutes) }}</span></span>
|
|
||||||
<span v-else> </span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Total row -->
|
<!-- Total row -->
|
||||||
@@ -150,8 +142,7 @@
|
|||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-t-2">{{ formatMinutes(totals.base50) }}</td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-t-2">{{ formatMinutes(totals.base50) }}</td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-t-2">{{ formatMinutes(totals.bonus50) }}</td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-t-2">{{ formatMinutes(totals.bonus50) }}</td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2 border-t-2">{{ formatMinutes(totals.total50) }}</td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2 border-t-2">{{ formatMinutes(totals.total50) }}</td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2 border-t-2">{{ formatMinutes(totals.total) }}</td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-t-2">{{ formatMinutes(totals.total) }}</td>
|
||||||
<td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500 border-t-2">-</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Payé row -->
|
<!-- Payé row -->
|
||||||
@@ -164,8 +155,7 @@
|
|||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBase50Minutes) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -currentPayment.paidBase50Minutes : 0) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBase50Minutes) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -currentPayment.paidBase50Minutes : 0) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBonus50Minutes) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -currentPayment.paidBonus50Minutes : 0) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBonus50Minutes) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -currentPayment.paidBonus50Minutes : 0) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ currentPayment ? formatMinutes(-(currentPayment.paidBase50Minutes + currentPayment.paidBonus50Minutes)) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -(currentPayment.paidBase50Minutes + currentPayment.paidBonus50Minutes) : 0) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ currentPayment ? formatMinutes(-(currentPayment.paidBase50Minutes + currentPayment.paidBonus50Minutes)) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -(currentPayment.paidBase50Minutes + currentPayment.paidBonus50Minutes) : 0) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(paidTotal) }} <span class="text-neutral-400">/ {{ formatCentiemes(paidTotal) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(paidTotal) }} <span class="text-neutral-400">/ {{ formatCentiemes(paidTotal) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500">-</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Reste row -->
|
<!-- Reste row -->
|
||||||
@@ -178,8 +168,7 @@
|
|||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(reste.base50) }} <span class="text-neutral-400">/ {{ formatCentiemes(reste.base50) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(reste.base50) }} <span class="text-neutral-400">/ {{ formatCentiemes(reste.base50) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(reste.bonus50) }} <span class="text-neutral-400">/ {{ formatCentiemes(reste.bonus50) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(reste.bonus50) }} <span class="text-neutral-400">/ {{ formatCentiemes(reste.bonus50) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(reste.total50) }} <span class="text-neutral-400">/ {{ formatCentiemes(reste.total50) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(reste.total50) }} <span class="text-neutral-400">/ {{ formatCentiemes(reste.total50) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ formatMinutes(reste.total) }} <span class="text-neutral-400">/ {{ formatCentiemes(reste.total) }}</span></td>
|
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(reste.total) }} <span class="text-neutral-400">/ {{ formatCentiemes(reste.total) }}</span></td>
|
||||||
<td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500">-</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
<MalioSelectCheckbox
|
<MalioSelectCheckbox
|
||||||
v-model="selectedSiteIds"
|
v-model="selectedSiteIds"
|
||||||
:options="siteOptions"
|
:options="siteOptions"
|
||||||
groupClass="w-80"
|
|
||||||
label="Sites"
|
label="Sites"
|
||||||
display-select-all
|
display-select-all
|
||||||
/>
|
/>
|
||||||
@@ -47,7 +46,6 @@
|
|||||||
<MalioSelectCheckbox
|
<MalioSelectCheckbox
|
||||||
v-model="selectedSiteIds"
|
v-model="selectedSiteIds"
|
||||||
:options="siteOptions"
|
:options="siteOptions"
|
||||||
groupClass="w-80"
|
|
||||||
label="Sites"
|
label="Sites"
|
||||||
display-select-all
|
display-select-all
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
class="flex items-center justify-between rounded-md px-2 py-1 text-xs"
|
class="flex items-center justify-between rounded-md px-2 py-1 text-xs"
|
||||||
:class="daily.hasAbsence ? 'text-white' : 'text-primary-500'"
|
:class="daily.hasAbsence ? 'text-white' : 'text-primary-500'"
|
||||||
:style="getDailyCellStyle(daily)"
|
:style="getDailyCellStyle(daily)"
|
||||||
:title="cellTitle(daily)"
|
|
||||||
>
|
>
|
||||||
<span class="font-semibold">{{ weekDayHeaders[i]?.label ?? '' }}</span>
|
<span class="font-semibold">{{ weekDayHeaders[i]?.label ?? '' }}</span>
|
||||||
<span v-if="row.trackingMode === 'PRESENCE'">{{ daily.present ?? 0 }}</span>
|
<span v-if="row.trackingMode === 'PRESENCE'">{{ daily.present ?? 0 }}</span>
|
||||||
@@ -94,11 +93,8 @@
|
|||||||
{{ row.firstName }} {{ row.lastName }}
|
{{ row.firstName }} {{ row.lastName }}
|
||||||
<span class="font-normal text-neutral-600">({{ row.contractName ?? '-' }})</span>
|
<span class="font-normal text-neutral-600">({{ row.contractName ?? '-' }})</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-[11px] text-neutral-500 truncate inline-flex items-center gap-2">
|
<p class="text-[11px] text-neutral-500 truncate">
|
||||||
<span>{{ row.siteName ?? 'Sans site' }}<span v-if="row.contractNature"> — {{ contractNatureLabel(row.contractNature) }}</span></span>
|
{{ row.siteName ?? 'Sans site' }}<span v-if="row.contractNature"> — {{ contractNatureLabel(row.contractNature) }}</span>
|
||||||
<button v-if="isAdmin" type="button" class="flex items-center text-white p-1" :class="row.comment ? 'bg-red-500 hover:bg-red-600' : 'bg-primary-500 hover:bg-secondary-500'" :title="row.comment ?? 'Ajouter un commentaire'" @click="$emit('open-comment', row)">
|
|
||||||
<Icon name="mdi:comment-text-outline" size="12"/>
|
|
||||||
</button>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -108,7 +104,7 @@
|
|||||||
class="text-left leading-4 rounded-md px-2 py-1"
|
class="text-left leading-4 rounded-md px-2 py-1"
|
||||||
:class="daily.hasAbsence ? 'text-white' : ''"
|
:class="daily.hasAbsence ? 'text-white' : ''"
|
||||||
:style="getDailyCellStyle(daily)"
|
:style="getDailyCellStyle(daily)"
|
||||||
:title="cellTitle(daily)"
|
:title="daily.absenceLabel ?? ''"
|
||||||
>
|
>
|
||||||
<template v-if="row.trackingMode === 'PRESENCE'">{{ daily.present ?? 0 }}</template>
|
<template v-if="row.trackingMode === 'PRESENCE'">{{ daily.present ?? 0 }}</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -157,37 +153,19 @@ const isInterimContract = (contractType?: ContractType | null) => {
|
|||||||
return contractType === CONTRACT_TYPES.INTERIM
|
return contractType === CONTRACT_TYPES.INTERIM
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOLIDAY_BG_COLOR = '#b3e5fc'
|
|
||||||
|
|
||||||
const getDailyCellStyle = (daily: {
|
const getDailyCellStyle = (daily: {
|
||||||
hasAbsence?: boolean
|
hasAbsence?: boolean
|
||||||
absenceColor?: string | null
|
absenceColor?: string | null
|
||||||
holidayLabel?: string | null
|
|
||||||
}) => {
|
}) => {
|
||||||
if (daily.hasAbsence) return { backgroundColor: daily.absenceColor || '#dc2626' }
|
if (!daily.hasAbsence) return undefined
|
||||||
if (daily.holidayLabel) return { backgroundColor: HOLIDAY_BG_COLOR }
|
return { backgroundColor: daily.absenceColor || '#dc2626' }
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellTitle = (daily: {
|
|
||||||
hasAbsence?: boolean
|
|
||||||
absenceLabel?: string | null
|
|
||||||
holidayLabel?: string | null
|
|
||||||
}) => {
|
|
||||||
const parts: string[] = []
|
|
||||||
if (daily.absenceLabel) parts.push(daily.absenceLabel)
|
|
||||||
if (daily.holidayLabel) parts.push(`Férié : ${daily.holidayLabel}`)
|
|
||||||
return parts.join(' — ')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
isWeekLoading: boolean
|
isWeekLoading: boolean
|
||||||
isAdmin: boolean
|
|
||||||
weekGridCols: string
|
weekGridCols: string
|
||||||
weeklySummary: WeeklyWorkHourSummary | null
|
weeklySummary: WeeklyWorkHourSummary | null
|
||||||
weekDayHeaders: Array<{ date: string; label: string }>
|
weekDayHeaders: Array<{ date: string; label: string }>
|
||||||
formatMinutes: (minutes: number) => string
|
formatMinutes: (minutes: number) => string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{ (e: 'open-comment', row: WeeklyWorkHourSummary['rows'][number]): void }>()
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
<template>
|
|
||||||
<MalioDrawer v-model="drawerOpen" title="Commentaire">
|
|
||||||
<form class="space-y-4" @submit.prevent="onSave">
|
|
||||||
<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>
|
|
||||||
<MalioInputTextArea
|
|
||||||
v-model="content"
|
|
||||||
label="Commentaire"
|
|
||||||
:size="8"
|
|
||||||
:max-length="5000"
|
|
||||||
:show-counter="true"
|
|
||||||
resize="vertical"
|
|
||||||
/>
|
|
||||||
<div class="sticky bottom-0 -mx-[20px] bg-white px-[20px] py-3 flex justify-between gap-3">
|
|
||||||
<MalioButton
|
|
||||||
v-if="commentId"
|
|
||||||
label="Supprimer"
|
|
||||||
variant="danger"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
@click="onDelete"
|
|
||||||
/>
|
|
||||||
<MalioButton
|
|
||||||
label="Enregistrer"
|
|
||||||
button-class="ml-auto"
|
|
||||||
:disabled="isSubmitting || !canSubmit"
|
|
||||||
@click="onSave"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</MalioDrawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { createWeekComment, deleteWeekComment, updateWeekComment } from '~/services/employee-week-comments'
|
|
||||||
import { getIsoWeekNumber, parseYmd } from '~/utils/date'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: boolean
|
|
||||||
employeeId: number | null
|
|
||||||
weekStart: string
|
|
||||||
weekEnd: string
|
|
||||||
initialContent: string
|
|
||||||
commentId: number | null
|
|
||||||
employeeLabel?: string
|
|
||||||
}>()
|
|
||||||
const emit = defineEmits<{ (e: 'update:modelValue', v: boolean): void; (e: 'saved'): void }>()
|
|
||||||
|
|
||||||
const drawerOpen = computed({ get: () => props.modelValue, set: (v) => emit('update:modelValue', v) })
|
|
||||||
const content = ref('')
|
|
||||||
const isSubmitting = ref(false)
|
|
||||||
|
|
||||||
watch(() => [props.modelValue, props.initialContent] as const, ([open, init]) => { if (open) content.value = init ?? '' }, { immediate: true })
|
|
||||||
|
|
||||||
const formatWeekRange = computed(() => {
|
|
||||||
const fmt = (ymd: string) => { const p = ymd.split('-'); return p.length === 3 ? `${p[2]}/${p[1]}/${p[0]}` : ymd }
|
|
||||||
const start = parseYmd(props.weekStart)
|
|
||||||
const weekLabel = start ? `S${getIsoWeekNumber(start)}` : ''
|
|
||||||
return weekLabel ? `${weekLabel} du ${fmt(props.weekStart)} au ${fmt(props.weekEnd)}` : `${fmt(props.weekStart)} → ${fmt(props.weekEnd)}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const canSubmit = computed(() => content.value.trim().length > 0 || props.commentId !== null)
|
|
||||||
|
|
||||||
const onSave = async () => {
|
|
||||||
if (!props.employeeId || isSubmitting.value) return
|
|
||||||
const trimmed = content.value.trim()
|
|
||||||
isSubmitting.value = true
|
|
||||||
try {
|
|
||||||
if (trimmed === '' && props.commentId) await deleteWeekComment(props.commentId)
|
|
||||||
else if (trimmed !== '' && props.commentId) await updateWeekComment(props.commentId, trimmed)
|
|
||||||
else if (trimmed !== '') await createWeekComment({ employeeId: props.employeeId, weekStartDate: props.weekStart, content: trimmed })
|
|
||||||
emit('saved'); drawerOpen.value = false
|
|
||||||
} finally { isSubmitting.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDelete = async () => {
|
|
||||||
if (!props.commentId || isSubmitting.value) return
|
|
||||||
isSubmitting.value = true
|
|
||||||
try { await deleteWeekComment(props.commentId); emit('saved'); drawerOpen.value = false } finally { isSubmitting.value = false }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -926,15 +926,6 @@ export const useDriverHoursPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWeekCommentDrawerOpen = ref(false)
|
|
||||||
const weekCommentContext = ref<{ employeeId: number; employeeLabel: string; weekStart: string; weekEnd: string; content: string; commentId: number | null } | null>(null)
|
|
||||||
const openWeekCommentDrawer = (row: { employeeId: number; firstName: string; lastName: string; comment?: string | null; commentId?: number | null }) => {
|
|
||||||
if (!weeklySummary.value) return
|
|
||||||
weekCommentContext.value = { employeeId: row.employeeId, employeeLabel: `${row.firstName} ${row.lastName}`.trim(), weekStart: weeklySummary.value.weekStart, weekEnd: weeklySummary.value.weekEnd, content: row.comment ?? '', commentId: row.commentId ?? null }
|
|
||||||
isWeekCommentDrawerOpen.value = true
|
|
||||||
}
|
|
||||||
const reloadWeeklySummary = async () => { await loadWeeklySummary() }
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isSelfUser,
|
isSelfUser,
|
||||||
@@ -1002,10 +993,6 @@ export const useDriverHoursPage = () => {
|
|||||||
deleteAbsenceFromDrawer,
|
deleteAbsenceFromDrawer,
|
||||||
closeAbsenceDrawer,
|
closeAbsenceDrawer,
|
||||||
formatMinutes,
|
formatMinutes,
|
||||||
handleSave,
|
handleSave
|
||||||
isWeekCommentDrawerOpen,
|
|
||||||
weekCommentContext,
|
|
||||||
openWeekCommentDrawer,
|
|
||||||
reloadWeeklySummary
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ export const useEmployeeDetailPage = () => {
|
|||||||
|
|
||||||
const showLeaveTab = computed(() => employee.value?.currentContractNature !== 'INTERIM')
|
const showLeaveTab = computed(() => employee.value?.currentContractNature !== 'INTERIM')
|
||||||
const showRttTab = computed(() => employee.value?.contract?.type !== CONTRACT_TYPES.FORFAIT)
|
const showRttTab = computed(() => employee.value?.contract?.type !== CONTRACT_TYPES.FORFAIT)
|
||||||
const isForfait = computed(() => employee.value?.contract?.type === CONTRACT_TYPES.FORFAIT)
|
|
||||||
const employeeContractWorkLabel = computed(() => {
|
const employeeContractWorkLabel = computed(() => {
|
||||||
const contract = employee.value?.contract
|
const contract = employee.value?.contract
|
||||||
if (!contract) return '-'
|
if (!contract) return '-'
|
||||||
if (contract.type === CONTRACT_TYPES.FORFAIT) return 'Forfait - 218 jours'
|
if (contract.type === CONTRACT_TYPES.FORFAIT) return 'Forfait'
|
||||||
if (contract.weeklyHours !== null && contract.weeklyHours !== undefined) return `${contract.weeklyHours} heures`
|
if (contract.weeklyHours !== null && contract.weeklyHours !== undefined) return `${contract.weeklyHours} heures`
|
||||||
return contract.name || '-'
|
return contract.name || '-'
|
||||||
})
|
})
|
||||||
@@ -56,9 +55,6 @@ export const useEmployeeDetailPage = () => {
|
|||||||
await bonus.loadBonusData()
|
await bonus.loadBonusData()
|
||||||
} else if (activeTab.value === 'observation') {
|
} else if (activeTab.value === 'observation') {
|
||||||
await observation.loadObservationData()
|
await observation.loadObservationData()
|
||||||
} else if (isForfait.value && showLeaveTab.value) {
|
|
||||||
// Eager load: needed for the "X jours restants" header label on forfait employees.
|
|
||||||
await leave.loadLeaveData()
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
@@ -67,13 +63,6 @@ export const useEmployeeDetailPage = () => {
|
|||||||
|
|
||||||
const contract = useEmployeeContract(employee, loadEmployee)
|
const contract = useEmployeeContract(employee, loadEmployee)
|
||||||
const leave = useEmployeeLeave(employee, loadEmployee)
|
const leave = useEmployeeLeave(employee, loadEmployee)
|
||||||
const forfaitRemainingDaysLabel = computed(() => {
|
|
||||||
if (!isForfait.value) return ''
|
|
||||||
const presence = leave.leaveSummary.value?.presenceDaysToToday
|
|
||||||
if (presence === undefined || presence === null) return ''
|
|
||||||
const remaining = 218 - presence
|
|
||||||
return ` (${remaining} restants)`
|
|
||||||
})
|
|
||||||
const rtt = useEmployeeRtt(employee, loadEmployee)
|
const rtt = useEmployeeRtt(employee, loadEmployee)
|
||||||
const mileage = useEmployeeMileage(employee, loadEmployee)
|
const mileage = useEmployeeMileage(employee, loadEmployee)
|
||||||
const formation = useEmployeeFormation(employee, loadEmployee)
|
const formation = useEmployeeFormation(employee, loadEmployee)
|
||||||
@@ -108,7 +97,6 @@ export const useEmployeeDetailPage = () => {
|
|||||||
showLeaveTab,
|
showLeaveTab,
|
||||||
showRttTab,
|
showRttTab,
|
||||||
employeeContractWorkLabel,
|
employeeContractWorkLabel,
|
||||||
forfaitRemainingDaysLabel,
|
|
||||||
...contract,
|
...contract,
|
||||||
...leave,
|
...leave,
|
||||||
...rtt,
|
...rtt,
|
||||||
|
|||||||
@@ -1112,15 +1112,6 @@ export const useHoursPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWeekCommentDrawerOpen = ref(false)
|
|
||||||
const weekCommentContext = ref<{ employeeId: number; employeeLabel: string; weekStart: string; weekEnd: string; content: string; commentId: number | null } | null>(null)
|
|
||||||
const openWeekCommentDrawer = (row: { employeeId: number; firstName: string; lastName: string; comment?: string | null; commentId?: number | null }) => {
|
|
||||||
if (!weeklySummary.value) return
|
|
||||||
weekCommentContext.value = { employeeId: row.employeeId, employeeLabel: `${row.firstName} ${row.lastName}`.trim(), weekStart: weeklySummary.value.weekStart, weekEnd: weeklySummary.value.weekEnd, content: row.comment ?? '', commentId: row.commentId ?? null }
|
|
||||||
isWeekCommentDrawerOpen.value = true
|
|
||||||
}
|
|
||||||
const reloadWeeklySummary = async () => { await loadWeeklySummary() }
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isSelfUser,
|
isSelfUser,
|
||||||
@@ -1195,10 +1186,6 @@ export const useHoursPage = () => {
|
|||||||
deleteAbsenceFromDrawer,
|
deleteAbsenceFromDrawer,
|
||||||
closeAbsenceDrawer,
|
closeAbsenceDrawer,
|
||||||
formatMinutes,
|
formatMinutes,
|
||||||
handleSave,
|
handleSave
|
||||||
isWeekCommentDrawerOpen,
|
|
||||||
weekCommentContext,
|
|
||||||
openWeekCommentDrawer,
|
|
||||||
reloadWeeklySummary
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,16 +80,6 @@ export const documentationSections: DocSection[] = [
|
|||||||
{ type: 'list', content: 'Jour : total des heures dans la plage 06:00–21:00\nNuit : total des heures dans les plages 00:00–06:00 et 21:00–24:00\nTotal : somme des heures de jour et de nuit' },
|
{ type: 'list', content: 'Jour : total des heures dans la plage 06:00–21:00\nNuit : total des heures dans les plages 00:00–06:00 et 21:00–24:00\nTotal : somme des heures de jour et de nuit' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'commentaire-semaine',
|
|
||||||
title: 'Commentaires de semaine (admin)',
|
|
||||||
requiredLevel: 'admin',
|
|
||||||
blocks: [
|
|
||||||
{ type: 'paragraph', content: 'Sur la vue semaine, un picto bulle permet d\'attacher un commentaire libre sur la semaine d\'un employé.' },
|
|
||||||
{ type: 'list', content: 'Bulle bleue : pas de commentaire\nBulle rouge : un commentaire existe\nClic : ouvre le drawer avec textarea' },
|
|
||||||
{ type: 'note', content: 'Les commentaires n\'affectent aucun calcul. Pour supprimer, videz la textarea puis Enregistrer, ou bouton Supprimer.' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -342,7 +332,7 @@ export const documentationSections: DocSection[] = [
|
|||||||
requiredLevel: 'admin',
|
requiredLevel: 'admin',
|
||||||
blocks: [
|
blocks: [
|
||||||
{ type: 'paragraph', content: 'La vue semaine est réservée aux administrateurs. Elle affiche une synthèse hebdomadaire par employé avec les heures supplémentaires calculées.' },
|
{ type: 'paragraph', content: 'La vue semaine est réservée aux administrateurs. Elle affiche une synthèse hebdomadaire par employé avec les heures supplémentaires calculées.' },
|
||||||
{ type: 'list', content: 'Filtrage par site et par employé\nDétail par jour avec totaux hebdomadaires\nColonnes de calcul : base, heures sup 25%, heures sup 50%, total récupération\nLes jours fériés sont signalés sur la cellule du jour : fond bleu clair quand pas d\'absence, nom du férié au survol' },
|
{ type: 'list', content: 'Filtrage par site et par employé\nDétail par jour avec totaux hebdomadaires\nColonnes de calcul : base, heures sup 25%, heures sup 50%, total récupération' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -492,7 +482,6 @@ export const documentationSections: DocSection[] = [
|
|||||||
blocks: [
|
blocks: [
|
||||||
{ type: 'list', content: 'Report N-1 : solde de l\'exercice précédent\nAcquis : cumul des heures supplémentaires de l\'exercice en cours\nDisponible : report + acquis − payé\nPayé : RTT convertis en salaire (soustraits du disponible)' },
|
{ type: 'list', content: 'Report N-1 : solde de l\'exercice précédent\nAcquis : cumul des heures supplémentaires de l\'exercice en cours\nDisponible : report + acquis − payé\nPayé : RTT convertis en salaire (soustraits du disponible)' },
|
||||||
{ type: 'note', content: 'Les contrats INTERIM et le mode PRESENCE n\'accumulent pas de RTT (affiché à 0).' },
|
{ type: 'note', content: 'Les contrats INTERIM et le mode PRESENCE n\'accumulent pas de RTT (affiché à 0).' },
|
||||||
{ type: 'paragraph', content: 'La colonne "Cumul" affiche le solde RTT à la fin de chaque semaine : Report N-1 + somme des heures hebdomadaires jusqu\'à la semaine concernée − paiements RTT des mois précédents. Un paiement enregistré sur le mois M n\'est déduit qu\'à partir des semaines du mois M+1. Permet la comparaison ligne à ligne avec un suivi RH externe (Excel).' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -581,7 +570,7 @@ export const documentationSections: DocSection[] = [
|
|||||||
requiredLevel: 'admin',
|
requiredLevel: 'admin',
|
||||||
blocks: [
|
blocks: [
|
||||||
{ type: 'paragraph', content: 'Génère un PDF A4 paysage avec le détail mensuel pour la paie.' },
|
{ type: 'paragraph', content: 'Génère un PDF A4 paysage avec le détail mensuel pour la paie.' },
|
||||||
{ type: 'list', content: 'Sélecteur de mois (défaut = mois courant)\nDonnées groupées par site\nColonnes : nom, base contrat, jours de présence cadre, heures de nuit, panier de nuit, heures RTT payées, congés (nombre + dates), maladie/AT (nombre + dates), primes conducteur (PDJ, repas, nuitée, samedi), observations\nColonne « Repas » chauffeur : somme déjeuner + dîner sur le mois (un jour avec les deux compte 2 repas)' },
|
{ type: 'list', content: 'Sélecteur de mois (défaut = mois courant)\nDonnées groupées par site\nColonnes : nom, base contrat, jours de présence cadre, heures de nuit, panier de nuit, heures RTT payées, congés (nombre + dates), maladie/AT (nombre + dates), primes conducteur (PDJ, repas, nuitée, samedi), observations' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -599,7 +588,7 @@ export const documentationSections: DocSection[] = [
|
|||||||
requiredLevel: 'admin',
|
requiredLevel: 'admin',
|
||||||
blocks: [
|
blocks: [
|
||||||
{ type: 'paragraph', content: 'Génère un PDF par employé avec le détail jour par jour de ses heures sur une année.' },
|
{ type: 'paragraph', content: 'Génère un PDF par employé avec le détail jour par jour de ses heures sur une année.' },
|
||||||
{ type: 'list', content: 'Accessible depuis la fiche employé (bouton imprimante)\nChoix de l\'année civile (janvier à décembre)\nColonnes adaptées au mode de suivi (TIME, PRESENCE, conducteur)\nSections séparées en cas de changement de contrat en cours d\'année\nLes jours fériés apparaissent toujours (ligne bleue) avec la mention « Férié : {nom} » dans la colonne Absence ; le total reprend les heures contractuelles créditées (hors Forfait)' },
|
{ type: 'list', content: 'Accessible depuis la fiche employé (bouton imprimante)\nChoix de l\'année civile (janvier à décembre)\nColonnes adaptées au mode de suivi (TIME, PRESENCE, conducteur)\nSections séparées en cas de changement de contrat en cours d\'année' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -59,10 +59,6 @@
|
|||||||
},
|
},
|
||||||
"leaveRecap": {
|
"leaveRecap": {
|
||||||
"load": "Impossible de charger le récap des congés."
|
"load": "Impossible de charger le récap des congés."
|
||||||
},
|
|
||||||
"weekComment": {
|
|
||||||
"save": "Impossible d'enregistrer le commentaire de semaine.",
|
|
||||||
"delete": "Impossible de supprimer le commentaire de semaine."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
@@ -114,10 +110,6 @@
|
|||||||
"create": "Observation créée.",
|
"create": "Observation créée.",
|
||||||
"update": "Observation mise à jour.",
|
"update": "Observation mise à jour.",
|
||||||
"delete": "Observation supprimée."
|
"delete": "Observation supprimée."
|
||||||
},
|
|
||||||
"weekComment": {
|
|
||||||
"save": "Commentaire enregistré.",
|
|
||||||
"delete": "Commentaire supprimé."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
frontend/package-lock.json
generated
8
frontend/package-lock.json
generated
@@ -7,7 +7,7 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@malio/layer-ui": "^1.4.6",
|
"@malio/layer-ui": "^1.4.3",
|
||||||
"@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",
|
||||||
@@ -2222,9 +2222,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@malio/layer-ui": {
|
"node_modules/@malio/layer-ui": {
|
||||||
"version": "1.4.6",
|
"version": "1.4.3",
|
||||||
"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.4.3/layer-ui-1.4.3.tgz",
|
||||||
"integrity": "sha512-stHqUAJ8E6a62Ka7QXlE177GhkIsjtmYNa/tNk1TVpbJ099okfLLivrlofEl7CCAqDeMaIepnW4q0vxJT+EFEA==",
|
"integrity": "sha512-XGR0VteuRGGizl8ZP2ZRlyWsdSTAwOYR7z5687Gx/SFr5eTg+poOV2NupqOuWCksxEcXA54vzzC0vMG8PbSvxg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/icon": "^2.2.1",
|
"@nuxt/icon": "^2.2.1",
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@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",
|
"@malio/layer-ui": "^1.4.3",
|
||||||
"@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",
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
<div class="h-full flex flex-col overflow-hidden">
|
<div class="h-full flex flex-col overflow-hidden">
|
||||||
<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
|
<button
|
||||||
label="Ajouter un type"
|
type="button"
|
||||||
icon-name="mdi:plus"
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
icon-position="left"
|
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
/>
|
>
|
||||||
|
+ Ajouter un type
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -55,40 +56,60 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MalioDrawer v-model="isDrawerOpen" :title="drawerTitle">
|
<AppDrawer v-model="isDrawerOpen" :title="drawerTitle">
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<MalioInputText
|
<div>
|
||||||
v-model="form.code"
|
<label class="text-md font-semibold text-neutral-700" for="code">
|
||||||
label="Code *"
|
Code <span class="text-red-600">*</span>
|
||||||
group-class="mt-2"
|
</label>
|
||||||
:max-length="10"
|
<input
|
||||||
:error="showCodeError ? 'Le code est obligatoire.' : ''"
|
id="code"
|
||||||
/>
|
v-model="form.code"
|
||||||
<MalioInputText
|
type="text"
|
||||||
v-model="form.label"
|
maxlength="10"
|
||||||
label="Libellé *"
|
:class="codeFieldClass"
|
||||||
group-class="mt-2"
|
/>
|
||||||
:error="showLabelError ? 'Le libellé est obligatoire.' : ''"
|
<p v-if="showCodeError" class="mt-1 text-sm text-red-600">
|
||||||
/>
|
Le code est obligatoire.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="label">
|
||||||
|
Libellé <span class="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="label"
|
||||||
|
v-model="form.label"
|
||||||
|
type="text"
|
||||||
|
:class="labelFieldClass"
|
||||||
|
/>
|
||||||
|
<p v-if="showLabelError" class="mt-1 text-sm text-red-600">
|
||||||
|
Le libellé est obligatoire.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700">
|
<label class="text-md font-semibold text-neutral-700">
|
||||||
Compté comme travaillé
|
Compté comme travaillé
|
||||||
</label>
|
</label>
|
||||||
<div class="mt-2 flex items-center gap-6">
|
<div class="mt-2 flex items-center gap-6">
|
||||||
<MalioRadioButton
|
<label class="inline-flex items-center gap-2 text-md text-neutral-800">
|
||||||
v-model="form.countAsWorkedHours"
|
<input
|
||||||
name="countAsWorkedHours"
|
v-model="form.countAsWorkedHours"
|
||||||
:value="true"
|
type="radio"
|
||||||
label="Oui"
|
class="h-4 w-4"
|
||||||
group-class="w-auto mt-0"
|
:value="true"
|
||||||
/>
|
/>
|
||||||
<MalioRadioButton
|
Oui
|
||||||
v-model="form.countAsWorkedHours"
|
</label>
|
||||||
name="countAsWorkedHours"
|
<label class="inline-flex items-center gap-2 text-md text-neutral-800">
|
||||||
:value="false"
|
<input
|
||||||
label="Non"
|
v-model="form.countAsWorkedHours"
|
||||||
group-class="w-auto mt-0"
|
type="radio"
|
||||||
/>
|
class="h-4 w-4"
|
||||||
|
:value="false"
|
||||||
|
/>
|
||||||
|
Non
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -109,29 +130,32 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editingType" class="grid grid-cols-2 gap-3 pt-2">
|
<div v-if="editingType" class="grid grid-cols-2 gap-3 pt-2">
|
||||||
<MalioButton
|
<button
|
||||||
label="Supprimer"
|
type="button"
|
||||||
variant="danger"
|
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"
|
||||||
button-class="w-full"
|
|
||||||
@click="confirmDelete(editingType)"
|
@click="confirmDelete(editingType)"
|
||||||
/>
|
>
|
||||||
<MalioButton
|
Supprimer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Modifier"
|
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"
|
||||||
button-class="w-full"
|
:class="submitButtonClass"
|
||||||
:disabled="isSubmitting || !isFormValid"
|
>
|
||||||
/>
|
Modifier
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex justify-center pt-2">
|
<div v-else class="flex justify-center pt-2">
|
||||||
<MalioButton
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Valider"
|
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"
|
||||||
:disabled="isSubmitting || !isFormValid"
|
>
|
||||||
/>
|
+ Ajouter
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</MalioDrawer>
|
</AppDrawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -178,6 +202,20 @@ const showCodeError = computed(() => validationTouched.code && !isCodeValid.valu
|
|||||||
const showLabelError = computed(() => validationTouched.label && !isLabelValid.value)
|
const showLabelError = computed(() => validationTouched.label && !isLabelValid.value)
|
||||||
const showColorError = computed(() => validationTouched.color && !isColorValid.value)
|
const showColorError = computed(() => validationTouched.color && !isColorValid.value)
|
||||||
|
|
||||||
|
const baseInputClass =
|
||||||
|
'mt-2 w-full rounded-md border px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20'
|
||||||
|
const codeFieldClass = computed(() => {
|
||||||
|
if (showCodeError.value) {
|
||||||
|
return `${baseInputClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseInputClass} border-neutral-300`
|
||||||
|
})
|
||||||
|
const labelFieldClass = computed(() => {
|
||||||
|
if (showLabelError.value) {
|
||||||
|
return `${baseInputClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseInputClass} border-neutral-300`
|
||||||
|
})
|
||||||
const colorFieldClass = computed(() => {
|
const colorFieldClass = computed(() => {
|
||||||
const baseColorClass = 'h-10 w-16 cursor-pointer rounded-md border bg-white p-1'
|
const baseColorClass = 'h-10 w-16 cursor-pointer rounded-md border bg-white p-1'
|
||||||
if (showColorError.value) {
|
if (showColorError.value) {
|
||||||
@@ -186,6 +224,13 @@ const colorFieldClass = computed(() => {
|
|||||||
return `${baseColorClass} border-neutral-300`
|
return `${baseColorClass} border-neutral-300`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const submitButtonClass = computed(() => {
|
||||||
|
if (isSubmitting.value || !isFormValid.value) {
|
||||||
|
return 'opacity-50 cursor-not-allowed'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
const loadAbsenceTypes = async () => {
|
const loadAbsenceTypes = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,13 +5,16 @@
|
|||||||
</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
|
<div class="flex items-center gap-4">
|
||||||
v-model="selectedSiteIds"
|
<div class="relative z-50 w-80">
|
||||||
:options="siteOptions"
|
<MalioSelectCheckbox
|
||||||
label="Sites"
|
v-model="selectedSiteIds"
|
||||||
groupClass="relative z-50 w-80 h-10"
|
:options="siteOptions"
|
||||||
display-select-all
|
label="Sites"
|
||||||
/>
|
display-select-all
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<MalioButton
|
<MalioButton
|
||||||
label="Ajouter une absence"
|
label="Ajouter une absence"
|
||||||
|
|||||||
@@ -74,13 +74,11 @@
|
|||||||
<DriverHoursWeekView
|
<DriverHoursWeekView
|
||||||
v-else-if="isAdmin && viewMode === 'week'"
|
v-else-if="isAdmin && viewMode === 'week'"
|
||||||
:is-week-loading="isWeekLoading"
|
:is-week-loading="isWeekLoading"
|
||||||
:is-admin="isAdmin"
|
|
||||||
:week-grid-cols="weekGridCols"
|
:week-grid-cols="weekGridCols"
|
||||||
:weekly-summary="filteredWeeklySummary"
|
:weekly-summary="filteredWeeklySummary"
|
||||||
:week-day-headers="weekDayHeaders"
|
:week-day-headers="weekDayHeaders"
|
||||||
:format-minutes="formatMinutes"
|
:format-minutes="formatMinutes"
|
||||||
class="max-h-[calc(100vh-300px)]"
|
class="max-h-[calc(100vh-300px)]"
|
||||||
@open-comment="openWeekCommentDrawer"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -112,17 +110,6 @@
|
|||||||
@cancel="closeAbsenceDrawer"
|
@cancel="closeAbsenceDrawer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HoursWeekCommentDrawer
|
|
||||||
v-if="weekCommentContext"
|
|
||||||
v-model="isWeekCommentDrawerOpen"
|
|
||||||
:employee-id="weekCommentContext.employeeId"
|
|
||||||
:employee-label="weekCommentContext.employeeLabel"
|
|
||||||
:week-start="weekCommentContext.weekStart"
|
|
||||||
:week-end="weekCommentContext.weekEnd"
|
|
||||||
:initial-content="weekCommentContext.content"
|
|
||||||
:comment-id="weekCommentContext.commentId"
|
|
||||||
@saved="reloadWeeklySummary"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -192,11 +179,7 @@ const {
|
|||||||
formatMinutes,
|
formatMinutes,
|
||||||
isSelectedDateHoliday,
|
isSelectedDateHoliday,
|
||||||
selectedHolidayLabel,
|
selectedHolidayLabel,
|
||||||
handleSave,
|
handleSave
|
||||||
isWeekCommentDrawerOpen,
|
|
||||||
weekCommentContext,
|
|
||||||
openWeekCommentDrawer,
|
|
||||||
reloadWeeklySummary
|
|
||||||
} = useDriverHoursPage()
|
} = useDriverHoursPage()
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<p>Date d'entrée : {{ employee.entryDate ? employee.entryDate.split('-').reverse().join('/') : '-' }}</p>
|
<p>Date d'entrée : {{ employee.entryDate ? employee.entryDate.split('-').reverse().join('/') : '-' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="font-bold text-[22px]">{{ contractNatureLabel(employee.currentContractNature) }} {{ employeeContractWorkLabel }}{{ forfaitRemainingDaysLabel }}</p>
|
<p class="font-bold text-[22px]">{{ contractNatureLabel(employee.currentContractNature) }} {{ employeeContractWorkLabel }}</p>
|
||||||
<p class="text-[18px]">{{ employee.site?.name ?? '-' }}</p>
|
<p class="text-[18px]">{{ employee.site?.name ?? '-' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -257,7 +257,6 @@ const {
|
|||||||
showRttTab,
|
showRttTab,
|
||||||
contractHistory,
|
contractHistory,
|
||||||
employeeContractWorkLabel,
|
employeeContractWorkLabel,
|
||||||
forfaitRemainingDaysLabel,
|
|
||||||
contractForm,
|
contractForm,
|
||||||
createContractForm,
|
createContractForm,
|
||||||
isContractDrawerOpen,
|
isContractDrawerOpen,
|
||||||
|
|||||||
@@ -4,19 +4,34 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="text-4xl font-bold text-primary-500">Employés</h1>
|
<h1 class="text-4xl font-bold text-primary-500">Employés</h1>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<MalioButton
|
<button
|
||||||
label="Export"
|
type="button"
|
||||||
variant="secondary"
|
class="flex items-center gap-2 rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
icon-name="mdi:download"
|
@click="handleLeaveRecapPrint"
|
||||||
icon-position="left"
|
>
|
||||||
@click="openExportDrawer"
|
Export récap. congés
|
||||||
/>
|
</button>
|
||||||
<MalioButton
|
<button
|
||||||
label="Ajouter un employé"
|
type="button"
|
||||||
icon-name="mdi:plus"
|
class="flex items-center gap-2 rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
icon-position="left"
|
@click="isSalaryRecapOpen = true"
|
||||||
|
>
|
||||||
|
Export récap. salaire
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center gap-2 rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
|
@click="isYearlyHoursBulkOpen = true"
|
||||||
|
>
|
||||||
|
Export heures
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center gap-2 rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
/>
|
>
|
||||||
|
+ Ajouter un employé
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 py-7">
|
<div class="flex items-center gap-3 py-7">
|
||||||
@@ -31,7 +46,6 @@
|
|||||||
<MalioSelectCheckbox
|
<MalioSelectCheckbox
|
||||||
v-model="selectedSiteIds"
|
v-model="selectedSiteIds"
|
||||||
:options="siteOptions"
|
:options="siteOptions"
|
||||||
groupClass="w-80"
|
|
||||||
label="Sites"
|
label="Sites"
|
||||||
display-select-all
|
display-select-all
|
||||||
/>
|
/>
|
||||||
@@ -84,53 +98,105 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MalioDrawer v-model="isDrawerOpen" :title="drawerTitle">
|
<AppDrawer v-model="isDrawerOpen" :title="drawerTitle">
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<MalioInputText
|
<div>
|
||||||
v-model="form.firstName"
|
<label class="text-md font-semibold text-neutral-700" for="first-name">
|
||||||
label="Prénom *"
|
Prénom <span class="text-red-600">*</span>
|
||||||
group-class="mt-2"
|
</label>
|
||||||
:error="showFirstNameError ? 'Le prénom est obligatoire.' : ''"
|
<input
|
||||||
/>
|
id="first-name"
|
||||||
<MalioInputText
|
v-model="form.firstName"
|
||||||
v-model="form.lastName"
|
type="text"
|
||||||
label="Nom *"
|
:class="firstNameFieldClass"
|
||||||
group-class="mt-2"
|
/>
|
||||||
:error="showLastNameError ? 'Le nom est obligatoire.' : ''"
|
<p v-if="showFirstNameError" class="mt-1 text-sm text-red-600">
|
||||||
/>
|
Le prénom est obligatoire.
|
||||||
<MalioSelect
|
</p>
|
||||||
:model-value="form.siteId === '' ? null : form.siteId"
|
</div>
|
||||||
:options="formSiteOptions"
|
<div>
|
||||||
label="Site *"
|
<label class="text-md font-semibold text-neutral-700" for="last-name">
|
||||||
min-width=""
|
Nom <span class="text-red-600">*</span>
|
||||||
:error="showSiteError ? 'Le site est obligatoire.' : ''"
|
</label>
|
||||||
@update:model-value="(v) => { form.siteId = v === null ? '' : Number(v) }"
|
<input
|
||||||
/>
|
id="last-name"
|
||||||
|
v-model="form.lastName"
|
||||||
|
type="text"
|
||||||
|
:class="lastNameFieldClass"
|
||||||
|
/>
|
||||||
|
<p v-if="showLastNameError" class="mt-1 text-sm text-red-600">
|
||||||
|
Le nom est obligatoire.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="site">
|
||||||
|
Site <span class="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="site"
|
||||||
|
v-model="form.siteId"
|
||||||
|
:class="siteFieldClass"
|
||||||
|
>
|
||||||
|
<option value="">Aucun site</option>
|
||||||
|
<option v-for="site in sites" :key="site.id" :value="site.id">
|
||||||
|
{{ site.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p v-if="showSiteError" class="mt-1 text-sm text-red-600">
|
||||||
|
Le site est obligatoire.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<template v-if="!editingEmployee">
|
<template v-if="!editingEmployee">
|
||||||
<MalioSelect
|
<div>
|
||||||
:model-value="form.contractNature"
|
<label class="text-md font-semibold text-neutral-700" for="contract-nature">
|
||||||
:options="contractNatureFormOptions"
|
Type de contrat <span class="text-red-600">*</span>
|
||||||
label="Type de contrat *"
|
</label>
|
||||||
min-width=""
|
<select
|
||||||
:error="showContractNatureError ? 'Le type de contrat est obligatoire.' : ''"
|
id="contract-nature"
|
||||||
@update:model-value="(v) => { if (v !== null) form.contractNature = v as 'CDI' | 'CDD' | 'INTERIM' }"
|
v-model="form.contractNature"
|
||||||
/>
|
:class="contractNatureFieldClass"
|
||||||
<MalioSelect
|
>
|
||||||
v-if="form.contractNature === 'INTERIM'"
|
<option value="CDI">CDI</option>
|
||||||
:model-value="form.interimAgencyId === '' ? null : form.interimAgencyId"
|
<option value="CDD">CDD</option>
|
||||||
:options="interimAgencyOptions"
|
<option value="INTERIM">Intérim</option>
|
||||||
label="Agence d'intérim"
|
</select>
|
||||||
min-width=""
|
<p v-if="showContractNatureError" class="mt-1 text-sm text-red-600">
|
||||||
@update:model-value="(v) => { form.interimAgencyId = v === null ? '' : Number(v) }"
|
Le type de contrat est obligatoire.
|
||||||
/>
|
</p>
|
||||||
<MalioSelect
|
</div>
|
||||||
:model-value="form.contractId === '' ? null : form.contractId"
|
<div v-if="form.contractNature === 'INTERIM'">
|
||||||
:options="contractFormOptions"
|
<label class="text-md font-semibold text-neutral-700" for="interim-agency">
|
||||||
label="Temps de travail *"
|
Agence d'intérim
|
||||||
min-width=""
|
</label>
|
||||||
:error="showContractError ? 'Le temps de travail est obligatoire.' : ''"
|
<select
|
||||||
@update:model-value="(v) => { form.contractId = v === null ? '' : Number(v) }"
|
id="interim-agency"
|
||||||
/>
|
v-model="form.interimAgencyId"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||||
|
>
|
||||||
|
<option value="">Aucune</option>
|
||||||
|
<option v-for="agency in interimAgencies" :key="agency.id" :value="agency.id">
|
||||||
|
{{ agency.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="contract">
|
||||||
|
Temps de travail <span class="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="contract"
|
||||||
|
v-model="form.contractId"
|
||||||
|
:class="contractFieldClass"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner un contrat</option>
|
||||||
|
<option v-for="contract in contracts" :key="contract.id" :value="contract.id">
|
||||||
|
{{ contract.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p v-if="showContractError" class="mt-1 text-sm text-red-600">
|
||||||
|
Le temps de travail est obligatoire.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="contract-start-date">
|
<label class="text-md font-semibold text-neutral-700" for="contract-start-date">
|
||||||
Début contrat <span class="text-red-600">*</span>
|
Début contrat <span class="text-red-600">*</span>
|
||||||
@@ -139,7 +205,7 @@
|
|||||||
id="contract-start-date"
|
id="contract-start-date"
|
||||||
v-model="form.contractStartDate"
|
v-model="form.contractStartDate"
|
||||||
type="date"
|
type="date"
|
||||||
:class="[dateInputBaseClass, form.contractStartDate ? 'border-black' : 'border-m-muted', showContractStartDateError ? '!border-m-danger' : '']"
|
:class="contractStartDateFieldClass"
|
||||||
/>
|
/>
|
||||||
<p v-if="showContractStartDateError" class="mt-1 text-sm text-red-600">
|
<p v-if="showContractStartDateError" class="mt-1 text-sm text-red-600">
|
||||||
La date de début est obligatoire.
|
La date de début est obligatoire.
|
||||||
@@ -154,18 +220,22 @@
|
|||||||
id="contract-end-date"
|
id="contract-end-date"
|
||||||
v-model="form.contractEndDate"
|
v-model="form.contractEndDate"
|
||||||
type="date"
|
type="date"
|
||||||
:class="[dateInputBaseClass, form.contractEndDate ? 'border-black' : 'border-m-muted', showContractEndDateError ? '!border-m-danger' : '']"
|
:class="contractEndDateFieldClass"
|
||||||
/>
|
/>
|
||||||
<p v-if="showContractEndDateError" class="mt-1 text-sm text-red-600">
|
<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.
|
La date de fin est obligatoire pour un CDD ou un Intérim.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-10 items-center rounded-md border border-neutral-200 bg-neutral-50 px-3">
|
<div class="rounded-md border border-neutral-200 bg-neutral-50 p-3">
|
||||||
<MalioCheckbox
|
<label class="inline-flex items-center gap-2 text-md font-semibold text-neutral-700" for="is-driver">
|
||||||
v-model="form.isDriver"
|
<input
|
||||||
label="Chauffeur"
|
id="is-driver"
|
||||||
group-class="flex items-center"
|
v-model="form.isDriver"
|
||||||
/>
|
type="checkbox"
|
||||||
|
class="h-4 w-4 rounded border-neutral-300 text-primary-500 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
Chauffeur
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<WorkDaysHoursInput
|
<WorkDaysHoursInput
|
||||||
v-if="requiresSchedule"
|
v-if="requiresSchedule"
|
||||||
@@ -174,72 +244,34 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex justify-end gap-3 pt-2">
|
<div class="flex justify-end gap-3 pt-2">
|
||||||
<MalioButton
|
<button
|
||||||
label="Annuler"
|
type="button"
|
||||||
variant="tertiary"
|
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||||
@click="isDrawerOpen = false"
|
@click="isDrawerOpen = false"
|
||||||
/>
|
>
|
||||||
<MalioButton
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Enregistrer"
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
:disabled="isSubmitting || !isFormValid"
|
:class="submitButtonClass"
|
||||||
/>
|
>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</MalioDrawer>
|
</AppDrawer>
|
||||||
|
|
||||||
<MalioDrawer v-model="isExportDrawerOpen" title="Export">
|
<SalaryRecapDrawer
|
||||||
<div class="space-y-4">
|
v-model="isSalaryRecapOpen"
|
||||||
<MalioSelect
|
@submit="handleSalaryRecapPrint"
|
||||||
:model-value="exportChoice === '' ? null : exportChoice"
|
/>
|
||||||
:options="exportTypeOptions"
|
|
||||||
label="Type d'export"
|
|
||||||
empty-option-label="Choisir un export"
|
|
||||||
group-class="mt-2"
|
|
||||||
min-width=""
|
|
||||||
@update:model-value="onExportChoiceChange"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-if="exportChoice === 'salary-recap'">
|
<BulkYearlyHoursDrawer
|
||||||
<label class="text-md font-semibold text-neutral-700" for="export-salary-month">
|
v-model="isYearlyHoursBulkOpen"
|
||||||
Mois <span class="text-red-600">*</span>
|
:is-loading="isYearlyHoursBulkLoading"
|
||||||
</label>
|
@submit="handleBulkYearlyHoursPrint"
|
||||||
<input
|
/>
|
||||||
id="export-salary-month"
|
|
||||||
v-model="exportSalaryMonth"
|
|
||||||
type="month"
|
|
||||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-else-if="exportChoice === 'yearly-hours'">
|
|
||||||
<MalioSelect
|
|
||||||
:model-value="exportYear"
|
|
||||||
:options="exportYearOptions"
|
|
||||||
label="Année *"
|
|
||||||
min-width=""
|
|
||||||
@update:model-value="(v) => { if (v !== null) exportYear = Number(v) }"
|
|
||||||
/>
|
|
||||||
<MalioSelect
|
|
||||||
:model-value="exportMonth === '' ? null : exportMonth"
|
|
||||||
:options="exportMonthOptions"
|
|
||||||
label="Mois *"
|
|
||||||
empty-option-label="Choisir un mois"
|
|
||||||
min-width=""
|
|
||||||
@update:model-value="(v) => { exportMonth = v === null ? '' : Number(v) }"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="flex justify-center pt-2">
|
|
||||||
<MalioButton
|
|
||||||
label="Valider"
|
|
||||||
button-class="w-[200px]"
|
|
||||||
:disabled="!isExportValid"
|
|
||||||
@click="handleExportValidate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MalioDrawer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -253,6 +285,8 @@ import {listContracts} from '~/services/contracts'
|
|||||||
import {createEmployee, deleteEmployee, listEmployees, updateEmployee} from '~/services/employees'
|
import {createEmployee, deleteEmployee, listEmployees, updateEmployee} from '~/services/employees'
|
||||||
import {listSites} from '~/services/sites'
|
import {listSites} from '~/services/sites'
|
||||||
import {listInterimAgencies, type InterimAgency} from '~/services/interim-agencies'
|
import {listInterimAgencies, type InterimAgency} from '~/services/interim-agencies'
|
||||||
|
import SalaryRecapDrawer from '~/components/SalaryRecapDrawer.vue'
|
||||||
|
import BulkYearlyHoursDrawer from '~/components/BulkYearlyHoursDrawer.vue'
|
||||||
import {contractNatureLabel, isContractNature, requiresContractEndDate, showsContractEndDate} from '~/utils/contract'
|
import {contractNatureLabel, isContractNature, requiresContractEndDate, showsContractEndDate} from '~/utils/contract'
|
||||||
import {usePdfPrinter} from '~/composables/usePdfPrinter'
|
import {usePdfPrinter} from '~/composables/usePdfPrinter'
|
||||||
|
|
||||||
@@ -263,50 +297,9 @@ useHead({
|
|||||||
const isDrawerOpen = ref(false)
|
const isDrawerOpen = ref(false)
|
||||||
const isSubmitting = ref(false)
|
const isSubmitting = ref(false)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const isExportDrawerOpen = ref(false)
|
const isSalaryRecapOpen = ref(false)
|
||||||
const exportChoice = ref<'leave-recap' | 'salary-recap' | 'yearly-hours' | ''>('')
|
const isYearlyHoursBulkOpen = ref(false)
|
||||||
const exportYear = ref<number>(new Date().getFullYear())
|
const isYearlyHoursBulkLoading = ref(false)
|
||||||
const exportMonth = ref<number | ''>(new Date().getMonth() + 1)
|
|
||||||
const exportSalaryMonth = ref<string>(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`)
|
|
||||||
|
|
||||||
const exportTypeOptions = [
|
|
||||||
{ label: 'Récap. congés', value: 'leave-recap' },
|
|
||||||
{ label: 'Récap. salaire', value: 'salary-recap' },
|
|
||||||
{ label: 'Heures annuelles', value: 'yearly-hours' }
|
|
||||||
]
|
|
||||||
const exportYearOptions = computed(() => {
|
|
||||||
const current = new Date().getFullYear()
|
|
||||||
return Array.from({ length: 6 }, (_, i) => ({ label: String(current - i), value: current - i }))
|
|
||||||
})
|
|
||||||
const exportMonthOptions = [
|
|
||||||
{ label: 'Janvier', value: 1 },
|
|
||||||
{ label: 'Février', value: 2 },
|
|
||||||
{ label: 'Mars', value: 3 },
|
|
||||||
{ label: 'Avril', value: 4 },
|
|
||||||
{ label: 'Mai', value: 5 },
|
|
||||||
{ label: 'Juin', value: 6 },
|
|
||||||
{ label: 'Juillet', value: 7 },
|
|
||||||
{ label: 'Août', value: 8 },
|
|
||||||
{ label: 'Septembre', value: 9 },
|
|
||||||
{ label: 'Octobre', value: 10 },
|
|
||||||
{ label: 'Novembre', value: 11 },
|
|
||||||
{ label: 'Décembre', value: 12 }
|
|
||||||
]
|
|
||||||
|
|
||||||
const isExportValid = computed(() => {
|
|
||||||
if (!exportChoice.value) return false
|
|
||||||
if (exportChoice.value === 'salary-recap') {
|
|
||||||
return exportSalaryMonth.value.trim() !== ''
|
|
||||||
}
|
|
||||||
if (exportChoice.value === 'yearly-hours') {
|
|
||||||
return exportYear.value > 0 && exportMonth.value !== ''
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
const onExportChoiceChange = (value: string | number | null) => {
|
|
||||||
exportChoice.value = (value === null ? '' : String(value)) as 'leave-recap' | 'salary-recap' | 'yearly-hours' | ''
|
|
||||||
}
|
|
||||||
const { printPdf } = usePdfPrinter()
|
const { printPdf } = usePdfPrinter()
|
||||||
const sitesInitialized = ref(false)
|
const sitesInitialized = ref(false)
|
||||||
const editingEmployee = ref<Employee | null>(null)
|
const editingEmployee = ref<Employee | null>(null)
|
||||||
@@ -432,23 +425,63 @@ const showContractEndDateError = computed(
|
|||||||
() => !editingEmployee.value && validationTouched.contractEndDate && !isContractEndDateValid.value
|
() => !editingEmployee.value && validationTouched.contractEndDate && !isContractEndDateValid.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const dateInputBaseClass =
|
const baseInputClass =
|
||||||
'mt-2 h-10 w-full rounded-md border px-3 text-md text-black outline-none focus:border-2 focus:border-m-primary'
|
'mt-2 w-full rounded-md border px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20'
|
||||||
|
const firstNameFieldClass = computed(() => {
|
||||||
|
if (showFirstNameError.value) {
|
||||||
|
return `${baseInputClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseInputClass} border-neutral-300`
|
||||||
|
})
|
||||||
|
const lastNameFieldClass = computed(() => {
|
||||||
|
if (showLastNameError.value) {
|
||||||
|
return `${baseInputClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseInputClass} border-neutral-300`
|
||||||
|
})
|
||||||
|
const siteFieldClass = computed(() => {
|
||||||
|
const baseSelectClass =
|
||||||
|
'mt-2 w-full rounded-md border bg-white px-3 py-2 text-md text-neutral-900'
|
||||||
|
if (showSiteError.value) {
|
||||||
|
return `${baseSelectClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseSelectClass} border-neutral-300`
|
||||||
|
})
|
||||||
|
const contractFieldClass = computed(() => {
|
||||||
|
const baseClass =
|
||||||
|
'mt-2 w-full rounded-md border bg-white px-3 py-2 text-md text-neutral-900'
|
||||||
|
if (showContractError.value) {
|
||||||
|
return `${baseClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseClass} border-neutral-300`
|
||||||
|
})
|
||||||
|
const contractNatureFieldClass = computed(() => {
|
||||||
|
const baseClass =
|
||||||
|
'mt-2 w-full rounded-md border bg-white px-3 py-2 text-md text-neutral-900'
|
||||||
|
if (showContractNatureError.value) {
|
||||||
|
return `${baseClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseClass} border-neutral-300`
|
||||||
|
})
|
||||||
|
const contractStartDateFieldClass = computed(() => {
|
||||||
|
if (showContractStartDateError.value) {
|
||||||
|
return `${baseInputClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseInputClass} border-neutral-300`
|
||||||
|
})
|
||||||
|
const contractEndDateFieldClass = computed(() => {
|
||||||
|
if (showContractEndDateError.value) {
|
||||||
|
return `${baseInputClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseInputClass} border-neutral-300`
|
||||||
|
})
|
||||||
|
|
||||||
const formSiteOptions = computed(() =>
|
const submitButtonClass = computed(() => {
|
||||||
sites.value.map((site) => ({ label: site.name, value: site.id }))
|
if (isSubmitting.value || !isFormValid.value) {
|
||||||
)
|
return 'opacity-50 cursor-not-allowed'
|
||||||
const interimAgencyOptions = computed(() =>
|
}
|
||||||
interimAgencies.value.map((agency) => ({ label: agency.name, value: agency.id }))
|
return ''
|
||||||
)
|
})
|
||||||
const contractFormOptions = computed(() =>
|
|
||||||
contracts.value.map((contract) => ({ label: contract.name, value: contract.id }))
|
|
||||||
)
|
|
||||||
const contractNatureFormOptions = [
|
|
||||||
{ label: 'CDI', value: 'CDI' },
|
|
||||||
{ label: 'CDD', value: 'CDD' },
|
|
||||||
{ label: 'Intérim', value: 'INTERIM' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const loadEmployees = async () => {
|
const loadEmployees = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
@@ -599,29 +632,26 @@ const openCreate = () => {
|
|||||||
isDrawerOpen.value = true
|
isDrawerOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const openExportDrawer = () => {
|
const handleLeaveRecapPrint = async () => {
|
||||||
exportChoice.value = ''
|
await printPdf('/leave-recap/print')
|
||||||
const now = new Date()
|
|
||||||
exportYear.value = now.getFullYear()
|
|
||||||
exportMonth.value = now.getMonth() + 1
|
|
||||||
exportSalaryMonth.value = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
|
||||||
isExportDrawerOpen.value = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExportValidate = async () => {
|
const handleSalaryRecapPrint = async (month: string) => {
|
||||||
if (!isExportValid.value) return
|
await printPdf(`/salary-recap/print?month=${month}`)
|
||||||
const choice = exportChoice.value
|
isSalaryRecapOpen.value = false
|
||||||
isExportDrawerOpen.value = false
|
}
|
||||||
if (choice === 'leave-recap') {
|
|
||||||
await printPdf('/leave-recap/print')
|
const handleBulkYearlyHoursPrint = async (payload: { year: number; month: number | null }) => {
|
||||||
} else if (choice === 'salary-recap') {
|
isYearlyHoursBulkLoading.value = true
|
||||||
await printPdf(`/salary-recap/print?month=${exportSalaryMonth.value}`)
|
try {
|
||||||
} else if (choice === 'yearly-hours') {
|
const monthParam = null !== payload.month ? `&month=${payload.month}` : ''
|
||||||
await printPdf(`/yearly-hours/print-all?year=${exportYear.value}&month=${exportMonth.value}`)
|
await printPdf(`/yearly-hours/print-all?year=${payload.year}${monthParam}`)
|
||||||
|
isYearlyHoursBulkOpen.value = false
|
||||||
|
} finally {
|
||||||
|
isYearlyHoursBulkLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const confirmDelete = async (employee: Employee) => {
|
const confirmDelete = async (employee: Employee) => {
|
||||||
const ok = window.confirm(`Supprimer ${employee.firstName} ${employee.lastName} ?`)
|
const ok = window.confirm(`Supprimer ${employee.firstName} ${employee.lastName} ?`)
|
||||||
if (!ok) return
|
if (!ok) return
|
||||||
|
|||||||
@@ -81,13 +81,11 @@
|
|||||||
<HoursWeekView
|
<HoursWeekView
|
||||||
v-else-if="isAdmin && viewMode === 'week'"
|
v-else-if="isAdmin && viewMode === 'week'"
|
||||||
:is-week-loading="isWeekLoading"
|
:is-week-loading="isWeekLoading"
|
||||||
:is-admin="isAdmin"
|
|
||||||
:week-grid-cols="weekGridCols"
|
:week-grid-cols="weekGridCols"
|
||||||
:weekly-summary="filteredWeeklySummary"
|
:weekly-summary="filteredWeeklySummary"
|
||||||
:week-day-headers="weekDayHeaders"
|
:week-day-headers="weekDayHeaders"
|
||||||
:format-minutes="formatMinutes"
|
:format-minutes="formatMinutes"
|
||||||
class="max-h-[calc(100vh-300px)]"
|
class="max-h-[calc(100vh-300px)]"
|
||||||
@open-comment="openWeekCommentDrawer"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -119,17 +117,6 @@
|
|||||||
@cancel="closeAbsenceDrawer"
|
@cancel="closeAbsenceDrawer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HoursWeekCommentDrawer
|
|
||||||
v-if="weekCommentContext"
|
|
||||||
v-model="isWeekCommentDrawerOpen"
|
|
||||||
:employee-id="weekCommentContext.employeeId"
|
|
||||||
:employee-label="weekCommentContext.employeeLabel"
|
|
||||||
:week-start="weekCommentContext.weekStart"
|
|
||||||
:week-end="weekCommentContext.weekEnd"
|
|
||||||
:initial-content="weekCommentContext.content"
|
|
||||||
:comment-id="weekCommentContext.commentId"
|
|
||||||
@saved="reloadWeeklySummary"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -206,11 +193,7 @@ const {
|
|||||||
deleteAbsenceFromDrawer,
|
deleteAbsenceFromDrawer,
|
||||||
closeAbsenceDrawer,
|
closeAbsenceDrawer,
|
||||||
formatMinutes,
|
formatMinutes,
|
||||||
handleSave,
|
handleSave
|
||||||
isWeekCommentDrawerOpen,
|
|
||||||
weekCommentContext,
|
|
||||||
openWeekCommentDrawer,
|
|
||||||
reloadWeeklySummary
|
|
||||||
} = useHoursPage()
|
} = useHoursPage()
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
|
|||||||
@@ -9,18 +9,31 @@
|
|||||||
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
|
<div>
|
||||||
v-model="username"
|
<label class="text-sm font-semibold text-neutral-700" for="username">
|
||||||
label="Nom d'utilisateur"
|
Nom d'utilisateur
|
||||||
autocomplete="username"
|
</label>
|
||||||
group-class="mt-2"
|
<input
|
||||||
/>
|
id="username"
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
autocomplete="username"
|
||||||
|
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>
|
||||||
|
|
||||||
<MalioInputPassword
|
<div>
|
||||||
v-model="password"
|
<label class="text-sm font-semibold text-neutral-700" for="password">
|
||||||
label="Mot de passe"
|
Mot de passe
|
||||||
autocomplete="current-password"
|
</label>
|
||||||
/>
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
<div class="h-full flex flex-col overflow-hidden">
|
<div class="h-full flex flex-col overflow-hidden">
|
||||||
<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
|
<button
|
||||||
label="Ajouter un site"
|
type="button"
|
||||||
icon-name="mdi:plus"
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||||
icon-position="left"
|
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
/>
|
>
|
||||||
|
+ Ajouter un site
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -51,14 +52,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MalioDrawer v-model="isDrawerOpen" :title="drawerTitle">
|
<AppDrawer v-model="isDrawerOpen" :title="drawerTitle">
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<MalioInputText
|
<div>
|
||||||
v-model="form.name"
|
<label class="text-md font-semibold text-neutral-700" for="name">
|
||||||
label="Nom *"
|
Nom <span class="text-red-600">*</span>
|
||||||
group-class="mt-2"
|
</label>
|
||||||
:error="showNameError ? 'Le nom du site est obligatoire.' : ''"
|
<input
|
||||||
/>
|
id="name"
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
:class="nameFieldClass"
|
||||||
|
/>
|
||||||
|
<p v-if="showNameError" class="mt-1 text-sm text-red-600">
|
||||||
|
Le nom du site est obligatoire.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-md font-semibold text-neutral-700" for="color">
|
<label class="text-md font-semibold text-neutral-700" for="color">
|
||||||
Couleur <span class="text-red-600">*</span>
|
Couleur <span class="text-red-600">*</span>
|
||||||
@@ -74,29 +83,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editingSite" class="grid grid-cols-2 gap-3 pt-2">
|
<div v-if="editingSite" class="grid grid-cols-2 gap-3 pt-2">
|
||||||
<MalioButton
|
<button
|
||||||
label="Supprimer"
|
type="button"
|
||||||
variant="danger"
|
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"
|
||||||
button-class="w-full"
|
|
||||||
@click="confirmDelete(editingSite)"
|
@click="confirmDelete(editingSite)"
|
||||||
/>
|
>
|
||||||
<MalioButton
|
Supprimer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Modifier"
|
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"
|
||||||
button-class="w-full"
|
:class="submitButtonClass"
|
||||||
:disabled="isSubmitting || !isFormValid"
|
>
|
||||||
/>
|
Modifier
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex justify-center pt-2">
|
<div v-else class="flex justify-center pt-2">
|
||||||
<MalioButton
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Valider"
|
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"
|
||||||
:disabled="isSubmitting || !isFormValid"
|
>
|
||||||
/>
|
+ Ajouter
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</MalioDrawer>
|
</AppDrawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -134,6 +146,22 @@ const isFormValid = computed(() => isNameValid.value)
|
|||||||
|
|
||||||
const showNameError = computed(() => validationTouched.name && !isNameValid.value)
|
const showNameError = computed(() => validationTouched.name && !isNameValid.value)
|
||||||
|
|
||||||
|
const baseInputClass =
|
||||||
|
'mt-2 w-full rounded-md border px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20'
|
||||||
|
const nameFieldClass = computed(() => {
|
||||||
|
if (showNameError.value) {
|
||||||
|
return `${baseInputClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseInputClass} border-neutral-300`
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitButtonClass = computed(() => {
|
||||||
|
if (isSubmitting.value || !isFormValid.value) {
|
||||||
|
return 'opacity-50 cursor-not-allowed'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
const loadSites = async () => {
|
const loadSites = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
<div class="h-full flex flex-col overflow-hidden">
|
<div class="h-full flex flex-col overflow-hidden">
|
||||||
<div class="flex items-center justify-between pb-6">
|
<div class="flex items-center justify-between pb-6">
|
||||||
<h1 class="text-2xl font-bold text-primary-500 lg:text-4xl">Utilisateurs</h1>
|
<h1 class="text-2xl font-bold text-primary-500 lg:text-4xl">Utilisateurs</h1>
|
||||||
<MalioButton
|
<button
|
||||||
label="Ajouter"
|
type="button"
|
||||||
icon-name="mdi:plus"
|
class="rounded-lg bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-secondary-500 lg:px-4 lg:text-md"
|
||||||
icon-position="left"
|
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
/>
|
>
|
||||||
|
+ Ajouter
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -92,25 +93,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MalioDrawer
|
<AppDrawer
|
||||||
v-model="isDrawerOpen"
|
v-model="isDrawerOpen"
|
||||||
:title="editingUser ? 'Modifier un utilisateur' : 'Ajouter un utilisateur'"
|
:title="editingUser ? 'Modifier un utilisateur' : 'Ajouter un utilisateur'"
|
||||||
>
|
>
|
||||||
<form class="space-y-4" @submit.prevent="handleSubmit">
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
||||||
<MalioInputText
|
<div>
|
||||||
v-model="form.username"
|
<label class="text-md font-semibold text-neutral-700" for="username">
|
||||||
:label="editingUser ? `Nom d'utilisateur` : `Nom d'utilisateur *`"
|
Nom d'utilisateur <span class="text-red-600">*</span>
|
||||||
group-class="mt-2"
|
</label>
|
||||||
:error="showUsernameError ? `Le nom d'utilisateur est obligatoire.` : ''"
|
<input
|
||||||
/>
|
id="username"
|
||||||
|
v-model="form.username"
|
||||||
|
type="text"
|
||||||
|
:class="usernameFieldClass"
|
||||||
|
/>
|
||||||
|
<p v-if="showUsernameError" class="mt-1 text-sm text-red-600">
|
||||||
|
Le nom d'utilisateur est obligatoire.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MalioInputPassword
|
<label class="text-md font-semibold text-neutral-700" for="password">
|
||||||
|
Mot de passe
|
||||||
|
<span v-if="!editingUser" class="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
v-model="form.password"
|
v-model="form.password"
|
||||||
:label="editingUser ? 'Mot de passe' : 'Mot de passe *'"
|
type="password"
|
||||||
:hint="editingUser ? 'Laisse vide pour ne pas changer le mot de passe.' : ''"
|
:class="passwordFieldClass"
|
||||||
:error="!editingUser && showPasswordError ? 'Le mot de passe est obligatoire.' : ''"
|
|
||||||
/>
|
/>
|
||||||
|
<p v-if="editingUser" class="mt-1 text-sm text-neutral-500">
|
||||||
|
Laisse vide pour ne pas changer le mot de passe.
|
||||||
|
</p>
|
||||||
|
<p v-else-if="showPasswordError" class="mt-1 text-sm text-red-600">
|
||||||
|
Le mot de passe est obligatoire.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -153,32 +172,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="form.accessMode === 'self'">
|
<div v-if="form.accessMode === 'self'">
|
||||||
<MalioSelect
|
<label class="text-md font-semibold text-neutral-700" for="employee">
|
||||||
:model-value="form.employeeId === '' ? null : form.employeeId"
|
Employé lié
|
||||||
:options="employeeOptions"
|
</label>
|
||||||
label="Employé lié"
|
<select
|
||||||
empty-option-label="Aucun"
|
id="employee"
|
||||||
min-width=""
|
v-model="form.employeeId"
|
||||||
:error="showSelfEmployeeError ? 'Sélectionne un employé.' : ''"
|
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||||
@update:model-value="onEmployeeChange"
|
>
|
||||||
/>
|
<option value="">Aucun</option>
|
||||||
|
<option v-for="employee in employees" :key="employee.id" :value="employee.id">
|
||||||
|
{{ employee.firstName }} {{ employee.lastName }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p v-if="showSelfEmployeeError" class="mt-1 text-sm text-red-600">
|
||||||
|
Sélectionne un employé.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="form.accessMode === 'sites'">
|
<div v-if="form.accessMode === 'sites'">
|
||||||
<p class="text-md font-semibold text-neutral-700">Sites autorisés</p>
|
<p class="text-md font-semibold text-neutral-700">Sites autorisés</p>
|
||||||
<div class="mt-2 grid gap-2 sm:grid-cols-2">
|
<div class="mt-2 grid gap-2 sm:grid-cols-2">
|
||||||
<div
|
<label
|
||||||
v-for="site in sites"
|
v-for="site in sites"
|
||||||
:key="site.id"
|
:key="site.id"
|
||||||
class="flex h-10 items-center rounded-md border border-neutral-200 px-3"
|
class="flex items-center gap-2 rounded-md border border-neutral-200 px-3 py-2 text-sm text-neutral-700 cursor-pointer"
|
||||||
>
|
>
|
||||||
<MalioCheckbox
|
<input
|
||||||
:model-value="form.siteIds.includes(site.id)"
|
type="checkbox"
|
||||||
:label="site.name"
|
class="cursor-pointer"
|
||||||
group-class="flex items-center"
|
:checked="form.siteIds.includes(site.id)"
|
||||||
@update:model-value="toggleSite(site.id)"
|
@change="toggleSite(site.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
<span>{{ site.name }}</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="showSitesError" class="mt-1 text-sm text-red-600">
|
<p v-if="showSitesError" class="mt-1 text-sm text-red-600">
|
||||||
Sélectionne au moins un site.
|
Sélectionne au moins un site.
|
||||||
@@ -186,31 +213,44 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MalioCheckbox
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
v-model="form.isLocked"
|
<input
|
||||||
label="Verrouiller le compte"
|
v-model="form.isLocked"
|
||||||
hint="Un compte verrouillé ne peut plus se connecter."
|
type="checkbox"
|
||||||
/>
|
class="cursor-pointer"
|
||||||
|
/>
|
||||||
|
<span class="text-md font-semibold text-neutral-700">Verrouiller le compte</span>
|
||||||
|
</label>
|
||||||
|
<p class="mt-1 text-sm text-neutral-500">
|
||||||
|
Un compte verrouillé ne peut plus se connecter.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MalioCheckbox
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
v-model="form.hasLeaveRecapAccess"
|
<input
|
||||||
label="Accès à l'écran Récap. congés"
|
v-model="form.hasLeaveRecapAccess"
|
||||||
hint="Affiche l'onglet dans la sidebar et donne accès au tableau récap."
|
type="checkbox"
|
||||||
/>
|
class="cursor-pointer"
|
||||||
|
/>
|
||||||
|
<span class="text-md font-semibold text-neutral-700">Accès à l'écran Récap. congés</span>
|
||||||
|
</label>
|
||||||
|
<p class="mt-1 text-sm text-neutral-500">
|
||||||
|
Affiche l'onglet dans la sidebar et donne accès au tableau récap.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center pt-2">
|
<div class="flex justify-center pt-2">
|
||||||
<MalioButton
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
:label="editingUser ? 'Modifier' : 'Valider'"
|
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"
|
||||||
:disabled="isSubmitting || !isFormValid"
|
>
|
||||||
/>
|
{{ editingUser ? 'Modifier' : '+ Ajouter' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</MalioDrawer>
|
</AppDrawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -308,13 +348,27 @@ const getSiteLabels = (user: User) => {
|
|||||||
return names.length > 0 ? names.join(', ') : 'Sites sélectionnés'
|
return names.length > 0 ? names.join(', ') : 'Sites sélectionnés'
|
||||||
}
|
}
|
||||||
|
|
||||||
const employeeOptions = computed(() =>
|
const baseInputClass =
|
||||||
employees.value.map((e) => ({ label: `${e.firstName} ${e.lastName}`, value: e.id }))
|
'mt-2 w-full rounded-md border px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20'
|
||||||
)
|
const usernameFieldClass = computed(() => {
|
||||||
|
if (showUsernameError.value) {
|
||||||
|
return `${baseInputClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseInputClass} border-neutral-300`
|
||||||
|
})
|
||||||
|
const passwordFieldClass = computed(() => {
|
||||||
|
if (showPasswordError.value) {
|
||||||
|
return `${baseInputClass} border-red-500`
|
||||||
|
}
|
||||||
|
return `${baseInputClass} border-neutral-300`
|
||||||
|
})
|
||||||
|
|
||||||
const onEmployeeChange = (value: string | number | null) => {
|
const submitButtonClass = computed(() => {
|
||||||
form.employeeId = value === null ? '' : Number(value)
|
if (isSubmitting.value || !isFormValid.value) {
|
||||||
}
|
return 'opacity-50 cursor-not-allowed'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|||||||
@@ -15,6 +15,5 @@ export type EmployeeLeaveSummary = {
|
|||||||
previousYearRemainingDays: number
|
previousYearRemainingDays: number
|
||||||
previousYearPaidDays: number
|
previousYearPaidDays: number
|
||||||
presenceDaysByMonth: Record<string, number>
|
presenceDaysByMonth: Record<string, number>
|
||||||
presenceDaysToToday: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export type EmployeeRttWeekSummary = {
|
|||||||
base50Minutes: number
|
base50Minutes: number
|
||||||
bonus50Minutes: number
|
bonus50Minutes: number
|
||||||
totalMinutes: number
|
totalMinutes: number
|
||||||
cumulativeBalanceMinutes: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RttMonthPayment = {
|
export type RttMonthPayment = {
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ export type WeeklyWorkHourDailySummary = {
|
|||||||
hasDinner?: boolean
|
hasDinner?: boolean
|
||||||
hasOvernight?: boolean
|
hasOvernight?: boolean
|
||||||
virtualHolidayMinutes?: number
|
virtualHolidayMinutes?: number
|
||||||
holidayLabel?: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WeeklyWorkHourRowSummary = {
|
export type WeeklyWorkHourRowSummary = {
|
||||||
@@ -89,8 +88,6 @@ export type WeeklyWorkHourRowSummary = {
|
|||||||
weeklyOvernightCount?: number
|
weeklyOvernightCount?: number
|
||||||
hasContractForWeek?: boolean
|
hasContractForWeek?: boolean
|
||||||
contractNature?: 'CDI' | 'CDD' | 'INTERIM' | null
|
contractNature?: 'CDI' | 'CDD' | 'INTERIM' | null
|
||||||
comment?: string | null
|
|
||||||
commentId?: number | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WeeklyWorkHourSummary = {
|
export type WeeklyWorkHourSummary = {
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
export type EmployeeWeekComment = {
|
|
||||||
id: number
|
|
||||||
weekStartDate: string
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createWeekComment = async (payload: { employeeId: number; weekStartDate: string; content: string }) => {
|
|
||||||
const api = useApi()
|
|
||||||
return api.post<EmployeeWeekComment>('/employee_week_comments', {
|
|
||||||
employee: `/api/employees/${payload.employeeId}`,
|
|
||||||
weekStartDate: payload.weekStartDate,
|
|
||||||
content: payload.content
|
|
||||||
}, { toastSuccessKey: 'success.weekComment.save', toastErrorKey: 'errors.weekComment.save' })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateWeekComment = async (id: number, content: string) => {
|
|
||||||
const api = useApi()
|
|
||||||
return api.patch<EmployeeWeekComment>(`/employee_week_comments/${id}`, { content }, { toastSuccessKey: 'success.weekComment.save', toastErrorKey: 'errors.weekComment.save' })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteWeekComment = async (id: number) => {
|
|
||||||
const api = useApi()
|
|
||||||
await api.delete(`/employee_week_comments/${id}`, {}, { toastSuccessKey: 'success.weekComment.delete', toastErrorKey: 'errors.weekComment.delete' })
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20260417100000 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Create employee_week_comments table for per-week admin annotations on the hours weekly view';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('CREATE TABLE employee_week_comments (id SERIAL NOT NULL, employee_id INT NOT NULL, week_start_date DATE NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
|
|
||||||
$this->addSql('CREATE UNIQUE INDEX uniq_employee_week_comment ON employee_week_comments (employee_id, week_start_date)');
|
|
||||||
$this->addSql('CREATE INDEX idx_ewc_week_start ON employee_week_comments (week_start_date)');
|
|
||||||
$this->addSql('ALTER TABLE employee_week_comments ADD CONSTRAINT fk_ewc_employee FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('DROP TABLE employee_week_comments');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,4 @@ final class EmployeeLeaveSummary
|
|||||||
|
|
||||||
/** @var array<string, float> YYYY-MM => count (0.5 for half-days) */
|
/** @var array<string, float> YYYY-MM => count (0.5 for half-days) */
|
||||||
public array $presenceDaysByMonth = [];
|
public array $presenceDaysByMonth = [];
|
||||||
|
|
||||||
/** Cumul des jours de présence depuis le début de l'année de congé jusqu'à aujourd'hui (forfait). */
|
|
||||||
public float $presenceDaysToToday = 0.0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,5 @@ final class EmployeeRttWeekSummary
|
|||||||
public int $base50Minutes = 0,
|
public int $base50Minutes = 0,
|
||||||
public int $bonus50Minutes = 0,
|
public int $bonus50Minutes = 0,
|
||||||
public int $totalMinutes = 0,
|
public int $totalMinutes = 0,
|
||||||
public int $cumulativeBalanceMinutes = 0,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,5 @@ final class WeeklyDaySummary
|
|||||||
public bool $hasDinner = false,
|
public bool $hasDinner = false,
|
||||||
public bool $hasOvernight = false,
|
public bool $hasOvernight = false,
|
||||||
public int $virtualHolidayMinutes = 0,
|
public int $virtualHolidayMinutes = 0,
|
||||||
public ?string $holidayLabel = null,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,5 @@ final class WeeklySummaryRow
|
|||||||
public int $weeklyOvernightCount = 0,
|
public int $weeklyOvernightCount = 0,
|
||||||
public bool $hasContractForWeek = true,
|
public bool $hasContractForWeek = true,
|
||||||
public ?string $contractNature = null,
|
public ?string $contractNature = null,
|
||||||
public ?string $comment = null,
|
|
||||||
public ?int $commentId = null,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Entity;
|
|
||||||
|
|
||||||
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
|
|
||||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
|
||||||
use ApiPlatform\Metadata\ApiFilter;
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
|
||||||
use ApiPlatform\Metadata\Delete;
|
|
||||||
use ApiPlatform\Metadata\Get;
|
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
|
||||||
use ApiPlatform\Metadata\Patch;
|
|
||||||
use ApiPlatform\Metadata\Post;
|
|
||||||
use App\Repository\EmployeeWeekCommentRepository;
|
|
||||||
use App\State\EmployeeWeekCommentWriteProcessor;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
|
||||||
use Symfony\Component\Serializer\Attribute\Groups;
|
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
|
||||||
|
|
||||||
#[ApiResource(
|
|
||||||
operations: [
|
|
||||||
new Get(security: "is_granted('ROLE_ADMIN')"),
|
|
||||||
new GetCollection(security: "is_granted('ROLE_ADMIN')"),
|
|
||||||
new Post(security: "is_granted('ROLE_ADMIN')", processor: EmployeeWeekCommentWriteProcessor::class),
|
|
||||||
new Patch(security: "is_granted('ROLE_ADMIN')", processor: EmployeeWeekCommentWriteProcessor::class),
|
|
||||||
new Delete(security: "is_granted('ROLE_ADMIN')", processor: EmployeeWeekCommentWriteProcessor::class),
|
|
||||||
],
|
|
||||||
normalizationContext: ['groups' => ['week_comment:read'], 'datetime_format' => 'Y-m-d'],
|
|
||||||
denormalizationContext: ['groups' => ['week_comment:write'], 'datetime_format' => 'Y-m-d'],
|
|
||||||
order: ['weekStartDate' => 'DESC'],
|
|
||||||
paginationEnabled: false,
|
|
||||||
)]
|
|
||||||
#[ApiFilter(DateFilter::class, properties: ['weekStartDate'])]
|
|
||||||
#[ApiFilter(SearchFilter::class, properties: ['employee' => 'exact'])]
|
|
||||||
#[ORM\Entity(repositoryClass: EmployeeWeekCommentRepository::class)]
|
|
||||||
#[ORM\Table(name: 'employee_week_comments')]
|
|
||||||
#[ORM\UniqueConstraint(name: 'uniq_employee_week_comment', columns: ['employee_id', 'week_start_date'])]
|
|
||||||
class EmployeeWeekComment
|
|
||||||
{
|
|
||||||
#[ORM\Id]
|
|
||||||
#[ORM\GeneratedValue]
|
|
||||||
#[ORM\Column(type: 'integer')]
|
|
||||||
#[Groups(['week_comment:read'])]
|
|
||||||
private ?int $id = null;
|
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Employee::class)]
|
|
||||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
|
||||||
#[Groups(['week_comment:read', 'week_comment:write'])]
|
|
||||||
#[Assert\NotNull]
|
|
||||||
private ?Employee $employee = null;
|
|
||||||
|
|
||||||
#[ORM\Column(type: 'date_immutable')]
|
|
||||||
#[Groups(['week_comment:read', 'week_comment:write'])]
|
|
||||||
#[Assert\NotNull]
|
|
||||||
private ?DateTimeImmutable $weekStartDate = null;
|
|
||||||
|
|
||||||
#[ORM\Column(type: 'text')]
|
|
||||||
#[Groups(['week_comment:read', 'week_comment:write'])]
|
|
||||||
#[Assert\NotBlank]
|
|
||||||
#[Assert\Length(max: 5000)]
|
|
||||||
private string $content = '';
|
|
||||||
|
|
||||||
#[ORM\Column(type: 'datetime_immutable')]
|
|
||||||
#[Groups(['week_comment:read'])]
|
|
||||||
private DateTimeImmutable $createdAt;
|
|
||||||
|
|
||||||
#[ORM\Column(type: 'datetime_immutable')]
|
|
||||||
#[Groups(['week_comment:read'])]
|
|
||||||
private DateTimeImmutable $updatedAt;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$now = new DateTimeImmutable();
|
|
||||||
$this->createdAt = $now;
|
|
||||||
$this->updatedAt = $now;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getId(): ?int
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getEmployee(): ?Employee
|
|
||||||
{
|
|
||||||
return $this->employee;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setEmployee(?Employee $employee): self
|
|
||||||
{
|
|
||||||
$this->employee = $employee;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getWeekStartDate(): ?DateTimeImmutable
|
|
||||||
{
|
|
||||||
return $this->weekStartDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setWeekStartDate(?DateTimeImmutable $weekStartDate): self
|
|
||||||
{
|
|
||||||
$this->weekStartDate = $weekStartDate;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getContent(): string
|
|
||||||
{
|
|
||||||
return $this->content;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setContent(string $content): self
|
|
||||||
{
|
|
||||||
$this->content = $content;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCreatedAt(): DateTimeImmutable
|
|
||||||
{
|
|
||||||
return $this->createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUpdatedAt(): DateTimeImmutable
|
|
||||||
{
|
|
||||||
return $this->updatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function touchUpdatedAt(): void
|
|
||||||
{
|
|
||||||
$this->updatedAt = new DateTimeImmutable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Repository;
|
|
||||||
|
|
||||||
use App\Entity\Employee;
|
|
||||||
use App\Entity\EmployeeWeekComment;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @extends ServiceEntityRepository<EmployeeWeekComment>
|
|
||||||
*/
|
|
||||||
class EmployeeWeekCommentRepository extends ServiceEntityRepository
|
|
||||||
{
|
|
||||||
public function __construct(ManagerRegistry $registry)
|
|
||||||
{
|
|
||||||
parent::__construct($registry, EmployeeWeekComment::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findOneByEmployeeAndWeek(Employee $employee, DateTimeImmutable $weekStart): ?EmployeeWeekComment
|
|
||||||
{
|
|
||||||
return $this->findOneBy(['employee' => $employee, 'weekStartDate' => $weekStart]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<Employee> $employees
|
|
||||||
*
|
|
||||||
* @return array<int, EmployeeWeekComment> employee_id → comment
|
|
||||||
*/
|
|
||||||
public function findByWeekAndEmployees(DateTimeImmutable $weekStart, array $employees): array
|
|
||||||
{
|
|
||||||
if ([] === $employees) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows = $this->createQueryBuilder('c')
|
|
||||||
->andWhere('c.weekStartDate = :weekStart')
|
|
||||||
->andWhere('c.employee IN (:employees)')
|
|
||||||
->setParameter('weekStart', $weekStart)
|
|
||||||
->setParameter('employees', $employees)
|
|
||||||
->innerJoin('c.employee', 'e')->addSelect('e')
|
|
||||||
->getQuery()->getResult()
|
|
||||||
;
|
|
||||||
|
|
||||||
$map = [];
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$eid = $row->getEmployee()?->getId();
|
|
||||||
if (null !== $eid) {
|
|
||||||
$map[$eid] = $row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -73,7 +73,7 @@ final readonly class LeaveRecapRowBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$cpN1Remaining = round($yearSummary['previousYearRemainingDays'], 2);
|
$cpN1Remaining = round($yearSummary['previousYearRemainingDays'], 2);
|
||||||
$cpN = (string) round($yearSummary['remainingDays'], 2);
|
$cpN = (string) round($yearSummary['acquiredDays'], 2);
|
||||||
$acquiredSaturdays = '-';
|
$acquiredSaturdays = '-';
|
||||||
} else {
|
} else {
|
||||||
$cpN1Remaining = round($yearSummary['remainingDays'], 2);
|
$cpN1Remaining = round($yearSummary['remainingDays'], 2);
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ use App\Enum\TrackingMode;
|
|||||||
use App\Repository\AbsenceRepository;
|
use App\Repository\AbsenceRepository;
|
||||||
use App\Repository\WorkHourRepository;
|
use App\Repository\WorkHourRepository;
|
||||||
use App\Service\Contracts\EmployeeContractResolver;
|
use App\Service\Contracts\EmployeeContractResolver;
|
||||||
use App\Service\PublicHolidayServiceInterface;
|
|
||||||
use DateInterval;
|
use DateInterval;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
class YearlyHoursExportBuilder
|
class YearlyHoursExportBuilder
|
||||||
{
|
{
|
||||||
@@ -27,8 +25,6 @@ class YearlyHoursExportBuilder
|
|||||||
private EmployeeContractResolver $contractResolver,
|
private EmployeeContractResolver $contractResolver,
|
||||||
private AbsenceSegmentsResolver $absenceSegmentsResolver,
|
private AbsenceSegmentsResolver $absenceSegmentsResolver,
|
||||||
private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
|
private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
|
||||||
private PublicHolidayServiceInterface $publicHolidayService,
|
|
||||||
private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,8 +56,6 @@ class YearlyHoursExportBuilder
|
|||||||
$absences = $this->absenceRepository->findForPrint($from, $to, $employees);
|
$absences = $this->absenceRepository->findForPrint($from, $to, $employees);
|
||||||
$contractMap = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
|
$contractMap = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
|
||||||
$driverMap = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
|
$driverMap = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
|
||||||
$workDaysMap = $this->contractResolver->resolveWorkDaysMinutesForEmployeesAndDays($employees, $days);
|
|
||||||
$holidayMap = $this->buildHolidayMap($from, $to);
|
|
||||||
|
|
||||||
$workHourMap = $this->buildWorkHourMap($workHours);
|
$workHourMap = $this->buildWorkHourMap($workHours);
|
||||||
$absenceMap = $this->buildAbsenceMap($absences, $days);
|
$absenceMap = $this->buildAbsenceMap($absences, $days);
|
||||||
@@ -77,8 +71,6 @@ class YearlyHoursExportBuilder
|
|||||||
$driverMap[$employeeId] ?? [],
|
$driverMap[$employeeId] ?? [],
|
||||||
$workHourMap[$employeeId] ?? [],
|
$workHourMap[$employeeId] ?? [],
|
||||||
$absenceData,
|
$absenceData,
|
||||||
$workDaysMap[$employeeId] ?? [],
|
|
||||||
$holidayMap,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if ([] === $segments) {
|
if ([] === $segments) {
|
||||||
@@ -213,9 +205,6 @@ class YearlyHoursExportBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, ?array<int, int>> $workDaysMinutesByDate
|
|
||||||
* @param array<string, string> $holidayMap
|
|
||||||
*
|
|
||||||
* @return list<array{mode: string, contractName: ?string, rows: list<array>}>
|
* @return list<array{mode: string, contractName: ?string, rows: list<array>}>
|
||||||
*/
|
*/
|
||||||
private function buildSegments(
|
private function buildSegments(
|
||||||
@@ -224,8 +213,6 @@ class YearlyHoursExportBuilder
|
|||||||
array $driverByDate,
|
array $driverByDate,
|
||||||
array $workHoursByDate,
|
array $workHoursByDate,
|
||||||
array $absenceData,
|
array $absenceData,
|
||||||
array $workDaysMinutesByDate,
|
|
||||||
array $holidayMap,
|
|
||||||
): array {
|
): array {
|
||||||
$segments = [];
|
$segments = [];
|
||||||
$currentMode = null;
|
$currentMode = null;
|
||||||
@@ -235,8 +222,7 @@ class YearlyHoursExportBuilder
|
|||||||
$firstDataDate = null;
|
$firstDataDate = null;
|
||||||
foreach ($days as $date) {
|
foreach ($days as $date) {
|
||||||
$hasRow = null !== ($workHoursByDate[$date] ?? null)
|
$hasRow = null !== ($workHoursByDate[$date] ?? null)
|
||||||
|| ($absenceData['hasDayAbsence'][$date] ?? false)
|
|| ($absenceData['hasDayAbsence'][$date] ?? false);
|
||||||
|| isset($holidayMap[$date]);
|
|
||||||
if ($hasRow) {
|
if ($hasRow) {
|
||||||
$firstDataDate = $date;
|
$firstDataDate = $date;
|
||||||
|
|
||||||
@@ -255,16 +241,14 @@ class YearlyHoursExportBuilder
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$contract = $contractsByDate[$date] ?? null;
|
$contract = $contractsByDate[$date] ?? null;
|
||||||
$isDriver = $driverByDate[$date] ?? false;
|
$isDriver = $driverByDate[$date] ?? false;
|
||||||
$wh = $workHoursByDate[$date] ?? null;
|
$wh = $workHoursByDate[$date] ?? null;
|
||||||
$hasData = null !== $wh || ($absenceData['hasDayAbsence'][$date] ?? false);
|
$hasData = null !== $wh || ($absenceData['hasDayAbsence'][$date] ?? false);
|
||||||
$holidayLabel = $holidayMap[$date] ?? null;
|
$isoDay = (int) new DateTimeImmutable($date)->format('N');
|
||||||
$isHoliday = null !== $holidayLabel;
|
$isWeekend = $isoDay >= 6;
|
||||||
$isoDay = (int) new DateTimeImmutable($date)->format('N');
|
|
||||||
$isWeekend = $isoDay >= 6;
|
|
||||||
|
|
||||||
if (!$hasData && !$isWeekend && !$isHoliday) {
|
if (!$hasData && !$isWeekend) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,18 +275,10 @@ class YearlyHoursExportBuilder
|
|||||||
|
|
||||||
$creditedMinutes = $absenceData['credited'][$date] ?? 0;
|
$creditedMinutes = $absenceData['credited'][$date] ?? 0;
|
||||||
$absenceLabel = $absenceData['labels'][$date] ?? null;
|
$absenceLabel = $absenceData['labels'][$date] ?? null;
|
||||||
$hasAbsence = $absenceData['hasDayAbsence'][$date] ?? false;
|
|
||||||
$virtualMinutes = $this->holidayVirtualHoursResolver->resolveVirtualCredit(
|
|
||||||
$contract,
|
|
||||||
new DateTimeImmutable($date),
|
|
||||||
$hasAbsence,
|
|
||||||
$workDaysMinutesByDate[$date] ?? null,
|
|
||||||
);
|
|
||||||
|
|
||||||
$row = [
|
$row = [
|
||||||
'date' => new DateTimeImmutable($date)->format('d/m/Y'),
|
'date' => new DateTimeImmutable($date)->format('d/m/Y'),
|
||||||
'absenceLabel' => $absenceLabel,
|
'absenceLabel' => $absenceLabel,
|
||||||
'holidayLabel' => $holidayLabel,
|
|
||||||
'isWeekend' => $isWeekend,
|
'isWeekend' => $isWeekend,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -321,9 +297,6 @@ class YearlyHoursExportBuilder
|
|||||||
$nightMin = $wh?->getNightHoursMinutes() ?? 0;
|
$nightMin = $wh?->getNightHoursMinutes() ?? 0;
|
||||||
$workshopMin = $wh?->getWorkshopHoursMinutes() ?? 0;
|
$workshopMin = $wh?->getWorkshopHoursMinutes() ?? 0;
|
||||||
$totalMin = $dayMin + $nightMin + $workshopMin + $creditedMinutes;
|
$totalMin = $dayMin + $nightMin + $workshopMin + $creditedMinutes;
|
||||||
if ($virtualMinutes > $totalMin) {
|
|
||||||
$totalMin = $virtualMinutes;
|
|
||||||
}
|
|
||||||
|
|
||||||
$row['dayHours'] = $this->formatMinutes($dayMin);
|
$row['dayHours'] = $this->formatMinutes($dayMin);
|
||||||
$row['nightHours'] = $this->formatMinutes($nightMin);
|
$row['nightHours'] = $this->formatMinutes($nightMin);
|
||||||
@@ -332,10 +305,6 @@ class YearlyHoursExportBuilder
|
|||||||
} else {
|
} else {
|
||||||
$metrics = null !== $wh ? $this->computeMetrics($wh) : new WorkMetrics();
|
$metrics = null !== $wh ? $this->computeMetrics($wh) : new WorkMetrics();
|
||||||
$metrics->addCreditedMinutes($creditedMinutes);
|
$metrics->addCreditedMinutes($creditedMinutes);
|
||||||
$totalMin = $metrics->totalMinutes;
|
|
||||||
if ($virtualMinutes > $totalMin) {
|
|
||||||
$totalMin = $virtualMinutes;
|
|
||||||
}
|
|
||||||
|
|
||||||
$row['morningFrom'] = $wh?->getMorningFrom() ?? '';
|
$row['morningFrom'] = $wh?->getMorningFrom() ?? '';
|
||||||
$row['morningTo'] = $wh?->getMorningTo() ?? '';
|
$row['morningTo'] = $wh?->getMorningTo() ?? '';
|
||||||
@@ -343,7 +312,7 @@ class YearlyHoursExportBuilder
|
|||||||
$row['afternoonTo'] = $wh?->getAfternoonTo() ?? '';
|
$row['afternoonTo'] = $wh?->getAfternoonTo() ?? '';
|
||||||
$row['eveningFrom'] = $wh?->getEveningFrom() ?? '';
|
$row['eveningFrom'] = $wh?->getEveningFrom() ?? '';
|
||||||
$row['eveningTo'] = $wh?->getEveningTo() ?? '';
|
$row['eveningTo'] = $wh?->getEveningTo() ?? '';
|
||||||
$row['total'] = $this->formatMinutes($totalMin);
|
$row['total'] = $this->formatMinutes($metrics->totalMinutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
$currentRows[] = $row;
|
$currentRows[] = $row;
|
||||||
@@ -360,29 +329,6 @@ class YearlyHoursExportBuilder
|
|||||||
return $segments;
|
return $segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string> Y-m-d => label
|
|
||||||
*/
|
|
||||||
private function buildHolidayMap(DateTimeImmutable $from, DateTimeImmutable $to): array
|
|
||||||
{
|
|
||||||
$map = [];
|
|
||||||
$startYear = (int) $from->format('Y');
|
|
||||||
$endYear = (int) $to->format('Y');
|
|
||||||
|
|
||||||
try {
|
|
||||||
for ($year = $startYear; $year <= $endYear; ++$year) {
|
|
||||||
$holidays = $this->publicHolidayService->getHolidaysDayByYears('metropole', (string) $year);
|
|
||||||
foreach ($holidays as $date => $label) {
|
|
||||||
$map[(string) $date] = (string) $label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveSegmentMode(string $trackingMode, bool $isDriver): string
|
private function resolveSegmentMode(string $trackingMode, bool $isDriver): string
|
||||||
{
|
{
|
||||||
if ($isDriver) {
|
if ($isDriver) {
|
||||||
|
|||||||
@@ -119,29 +119,8 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
$summary->previousYearRemainingDays = $yearSummary['previousYearRemainingDays'];
|
$summary->previousYearRemainingDays = $yearSummary['previousYearRemainingDays'];
|
||||||
$summary->previousYearPaidDays = $paidLeaveDays;
|
$summary->previousYearPaidDays = $paidLeaveDays;
|
||||||
|
|
||||||
[$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year);
|
[$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year);
|
||||||
// Forfait-only: leaves taken from N-1 stock do NOT decrement presence days.
|
$summary->presenceDaysByMonth = $this->computePresenceDaysByMonth($employee, $periodFrom, $periodTo);
|
||||||
// For non-forfait, previousYearTakenDays is always 0, so the budget has no effect.
|
|
||||||
$n1AbsencesBudget = $yearSummary['previousYearTakenDays'];
|
|
||||||
$summary->presenceDaysByMonth = $this->computePresenceDaysByMonth(
|
|
||||||
$employee,
|
|
||||||
$periodFrom,
|
|
||||||
$periodTo,
|
|
||||||
$n1AbsencesBudget
|
|
||||||
);
|
|
||||||
|
|
||||||
// Same logic as presenceDaysByMonth but bounded at today: number of presence days
|
|
||||||
// accumulated from leave year start up to today (inclusive).
|
|
||||||
$today = new DateTimeImmutable('today');
|
|
||||||
$cappedTo = $today < $periodTo ? $today : $periodTo;
|
|
||||||
$summary->presenceDaysToToday = $today < $periodFrom
|
|
||||||
? 0.0
|
|
||||||
: array_sum($this->computePresenceDaysByMonth(
|
|
||||||
$employee,
|
|
||||||
$periodFrom,
|
|
||||||
$cappedTo,
|
|
||||||
$n1AbsencesBudget
|
|
||||||
));
|
|
||||||
|
|
||||||
return $summary;
|
return $summary;
|
||||||
}
|
}
|
||||||
@@ -707,12 +686,8 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
*
|
*
|
||||||
* @return array<string, float> YYYY-MM => presence day count
|
* @return array<string, float> YYYY-MM => presence day count
|
||||||
*/
|
*/
|
||||||
private function computePresenceDaysByMonth(
|
private function computePresenceDaysByMonth(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): array
|
||||||
Employee $employee,
|
{
|
||||||
DateTimeImmutable $from,
|
|
||||||
DateTimeImmutable $to,
|
|
||||||
float $n1AbsencesBudget = 0.0
|
|
||||||
): array {
|
|
||||||
$publicHolidays = $this->buildPublicHolidayMap($from, $to);
|
$publicHolidays = $this->buildPublicHolidayMap($from, $to);
|
||||||
$weekendWorkedDays = $this->workHourRepository->countWeekendWorkedDaysByMonth($employee, $from, $to);
|
$weekendWorkedDays = $this->workHourRepository->countWeekendWorkedDaysByMonth($employee, $from, $to);
|
||||||
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $from, $to);
|
$absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $from, $to);
|
||||||
@@ -722,20 +697,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
? $this->workHourRepository->findWorkedDatesAmong($employee, array_keys($publicHolidays))
|
? $this->workHourRepository->findWorkedDatesAmong($employee, array_keys($publicHolidays))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Sort absences chronologically so N-1 budget (forfait only) is consumed in date order:
|
|
||||||
// earliest absences attribute to N-1 first, later ones overflow to N and reduce presence.
|
|
||||||
$sortedAbsences = $absences;
|
|
||||||
usort(
|
|
||||||
$sortedAbsences,
|
|
||||||
static fn ($a, $b): int => $a->getStartDate() <=> $b->getStartDate()
|
|
||||||
);
|
|
||||||
|
|
||||||
$remainingN1Budget = $n1AbsencesBudget;
|
|
||||||
|
|
||||||
// Count absence days per month, iterating day by day to handle multi-day absences
|
// Count absence days per month, iterating day by day to handle multi-day absences
|
||||||
// and properly distribute across months.
|
// and properly distribute across months.
|
||||||
$absenceDaysByMonth = [];
|
$absenceDaysByMonth = [];
|
||||||
foreach ($sortedAbsences as $absence) {
|
foreach ($absences as $absence) {
|
||||||
$start = DateTimeImmutable::createFromInterface($absence->getStartDate())->setTime(0, 0);
|
$start = DateTimeImmutable::createFromInterface($absence->getStartDate())->setTime(0, 0);
|
||||||
$end = DateTimeImmutable::createFromInterface($absence->getEndDate())->setTime(0, 0);
|
$end = DateTimeImmutable::createFromInterface($absence->getEndDate())->setTime(0, 0);
|
||||||
|
|
||||||
@@ -753,17 +718,6 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forfait: leaves taken from N-1 stock do NOT decrement presence days.
|
|
||||||
// We chronologically consume the N-1 budget before counting any absence.
|
|
||||||
if ($remainingN1Budget > 0.0) {
|
|
||||||
$consumed = min($remainingN1Budget, $dayAmount);
|
|
||||||
$remainingN1Budget -= $consumed;
|
|
||||||
$dayAmount -= $consumed;
|
|
||||||
if ($dayAmount <= 0.0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$absenceDaysByMonth[$monthKey] = ($absenceDaysByMonth[$monthKey] ?? 0.0) + $dayAmount;
|
$absenceDaysByMonth[$monthKey] = ($absenceDaysByMonth[$monthKey] ?? 0.0) + $dayAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,18 +164,6 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
|
|||||||
$monthBuckets[$m]['bonus50'] += $payment->getBonus50Minutes();
|
$monthBuckets[$m]['bonus50'] += $payment->getBonus50Minutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
$runningCumul = $summary->carryFromPreviousYearMinutes;
|
|
||||||
$prevMonth = null;
|
|
||||||
foreach ($summary->weeks as $week) {
|
|
||||||
if (null !== $prevMonth && $week->month !== $prevMonth && isset($monthBuckets[$prevMonth])) {
|
|
||||||
$b = $monthBuckets[$prevMonth];
|
|
||||||
$runningCumul -= $b['base25'] + $b['bonus25'] + $b['base50'] + $b['bonus50'];
|
|
||||||
}
|
|
||||||
$runningCumul += $week->totalMinutes;
|
|
||||||
$week->cumulativeBalanceMinutes = $runningCumul;
|
|
||||||
$prevMonth = $week->month;
|
|
||||||
}
|
|
||||||
|
|
||||||
$monthPayments = [];
|
$monthPayments = [];
|
||||||
$totalPaidMinutes = 0;
|
$totalPaidMinutes = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\State;
|
|
||||||
|
|
||||||
use ApiPlatform\Metadata\DeleteOperationInterface;
|
|
||||||
use ApiPlatform\Metadata\Operation;
|
|
||||||
use ApiPlatform\State\ProcessorInterface;
|
|
||||||
use App\Entity\Employee;
|
|
||||||
use App\Entity\EmployeeWeekComment;
|
|
||||||
use App\Service\AuditLogger;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
|
||||||
|
|
||||||
final readonly class EmployeeWeekCommentWriteProcessor implements ProcessorInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
|
|
||||||
private ProcessorInterface $persistProcessor,
|
|
||||||
#[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')]
|
|
||||||
private ProcessorInterface $removeProcessor,
|
|
||||||
private EntityManagerInterface $entityManager,
|
|
||||||
private AuditLogger $auditLogger,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
|
||||||
{
|
|
||||||
if (!$data instanceof EmployeeWeekComment) {
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
$employee = $data->getEmployee();
|
|
||||||
|
|
||||||
if ($operation instanceof DeleteOperationInterface) {
|
|
||||||
$this->auditLogger->log(
|
|
||||||
$employee,
|
|
||||||
'delete',
|
|
||||||
'week_comment',
|
|
||||||
$data->getId(),
|
|
||||||
sprintf('Commentaire semaine supprimé pour %s (semaine du %s)', $this->label($employee), $data->getWeekStartDate()?->format('d/m/Y') ?? '?'),
|
|
||||||
['old' => ['content' => $data->getContent()]],
|
|
||||||
$data->getWeekStartDate(),
|
|
||||||
);
|
|
||||||
$result = $this->removeProcessor->process($data, $operation, $uriVariables, $context);
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
$weekStart = $data->getWeekStartDate();
|
|
||||||
if (null === $weekStart || '1' !== $weekStart->format('N')) {
|
|
||||||
throw new UnprocessableEntityHttpException('weekStartDate must be a Monday (ISO weekday 1).');
|
|
||||||
}
|
|
||||||
|
|
||||||
$prev = null;
|
|
||||||
if (null !== $data->getId()) {
|
|
||||||
$prev = $this->entityManager->getUnitOfWork()->getOriginalEntityData($data)['content'] ?? null;
|
|
||||||
$data->touchUpdatedAt();
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
|
||||||
|
|
||||||
if (null === $prev) {
|
|
||||||
$this->auditLogger->log($employee, 'create', 'week_comment', $data->getId(), sprintf('Commentaire semaine créé pour %s (semaine du %s)', $this->label($employee), $weekStart->format('d/m/Y')), ['new' => ['content' => $data->getContent()]], $weekStart);
|
|
||||||
} elseif ($prev !== $data->getContent()) {
|
|
||||||
$this->auditLogger->log($employee, 'update', 'week_comment', $data->getId(), sprintf('Commentaire semaine modifié pour %s (semaine du %s)', $this->label($employee), $weekStart->format('d/m/Y')), ['old' => ['content' => $prev], 'new' => ['content' => $data->getContent()]], $weekStart);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->entityManager->flush();
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function label(mixed $e): string
|
|
||||||
{
|
|
||||||
return $e instanceof Employee ? trim(($e->getLastName() ?? '').' '.($e->getFirstName() ?? '')) : '?';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -363,10 +363,7 @@ class SalaryRecapPrintProvider implements ProviderInterface
|
|||||||
if ($wh->getHasBreakfast()) {
|
if ($wh->getHasBreakfast()) {
|
||||||
++$driverBreakfast;
|
++$driverBreakfast;
|
||||||
}
|
}
|
||||||
if ($wh->getHasLunch()) {
|
if ($wh->getHasLunch() || $wh->getHasDinner()) {
|
||||||
++$driverMeals;
|
|
||||||
}
|
|
||||||
if ($wh->getHasDinner()) {
|
|
||||||
++$driverMeals;
|
++$driverMeals;
|
||||||
}
|
}
|
||||||
if ($wh->getHasOvernight()) {
|
if ($wh->getHasOvernight()) {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use App\Dto\WorkHours\WorkMetrics;
|
|||||||
use App\Entity\Absence;
|
use App\Entity\Absence;
|
||||||
use App\Entity\Contract;
|
use App\Entity\Contract;
|
||||||
use App\Entity\Employee;
|
use App\Entity\Employee;
|
||||||
use App\Entity\EmployeeWeekComment;
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Entity\WorkHour;
|
use App\Entity\WorkHour;
|
||||||
use App\Enum\ContractNature;
|
use App\Enum\ContractNature;
|
||||||
@@ -22,9 +21,7 @@ use App\Enum\TrackingMode;
|
|||||||
use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
||||||
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
||||||
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
||||||
use App\Repository\EmployeeWeekCommentRepository;
|
|
||||||
use App\Service\Contracts\EmployeeContractResolver;
|
use App\Service\Contracts\EmployeeContractResolver;
|
||||||
use App\Service\PublicHolidayServiceInterface;
|
|
||||||
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
||||||
use App\Service\WorkHours\DailyReferenceMinutesResolver;
|
use App\Service\WorkHours\DailyReferenceMinutesResolver;
|
||||||
use App\Service\WorkHours\HolidayVirtualHoursResolver;
|
use App\Service\WorkHours\HolidayVirtualHoursResolver;
|
||||||
@@ -34,7 +31,6 @@ use Symfony\Bundle\SecurityBundle\Security;
|
|||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
||||||
{
|
{
|
||||||
@@ -49,8 +45,6 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
|||||||
private EmployeeContractResolver $contractResolver,
|
private EmployeeContractResolver $contractResolver,
|
||||||
private DailyReferenceMinutesResolver $dailyReferenceResolver,
|
private DailyReferenceMinutesResolver $dailyReferenceResolver,
|
||||||
private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
|
private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
|
||||||
private PublicHolidayServiceInterface $publicHolidayService,
|
|
||||||
private EmployeeWeekCommentRepository $weekCommentRepository,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourWeeklySummary
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): WorkHourWeeklySummary
|
||||||
@@ -68,13 +62,11 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
|||||||
$workHours = $this->workHourRepository->findByDateRangeAndEmployees($weekStart, $weekEnd, $employees);
|
$workHours = $this->workHourRepository->findByDateRangeAndEmployees($weekStart, $weekEnd, $employees);
|
||||||
$absences = $this->absenceRepository->findForPrint($weekStart, $weekEnd, $employees);
|
$absences = $this->absenceRepository->findForPrint($weekStart, $weekEnd, $employees);
|
||||||
|
|
||||||
$weekComments = $this->weekCommentRepository->findByWeekAndEmployees($weekStart, $employees);
|
|
||||||
|
|
||||||
$summary = new WorkHourWeeklySummary();
|
$summary = new WorkHourWeeklySummary();
|
||||||
$summary->weekStart = $weekStart->format('Y-m-d');
|
$summary->weekStart = $weekStart->format('Y-m-d');
|
||||||
$summary->weekEnd = $weekEnd->format('Y-m-d');
|
$summary->weekEnd = $weekEnd->format('Y-m-d');
|
||||||
$summary->days = $days;
|
$summary->days = $days;
|
||||||
$summary->rows = $this->buildRows($employees, $workHours, $absences, $days, $anchorDate->format('Y-m-d'), $weekComments);
|
$summary->rows = $this->buildRows($employees, $workHours, $absences, $days, $anchorDate->format('Y-m-d'));
|
||||||
|
|
||||||
return $summary;
|
return $summary;
|
||||||
}
|
}
|
||||||
@@ -117,21 +109,19 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param list<Employee> $employees
|
* @param list<Employee> $employees
|
||||||
* @param list<WorkHour> $workHours
|
* @param list<WorkHour> $workHours
|
||||||
* @param list<Absence> $absences
|
* @param list<Absence> $absences
|
||||||
* @param list<string> $days
|
* @param list<string> $days
|
||||||
* @param array<int, EmployeeWeekComment> $weekComments
|
|
||||||
*
|
*
|
||||||
* @return list<WeeklySummaryRow>
|
* @return list<WeeklySummaryRow>
|
||||||
*/
|
*/
|
||||||
private function buildRows(array $employees, array $workHours, array $absences, array $days, string $anchorDateYmd, array $weekComments = []): array
|
private function buildRows(array $employees, array $workHours, array $absences, array $days, string $anchorDateYmd): array
|
||||||
{
|
{
|
||||||
$contractsByEmployeeDate = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
|
$contractsByEmployeeDate = $this->contractResolver->resolveForEmployeesAndDays($employees, $days);
|
||||||
$contractNaturesByEmployeeDate = $this->contractResolver->resolveNaturesForEmployeesAndDays($employees, $days);
|
$contractNaturesByEmployeeDate = $this->contractResolver->resolveNaturesForEmployeesAndDays($employees, $days);
|
||||||
$isDriverByEmployeeDate = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
|
$isDriverByEmployeeDate = $this->contractResolver->resolveIsDriverForEmployeesAndDays($employees, $days);
|
||||||
$workDaysByEmployeeDate = $this->contractResolver->resolveWorkDaysMinutesForEmployeesAndDays($employees, $days);
|
$workDaysByEmployeeDate = $this->contractResolver->resolveWorkDaysMinutesForEmployeesAndDays($employees, $days);
|
||||||
$holidayLabelsByDate = $this->buildHolidayLabelsForDays($days);
|
|
||||||
$metricsByEmployeeDate = [];
|
$metricsByEmployeeDate = [];
|
||||||
foreach ($workHours as $workHour) {
|
foreach ($workHours as $workHour) {
|
||||||
$employeeId = $workHour->getEmployee()?->getId();
|
$employeeId = $workHour->getEmployee()?->getId();
|
||||||
@@ -334,7 +324,6 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
|||||||
hasDinner: $hasDinner,
|
hasDinner: $hasDinner,
|
||||||
hasOvernight: $hasOvernight,
|
hasOvernight: $hasOvernight,
|
||||||
virtualHolidayMinutes: $virtualHolidayMinutes,
|
virtualHolidayMinutes: $virtualHolidayMinutes,
|
||||||
holidayLabel: $holidayLabelsByDate[$date] ?? null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,46 +370,12 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface
|
|||||||
weeklyOvernightCount: $weeklyOvernightCount,
|
weeklyOvernightCount: $weeklyOvernightCount,
|
||||||
hasContractForWeek: $hasContractForWeek,
|
hasContractForWeek: $hasContractForWeek,
|
||||||
contractNature: $weekAnchorContractNature->value,
|
contractNature: $weekAnchorContractNature->value,
|
||||||
comment: ($weekComments[$employeeId] ?? null)?->getContent(),
|
|
||||||
commentId: ($weekComments[$employeeId] ?? null)?->getId(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $rows;
|
return $rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<string> $days
|
|
||||||
*
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
private function buildHolidayLabelsForDays(array $days): array
|
|
||||||
{
|
|
||||||
if ([] === $days) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$years = [];
|
|
||||||
foreach ($days as $day) {
|
|
||||||
$years[substr($day, 0, 4)] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$map = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
foreach (array_keys($years) as $year) {
|
|
||||||
$holidays = $this->publicHolidayService->getHolidaysDayByYears('metropole', (string) $year);
|
|
||||||
foreach ($holidays as $date => $label) {
|
|
||||||
$map[(string) $date] = (string) $label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function computeMetrics(WorkHour $workHour): WorkMetrics
|
private function computeMetrics(WorkHour $workHour): WorkMetrics
|
||||||
{
|
{
|
||||||
$ranges = [
|
$ranges = [
|
||||||
|
|||||||
@@ -76,14 +76,11 @@
|
|||||||
td { font-size: 9px; }
|
td { font-size: 9px; }
|
||||||
td.date { text-align: left; font-weight: bold; }
|
td.date { text-align: left; font-weight: bold; }
|
||||||
td.absence { text-align: left; color: #c00; }
|
td.absence { text-align: left; color: #c00; }
|
||||||
td.absence .holiday { color: #0277bd; font-weight: 600; }
|
|
||||||
td.absence .holiday.with-absence { display: block; }
|
|
||||||
td.time { text-align: center; }
|
td.time { text-align: center; }
|
||||||
td.presence { text-align: center; }
|
td.presence { text-align: center; }
|
||||||
td.total { text-align: center; font-weight: bold; }
|
td.total { text-align: center; font-weight: bold; }
|
||||||
tr.weekend td { background: #f3f3f3; color: #555; }
|
tr.weekend td { background: #f3f3f3; color: #555; }
|
||||||
tr.weekend td.date { color: #333; }
|
tr.weekend td.date { color: #333; }
|
||||||
tr.holiday td { background: #e1f5fe; }
|
|
||||||
|
|
||||||
.signature-footer {
|
.signature-footer {
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
@@ -168,12 +165,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in segment.rows %}
|
{% for row in segment.rows %}
|
||||||
<tr class="{{ row.isWeekend ? 'weekend' : (row.holidayLabel ? 'holiday' : '') }}">
|
<tr class="{{ row.isWeekend ? 'weekend' : '' }}">
|
||||||
<td class="date">{{ row.date }}</td>
|
<td class="date">{{ row.date }}</td>
|
||||||
<td class="absence">
|
<td class="absence">{{ row.absenceLabel ?? '' }}</td>
|
||||||
{{ row.absenceLabel ?? '' }}
|
|
||||||
{% if row.holidayLabel %}<span class="holiday {{ row.absenceLabel ? 'with-absence' : '' }}">Férié : {{ row.holidayLabel }}</span>{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="presence">{{ row.presentMorning ? 'X' : '' }}</td>
|
<td class="presence">{{ row.presentMorning ? 'X' : '' }}</td>
|
||||||
<td class="presence">{{ row.presentAfternoon ? 'X' : '' }}</td>
|
<td class="presence">{{ row.presentAfternoon ? 'X' : '' }}</td>
|
||||||
<td class="total">{{ row.total }}</td>
|
<td class="total">{{ row.total }}</td>
|
||||||
@@ -195,12 +189,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in segment.rows %}
|
{% for row in segment.rows %}
|
||||||
<tr class="{{ row.isWeekend ? 'weekend' : (row.holidayLabel ? 'holiday' : '') }}">
|
<tr class="{{ row.isWeekend ? 'weekend' : '' }}">
|
||||||
<td class="date">{{ row.date }}</td>
|
<td class="date">{{ row.date }}</td>
|
||||||
<td class="absence">
|
<td class="absence">{{ row.absenceLabel ?? '' }}</td>
|
||||||
{{ row.absenceLabel ?? '' }}
|
|
||||||
{% if row.holidayLabel %}<span class="holiday {{ row.absenceLabel ? 'with-absence' : '' }}">Férié : {{ row.holidayLabel }}</span>{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="time">{{ row.dayHours }}</td>
|
<td class="time">{{ row.dayHours }}</td>
|
||||||
<td class="time">{{ row.nightHours }}</td>
|
<td class="time">{{ row.nightHours }}</td>
|
||||||
<td class="time">{{ row.workshopHours }}</td>
|
<td class="time">{{ row.workshopHours }}</td>
|
||||||
@@ -226,12 +217,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in segment.rows %}
|
{% for row in segment.rows %}
|
||||||
<tr class="{{ row.isWeekend ? 'weekend' : (row.holidayLabel ? 'holiday' : '') }}">
|
<tr class="{{ row.isWeekend ? 'weekend' : '' }}">
|
||||||
<td class="date">{{ row.date }}</td>
|
<td class="date">{{ row.date }}</td>
|
||||||
<td class="absence">
|
<td class="absence">{{ row.absenceLabel ?? '' }}</td>
|
||||||
{{ row.absenceLabel ?? '' }}
|
|
||||||
{% if row.holidayLabel %}<span class="holiday {{ row.absenceLabel ? 'with-absence' : '' }}">Férié : {{ row.holidayLabel }}</span>{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="time">{{ row.morningFrom }}</td>
|
<td class="time">{{ row.morningFrom }}</td>
|
||||||
<td class="time">{{ row.morningTo }}</td>
|
<td class="time">{{ row.morningTo }}</td>
|
||||||
<td class="time">{{ row.afternoonFrom }}</td>
|
<td class="time">{{ row.afternoonFrom }}</td>
|
||||||
|
|||||||
@@ -65,14 +65,11 @@
|
|||||||
td { font-size: 9px; }
|
td { font-size: 9px; }
|
||||||
td.date { text-align: left; font-weight: bold; }
|
td.date { text-align: left; font-weight: bold; }
|
||||||
td.absence { text-align: left; color: #c00; }
|
td.absence { text-align: left; color: #c00; }
|
||||||
td.absence .holiday { color: #0277bd; font-weight: 600; }
|
|
||||||
td.absence .holiday.with-absence { display: block; }
|
|
||||||
td.time { text-align: center; }
|
td.time { text-align: center; }
|
||||||
td.presence { text-align: center; }
|
td.presence { text-align: center; }
|
||||||
td.total { text-align: center; font-weight: bold; }
|
td.total { text-align: center; font-weight: bold; }
|
||||||
tr.weekend td { background: #f3f3f3; color: #555; }
|
tr.weekend td { background: #f3f3f3; color: #555; }
|
||||||
tr.weekend td.date { color: #333; }
|
tr.weekend td.date { color: #333; }
|
||||||
tr.holiday td { background: #e1f5fe; }
|
|
||||||
|
|
||||||
.signature-footer {
|
.signature-footer {
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
@@ -154,12 +151,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in segment.rows %}
|
{% for row in segment.rows %}
|
||||||
<tr class="{{ row.isWeekend ? 'weekend' : (row.holidayLabel ? 'holiday' : '') }}">
|
<tr class="{{ row.isWeekend ? 'weekend' : '' }}">
|
||||||
<td class="date">{{ row.date }}</td>
|
<td class="date">{{ row.date }}</td>
|
||||||
<td class="absence">
|
<td class="absence">{{ row.absenceLabel ?? '' }}</td>
|
||||||
{{ row.absenceLabel ?? '' }}
|
|
||||||
{% if row.holidayLabel %}<span class="holiday {{ row.absenceLabel ? 'with-absence' : '' }}">Férié : {{ row.holidayLabel }}</span>{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="presence">{{ row.presentMorning ? 'X' : '' }}</td>
|
<td class="presence">{{ row.presentMorning ? 'X' : '' }}</td>
|
||||||
<td class="presence">{{ row.presentAfternoon ? 'X' : '' }}</td>
|
<td class="presence">{{ row.presentAfternoon ? 'X' : '' }}</td>
|
||||||
<td class="total">{{ row.total }}</td>
|
<td class="total">{{ row.total }}</td>
|
||||||
@@ -181,12 +175,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in segment.rows %}
|
{% for row in segment.rows %}
|
||||||
<tr class="{{ row.isWeekend ? 'weekend' : (row.holidayLabel ? 'holiday' : '') }}">
|
<tr class="{{ row.isWeekend ? 'weekend' : '' }}">
|
||||||
<td class="date">{{ row.date }}</td>
|
<td class="date">{{ row.date }}</td>
|
||||||
<td class="absence">
|
<td class="absence">{{ row.absenceLabel ?? '' }}</td>
|
||||||
{{ row.absenceLabel ?? '' }}
|
|
||||||
{% if row.holidayLabel %}<span class="holiday {{ row.absenceLabel ? 'with-absence' : '' }}">Férié : {{ row.holidayLabel }}</span>{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="time">{{ row.dayHours }}</td>
|
<td class="time">{{ row.dayHours }}</td>
|
||||||
<td class="time">{{ row.nightHours }}</td>
|
<td class="time">{{ row.nightHours }}</td>
|
||||||
<td class="time">{{ row.workshopHours }}</td>
|
<td class="time">{{ row.workshopHours }}</td>
|
||||||
@@ -212,12 +203,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in segment.rows %}
|
{% for row in segment.rows %}
|
||||||
<tr class="{{ row.isWeekend ? 'weekend' : (row.holidayLabel ? 'holiday' : '') }}">
|
<tr class="{{ row.isWeekend ? 'weekend' : '' }}">
|
||||||
<td class="date">{{ row.date }}</td>
|
<td class="date">{{ row.date }}</td>
|
||||||
<td class="absence">
|
<td class="absence">{{ row.absenceLabel ?? '' }}</td>
|
||||||
{{ row.absenceLabel ?? '' }}
|
|
||||||
{% if row.holidayLabel %}<span class="holiday {{ row.absenceLabel ? 'with-absence' : '' }}">Férié : {{ row.holidayLabel }}</span>{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="time">{{ row.morningFrom }}</td>
|
<td class="time">{{ row.morningFrom }}</td>
|
||||||
<td class="time">{{ row.morningTo }}</td>
|
<td class="time">{{ row.morningTo }}</td>
|
||||||
<td class="time">{{ row.afternoonFrom }}</td>
|
<td class="time">{{ row.afternoonFrom }}</td>
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Tests\State;
|
|
||||||
|
|
||||||
use ApiPlatform\Metadata\Delete;
|
|
||||||
use ApiPlatform\Metadata\Post;
|
|
||||||
use ApiPlatform\State\ProcessorInterface;
|
|
||||||
use App\Entity\Employee;
|
|
||||||
use App\Entity\EmployeeWeekComment;
|
|
||||||
use App\Service\AuditLogger;
|
|
||||||
use App\State\EmployeeWeekCommentWriteProcessor;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Doctrine\ORM\UnitOfWork;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class EmployeeWeekCommentWriteProcessorTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testRejectsNonMondayWeekStart(): void
|
|
||||||
{
|
|
||||||
$processor = new EmployeeWeekCommentWriteProcessor(
|
|
||||||
$this->createStub(ProcessorInterface::class),
|
|
||||||
$this->createStub(ProcessorInterface::class),
|
|
||||||
$this->createStub(EntityManagerInterface::class),
|
|
||||||
$this->createStub(AuditLogger::class),
|
|
||||||
);
|
|
||||||
|
|
||||||
$comment = new EmployeeWeekComment()
|
|
||||||
->setEmployee(new Employee()->setFirstName('A')->setLastName('B'))
|
|
||||||
->setWeekStartDate(new DateTimeImmutable('2026-04-14'))
|
|
||||||
->setContent('test')
|
|
||||||
;
|
|
||||||
|
|
||||||
$this->expectException(UnprocessableEntityHttpException::class);
|
|
||||||
$processor->process($comment, new Post());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAcceptsMondayAndAuditsCreate(): void
|
|
||||||
{
|
|
||||||
$persist = $this->createMock(ProcessorInterface::class);
|
|
||||||
$persist->expects(self::once())->method('process');
|
|
||||||
$em = $this->createMock(EntityManagerInterface::class);
|
|
||||||
$em->method('getUnitOfWork')->willReturn($this->createStub(UnitOfWork::class));
|
|
||||||
$em->expects(self::once())->method('flush');
|
|
||||||
$auditor = $this->createMock(AuditLogger::class);
|
|
||||||
$auditor->expects(self::once())->method('log')->with(self::anything(), 'create', 'week_comment');
|
|
||||||
|
|
||||||
$processor = new EmployeeWeekCommentWriteProcessor($persist, $this->createStub(ProcessorInterface::class), $em, $auditor);
|
|
||||||
$processor->process(
|
|
||||||
new EmployeeWeekComment()->setEmployee(new Employee()->setFirstName('A')->setLastName('B'))->setWeekStartDate(new DateTimeImmutable('2026-04-13'))->setContent('x'),
|
|
||||||
new Post()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDeleteAudits(): void
|
|
||||||
{
|
|
||||||
$remove = $this->createMock(ProcessorInterface::class);
|
|
||||||
$remove->expects(self::once())->method('process');
|
|
||||||
$em = $this->createMock(EntityManagerInterface::class);
|
|
||||||
$em->expects(self::once())->method('flush');
|
|
||||||
$auditor = $this->createMock(AuditLogger::class);
|
|
||||||
$auditor->expects(self::once())->method('log')->with(self::anything(), 'delete', 'week_comment');
|
|
||||||
|
|
||||||
$processor = new EmployeeWeekCommentWriteProcessor($this->createStub(ProcessorInterface::class), $remove, $em, $auditor);
|
|
||||||
$processor->process(
|
|
||||||
new EmployeeWeekComment()->setEmployee(new Employee()->setFirstName('A')->setLastName('B'))->setWeekStartDate(new DateTimeImmutable('2026-04-13'))->setContent('x'),
|
|
||||||
new Delete()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@ use App\Enum\HalfDay;
|
|||||||
use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
use App\Repository\Contract\AbsenceReadRepositoryInterface;
|
||||||
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
use App\Repository\Contract\EmployeeScopedRepositoryInterface;
|
||||||
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
use App\Repository\Contract\WorkHourReadRepositoryInterface;
|
||||||
use App\Repository\EmployeeWeekCommentRepository;
|
|
||||||
use App\Service\Contracts\EmployeeContractResolver;
|
use App\Service\Contracts\EmployeeContractResolver;
|
||||||
use App\Service\PublicHolidayServiceInterface;
|
use App\Service\PublicHolidayServiceInterface;
|
||||||
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
||||||
@@ -67,8 +66,6 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
|
|||||||
$this->buildResolverStub(),
|
$this->buildResolverStub(),
|
||||||
new DailyReferenceMinutesResolver(),
|
new DailyReferenceMinutesResolver(),
|
||||||
$this->buildHolidayResolver(),
|
$this->buildHolidayResolver(),
|
||||||
$this->buildHolidayService(),
|
|
||||||
$this->buildWeekCommentRepoStub(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->expectException(AccessDeniedHttpException::class);
|
$this->expectException(AccessDeniedHttpException::class);
|
||||||
@@ -131,8 +128,6 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
|
|||||||
$this->buildWeeklyResolverStub($employees),
|
$this->buildWeeklyResolverStub($employees),
|
||||||
new DailyReferenceMinutesResolver(),
|
new DailyReferenceMinutesResolver(),
|
||||||
$this->buildHolidayResolver(),
|
$this->buildHolidayResolver(),
|
||||||
$this->buildHolidayService(),
|
|
||||||
$this->buildWeekCommentRepoStub(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$result = $provider->provide(new Get());
|
$result = $provider->provide(new Get());
|
||||||
@@ -183,29 +178,16 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase
|
|||||||
$property->setValue($entity, $id);
|
$property->setValue($entity, $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildWeekCommentRepoStub(): EmployeeWeekCommentRepository
|
|
||||||
{
|
|
||||||
$r = $this->createStub(EmployeeWeekCommentRepository::class);
|
|
||||||
$r->method('findByWeekAndEmployees')->willReturn([]);
|
|
||||||
|
|
||||||
return $r;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildHolidayResolver(array $holidayMap = []): HolidayVirtualHoursResolver
|
private function buildHolidayResolver(array $holidayMap = []): HolidayVirtualHoursResolver
|
||||||
{
|
|
||||||
return new HolidayVirtualHoursResolver(
|
|
||||||
new DailyReferenceMinutesResolver(),
|
|
||||||
$this->buildHolidayService($holidayMap),
|
|
||||||
$this->createStub(EmployeeContractResolver::class),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildHolidayService(array $holidayMap = []): PublicHolidayServiceInterface
|
|
||||||
{
|
{
|
||||||
$service = $this->createStub(PublicHolidayServiceInterface::class);
|
$service = $this->createStub(PublicHolidayServiceInterface::class);
|
||||||
$service->method('getHolidaysDayByYears')->willReturn($holidayMap);
|
$service->method('getHolidaysDayByYears')->willReturn($holidayMap);
|
||||||
|
|
||||||
return $service;
|
return new HolidayVirtualHoursResolver(
|
||||||
|
new DailyReferenceMinutesResolver(),
|
||||||
|
$service,
|
||||||
|
$this->createStub(EmployeeContractResolver::class),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildResolverStub(): EmployeeContractResolver
|
private function buildResolverStub(): EmployeeContractResolver
|
||||||
|
|||||||
Reference in New Issue
Block a user