Retour RH: vue jour par date, RTT mi-semaine, récap salaire & exports, panier de nuit #21
@@ -35,7 +35,7 @@
|
||||
- Employee contract history: `employee_contract_periods`, resolved by `EmployeeContractResolver`
|
||||
- **Écrans Heures / Heures Conducteurs (vue jour)** : le libellé nature (CDI/CDD/Intérim) sous le nom de l'employé est résolu **à la date filtrée** via `WorkHourDayContext.contractNature` (alimenté par `EmployeeContractResolver::resolveNatureForEmployeeAndDate`), pas via `Employee.currentContractNature` (qui est résolu à aujourd'hui). Idem pour le **mode de suivi (TIME/PRESENCE), les heures hebdo et le libellé de contrat** sur la vue Jour : résolus à la date filtrée via `WorkHourDayContext` (`trackingMode`/`weeklyHours`/`contractType`/`contractName`, peuplés depuis `EmployeeContractResolver::resolveForEmployeeAndDate`), pas via `employee.contract` (résolu à aujourd'hui). Côté front, `resolveDayContract()` (`useHoursPage.ts`) pilote l'affichage et `handleSave` (heures vs présence par date).
|
||||
- **Exports heures annuelles** (par salarié `EmployeeYearlyHoursPrintProvider` + tous `EmployeeYearlyHoursBulkPrintProvider`, via `YearlyHoursExportBuilder`) : **tous les jours sous contrat sont affichés**, même vides ou non saisis (jusqu'à aujourd'hui). Seuls les jours hors contrat sont omis (`buildSegments` : un seul filtre `!$hasData && null === $contract`). Ne pas réintroduire de saut des jours de semaine vides. Samedis/dimanches grisés (`#c0c0c0`) dans les templates `employee-yearly-hours/print*.html.twig`. NB : l'export *tous employés* sur l'année peut dépasser `memory_limit=256M` (Dompdf) — limitation pré-existante, voir avec l'infra si besoin.
|
||||
- **Écran Calendrier** : un employé est affiché uniquement si au moins une de ses périodes de contrat (`employee.contractHistory`) intersecte le mois affiché (`[1er ; dernier jour]`). Filtre côté frontend dans `visibleEmployees` (`pages/calendar.vue`). **L'impression PDF des absences applique le même filtre** côté backend (`AbsencePrintProvider::hasContractInRange` sur la période `[from, to]`) : un salarié parti en avril n'apparaît pas sur une impression de mai.
|
||||
- **Écran Calendrier** : un employé est affiché uniquement si au moins une de ses périodes de contrat (`employee.contractHistory`) intersecte le mois affiché (`[1er ; dernier jour]`). Filtre côté frontend dans `visibleEmployees` (`pages/calendar.vue`). **L'impression PDF des absences applique le même filtre** côté backend (`AbsencePrintProvider::hasContractInRange` sur la période `[from, to]`) : un salarié parti en avril n'apparaît pas sur une impression de mai. **Le récap salaire applique le même filtre** (`SalaryRecapPrintProvider::hasContractInRange` sur le mois imprimé) : un salarié sans contrat sur le mois (ex. parti en février) n'apparaît pas sur le récap de juin.
|
||||
- **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
|
||||
- Absences with `countAsWorkedHours=true`: credit minutes (TIME) or nothing (PRESENCE)
|
||||
|
||||
@@ -63,6 +63,7 @@ Documents complementaires:
|
||||
- masqué si aucun contrat à cette date (cas rarissime en vue jour puisque l'employé est alors déjà filtré)
|
||||
- **Vue Jour (Heures) — contrat à la date affichée** : le mode de suivi (saisie d'heures vs cases de présence), le libellé de contrat et la logique de sauvegarde sont résolus selon la période de contrat valable à la date filtrée (champs `trackingMode`/`weeklyHours`/`contractType`/`contractName` portés par `WorkHourDayContext`, alimentés par `EmployeeContractResolver::resolveForEmployeeAndDate`), et non selon le contrat courant de l'employé. Un salarié passé 39h/35h → Forfait conserve donc la saisie d'heures sur ses dates antérieures à la bascule, et bascule en cases de présence à partir de la date de passage en forfait. La vue Semaine était déjà résolue par date.
|
||||
- **Exports heures annuelles (par salarié et tous salariés)** : affichent **tous les jours sous contrat**, même vides ou non saisis, jusqu'à la date du jour ; seuls les jours hors contrat (avant embauche, après départ, suspension) sont omis. Les samedis et dimanches sont grisés (gris foncé), les jours fériés en bleu.
|
||||
- **Récap salaire (export PDF mensuel)** : seuls les salariés ayant un contrat couvrant tout ou partie du mois imprimé apparaissent (filtre `hasContractInRange`). Un salarié dont le contrat est terminé avant le mois (ex. parti en février) n'est pas listé sur le récap des mois suivants.
|
||||
|
||||
## 4) Absences
|
||||
|
||||
|
||||
@@ -622,6 +622,7 @@ export const documentationSections: DocSection[] = [
|
||||
blocks: [
|
||||
{ type: 'paragraph', content: 'Génère un PDF A4 paysage avec le détail mensuel pour la paie.' },
|
||||
{ type: 'list', content: 'Sélecteur de mois (défaut = mois courant)\nDonnées groupées par site\nColonnes : nom, base contrat, jours de présence cadre, heures de nuit, panier de nuit, heures RTT payées (en-tête fusionné scindé en deux sous-colonnes 25 % et 50 %), congés (nombre + dates), maladie/AT (nombre + dates), primes conducteur (PDJ, repas, nuitée, samedi), observations\nColonne « Repas » chauffeur : somme déjeuner + dîner sur le mois (un jour avec les deux compte 2 repas)' },
|
||||
{ type: 'note', content: 'Seuls les salariés ayant un contrat couvrant tout ou partie du mois apparaissent : un salarié dont le contrat est terminé (ex. parti en février) n\'est pas listé sur le récap des mois suivants.' },
|
||||
{ type: 'note', content: 'Forfait : un congé imputé sur le stock de l\'année précédente (N-1) n\'apparaît pas dans la colonne congés et compte comme un jour de présence. Le budget N-1 est consommé dans l\'ordre chronologique depuis janvier, de façon cohérente avec la fiche employé (les jours payés réduisent le stock N-1 d\'abord). Au-delà du budget N-1, les congés s\'affichent normalement.' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -62,7 +62,13 @@ class SalaryRecapPrintProvider implements ProviderInterface
|
||||
$from = DateTimeImmutable::createFromFormat('Y-m-d', $month.'-01');
|
||||
$to = $from->modify('last day of this month');
|
||||
|
||||
$employees = $this->employeeRepository->findForPrintBySiteIds([]);
|
||||
// N'inclure que les employés ayant un contrat couvrant tout ou partie du mois.
|
||||
// Sans ce filtre, un salarié dont le contrat est terminé (ex. parti en février)
|
||||
// apparaît à tort sur le récap des mois suivants.
|
||||
$employees = array_values(array_filter(
|
||||
$this->employeeRepository->findForPrintBySiteIds([]),
|
||||
fn (Employee $employee): bool => $this->hasContractInRange($employee, $from, $to)
|
||||
));
|
||||
$workHours = $this->workHourRepository->findByDateRangeAndEmployees($from, $to, $employees);
|
||||
$absences = $this->absenceRepository->findForPrint($from, $to, $employees);
|
||||
|
||||
@@ -120,6 +126,22 @@ class SalaryRecapPrintProvider implements ProviderInterface
|
||||
]);
|
||||
}
|
||||
|
||||
private function hasContractInRange(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): bool
|
||||
{
|
||||
$fromDay = $from->format('Y-m-d');
|
||||
$toDay = $to->format('Y-m-d');
|
||||
|
||||
foreach ($employee->getContractPeriods() as $period) {
|
||||
$start = $period->getStartDate()->format('Y-m-d');
|
||||
$end = $period->getEndDate()?->format('Y-m-d');
|
||||
if ($start <= $toDay && (null === $end || $end >= $fromDay)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
||||
namespace App\Tests\State;
|
||||
|
||||
use App\Entity\Absence;
|
||||
use App\Entity\Employee;
|
||||
use App\Entity\EmployeeContractPeriod;
|
||||
use App\Enum\HalfDay;
|
||||
use App\Service\WorkHours\AbsenceSegmentsResolver;
|
||||
use App\State\SalaryRecapPrintProvider;
|
||||
@@ -67,6 +69,54 @@ final class SalaryRecapPrintProviderTest extends TestCase
|
||||
self::assertSame('03/03', $result['dates']);
|
||||
}
|
||||
|
||||
public function testTerminatedContractExcludedFromMonth(): void
|
||||
{
|
||||
// Marine : contrat terminé le 26/02 → absente du récap de juin.
|
||||
$employee = $this->buildEmployeeWithPeriod('2025-02-10', '2026-02-26');
|
||||
|
||||
self::assertFalse($this->hasInRange($employee, '2026-06-01', '2026-06-30'));
|
||||
}
|
||||
|
||||
public function testOngoingContractIncluded(): void
|
||||
{
|
||||
$employee = $this->buildEmployeeWithPeriod('2025-01-01', null);
|
||||
|
||||
self::assertTrue($this->hasInRange($employee, '2026-06-01', '2026-06-30'));
|
||||
}
|
||||
|
||||
public function testContractEndingOnFromDayIncluded(): void
|
||||
{
|
||||
$employee = $this->buildEmployeeWithPeriod('2025-01-01', '2026-06-01');
|
||||
|
||||
self::assertTrue($this->hasInRange($employee, '2026-06-01', '2026-06-30'));
|
||||
}
|
||||
|
||||
public function testNoPeriodsExcluded(): void
|
||||
{
|
||||
self::assertFalse($this->hasInRange(new Employee(), '2026-06-01', '2026-06-30'));
|
||||
}
|
||||
|
||||
private function hasInRange(Employee $employee, string $from, string $to): bool
|
||||
{
|
||||
$provider = new ReflectionClass(SalaryRecapPrintProvider::class)->newInstanceWithoutConstructor();
|
||||
|
||||
return new ReflectionClass($provider::class)
|
||||
->getMethod('hasContractInRange')
|
||||
->invoke($provider, $employee, new DateTimeImmutable($from), new DateTimeImmutable($to));
|
||||
}
|
||||
|
||||
private function buildEmployeeWithPeriod(string $start, ?string $end): Employee
|
||||
{
|
||||
$employee = new Employee();
|
||||
$period = new EmployeeContractPeriod();
|
||||
$period->setEmployee($employee);
|
||||
$period->setStartDate(new DateTimeImmutable($start));
|
||||
$period->setEndDate(null !== $end ? new DateTimeImmutable($end) : null);
|
||||
$employee->getContractPeriods()->add($period);
|
||||
|
||||
return $employee;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Absence> $conges
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user