feat: ajout des commentaires à la semaine (#15)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [x] Pas de régression - [ ] TU/TI/TF rédigée - [x] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #15 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #15.
This commit is contained in:
@@ -33,8 +33,11 @@
|
||||
{{ row.firstName }} {{ row.lastName }}
|
||||
<span class="font-normal text-neutral-600">({{ row.contractName ?? '-' }})</span>
|
||||
</p>
|
||||
<p class="text-[11px] text-neutral-500 truncate">
|
||||
{{ row.siteName ?? 'Sans site' }}<span v-if="row.contractNature"> — {{ contractNatureLabel(row.contractNature) }}</span>
|
||||
<p class="text-[11px] text-neutral-500 truncate inline-flex items-center gap-2">
|
||||
<span>{{ row.siteName ?? 'Sans site' }}<span v-if="row.contractNature"> — {{ contractNatureLabel(row.contractNature) }}</span></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>
|
||||
</div>
|
||||
|
||||
@@ -118,9 +121,12 @@ const cellTitle = (daily: {
|
||||
|
||||
defineProps<{
|
||||
isWeekLoading: boolean
|
||||
isAdmin: boolean
|
||||
weekGridCols: string
|
||||
weeklySummary: WeeklyWorkHourSummary | null
|
||||
weekDayHeaders: Array<{ date: string; weekday: string; dayDate: string }>
|
||||
formatMinutes: (minutes: number) => string
|
||||
}>()
|
||||
|
||||
defineEmits<{ (e: 'open-comment', row: WeeklyWorkHourSummary['rows'][number]): void }>()
|
||||
</script>
|
||||
|
||||
@@ -94,8 +94,11 @@
|
||||
{{ row.firstName }} {{ row.lastName }}
|
||||
<span class="font-normal text-neutral-600">({{ row.contractName ?? '-' }})</span>
|
||||
</p>
|
||||
<p class="text-[11px] text-neutral-500 truncate">
|
||||
{{ row.siteName ?? 'Sans site' }}<span v-if="row.contractNature"> — {{ contractNatureLabel(row.contractNature) }}</span>
|
||||
<p class="text-[11px] text-neutral-500 truncate inline-flex items-center gap-2">
|
||||
<span>{{ row.siteName ?? 'Sans site' }}<span v-if="row.contractNature"> — {{ contractNatureLabel(row.contractNature) }}</span></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>
|
||||
</div>
|
||||
|
||||
@@ -179,9 +182,12 @@ const cellTitle = (daily: {
|
||||
|
||||
defineProps<{
|
||||
isWeekLoading: boolean
|
||||
isAdmin: boolean
|
||||
weekGridCols: string
|
||||
weeklySummary: WeeklyWorkHourSummary | null
|
||||
weekDayHeaders: Array<{ date: string; label: string }>
|
||||
formatMinutes: (minutes: number) => string
|
||||
}>()
|
||||
|
||||
defineEmits<{ (e: 'open-comment', row: WeeklyWorkHourSummary['rows'][number]): void }>()
|
||||
</script>
|
||||
|
||||
81
frontend/components/hours/WeekCommentDrawer.vue
Normal file
81
frontend/components/hours/WeekCommentDrawer.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<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,6 +926,15 @@ 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 {
|
||||
isAdmin,
|
||||
isSelfUser,
|
||||
@@ -993,6 +1002,10 @@ export const useDriverHoursPage = () => {
|
||||
deleteAbsenceFromDrawer,
|
||||
closeAbsenceDrawer,
|
||||
formatMinutes,
|
||||
handleSave
|
||||
handleSave,
|
||||
isWeekCommentDrawerOpen,
|
||||
weekCommentContext,
|
||||
openWeekCommentDrawer,
|
||||
reloadWeeklySummary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,6 +1112,15 @@ 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 {
|
||||
isAdmin,
|
||||
isSelfUser,
|
||||
@@ -1186,6 +1195,10 @@ export const useHoursPage = () => {
|
||||
deleteAbsenceFromDrawer,
|
||||
closeAbsenceDrawer,
|
||||
formatMinutes,
|
||||
handleSave
|
||||
handleSave,
|
||||
isWeekCommentDrawerOpen,
|
||||
weekCommentContext,
|
||||
openWeekCommentDrawer,
|
||||
reloadWeeklySummary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,16 @@ 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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
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.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
},
|
||||
"leaveRecap": {
|
||||
"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": {
|
||||
@@ -110,6 +114,10 @@
|
||||
"create": "Observation créée.",
|
||||
"update": "Observation mise à jour.",
|
||||
"delete": "Observation supprimée."
|
||||
},
|
||||
"weekComment": {
|
||||
"save": "Commentaire enregistré.",
|
||||
"delete": "Commentaire supprimé."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,11 +74,13 @@
|
||||
<DriverHoursWeekView
|
||||
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>
|
||||
|
||||
@@ -110,6 +112,17 @@
|
||||
@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>
|
||||
|
||||
@@ -179,7 +192,11 @@ const {
|
||||
formatMinutes,
|
||||
isSelectedDateHoliday,
|
||||
selectedHolidayLabel,
|
||||
handleSave
|
||||
handleSave,
|
||||
isWeekCommentDrawerOpen,
|
||||
weekCommentContext,
|
||||
openWeekCommentDrawer,
|
||||
reloadWeeklySummary
|
||||
} = useDriverHoursPage()
|
||||
|
||||
useHead({
|
||||
|
||||
@@ -81,11 +81,13 @@
|
||||
<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>
|
||||
|
||||
@@ -117,6 +119,17 @@
|
||||
@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>
|
||||
|
||||
@@ -193,7 +206,11 @@ const {
|
||||
deleteAbsenceFromDrawer,
|
||||
closeAbsenceDrawer,
|
||||
formatMinutes,
|
||||
handleSave
|
||||
handleSave,
|
||||
isWeekCommentDrawerOpen,
|
||||
weekCommentContext,
|
||||
openWeekCommentDrawer,
|
||||
reloadWeeklySummary
|
||||
} = useHoursPage()
|
||||
|
||||
useHead({
|
||||
|
||||
@@ -89,6 +89,8 @@ export type WeeklyWorkHourRowSummary = {
|
||||
weeklyOvernightCount?: number
|
||||
hasContractForWeek?: boolean
|
||||
contractNature?: 'CDI' | 'CDD' | 'INTERIM' | null
|
||||
comment?: string | null
|
||||
commentId?: number | null
|
||||
}
|
||||
|
||||
export type WeeklyWorkHourSummary = {
|
||||
|
||||
24
frontend/services/employee-week-comments.ts
Normal file
24
frontend/services/employee-week-comments.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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' })
|
||||
}
|
||||
Reference in New Issue
Block a user