feat : ajout de la clôture de contrat et de la création de contrat
This commit is contained in:
@@ -28,7 +28,8 @@ Ce document centralise les règles métier actuellement implémentées dans l'ap
|
|||||||
### Règles de période
|
### Règles de période
|
||||||
|
|
||||||
- `CDI`:
|
- `CDI`:
|
||||||
- `endDate` doit être vide
|
- à la création d'une période: `endDate` doit être vide
|
||||||
|
- en clôture d'un contrat en cours: `endDate` peut être renseignée
|
||||||
- `CDD` / `INTERIM`:
|
- `CDD` / `INTERIM`:
|
||||||
- `endDate` obligatoire
|
- `endDate` obligatoire
|
||||||
- `endDate` ne peut pas être antérieure à `startDate`
|
- `endDate` ne peut pas être antérieure à `startDate`
|
||||||
@@ -134,6 +135,14 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
|
|||||||
- Détail employé:
|
- Détail employé:
|
||||||
- onglet `Suivi contrat` avec affichage de l'historique des périodes de contrat
|
- onglet `Suivi contrat` avec affichage de l'historique des périodes de contrat
|
||||||
- chaque ligne expose: nature (`CDI`/`CDD`/`INTERIM`), contrat/temps de travail, date de début, date de fin (ou "En cours")
|
- chaque ligne expose: nature (`CDI`/`CDD`/`INTERIM`), contrat/temps de travail, date de début, date de fin (ou "En cours")
|
||||||
|
- action `Clôturer`:
|
||||||
|
- bouton actif uniquement s'il existe un contrat en cours non déjà clôturé à la date du jour
|
||||||
|
- ouvre un drawer en lecture seule (type/temps de travail/date de début)
|
||||||
|
- seule la date de fin est saisissable (préremplie à aujourd'hui)
|
||||||
|
- backend: en mode clôture, seule `contractEndDate` est acceptée
|
||||||
|
- action `Ajouter`:
|
||||||
|
- conserve le flux d'ajout d'un nouveau contrat via drawer dédié
|
||||||
|
- disponible uniquement s'il n'y a pas de contrat en cours, ou si le contrat en cours a déjà une date de fin
|
||||||
|
|
||||||
## 10) Notifications
|
## 10) Notifications
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
/>
|
/>
|
||||||
<span :style="{ backgroundColor: site.color }" class="h-3 w-3 rounded" />
|
|
||||||
<span class="text-md text-neutral-800">{{ site.name }}</span>
|
<span class="text-md text-neutral-800">{{ site.name }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
222
frontend/components/employees/ContractTab.vue
Normal file
222
frontend/components/employees/ContractTab.vue
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<template>
|
||||||
|
<section class="mt-8">
|
||||||
|
<div class="overflow-hidden rounded-lg border border-neutral-200 bg-white">
|
||||||
|
<div class="grid grid-cols-4 border-b border-neutral-200 bg-neutral-50 px-6 py-3 text-md font-semibold text-neutral-700">
|
||||||
|
<p>Contrat</p>
|
||||||
|
<p>Heures</p>
|
||||||
|
<p>Date de début</p>
|
||||||
|
<p>Date de fin</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="contractHistory.length === 0" class="px-6 py-4 text-md text-neutral-600">
|
||||||
|
Aucun historique de contrat.
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-for="item in contractHistory"
|
||||||
|
:key="`${item.startDate}-${item.endDate ?? 'open'}-${item.contractId ?? item.contractName}`"
|
||||||
|
class="grid grid-cols-4 border-b border-neutral-100 px-6 py-3 text-md text-primary-500 last:border-b-0"
|
||||||
|
>
|
||||||
|
<p>{{ contractNatureLabel(item.contractNature) }}</p>
|
||||||
|
<p>{{ contractHistoryLabel(item) }}</p>
|
||||||
|
<p>{{ formatDate(item.startDate) }}</p>
|
||||||
|
<p>{{ formatDate(item.endDate) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 flex justify-center gap-12">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-[200px] rounded-md bg-blue-500 py-2 text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
:disabled="isContractSubmitting || !canCloseCurrentContract"
|
||||||
|
@click="onOpenCloseContractDrawer"
|
||||||
|
>
|
||||||
|
Clôturer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex w-[200px] items-center justify-center gap-2 rounded-md bg-primary-500 px-4 py-2 text-md text-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
:disabled="isCreateContractSubmitting || contracts.length === 0 || !canCreateContract"
|
||||||
|
@click="onOpenCreateContractDrawer"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:plus-thick" size="16" />
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AppDrawer :model-value="isContractDrawerOpen" title="Clôturer le contrat" @update:model-value="onUpdateContractDrawerOpen">
|
||||||
|
<form class="space-y-4" @submit.prevent="onSubmitCloseContract">
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="contract-nature">
|
||||||
|
Type de contrat
|
||||||
|
</label>
|
||||||
|
<input id="contract-nature" :value="contractNatureLabel(contractForm.contractNature)" type="text" :class="readonlyFieldClass" readonly />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="contract">
|
||||||
|
Temps de travail
|
||||||
|
</label>
|
||||||
|
<input id="contract" :value="closeContractWorkedHoursLabel" type="text" :class="readonlyFieldClass" readonly />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="contract-start-date">
|
||||||
|
Début contrat
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="contract-start-date"
|
||||||
|
:value="contractForm.startDate"
|
||||||
|
type="date"
|
||||||
|
:class="readonlyFieldClass"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="contract-end-date">
|
||||||
|
Fin contrat <span class="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="contract-end-date"
|
||||||
|
v-model="contractForm.endDate"
|
||||||
|
type="date"
|
||||||
|
:class="contractEndDateFieldClass"
|
||||||
|
/>
|
||||||
|
<p v-if="showContractEndDateError" class="mt-1 text-sm text-red-600">La date de fin est obligatoire.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||||
|
:disabled="isContractSubmitting"
|
||||||
|
@click="onUpdateContractDrawerOpen(false)"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
:disabled="isContractSubmitting || !isContractEndDateValid"
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</AppDrawer>
|
||||||
|
|
||||||
|
<AppDrawer :model-value="isCreateContractDrawerOpen" title="Ajouter un contrat" @update:model-value="onUpdateCreateContractDrawerOpen">
|
||||||
|
<form class="space-y-4" @submit.prevent="onSubmitCreateContract">
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="create-contract-nature">
|
||||||
|
Type de contrat <span class="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<select id="create-contract-nature" v-model="createContractForm.contractNature" :class="createContractNatureFieldClass">
|
||||||
|
<option value="CDI">CDI</option>
|
||||||
|
<option value="CDD">CDD</option>
|
||||||
|
<option value="INTERIM">Intérim</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="create-contract-id">
|
||||||
|
Temps de travail <span class="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<select id="create-contract-id" v-model="createContractForm.contractId" :class="createContractFieldClass">
|
||||||
|
<option value="">Sélectionner un contrat</option>
|
||||||
|
<option v-for="contract in contracts" :key="contract.id" :value="contract.id">
|
||||||
|
{{ contract.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="create-contract-start-date">
|
||||||
|
Début contrat <span class="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<input id="create-contract-start-date" v-model="createContractForm.startDate" type="date" :class="createContractStartDateFieldClass" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="requiresCreateContractEndDate">
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="create-contract-end-date">
|
||||||
|
Fin contrat <span class="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<input id="create-contract-end-date" v-model="createContractForm.endDate" type="date" :class="createContractEndDateFieldClass" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg border border-neutral-200 px-4 py-2 text-md font-semibold text-neutral-700 hover:bg-neutral-100"
|
||||||
|
:disabled="isCreateContractSubmitting"
|
||||||
|
@click="onUpdateCreateContractDrawerOpen(false)"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
:disabled="isCreateContractSubmitting || !isCreateContractFormValid"
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</AppDrawer>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Contract } from '~/services/dto/contract'
|
||||||
|
import type { ContractHistoryItem } from '~/services/dto/employee'
|
||||||
|
|
||||||
|
type ContractForm = {
|
||||||
|
contractId: number | ''
|
||||||
|
contractName: string
|
||||||
|
weeklyHours: number | null
|
||||||
|
contractNature: 'CDI' | 'CDD' | 'INTERIM'
|
||||||
|
startDate: string
|
||||||
|
endDate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateContractForm = {
|
||||||
|
contractId: number | ''
|
||||||
|
contractNature: 'CDI' | 'CDD' | 'INTERIM'
|
||||||
|
startDate: string
|
||||||
|
endDate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
contractHistory: ContractHistoryItem[]
|
||||||
|
contractNatureLabel: (value?: 'CDI' | 'CDD' | 'INTERIM') => string
|
||||||
|
contractHistoryLabel: (item: ContractHistoryItem) => string
|
||||||
|
formatDate: (value?: string | null) => string
|
||||||
|
isContractSubmitting: boolean
|
||||||
|
canCloseCurrentContract: boolean
|
||||||
|
isCreateContractSubmitting: boolean
|
||||||
|
contracts: Contract[]
|
||||||
|
canCreateContract: boolean
|
||||||
|
isContractDrawerOpen: boolean
|
||||||
|
contractForm: ContractForm
|
||||||
|
readonlyFieldClass: string
|
||||||
|
closeContractWorkedHoursLabel: string
|
||||||
|
contractEndDateFieldClass: string
|
||||||
|
showContractEndDateError: boolean
|
||||||
|
isContractEndDateValid: boolean
|
||||||
|
isCreateContractDrawerOpen: boolean
|
||||||
|
createContractForm: CreateContractForm
|
||||||
|
createContractNatureFieldClass: string
|
||||||
|
createContractFieldClass: string
|
||||||
|
createContractStartDateFieldClass: string
|
||||||
|
requiresCreateContractEndDate: boolean
|
||||||
|
createContractEndDateFieldClass: string
|
||||||
|
isCreateContractFormValid: boolean
|
||||||
|
onOpenCloseContractDrawer: () => void
|
||||||
|
onOpenCreateContractDrawer: () => void
|
||||||
|
onUpdateContractDrawerOpen: (open: boolean) => void
|
||||||
|
onUpdateCreateContractDrawerOpen: (open: boolean) => void
|
||||||
|
onSubmitCloseContract: () => void
|
||||||
|
onSubmitCreateContract: () => void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
7
frontend/components/employees/LeaveTab.vue
Normal file
7
frontend/components/employees/LeaveTab.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<section class="mt-8">
|
||||||
|
<div class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
||||||
|
Bloc Congé (à implémenter)
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
7
frontend/components/employees/RttTab.vue
Normal file
7
frontend/components/employees/RttTab.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<section class="mt-8">
|
||||||
|
<div class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
||||||
|
Bloc RTT (à implémenter)
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
296
frontend/composables/useEmployeeDetailPage.ts
Normal file
296
frontend/composables/useEmployeeDetailPage.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import type { Contract } from '~/services/dto/contract'
|
||||||
|
import type { ContractHistoryItem, Employee } from '~/services/dto/employee'
|
||||||
|
import { CONTRACT_TYPES } from '~/services/dto/contract'
|
||||||
|
import { listContracts } from '~/services/contracts'
|
||||||
|
import { getEmployee, updateEmployee } from '~/services/employees'
|
||||||
|
import { formatNullableYmdToFr, getTodayYmd, shiftYmd } from '~/utils/date'
|
||||||
|
import { contractNatureLabel, isContractNature, requiresContractEndDate } from '~/utils/contract'
|
||||||
|
|
||||||
|
export const useEmployeeDetailPage = () => {
|
||||||
|
const route = useRoute()
|
||||||
|
const toast = useToast()
|
||||||
|
const employee = ref<Employee | null>(null)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const activeTab = ref<'contract' | 'leave' | 'rtt'>('contract')
|
||||||
|
const contracts = ref<Contract[]>([])
|
||||||
|
const isContractDrawerOpen = ref(false)
|
||||||
|
const isContractSubmitting = ref(false)
|
||||||
|
const isCreateContractDrawerOpen = ref(false)
|
||||||
|
const isCreateContractSubmitting = ref(false)
|
||||||
|
|
||||||
|
const contractForm = reactive({
|
||||||
|
contractId: '' as number | '',
|
||||||
|
contractName: '',
|
||||||
|
weeklyHours: null as number | null,
|
||||||
|
contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM',
|
||||||
|
startDate: '',
|
||||||
|
endDate: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
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 employeeContractWorkLabel = computed(() => {
|
||||||
|
const contract = employee.value?.contract
|
||||||
|
if (!contract) return '-'
|
||||||
|
if (contract.type === CONTRACT_TYPES.FORFAIT) return 'Forfait'
|
||||||
|
if (contract.weeklyHours !== null && contract.weeklyHours !== undefined) return `${contract.weeklyHours} heures`
|
||||||
|
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 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 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 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCloseContractDrawer = () => {
|
||||||
|
if (!employee.value || !canCloseCurrentContract.value) return
|
||||||
|
hydrateContractFormFromCurrent()
|
||||||
|
resetContractValidation()
|
||||||
|
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 idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
||||||
|
const employeeId = Number(idParam)
|
||||||
|
if (!Number.isInteger(employeeId) || employeeId <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
employee.value = await getEmployee(employeeId)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(requiresCreateContractEndDate, (required) => {
|
||||||
|
if (!required) {
|
||||||
|
createContractForm.endDate = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
contracts.value = await listContracts()
|
||||||
|
await loadEmployee()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
employee,
|
||||||
|
isLoading,
|
||||||
|
activeTab,
|
||||||
|
contracts,
|
||||||
|
contractHistory,
|
||||||
|
employeeContractWorkLabel,
|
||||||
|
contractForm,
|
||||||
|
createContractForm,
|
||||||
|
isContractDrawerOpen,
|
||||||
|
isContractSubmitting,
|
||||||
|
isCreateContractDrawerOpen,
|
||||||
|
isCreateContractSubmitting,
|
||||||
|
canCloseCurrentContract,
|
||||||
|
canCreateContract,
|
||||||
|
readonlyFieldClass,
|
||||||
|
closeContractWorkedHoursLabel,
|
||||||
|
contractEndDateFieldClass,
|
||||||
|
showContractEndDateError,
|
||||||
|
isContractEndDateValid,
|
||||||
|
createContractNatureFieldClass,
|
||||||
|
createContractFieldClass,
|
||||||
|
createContractStartDateFieldClass,
|
||||||
|
requiresCreateContractEndDate,
|
||||||
|
createContractEndDateFieldClass,
|
||||||
|
isCreateContractFormValid,
|
||||||
|
contractNatureLabel,
|
||||||
|
contractHistoryLabel,
|
||||||
|
formatDate,
|
||||||
|
openCloseContractDrawer,
|
||||||
|
openCreateContractDrawer,
|
||||||
|
setContractDrawerOpen,
|
||||||
|
setCreateContractDrawerOpen,
|
||||||
|
submitContractUpdate,
|
||||||
|
submitCreateContract
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="text-4xl font-bold text-primary-500">{{ employee.firstName }} {{ employee.lastName }}</h1>
|
<h1 class="text-4xl font-bold text-primary-500">{{ employee.firstName }} {{ employee.lastName }}</h1>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="font-bold text-[20px]">{{ contractNatureLabel(employee.currentContractNature) }} {{ employee.contract?.weeklyHours ?? '-' }} heures</p>
|
<p class="font-bold text-[20px]">{{ contractNatureLabel(employee.currentContractNature) }} {{ employeeContractWorkLabel }}</p>
|
||||||
<p class="text-[18px]">{{ employee.site?.name ?? '-' }}</p>
|
<p class="text-[18px]">{{ employee.site?.name ?? '-' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,97 +53,86 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section v-if="activeTab === 'contract'" class="mt-8">
|
<EmployeesContractTab
|
||||||
<div class="overflow-hidden rounded-lg border border-neutral-200 bg-white">
|
v-if="activeTab === 'contract'"
|
||||||
<div class="grid grid-cols-4 border-b border-neutral-200 bg-neutral-50 px-6 py-3 text-md font-semibold text-neutral-700">
|
:contract-history="contractHistory"
|
||||||
<p>Contrat</p>
|
:contract-nature-label="contractNatureLabel"
|
||||||
<p>Heures</p>
|
:contract-history-label="contractHistoryLabel"
|
||||||
<p>Date de début</p>
|
:format-date="formatDate"
|
||||||
<p>Date de fin</p>
|
:is-contract-submitting="isContractSubmitting"
|
||||||
</div>
|
:can-close-current-contract="canCloseCurrentContract"
|
||||||
<div v-if="contractHistory.length === 0" class="px-6 py-4 text-md text-neutral-600">
|
:is-create-contract-submitting="isCreateContractSubmitting"
|
||||||
Aucun historique de contrat.
|
:contracts="contracts"
|
||||||
</div>
|
:can-create-contract="canCreateContract"
|
||||||
<div v-else>
|
:is-contract-drawer-open="isContractDrawerOpen"
|
||||||
<div
|
:contract-form="contractForm"
|
||||||
v-for="item in contractHistory"
|
:readonly-field-class="readonlyFieldClass"
|
||||||
:key="`${item.startDate}-${item.endDate ?? 'open'}-${item.contractId ?? item.contractName}`"
|
:close-contract-worked-hours-label="closeContractWorkedHoursLabel"
|
||||||
class="grid grid-cols-4 border-b border-neutral-100 px-6 py-3 text-md text-primary-500 last:border-b-0"
|
:contract-end-date-field-class="contractEndDateFieldClass"
|
||||||
>
|
:show-contract-end-date-error="showContractEndDateError"
|
||||||
<p>{{ contractNatureLabel(item.contractNature) }}</p>
|
:is-contract-end-date-valid="isContractEndDateValid"
|
||||||
<p>{{ contractHistoryLabel(item) }}</p>
|
:is-create-contract-drawer-open="isCreateContractDrawerOpen"
|
||||||
<p>{{ formatDate(item.startDate) }}</p>
|
:create-contract-form="createContractForm"
|
||||||
<p>{{ formatDate(item.endDate) }}</p>
|
:create-contract-nature-field-class="createContractNatureFieldClass"
|
||||||
</div>
|
:create-contract-field-class="createContractFieldClass"
|
||||||
</div>
|
:create-contract-start-date-field-class="createContractStartDateFieldClass"
|
||||||
</div>
|
:requires-create-contract-end-date="requiresCreateContractEndDate"
|
||||||
<div class="flex justify-center mt-8 gap-12">
|
:create-contract-end-date-field-class="createContractEndDateFieldClass"
|
||||||
<button class="bg-blue-500 text-white rounded-md w-[200px]">Modifier</button>
|
:is-create-contract-form-valid="isCreateContractFormValid"
|
||||||
<button class="bg-primary-500 px-4 py-2 text-white text-md rounded-md flex justify-center items-center gap-2 w-[200px]">
|
:on-open-close-contract-drawer="openCloseContractDrawer"
|
||||||
<Icon name="mdi:plus-thick" size="16" />
|
:on-open-create-contract-drawer="openCreateContractDrawer"
|
||||||
Ajouter
|
:on-update-contract-drawer-open="setContractDrawerOpen"
|
||||||
</button>
|
:on-update-create-contract-drawer-open="setCreateContractDrawerOpen"
|
||||||
</div>
|
:on-submit-close-contract="submitContractUpdate"
|
||||||
</section>
|
:on-submit-create-contract="submitCreateContract"
|
||||||
<section v-else-if="activeTab === 'leave'" class="mt-8">
|
/>
|
||||||
<!-- Bloc Congé -->
|
<EmployeesLeaveTab v-else-if="activeTab === 'leave'" />
|
||||||
</section>
|
<EmployeesRttTab v-else />
|
||||||
<section v-else class="mt-8">
|
|
||||||
<!-- Bloc RTT -->
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {ContractHistoryItem, Employee} from '~/services/dto/employee'
|
const {
|
||||||
import {getEmployee} from '~/services/employees'
|
employee,
|
||||||
|
isLoading,
|
||||||
const route = useRoute()
|
activeTab,
|
||||||
const employee = ref<Employee | null>(null)
|
contracts,
|
||||||
const isLoading = ref(false)
|
contractHistory,
|
||||||
const activeTab = ref<'contract' | 'leave' | 'rtt'>('contract')
|
employeeContractWorkLabel,
|
||||||
|
contractForm,
|
||||||
|
createContractForm,
|
||||||
|
isContractDrawerOpen,
|
||||||
|
isContractSubmitting,
|
||||||
|
isCreateContractDrawerOpen,
|
||||||
|
isCreateContractSubmitting,
|
||||||
|
canCloseCurrentContract,
|
||||||
|
canCreateContract,
|
||||||
|
readonlyFieldClass,
|
||||||
|
closeContractWorkedHoursLabel,
|
||||||
|
contractEndDateFieldClass,
|
||||||
|
showContractEndDateError,
|
||||||
|
isContractEndDateValid,
|
||||||
|
createContractNatureFieldClass,
|
||||||
|
createContractFieldClass,
|
||||||
|
createContractStartDateFieldClass,
|
||||||
|
requiresCreateContractEndDate,
|
||||||
|
createContractEndDateFieldClass,
|
||||||
|
isCreateContractFormValid,
|
||||||
|
contractNatureLabel,
|
||||||
|
contractHistoryLabel,
|
||||||
|
formatDate,
|
||||||
|
openCloseContractDrawer,
|
||||||
|
openCreateContractDrawer,
|
||||||
|
setContractDrawerOpen,
|
||||||
|
setCreateContractDrawerOpen,
|
||||||
|
submitContractUpdate,
|
||||||
|
submitCreateContract
|
||||||
|
} = useEmployeeDetailPage()
|
||||||
|
|
||||||
useHead(() => ({
|
useHead(() => ({
|
||||||
title: employee.value
|
title: employee.value
|
||||||
? `${employee.value.firstName} ${employee.value.lastName}`
|
? `${employee.value.firstName} ${employee.value.lastName}`
|
||||||
: 'Détail employé'
|
: 'Détail employé'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const contractNatureLabel = (value?: 'CDI' | 'CDD' | 'INTERIM') => {
|
|
||||||
if (value === 'CDD') return 'CDD'
|
|
||||||
if (value === 'INTERIM') return 'Intérim'
|
|
||||||
return 'CDI'
|
|
||||||
}
|
|
||||||
|
|
||||||
const contractHistory = computed(() => employee.value?.contractHistory ?? [])
|
|
||||||
|
|
||||||
const formatDate = (value?: string | null) => {
|
|
||||||
if (!value) return 'En cours'
|
|
||||||
const [year, month, day] = value.split('-')
|
|
||||||
if (!year || !month || !day) return value
|
|
||||||
return `${day}/${month}/${year}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const contractHistoryLabel = (item: ContractHistoryItem) => {
|
|
||||||
if (item.weeklyHours !== null && item.weeklyHours !== undefined) {
|
|
||||||
return `${item.weeklyHours} heures`
|
|
||||||
}
|
|
||||||
return item.contractName ?? '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const idParam = Array.isArray(route.params.id) ? route.params.id[0] : route.params.id
|
|
||||||
const employeeId = Number(idParam)
|
|
||||||
if (!Number.isInteger(employeeId) || employeeId <= 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true
|
|
||||||
try {
|
|
||||||
employee.value = await getEmployee(employeeId)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -154,10 +154,10 @@
|
|||||||
La date de début est obligatoire.
|
La date de début est obligatoire.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="requiresContractEndDate">
|
<div v-if="requiresContractEndDateComputed">
|
||||||
<label class="text-md font-semibold text-neutral-700" for="contract-end-date">
|
<label class="text-md font-semibold text-neutral-700" for="contract-end-date">
|
||||||
Fin contrat
|
Fin contrat
|
||||||
<span v-if="requiresContractEndDate" class="text-red-600">*</span>
|
<span v-if="requiresContractEndDateComputed" class="text-red-600">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="contract-end-date"
|
id="contract-end-date"
|
||||||
@@ -199,6 +199,7 @@ import { listContracts } from '~/services/contracts'
|
|||||||
import { createEmployee, deleteEmployee, listEmployees, updateEmployee } from '~/services/employees'
|
import { createEmployee, deleteEmployee, listEmployees, updateEmployee } from '~/services/employees'
|
||||||
import { listSites } from '~/services/sites'
|
import { listSites } from '~/services/sites'
|
||||||
import SiteFilterSelector from '~/components/SiteFilterSelector.vue'
|
import SiteFilterSelector from '~/components/SiteFilterSelector.vue'
|
||||||
|
import { contractNatureLabel, isContractNature, requiresContractEndDate } from '~/utils/contract'
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Employés'
|
title: 'Employés'
|
||||||
})
|
})
|
||||||
@@ -236,12 +237,6 @@ const filteredEmployees = computed<Employee[]>(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const contractNatureLabel = (value?: 'CDI' | 'CDD' | 'INTERIM') => {
|
|
||||||
if (value === 'CDD') return 'CDD'
|
|
||||||
if (value === 'INTERIM') return 'Intérim'
|
|
||||||
return 'CDI'
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
@@ -266,11 +261,11 @@ const isFirstNameValid = computed(() => form.firstName.trim() !== '')
|
|||||||
const isLastNameValid = computed(() => form.lastName.trim() !== '')
|
const isLastNameValid = computed(() => form.lastName.trim() !== '')
|
||||||
const isSiteValid = computed(() => form.siteId !== '')
|
const isSiteValid = computed(() => form.siteId !== '')
|
||||||
const isContractValid = computed(() => form.contractId !== '')
|
const isContractValid = computed(() => form.contractId !== '')
|
||||||
const isContractNatureValid = computed(() => ['CDI', 'CDD', 'INTERIM'].includes(form.contractNature))
|
const isContractNatureValid = computed(() => isContractNature(form.contractNature))
|
||||||
const isContractStartDateValid = computed(() => form.contractStartDate !== '')
|
const isContractStartDateValid = computed(() => form.contractStartDate !== '')
|
||||||
const requiresContractEndDate = computed(() => form.contractNature === 'CDD' || form.contractNature === 'INTERIM')
|
const requiresContractEndDateComputed = computed(() => requiresContractEndDate(form.contractNature))
|
||||||
const isContractEndDateValid = computed(() => {
|
const isContractEndDateValid = computed(() => {
|
||||||
if (!requiresContractEndDate.value) return true
|
if (!requiresContractEndDateComputed.value) return true
|
||||||
return form.contractEndDate !== ''
|
return form.contractEndDate !== ''
|
||||||
})
|
})
|
||||||
const isFormValid = computed(
|
const isFormValid = computed(
|
||||||
@@ -433,7 +428,7 @@ const handleSubmit = async () => {
|
|||||||
contractId: Number(form.contractId),
|
contractId: Number(form.contractId),
|
||||||
contractNature: form.contractNature,
|
contractNature: form.contractNature,
|
||||||
contractStartDate: form.contractStartDate,
|
contractStartDate: form.contractStartDate,
|
||||||
contractEndDate: requiresContractEndDate.value ? form.contractEndDate : null
|
contractEndDate: requiresContractEndDateComputed.value ? form.contractEndDate : null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +459,7 @@ watch(isDrawerOpen, (isOpen) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(requiresContractEndDate, (required) => {
|
watch(requiresContractEndDateComputed, (required) => {
|
||||||
if (!required) {
|
if (!required) {
|
||||||
form.contractEndDate = ''
|
form.contractEndDate = ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const updateEmployee = async (
|
|||||||
firstName: string
|
firstName: string
|
||||||
lastName: string
|
lastName: string
|
||||||
siteId?: number | null
|
siteId?: number | null
|
||||||
contractId: number
|
contractId?: number
|
||||||
contractNature?: 'CDI' | 'CDD' | 'INTERIM'
|
contractNature?: 'CDI' | 'CDD' | 'INTERIM'
|
||||||
contractStartDate?: string
|
contractStartDate?: string
|
||||||
contractEndDate?: string | null
|
contractEndDate?: string | null
|
||||||
@@ -64,16 +64,27 @@ export const updateEmployee = async (
|
|||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
return api.patch<Employee>(`/employees/${id}`, {
|
const body: Record<string, unknown> = {
|
||||||
firstName: payload.firstName,
|
firstName: payload.firstName,
|
||||||
lastName: payload.lastName,
|
lastName: payload.lastName,
|
||||||
site: payload.siteId ? `/api/sites/${payload.siteId}` : null,
|
site: payload.siteId ? `/api/sites/${payload.siteId}` : null,
|
||||||
contract: `/api/contracts/${payload.contractId}`,
|
|
||||||
contractNature: payload.contractNature,
|
|
||||||
contractStartDate: payload.contractStartDate,
|
|
||||||
contractEndDate: payload.contractEndDate ?? null,
|
|
||||||
displayOrder: payload.displayOrder
|
displayOrder: payload.displayOrder
|
||||||
}, {
|
}
|
||||||
|
|
||||||
|
if (payload.contractId !== undefined) {
|
||||||
|
body.contract = `/api/contracts/${payload.contractId}`
|
||||||
|
}
|
||||||
|
if (payload.contractNature !== undefined) {
|
||||||
|
body.contractNature = payload.contractNature
|
||||||
|
}
|
||||||
|
if (payload.contractStartDate !== undefined) {
|
||||||
|
body.contractStartDate = payload.contractStartDate
|
||||||
|
}
|
||||||
|
if (payload.contractEndDate !== undefined) {
|
||||||
|
body.contractEndDate = payload.contractEndDate ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.patch<Employee>(`/employees/${id}`, body, {
|
||||||
toastSuccessKey: 'success.employee.update',
|
toastSuccessKey: 'success.employee.update',
|
||||||
toastErrorKey: 'errors.employee.update'
|
toastErrorKey: 'errors.employee.update'
|
||||||
})
|
})
|
||||||
|
|||||||
17
frontend/utils/contract.ts
Normal file
17
frontend/utils/contract.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const CONTRACT_NATURES = ['CDI', 'CDD', 'INTERIM'] as const
|
||||||
|
|
||||||
|
export type ContractNature = (typeof CONTRACT_NATURES)[number]
|
||||||
|
|
||||||
|
export const contractNatureLabel = (value?: ContractNature) => {
|
||||||
|
if (value === 'CDD') return 'CDD'
|
||||||
|
if (value === 'INTERIM') return 'Intérim'
|
||||||
|
return 'CDI'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requiresContractEndDate = (nature: ContractNature) => {
|
||||||
|
return nature === 'CDD' || nature === 'INTERIM'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isContractNature = (value: string): value is ContractNature => {
|
||||||
|
return (CONTRACT_NATURES as readonly string[]).includes(value)
|
||||||
|
}
|
||||||
@@ -6,6 +6,17 @@ export const toYmd = (year: number, month: number, day: number) => {
|
|||||||
|
|
||||||
export const normalizeDate = (value: string) => value.slice(0, 10)
|
export const normalizeDate = (value: string) => value.slice(0, 10)
|
||||||
|
|
||||||
|
export const formatYmdToFr = (value: string) => {
|
||||||
|
const [year, month, day] = value.split('-')
|
||||||
|
if (!year || !month || !day) return value
|
||||||
|
return `${day}/${month}/${year}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatNullableYmdToFr = (value?: string | null, fallback = 'En cours') => {
|
||||||
|
if (!value) return fallback
|
||||||
|
return formatYmdToFr(value)
|
||||||
|
}
|
||||||
|
|
||||||
export const parseYmd = (value: string) => {
|
export const parseYmd = (value: string) => {
|
||||||
const [year, month, day] = value.split('-').map(Number)
|
const [year, month, day] = value.split('-').map(Number)
|
||||||
if (!year || !month || !day) return null
|
if (!year || !month || !day) return null
|
||||||
|
|||||||
@@ -70,26 +70,53 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
$startDate = $requestedStartDate ?? $today;
|
$todayPeriod = $this->periodRepository->findOneCoveringDate($data, $today);
|
||||||
$todayPeriod = $this->periodRepository->findOneCoveringDate($data, $today);
|
$currentPeriodContract = $todayPeriod?->getContract();
|
||||||
$nature = $requestedContractNature ?? $todayPeriod?->getContractNatureEnum() ?? ContractNature::CDI;
|
$contractChanged = $currentPeriodContract instanceof Contract
|
||||||
$endDate = $requestedEndDate;
|
? $currentPeriodContract->getId() !== $currentContract->getId()
|
||||||
$this->assertPeriodDates($startDate, $endDate, $nature);
|
: true;
|
||||||
|
$isCloseOnlyRequest = !$contractChanged
|
||||||
|
&& null === $requestedStartDate
|
||||||
|
&& null === $requestedContractNature
|
||||||
|
&& null !== $requestedEndDate;
|
||||||
|
|
||||||
if (
|
if ($isCloseOnlyRequest) {
|
||||||
null !== $todayPeriod
|
if (null === $todayPeriod) {
|
||||||
&& null === $todayPeriod->getEndDate()
|
throw new UnprocessableEntityHttpException('No active contract period to close.');
|
||||||
&& $todayPeriod->getStartDate()->format('Y-m-d') === $startDate->format('Y-m-d')
|
}
|
||||||
) {
|
|
||||||
$todayPeriod->setContract($currentContract);
|
$currentNature = $todayPeriod->getContractNatureEnum();
|
||||||
$todayPeriod->setContractNature($nature);
|
$this->assertPeriodDates($todayPeriod->getStartDate(), $requestedEndDate, $currentNature, true);
|
||||||
$todayPeriod->setEndDate($endDate);
|
|
||||||
|
$currentEndDate = $todayPeriod->getEndDate();
|
||||||
|
if (null !== $currentEndDate && $requestedEndDate > $currentEndDate) {
|
||||||
|
throw new UnprocessableEntityHttpException('contractEndDate cannot be increased on current contract.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$todayPeriod->setEndDate($requestedEndDate);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->periodRepository->closeOpenPeriods($data, $startDate->modify('-1 day'));
|
$startDate = $requestedStartDate ?? $today;
|
||||||
|
$nature = $requestedContractNature ?? $todayPeriod?->getContractNatureEnum() ?? ContractNature::CDI;
|
||||||
|
$endDate = $requestedEndDate;
|
||||||
|
$this->assertPeriodDates($startDate, $endDate, $nature);
|
||||||
|
|
||||||
|
if (null !== $todayPeriod) {
|
||||||
|
$currentEndDate = $todayPeriod->getEndDate();
|
||||||
|
if (null === $currentEndDate) {
|
||||||
|
if ($startDate <= $todayPeriod->getStartDate()) {
|
||||||
|
throw new UnprocessableEntityHttpException('contractStartDate must be after current contract start date.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$todayPeriod->setEndDate($startDate->modify('-1 day'));
|
||||||
|
} elseif ($startDate <= $currentEndDate) {
|
||||||
|
throw new UnprocessableEntityHttpException('contractStartDate must be after current contract end date.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->createPeriod($data, $currentContract, $startDate, $endDate, $nature);
|
$this->createPeriod($data, $currentContract, $startDate, $endDate, $nature);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
@@ -179,7 +206,8 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface
|
|||||||
private function assertPeriodDates(
|
private function assertPeriodDates(
|
||||||
DateTimeImmutable $startDate,
|
DateTimeImmutable $startDate,
|
||||||
?DateTimeImmutable $endDate,
|
?DateTimeImmutable $endDate,
|
||||||
ContractNature $nature
|
ContractNature $nature,
|
||||||
|
bool $allowCdiEndDate = false
|
||||||
): void {
|
): void {
|
||||||
if (null !== $endDate && $endDate < $startDate) {
|
if (null !== $endDate && $endDate < $startDate) {
|
||||||
throw new UnprocessableEntityHttpException('contractEndDate cannot be before contractStartDate.');
|
throw new UnprocessableEntityHttpException('contractEndDate cannot be before contractStartDate.');
|
||||||
@@ -189,7 +217,7 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface
|
|||||||
throw new UnprocessableEntityHttpException('contractEndDate is required for CDD and INTERIM.');
|
throw new UnprocessableEntityHttpException('contractEndDate is required for CDD and INTERIM.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ContractNature::CDI === $nature && null !== $endDate) {
|
if (!$allowCdiEndDate && ContractNature::CDI === $nature && null !== $endDate) {
|
||||||
throw new UnprocessableEntityHttpException('contractEndDate must be empty for CDI.');
|
throw new UnprocessableEntityHttpException('contractEndDate must be empty for CDI.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user