Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c317a2a026 | ||
| 8846e83df1 | |||
|
|
ff824f233a | ||
| c4c9dfceab |
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.32'
|
app.version: '0.1.34'
|
||||||
|
|||||||
@@ -1,35 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="flex h-full min-h-0 flex-col overflow-hidden pt-8">
|
<section class="flex h-full min-h-0 flex-col overflow-hidden pt-8">
|
||||||
<div class="grid grid-cols-4 rounded-md bg-primary-500 text-white text-[20]">
|
<div class="grid grid-cols-4 rounded-md bg-tertiary-500 text-primary-500 text-[18px] border border-primary-500">
|
||||||
<div class="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3">
|
<p class="col-start-1 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">Année acquis :</strong> {{
|
||||||
<p><strong class="uppercase font-semibold">Année acquis :</strong> {{
|
formatCount(summary?.acquiredDays)
|
||||||
formatCount(summary?.acquiredDays)
|
}} Jours
|
||||||
}} Jours</p>
|
</p>
|
||||||
<p><strong class="uppercase font-semibold">Reste à prendre :</strong>
|
<p class="col-start-2 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">Pris :</strong>
|
||||||
{{ formatCount(summary?.remainingDays) }} Jours</p>
|
{{ formatCount(isForfaitRule ? currentYearTakenDays : summary?.takenDays) }} Jours
|
||||||
</div>
|
</p>
|
||||||
<div class="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3">
|
<p class="col-start-3 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">Reste à prendre :</strong>
|
||||||
<p><span class="uppercase font-semibold">Samedi acquis :</span>
|
{{ formatCount(summary?.remainingDays) }} Jours
|
||||||
{{ formatCount(summary?.acquiredSaturdays) }} Jours</p>
|
</p>
|
||||||
<p><span class="uppercase font-semibold">Reste à prendre :</span>
|
<p class="col-start-4 p-[10px] border-b border-b-black"><strong class="uppercase font-semibold">En cours d'acquisition :</strong>
|
||||||
{{ formatCount(summary?.remainingSaturdays) }} Jours</p>
|
{{ formatCount(summary?.accruingDays) }} Jours
|
||||||
</div>
|
</p>
|
||||||
<div class="flex flex-col gap-2 jutify-center items-center border-r-4 border-white py-3">
|
<p v-if="!isForfaitRule" class="col-start-1 p-[10px]"><span class="uppercase font-semibold">Samedi acquis :</span>
|
||||||
<p><span class="uppercase font-semibold">Fractionné acquis : </span>{{ formatCount(summary?.fractionedDays) }} Jours</p>
|
{{ formatCount(summary?.acquiredSaturdays) }} Jours
|
||||||
|
</p>
|
||||||
|
<p v-else class="col-start-1 p-[10px]"><span class="uppercase font-semibold">Année N-1 acquis :</span>
|
||||||
|
{{ formatCount(summary?.previousYearAcquiredDays) }} Jours
|
||||||
|
</p>
|
||||||
|
<p v-if="!isForfaitRule" class="col-start-2 p-[10px]"><span class="uppercase font-semibold">Pris :</span>
|
||||||
|
{{ formatCount(summary?.takenSaturdays) }} Jours
|
||||||
|
</p>
|
||||||
|
<p v-if="!isForfaitRule" class="col-start-3 p-[10px]"><span class="uppercase font-semibold">Reste à prendre :</span>
|
||||||
|
{{ formatCount(summary?.remainingSaturdays) }} Jours
|
||||||
|
</p>
|
||||||
|
<p v-else class="col-start-2 p-[10px]"><span class="uppercase font-semibold">Pris :</span>
|
||||||
|
{{ formatCount(summary?.previousYearTakenDays) }} Jours
|
||||||
|
</p>
|
||||||
|
<p v-if="isForfaitRule" class="col-start-3 p-[10px]"><span class="uppercase font-semibold">Reste à prendre :</span>
|
||||||
|
{{ formatCount(summary?.previousYearRemainingDays) }} Jours
|
||||||
|
</p>
|
||||||
|
<div v-if="!isForfaitRule" class="col-start-4 p-[10px] flex gap-7 items-center">
|
||||||
|
<div>
|
||||||
|
<span class="uppercase font-semibold">Fractionné acquis : </span>
|
||||||
|
<span>{{ formatCount(summary?.fractionedDays) }} Jours</span>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
class="flex justify-center items-center gap-2 bg-white text-primary-500 font-bold w-[150px] rounded-md py-[1px]"
|
class="flex items-center"
|
||||||
@click="openFractionedDrawer"
|
@click="openFractionedDrawer"
|
||||||
>
|
>
|
||||||
{{ summary?.fractionedDays === 0 ? '+ Ajouter' : 'Modifier' }}</button>
|
<Icon name="mdi:edit-box" size="24"/>
|
||||||
</div>
|
</button>
|
||||||
<div class="flex flex-col jutify-center gap-2 items-center py-3">
|
|
||||||
<p><span class="uppercase font-semibold">En cours d'acquisition :</span></p>
|
|
||||||
<p>{{ formatCount(summary?.accruingDays) }} Jours</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-8 min-h-0 flex-1 overflow-y-auto pr-2">
|
<div class="mt-8 min-h-0 flex-1 overflow-y-auto pr-2">
|
||||||
<div class="grid grid-cols-4 gap-10">
|
<div class="grid grid-cols-4 gap-10">
|
||||||
<div v-for="month in months" :key="month.label" class="rounded-md bg-tertiary-500 text-primary-500 flex flex-col justify-between">
|
<div v-for="month in months" :key="month.label"
|
||||||
|
class="rounded-md bg-tertiary-500 text-primary-500 flex flex-col justify-between">
|
||||||
<div class="flex justify-center rounded-t-md bg-primary-500 py-1 font-bold uppercase text-white">
|
<div class="flex justify-center rounded-t-md bg-primary-500 py-1 font-bold uppercase text-white">
|
||||||
{{ month.label }}
|
{{ month.label }}
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +73,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-2 py-2 text-center border-t border-primary-500">Jours de présence : {{ summary?.presenceDaysByMonth?.[month.monthKey] ?? 0 }}</div>
|
<div class="px-2 py-2 text-center border-t border-primary-500">Jours de présence :
|
||||||
|
{{ summary?.presenceDaysByMonth?.[month.monthKey] ?? 0 }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,7 +139,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isFractionedDrawerOpen = ref(false)
|
const isFractionedDrawerOpen = ref(false)
|
||||||
const fractionedForm = reactive({ days: 0 })
|
const fractionedForm = reactive({days: 0})
|
||||||
|
|
||||||
const openFractionedDrawer = () => {
|
const openFractionedDrawer = () => {
|
||||||
fractionedForm.days = props.summary?.fractionedDays ?? 0
|
fractionedForm.days = props.summary?.fractionedDays ?? 0
|
||||||
@@ -151,6 +172,11 @@ const weekDayLabels = ['L', 'M', 'M', 'J', 'V', 'S', 'D'] as const
|
|||||||
|
|
||||||
const isForfaitRule = computed(() => props.summary?.ruleCode === 'FORFAIT_218')
|
const isForfaitRule = computed(() => props.summary?.ruleCode === 'FORFAIT_218')
|
||||||
|
|
||||||
|
const currentYearTakenDays = computed(() => {
|
||||||
|
if (!props.summary) return null
|
||||||
|
return props.summary.takenDays - (props.summary.previousYearTakenDays ?? 0)
|
||||||
|
})
|
||||||
|
|
||||||
const displayedYear = computed(() => {
|
const displayedYear = computed(() => {
|
||||||
if (props.summary?.year) return props.summary.year
|
if (props.summary?.year) return props.summary.year
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
@@ -282,15 +308,15 @@ const getDayStyle = (day: { leave: DayLeaveState | null; isHoliday: boolean }) =
|
|||||||
if (day.leave) {
|
if (day.leave) {
|
||||||
const color = day.leave.colors[0] ?? '#222783'
|
const color = day.leave.colors[0] ?? '#222783'
|
||||||
if (day.leave.am && day.leave.pm) {
|
if (day.leave.am && day.leave.pm) {
|
||||||
return { backgroundColor: color }
|
return {backgroundColor: color}
|
||||||
}
|
}
|
||||||
const colorFaded = `${color}60`
|
const colorFaded = `${color}60`
|
||||||
const backgroundImage = day.leave.am
|
const backgroundImage = day.leave.am
|
||||||
? `linear-gradient(180deg, ${color} 0 50%, ${colorFaded} 50% 100%)`
|
? `linear-gradient(180deg, ${color} 0 50%, ${colorFaded} 50% 100%)`
|
||||||
: `linear-gradient(180deg, ${colorFaded} 0 50%, ${color} 50% 100%)`
|
: `linear-gradient(180deg, ${colorFaded} 0 50%, ${color} 50% 100%)`
|
||||||
return { backgroundImage, backgroundColor: 'transparent' }
|
return {backgroundImage, backgroundColor: 'transparent'}
|
||||||
}
|
}
|
||||||
if (day.isHoliday) return { backgroundColor: 'rgb(179, 229, 252)' }
|
if (day.isHoliday) return {backgroundColor: 'rgb(179, 229, 252)'}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
344
frontend/composables/useEmployeeContract.ts
Normal file
344
frontend/composables/useEmployeeContract.ts
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { Contract } from '~/services/dto/contract'
|
||||||
|
import type { ContractHistoryItem, Employee } from '~/services/dto/employee'
|
||||||
|
import { listContracts } from '~/services/contracts'
|
||||||
|
import { updateEmployee } from '~/services/employees'
|
||||||
|
import { createSuspension, updateSuspension } from '~/services/contractSuspensions'
|
||||||
|
import { formatNullableYmdToFr, getTodayYmd, shiftYmd } from '~/utils/date'
|
||||||
|
import { contractNatureLabel, isContractNature, requiresContractEndDate, showsContractEndDate } from '~/utils/contract'
|
||||||
|
|
||||||
|
type SuspensionForm = {
|
||||||
|
id: number | null
|
||||||
|
startDate: string
|
||||||
|
endDate: string
|
||||||
|
comment: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEmployeeContract = (employee: Ref<Employee | null>, reloadEmployee: () => Promise<void>) => {
|
||||||
|
const toast = useToast()
|
||||||
|
const contracts = ref<Contract[]>([])
|
||||||
|
const isContractDrawerOpen = ref(false)
|
||||||
|
const isContractSubmitting = ref(false)
|
||||||
|
const isCreateContractDrawerOpen = ref(false)
|
||||||
|
const isCreateContractSubmitting = ref(false)
|
||||||
|
const suspensionForms = ref<SuspensionForm[]>([])
|
||||||
|
const isSuspensionSubmitting = ref(false)
|
||||||
|
|
||||||
|
const contractForm = reactive({
|
||||||
|
contractId: '' as number | '',
|
||||||
|
contractName: '',
|
||||||
|
weeklyHours: null as number | null,
|
||||||
|
contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
paidLeaveSettled: false,
|
||||||
|
comment: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const validationTouched = reactive({
|
||||||
|
endDate: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const createContractForm = reactive({
|
||||||
|
contractId: '' as number | '',
|
||||||
|
contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM',
|
||||||
|
startDate: '',
|
||||||
|
endDate: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const createValidationTouched = reactive({
|
||||||
|
contractId: false,
|
||||||
|
contractNature: false,
|
||||||
|
startDate: false,
|
||||||
|
endDate: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const contractHistory = computed(() => employee.value?.contractHistory ?? [])
|
||||||
|
|
||||||
|
const formatDate = (value?: string | null) => formatNullableYmdToFr(value)
|
||||||
|
|
||||||
|
const contractHistoryLabel = (item: ContractHistoryItem) => {
|
||||||
|
if (item.weeklyHours !== null && item.weeklyHours !== undefined) {
|
||||||
|
return `${item.weeklyHours} heures`
|
||||||
|
}
|
||||||
|
return item.contractName ?? '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentActiveContractPeriod = computed(() => {
|
||||||
|
const today = getTodayYmd()
|
||||||
|
const history = employee.value?.contractHistory ?? []
|
||||||
|
return history.find((item) => item.startDate <= today && (!item.endDate || item.endDate >= today)) ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentActiveContractPeriodId = computed<number | null>(() => {
|
||||||
|
const period = currentActiveContractPeriod.value
|
||||||
|
return period?.periodId ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
const canCloseCurrentContract = computed(() => {
|
||||||
|
const active = currentActiveContractPeriod.value
|
||||||
|
if (!active) return false
|
||||||
|
if (!active.endDate) return true
|
||||||
|
return active.endDate > getTodayYmd()
|
||||||
|
})
|
||||||
|
|
||||||
|
const canCreateContract = computed(() => {
|
||||||
|
const active = currentActiveContractPeriod.value
|
||||||
|
if (!active) return true
|
||||||
|
return !!active.endDate
|
||||||
|
})
|
||||||
|
|
||||||
|
const isContractEndDateValid = computed(() => contractForm.endDate !== '')
|
||||||
|
const showContractEndDateError = computed(() => validationTouched.endDate && !isContractEndDateValid.value)
|
||||||
|
|
||||||
|
const showsCreateContractEndDate = computed(() => showsContractEndDate(createContractForm.contractNature))
|
||||||
|
const requiresCreateContractEndDate = computed(() => requiresContractEndDate(createContractForm.contractNature))
|
||||||
|
const isCreateContractValid = computed(() => createContractForm.contractId !== '')
|
||||||
|
const isCreateContractNatureValid = computed(() => isContractNature(createContractForm.contractNature))
|
||||||
|
const isCreateContractStartDateValid = computed(() => createContractForm.startDate !== '')
|
||||||
|
const isCreateContractEndDateValid = computed(() => !requiresCreateContractEndDate.value || createContractForm.endDate !== '')
|
||||||
|
const isCreateContractFormValid = computed(() =>
|
||||||
|
isCreateContractValid.value &&
|
||||||
|
isCreateContractNatureValid.value &&
|
||||||
|
isCreateContractStartDateValid.value &&
|
||||||
|
isCreateContractEndDateValid.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 readonlyFieldClass = `${baseInputClass} border-neutral-300 bg-neutral-100 text-neutral-700`
|
||||||
|
const contractEndDateFieldClass = computed(() => showContractEndDateError.value ? `${baseInputClass} border-red-500` : `${baseInputClass} border-neutral-300`)
|
||||||
|
const baseSelectClass = 'mt-2 w-full rounded-md border bg-white px-3 py-2 text-md text-neutral-900'
|
||||||
|
const createContractFieldClass = computed(() => createValidationTouched.contractId && !isCreateContractValid.value ? `${baseSelectClass} border-red-500` : `${baseSelectClass} border-neutral-300`)
|
||||||
|
const createContractNatureFieldClass = computed(() => createValidationTouched.contractNature && !isCreateContractNatureValid.value ? `${baseSelectClass} border-red-500` : `${baseSelectClass} border-neutral-300`)
|
||||||
|
const createContractStartDateFieldClass = computed(() => createValidationTouched.startDate && !isCreateContractStartDateValid.value ? `${baseInputClass} border-red-500` : `${baseInputClass} border-neutral-300`)
|
||||||
|
const createContractEndDateFieldClass = computed(() => createValidationTouched.endDate && !isCreateContractEndDateValid.value ? `${baseInputClass} border-red-500` : `${baseInputClass} border-neutral-300`)
|
||||||
|
const closeContractWorkedHoursLabel = computed(() => {
|
||||||
|
if (contractForm.weeklyHours !== null && contractForm.weeklyHours !== undefined) return `${contractForm.weeklyHours} heures`
|
||||||
|
return contractForm.contractName || '-'
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetContractValidation = () => {
|
||||||
|
validationTouched.endDate = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrateSuspensionForms = () => {
|
||||||
|
const current = employee.value?.currentSuspensions ?? []
|
||||||
|
suspensionForms.value = current.map(s => ({
|
||||||
|
id: s.id,
|
||||||
|
startDate: s.startDate,
|
||||||
|
endDate: s.endDate ?? '',
|
||||||
|
comment: s.comment ?? ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydrateContractFormFromCurrent = () => {
|
||||||
|
const current = employee.value
|
||||||
|
const active = currentActiveContractPeriod.value
|
||||||
|
if (!current || !active) return
|
||||||
|
|
||||||
|
contractForm.contractId = active.contractId ?? current.contract?.id ?? ''
|
||||||
|
contractForm.contractName = active.contractName ?? current.contract?.name ?? ''
|
||||||
|
contractForm.weeklyHours = active.weeklyHours ?? current.contract?.weeklyHours ?? null
|
||||||
|
contractForm.contractNature = active.contractNature
|
||||||
|
contractForm.startDate = active.startDate
|
||||||
|
contractForm.endDate = getTodayYmd()
|
||||||
|
contractForm.paidLeaveSettled = false
|
||||||
|
contractForm.comment = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCloseContractDrawer = () => {
|
||||||
|
if (!employee.value || !canCloseCurrentContract.value) return
|
||||||
|
hydrateContractFormFromCurrent()
|
||||||
|
resetContractValidation()
|
||||||
|
hydrateSuspensionForms()
|
||||||
|
isContractDrawerOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const setContractDrawerOpen = (open: boolean) => {
|
||||||
|
isContractDrawerOpen.value = open
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetCreateValidation = () => {
|
||||||
|
createValidationTouched.contractId = false
|
||||||
|
createValidationTouched.contractNature = false
|
||||||
|
createValidationTouched.startDate = false
|
||||||
|
createValidationTouched.endDate = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreateContractDrawer = () => {
|
||||||
|
if (!employee.value || !canCreateContract.value) return
|
||||||
|
createContractForm.contractId = ''
|
||||||
|
createContractForm.contractNature = 'CDI'
|
||||||
|
createContractForm.endDate = ''
|
||||||
|
createContractForm.startDate = currentActiveContractPeriod.value?.endDate
|
||||||
|
? (shiftYmd(currentActiveContractPeriod.value.endDate, 1) ?? currentActiveContractPeriod.value.endDate)
|
||||||
|
: getTodayYmd()
|
||||||
|
resetCreateValidation()
|
||||||
|
isCreateContractDrawerOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCreateContractDrawerOpen = (open: boolean) => {
|
||||||
|
isCreateContractDrawerOpen.value = open
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitContractUpdate = async () => {
|
||||||
|
if (!employee.value || isContractSubmitting.value || !currentActiveContractPeriod.value) return
|
||||||
|
|
||||||
|
validationTouched.endDate = true
|
||||||
|
if (!isContractEndDateValid.value) return
|
||||||
|
|
||||||
|
if (contractForm.endDate < currentActiveContractPeriod.value.startDate) {
|
||||||
|
toast.error({
|
||||||
|
title: 'Erreur',
|
||||||
|
message: `La date de fin doit être postérieure au ${formatDate(currentActiveContractPeriod.value.startDate)}.`
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isContractSubmitting.value = true
|
||||||
|
try {
|
||||||
|
await updateEmployee(employee.value.id, {
|
||||||
|
firstName: employee.value.firstName,
|
||||||
|
lastName: employee.value.lastName,
|
||||||
|
siteId: employee.value.site?.id ?? null,
|
||||||
|
contractId: Number(contractForm.contractId),
|
||||||
|
contractEndDate: contractForm.endDate || null,
|
||||||
|
contractPaidLeaveSettled: contractForm.paidLeaveSettled,
|
||||||
|
contractComment: contractForm.comment || null
|
||||||
|
})
|
||||||
|
|
||||||
|
isContractDrawerOpen.value = false
|
||||||
|
await reloadEmployee()
|
||||||
|
} finally {
|
||||||
|
isContractSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitCreateContract = async () => {
|
||||||
|
if (!employee.value || isCreateContractSubmitting.value) return
|
||||||
|
|
||||||
|
createValidationTouched.contractId = true
|
||||||
|
createValidationTouched.contractNature = true
|
||||||
|
createValidationTouched.startDate = true
|
||||||
|
createValidationTouched.endDate = true
|
||||||
|
if (!isCreateContractFormValid.value) return
|
||||||
|
|
||||||
|
if (currentActiveContractPeriod.value?.endDate) {
|
||||||
|
const minStartDate = shiftYmd(currentActiveContractPeriod.value.endDate, 1) ?? currentActiveContractPeriod.value.endDate
|
||||||
|
if (createContractForm.startDate < minStartDate) {
|
||||||
|
toast.error({
|
||||||
|
title: 'Erreur',
|
||||||
|
message: `La date de début doit être au moins le ${formatDate(minStartDate)}.`
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreateContractSubmitting.value = true
|
||||||
|
try {
|
||||||
|
await updateEmployee(employee.value.id, {
|
||||||
|
firstName: employee.value.firstName,
|
||||||
|
lastName: employee.value.lastName,
|
||||||
|
siteId: employee.value.site?.id ?? null,
|
||||||
|
contractId: Number(createContractForm.contractId),
|
||||||
|
contractNature: createContractForm.contractNature,
|
||||||
|
contractStartDate: createContractForm.startDate,
|
||||||
|
contractEndDate: createContractForm.endDate || null
|
||||||
|
})
|
||||||
|
isCreateContractDrawerOpen.value = false
|
||||||
|
await reloadEmployee()
|
||||||
|
} finally {
|
||||||
|
isCreateContractSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitSuspension = async (index: number) => {
|
||||||
|
const form = suspensionForms.value[index]
|
||||||
|
if (!form || !form.startDate) return
|
||||||
|
|
||||||
|
const periodId = currentActiveContractPeriodId.value
|
||||||
|
if (!periodId) return
|
||||||
|
|
||||||
|
isSuspensionSubmitting.value = true
|
||||||
|
try {
|
||||||
|
if (form.id) {
|
||||||
|
await updateSuspension(form.id, {
|
||||||
|
startDate: form.startDate,
|
||||||
|
endDate: form.endDate || null,
|
||||||
|
comment: form.comment || null
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createSuspension({
|
||||||
|
contractPeriodId: periodId,
|
||||||
|
startDate: form.startDate,
|
||||||
|
endDate: form.endDate || null,
|
||||||
|
comment: form.comment || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await reloadEmployee()
|
||||||
|
hydrateSuspensionForms()
|
||||||
|
} finally {
|
||||||
|
isSuspensionSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSuspensionForm = () => {
|
||||||
|
suspensionForms.value.push({
|
||||||
|
id: null,
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
comment: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadContracts = async () => {
|
||||||
|
contracts.value = await listContracts()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(showsCreateContractEndDate, (shows) => {
|
||||||
|
if (!shows) {
|
||||||
|
createContractForm.endDate = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
contracts,
|
||||||
|
contractHistory,
|
||||||
|
contractForm,
|
||||||
|
createContractForm,
|
||||||
|
isContractDrawerOpen,
|
||||||
|
isContractSubmitting,
|
||||||
|
isCreateContractDrawerOpen,
|
||||||
|
isCreateContractSubmitting,
|
||||||
|
canCloseCurrentContract,
|
||||||
|
canCreateContract,
|
||||||
|
readonlyFieldClass,
|
||||||
|
closeContractWorkedHoursLabel,
|
||||||
|
contractEndDateFieldClass,
|
||||||
|
showContractEndDateError,
|
||||||
|
isContractEndDateValid,
|
||||||
|
createContractNatureFieldClass,
|
||||||
|
createContractFieldClass,
|
||||||
|
createContractStartDateFieldClass,
|
||||||
|
showsCreateContractEndDate,
|
||||||
|
requiresCreateContractEndDate,
|
||||||
|
createContractEndDateFieldClass,
|
||||||
|
isCreateContractFormValid,
|
||||||
|
contractNatureLabel,
|
||||||
|
contractHistoryLabel,
|
||||||
|
formatDate,
|
||||||
|
openCloseContractDrawer,
|
||||||
|
openCreateContractDrawer,
|
||||||
|
setContractDrawerOpen,
|
||||||
|
setCreateContractDrawerOpen,
|
||||||
|
submitContractUpdate,
|
||||||
|
submitCreateContract,
|
||||||
|
suspensionForms,
|
||||||
|
isSuspensionSubmitting,
|
||||||
|
submitSuspension,
|
||||||
|
addSuspensionForm,
|
||||||
|
currentActiveContractPeriodId,
|
||||||
|
loadContracts
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,75 +1,13 @@
|
|||||||
import type { Contract } from '~/services/dto/contract'
|
import type { Employee } from '~/services/dto/employee'
|
||||||
import type { Absence } from '~/services/dto/absence'
|
|
||||||
import type { EmployeeLeaveSummary } from '~/services/dto/employee-leave-summary'
|
|
||||||
import type { EmployeeRttSummary } from '~/services/dto/employee-rtt-summary'
|
|
||||||
import type { ContractHistoryItem, Employee } from '~/services/dto/employee'
|
|
||||||
import { CONTRACT_TYPES } from '~/services/dto/contract'
|
import { CONTRACT_TYPES } from '~/services/dto/contract'
|
||||||
import { listAbsences } from '~/services/absences'
|
import { getEmployee } from '~/services/employees'
|
||||||
import { listContracts } from '~/services/contracts'
|
|
||||||
import { getEmployeeLeaveSummary, updateFractionedDays } from '~/services/employee-leave-summary'
|
|
||||||
import { getEmployeeRttSummary, createRttPayment } 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, showsContractEndDate } from '~/utils/contract'
|
|
||||||
import { createSuspension, updateSuspension } from '~/services/contractSuspensions'
|
|
||||||
|
|
||||||
export const useEmployeeDetailPage = () => {
|
export const useEmployeeDetailPage = () => {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const toast = useToast()
|
|
||||||
const employee = ref<Employee | null>(null)
|
const employee = ref<Employee | null>(null)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const activeTab = ref<'contract' | 'leave' | 'rtt'>('contract')
|
const activeTab = ref<'contract' | 'leave' | 'rtt'>('contract')
|
||||||
const contracts = ref<Contract[]>([])
|
|
||||||
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)
|
|
||||||
const isCreateContractSubmitting = ref(false)
|
|
||||||
|
|
||||||
type SuspensionForm = {
|
|
||||||
id: number | null
|
|
||||||
startDate: string
|
|
||||||
endDate: string
|
|
||||||
comment: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const suspensionForms = ref<SuspensionForm[]>([])
|
|
||||||
const isSuspensionSubmitting = ref(false)
|
|
||||||
|
|
||||||
const contractForm = reactive({
|
|
||||||
contractId: '' as number | '',
|
|
||||||
contractName: '',
|
|
||||||
weeklyHours: null as number | null,
|
|
||||||
contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM',
|
|
||||||
startDate: '',
|
|
||||||
endDate: '',
|
|
||||||
paidLeaveSettled: false,
|
|
||||||
comment: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const validationTouched = reactive({
|
|
||||||
endDate: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const createContractForm = reactive({
|
|
||||||
contractId: '' as number | '',
|
|
||||||
contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM',
|
|
||||||
startDate: '',
|
|
||||||
endDate: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const createValidationTouched = reactive({
|
|
||||||
contractId: false,
|
|
||||||
contractNature: false,
|
|
||||||
startDate: false,
|
|
||||||
endDate: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const contractHistory = computed(() => employee.value?.contractHistory ?? [])
|
|
||||||
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 employeeContractWorkLabel = computed(() => {
|
const employeeContractWorkLabel = computed(() => {
|
||||||
@@ -80,133 +18,6 @@ export const useEmployeeDetailPage = () => {
|
|||||||
return contract.name || '-'
|
return contract.name || '-'
|
||||||
})
|
})
|
||||||
|
|
||||||
const formatDate = (value?: string | null) => formatNullableYmdToFr(value)
|
|
||||||
|
|
||||||
const contractHistoryLabel = (item: ContractHistoryItem) => {
|
|
||||||
if (item.weeklyHours !== null && item.weeklyHours !== undefined) {
|
|
||||||
return `${item.weeklyHours} heures`
|
|
||||||
}
|
|
||||||
return item.contractName ?? '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentActiveContractPeriod = computed(() => {
|
|
||||||
const today = getTodayYmd()
|
|
||||||
const history = employee.value?.contractHistory ?? []
|
|
||||||
return history.find((item) => item.startDate <= today && (!item.endDate || item.endDate >= today)) ?? null
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentActiveContractPeriodId = computed<number | null>(() => {
|
|
||||||
const period = currentActiveContractPeriod.value
|
|
||||||
return period?.periodId ?? null
|
|
||||||
})
|
|
||||||
|
|
||||||
const canCloseCurrentContract = computed(() => {
|
|
||||||
const active = currentActiveContractPeriod.value
|
|
||||||
if (!active) return false
|
|
||||||
if (!active.endDate) return true
|
|
||||||
return active.endDate > getTodayYmd()
|
|
||||||
})
|
|
||||||
|
|
||||||
const canCreateContract = computed(() => {
|
|
||||||
const active = currentActiveContractPeriod.value
|
|
||||||
if (!active) return true
|
|
||||||
return !!active.endDate
|
|
||||||
})
|
|
||||||
|
|
||||||
const isContractEndDateValid = computed(() => contractForm.endDate !== '')
|
|
||||||
const showContractEndDateError = computed(() => validationTouched.endDate && !isContractEndDateValid.value)
|
|
||||||
|
|
||||||
const showsCreateContractEndDate = computed(() => showsContractEndDate(createContractForm.contractNature))
|
|
||||||
const requiresCreateContractEndDate = computed(() => requiresContractEndDate(createContractForm.contractNature))
|
|
||||||
const isCreateContractValid = computed(() => createContractForm.contractId !== '')
|
|
||||||
const isCreateContractNatureValid = computed(() => isContractNature(createContractForm.contractNature))
|
|
||||||
const isCreateContractStartDateValid = computed(() => createContractForm.startDate !== '')
|
|
||||||
const isCreateContractEndDateValid = computed(() => !requiresCreateContractEndDate.value || createContractForm.endDate !== '')
|
|
||||||
const isCreateContractFormValid = computed(() =>
|
|
||||||
isCreateContractValid.value &&
|
|
||||||
isCreateContractNatureValid.value &&
|
|
||||||
isCreateContractStartDateValid.value &&
|
|
||||||
isCreateContractEndDateValid.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 readonlyFieldClass = `${baseInputClass} border-neutral-300 bg-neutral-100 text-neutral-700`
|
|
||||||
const contractEndDateFieldClass = computed(() => showContractEndDateError.value ? `${baseInputClass} border-red-500` : `${baseInputClass} border-neutral-300`)
|
|
||||||
const baseSelectClass = 'mt-2 w-full rounded-md border bg-white px-3 py-2 text-md text-neutral-900'
|
|
||||||
const createContractFieldClass = computed(() => createValidationTouched.contractId && !isCreateContractValid.value ? `${baseSelectClass} border-red-500` : `${baseSelectClass} border-neutral-300`)
|
|
||||||
const createContractNatureFieldClass = computed(() => createValidationTouched.contractNature && !isCreateContractNatureValid.value ? `${baseSelectClass} border-red-500` : `${baseSelectClass} border-neutral-300`)
|
|
||||||
const createContractStartDateFieldClass = computed(() => createValidationTouched.startDate && !isCreateContractStartDateValid.value ? `${baseInputClass} border-red-500` : `${baseInputClass} border-neutral-300`)
|
|
||||||
const createContractEndDateFieldClass = computed(() => createValidationTouched.endDate && !isCreateContractEndDateValid.value ? `${baseInputClass} border-red-500` : `${baseInputClass} border-neutral-300`)
|
|
||||||
const closeContractWorkedHoursLabel = computed(() => {
|
|
||||||
if (contractForm.weeklyHours !== null && contractForm.weeklyHours !== undefined) return `${contractForm.weeklyHours} heures`
|
|
||||||
return contractForm.contractName || '-'
|
|
||||||
})
|
|
||||||
|
|
||||||
const resetContractValidation = () => {
|
|
||||||
validationTouched.endDate = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const hydrateSuspensionForms = () => {
|
|
||||||
const current = employee.value?.currentSuspensions ?? []
|
|
||||||
suspensionForms.value = current.map(s => ({
|
|
||||||
id: s.id,
|
|
||||||
startDate: s.startDate,
|
|
||||||
endDate: s.endDate ?? '',
|
|
||||||
comment: s.comment ?? ''
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const hydrateContractFormFromCurrent = () => {
|
|
||||||
const current = employee.value
|
|
||||||
const active = currentActiveContractPeriod.value
|
|
||||||
if (!current || !active) return
|
|
||||||
|
|
||||||
contractForm.contractId = active.contractId ?? current.contract?.id ?? ''
|
|
||||||
contractForm.contractName = active.contractName ?? current.contract?.name ?? ''
|
|
||||||
contractForm.weeklyHours = active.weeklyHours ?? current.contract?.weeklyHours ?? null
|
|
||||||
contractForm.contractNature = active.contractNature
|
|
||||||
contractForm.startDate = active.startDate
|
|
||||||
contractForm.endDate = getTodayYmd()
|
|
||||||
contractForm.paidLeaveSettled = false
|
|
||||||
contractForm.comment = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const openCloseContractDrawer = () => {
|
|
||||||
if (!employee.value || !canCloseCurrentContract.value) return
|
|
||||||
hydrateContractFormFromCurrent()
|
|
||||||
resetContractValidation()
|
|
||||||
hydrateSuspensionForms()
|
|
||||||
isContractDrawerOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const setContractDrawerOpen = (open: boolean) => {
|
|
||||||
isContractDrawerOpen.value = open
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetCreateValidation = () => {
|
|
||||||
createValidationTouched.contractId = false
|
|
||||||
createValidationTouched.contractNature = false
|
|
||||||
createValidationTouched.startDate = false
|
|
||||||
createValidationTouched.endDate = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const openCreateContractDrawer = () => {
|
|
||||||
if (!employee.value || !canCreateContract.value) return
|
|
||||||
createContractForm.contractId = ''
|
|
||||||
createContractForm.contractNature = 'CDI'
|
|
||||||
createContractForm.endDate = ''
|
|
||||||
createContractForm.startDate = currentActiveContractPeriod.value?.endDate
|
|
||||||
? (shiftYmd(currentActiveContractPeriod.value.endDate, 1) ?? currentActiveContractPeriod.value.endDate)
|
|
||||||
: getTodayYmd()
|
|
||||||
resetCreateValidation()
|
|
||||||
isCreateContractDrawerOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const setCreateContractDrawerOpen = (open: boolean) => {
|
|
||||||
isCreateContractDrawerOpen.value = open
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadEmployee = async () => {
|
const loadEmployee = async () => {
|
||||||
const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
||||||
const employeeId = Number(idParam)
|
const employeeId = Number(idParam)
|
||||||
@@ -216,185 +27,42 @@ export const useEmployeeDetailPage = () => {
|
|||||||
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
const loadedEmployee = await getEmployee(employeeId)
|
employee.value = await getEmployee(employeeId)
|
||||||
employee.value = loadedEmployee
|
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
const isForfait = loadedEmployee.contract?.type === CONTRACT_TYPES.FORFAIT
|
|
||||||
const leaveYear = isForfait
|
|
||||||
? now.getFullYear()
|
|
||||||
: (now.getMonth() >= 5 ? now.getFullYear() + 1 : now.getFullYear())
|
|
||||||
const rttYear = now.getMonth() >= 5 ? now.getFullYear() + 1 : now.getFullYear()
|
|
||||||
const from = isForfait
|
|
||||||
? `${leaveYear}-01-01`
|
|
||||||
: `${leaveYear - 1}-06-01`
|
|
||||||
const to = isForfait
|
|
||||||
? `${leaveYear}-12-31`
|
|
||||||
: `${leaveYear}-05-31`
|
|
||||||
const holidayYears = isForfait
|
|
||||||
? [leaveYear]
|
|
||||||
: [leaveYear - 1, leaveYear]
|
|
||||||
const [absences, summary, rtt, ...holidayResults] = await Promise.all([
|
|
||||||
listAbsences({
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
employeeId: loadedEmployee.id
|
|
||||||
}),
|
|
||||||
showLeaveTab.value
|
|
||||||
? getEmployeeLeaveSummary(loadedEmployee.id, leaveYear)
|
|
||||||
: Promise.resolve(null),
|
|
||||||
showRttTab.value
|
|
||||||
? getEmployeeRttSummary(loadedEmployee.id, rttYear)
|
|
||||||
: Promise.resolve(null),
|
|
||||||
...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') {
|
if (!showLeaveTab.value && activeTab.value === 'leave') {
|
||||||
activeTab.value = 'contract'
|
activeTab.value = 'contract'
|
||||||
}
|
}
|
||||||
if (!showRttTab.value && activeTab.value === 'rtt') {
|
if (!showRttTab.value && activeTab.value === 'rtt') {
|
||||||
activeTab.value = 'contract'
|
activeTab.value = 'contract'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leave.resetLoaded()
|
||||||
|
rtt.resetLoaded()
|
||||||
|
|
||||||
|
if (activeTab.value === 'leave' && showLeaveTab.value) {
|
||||||
|
await leave.loadLeaveData()
|
||||||
|
} else if (activeTab.value === 'rtt' && showRttTab.value) {
|
||||||
|
await rtt.loadRttData()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitContractUpdate = async () => {
|
const contract = useEmployeeContract(employee, loadEmployee)
|
||||||
if (!employee.value || isContractSubmitting.value || !currentActiveContractPeriod.value) return
|
const leave = useEmployeeLeave(employee, loadEmployee)
|
||||||
|
const rtt = useEmployeeRtt(employee, loadEmployee)
|
||||||
|
|
||||||
validationTouched.endDate = true
|
watch(activeTab, (tab) => {
|
||||||
if (!isContractEndDateValid.value) return
|
if (tab === 'leave' && !leave.leaveDataLoaded.value && showLeaveTab.value) {
|
||||||
|
leave.loadLeaveData()
|
||||||
if (contractForm.endDate < currentActiveContractPeriod.value.startDate) {
|
} else if (tab === 'rtt' && !rtt.rttDataLoaded.value && showRttTab.value) {
|
||||||
toast.error({
|
rtt.loadRttData()
|
||||||
title: 'Erreur',
|
|
||||||
message: `La date de fin doit être postérieure au ${formatDate(currentActiveContractPeriod.value.startDate)}.`
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isContractSubmitting.value = true
|
|
||||||
try {
|
|
||||||
await updateEmployee(employee.value.id, {
|
|
||||||
firstName: employee.value.firstName,
|
|
||||||
lastName: employee.value.lastName,
|
|
||||||
siteId: employee.value.site?.id ?? null,
|
|
||||||
contractId: Number(contractForm.contractId),
|
|
||||||
contractEndDate: contractForm.endDate || null,
|
|
||||||
contractPaidLeaveSettled: contractForm.paidLeaveSettled,
|
|
||||||
contractComment: contractForm.comment || null
|
|
||||||
})
|
|
||||||
|
|
||||||
isContractDrawerOpen.value = false
|
|
||||||
await loadEmployee()
|
|
||||||
} finally {
|
|
||||||
isContractSubmitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitCreateContract = async () => {
|
|
||||||
if (!employee.value || isCreateContractSubmitting.value) return
|
|
||||||
|
|
||||||
createValidationTouched.contractId = true
|
|
||||||
createValidationTouched.contractNature = true
|
|
||||||
createValidationTouched.startDate = true
|
|
||||||
createValidationTouched.endDate = true
|
|
||||||
if (!isCreateContractFormValid.value) return
|
|
||||||
|
|
||||||
if (currentActiveContractPeriod.value?.endDate) {
|
|
||||||
const minStartDate = shiftYmd(currentActiveContractPeriod.value.endDate, 1) ?? currentActiveContractPeriod.value.endDate
|
|
||||||
if (createContractForm.startDate < minStartDate) {
|
|
||||||
toast.error({
|
|
||||||
title: 'Erreur',
|
|
||||||
message: `La date de début doit être au moins le ${formatDate(minStartDate)}.`
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isCreateContractSubmitting.value = true
|
|
||||||
try {
|
|
||||||
await updateEmployee(employee.value.id, {
|
|
||||||
firstName: employee.value.firstName,
|
|
||||||
lastName: employee.value.lastName,
|
|
||||||
siteId: employee.value.site?.id ?? null,
|
|
||||||
contractId: Number(createContractForm.contractId),
|
|
||||||
contractNature: createContractForm.contractNature,
|
|
||||||
contractStartDate: createContractForm.startDate,
|
|
||||||
contractEndDate: createContractForm.endDate || null
|
|
||||||
})
|
|
||||||
isCreateContractDrawerOpen.value = false
|
|
||||||
await loadEmployee()
|
|
||||||
} finally {
|
|
||||||
isCreateContractSubmitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitSuspension = async (index: number) => {
|
|
||||||
const form = suspensionForms.value[index]
|
|
||||||
if (!form || !form.startDate) return
|
|
||||||
|
|
||||||
const periodId = currentActiveContractPeriodId.value
|
|
||||||
if (!periodId) return
|
|
||||||
|
|
||||||
isSuspensionSubmitting.value = true
|
|
||||||
try {
|
|
||||||
if (form.id) {
|
|
||||||
await updateSuspension(form.id, {
|
|
||||||
startDate: form.startDate,
|
|
||||||
endDate: form.endDate || null,
|
|
||||||
comment: form.comment || null
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await createSuspension({
|
|
||||||
contractPeriodId: periodId,
|
|
||||||
startDate: form.startDate,
|
|
||||||
endDate: form.endDate || null,
|
|
||||||
comment: form.comment || null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await loadEmployee()
|
|
||||||
hydrateSuspensionForms()
|
|
||||||
} finally {
|
|
||||||
isSuspensionSubmitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addSuspensionForm = () => {
|
|
||||||
suspensionForms.value.push({
|
|
||||||
id: null,
|
|
||||||
startDate: '',
|
|
||||||
endDate: '',
|
|
||||||
comment: ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitFractionedDays = async (days: number) => {
|
|
||||||
if (!employee.value) return
|
|
||||||
const year = leaveSummary.value?.year ?? undefined
|
|
||||||
await updateFractionedDays(employee.value.id, days, year)
|
|
||||||
await loadEmployee()
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitRttPayment = async (month: number, base25Minutes: number, bonus25Minutes: number, base50Minutes: number, bonus50Minutes: number) => {
|
|
||||||
if (!employee.value) return
|
|
||||||
const year = rttSummary.value?.year ?? undefined
|
|
||||||
await createRttPayment(employee.value.id, month, base25Minutes, bonus25Minutes, base50Minutes, bonus50Minutes, year)
|
|
||||||
await loadEmployee()
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(showsCreateContractEndDate, (shows) => {
|
|
||||||
if (!shows) {
|
|
||||||
createContractForm.endDate = ''
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
contracts.value = await listContracts()
|
await contract.loadContracts()
|
||||||
await loadEmployee()
|
await loadEmployee()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -402,50 +70,11 @@ export const useEmployeeDetailPage = () => {
|
|||||||
employee,
|
employee,
|
||||||
isLoading,
|
isLoading,
|
||||||
activeTab,
|
activeTab,
|
||||||
contracts,
|
|
||||||
employeeAbsences,
|
|
||||||
leaveSummary,
|
|
||||||
rttSummary,
|
|
||||||
publicHolidays,
|
|
||||||
showLeaveTab,
|
showLeaveTab,
|
||||||
showRttTab,
|
showRttTab,
|
||||||
contractHistory,
|
|
||||||
employeeContractWorkLabel,
|
employeeContractWorkLabel,
|
||||||
contractForm,
|
...contract,
|
||||||
createContractForm,
|
...leave,
|
||||||
isContractDrawerOpen,
|
...rtt
|
||||||
isContractSubmitting,
|
|
||||||
isCreateContractDrawerOpen,
|
|
||||||
isCreateContractSubmitting,
|
|
||||||
canCloseCurrentContract,
|
|
||||||
canCreateContract,
|
|
||||||
readonlyFieldClass,
|
|
||||||
closeContractWorkedHoursLabel,
|
|
||||||
contractEndDateFieldClass,
|
|
||||||
showContractEndDateError,
|
|
||||||
isContractEndDateValid,
|
|
||||||
createContractNatureFieldClass,
|
|
||||||
createContractFieldClass,
|
|
||||||
createContractStartDateFieldClass,
|
|
||||||
showsCreateContractEndDate,
|
|
||||||
requiresCreateContractEndDate,
|
|
||||||
createContractEndDateFieldClass,
|
|
||||||
isCreateContractFormValid,
|
|
||||||
contractNatureLabel,
|
|
||||||
contractHistoryLabel,
|
|
||||||
formatDate,
|
|
||||||
openCloseContractDrawer,
|
|
||||||
openCreateContractDrawer,
|
|
||||||
setContractDrawerOpen,
|
|
||||||
setCreateContractDrawerOpen,
|
|
||||||
submitContractUpdate,
|
|
||||||
submitCreateContract,
|
|
||||||
submitFractionedDays,
|
|
||||||
submitRttPayment,
|
|
||||||
suspensionForms,
|
|
||||||
isSuspensionSubmitting,
|
|
||||||
submitSuspension,
|
|
||||||
addSuspensionForm,
|
|
||||||
currentActiveContractPeriodId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
70
frontend/composables/useEmployeeLeave.ts
Normal file
70
frontend/composables/useEmployeeLeave.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { Absence } from '~/services/dto/absence'
|
||||||
|
import type { EmployeeLeaveSummary } from '~/services/dto/employee-leave-summary'
|
||||||
|
import type { Employee } from '~/services/dto/employee'
|
||||||
|
import { CONTRACT_TYPES } from '~/services/dto/contract'
|
||||||
|
import { listAbsences } from '~/services/absences'
|
||||||
|
import { getEmployeeLeaveSummary, updateFractionedDays } from '~/services/employee-leave-summary'
|
||||||
|
import { listPublicHolidays } from '~/services/public-holidays'
|
||||||
|
|
||||||
|
export const useEmployeeLeave = (employee: Ref<Employee | null>, reloadEmployee: () => Promise<void>) => {
|
||||||
|
const employeeAbsences = ref<Absence[]>([])
|
||||||
|
const leaveSummary = ref<EmployeeLeaveSummary | null>(null)
|
||||||
|
const publicHolidays = ref<Record<string, string>>({})
|
||||||
|
const isLeaveLoading = ref(false)
|
||||||
|
const leaveDataLoaded = ref(false)
|
||||||
|
|
||||||
|
const getLeaveYear = () => {
|
||||||
|
const now = new Date()
|
||||||
|
const isForfait = employee.value?.contract?.type === CONTRACT_TYPES.FORFAIT
|
||||||
|
return isForfait
|
||||||
|
? now.getFullYear()
|
||||||
|
: (now.getMonth() >= 5 ? now.getFullYear() + 1 : now.getFullYear())
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadLeaveData = async () => {
|
||||||
|
if (!employee.value || isLeaveLoading.value) return
|
||||||
|
isLeaveLoading.value = true
|
||||||
|
try {
|
||||||
|
const isForfait = employee.value.contract?.type === CONTRACT_TYPES.FORFAIT
|
||||||
|
const leaveYear = getLeaveYear()
|
||||||
|
const from = isForfait ? `${leaveYear}-01-01` : `${leaveYear - 1}-06-01`
|
||||||
|
const to = isForfait ? `${leaveYear}-12-31` : `${leaveYear}-05-31`
|
||||||
|
const holidayYears = isForfait ? [leaveYear] : [leaveYear - 1, leaveYear]
|
||||||
|
|
||||||
|
const [absences, summary, ...holidayResults] = await Promise.all([
|
||||||
|
listAbsences({ from, to, employeeId: employee.value.id }),
|
||||||
|
getEmployeeLeaveSummary(employee.value.id, leaveYear),
|
||||||
|
...holidayYears.map((y) => listPublicHolidays('metropole', y))
|
||||||
|
])
|
||||||
|
employeeAbsences.value = absences
|
||||||
|
leaveSummary.value = summary
|
||||||
|
publicHolidays.value = Object.assign({}, ...holidayResults)
|
||||||
|
leaveDataLoaded.value = true
|
||||||
|
} finally {
|
||||||
|
isLeaveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetLoaded = () => {
|
||||||
|
leaveDataLoaded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitFractionedDays = async (days: number) => {
|
||||||
|
if (!employee.value) return
|
||||||
|
const year = leaveSummary.value?.year ?? undefined
|
||||||
|
await updateFractionedDays(employee.value.id, days, year)
|
||||||
|
await reloadEmployee()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
employeeAbsences,
|
||||||
|
leaveSummary,
|
||||||
|
publicHolidays,
|
||||||
|
isLeaveLoading,
|
||||||
|
leaveDataLoaded,
|
||||||
|
loadLeaveData,
|
||||||
|
resetLoaded,
|
||||||
|
submitFractionedDays
|
||||||
|
}
|
||||||
|
}
|
||||||
42
frontend/composables/useEmployeeRtt.ts
Normal file
42
frontend/composables/useEmployeeRtt.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type { EmployeeRttSummary } from '~/services/dto/employee-rtt-summary'
|
||||||
|
import type { Employee } from '~/services/dto/employee'
|
||||||
|
import { getEmployeeRttSummary, createRttPayment } from '~/services/employee-rtt-summary'
|
||||||
|
|
||||||
|
export const useEmployeeRtt = (employee: Ref<Employee | null>, reloadEmployee: () => Promise<void>) => {
|
||||||
|
const rttSummary = ref<EmployeeRttSummary | null>(null)
|
||||||
|
const isRttLoading = ref(false)
|
||||||
|
const rttDataLoaded = ref(false)
|
||||||
|
|
||||||
|
const loadRttData = async () => {
|
||||||
|
if (!employee.value || isRttLoading.value) return
|
||||||
|
isRttLoading.value = true
|
||||||
|
try {
|
||||||
|
const rttYear = new Date().getMonth() >= 5 ? new Date().getFullYear() + 1 : new Date().getFullYear()
|
||||||
|
rttSummary.value = await getEmployeeRttSummary(employee.value.id, rttYear)
|
||||||
|
rttDataLoaded.value = true
|
||||||
|
} finally {
|
||||||
|
isRttLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetLoaded = () => {
|
||||||
|
rttDataLoaded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitRttPayment = async (month: number, base25Minutes: number, bonus25Minutes: number, base50Minutes: number, bonus50Minutes: number) => {
|
||||||
|
if (!employee.value) return
|
||||||
|
const year = rttSummary.value?.year ?? undefined
|
||||||
|
await createRttPayment(employee.value.id, month, base25Minutes, bonus25Minutes, base50Minutes, bonus50Minutes, year)
|
||||||
|
await reloadEmployee()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rttSummary,
|
||||||
|
isRttLoading,
|
||||||
|
rttDataLoaded,
|
||||||
|
loadRttData,
|
||||||
|
resetLoaded,
|
||||||
|
submitRttPayment
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,15 +98,25 @@
|
|||||||
:on-add-suspension-form="addSuspensionForm"
|
:on-add-suspension-form="addSuspensionForm"
|
||||||
:current-contract-period-id="currentActiveContractPeriodId"
|
:current-contract-period-id="currentActiveContractPeriodId"
|
||||||
/>
|
/>
|
||||||
<EmployeesLeaveTab
|
<div v-else-if="showLeaveTab && activeTab === 'leave'" class="h-full">
|
||||||
v-else-if="showLeaveTab && activeTab === 'leave'"
|
<div v-if="isLeaveLoading" class="mt-6 rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
||||||
class="h-full"
|
Chargement...
|
||||||
:absences="employeeAbsences"
|
</div>
|
||||||
:summary="leaveSummary"
|
<EmployeesLeaveTab
|
||||||
:public-holidays="publicHolidays"
|
v-else
|
||||||
@update-fractioned-days="submitFractionedDays"
|
class="h-full"
|
||||||
/>
|
:absences="employeeAbsences"
|
||||||
<EmployeesRttTab v-else-if="showRttTab && activeTab === 'rtt'" class="h-full" :summary="rttSummary" @submit-rtt-payment="submitRttPayment" />
|
:summary="leaveSummary"
|
||||||
|
:public-holidays="publicHolidays"
|
||||||
|
@update-fractioned-days="submitFractionedDays"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="showRttTab && activeTab === 'rtt'" class="h-full">
|
||||||
|
<div v-if="isRttLoading" class="mt-6 rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
||||||
|
Chargement...
|
||||||
|
</div>
|
||||||
|
<EmployeesRttTab v-else class="h-full" :summary="rttSummary" @submit-rtt-payment="submitRttPayment" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,7 +171,9 @@ const {
|
|||||||
isSuspensionSubmitting,
|
isSuspensionSubmitting,
|
||||||
submitSuspension,
|
submitSuspension,
|
||||||
addSuspensionForm,
|
addSuspensionForm,
|
||||||
currentActiveContractPeriodId
|
currentActiveContractPeriodId,
|
||||||
|
isLeaveLoading,
|
||||||
|
isRttLoading
|
||||||
} = useEmployeeDetailPage()
|
} = useEmployeeDetailPage()
|
||||||
|
|
||||||
useHead(() => ({
|
useHead(() => ({
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ export type EmployeeLeaveSummary = {
|
|||||||
takenSaturdays: number
|
takenSaturdays: number
|
||||||
fractionedDays: number
|
fractionedDays: number
|
||||||
accruingDays: number
|
accruingDays: number
|
||||||
|
previousYearAcquiredDays: number
|
||||||
|
previousYearTakenDays: number
|
||||||
|
previousYearRemainingDays: number
|
||||||
presenceDaysByMonth: Record<string, number>
|
presenceDaysByMonth: Record<string, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,17 +20,20 @@ use App\State\EmployeeLeaveSummaryProvider;
|
|||||||
)]
|
)]
|
||||||
final class EmployeeLeaveSummary
|
final class EmployeeLeaveSummary
|
||||||
{
|
{
|
||||||
public int $year = 0;
|
public int $year = 0;
|
||||||
public bool $isSupported = false;
|
public bool $isSupported = false;
|
||||||
public string $ruleCode = '';
|
public string $ruleCode = '';
|
||||||
public float $acquiredDays = 0.0;
|
public float $acquiredDays = 0.0;
|
||||||
public float $remainingDays = 0.0;
|
public float $remainingDays = 0.0;
|
||||||
public float $takenDays = 0.0;
|
public float $takenDays = 0.0;
|
||||||
public float $acquiredSaturdays = 0.0;
|
public float $acquiredSaturdays = 0.0;
|
||||||
public float $remainingSaturdays = 0.0;
|
public float $remainingSaturdays = 0.0;
|
||||||
public float $takenSaturdays = 0.0;
|
public float $takenSaturdays = 0.0;
|
||||||
public float $fractionedDays = 0.0;
|
public float $fractionedDays = 0.0;
|
||||||
public float $accruingDays = 0.0;
|
public float $accruingDays = 0.0;
|
||||||
|
public float $previousYearAcquiredDays = 0.0;
|
||||||
|
public float $previousYearTakenDays = 0.0;
|
||||||
|
public float $previousYearRemainingDays = 0.0;
|
||||||
|
|
||||||
/** @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 = [];
|
||||||
|
|||||||
@@ -91,16 +91,19 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
|
|
||||||
$fractionedDays = $this->resolveFractionedDays($employee, $yearSummary['ruleCode'], $year);
|
$fractionedDays = $this->resolveFractionedDays($employee, $yearSummary['ruleCode'], $year);
|
||||||
|
|
||||||
$summary->isSupported = true;
|
$summary->isSupported = true;
|
||||||
$summary->ruleCode = $yearSummary['ruleCode'];
|
$summary->ruleCode = $yearSummary['ruleCode'];
|
||||||
$summary->acquiredDays = $yearSummary['acquiredDays'] + $fractionedDays;
|
$summary->acquiredDays = $yearSummary['acquiredDays'] + $fractionedDays;
|
||||||
$summary->acquiredSaturdays = $yearSummary['acquiredSaturdays'];
|
$summary->acquiredSaturdays = $yearSummary['acquiredSaturdays'];
|
||||||
$summary->fractionedDays = $fractionedDays;
|
$summary->fractionedDays = $fractionedDays;
|
||||||
$summary->accruingDays = $yearSummary['accruingDays'];
|
$summary->accruingDays = $yearSummary['accruingDays'];
|
||||||
$summary->takenDays = $yearSummary['takenDays'];
|
$summary->takenDays = $yearSummary['takenDays'];
|
||||||
$summary->takenSaturdays = $yearSummary['takenSaturdays'];
|
$summary->takenSaturdays = $yearSummary['takenSaturdays'];
|
||||||
$summary->remainingDays = $yearSummary['remainingDays'] + $fractionedDays;
|
$summary->remainingDays = $yearSummary['remainingDays'] + $fractionedDays;
|
||||||
$summary->remainingSaturdays = $yearSummary['remainingSaturdays'];
|
$summary->remainingSaturdays = $yearSummary['remainingSaturdays'];
|
||||||
|
$summary->previousYearAcquiredDays = $yearSummary['previousYearAcquiredDays'];
|
||||||
|
$summary->previousYearTakenDays = $yearSummary['previousYearTakenDays'];
|
||||||
|
$summary->previousYearRemainingDays = $yearSummary['previousYearRemainingDays'];
|
||||||
|
|
||||||
[$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year);
|
[$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year);
|
||||||
$summary->presenceDaysByMonth = $this->computePresenceDaysByMonth($employee, $periodFrom, $periodTo);
|
$summary->presenceDaysByMonth = $this->computePresenceDaysByMonth($employee, $periodFrom, $periodTo);
|
||||||
@@ -117,7 +120,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
* takenDays: float,
|
* takenDays: float,
|
||||||
* takenSaturdays: float,
|
* takenSaturdays: float,
|
||||||
* remainingDays: float,
|
* remainingDays: float,
|
||||||
* remainingSaturdays: float
|
* remainingSaturdays: float,
|
||||||
|
* previousYearAcquiredDays: float,
|
||||||
|
* previousYearTakenDays: float,
|
||||||
|
* previousYearRemainingDays: float
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
private function computeYearSummary(Employee $employee, int $targetYear): ?array
|
private function computeYearSummary(Employee $employee, int $targetYear): ?array
|
||||||
@@ -214,6 +220,10 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
$takenDays += $openingBalance->getTakenDays();
|
$takenDays += $openingBalance->getTakenDays();
|
||||||
$takenSaturdays += $openingBalance->getTakenSaturdays();
|
$takenSaturdays += $openingBalance->getTakenSaturdays();
|
||||||
}
|
}
|
||||||
|
$previousYearAcquired = 0.0;
|
||||||
|
$previousYearTaken = 0.0;
|
||||||
|
$previousYearRemaining = 0.0;
|
||||||
|
|
||||||
if (LeaveRuleCode::CDI_CDD_NON_FORFAIT->value === $leavePolicy['ruleCode']) {
|
if (LeaveRuleCode::CDI_CDD_NON_FORFAIT->value === $leavePolicy['ruleCode']) {
|
||||||
$availableAcquired = max(0.0, $carryDays);
|
$availableAcquired = max(0.0, $carryDays);
|
||||||
$takenFromAcquired = min($availableAcquired, $takenDays);
|
$takenFromAcquired = min($availableAcquired, $takenDays);
|
||||||
@@ -238,26 +248,37 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
|||||||
} else {
|
} else {
|
||||||
// Forfait: no "en cours d'acquisition" counter, all rights are in acquired.
|
// Forfait: no "en cours d'acquisition" counter, all rights are in acquired.
|
||||||
// Suspensions do not impact forfait 218 leave calculation.
|
// Suspensions do not impact forfait 218 leave calculation.
|
||||||
$acquiredDays = $carryDays + $leavePolicy['acquiredDays'];
|
// Taken days are first deducted from N-1 carry, then from current year.
|
||||||
|
$previousYearAcquired = $carryDays;
|
||||||
|
$takenFromPrevious = min(max(0.0, $previousYearAcquired), $takenDays);
|
||||||
|
$previousYearTaken = $takenFromPrevious;
|
||||||
|
$takenFromCurrent = $takenDays - $takenFromPrevious;
|
||||||
|
|
||||||
|
$previousYearRemaining = max(0.0, $previousYearAcquired - $takenFromPrevious);
|
||||||
|
|
||||||
|
$acquiredDays = $leavePolicy['acquiredDays'];
|
||||||
$accruingDays = 0.0;
|
$accruingDays = 0.0;
|
||||||
$remainingDays = max(0.0, $acquiredDays - $takenDays);
|
$remainingDays = max(0.0, $acquiredDays - $takenFromCurrent);
|
||||||
$acquiredSaturdays = 0.0;
|
$acquiredSaturdays = 0.0;
|
||||||
$remainingSaturdays = 0.0;
|
$remainingSaturdays = 0.0;
|
||||||
|
|
||||||
$previousRemainingDays = $remainingDays;
|
$previousRemainingDays = $previousYearRemaining + $remainingDays;
|
||||||
$previousRemainingSaturdays = 0.0;
|
$previousRemainingSaturdays = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($year === $targetYear) {
|
if ($year === $targetYear) {
|
||||||
$targetSummary = [
|
$targetSummary = [
|
||||||
'ruleCode' => $leavePolicy['ruleCode'],
|
'ruleCode' => $leavePolicy['ruleCode'],
|
||||||
'acquiredDays' => $acquiredDays,
|
'acquiredDays' => $acquiredDays,
|
||||||
'acquiredSaturdays' => $acquiredSaturdays,
|
'acquiredSaturdays' => $acquiredSaturdays,
|
||||||
'accruingDays' => $accruingDays,
|
'accruingDays' => $accruingDays,
|
||||||
'takenDays' => $takenDays,
|
'takenDays' => $takenDays,
|
||||||
'takenSaturdays' => $takenSaturdays,
|
'takenSaturdays' => $takenSaturdays,
|
||||||
'remainingDays' => $remainingDays,
|
'remainingDays' => $remainingDays,
|
||||||
'remainingSaturdays' => $remainingSaturdays,
|
'remainingSaturdays' => $remainingSaturdays,
|
||||||
|
'previousYearAcquiredDays' => $previousYearAcquired,
|
||||||
|
'previousYearTakenDays' => $previousYearTaken,
|
||||||
|
'previousYearRemainingDays' => $previousYearRemaining,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user