Files
SIRH/frontend/pages/hours.vue
T
tristan 143278a368
Auto Tag Develop / tag (push) Successful in 10s
feat(heures) : export PDF jour accessible aux chefs de site (périmètre par site)
L'export des heures de la vue Jour était réservé aux admins. Il est désormais
ouvert aux chefs de site, restreint à leurs sites :
- sécurité endpoint ROLE_ADMIN -> ROLE_USER
- périmètre résolu côté backend via EmployeeRepository::findScoped() (un siteIds
  hors périmètre est ignoré, aucune fuite inter-sites)
- bouton Exporter visible pour admin + chef de site (masqué pour ROLE_SELF)
- doc, doc in-app et CLAUDE.md mis à jour

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 08:31:30 +02:00

251 lines
8.7 KiB
Vue

<template>
<div class="h-full overflow-hidden flex flex-col">
<div class="flex flex-wrap items-center justify-between gap-4">
<h1 class="text-2xl font-bold text-primary-500 lg:text-4xl">Heures</h1>
<MalioButton
v-if="(isAdmin || isSiteManager) && viewMode === 'day'"
label="Export"
variant="secondary"
icon-name="mdi:download"
icon-position="left"
@click="isExportDrawerOpen = true"
/>
</div>
<HoursDayExportDrawer
v-model="isExportDrawerOpen"
:sites="sites"
:initial-date="selectedDate"
:is-loading="isExporting"
@submit="handleExport"
/>
<HoursToolbar
v-model:selected-date="selectedDate"
v-model:view-mode="viewMode"
v-model:selected-site-ids="selectedSiteIds"
v-model:employee-filter="employeeFilter"
:is-admin="isAdmin"
:sites="sites"
:absence-types="absenceTypes"
:formatted-selected-date="formattedSelectedDate"
:shortcut-button-class="shortcutButtonClass"
:week-shortcut-button-class="weekShortcutButtonClass"
:get-week-shortcut-label="getWeekShortcutLabel"
@set-yesterday="setYesterday"
@set-today="setToday"
@set-tomorrow="setTomorrow"
@set-previous-week="setPreviousWeek"
@set-this-week="setThisWeek"
@set-next-week="setNextWeek"
@shift-date="shiftDate"
/>
<div v-if="isLoading" class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
Chargement...
</div>
<div v-else-if="employees.length === 0" class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
Aucun employé accessible.
</div>
<div v-else class="flex min-h-0 flex-col gap-4">
<div class="min-h-0 flex flex-col max-h-[calc(100vh-300px)]">
<HoursDayView
v-if="viewMode === 'day'"
v-model:rows="rows"
:employees="displayedEmployees"
:is-admin="isAdmin"
:is-site-manager="isSiteManager"
:day-grid-cols="dayGridCols"
:is-holiday="isSelectedDateHoliday"
:holiday-label="selectedHolidayLabel"
:contract-label="contractLabel"
:is-time-tracking="isTimeTracking"
:is-presence-tracking="isPresenceTracking"
:is-row-locked="isRowLocked"
:is-half-locked-by-absence="isHalfLockedByAbsence"
:is-evening-locked-by-absence="isEveningLockedByAbsence"
:has-contract-at-selected-date="hasContractAtSelectedDate"
:is-validation-pending="isValidationPending"
:is-site-validation-pending="isSiteValidationPending"
:can-toggle-validation="canToggleValidation"
:can-toggle-site-validation="canToggleSiteValidation"
:can-create-site-validation-row-from-absence="canCreateSiteValidationRowFromAbsence"
:is-bulk-validation-checked="isBulkValidationChecked"
:is-bulk-validation-indeterminate="isBulkValidationIndeterminate"
:is-bulk-site-validation-checked="isBulkSiteValidationChecked"
:is-bulk-site-validation-indeterminate="isBulkSiteValidationIndeterminate"
:can-bulk-toggle-site-validation="canBulkToggleSiteValidation"
:on-toggle-validation="toggleValidation"
:on-toggle-site-validation="toggleSiteValidation"
:on-toggle-validation-bulk="toggleValidationBulk"
:on-toggle-site-validation-bulk="toggleSiteValidationBulk"
:get-row-metrics="getRowMetrics"
:get-row-absence-label="getRowAbsenceLabel"
:get-row-absence-style="getRowAbsenceStyle"
:has-row-formation="hasRowFormation"
:get-row-formation-label="getRowFormationLabel"
:get-row-contract-nature="getRowContractNature"
:get-row-updated-at="getRowUpdatedAt"
:get-presence-day-value="getPresenceDayValue"
:on-absence-click="openAbsenceDrawer"
:format-minutes="formatMinutes"
class="max-h-[calc(100vh-300px)]"
/>
<HoursWeekView
v-else-if="isAdmin && viewMode === 'week'"
:is-week-loading="isWeekLoading"
:is-admin="isAdmin"
:week-grid-cols="weekGridCols"
:weekly-summary="filteredWeeklySummary"
:week-day-headers="weekDayHeaders"
:format-minutes="formatMinutes"
class="max-h-[calc(100vh-300px)]"
@open-comment="openWeekCommentDrawer"
/>
</div>
<div v-if="viewMode === 'day'" class="shrink-0 px-4 pt-4 flex justify-center">
<button
type="button"
class="rounded-lg bg-primary-500 px-6 py-2 text-md font-semibold text-white hover:bg-secondary-500"
:class="saveButtonClass"
:disabled="isSubmitting || visibleEmployees.length === 0"
@click="handleSave"
>
Enregistrer
</button>
</div>
</div>
<AbsenceFormDrawer
v-model="isAbsenceDrawerOpen"
:employees="employees"
:absence-types="absenceTypes"
:form="absenceForm"
:editing-absence="editingAbsence"
:is-submitting="isAbsenceSubmitting"
:lock-employee="true"
:lock-dates="true"
:show-comment="false"
@submit="submitAbsence"
@delete="deleteAbsenceFromDrawer"
@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>
</template>
<script setup lang="ts">
const {
isAdmin,
isSiteManager,
viewMode,
selectedDate,
employeeFilter,
sites,
selectedSiteIds,
employees,
visibleEmployees,
displayedEmployees,
rows,
absenceTypes,
absenceForm,
isAbsenceDrawerOpen,
isAbsenceSubmitting,
editingAbsence,
filteredWeeklySummary,
isLoading,
isWeekLoading,
isSubmitting,
dayGridCols,
isSelectedDateHoliday,
selectedHolidayLabel,
weekGridCols,
saveButtonClass,
formattedSelectedDate,
weekDayHeaders,
shortcutButtonClass,
weekShortcutButtonClass,
getWeekShortcutLabel,
setToday,
setYesterday,
setTomorrow,
setThisWeek,
setPreviousWeek,
setNextWeek,
shiftDate,
contractLabel,
isTimeTracking,
isPresenceTracking,
isRowLocked,
isHalfLockedByAbsence,
isEveningLockedByAbsence,
hasContractAtSelectedDate,
isValidationPending,
isSiteValidationPending,
canToggleValidation,
canToggleSiteValidation,
canCreateSiteValidationRowFromAbsence,
isBulkValidationChecked,
isBulkValidationIndeterminate,
isBulkSiteValidationChecked,
isBulkSiteValidationIndeterminate,
canBulkToggleSiteValidation,
toggleValidation,
toggleSiteValidation,
toggleValidationBulk,
toggleSiteValidationBulk,
getRowMetrics,
getRowAbsenceLabel,
getRowAbsenceStyle,
hasRowFormation,
getRowFormationLabel,
getRowContractNature,
getRowUpdatedAt,
getPresenceDayValue,
openAbsenceDrawer,
submitAbsence,
deleteAbsenceFromDrawer,
closeAbsenceDrawer,
formatMinutes,
handleSave,
isWeekCommentDrawerOpen,
weekCommentContext,
openWeekCommentDrawer,
reloadWeeklySummary
} = useHoursPage()
const { printPdf } = usePdfPrinter()
const isExportDrawerOpen = ref(false)
const isExporting = ref(false)
const handleExport = async (payload: { date: string; siteIds: number[] }) => {
isExporting.value = true
try {
const siteIdsParam = payload.siteIds.join(',')
await printPdf(`/work-hours/day-export?workDate=${payload.date}&siteIds=${siteIdsParam}`)
isExportDrawerOpen.value = false
} finally {
isExporting.value = false
}
}
useHead({
title: 'Heures'
})
</script>