feat : ajout d'un champ commentaire sur les contrats + correction de plusieurs bugs
This commit is contained in:
@@ -5,7 +5,8 @@
|
|||||||
"Bash(npx nuxi:*)",
|
"Bash(npx nuxi:*)",
|
||||||
"Bash(php:*)",
|
"Bash(php:*)",
|
||||||
"Bash(docker compose:*)",
|
"Bash(docker compose:*)",
|
||||||
"Bash(make test:*)"
|
"Bash(make test:*)",
|
||||||
|
"Bash(grep:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,10 +43,17 @@ Documents complementaires:
|
|||||||
- Saisie par salarié et par date:
|
- Saisie par salarié et par date:
|
||||||
- matin / après-midi / soir
|
- matin / après-midi / soir
|
||||||
- pour `PRESENCE`: demi-journées matin/après-midi
|
- pour `PRESENCE`: demi-journées matin/après-midi
|
||||||
|
- Sélecteur de temps:
|
||||||
|
- créneaux de 15 minutes uniquement (00:00, 00:15, ..., 23:45)
|
||||||
|
- saisie libre possible mais valeur vidée au blur si hors options
|
||||||
- Calculs affichés:
|
- Calculs affichés:
|
||||||
- `Jour`, `Nuit`, `Total`
|
- `Jour`, `Nuit`, `Total`
|
||||||
- Heures de nuit:
|
- Heures de nuit:
|
||||||
- fenêtres `00:00-06:00` et `21:00-24:00`
|
- fenêtres `00:00-06:00` et `21:00-24:00`
|
||||||
|
- Date de modification (`updatedAt`):
|
||||||
|
- mise à jour uniquement quand un employé (`ROLE_SELF`) modifie ses propres heures
|
||||||
|
- non mise à jour lors de modifications admin ou chef de site
|
||||||
|
- affichée sous le nom de l'employé (visible admin uniquement)
|
||||||
|
|
||||||
## 4) Absences
|
## 4) Absences
|
||||||
|
|
||||||
@@ -57,8 +64,9 @@ Documents complementaires:
|
|||||||
- Colonne absence (vue jour):
|
- Colonne absence (vue jour):
|
||||||
- affiche le libellé
|
- affiche le libellé
|
||||||
- fond coloré selon le type d'absence
|
- fond coloré selon le type d'absence
|
||||||
- Si plusieurs absences de couleurs différentes sur le même jour:
|
- Calendrier congés: fond coloré selon la couleur du type d'absence (`AbsenceType.color`)
|
||||||
- fallback rouge
|
- demi-journée: dégradé diagonal
|
||||||
|
- journée complète: fond plein
|
||||||
|
|
||||||
### Effet absence sur les heures
|
### Effet absence sur les heures
|
||||||
|
|
||||||
@@ -73,7 +81,7 @@ Documents complementaires:
|
|||||||
|
|
||||||
- Si `countAsWorkedHours = true`:
|
- Si `countAsWorkedHours = true`:
|
||||||
- `TIME`: crédit de minutes selon contrat actif du jour
|
- `TIME`: crédit de minutes selon contrat actif du jour
|
||||||
- `PRESENCE`: crédit d'unités (0.5 / demi-journée)
|
- `PRESENCE` (forfait): aucun crédit de présence (seules les checkboxes cochées comptent)
|
||||||
|
|
||||||
## 5) Validations des lignes d'heures
|
## 5) Validations des lignes d'heures
|
||||||
|
|
||||||
@@ -112,6 +120,7 @@ Documents complementaires:
|
|||||||
## 7) Fériés
|
## 7) Fériés
|
||||||
|
|
||||||
- Les jours fériés sont identifiés et affichés
|
- Les jours fériés sont identifiés et affichés
|
||||||
|
- Onglet congés: jours fériés affichés sur le calendrier avec fond `rgb(179, 229, 252)` et nom au survol
|
||||||
- Règle courante:
|
- Règle courante:
|
||||||
- absences bloquées sur jour férié
|
- absences bloquées sur jour férié
|
||||||
- saisie d'heures autorisée
|
- saisie d'heures autorisée
|
||||||
|
|||||||
@@ -86,6 +86,19 @@
|
|||||||
<p v-if="showContractEndDateError" class="mt-1 text-sm text-red-600">La date de fin est obligatoire.</p>
|
<p v-if="showContractEndDateError" class="mt-1 text-sm text-red-600">La date de fin est obligatoire.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-md font-semibold text-neutral-700" for="contract-comment">
|
||||||
|
Commentaire
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="contract-comment"
|
||||||
|
v-model="contractForm.comment"
|
||||||
|
rows="3"
|
||||||
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||||
|
placeholder="Motif de la clôture..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rounded-md border border-neutral-200 bg-neutral-50 p-3">
|
<div class="rounded-md border border-neutral-200 bg-neutral-50 p-3">
|
||||||
<label class="inline-flex items-center gap-2 text-md font-semibold text-neutral-700" for="contract-paid-leave-settled">
|
<label class="inline-flex items-center gap-2 text-md font-semibold text-neutral-700" for="contract-paid-leave-settled">
|
||||||
<input
|
<input
|
||||||
@@ -191,6 +204,7 @@ type ContractForm = {
|
|||||||
startDate: string
|
startDate: string
|
||||||
endDate: string
|
endDate: string
|
||||||
paidLeaveSettled: boolean
|
paidLeaveSettled: boolean
|
||||||
|
comment: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateContractForm = {
|
type CreateContractForm = {
|
||||||
|
|||||||
@@ -41,9 +41,9 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
:class="getDayClass(day.leave)"
|
:class="getDayClass(day)"
|
||||||
:style="getDayStyle(day.leave)"
|
:style="getDayStyle(day)"
|
||||||
:title="getDayTitle(day.leave)"
|
:title="getDayTitle(day)"
|
||||||
>
|
>
|
||||||
{{ getDayText(day) }}
|
{{ getDayText(day) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -65,13 +65,13 @@ type DayLeaveState = {
|
|||||||
am: boolean
|
am: boolean
|
||||||
pm: boolean
|
pm: boolean
|
||||||
labels: string[]
|
labels: string[]
|
||||||
hasCongeTypeC: boolean
|
colors: string[]
|
||||||
hasOtherTypes: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
absences: Absence[]
|
absences: Absence[]
|
||||||
summary: EmployeeLeaveSummary | null
|
summary: EmployeeLeaveSummary | null
|
||||||
|
publicHolidays: Record<string, string>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const monthLabels = [
|
const monthLabels = [
|
||||||
@@ -124,8 +124,7 @@ const dayLeaveMap = computed(() => {
|
|||||||
am: false,
|
am: false,
|
||||||
pm: false,
|
pm: false,
|
||||||
labels: [] as string[],
|
labels: [] as string[],
|
||||||
hasCongeTypeC: false,
|
colors: [] as string[]
|
||||||
hasOtherTypes: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStart = ymd === startYmd
|
const isStart = ymd === startYmd
|
||||||
@@ -150,18 +149,21 @@ const dayLeaveMap = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const typeLabel = absence.type?.label ?? absence.type?.code ?? 'Absence'
|
const typeLabel = absence.type?.label ?? absence.type?.code ?? 'Absence'
|
||||||
const typeCode = (absence.type?.code ?? '').toUpperCase()
|
const typeColor = absence.type?.color ?? '#222783'
|
||||||
const halfSuffix = am && !pm ? ' (Matin)' : (!am && pm ? ' (Apres-midi)' : '')
|
const halfSuffix = am && !pm ? ' (Matin)' : (!am && pm ? ' (Apres-midi)' : '')
|
||||||
const hoverLabel = `${typeLabel}${halfSuffix}`
|
const hoverLabel = `${typeLabel}${halfSuffix}`
|
||||||
|
|
||||||
|
const colors = existing.colors.includes(typeColor)
|
||||||
|
? existing.colors
|
||||||
|
: [...existing.colors, typeColor]
|
||||||
|
|
||||||
map.set(ymd, {
|
map.set(ymd, {
|
||||||
am: existing.am || am,
|
am: existing.am || am,
|
||||||
pm: existing.pm || pm,
|
pm: existing.pm || pm,
|
||||||
labels: existing.labels.includes(hoverLabel)
|
labels: existing.labels.includes(hoverLabel)
|
||||||
? existing.labels
|
? existing.labels
|
||||||
: [...existing.labels, hoverLabel],
|
: [...existing.labels, hoverLabel],
|
||||||
hasCongeTypeC: existing.hasCongeTypeC || typeCode === 'C',
|
colors
|
||||||
hasOtherTypes: existing.hasOtherTypes || typeCode !== 'C'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +182,7 @@ const months = computed(() => {
|
|||||||
const daysInMonth = new Date(monthYear, monthIndex + 1, 0).getDate()
|
const daysInMonth = new Date(monthYear, monthIndex + 1, 0).getDate()
|
||||||
const mondayBasedFirstDay = (first.getDay() + 6) % 7
|
const mondayBasedFirstDay = (first.getDay() + 6) % 7
|
||||||
|
|
||||||
const cells: Array<{ ymd: string; label: string; leave: DayLeaveState | null } | null> = []
|
const cells: Array<{ ymd: string; label: string; leave: DayLeaveState | null; isHoliday: boolean } | null> = []
|
||||||
|
|
||||||
for (let i = 0; i < mondayBasedFirstDay; i += 1) {
|
for (let i = 0; i < mondayBasedFirstDay; i += 1) {
|
||||||
cells.push(null)
|
cells.push(null)
|
||||||
@@ -191,7 +193,8 @@ const months = computed(() => {
|
|||||||
cells.push({
|
cells.push({
|
||||||
ymd,
|
ymd,
|
||||||
label: String(day),
|
label: String(day),
|
||||||
leave: dayLeaveMap.value.get(ymd) ?? null
|
leave: dayLeaveMap.value.get(ymd) ?? null,
|
||||||
|
isHoliday: ymd in props.publicHolidays
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,37 +209,37 @@ const months = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const getDayClass = (leave: DayLeaveState | null) => {
|
const getDayClass = (day: { leave: DayLeaveState | null; isHoliday: boolean }) => {
|
||||||
if (!leave) return 'text-primary-500'
|
if (day.leave) {
|
||||||
if (leave.am && leave.pm) {
|
return 'rounded font-semibold text-white'
|
||||||
return leave.hasOtherTypes
|
|
||||||
? 'bg-red-600 text-white rounded font-semibold'
|
|
||||||
: 'bg-primary-500 text-white rounded font-semibold'
|
|
||||||
}
|
}
|
||||||
return 'rounded text-primary-700 font-semibold text-white'
|
if (day.isHoliday) return 'text-primary-500 rounded font-semibold'
|
||||||
|
return 'text-primary-500'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDayStyle = (leave: DayLeaveState | null) => {
|
const getDayStyle = (day: { leave: DayLeaveState | null; isHoliday: boolean }) => {
|
||||||
if (!leave || (leave.am && leave.pm)) return undefined
|
if (day.leave) {
|
||||||
|
const color = day.leave.colors[0] ?? '#222783'
|
||||||
const color = leave.hasOtherTypes ? '#dc2626' : '#222783'
|
if (day.leave.am && day.leave.pm) {
|
||||||
const backgroundImage = leave.am
|
return { backgroundColor: color }
|
||||||
? `linear-gradient(135deg, ${color} 0 50%, transparent 50% 100%)`
|
}
|
||||||
: `linear-gradient(135deg, transparent 0 50%, ${color} 50% 100%)`
|
const backgroundImage = day.leave.am
|
||||||
|
? `linear-gradient(135deg, ${color} 0 50%, transparent 50% 100%)`
|
||||||
return {
|
: `linear-gradient(135deg, transparent 0 50%, ${color} 50% 100%)`
|
||||||
backgroundImage,
|
return { backgroundImage, backgroundColor: 'transparent' }
|
||||||
backgroundColor: 'transparent'
|
|
||||||
}
|
}
|
||||||
|
if (day.isHoliday) return { backgroundColor: 'rgb(179, 229, 252)' }
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDayText = (day: { label: string; leave: DayLeaveState | null }) => {
|
const getDayText = (day: { label: string; leave: DayLeaveState | null }) => {
|
||||||
return day.label
|
return day.label
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDayTitle = (leave: DayLeaveState | null) => {
|
const getDayTitle = (day: { leave: DayLeaveState | null; isHoliday: boolean; ymd: string }) => {
|
||||||
if (!leave || leave.labels.length === 0) return ''
|
if (day.leave && day.leave.labels.length > 0) return day.leave.labels.join(' / ')
|
||||||
return leave.labels.join(' / ')
|
if (day.isHoliday) return props.publicHolidays[day.ymd] ?? 'Jour férié'
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatCount = (value: number | null | undefined) => {
|
const formatCount = (value: number | null | undefined) => {
|
||||||
|
|||||||
@@ -63,6 +63,9 @@
|
|||||||
<Icon name="mdi:check"/>
|
<Icon name="mdi:check"/>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
<p v-if="isAdmin && getRowUpdatedAt(employee.id)" class="text-neutral-400 text-xs truncate">
|
||||||
|
Modifié le {{ getRowUpdatedAt(employee.id) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-2 min-w-0 self-stretch flex flex-col gap-1 justify-between py-0.5">
|
<div class="pl-2 min-w-0 self-stretch flex flex-col gap-1 justify-between py-0.5">
|
||||||
<p
|
<p
|
||||||
@@ -216,6 +219,7 @@ const props = defineProps<{
|
|||||||
getRowMetrics: (employeeId: number) => { dayMinutes: number; nightMinutes: number; totalMinutes: number }
|
getRowMetrics: (employeeId: number) => { dayMinutes: number; nightMinutes: number; totalMinutes: number }
|
||||||
getRowAbsenceLabel: (employeeId: number) => string
|
getRowAbsenceLabel: (employeeId: number) => string
|
||||||
getRowAbsenceStyle: (employeeId: number) => { backgroundColor: string } | undefined
|
getRowAbsenceStyle: (employeeId: number) => { backgroundColor: string } | undefined
|
||||||
|
getRowUpdatedAt: (employeeId: number) => string
|
||||||
getPresenceDayValue: (employeeId: number) => string
|
getPresenceDayValue: (employeeId: number) => string
|
||||||
onAbsenceClick: (employeeId: number) => void
|
onAbsenceClick: (employeeId: number) => void
|
||||||
formatMinutes: (minutes: number) => string
|
formatMinutes: (minutes: number) => string
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ export type HourRow = {
|
|||||||
isPresentAfternoon: boolean
|
isPresentAfternoon: boolean
|
||||||
isSiteValid: boolean
|
isSiteValid: boolean
|
||||||
isValid: boolean
|
isValid: boolean
|
||||||
|
updatedAt: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,8 +167,9 @@ const closeMenu = () => {
|
|||||||
|
|
||||||
const commitInput = () => {
|
const commitInput = () => {
|
||||||
const normalized = normalizeTypedTime(inputValue.value)
|
const normalized = normalizeTypedTime(inputValue.value)
|
||||||
if (normalized === null) {
|
if (normalized === null || (normalized !== '' && !timeSlots.value.includes(normalized))) {
|
||||||
inputValue.value = props.modelValue
|
emit('update:modelValue', '')
|
||||||
|
inputValue.value = ''
|
||||||
closeMenu()
|
closeMenu()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { listContracts } from '~/services/contracts'
|
|||||||
import { getEmployeeLeaveSummary } from '~/services/employee-leave-summary'
|
import { getEmployeeLeaveSummary } from '~/services/employee-leave-summary'
|
||||||
import { getEmployeeRttSummary } from '~/services/employee-rtt-summary'
|
import { getEmployeeRttSummary } from '~/services/employee-rtt-summary'
|
||||||
import { getEmployee, updateEmployee } from '~/services/employees'
|
import { getEmployee, updateEmployee } from '~/services/employees'
|
||||||
|
import { listPublicHolidays } from '~/services/public-holidays'
|
||||||
import { formatNullableYmdToFr, getTodayYmd, shiftYmd } from '~/utils/date'
|
import { formatNullableYmdToFr, getTodayYmd, shiftYmd } from '~/utils/date'
|
||||||
import { contractNatureLabel, isContractNature, requiresContractEndDate } from '~/utils/contract'
|
import { contractNatureLabel, isContractNature, requiresContractEndDate } from '~/utils/contract'
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export const useEmployeeDetailPage = () => {
|
|||||||
const employeeAbsences = ref<Absence[]>([])
|
const employeeAbsences = ref<Absence[]>([])
|
||||||
const leaveSummary = ref<EmployeeLeaveSummary | null>(null)
|
const leaveSummary = ref<EmployeeLeaveSummary | null>(null)
|
||||||
const rttSummary = ref<EmployeeRttSummary | null>(null)
|
const rttSummary = ref<EmployeeRttSummary | null>(null)
|
||||||
|
const publicHolidays = ref<Record<string, string>>({})
|
||||||
const isContractDrawerOpen = ref(false)
|
const isContractDrawerOpen = ref(false)
|
||||||
const isContractSubmitting = ref(false)
|
const isContractSubmitting = ref(false)
|
||||||
const isCreateContractDrawerOpen = ref(false)
|
const isCreateContractDrawerOpen = ref(false)
|
||||||
@@ -34,7 +36,8 @@ export const useEmployeeDetailPage = () => {
|
|||||||
contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM',
|
contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
paidLeaveSettled: false
|
paidLeaveSettled: false,
|
||||||
|
comment: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const validationTouched = reactive({
|
const validationTouched = reactive({
|
||||||
@@ -138,6 +141,7 @@ export const useEmployeeDetailPage = () => {
|
|||||||
contractForm.startDate = active.startDate
|
contractForm.startDate = active.startDate
|
||||||
contractForm.endDate = getTodayYmd()
|
contractForm.endDate = getTodayYmd()
|
||||||
contractForm.paidLeaveSettled = false
|
contractForm.paidLeaveSettled = false
|
||||||
|
contractForm.comment = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const openCloseContractDrawer = () => {
|
const openCloseContractDrawer = () => {
|
||||||
@@ -198,7 +202,10 @@ export const useEmployeeDetailPage = () => {
|
|||||||
const to = isForfait
|
const to = isForfait
|
||||||
? `${leaveYear}-12-31`
|
? `${leaveYear}-12-31`
|
||||||
: `${leaveYear}-05-31`
|
: `${leaveYear}-05-31`
|
||||||
const [absences, summary, rtt] = await Promise.all([
|
const holidayYears = isForfait
|
||||||
|
? [leaveYear]
|
||||||
|
: [leaveYear - 1, leaveYear]
|
||||||
|
const [absences, summary, rtt, ...holidayResults] = await Promise.all([
|
||||||
listAbsences({
|
listAbsences({
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
@@ -207,11 +214,13 @@ export const useEmployeeDetailPage = () => {
|
|||||||
showLeaveTab.value
|
showLeaveTab.value
|
||||||
? getEmployeeLeaveSummary(loadedEmployee.id, leaveYear)
|
? getEmployeeLeaveSummary(loadedEmployee.id, leaveYear)
|
||||||
: Promise.resolve(null),
|
: Promise.resolve(null),
|
||||||
getEmployeeRttSummary(loadedEmployee.id, rttYear)
|
getEmployeeRttSummary(loadedEmployee.id, rttYear),
|
||||||
|
...holidayYears.map((y) => listPublicHolidays('metropole', y))
|
||||||
])
|
])
|
||||||
employeeAbsences.value = absences
|
employeeAbsences.value = absences
|
||||||
leaveSummary.value = summary
|
leaveSummary.value = summary
|
||||||
rttSummary.value = rtt
|
rttSummary.value = rtt
|
||||||
|
publicHolidays.value = Object.assign({}, ...holidayResults)
|
||||||
if (!showLeaveTab.value && activeTab.value === 'leave') {
|
if (!showLeaveTab.value && activeTab.value === 'leave') {
|
||||||
activeTab.value = 'contract'
|
activeTab.value = 'contract'
|
||||||
}
|
}
|
||||||
@@ -242,7 +251,8 @@ export const useEmployeeDetailPage = () => {
|
|||||||
siteId: employee.value.site?.id ?? null,
|
siteId: employee.value.site?.id ?? null,
|
||||||
contractId: Number(contractForm.contractId),
|
contractId: Number(contractForm.contractId),
|
||||||
contractEndDate: contractForm.endDate || null,
|
contractEndDate: contractForm.endDate || null,
|
||||||
contractPaidLeaveSettled: contractForm.paidLeaveSettled
|
contractPaidLeaveSettled: contractForm.paidLeaveSettled,
|
||||||
|
contractComment: contractForm.comment || null
|
||||||
})
|
})
|
||||||
|
|
||||||
isContractDrawerOpen.value = false
|
isContractDrawerOpen.value = false
|
||||||
@@ -309,6 +319,7 @@ export const useEmployeeDetailPage = () => {
|
|||||||
employeeAbsences,
|
employeeAbsences,
|
||||||
leaveSummary,
|
leaveSummary,
|
||||||
rttSummary,
|
rttSummary,
|
||||||
|
publicHolidays,
|
||||||
showLeaveTab,
|
showLeaveTab,
|
||||||
contractHistory,
|
contractHistory,
|
||||||
employeeContractWorkLabel,
|
employeeContractWorkLabel,
|
||||||
|
|||||||
@@ -341,7 +341,8 @@ export const useHoursPage = () => {
|
|||||||
isPresentMorning: false,
|
isPresentMorning: false,
|
||||||
isPresentAfternoon: false,
|
isPresentAfternoon: false,
|
||||||
isSiteValid: false,
|
isSiteValid: false,
|
||||||
isValid: false
|
isValid: false,
|
||||||
|
updatedAt: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === TRACKING_MODES.PRESENCE
|
const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === TRACKING_MODES.PRESENCE
|
||||||
@@ -463,6 +464,14 @@ export const useHoursPage = () => {
|
|||||||
return { backgroundColor: dayRow.absenceColor || '#dc2626' }
|
return { backgroundColor: dayRow.absenceColor || '#dc2626' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRowUpdatedAt = (employeeId: number): string => {
|
||||||
|
const raw = rows.value[employeeId]?.updatedAt
|
||||||
|
if (!raw) return ''
|
||||||
|
const date = new Date(raw)
|
||||||
|
if (Number.isNaN(date.getTime())) return ''
|
||||||
|
return date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
|
||||||
|
}
|
||||||
|
|
||||||
const getPresenceDayValue = (employeeId: number) => {
|
const getPresenceDayValue = (employeeId: number) => {
|
||||||
const row = rows.value[employeeId]
|
const row = rows.value[employeeId]
|
||||||
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
const dayRow = dayContextByEmployeeId.value.get(employeeId)
|
||||||
@@ -521,7 +530,8 @@ export const useHoursPage = () => {
|
|||||||
isPresentMorning: workHour?.isPresentMorning ?? false,
|
isPresentMorning: workHour?.isPresentMorning ?? false,
|
||||||
isPresentAfternoon: workHour?.isPresentAfternoon ?? false,
|
isPresentAfternoon: workHour?.isPresentAfternoon ?? false,
|
||||||
isSiteValid: workHour?.isSiteValid ?? false,
|
isSiteValid: workHour?.isSiteValid ?? false,
|
||||||
isValid: workHour?.isValid ?? false
|
isValid: workHour?.isValid ?? false,
|
||||||
|
updatedAt: workHour?.updatedAt ?? null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1140,6 +1150,7 @@ export const useHoursPage = () => {
|
|||||||
getRowMetrics,
|
getRowMetrics,
|
||||||
getRowAbsenceLabel,
|
getRowAbsenceLabel,
|
||||||
getRowAbsenceStyle,
|
getRowAbsenceStyle,
|
||||||
|
getRowUpdatedAt,
|
||||||
getPresenceDayValue,
|
getPresenceDayValue,
|
||||||
openAbsenceDrawer,
|
openAbsenceDrawer,
|
||||||
submitAbsence,
|
submitAbsence,
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
class="h-full"
|
class="h-full"
|
||||||
:absences="employeeAbsences"
|
:absences="employeeAbsences"
|
||||||
:summary="leaveSummary"
|
:summary="leaveSummary"
|
||||||
|
:public-holidays="publicHolidays"
|
||||||
/>
|
/>
|
||||||
<EmployeesRttTab v-else class="h-full" :summary="rttSummary" />
|
<EmployeesRttTab v-else class="h-full" :summary="rttSummary" />
|
||||||
</div>
|
</div>
|
||||||
@@ -109,6 +110,7 @@ const {
|
|||||||
employeeAbsences,
|
employeeAbsences,
|
||||||
leaveSummary,
|
leaveSummary,
|
||||||
rttSummary,
|
rttSummary,
|
||||||
|
publicHolidays,
|
||||||
showLeaveTab,
|
showLeaveTab,
|
||||||
contractHistory,
|
contractHistory,
|
||||||
employeeContractWorkLabel,
|
employeeContractWorkLabel,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
:get-row-metrics="getRowMetrics"
|
:get-row-metrics="getRowMetrics"
|
||||||
:get-row-absence-label="getRowAbsenceLabel"
|
:get-row-absence-label="getRowAbsenceLabel"
|
||||||
:get-row-absence-style="getRowAbsenceStyle"
|
:get-row-absence-style="getRowAbsenceStyle"
|
||||||
|
:get-row-updated-at="getRowUpdatedAt"
|
||||||
:get-presence-day-value="getPresenceDayValue"
|
:get-presence-day-value="getPresenceDayValue"
|
||||||
:on-absence-click="openAbsenceDrawer"
|
:on-absence-click="openAbsenceDrawer"
|
||||||
:format-minutes="formatMinutes"
|
:format-minutes="formatMinutes"
|
||||||
@@ -173,6 +174,7 @@ const {
|
|||||||
getRowMetrics,
|
getRowMetrics,
|
||||||
getRowAbsenceLabel,
|
getRowAbsenceLabel,
|
||||||
getRowAbsenceStyle,
|
getRowAbsenceStyle,
|
||||||
|
getRowUpdatedAt,
|
||||||
getPresenceDayValue,
|
getPresenceDayValue,
|
||||||
openAbsenceDrawer,
|
openAbsenceDrawer,
|
||||||
submitAbsence,
|
submitAbsence,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type ContractHistoryItem = {
|
|||||||
contractNature: 'CDI' | 'CDD' | 'INTERIM'
|
contractNature: 'CDI' | 'CDD' | 'INTERIM'
|
||||||
startDate: string
|
startDate: string
|
||||||
endDate?: string | null
|
endDate?: string | null
|
||||||
|
comment?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Employee = {
|
export type Employee = {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type WorkHour = {
|
|||||||
isPresentAfternoon?: boolean
|
isPresentAfternoon?: boolean
|
||||||
isSiteValid?: boolean
|
isSiteValid?: boolean
|
||||||
isValid?: boolean
|
isValid?: boolean
|
||||||
|
updatedAt?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkHourEntryPayload = {
|
export type WorkHourEntryPayload = {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export const updateEmployee = async (
|
|||||||
contractStartDate?: string
|
contractStartDate?: string
|
||||||
contractEndDate?: string | null
|
contractEndDate?: string | null
|
||||||
contractPaidLeaveSettled?: boolean
|
contractPaidLeaveSettled?: boolean
|
||||||
|
contractComment?: string | null
|
||||||
displayOrder?: number
|
displayOrder?: number
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
@@ -87,6 +88,9 @@ export const updateEmployee = async (
|
|||||||
if (payload.contractPaidLeaveSettled !== undefined) {
|
if (payload.contractPaidLeaveSettled !== undefined) {
|
||||||
body.contractPaidLeaveSettled = payload.contractPaidLeaveSettled
|
body.contractPaidLeaveSettled = payload.contractPaidLeaveSettled
|
||||||
}
|
}
|
||||||
|
if (payload.contractComment !== undefined) {
|
||||||
|
body.contractComment = payload.contractComment ?? null
|
||||||
|
}
|
||||||
|
|
||||||
return api.patch<Employee>(`/employees/${id}`, body, {
|
return api.patch<Employee>(`/employees/${id}`, body, {
|
||||||
toastSuccessKey: 'success.employee.update',
|
toastSuccessKey: 'success.employee.update',
|
||||||
|
|||||||
26
migrations/Version20260306140000.php
Normal file
26
migrations/Version20260306140000.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260306140000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add updated_at column to work_hours table.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE work_hours ADD COLUMN updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE work_hours DROP COLUMN updated_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
26
migrations/Version20260306160000.php
Normal file
26
migrations/Version20260306160000.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260306160000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add comment column to employee_contract_periods table.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE employee_contract_periods ADD COLUMN comment TEXT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE employee_contract_periods DROP COLUMN comment');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,5 +21,7 @@ final class ContractHistoryItem
|
|||||||
public string $startDate,
|
public string $startDate,
|
||||||
#[Groups(['employee:read'])]
|
#[Groups(['employee:read'])]
|
||||||
public ?string $endDate,
|
public ?string $endDate,
|
||||||
|
#[Groups(['employee:read'])]
|
||||||
|
public ?string $comment = null,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ class Employee
|
|||||||
#[Groups(['employee:write'])]
|
#[Groups(['employee:write'])]
|
||||||
private ?bool $contractPaidLeaveSettled = null;
|
private ?bool $contractPaidLeaveSettled = null;
|
||||||
|
|
||||||
|
#[Groups(['employee:write'])]
|
||||||
|
private ?string $contractComment = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->createdAt = new DateTimeImmutable();
|
$this->createdAt = new DateTimeImmutable();
|
||||||
@@ -202,6 +205,18 @@ class Employee
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getContractComment(): ?string
|
||||||
|
{
|
||||||
|
return $this->contractComment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContractComment(?string $contractComment): self
|
||||||
|
{
|
||||||
|
$this->contractComment = $contractComment;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
#[Groups(['employee:read'])]
|
#[Groups(['employee:read'])]
|
||||||
public function getCurrentContractNature(): string
|
public function getCurrentContractNature(): string
|
||||||
{
|
{
|
||||||
@@ -243,6 +258,7 @@ class Employee
|
|||||||
contractNature: $period->getContractNatureEnum()->value,
|
contractNature: $period->getContractNatureEnum()->value,
|
||||||
startDate: $period->getStartDate()->format('Y-m-d'),
|
startDate: $period->getStartDate()->format('Y-m-d'),
|
||||||
endDate: $period->getEndDate()?->format('Y-m-d'),
|
endDate: $period->getEndDate()?->format('Y-m-d'),
|
||||||
|
comment: $period->getComment(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
$periods
|
$periods
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ class EmployeeContractPeriod
|
|||||||
#[ORM\Column(type: 'boolean', options: ['default' => false])]
|
#[ORM\Column(type: 'boolean', options: ['default' => false])]
|
||||||
private bool $paidLeaveSettled = false;
|
private bool $paidLeaveSettled = false;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
|
private ?string $comment = null;
|
||||||
|
|
||||||
#[ORM\Column(type: 'datetime_immutable')]
|
#[ORM\Column(type: 'datetime_immutable')]
|
||||||
private DateTimeImmutable $createdAt;
|
private DateTimeImmutable $createdAt;
|
||||||
|
|
||||||
@@ -136,4 +139,16 @@ class EmployeeContractPeriod
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getComment(): ?string
|
||||||
|
{
|
||||||
|
return $this->comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setComment(?string $comment): self
|
||||||
|
{
|
||||||
|
$this->comment = $comment;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use ApiPlatform\Metadata\GetCollection;
|
|||||||
use ApiPlatform\Metadata\Patch;
|
use ApiPlatform\Metadata\Patch;
|
||||||
use App\Repository\WorkHourRepository;
|
use App\Repository\WorkHourRepository;
|
||||||
use App\State\WorkHourSiteValidationProcessor;
|
use App\State\WorkHourSiteValidationProcessor;
|
||||||
|
use DateTimeImmutable;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Serializer\Attribute\Groups;
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
@@ -106,6 +107,10 @@ class WorkHour
|
|||||||
#[Groups(['work_hour:read', 'work_hour:site_validate'])]
|
#[Groups(['work_hour:read', 'work_hour:site_validate'])]
|
||||||
private bool $isSiteValid = false;
|
private bool $isSiteValid = false;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||||
|
#[Groups(['work_hour:read'])]
|
||||||
|
private ?DateTimeImmutable $updatedAt = null;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
@@ -274,4 +279,16 @@ class WorkHour
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUpdatedAt(): ?DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUpdatedAt(?DateTimeImmutable $updatedAt): self
|
||||||
|
{
|
||||||
|
$this->updatedAt = $updatedAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ final readonly class EmployeeContractChangeRequest
|
|||||||
public ?DateTimeImmutable $contractStartDate,
|
public ?DateTimeImmutable $contractStartDate,
|
||||||
public ?DateTimeImmutable $contractEndDate,
|
public ?DateTimeImmutable $contractEndDate,
|
||||||
public ?bool $contractPaidLeaveSettled,
|
public ?bool $contractPaidLeaveSettled,
|
||||||
|
public ?string $contractComment,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function hasPeriodChangeRequest(): bool
|
public function hasPeriodChangeRequest(): bool
|
||||||
@@ -21,7 +22,8 @@ final readonly class EmployeeContractChangeRequest
|
|||||||
return null !== $this->contractNature
|
return null !== $this->contractNature
|
||||||
|| null !== $this->contractStartDate
|
|| null !== $this->contractStartDate
|
||||||
|| null !== $this->contractEndDate
|
|| null !== $this->contractEndDate
|
||||||
|| null !== $this->contractPaidLeaveSettled;
|
|| null !== $this->contractPaidLeaveSettled
|
||||||
|
|| null !== $this->contractComment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isCloseOnlyRequest(bool $contractChanged): bool
|
public function isCloseOnlyRequest(bool $contractChanged): bool
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ final class EmployeeContractChangeRequestFactory
|
|||||||
contractStartDate: $this->parseOptionalYmd($employee->getContractStartDate(), 'contractStartDate'),
|
contractStartDate: $this->parseOptionalYmd($employee->getContractStartDate(), 'contractStartDate'),
|
||||||
contractEndDate: $this->parseOptionalYmd($employee->getContractEndDate(), 'contractEndDate'),
|
contractEndDate: $this->parseOptionalYmd($employee->getContractEndDate(), 'contractEndDate'),
|
||||||
contractPaidLeaveSettled: $employee->getContractPaidLeaveSettled(),
|
contractPaidLeaveSettled: $employee->getContractPaidLeaveSettled(),
|
||||||
|
contractComment: $employee->getContractComment(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
|
|||||||
public function closeCurrentPeriod(
|
public function closeCurrentPeriod(
|
||||||
?EmployeeContractPeriod $todayPeriod,
|
?EmployeeContractPeriod $todayPeriod,
|
||||||
DateTimeImmutable $requestedEndDate,
|
DateTimeImmutable $requestedEndDate,
|
||||||
bool $paidLeaveSettled
|
bool $paidLeaveSettled,
|
||||||
|
?string $comment = null
|
||||||
): void {
|
): void {
|
||||||
if (null === $todayPeriod) {
|
if (null === $todayPeriod) {
|
||||||
throw new UnprocessableEntityHttpException('No active contract period to close.');
|
throw new UnprocessableEntityHttpException('No active contract period to close.');
|
||||||
@@ -58,6 +59,7 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
|
|||||||
|
|
||||||
$todayPeriod->setEndDate($requestedEndDate);
|
$todayPeriod->setEndDate($requestedEndDate);
|
||||||
$todayPeriod->setPaidLeaveSettled($paidLeaveSettled);
|
$todayPeriod->setPaidLeaveSettled($paidLeaveSettled);
|
||||||
|
$todayPeriod->setComment($comment);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ interface EmployeeContractPeriodManagerInterface
|
|||||||
public function closeCurrentPeriod(
|
public function closeCurrentPeriod(
|
||||||
?EmployeeContractPeriod $todayPeriod,
|
?EmployeeContractPeriod $todayPeriod,
|
||||||
DateTimeImmutable $requestedEndDate,
|
DateTimeImmutable $requestedEndDate,
|
||||||
bool $paidLeaveSettled
|
bool $paidLeaveSettled,
|
||||||
|
?string $comment = null
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
public function createNextPeriod(
|
public function createNextPeriod(
|
||||||
|
|||||||
@@ -69,13 +69,8 @@ final readonly class WorkedHoursCreditPolicy
|
|||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Règle forfait:
|
// Règle forfait: les absences ne créditent jamais de présence.
|
||||||
// - demi-journée d'absence => 0.5 travaillé
|
// Seules les checkboxes cochées par l'employé comptent.
|
||||||
// - journée complète d'absence => 0 travaillé
|
|
||||||
if ($absentMorning xor $absentAfternoon) {
|
|
||||||
return 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface
|
|||||||
$this->periodManager->closeCurrentPeriod(
|
$this->periodManager->closeCurrentPeriod(
|
||||||
$todayPeriod,
|
$todayPeriod,
|
||||||
$requestedEndDate,
|
$requestedEndDate,
|
||||||
$changeRequest->contractPaidLeaveSettled ?? false
|
$changeRequest->contractPaidLeaveSettled ?? false,
|
||||||
|
$changeRequest->contractComment
|
||||||
);
|
);
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
|||||||
$normalized = $this->normalizeEntry($entry, $employeeId, $isPresenceTracking);
|
$normalized = $this->normalizeEntry($entry, $employeeId, $isPresenceTracking);
|
||||||
$existing = $existingByEmployeeId[$employeeId] ?? null;
|
$existing = $existingByEmployeeId[$employeeId] ?? null;
|
||||||
$isAdmin = in_array('ROLE_ADMIN', $user->getRoles(), true);
|
$isAdmin = in_array('ROLE_ADMIN', $user->getRoles(), true);
|
||||||
|
$isSelf = in_array('ROLE_SELF', $user->getRoles(), true);
|
||||||
|
|
||||||
if ($existing?->isValid()) {
|
if ($existing?->isValid()) {
|
||||||
if (!$this->isSameAsExisting($existing, $normalized)) {
|
if (!$this->isSameAsExisting($existing, $normalized)) {
|
||||||
@@ -145,6 +146,9 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
|||||||
->setWorkDate($workDate)
|
->setWorkDate($workDate)
|
||||||
;
|
;
|
||||||
$this->hydrateWorkHour($workHour, $normalized);
|
$this->hydrateWorkHour($workHour, $normalized);
|
||||||
|
if ($isSelf) {
|
||||||
|
$workHour->setUpdatedAt(new DateTimeImmutable());
|
||||||
|
}
|
||||||
$this->entityManager->persist($workHour);
|
$this->entityManager->persist($workHour);
|
||||||
$existingByEmployeeId[$employeeId] = $workHour;
|
$existingByEmployeeId[$employeeId] = $workHour;
|
||||||
++$result->created;
|
++$result->created;
|
||||||
@@ -169,6 +173,9 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->hydrateWorkHour($workHour, $normalized);
|
$this->hydrateWorkHour($workHour, $normalized);
|
||||||
|
if (!$isAdmin) {
|
||||||
|
$workHour->setUpdatedAt(new DateTimeImmutable());
|
||||||
|
}
|
||||||
++$result->processed;
|
++$result->processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ final class WorkedHoursCreditPolicyTest extends TestCase
|
|||||||
$policy = new WorkedHoursCreditPolicy($this->buildResolverStub());
|
$policy = new WorkedHoursCreditPolicy($this->buildResolverStub());
|
||||||
$absence = $this->buildAbsence(trackMode: Contract::TRACKING_PRESENCE, weeklyHours: null, countAsWorked: true);
|
$absence = $this->buildAbsence(trackMode: Contract::TRACKING_PRESENCE, weeklyHours: null, countAsWorked: true);
|
||||||
|
|
||||||
$units = $policy->computeCreditedPresenceUnits($absence, '2026-02-16', true, false);
|
// Forfait : les absences ne créditent jamais de présence, seules les checkboxes comptent.
|
||||||
|
self::assertSame(0.0, $policy->computeCreditedPresenceUnits($absence, '2026-02-16', true, false));
|
||||||
self::assertSame(0.5, $units);
|
self::assertSame(0.0, $policy->computeCreditedPresenceUnits($absence, '2026-02-16', true, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNoCreditWhenAbsenceTypeDoesNotCount(): void
|
public function testNoCreditWhenAbsenceTypeDoesNotCount(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user