feat(heures) : calendrier des jours validés (vue Jour) + harmonisation Malio UI (#30)
Auto Tag Develop / tag (push) Successful in 10s
Auto Tag Develop / tag (push) Successful in 10s
## Fonctionnel - Calendrier MalioDate en vue Jour (écrans Heures ET Heures Conducteurs) : les jours entièrement validés par un admin sont peints en vert. - Endpoint `GET /work-hours/validation-status?from=&to=[&driver=1]` (scope conducteur inversé via `driver=1`), périmètre complet (ignore le filtre sites). - Chargement à la volée par mois (event `@month-change`), refresh après validation / saisie / absence. ## Harmonisation @malio/layer-ui 1.7.11 - `reserveMessageSpace=false` sur tous les champs (alignement). - Tous les drawers migrés sur `MalioDrawer` (titre via slot `#header`, `AppDrawer` custom supprimé). - Boutons d'action en `MalioButton` ; deux boutons côte à côte partagent l'espace. - Inputs date en `MalioDate`, sélecteur semaine en `MalioDateWeek`. - Boutons d'ajout uniformisés sur « Ajouter » + icône. ## Divers - `.env` : `EXCLUDED_PUBLIC_HOLIDAYS="null"`. - Doc : `doc/hours-validated-days.md`, `documentation-content.ts`, `CLAUDE.md`. - Tests : provider `WorkHourValidationStatus` (suite complète 236/236 OK via pre-commit hook). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #30 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #30.
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
bulkUpdateWorkHourValidation,
|
||||
bulkUpsertWorkHours,
|
||||
getWorkHourDayContext,
|
||||
getWorkHourValidationStatus,
|
||||
getWeeklyWorkHourSummary,
|
||||
listWorkHoursByDate,
|
||||
updateWorkHourSiteValidation,
|
||||
@@ -28,7 +29,8 @@ import {
|
||||
getWeekStartDate,
|
||||
getTodayYmd,
|
||||
parseYmd,
|
||||
shiftYmd
|
||||
shiftYmd,
|
||||
toYmd
|
||||
} from '~/utils/date'
|
||||
import { sortEmployeesBySiteAndOrder } from '~/utils/employee'
|
||||
|
||||
@@ -68,6 +70,9 @@ export const useDriverHoursPage = () => {
|
||||
const isSubmitting = ref(false)
|
||||
const validatingRowIds = ref<number[]>([])
|
||||
const siteValidatingRowIds = ref<number[]>([])
|
||||
// Jours entièrement validés (conducteurs) par mois civil, pour le calendrier de
|
||||
// la vue Jour. Clé 'YYYY-MM' → dates Y-m-d. Chargé à la volée sur @month-change.
|
||||
const validatedDaysByMonth = ref<Record<string, string[]>>({})
|
||||
|
||||
const dayGridCols = computed(() => {
|
||||
const metricCol = '0.4fr'
|
||||
@@ -519,10 +524,11 @@ export const useDriverHoursPage = () => {
|
||||
const refreshAfterAbsenceChange = async () => {
|
||||
if (isAdmin.value) {
|
||||
await Promise.all([loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
||||
return
|
||||
} else {
|
||||
weeklySummary.value = null
|
||||
await Promise.all([loadDayContext(), loadAbsences()])
|
||||
}
|
||||
weeklySummary.value = null
|
||||
await Promise.all([loadDayContext(), loadAbsences()])
|
||||
await reloadValidationMonth(selectedDate.value)
|
||||
}
|
||||
|
||||
const submitAbsence = async () => {
|
||||
@@ -626,6 +632,7 @@ export const useDriverHoursPage = () => {
|
||||
try {
|
||||
await updateWorkHourValidation(updatedRow.workHourId, checked, { toast: options.toast })
|
||||
updatedRow.isValid = checked
|
||||
await reloadValidationMonth(selectedDate.value)
|
||||
} finally {
|
||||
validatingRowIds.value = validatingRowIds.value.filter((id) => id !== employeeId)
|
||||
}
|
||||
@@ -708,6 +715,7 @@ export const useDriverHoursPage = () => {
|
||||
}, { toast: false })
|
||||
|
||||
await loadWorkHours()
|
||||
await reloadValidationMonth(selectedDate.value)
|
||||
|
||||
if (result.updated === 0) {
|
||||
toast.error({ title: 'Erreur', message: 'Aucune ligne mise à jour.' })
|
||||
@@ -825,6 +833,45 @@ export const useDriverHoursPage = () => {
|
||||
dayContext.value = await getWorkHourDayContext(selectedDate.value)
|
||||
}
|
||||
|
||||
// --- Calendrier vue Jour : jours validés en vert (scope conducteurs) ---------
|
||||
const monthKey = (year: number, monthIndex: number) => `${year}-${String(monthIndex + 1).padStart(2, '0')}`
|
||||
|
||||
const markedDates = computed<Record<string, 'success'>>(() => {
|
||||
const map: Record<string, 'success'> = {}
|
||||
for (const days of Object.values(validatedDaysByMonth.value)) {
|
||||
for (const day of days) map[day] = 'success'
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
// Plage = grille visible complète (lundi avant le 1er → dimanche après le dernier).
|
||||
// driver:true → l'endpoint ne considère que les conducteurs.
|
||||
const loadValidationMonth = async (monthIndex: number, year: number, options: { force?: boolean } = {}) => {
|
||||
const key = monthKey(year, monthIndex)
|
||||
if (!options.force && validatedDaysByMonth.value[key]) return
|
||||
|
||||
const gridStart = getWeekStartDate(new Date(year, monthIndex, 1))
|
||||
const gridEnd = getWeekStartDate(new Date(year, monthIndex + 1, 0))
|
||||
gridEnd.setDate(gridEnd.getDate() + 6)
|
||||
|
||||
const from = toYmd(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate())
|
||||
const to = toYmd(gridEnd.getFullYear(), gridEnd.getMonth(), gridEnd.getDate())
|
||||
const days = await getWorkHourValidationStatus(from, to, { driver: true })
|
||||
validatedDaysByMonth.value = { ...validatedDaysByMonth.value, [key]: days }
|
||||
}
|
||||
|
||||
const onCalendarMonthChange = (payload: { month: number; year: number }) => {
|
||||
void loadValidationMonth(payload.month, payload.year)
|
||||
}
|
||||
|
||||
const reloadValidationMonth = async (dateYmd: string) => {
|
||||
const parsed = parseYmd(dateYmd)
|
||||
if (!parsed) return
|
||||
const key = monthKey(parsed.getFullYear(), parsed.getMonth())
|
||||
if (!validatedDaysByMonth.value[key]) return
|
||||
await loadValidationMonth(parsed.getMonth(), parsed.getFullYear(), { force: true })
|
||||
}
|
||||
|
||||
const refreshByDate = async () => {
|
||||
if (isAdmin.value) {
|
||||
await Promise.all([loadWorkHours(), loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
||||
@@ -921,6 +968,7 @@ export const useDriverHoursPage = () => {
|
||||
})
|
||||
|
||||
await refreshByDate()
|
||||
await reloadValidationMonth(selectedDate.value)
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
@@ -1003,6 +1051,8 @@ export const useDriverHoursPage = () => {
|
||||
closeAbsenceDrawer,
|
||||
formatMinutes,
|
||||
handleSave,
|
||||
markedDates,
|
||||
onCalendarMonthChange,
|
||||
isWeekCommentDrawerOpen,
|
||||
weekCommentContext,
|
||||
openWeekCommentDrawer,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
bulkUpdateWorkHourValidation,
|
||||
bulkUpsertWorkHours,
|
||||
getWorkHourDayContext,
|
||||
getWorkHourValidationStatus,
|
||||
getWeeklyWorkHourSummary,
|
||||
listWorkHoursByDate,
|
||||
updateWorkHourSiteValidation,
|
||||
@@ -30,7 +31,8 @@ import {
|
||||
getWeekStartDate,
|
||||
getTodayYmd,
|
||||
parseYmd,
|
||||
shiftYmd
|
||||
shiftYmd,
|
||||
toYmd
|
||||
} from '~/utils/date'
|
||||
import { sortEmployeesBySiteAndOrder } from '~/utils/employee'
|
||||
|
||||
@@ -70,6 +72,10 @@ export const useHoursPage = () => {
|
||||
const isSubmitting = ref(false)
|
||||
const validatingRowIds = ref<number[]>([])
|
||||
const siteValidatingRowIds = ref<number[]>([])
|
||||
// Jours entièrement validés (admin) par mois civil affiché dans le calendrier
|
||||
// de la vue Jour. Clé = 'YYYY-MM', valeur = liste de dates Y-m-d. Chargé à la
|
||||
// volée sur @month-change (jamais préchargé sur plusieurs années).
|
||||
const validatedDaysByMonth = ref<Record<string, string[]>>({})
|
||||
|
||||
const dayGridCols = computed(() => {
|
||||
const metricCol = '0.4fr'
|
||||
@@ -686,11 +692,11 @@ export const useHoursPage = () => {
|
||||
const refreshAfterAbsenceChange = async () => {
|
||||
if (isAdmin.value) {
|
||||
await Promise.all([loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
||||
return
|
||||
} else {
|
||||
weeklySummary.value = null
|
||||
await Promise.all([loadDayContext(), loadAbsences()])
|
||||
}
|
||||
|
||||
weeklySummary.value = null
|
||||
await Promise.all([loadDayContext(), loadAbsences()])
|
||||
await reloadValidationMonth(selectedDate.value)
|
||||
}
|
||||
|
||||
const submitAbsence = async () => {
|
||||
@@ -787,6 +793,7 @@ export const useHoursPage = () => {
|
||||
try {
|
||||
await updateWorkHourValidation(updatedRow.workHourId, checked, { toast: options.toast })
|
||||
updatedRow.isValid = checked
|
||||
await reloadValidationMonth(selectedDate.value)
|
||||
} finally {
|
||||
validatingRowIds.value = validatingRowIds.value.filter((id) => id !== employeeId)
|
||||
}
|
||||
@@ -891,6 +898,7 @@ export const useHoursPage = () => {
|
||||
}, { toast: false })
|
||||
|
||||
await loadWorkHours()
|
||||
await reloadValidationMonth(selectedDate.value)
|
||||
|
||||
if (result.updated === 0) {
|
||||
toast.error({
|
||||
@@ -1031,6 +1039,50 @@ export const useHoursPage = () => {
|
||||
dayContext.value = await getWorkHourDayContext(selectedDate.value)
|
||||
}
|
||||
|
||||
// --- Calendrier vue Jour : jours validés en vert ---------------------------
|
||||
const monthKey = (year: number, monthIndex: number) => `${year}-${String(monthIndex + 1).padStart(2, '0')}`
|
||||
|
||||
// Fusionne tous les mois chargés en une seule map ISO → 'success' pour MalioDate.
|
||||
const markedDates = computed<Record<string, 'success'>>(() => {
|
||||
const map: Record<string, 'success'> = {}
|
||||
for (const days of Object.values(validatedDaysByMonth.value)) {
|
||||
for (const day of days) map[day] = 'success'
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
// Charge le statut du mois affiché. La plage couvre toute la grille visible
|
||||
// (lundi avant le 1er → dimanche après le dernier jour) pour colorer aussi les
|
||||
// jours débordants des mois adjacents.
|
||||
const loadValidationMonth = async (monthIndex: number, year: number, options: { force?: boolean } = {}) => {
|
||||
const key = monthKey(year, monthIndex)
|
||||
if (!options.force && validatedDaysByMonth.value[key]) return
|
||||
|
||||
const gridStart = getWeekStartDate(new Date(year, monthIndex, 1))
|
||||
const gridEnd = getWeekStartDate(new Date(year, monthIndex + 1, 0))
|
||||
gridEnd.setDate(gridEnd.getDate() + 6)
|
||||
|
||||
const from = toYmd(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate())
|
||||
const to = toYmd(gridEnd.getFullYear(), gridEnd.getMonth(), gridEnd.getDate())
|
||||
const days = await getWorkHourValidationStatus(from, to)
|
||||
validatedDaysByMonth.value = { ...validatedDaysByMonth.value, [key]: days }
|
||||
}
|
||||
|
||||
const onCalendarMonthChange = (payload: { month: number; year: number }) => {
|
||||
void loadValidationMonth(payload.month, payload.year)
|
||||
}
|
||||
|
||||
// Après une modification qui touche la validation d'un jour (validation,
|
||||
// sauvegarde d'heures, absence), recharge le mois concerné s'il est déjà en
|
||||
// cache → le calendrier se recolore. Sinon no-op (le prochain affichage fetch).
|
||||
const reloadValidationMonth = async (dateYmd: string) => {
|
||||
const parsed = parseYmd(dateYmd)
|
||||
if (!parsed) return
|
||||
const key = monthKey(parsed.getFullYear(), parsed.getMonth())
|
||||
if (!validatedDaysByMonth.value[key]) return
|
||||
await loadValidationMonth(parsed.getMonth(), parsed.getFullYear(), { force: true })
|
||||
}
|
||||
|
||||
const refreshByDate = async () => {
|
||||
if (isAdmin.value) {
|
||||
await Promise.all([loadWorkHours(), loadWeeklySummary(), loadDayContext(), loadAbsences()])
|
||||
@@ -1131,6 +1183,7 @@ export const useHoursPage = () => {
|
||||
})
|
||||
|
||||
await refreshByDate()
|
||||
await reloadValidationMonth(selectedDate.value)
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
@@ -1221,6 +1274,8 @@ export const useHoursPage = () => {
|
||||
closeAbsenceDrawer,
|
||||
formatMinutes,
|
||||
handleSave,
|
||||
markedDates,
|
||||
onCalendarMonthChange,
|
||||
isWeekCommentDrawerOpen,
|
||||
weekCommentContext,
|
||||
openWeekCommentDrawer,
|
||||
|
||||
Reference in New Issue
Block a user