feat : agence d'intérim sur les contrats INTERIM + renommage Types d'absence en Types de statut + colonne Absence en Statut
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
- Nouvelle entité InterimAgency (table interim_agencies, API lecture seule) - Sélecteur agence conditionnel dans les formulaires création employé et ajout contrat - Affichage "Intérim (NomAgence)" sur la liste employés et l'historique contrat - Date de fin obligatoire côté frontend pour CDD et INTERIM (aligné backend) - Renommage "Types d'absence" → "Types de statut" (sidebar, page, titre) - Renommage en-tête "Absence" → "Statut" sur les vues jour heures et conducteurs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
- Contracts: `trackingMode` (TIME=hours, PRESENCE=half-days), `weeklyHours`
|
||||
- Contract types: FORFAIT, THIRTY_FIVE_HOURS, THIRTY_NINE_HOURS, INTERIM, CUSTOM
|
||||
- Contract nature (per period): CDI, CDD, INTERIM
|
||||
- **Agence d'intérim** (`InterimAgency` entity, table `interim_agencies`): optionnelle sur `EmployeeContractPeriod` quand nature = INTERIM. Pas de CRUD UI — gérée en BDD. API lecture seule `GET /interim_agencies`. Affichée "Intérim (NomAgence)" sur la liste employés et l'historique contrat.
|
||||
- Employee contract history: `employee_contract_periods`, resolved by `EmployeeContractResolver`
|
||||
- **Planning jours travaillés** (`EmployeeContractPeriod.workDaysHours` : JSON `{iso_day: minutes}`) : obligatoire pour tout contrat TIME **hors 35h/39h/INTERIM** (ex. 4h, 25h, 28h). Somme = `weeklyHours × 60`. Utilisé par `HolidayVirtualHoursResolver` (crédit férié) et `WorkedHoursCreditPolicy` (crédit absence) pour ne créditer que les jours effectivement travaillés. Validation : `EmployeeContractPeriodValidator::assertWorkDaysHours`.
|
||||
- Absences: stored per day (auto-split), AM/PM/full day, clear corresponding hour slots
|
||||
|
||||
@@ -130,6 +130,7 @@ Documents complementaires:
|
||||
- pas de bonus 25%
|
||||
- pas de bonus 50%
|
||||
- pas de total récup
|
||||
- agence d'intérim optionnelle (table `interim_agencies`): affichée sur la fiche employé et le détail contrat sous la forme "Intérim (NomAgence)"
|
||||
|
||||
## 6bis) Heures Conducteurs
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:style="{ gridTemplateColumns: dayGridCols }"
|
||||
>
|
||||
<span>Nom</span>
|
||||
<span class="pl-2">Absence</span>
|
||||
<span class="pl-2">Statut</span>
|
||||
<span class="pl-4">Heure de jour</span>
|
||||
<span class="pl-2">Heure de nuit</span>
|
||||
<span class="pl-2">Heure atelier</span>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
:key="`${item.startDate}-${item.endDate ?? 'open'}-${item.contractId ?? item.contractName}`"
|
||||
class="grid grid-cols-4 border-b border-primary-500 px-6 py-3 text-md font-bold text-primary-500 last:border-b-0 hover:bg-tertiary-500"
|
||||
>
|
||||
<p>{{ contractNatureLabel(item.contractNature) }}</p>
|
||||
<p>{{ item.interimAgencyName ? `${contractNatureLabel(item.contractNature)} (${item.interimAgencyName})` : contractNatureLabel(item.contractNature) }}</p>
|
||||
<p>{{ contractHistoryLabel(item) }}</p>
|
||||
<p>{{ formatDate(item.startDate) }}</p>
|
||||
<p>{{ formatDate(item.endDate) }}</p>
|
||||
@@ -221,6 +221,22 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="createContractForm.contractNature === 'INTERIM'">
|
||||
<label class="text-md font-semibold text-neutral-700" for="create-interim-agency">
|
||||
Agence d'intérim
|
||||
</label>
|
||||
<select
|
||||
id="create-interim-agency"
|
||||
v-model="createContractForm.interimAgencyId"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||
>
|
||||
<option value="">Aucune</option>
|
||||
<option v-for="agency in interimAgencies" :key="agency.id" :value="agency.id">
|
||||
{{ agency.name }}
|
||||
</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>
|
||||
@@ -282,6 +298,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Contract } from '~/services/dto/contract'
|
||||
import type { ContractHistoryItem } from '~/services/dto/employee'
|
||||
import type { InterimAgency } from '~/services/interim-agencies'
|
||||
import WorkDaysHoursInput from '~/components/employees/WorkDaysHoursInput.vue'
|
||||
|
||||
type SuspensionForm = {
|
||||
@@ -310,6 +327,7 @@ type CreateContractForm = {
|
||||
endDate: string
|
||||
isDriver: boolean
|
||||
workDaysHours: Record<number, number> | null
|
||||
interimAgencyId: number | ''
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -351,6 +369,7 @@ const props = defineProps<{
|
||||
onSubmitSuspension: (index: number) => void
|
||||
onAddSuspensionForm: () => void
|
||||
currentContractPeriodId?: number | null
|
||||
interimAgencies: InterimAgency[]
|
||||
}>()
|
||||
|
||||
const drawerTab = ref<'close' | 'suspend'>('close')
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:style="{ gridTemplateColumns: dayGridCols }"
|
||||
>
|
||||
<span>Nom</span>
|
||||
<span class="pl-2">Absence</span>
|
||||
<span class="pl-2">Statut</span>
|
||||
<span class="pl-4">Début matin</span>
|
||||
<span class="pr-2">Fin matin</span>
|
||||
<span class="pl-2">Début après-midi</span>
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 { listInterimAgencies, type InterimAgency } from '~/services/interim-agencies'
|
||||
import { formatNullableYmdToFr, getTodayYmd, shiftYmd } from '~/utils/date'
|
||||
import { contractNatureLabel, formatWorkDaysHoursSummary, isContractNature, requiresContractEndDate, requiresWorkDaysHours, showsContractEndDate } from '~/utils/contract'
|
||||
|
||||
@@ -17,6 +18,7 @@ type SuspensionForm = {
|
||||
export const useEmployeeContract = (employee: Ref<Employee | null>, reloadEmployee: () => Promise<void>) => {
|
||||
const toast = useToast()
|
||||
const contracts = ref<Contract[]>([])
|
||||
const interimAgencies = ref<InterimAgency[]>([])
|
||||
const isContractDrawerOpen = ref(false)
|
||||
const isContractSubmitting = ref(false)
|
||||
const isCreateContractDrawerOpen = ref(false)
|
||||
@@ -46,7 +48,8 @@ export const useEmployeeContract = (employee: Ref<Employee | null>, reloadEmploy
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
isDriver: false,
|
||||
workDaysHours: null as Record<number, number> | null
|
||||
workDaysHours: null as Record<number, number> | null,
|
||||
interimAgencyId: '' as number | ''
|
||||
})
|
||||
|
||||
const createValidationTouched = reactive({
|
||||
@@ -207,6 +210,7 @@ export const useEmployeeContract = (employee: Ref<Employee | null>, reloadEmploy
|
||||
createContractForm.endDate = ''
|
||||
createContractForm.isDriver = false
|
||||
createContractForm.workDaysHours = null
|
||||
createContractForm.interimAgencyId = ''
|
||||
createContractForm.startDate = editableContractPeriod.value?.endDate
|
||||
? (shiftYmd(editableContractPeriod.value.endDate, 1) ?? editableContractPeriod.value.endDate)
|
||||
: getTodayYmd()
|
||||
@@ -283,7 +287,8 @@ export const useEmployeeContract = (employee: Ref<Employee | null>, reloadEmploy
|
||||
contractStartDate: createContractForm.startDate,
|
||||
contractEndDate: createContractForm.endDate || null,
|
||||
isDriverInput: createContractForm.isDriver,
|
||||
workDaysHoursInput: requiresCreateWorkDaysHours.value ? createContractForm.workDaysHours : null
|
||||
workDaysHoursInput: requiresCreateWorkDaysHours.value ? createContractForm.workDaysHours : null,
|
||||
interimAgencyId: createContractForm.contractNature === 'INTERIM' && createContractForm.interimAgencyId !== '' ? Number(createContractForm.interimAgencyId) : null
|
||||
})
|
||||
isCreateContractDrawerOpen.value = false
|
||||
await reloadEmployee()
|
||||
@@ -335,6 +340,16 @@ export const useEmployeeContract = (employee: Ref<Employee | null>, reloadEmploy
|
||||
contracts.value = await listContracts()
|
||||
}
|
||||
|
||||
const loadInterimAgencies = async () => {
|
||||
interimAgencies.value = await listInterimAgencies()
|
||||
}
|
||||
|
||||
watch(() => createContractForm.contractNature, (nature) => {
|
||||
if (nature !== 'INTERIM') {
|
||||
createContractForm.interimAgencyId = ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(showsCreateContractEndDate, (shows) => {
|
||||
if (!shows) {
|
||||
createContractForm.endDate = ''
|
||||
@@ -386,6 +401,8 @@ export const useEmployeeContract = (employee: Ref<Employee | null>, reloadEmploy
|
||||
submitSuspension,
|
||||
addSuspensionForm,
|
||||
currentActiveContractPeriodId,
|
||||
loadContracts
|
||||
interimAgencies,
|
||||
loadContracts,
|
||||
loadInterimAgencies
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export const useEmployeeDetailPage = () => {
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await contract.loadContracts()
|
||||
await Promise.all([contract.loadContracts(), contract.loadInterimAgencies()])
|
||||
await loadEmployee()
|
||||
})
|
||||
|
||||
|
||||
@@ -207,10 +207,10 @@ export const documentationSections: DocSection[] = [
|
||||
},
|
||||
{
|
||||
id: 'gestion-types-absence',
|
||||
title: 'Gestion des types d\'absence',
|
||||
title: 'Gestion des types de statut',
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Les types d\'absence définissent les catégories disponibles lors de la pose d\'une absence.' },
|
||||
{ type: 'paragraph', content: 'Les types de statut définissent les catégories disponibles lors de la pose d\'une absence.' },
|
||||
{ type: 'list', content: 'Code : identifiant court (max 10 caractères), ex: C, M, AT\nLibellé : nom affiché, ex: Congé, Maladie, Accident du travail\nCouleur : code couleur pour le calendrier et la vue jour\nOption "Compté comme travaillé" : si activé, l\'absence crédite des heures en mode TIME' },
|
||||
{ type: 'note', content: 'L\'option "Compté comme travaillé" impacte le calcul des heures supplémentaires. En mode TIME, les minutes sont créditées selon le contrat. En mode PRESENCE, aucun crédit n\'est appliqué.' },
|
||||
],
|
||||
@@ -258,7 +258,7 @@ export const documentationSections: DocSection[] = [
|
||||
requiredLevel: 'admin',
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'La création d\'un employé se fait via le drawer d\'ajout.' },
|
||||
{ type: 'list', content: 'Champs : prénom, nom, site\nNature du contrat : CDI, CDD ou INTERIM\nType de contrat / temps de travail (Forfait, 35h, 39h, etc.)\nDate de début (obligatoire)\nDate de fin (obligatoire pour CDD et INTERIM)' },
|
||||
{ type: 'list', content: 'Champs : prénom, nom, site\nNature du contrat : CDI, CDD ou INTERIM\nAgence d\'intérim (visible uniquement pour INTERIM, optionnel)\nType de contrat / temps de travail (Forfait, 35h, 39h, etc.)\nDate de début (obligatoire)\nDate de fin (obligatoire pour CDD et INTERIM)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
: ''"
|
||||
>
|
||||
<Icon name="mdi:umbrella-beach-outline" size="24"/>
|
||||
<p>Types d'absence</p>
|
||||
<p>Types de statut</p>
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/users"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-between pb-6">
|
||||
<h1 class="text-4xl font-bold text-primary-500">Types d'absence</h1>
|
||||
<h1 class="text-4xl font-bold text-primary-500">Types de statut</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||
@@ -164,7 +164,7 @@ import type { AbsenceType } from '~/services/dto/absence-type'
|
||||
import { createAbsenceType, deleteAbsenceType, listAbsenceTypes, updateAbsenceType } from '~/services/absence-types'
|
||||
|
||||
useHead({
|
||||
title: 'Types d\'absences'
|
||||
title: 'Types de statut'
|
||||
})
|
||||
|
||||
const isDrawerOpen = ref(false)
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
:on-submit-suspension="submitSuspension"
|
||||
:on-add-suspension-form="addSuspensionForm"
|
||||
:current-contract-period-id="currentActiveContractPeriodId"
|
||||
:interim-agencies="interimAgencies"
|
||||
/>
|
||||
<div v-else-if="showLeaveTab && activeTab === 'leave'" class="h-full">
|
||||
<div v-if="isLeaveLoading" class="mt-6 rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
||||
@@ -295,6 +296,7 @@ const {
|
||||
submitSuspension,
|
||||
addSuspensionForm,
|
||||
currentActiveContractPeriodId,
|
||||
interimAgencies,
|
||||
isLeaveLoading,
|
||||
isRttLoading,
|
||||
mileageAllowances,
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
class="absolute inset-0 flex items-center justify-center bg-primary-500 p-4 text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
<div class="w-full rounded-md bg-white/15 p-4 text-sm">
|
||||
<p class="text-base font-semibold">{{ employee.lastName }} {{ employee.firstName }}</p>
|
||||
<p><strong>Type:</strong> {{ contractNatureLabel(employee.currentContractNature) }}</p>
|
||||
<p><strong>Type:</strong> {{ employee.currentInterimAgencyName ? `${contractNatureLabel(employee.currentContractNature)} (${employee.currentInterimAgencyName})` : contractNatureLabel(employee.currentContractNature) }}</p>
|
||||
<p><strong>Temps de travail:</strong> {{ employee.contract?.name ?? '-' }}</p>
|
||||
<p><strong>Site:</strong> {{ employee.site?.name ?? '-' }}</p>
|
||||
<p><strong>Date d'entrée :</strong> {{ employee.entryDate ? employee.entryDate.split('-').reverse().join('/') : '-' }}</p>
|
||||
@@ -147,6 +147,21 @@
|
||||
Le type de contrat est obligatoire.
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="form.contractNature === 'INTERIM'">
|
||||
<label class="text-md font-semibold text-neutral-700" for="interim-agency">
|
||||
Agence d'intérim
|
||||
</label>
|
||||
<select
|
||||
id="interim-agency"
|
||||
v-model="form.interimAgencyId"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-md text-neutral-900"
|
||||
>
|
||||
<option value="">Aucune</option>
|
||||
<option v-for="agency in interimAgencies" :key="agency.id" :value="agency.id">
|
||||
{{ agency.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-md font-semibold text-neutral-700" for="contract">
|
||||
Temps de travail <span class="text-red-600">*</span>
|
||||
@@ -191,7 +206,7 @@
|
||||
:class="contractEndDateFieldClass"
|
||||
/>
|
||||
<p v-if="showContractEndDateError" class="mt-1 text-sm text-red-600">
|
||||
La date de fin est obligatoire pour un CDD.
|
||||
La date de fin est obligatoire pour un CDD ou un Intérim.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-md border border-neutral-200 bg-neutral-50 p-3">
|
||||
@@ -246,6 +261,7 @@ import type {Site} from '~/services/dto/site'
|
||||
import {listContracts} from '~/services/contracts'
|
||||
import {createEmployee, deleteEmployee, listEmployees, updateEmployee} from '~/services/employees'
|
||||
import {listSites} from '~/services/sites'
|
||||
import {listInterimAgencies, type InterimAgency} from '~/services/interim-agencies'
|
||||
import SiteFilterSelector from '~/components/SiteFilterSelector.vue'
|
||||
import SalaryRecapDrawer from '~/components/SalaryRecapDrawer.vue'
|
||||
import {contractNatureLabel, isContractNature, requiresContractEndDate, showsContractEndDate} from '~/utils/contract'
|
||||
@@ -269,6 +285,7 @@ const drawerTitle = computed(() =>
|
||||
const employees = ref<Employee[]>([])
|
||||
const sites = ref<Site[]>([])
|
||||
const contracts = ref<Contract[]>([])
|
||||
const interimAgencies = ref<InterimAgency[]>([])
|
||||
const employeeFilter = ref('')
|
||||
const contractStatusFilter = ref<'active' | 'inactive' | 'all'>('active')
|
||||
const selectedSiteIds = ref<number[]>([])
|
||||
@@ -300,7 +317,8 @@ const form = reactive({
|
||||
contractStartDate: '',
|
||||
contractEndDate: '',
|
||||
isDriver: false,
|
||||
workDaysHours: null as Record<number, number> | null
|
||||
workDaysHours: null as Record<number, number> | null,
|
||||
interimAgencyId: '' as number | ''
|
||||
})
|
||||
|
||||
const validationTouched = reactive({
|
||||
@@ -451,8 +469,12 @@ const loadContracts = async () => {
|
||||
contracts.value = await listContracts()
|
||||
}
|
||||
|
||||
const loadInterimAgencies = async () => {
|
||||
interimAgencies.value = await listInterimAgencies()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadEmployees(), loadSites(), loadContracts()])
|
||||
await Promise.all([loadEmployees(), loadSites(), loadContracts(), loadInterimAgencies()])
|
||||
if (form.contractStartDate === '') {
|
||||
form.contractStartDate = new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
@@ -503,7 +525,8 @@ const handleSubmit = async () => {
|
||||
contractStartDate: form.contractStartDate,
|
||||
contractEndDate: form.contractEndDate || null,
|
||||
isDriverInput: form.isDriver,
|
||||
workDaysHoursInput: requiresSchedule.value ? form.workDaysHours : null
|
||||
workDaysHoursInput: requiresSchedule.value ? form.workDaysHours : null,
|
||||
interimAgencyId: form.contractNature === 'INTERIM' && form.interimAgencyId !== '' ? Number(form.interimAgencyId) : null
|
||||
})
|
||||
}
|
||||
|
||||
@@ -516,6 +539,7 @@ const handleSubmit = async () => {
|
||||
form.contractEndDate = ''
|
||||
form.isDriver = false
|
||||
form.workDaysHours = null
|
||||
form.interimAgencyId = ''
|
||||
editingEmployee.value = null
|
||||
isDrawerOpen.value = false
|
||||
await loadEmployees()
|
||||
@@ -542,6 +566,12 @@ watch(showsContractEndDateComputed, (shows) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => form.contractNature, (nature) => {
|
||||
if (nature !== 'INTERIM') {
|
||||
form.interimAgencyId = ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(requiresSchedule, (required) => {
|
||||
if (!required) {
|
||||
form.workDaysHours = null
|
||||
@@ -567,6 +597,7 @@ const openCreate = () => {
|
||||
form.contractEndDate = ''
|
||||
form.isDriver = false
|
||||
form.workDaysHours = null
|
||||
form.interimAgencyId = ''
|
||||
isDrawerOpen.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ export type ContractHistoryItem = {
|
||||
suspensions?: ContractSuspension[]
|
||||
isDriver?: boolean
|
||||
workDaysHours?: Record<number, number> | null
|
||||
interimAgencyId?: number | null
|
||||
interimAgencyName?: string | null
|
||||
}
|
||||
|
||||
export type Employee = {
|
||||
@@ -37,4 +39,6 @@ export type Employee = {
|
||||
displayOrder?: number
|
||||
entryDate?: string | null
|
||||
currentSuspensions?: ContractSuspension[]
|
||||
currentInterimAgencyId?: number | null
|
||||
currentInterimAgencyName?: string | null
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export const createEmployee = async (payload: {
|
||||
contractEndDate?: string | null
|
||||
isDriverInput?: boolean
|
||||
workDaysHoursInput?: Record<number, number> | null
|
||||
interimAgencyId?: number | null
|
||||
}) => {
|
||||
const api = useApi()
|
||||
return api.post<Employee>('/employees', {
|
||||
@@ -47,7 +48,8 @@ export const createEmployee = async (payload: {
|
||||
contractStartDate: payload.contractStartDate,
|
||||
contractEndDate: payload.contractEndDate ?? null,
|
||||
isDriverInput: payload.isDriverInput ?? false,
|
||||
workDaysHoursInput: payload.workDaysHoursInput ?? null
|
||||
workDaysHoursInput: payload.workDaysHoursInput ?? null,
|
||||
interimAgencyId: payload.interimAgencyId ?? null
|
||||
}, {
|
||||
toastSuccessKey: 'success.employee.create',
|
||||
toastErrorKey: 'errors.employee.create'
|
||||
@@ -69,6 +71,7 @@ export const updateEmployee = async (
|
||||
displayOrder?: number
|
||||
isDriverInput?: boolean
|
||||
workDaysHoursInput?: Record<number, number> | null
|
||||
interimAgencyId?: number | null
|
||||
}
|
||||
) => {
|
||||
const api = useApi()
|
||||
@@ -103,6 +106,9 @@ export const updateEmployee = async (
|
||||
if (payload.workDaysHoursInput !== undefined) {
|
||||
body.workDaysHoursInput = payload.workDaysHoursInput
|
||||
}
|
||||
if (payload.interimAgencyId !== undefined) {
|
||||
body.interimAgencyId = payload.interimAgencyId
|
||||
}
|
||||
|
||||
return api.patch<Employee>(`/employees/${id}`, body, {
|
||||
toastSuccessKey: 'success.employee.update',
|
||||
|
||||
16
frontend/services/interim-agencies.ts
Normal file
16
frontend/services/interim-agencies.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { extractItems } from '~/utils/api'
|
||||
|
||||
export type InterimAgency = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export const listInterimAgencies = async (): Promise<InterimAgency[]> => {
|
||||
const api = useApi()
|
||||
const data = await api.get<InterimAgency[] | { 'hydra:member'?: InterimAgency[] }>(
|
||||
'/interim_agencies',
|
||||
{},
|
||||
{ toast: false }
|
||||
)
|
||||
return extractItems<InterimAgency>(data)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const showsContractEndDate = (nature: ContractNature) => {
|
||||
}
|
||||
|
||||
export const requiresContractEndDate = (nature: ContractNature) => {
|
||||
return nature === 'CDD'
|
||||
return nature === 'CDD' || nature === 'INTERIM'
|
||||
}
|
||||
|
||||
export const isContractNature = (value: string): value is ContractNature => {
|
||||
|
||||
32
migrations/Version20260417120000.php
Normal file
32
migrations/Version20260417120000.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260417120000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create interim_agencies table and add interim_agency_id to employee_contract_periods';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE interim_agencies (id SERIAL PRIMARY KEY, name VARCHAR(150) NOT NULL UNIQUE)');
|
||||
$this->addSql('ALTER TABLE employee_contract_periods ADD interim_agency_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE employee_contract_periods ADD CONSTRAINT fk_ecp_interim_agency FOREIGN KEY (interim_agency_id) REFERENCES interim_agencies (id) ON DELETE SET NULL');
|
||||
$this->addSql('CREATE INDEX idx_ecp_interim_agency ON employee_contract_periods (interim_agency_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE employee_contract_periods DROP CONSTRAINT IF EXISTS fk_ecp_interim_agency');
|
||||
$this->addSql('DROP INDEX IF EXISTS idx_ecp_interim_agency');
|
||||
$this->addSql('ALTER TABLE employee_contract_periods DROP COLUMN interim_agency_id');
|
||||
$this->addSql('DROP TABLE interim_agencies');
|
||||
}
|
||||
}
|
||||
@@ -34,5 +34,9 @@ final class ContractHistoryItem
|
||||
*/
|
||||
#[Groups(['employee:read'])]
|
||||
public ?array $workDaysHours = null,
|
||||
#[Groups(['employee:read'])]
|
||||
public ?int $interimAgencyId = null,
|
||||
#[Groups(['employee:read'])]
|
||||
public ?string $interimAgencyName = null,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,9 @@ class Employee
|
||||
#[Groups(['employee:write'])]
|
||||
private ?array $workDaysHoursInput = null;
|
||||
|
||||
#[Groups(['employee:write'])]
|
||||
private ?int $interimAgencyId = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
@@ -295,6 +298,30 @@ class Employee
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getInterimAgencyId(): ?int
|
||||
{
|
||||
return $this->interimAgencyId;
|
||||
}
|
||||
|
||||
public function setInterimAgencyId(?int $interimAgencyId): self
|
||||
{
|
||||
$this->interimAgencyId = $interimAgencyId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Groups(['employee:read'])]
|
||||
public function getCurrentInterimAgencyId(): ?int
|
||||
{
|
||||
return $this->resolveCurrentContractPeriod()?->getInterimAgency()?->getId();
|
||||
}
|
||||
|
||||
#[Groups(['employee:read'])]
|
||||
public function getCurrentInterimAgencyName(): ?string
|
||||
{
|
||||
return $this->resolveCurrentContractPeriod()?->getInterimAgency()?->getName();
|
||||
}
|
||||
|
||||
#[Groups(['employee:read'])]
|
||||
public function getHasActiveContract(): bool
|
||||
{
|
||||
@@ -393,6 +420,8 @@ class Employee
|
||||
suspensions: $suspensionData,
|
||||
isDriver: $period->getIsDriver(),
|
||||
workDaysHours: $period->getWorkDaysHours(),
|
||||
interimAgencyId: $period->getInterimAgency()?->getId(),
|
||||
interimAgencyName: $period->getInterimAgency()?->getName(),
|
||||
);
|
||||
},
|
||||
$periods
|
||||
|
||||
@@ -55,6 +55,10 @@ class EmployeeContractPeriod
|
||||
#[ORM\Column(type: 'json', nullable: true)]
|
||||
private ?array $workDaysHours = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: InterimAgency::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?InterimAgency $interimAgency = null;
|
||||
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $comment = null;
|
||||
|
||||
@@ -204,6 +208,18 @@ class EmployeeContractPeriod
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getInterimAgency(): ?InterimAgency
|
||||
{
|
||||
return $this->interimAgency;
|
||||
}
|
||||
|
||||
public function setInterimAgency(?InterimAgency $interimAgency): self
|
||||
{
|
||||
$this->interimAgency = $interimAgency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ContractSuspension>
|
||||
*/
|
||||
|
||||
51
src/Entity/InterimAgency.php
Normal file
51
src/Entity/InterimAgency.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(),
|
||||
],
|
||||
normalizationContext: ['groups' => ['interim_agency:read']],
|
||||
paginationEnabled: false,
|
||||
security: "is_granted('ROLE_USER')",
|
||||
order: ['name' => 'ASC'],
|
||||
)]
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'interim_agencies')]
|
||||
class InterimAgency
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[Groups(['interim_agency:read', 'employee:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 150, unique: true)]
|
||||
#[Groups(['interim_agency:read', 'employee:read'])]
|
||||
private string $name = '';
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ final readonly class EmployeeContractChangeRequest
|
||||
public ?string $contractComment,
|
||||
public ?bool $isDriver = null,
|
||||
public ?array $workDaysHours = null,
|
||||
public ?int $interimAgencyId = null,
|
||||
) {}
|
||||
|
||||
public function hasPeriodChangeRequest(): bool
|
||||
|
||||
@@ -21,6 +21,7 @@ final class EmployeeContractChangeRequestFactory
|
||||
contractComment: $employee->getContractComment(),
|
||||
isDriver: $employee->getIsDriverInput(),
|
||||
workDaysHours: $employee->getWorkDaysHoursInput(),
|
||||
interimAgencyId: $employee->getInterimAgencyId(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Service\Contracts;
|
||||
use App\Entity\Contract;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\EmployeeContractPeriod;
|
||||
use App\Entity\InterimAgency;
|
||||
use App\Enum\ContractNature;
|
||||
use DateTimeImmutable;
|
||||
|
||||
@@ -23,6 +24,7 @@ final class EmployeeContractPeriodBuilder
|
||||
ContractNature $nature,
|
||||
bool $isDriver = false,
|
||||
?array $workDaysHours = null,
|
||||
?InterimAgency $interimAgency = null,
|
||||
): EmployeeContractPeriod {
|
||||
return new EmployeeContractPeriod()
|
||||
->setEmployee($employee)
|
||||
@@ -32,6 +34,7 @@ final class EmployeeContractPeriodBuilder
|
||||
->setContractNature($nature)
|
||||
->setIsDriver($isDriver)
|
||||
->setWorkDaysHours($workDaysHours)
|
||||
->setInterimAgency($interimAgency)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Service\Contracts;
|
||||
use App\Entity\Contract;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\EmployeeContractPeriod;
|
||||
use App\Entity\InterimAgency;
|
||||
use App\Enum\ContractNature;
|
||||
use App\Repository\EmployeeContractPeriodRepository;
|
||||
use DateTimeImmutable;
|
||||
@@ -30,6 +31,7 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
|
||||
ContractNature $nature,
|
||||
bool $isDriver = false,
|
||||
?array $workDaysHours = null,
|
||||
?int $interimAgencyId = null,
|
||||
): void {
|
||||
$this->periodValidator->assertPeriodDates($startDate, $endDate, $nature);
|
||||
$this->periodValidator->assertWorkDaysHours($contract, $nature, $workDaysHours);
|
||||
@@ -39,7 +41,8 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
|
||||
return;
|
||||
}
|
||||
|
||||
$this->persistNewPeriod($employee, $contract, $startDate, $endDate, $nature, $isDriver, $workDaysHours);
|
||||
$interimAgency = $this->resolveInterimAgency($interimAgencyId);
|
||||
$this->persistNewPeriod($employee, $contract, $startDate, $endDate, $nature, $isDriver, $workDaysHours, $interimAgency);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
@@ -78,6 +81,7 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
|
||||
?EmployeeContractPeriod $todayPeriod,
|
||||
bool $isDriver = false,
|
||||
?array $workDaysHours = null,
|
||||
?int $interimAgencyId = null,
|
||||
): void {
|
||||
$this->periodValidator->assertPeriodDates($startDate, $endDate, $nature);
|
||||
$this->periodValidator->assertWorkDaysHours($contract, $nature, $workDaysHours);
|
||||
@@ -90,7 +94,8 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
|
||||
}
|
||||
}
|
||||
|
||||
$this->persistNewPeriod($employee, $contract, $startDate, $endDate, $nature, $isDriver, $workDaysHours);
|
||||
$interimAgency = $this->resolveInterimAgency($interimAgencyId);
|
||||
$this->persistNewPeriod($employee, $contract, $startDate, $endDate, $nature, $isDriver, $workDaysHours, $interimAgency);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
@@ -105,8 +110,23 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
|
||||
ContractNature $nature,
|
||||
bool $isDriver = false,
|
||||
?array $workDaysHours = null,
|
||||
?InterimAgency $interimAgency = null,
|
||||
): void {
|
||||
$period = $this->periodBuilder->build($employee, $contract, $startDate, $endDate, $nature, $isDriver, $workDaysHours);
|
||||
$period = $this->periodBuilder->build($employee, $contract, $startDate, $endDate, $nature, $isDriver, $workDaysHours, $interimAgency);
|
||||
$this->entityManager->persist($period);
|
||||
}
|
||||
|
||||
private function resolveInterimAgency(?int $id): ?InterimAgency
|
||||
{
|
||||
if (null === $id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$agency = $this->entityManager->find(InterimAgency::class, $id);
|
||||
if (null === $agency) {
|
||||
throw new UnprocessableEntityHttpException(sprintf('Interim agency with id %d not found.', $id));
|
||||
}
|
||||
|
||||
return $agency;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ interface EmployeeContractPeriodManagerInterface
|
||||
ContractNature $nature,
|
||||
bool $isDriver = false,
|
||||
?array $workDaysHours = null,
|
||||
?int $interimAgencyId = null,
|
||||
): void;
|
||||
|
||||
public function closeCurrentPeriod(
|
||||
@@ -45,5 +46,6 @@ interface EmployeeContractPeriodManagerInterface
|
||||
?EmployeeContractPeriod $todayPeriod,
|
||||
bool $isDriver = false,
|
||||
?array $workDaysHours = null,
|
||||
?int $interimAgencyId = null,
|
||||
): void;
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface
|
||||
nature: $nature,
|
||||
isDriver: $changeRequest->isDriver ?? false,
|
||||
workDaysHours: $changeRequest->workDaysHours,
|
||||
interimAgencyId: $changeRequest->interimAgencyId,
|
||||
);
|
||||
|
||||
$data->setEntryDate($startDate);
|
||||
@@ -140,6 +141,7 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface
|
||||
todayPeriod: $effectivePeriod,
|
||||
isDriver: $changeRequest->isDriver ?? false,
|
||||
workDaysHours: $changeRequest->workDaysHours,
|
||||
interimAgencyId: $changeRequest->interimAgencyId,
|
||||
);
|
||||
|
||||
return $result;
|
||||
|
||||
Reference in New Issue
Block a user