feat : ajout d'un champ commentaire sur les contrats + correction de plusieurs bugs
This commit is contained in:
@@ -86,6 +86,19 @@
|
||||
<p v-if="showContractEndDateError" class="mt-1 text-sm text-red-600">La date de fin est obligatoire.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="contract-comment">
|
||||
Commentaire
|
||||
</label>
|
||||
<textarea
|
||||
id="contract-comment"
|
||||
v-model="contractForm.comment"
|
||||
rows="3"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||
placeholder="Motif de la clôture..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-neutral-200 bg-neutral-50 p-3">
|
||||
<label class="inline-flex items-center gap-2 text-md font-semibold text-neutral-700" for="contract-paid-leave-settled">
|
||||
<input
|
||||
@@ -191,6 +204,7 @@ type ContractForm = {
|
||||
startDate: string
|
||||
endDate: string
|
||||
paidLeaveSettled: boolean
|
||||
comment: string
|
||||
}
|
||||
|
||||
type CreateContractForm = {
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
>
|
||||
<div
|
||||
class="h-6 w-6"
|
||||
:class="getDayClass(day.leave)"
|
||||
:style="getDayStyle(day.leave)"
|
||||
:title="getDayTitle(day.leave)"
|
||||
:class="getDayClass(day)"
|
||||
:style="getDayStyle(day)"
|
||||
:title="getDayTitle(day)"
|
||||
>
|
||||
{{ getDayText(day) }}
|
||||
</div>
|
||||
@@ -65,13 +65,13 @@ type DayLeaveState = {
|
||||
am: boolean
|
||||
pm: boolean
|
||||
labels: string[]
|
||||
hasCongeTypeC: boolean
|
||||
hasOtherTypes: boolean
|
||||
colors: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
absences: Absence[]
|
||||
summary: EmployeeLeaveSummary | null
|
||||
publicHolidays: Record<string, string>
|
||||
}>()
|
||||
|
||||
const monthLabels = [
|
||||
@@ -124,8 +124,7 @@ const dayLeaveMap = computed(() => {
|
||||
am: false,
|
||||
pm: false,
|
||||
labels: [] as string[],
|
||||
hasCongeTypeC: false,
|
||||
hasOtherTypes: false
|
||||
colors: [] as string[]
|
||||
}
|
||||
|
||||
const isStart = ymd === startYmd
|
||||
@@ -150,18 +149,21 @@ const dayLeaveMap = computed(() => {
|
||||
}
|
||||
|
||||
const typeLabel = absence.type?.label ?? absence.type?.code ?? 'Absence'
|
||||
const typeCode = (absence.type?.code ?? '').toUpperCase()
|
||||
const typeColor = absence.type?.color ?? '#222783'
|
||||
const halfSuffix = am && !pm ? ' (Matin)' : (!am && pm ? ' (Apres-midi)' : '')
|
||||
const hoverLabel = `${typeLabel}${halfSuffix}`
|
||||
|
||||
const colors = existing.colors.includes(typeColor)
|
||||
? existing.colors
|
||||
: [...existing.colors, typeColor]
|
||||
|
||||
map.set(ymd, {
|
||||
am: existing.am || am,
|
||||
pm: existing.pm || pm,
|
||||
labels: existing.labels.includes(hoverLabel)
|
||||
? existing.labels
|
||||
: [...existing.labels, hoverLabel],
|
||||
hasCongeTypeC: existing.hasCongeTypeC || typeCode === 'C',
|
||||
hasOtherTypes: existing.hasOtherTypes || typeCode !== 'C'
|
||||
colors
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -180,7 +182,7 @@ const months = computed(() => {
|
||||
const daysInMonth = new Date(monthYear, monthIndex + 1, 0).getDate()
|
||||
const mondayBasedFirstDay = (first.getDay() + 6) % 7
|
||||
|
||||
const cells: Array<{ ymd: string; label: string; leave: DayLeaveState | null } | null> = []
|
||||
const cells: Array<{ ymd: string; label: string; leave: DayLeaveState | null; isHoliday: boolean } | null> = []
|
||||
|
||||
for (let i = 0; i < mondayBasedFirstDay; i += 1) {
|
||||
cells.push(null)
|
||||
@@ -191,7 +193,8 @@ const months = computed(() => {
|
||||
cells.push({
|
||||
ymd,
|
||||
label: String(day),
|
||||
leave: dayLeaveMap.value.get(ymd) ?? null
|
||||
leave: dayLeaveMap.value.get(ymd) ?? null,
|
||||
isHoliday: ymd in props.publicHolidays
|
||||
})
|
||||
}
|
||||
|
||||
@@ -206,37 +209,37 @@ const months = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const getDayClass = (leave: DayLeaveState | null) => {
|
||||
if (!leave) return 'text-primary-500'
|
||||
if (leave.am && leave.pm) {
|
||||
return leave.hasOtherTypes
|
||||
? 'bg-red-600 text-white rounded font-semibold'
|
||||
: 'bg-primary-500 text-white rounded font-semibold'
|
||||
const getDayClass = (day: { leave: DayLeaveState | null; isHoliday: boolean }) => {
|
||||
if (day.leave) {
|
||||
return 'rounded font-semibold text-white'
|
||||
}
|
||||
return 'rounded text-primary-700 font-semibold text-white'
|
||||
if (day.isHoliday) return 'text-primary-500 rounded font-semibold'
|
||||
return 'text-primary-500'
|
||||
}
|
||||
|
||||
const getDayStyle = (leave: DayLeaveState | null) => {
|
||||
if (!leave || (leave.am && leave.pm)) return undefined
|
||||
|
||||
const color = leave.hasOtherTypes ? '#dc2626' : '#222783'
|
||||
const backgroundImage = leave.am
|
||||
? `linear-gradient(135deg, ${color} 0 50%, transparent 50% 100%)`
|
||||
: `linear-gradient(135deg, transparent 0 50%, ${color} 50% 100%)`
|
||||
|
||||
return {
|
||||
backgroundImage,
|
||||
backgroundColor: 'transparent'
|
||||
const getDayStyle = (day: { leave: DayLeaveState | null; isHoliday: boolean }) => {
|
||||
if (day.leave) {
|
||||
const color = day.leave.colors[0] ?? '#222783'
|
||||
if (day.leave.am && day.leave.pm) {
|
||||
return { backgroundColor: color }
|
||||
}
|
||||
const backgroundImage = day.leave.am
|
||||
? `linear-gradient(135deg, ${color} 0 50%, transparent 50% 100%)`
|
||||
: `linear-gradient(135deg, transparent 0 50%, ${color} 50% 100%)`
|
||||
return { backgroundImage, backgroundColor: 'transparent' }
|
||||
}
|
||||
if (day.isHoliday) return { backgroundColor: 'rgb(179, 229, 252)' }
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getDayText = (day: { label: string; leave: DayLeaveState | null }) => {
|
||||
return day.label
|
||||
}
|
||||
|
||||
const getDayTitle = (leave: DayLeaveState | null) => {
|
||||
if (!leave || leave.labels.length === 0) return ''
|
||||
return leave.labels.join(' / ')
|
||||
const getDayTitle = (day: { leave: DayLeaveState | null; isHoliday: boolean; ymd: string }) => {
|
||||
if (day.leave && day.leave.labels.length > 0) return day.leave.labels.join(' / ')
|
||||
if (day.isHoliday) return props.publicHolidays[day.ymd] ?? 'Jour férié'
|
||||
return ''
|
||||
}
|
||||
|
||||
const formatCount = (value: number | null | undefined) => {
|
||||
|
||||
@@ -63,6 +63,9 @@
|
||||
<Icon name="mdi:check"/>
|
||||
</span>
|
||||
</p>
|
||||
<p v-if="isAdmin && getRowUpdatedAt(employee.id)" class="text-neutral-400 text-xs truncate">
|
||||
Modifié le {{ getRowUpdatedAt(employee.id) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="pl-2 min-w-0 self-stretch flex flex-col gap-1 justify-between py-0.5">
|
||||
<p
|
||||
@@ -216,6 +219,7 @@ const props = defineProps<{
|
||||
getRowMetrics: (employeeId: number) => { dayMinutes: number; nightMinutes: number; totalMinutes: number }
|
||||
getRowAbsenceLabel: (employeeId: number) => string
|
||||
getRowAbsenceStyle: (employeeId: number) => { backgroundColor: string } | undefined
|
||||
getRowUpdatedAt: (employeeId: number) => string
|
||||
getPresenceDayValue: (employeeId: number) => string
|
||||
onAbsenceClick: (employeeId: number) => void
|
||||
formatMinutes: (minutes: number) => string
|
||||
|
||||
@@ -10,4 +10,5 @@ export type HourRow = {
|
||||
isPresentAfternoon: boolean
|
||||
isSiteValid: boolean
|
||||
isValid: boolean
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
@@ -167,8 +167,9 @@ const closeMenu = () => {
|
||||
|
||||
const commitInput = () => {
|
||||
const normalized = normalizeTypedTime(inputValue.value)
|
||||
if (normalized === null) {
|
||||
inputValue.value = props.modelValue
|
||||
if (normalized === null || (normalized !== '' && !timeSlots.value.includes(normalized))) {
|
||||
emit('update:modelValue', '')
|
||||
inputValue.value = ''
|
||||
closeMenu()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { listContracts } from '~/services/contracts'
|
||||
import { getEmployeeLeaveSummary } from '~/services/employee-leave-summary'
|
||||
import { getEmployeeRttSummary } from '~/services/employee-rtt-summary'
|
||||
import { getEmployee, updateEmployee } from '~/services/employees'
|
||||
import { listPublicHolidays } from '~/services/public-holidays'
|
||||
import { formatNullableYmdToFr, getTodayYmd, shiftYmd } from '~/utils/date'
|
||||
import { contractNatureLabel, isContractNature, requiresContractEndDate } from '~/utils/contract'
|
||||
|
||||
@@ -22,6 +23,7 @@ export const useEmployeeDetailPage = () => {
|
||||
const employeeAbsences = ref<Absence[]>([])
|
||||
const leaveSummary = ref<EmployeeLeaveSummary | null>(null)
|
||||
const rttSummary = ref<EmployeeRttSummary | null>(null)
|
||||
const publicHolidays = ref<Record<string, string>>({})
|
||||
const isContractDrawerOpen = ref(false)
|
||||
const isContractSubmitting = ref(false)
|
||||
const isCreateContractDrawerOpen = ref(false)
|
||||
@@ -34,7 +36,8 @@ export const useEmployeeDetailPage = () => {
|
||||
contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
paidLeaveSettled: false
|
||||
paidLeaveSettled: false,
|
||||
comment: ''
|
||||
})
|
||||
|
||||
const validationTouched = reactive({
|
||||
@@ -138,6 +141,7 @@ export const useEmployeeDetailPage = () => {
|
||||
contractForm.startDate = active.startDate
|
||||
contractForm.endDate = getTodayYmd()
|
||||
contractForm.paidLeaveSettled = false
|
||||
contractForm.comment = ''
|
||||
}
|
||||
|
||||
const openCloseContractDrawer = () => {
|
||||
@@ -198,7 +202,10 @@ export const useEmployeeDetailPage = () => {
|
||||
const to = isForfait
|
||||
? `${leaveYear}-12-31`
|
||||
: `${leaveYear}-05-31`
|
||||
const [absences, summary, rtt] = await Promise.all([
|
||||
const holidayYears = isForfait
|
||||
? [leaveYear]
|
||||
: [leaveYear - 1, leaveYear]
|
||||
const [absences, summary, rtt, ...holidayResults] = await Promise.all([
|
||||
listAbsences({
|
||||
from,
|
||||
to,
|
||||
@@ -207,11 +214,13 @@ export const useEmployeeDetailPage = () => {
|
||||
showLeaveTab.value
|
||||
? getEmployeeLeaveSummary(loadedEmployee.id, leaveYear)
|
||||
: Promise.resolve(null),
|
||||
getEmployeeRttSummary(loadedEmployee.id, rttYear)
|
||||
getEmployeeRttSummary(loadedEmployee.id, rttYear),
|
||||
...holidayYears.map((y) => listPublicHolidays('metropole', y))
|
||||
])
|
||||
employeeAbsences.value = absences
|
||||
leaveSummary.value = summary
|
||||
rttSummary.value = rtt
|
||||
publicHolidays.value = Object.assign({}, ...holidayResults)
|
||||
if (!showLeaveTab.value && activeTab.value === 'leave') {
|
||||
activeTab.value = 'contract'
|
||||
}
|
||||
@@ -242,7 +251,8 @@ export const useEmployeeDetailPage = () => {
|
||||
siteId: employee.value.site?.id ?? null,
|
||||
contractId: Number(contractForm.contractId),
|
||||
contractEndDate: contractForm.endDate || null,
|
||||
contractPaidLeaveSettled: contractForm.paidLeaveSettled
|
||||
contractPaidLeaveSettled: contractForm.paidLeaveSettled,
|
||||
contractComment: contractForm.comment || null
|
||||
})
|
||||
|
||||
isContractDrawerOpen.value = false
|
||||
@@ -309,6 +319,7 @@ export const useEmployeeDetailPage = () => {
|
||||
employeeAbsences,
|
||||
leaveSummary,
|
||||
rttSummary,
|
||||
publicHolidays,
|
||||
showLeaveTab,
|
||||
contractHistory,
|
||||
employeeContractWorkLabel,
|
||||
|
||||
@@ -341,7 +341,8 @@ export const useHoursPage = () => {
|
||||
isPresentMorning: false,
|
||||
isPresentAfternoon: false,
|
||||
isSiteValid: false,
|
||||
isValid: false
|
||||
isValid: false,
|
||||
updatedAt: null
|
||||
})
|
||||
|
||||
const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === TRACKING_MODES.PRESENCE
|
||||
@@ -463,6 +464,14 @@ export const useHoursPage = () => {
|
||||
return { backgroundColor: dayRow.absenceColor || '#dc2626' }
|
||||
}
|
||||
|
||||
const getRowUpdatedAt = (employeeId: number): string => {
|
||||
const raw = rows.value[employeeId]?.updatedAt
|
||||
if (!raw) return ''
|
||||
const date = new Date(raw)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
return date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
const getPresenceDayValue = (employeeId: number) => {
|
||||
const row = rows.value[employeeId]
|
||||
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
||||
@@ -521,7 +530,8 @@ export const useHoursPage = () => {
|
||||
isPresentMorning: workHour?.isPresentMorning ?? false,
|
||||
isPresentAfternoon: workHour?.isPresentAfternoon ?? false,
|
||||
isSiteValid: workHour?.isSiteValid ?? false,
|
||||
isValid: workHour?.isValid ?? false
|
||||
isValid: workHour?.isValid ?? false,
|
||||
updatedAt: workHour?.updatedAt ?? null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1140,6 +1150,7 @@ export const useHoursPage = () => {
|
||||
getRowMetrics,
|
||||
getRowAbsenceLabel,
|
||||
getRowAbsenceStyle,
|
||||
getRowUpdatedAt,
|
||||
getPresenceDayValue,
|
||||
openAbsenceDrawer,
|
||||
submitAbsence,
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
class="h-full"
|
||||
:absences="employeeAbsences"
|
||||
:summary="leaveSummary"
|
||||
:public-holidays="publicHolidays"
|
||||
/>
|
||||
<EmployeesRttTab v-else class="h-full" :summary="rttSummary" />
|
||||
</div>
|
||||
@@ -109,6 +110,7 @@ const {
|
||||
employeeAbsences,
|
||||
leaveSummary,
|
||||
rttSummary,
|
||||
publicHolidays,
|
||||
showLeaveTab,
|
||||
contractHistory,
|
||||
employeeContractWorkLabel,
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
:get-row-metrics="getRowMetrics"
|
||||
:get-row-absence-label="getRowAbsenceLabel"
|
||||
:get-row-absence-style="getRowAbsenceStyle"
|
||||
:get-row-updated-at="getRowUpdatedAt"
|
||||
:get-presence-day-value="getPresenceDayValue"
|
||||
:on-absence-click="openAbsenceDrawer"
|
||||
:format-minutes="formatMinutes"
|
||||
@@ -173,6 +174,7 @@ const {
|
||||
getRowMetrics,
|
||||
getRowAbsenceLabel,
|
||||
getRowAbsenceStyle,
|
||||
getRowUpdatedAt,
|
||||
getPresenceDayValue,
|
||||
openAbsenceDrawer,
|
||||
submitAbsence,
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ContractHistoryItem = {
|
||||
contractNature: 'CDI' | 'CDD' | 'INTERIM'
|
||||
startDate: string
|
||||
endDate?: string | null
|
||||
comment?: string | null
|
||||
}
|
||||
|
||||
export type Employee = {
|
||||
|
||||
@@ -15,6 +15,7 @@ export type WorkHour = {
|
||||
isPresentAfternoon?: boolean
|
||||
isSiteValid?: boolean
|
||||
isValid?: boolean
|
||||
updatedAt?: string | null
|
||||
}
|
||||
|
||||
export type WorkHourEntryPayload = {
|
||||
|
||||
@@ -61,6 +61,7 @@ export const updateEmployee = async (
|
||||
contractStartDate?: string
|
||||
contractEndDate?: string | null
|
||||
contractPaidLeaveSettled?: boolean
|
||||
contractComment?: string | null
|
||||
displayOrder?: number
|
||||
}
|
||||
) => {
|
||||
@@ -87,6 +88,9 @@ export const updateEmployee = async (
|
||||
if (payload.contractPaidLeaveSettled !== undefined) {
|
||||
body.contractPaidLeaveSettled = payload.contractPaidLeaveSettled
|
||||
}
|
||||
if (payload.contractComment !== undefined) {
|
||||
body.contractComment = payload.contractComment ?? null
|
||||
}
|
||||
|
||||
return api.patch<Employee>(`/employees/${id}`, body, {
|
||||
toastSuccessKey: 'success.employee.update',
|
||||
|
||||
Reference in New Issue
Block a user