feat : ajout d'un champ commentaire sur les contrats + correction de plusieurs bugs

This commit is contained in:
2026-03-06 16:58:29 +01:00
parent 4cf2608cdd
commit e794ad2514
28 changed files with 235 additions and 59 deletions

View File

@@ -5,7 +5,8 @@
"Bash(npx nuxi:*)",
"Bash(php:*)",
"Bash(docker compose:*)",
"Bash(make test:*)"
"Bash(make test:*)",
"Bash(grep:*)"
]
}
}

View File

@@ -43,10 +43,17 @@ Documents complementaires:
- Saisie par salarié et par date:
- matin / après-midi / soir
- 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:
- `Jour`, `Nuit`, `Total`
- Heures de nuit:
- 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
@@ -57,8 +64,9 @@ Documents complementaires:
- Colonne absence (vue jour):
- affiche le libellé
- fond coloré selon le type d'absence
- Si plusieurs absences de couleurs différentes sur le même jour:
- fallback rouge
- Calendrier congés: fond coloré selon la couleur du type d'absence (`AbsenceType.color`)
- demi-journée: dégradé diagonal
- journée complète: fond plein
### Effet absence sur les heures
@@ -73,7 +81,7 @@ Documents complementaires:
- Si `countAsWorkedHours = true`:
- `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
@@ -112,6 +120,7 @@ Documents complementaires:
## 7) Férié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:
- absences bloquées sur jour férié
- saisie d'heures autorisée

View File

@@ -86,6 +86,19 @@
<p v-if="showContractEndDateError" class="mt-1 text-sm text-red-600">La date de fin est obligatoire.</p>
</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">
<label class="inline-flex items-center gap-2 text-md font-semibold text-neutral-700" for="contract-paid-leave-settled">
<input
@@ -191,6 +204,7 @@ type ContractForm = {
startDate: string
endDate: string
paidLeaveSettled: boolean
comment: string
}
type CreateContractForm = {

View File

@@ -41,9 +41,9 @@
>
<div
class="h-6 w-6"
:class="getDayClass(day.leave)"
:style="getDayStyle(day.leave)"
:title="getDayTitle(day.leave)"
:class="getDayClass(day)"
:style="getDayStyle(day)"
:title="getDayTitle(day)"
>
{{ getDayText(day) }}
</div>
@@ -65,13 +65,13 @@ type DayLeaveState = {
am: boolean
pm: boolean
labels: string[]
hasCongeTypeC: boolean
hasOtherTypes: boolean
colors: string[]
}
const props = defineProps<{
absences: Absence[]
summary: EmployeeLeaveSummary | null
publicHolidays: Record<string, string>
}>()
const monthLabels = [
@@ -124,8 +124,7 @@ const dayLeaveMap = computed(() => {
am: false,
pm: false,
labels: [] as string[],
hasCongeTypeC: false,
hasOtherTypes: false
colors: [] as string[]
}
const isStart = ymd === startYmd
@@ -150,18 +149,21 @@ const dayLeaveMap = computed(() => {
}
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 hoverLabel = `${typeLabel}${halfSuffix}`
const colors = existing.colors.includes(typeColor)
? existing.colors
: [...existing.colors, typeColor]
map.set(ymd, {
am: existing.am || am,
pm: existing.pm || pm,
labels: existing.labels.includes(hoverLabel)
? existing.labels
: [...existing.labels, hoverLabel],
hasCongeTypeC: existing.hasCongeTypeC || typeCode === 'C',
hasOtherTypes: existing.hasOtherTypes || typeCode !== 'C'
colors
})
}
}
@@ -180,7 +182,7 @@ const months = computed(() => {
const daysInMonth = new Date(monthYear, monthIndex + 1, 0).getDate()
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) {
cells.push(null)
@@ -191,7 +193,8 @@ const months = computed(() => {
cells.push({
ymd,
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) => {
if (!leave) return 'text-primary-500'
if (leave.am && leave.pm) {
return leave.hasOtherTypes
? 'bg-red-600 text-white rounded font-semibold'
: 'bg-primary-500 text-white rounded font-semibold'
const getDayClass = (day: { leave: DayLeaveState | null; isHoliday: boolean }) => {
if (day.leave) {
return 'rounded font-semibold text-white'
}
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) => {
if (!leave || (leave.am && leave.pm)) return undefined
const color = leave.hasOtherTypes ? '#dc2626' : '#222783'
const backgroundImage = leave.am
? `linear-gradient(135deg, ${color} 0 50%, transparent 50% 100%)`
: `linear-gradient(135deg, transparent 0 50%, ${color} 50% 100%)`
return {
backgroundImage,
backgroundColor: 'transparent'
const getDayStyle = (day: { leave: DayLeaveState | null; isHoliday: boolean }) => {
if (day.leave) {
const color = day.leave.colors[0] ?? '#222783'
if (day.leave.am && day.leave.pm) {
return { backgroundColor: color }
}
const backgroundImage = day.leave.am
? `linear-gradient(135deg, ${color} 0 50%, transparent 50% 100%)`
: `linear-gradient(135deg, transparent 0 50%, ${color} 50% 100%)`
return { backgroundImage, backgroundColor: 'transparent' }
}
if (day.isHoliday) return { backgroundColor: 'rgb(179, 229, 252)' }
return undefined
}
const getDayText = (day: { label: string; leave: DayLeaveState | null }) => {
return day.label
}
const getDayTitle = (leave: DayLeaveState | null) => {
if (!leave || leave.labels.length === 0) return ''
return leave.labels.join(' / ')
const getDayTitle = (day: { leave: DayLeaveState | null; isHoliday: boolean; ymd: string }) => {
if (day.leave && day.leave.labels.length > 0) return day.leave.labels.join(' / ')
if (day.isHoliday) return props.publicHolidays[day.ymd] ?? 'Jour férié'
return ''
}
const formatCount = (value: number | null | undefined) => {

View File

@@ -63,6 +63,9 @@
<Icon name="mdi:check"/>
</span>
</p>
<p v-if="isAdmin && getRowUpdatedAt(employee.id)" class="text-neutral-400 text-xs truncate">
Modifié le {{ getRowUpdatedAt(employee.id) }}
</p>
</div>
<div class="pl-2 min-w-0 self-stretch flex flex-col gap-1 justify-between py-0.5">
<p
@@ -216,6 +219,7 @@ const props = defineProps<{
getRowMetrics: (employeeId: number) => { dayMinutes: number; nightMinutes: number; totalMinutes: number }
getRowAbsenceLabel: (employeeId: number) => string
getRowAbsenceStyle: (employeeId: number) => { backgroundColor: string } | undefined
getRowUpdatedAt: (employeeId: number) => string
getPresenceDayValue: (employeeId: number) => string
onAbsenceClick: (employeeId: number) => void
formatMinutes: (minutes: number) => string

View File

@@ -10,4 +10,5 @@ export type HourRow = {
isPresentAfternoon: boolean
isSiteValid: boolean
isValid: boolean
updatedAt: string | null
}

View File

@@ -167,8 +167,9 @@ const closeMenu = () => {
const commitInput = () => {
const normalized = normalizeTypedTime(inputValue.value)
if (normalized === null) {
inputValue.value = props.modelValue
if (normalized === null || (normalized !== '' && !timeSlots.value.includes(normalized))) {
emit('update:modelValue', '')
inputValue.value = ''
closeMenu()
return
}

View File

@@ -9,6 +9,7 @@ import { listContracts } from '~/services/contracts'
import { getEmployeeLeaveSummary } from '~/services/employee-leave-summary'
import { getEmployeeRttSummary } from '~/services/employee-rtt-summary'
import { getEmployee, updateEmployee } from '~/services/employees'
import { listPublicHolidays } from '~/services/public-holidays'
import { formatNullableYmdToFr, getTodayYmd, shiftYmd } from '~/utils/date'
import { contractNatureLabel, isContractNature, requiresContractEndDate } from '~/utils/contract'
@@ -22,6 +23,7 @@ export const useEmployeeDetailPage = () => {
const employeeAbsences = ref<Absence[]>([])
const leaveSummary = ref<EmployeeLeaveSummary | null>(null)
const rttSummary = ref<EmployeeRttSummary | null>(null)
const publicHolidays = ref<Record<string, string>>({})
const isContractDrawerOpen = ref(false)
const isContractSubmitting = ref(false)
const isCreateContractDrawerOpen = ref(false)
@@ -34,7 +36,8 @@ export const useEmployeeDetailPage = () => {
contractNature: 'CDI' as 'CDI' | 'CDD' | 'INTERIM',
startDate: '',
endDate: '',
paidLeaveSettled: false
paidLeaveSettled: false,
comment: ''
})
const validationTouched = reactive({
@@ -138,6 +141,7 @@ export const useEmployeeDetailPage = () => {
contractForm.startDate = active.startDate
contractForm.endDate = getTodayYmd()
contractForm.paidLeaveSettled = false
contractForm.comment = ''
}
const openCloseContractDrawer = () => {
@@ -198,7 +202,10 @@ export const useEmployeeDetailPage = () => {
const to = isForfait
? `${leaveYear}-12-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({
from,
to,
@@ -207,11 +214,13 @@ export const useEmployeeDetailPage = () => {
showLeaveTab.value
? getEmployeeLeaveSummary(loadedEmployee.id, leaveYear)
: Promise.resolve(null),
getEmployeeRttSummary(loadedEmployee.id, rttYear)
getEmployeeRttSummary(loadedEmployee.id, rttYear),
...holidayYears.map((y) => listPublicHolidays('metropole', y))
])
employeeAbsences.value = absences
leaveSummary.value = summary
rttSummary.value = rtt
publicHolidays.value = Object.assign({}, ...holidayResults)
if (!showLeaveTab.value && activeTab.value === 'leave') {
activeTab.value = 'contract'
}
@@ -242,7 +251,8 @@ export const useEmployeeDetailPage = () => {
siteId: employee.value.site?.id ?? null,
contractId: Number(contractForm.contractId),
contractEndDate: contractForm.endDate || null,
contractPaidLeaveSettled: contractForm.paidLeaveSettled
contractPaidLeaveSettled: contractForm.paidLeaveSettled,
contractComment: contractForm.comment || null
})
isContractDrawerOpen.value = false
@@ -309,6 +319,7 @@ export const useEmployeeDetailPage = () => {
employeeAbsences,
leaveSummary,
rttSummary,
publicHolidays,
showLeaveTab,
contractHistory,
employeeContractWorkLabel,

View File

@@ -341,7 +341,8 @@ export const useHoursPage = () => {
isPresentMorning: false,
isPresentAfternoon: false,
isSiteValid: false,
isValid: false
isValid: false,
updatedAt: null
})
const isPresenceTracking = (employee: Employee) => employee.contract?.trackingMode === TRACKING_MODES.PRESENCE
@@ -463,6 +464,14 @@ export const useHoursPage = () => {
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 row = rows.value[employeeId]
const dayRow = dayContextByEmployeeId.value.get(employeeId)
@@ -521,7 +530,8 @@ export const useHoursPage = () => {
isPresentMorning: workHour?.isPresentMorning ?? false,
isPresentAfternoon: workHour?.isPresentAfternoon ?? 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,
getRowAbsenceLabel,
getRowAbsenceStyle,
getRowUpdatedAt,
getPresenceDayValue,
openAbsenceDrawer,
submitAbsence,

View File

@@ -93,6 +93,7 @@
class="h-full"
:absences="employeeAbsences"
:summary="leaveSummary"
:public-holidays="publicHolidays"
/>
<EmployeesRttTab v-else class="h-full" :summary="rttSummary" />
</div>
@@ -109,6 +110,7 @@ const {
employeeAbsences,
leaveSummary,
rttSummary,
publicHolidays,
showLeaveTab,
contractHistory,
employeeContractWorkLabel,

View File

@@ -66,6 +66,7 @@
:get-row-metrics="getRowMetrics"
:get-row-absence-label="getRowAbsenceLabel"
:get-row-absence-style="getRowAbsenceStyle"
:get-row-updated-at="getRowUpdatedAt"
:get-presence-day-value="getPresenceDayValue"
:on-absence-click="openAbsenceDrawer"
:format-minutes="formatMinutes"
@@ -173,6 +174,7 @@ const {
getRowMetrics,
getRowAbsenceLabel,
getRowAbsenceStyle,
getRowUpdatedAt,
getPresenceDayValue,
openAbsenceDrawer,
submitAbsence,

View File

@@ -8,6 +8,7 @@ export type ContractHistoryItem = {
contractNature: 'CDI' | 'CDD' | 'INTERIM'
startDate: string
endDate?: string | null
comment?: string | null
}
export type Employee = {

View File

@@ -15,6 +15,7 @@ export type WorkHour = {
isPresentAfternoon?: boolean
isSiteValid?: boolean
isValid?: boolean
updatedAt?: string | null
}
export type WorkHourEntryPayload = {

View File

@@ -61,6 +61,7 @@ export const updateEmployee = async (
contractStartDate?: string
contractEndDate?: string | null
contractPaidLeaveSettled?: boolean
contractComment?: string | null
displayOrder?: number
}
) => {
@@ -87,6 +88,9 @@ export const updateEmployee = async (
if (payload.contractPaidLeaveSettled !== undefined) {
body.contractPaidLeaveSettled = payload.contractPaidLeaveSettled
}
if (payload.contractComment !== undefined) {
body.contractComment = payload.contractComment ?? null
}
return api.patch<Employee>(`/employees/${id}`, body, {
toastSuccessKey: 'success.employee.update',

View 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');
}
}

View 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');
}
}

View File

@@ -21,5 +21,7 @@ final class ContractHistoryItem
public string $startDate,
#[Groups(['employee:read'])]
public ?string $endDate,
#[Groups(['employee:read'])]
public ?string $comment = null,
) {}
}

View File

@@ -78,6 +78,9 @@ class Employee
#[Groups(['employee:write'])]
private ?bool $contractPaidLeaveSettled = null;
#[Groups(['employee:write'])]
private ?string $contractComment = null;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
@@ -202,6 +205,18 @@ class Employee
return $this;
}
public function getContractComment(): ?string
{
return $this->contractComment;
}
public function setContractComment(?string $contractComment): self
{
$this->contractComment = $contractComment;
return $this;
}
#[Groups(['employee:read'])]
public function getCurrentContractNature(): string
{
@@ -243,6 +258,7 @@ class Employee
contractNature: $period->getContractNatureEnum()->value,
startDate: $period->getStartDate()->format('Y-m-d'),
endDate: $period->getEndDate()?->format('Y-m-d'),
comment: $period->getComment(),
);
},
$periods

View File

@@ -40,6 +40,9 @@ class EmployeeContractPeriod
#[ORM\Column(type: 'boolean', options: ['default' => false])]
private bool $paidLeaveSettled = false;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $comment = null;
#[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $createdAt;
@@ -136,4 +139,16 @@ class EmployeeContractPeriod
return $this;
}
public function getComment(): ?string
{
return $this->comment;
}
public function setComment(?string $comment): self
{
$this->comment = $comment;
return $this;
}
}

View File

@@ -14,6 +14,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use App\Repository\WorkHourRepository;
use App\State\WorkHourSiteValidationProcessor;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
@@ -106,6 +107,10 @@ class WorkHour
#[Groups(['work_hour:read', 'work_hour:site_validate'])]
private bool $isSiteValid = false;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
#[Groups(['work_hour:read'])]
private ?DateTimeImmutable $updatedAt = null;
public function getId(): ?int
{
return $this->id;
@@ -274,4 +279,16 @@ class WorkHour
return $this;
}
public function getUpdatedAt(): ?DateTimeImmutable
{
return $this->updatedAt;
}
public function setUpdatedAt(?DateTimeImmutable $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
}

View File

@@ -14,6 +14,7 @@ final readonly class EmployeeContractChangeRequest
public ?DateTimeImmutable $contractStartDate,
public ?DateTimeImmutable $contractEndDate,
public ?bool $contractPaidLeaveSettled,
public ?string $contractComment,
) {}
public function hasPeriodChangeRequest(): bool
@@ -21,7 +22,8 @@ final readonly class EmployeeContractChangeRequest
return null !== $this->contractNature
|| null !== $this->contractStartDate
|| null !== $this->contractEndDate
|| null !== $this->contractPaidLeaveSettled;
|| null !== $this->contractPaidLeaveSettled
|| null !== $this->contractComment;
}
public function isCloseOnlyRequest(bool $contractChanged): bool

View File

@@ -18,6 +18,7 @@ final class EmployeeContractChangeRequestFactory
contractStartDate: $this->parseOptionalYmd($employee->getContractStartDate(), 'contractStartDate'),
contractEndDate: $this->parseOptionalYmd($employee->getContractEndDate(), 'contractEndDate'),
contractPaidLeaveSettled: $employee->getContractPaidLeaveSettled(),
contractComment: $employee->getContractComment(),
);
}

View File

@@ -43,7 +43,8 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
public function closeCurrentPeriod(
?EmployeeContractPeriod $todayPeriod,
DateTimeImmutable $requestedEndDate,
bool $paidLeaveSettled
bool $paidLeaveSettled,
?string $comment = null
): void {
if (null === $todayPeriod) {
throw new UnprocessableEntityHttpException('No active contract period to close.');
@@ -58,6 +59,7 @@ final readonly class EmployeeContractPeriodManager implements EmployeeContractPe
$todayPeriod->setEndDate($requestedEndDate);
$todayPeriod->setPaidLeaveSettled($paidLeaveSettled);
$todayPeriod->setComment($comment);
$this->entityManager->flush();
}

View File

@@ -23,7 +23,8 @@ interface EmployeeContractPeriodManagerInterface
public function closeCurrentPeriod(
?EmployeeContractPeriod $todayPeriod,
DateTimeImmutable $requestedEndDate,
bool $paidLeaveSettled
bool $paidLeaveSettled,
?string $comment = null
): void;
public function createNextPeriod(

View File

@@ -69,13 +69,8 @@ final readonly class WorkedHoursCreditPolicy
return 0.0;
}
// Règle forfait:
// - demi-journée d'absence => 0.5 travaillé
// - journée complète d'absence => 0 travaillé
if ($absentMorning xor $absentAfternoon) {
return 0.5;
}
// Règle forfait: les absences ne créditent jamais de présence.
// Seules les checkboxes cochées par l'employé comptent.
return 0.0;
}

View File

@@ -90,7 +90,8 @@ final readonly class EmployeeWriteProcessor implements ProcessorInterface
$this->periodManager->closeCurrentPeriod(
$todayPeriod,
$requestedEndDate,
$changeRequest->contractPaidLeaveSettled ?? false
$changeRequest->contractPaidLeaveSettled ?? false,
$changeRequest->contractComment
);
return $result;

View File

@@ -98,6 +98,7 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
$normalized = $this->normalizeEntry($entry, $employeeId, $isPresenceTracking);
$existing = $existingByEmployeeId[$employeeId] ?? null;
$isAdmin = in_array('ROLE_ADMIN', $user->getRoles(), true);
$isSelf = in_array('ROLE_SELF', $user->getRoles(), true);
if ($existing?->isValid()) {
if (!$this->isSameAsExisting($existing, $normalized)) {
@@ -145,6 +146,9 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
->setWorkDate($workDate)
;
$this->hydrateWorkHour($workHour, $normalized);
if ($isSelf) {
$workHour->setUpdatedAt(new DateTimeImmutable());
}
$this->entityManager->persist($workHour);
$existingByEmployeeId[$employeeId] = $workHour;
++$result->created;
@@ -169,6 +173,9 @@ final readonly class WorkHourBulkUpsertProcessor implements ProcessorInterface
}
$this->hydrateWorkHour($workHour, $normalized);
if (!$isAdmin) {
$workHour->setUpdatedAt(new DateTimeImmutable());
}
++$result->processed;
}

View File

@@ -43,9 +43,9 @@ final class WorkedHoursCreditPolicyTest extends TestCase
$policy = new WorkedHoursCreditPolicy($this->buildResolverStub());
$absence = $this->buildAbsence(trackMode: Contract::TRACKING_PRESENCE, weeklyHours: null, countAsWorked: true);
$units = $policy->computeCreditedPresenceUnits($absence, '2026-02-16', true, false);
self::assertSame(0.5, $units);
// 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.0, $policy->computeCreditedPresenceUnits($absence, '2026-02-16', true, true));
}
public function testNoCreditWhenAbsenceTypeDoesNotCount(): void