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:
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user