Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 081d92b9f4 | |||
| 143278a368 | |||
| 2802f9524c | |||
| 589018064b |
@@ -35,7 +35,7 @@
|
|||||||
- Employee contract history: `employee_contract_periods`, resolved by `EmployeeContractResolver`
|
- 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).
|
- **É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.
|
- **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.
|
||||||
- **Export heures vue Jour** (`WorkHourDayExportProvider`, endpoint `GET /work-hours/day-export?workDate=&siteIds=`, `ROLE_ADMIN`) : bouton « Exporter » à droite du titre « Heures », **visible uniquement en vue Jour** (`v-if="isAdmin && viewMode === 'day'"`, masqué en vue Semaine). PDF A4 portrait d'**une seule journée**, **regroupé par site**, colonnes de la vue Jour **sans « Valider »** (colonne **Total en gras**). Mêmes employés que l'écran : non-conducteurs, sous contrat à la date, sites cochés (lignes vides incluses). Calcul des cellules mutualisé via `YearlyHoursExportBuilder::buildDayRowsForEmployees` (Jour/Nuit/Total incluent crédit absence + crédit virtuel férié). Colonne **Statut = code** du type d'absence (`AbsenceType::getCode`, ex. `AT`) sur sa couleur de fond ; férié sans absence → nom du férié sur `#b3e5fc`. Chaque row porte `statut` (code), `statutLabel` (libellé, pour la légende) et `statutColor`. **Légende** sous le tableau (carré coloré contenant le code + libellé à droite), construite côté provider à partir des codes présents (hors férié, dédupliquée par code, triée). Gabarit `templates/work-hour-day-export/print.html.twig`.
|
- **Export heures vue Jour** (`WorkHourDayExportProvider`, endpoint `GET /work-hours/day-export?workDate=&siteIds=`, `ROLE_USER`) : bouton « Exporter » à droite du titre « Heures », **visible uniquement en vue Jour** (`v-if="(isAdmin || isSiteManager) && viewMode === 'day'"`, masqué en vue Semaine et pour `ROLE_SELF`). **Accessible aux admins ET aux chefs de site** : le périmètre est résolu côté backend via `EmployeeRepository::findScoped($user)` (admin → tous les sites, chef de site → ses sites uniquement, cf. `EmployeeScopeService`), donc un `siteIds` hors périmètre est ignoré ; le drawer front ne propose que les sites visibles (`sites` dérivé des employés scopés). PDF A4 portrait d'**une seule journée**, **regroupé par site**, colonnes de la vue Jour **sans « Valider »** (colonne **Total en gras**). Mêmes employés que l'écran : non-conducteurs, sous contrat à la date, sites cochés et dans le périmètre (lignes vides incluses). **Tri intra-site identique au calendrier** : `displayOrder` (ordre manuel), puis nom, puis prénom (cf. `compareEmployeesInSite` front). Calcul des cellules mutualisé via `YearlyHoursExportBuilder::buildDayRowsForEmployees` (Jour/Nuit/Total incluent crédit absence + crédit virtuel férié). Colonne **Statut = code** du type d'absence (`AbsenceType::getCode`, ex. `AT`) sur sa couleur de fond ; férié sans absence → nom du férié sur `#b3e5fc`. Chaque row porte `statut` (code), `statutLabel` (libellé, pour la légende) et `statutColor`. **Légende** sous le tableau (carré coloré contenant le code + libellé à droite), construite côté provider à partir des codes présents (hors férié, dédupliquée par code, triée). Gabarit `templates/work-hour-day-export/print.html.twig`.
|
||||||
- **É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.
|
- **É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`.
|
- **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: stored per day (auto-split), AM/PM/full day, clear corresponding hour slots
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.109'
|
app.version: '0.1.111'
|
||||||
|
|||||||
+15
-7
@@ -1,17 +1,24 @@
|
|||||||
# Export PDF des heures — vue Jour
|
# Export PDF des heures — vue Jour
|
||||||
|
|
||||||
Bouton **Exporter** à droite du titre « Heures », visible **uniquement pour les
|
Bouton **Exporter** à droite du titre « Heures », visible pour les **administrateurs**
|
||||||
administrateurs** (`ROLE_ADMIN`) et **uniquement en vue Jour** (masqué en vue Semaine).
|
(`ROLE_ADMIN`) **et les chefs de site** (`ROLE_USER`), **uniquement en vue Jour** (masqué
|
||||||
|
en vue Semaine, masqué pour les profils employé `ROLE_SELF`).
|
||||||
|
|
||||||
|
## Périmètre
|
||||||
|
- **Administrateur** : peut exporter tous les sites.
|
||||||
|
- **Chef de site** : ne voit dans le drawer que **ses sites** et ne peut exporter que
|
||||||
|
ceux-ci. Le périmètre est appliqué côté backend (`EmployeeRepository::findScoped`) — un
|
||||||
|
`siteIds` forcé hors de son périmètre est ignoré, aucune donnée d'un autre site ne fuit.
|
||||||
|
|
||||||
## Comportement
|
## Comportement
|
||||||
- Ouvre un drawer : un champ **date** (préremplit la date affichée) et des **cases à
|
- Ouvre un drawer : un champ **date** (préremplit la date affichée) et des **cases à
|
||||||
cocher des sites** (présélectionnées sur le filtre courant).
|
cocher des sites** (limitées au périmètre de l'utilisateur).
|
||||||
- Génère un **PDF A4 portrait** d'une seule journée, **regroupé par site**.
|
- Génère un **PDF A4 portrait** d'une seule journée, **regroupé par site**.
|
||||||
|
|
||||||
## Données
|
## Données
|
||||||
- Mêmes employés que la vue Jour : **non-conducteurs**, **sous contrat** à la date
|
- Mêmes employés que la vue Jour : **non-conducteurs**, **sous contrat** à la date
|
||||||
choisie, des sites cochés. Les employés sous contrat sans saisie apparaissent (lignes
|
choisie, des sites cochés et **dans le périmètre de l'utilisateur**. Les employés sous
|
||||||
vides).
|
contrat sans saisie apparaissent (lignes vides).
|
||||||
- Colonnes : Nom · Statut · Début matin · Fin matin · Début après-midi · Fin après-midi ·
|
- Colonnes : Nom · Statut · Début matin · Fin matin · Début après-midi · Fin après-midi ·
|
||||||
Début soir · Fin soir · Jour · Nuit · **Total** (en gras). **Pas de colonne « Valider ».**
|
Début soir · Fin soir · Jour · Nuit · **Total** (en gras). **Pas de colonne « Valider ».**
|
||||||
- Colonne **Statut** : affiche le **code** du type d'absence (ex. `AT`), pas le libellé,
|
- Colonne **Statut** : affiche le **code** du type d'absence (ex. `AT`), pas le libellé,
|
||||||
@@ -23,8 +30,9 @@ administrateurs** (`ROLE_ADMIN`) et **uniquement en vue Jour** (masqué en vue S
|
|||||||
de couleur contenant le code et le libellé du type à droite. Triée par code, dédupliquée.
|
de couleur contenant le code et le libellé du type à droite. Triée par code, dédupliquée.
|
||||||
|
|
||||||
## Technique
|
## Technique
|
||||||
- Endpoint : `GET /work-hours/day-export?workDate=YYYY-MM-DD&siteIds=1,2,3` (`ROLE_ADMIN`).
|
- Endpoint : `GET /work-hours/day-export?workDate=YYYY-MM-DD&siteIds=1,2,3` (`ROLE_USER`).
|
||||||
- Provider : `App\State\WorkHourDayExportProvider`.
|
- Provider : `App\State\WorkHourDayExportProvider` — résout le périmètre via
|
||||||
|
`EmployeeRepository::findScoped($user)` (admin → tous, chef de site → ses sites).
|
||||||
- Calcul des cellules : `YearlyHoursExportBuilder::buildDayRowsForEmployees` (source
|
- Calcul des cellules : `YearlyHoursExportBuilder::buildDayRowsForEmployees` (source
|
||||||
unique de vérité, partagée avec les exports annuels).
|
unique de vérité, partagée avec les exports annuels).
|
||||||
- Gabarit : `templates/work-hour-day-export/print.html.twig`.
|
- Gabarit : `templates/work-hour-day-export/print.html.twig`.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
groupClass="w-full mt-2"
|
groupClass="w-full mt-2"
|
||||||
label="Sites"
|
label="Sites"
|
||||||
display-select-all
|
display-select-all
|
||||||
|
display-tag
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,7 +50,6 @@ const props = defineProps<{
|
|||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
sites: Site[]
|
sites: Site[]
|
||||||
initialDate: string
|
initialDate: string
|
||||||
initialSiteIds: number[]
|
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ const drawerOpen = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const selectedDate = ref(props.initialDate)
|
const selectedDate = ref(props.initialDate)
|
||||||
const selectedSites = ref<number[]>([...props.initialSiteIds])
|
const selectedSites = ref<number[]>([])
|
||||||
|
|
||||||
const siteOptions = computed(() =>
|
const siteOptions = computed(() =>
|
||||||
props.sites.map((site) => ({ label: site.name, value: site.id }))
|
props.sites.map((site) => ({ label: site.name, value: site.id }))
|
||||||
@@ -80,7 +80,7 @@ watch(
|
|||||||
(isOpen) => {
|
(isOpen) => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
selectedDate.value = props.initialDate
|
selectedDate.value = props.initialDate
|
||||||
selectedSites.value = [...props.initialSiteIds]
|
selectedSites.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -84,9 +84,10 @@ export const documentationSections: DocSection[] = [
|
|||||||
{
|
{
|
||||||
id: 'export-heures-jour',
|
id: 'export-heures-jour',
|
||||||
title: 'Exporter les heures (PDF par jour)',
|
title: 'Exporter les heures (PDF par jour)',
|
||||||
requiredLevel: 'admin',
|
requiredLevel: 'site_manager',
|
||||||
blocks: [
|
blocks: [
|
||||||
{ type: 'paragraph', content: 'Le bouton « Exporter », à droite du titre « Heures » (visible uniquement en vue Jour), ouvre un panneau permettant de générer un PDF des heures d\'une journée. Choisissez la date et les sites concernés.' },
|
{ type: 'paragraph', content: 'Le bouton « Exporter », à droite du titre « Heures » (visible uniquement en vue Jour), ouvre un panneau permettant de générer un PDF des heures d\'une journée. Choisissez la date et les sites concernés.' },
|
||||||
|
{ type: 'paragraph', content: 'Les administrateurs peuvent exporter tous les sites. Un chef de site ne voit dans le panneau que ses propres sites et n\'exporte que ceux-ci.' },
|
||||||
{ type: 'paragraph', content: 'Le PDF est organisé par site et reprend les colonnes de la vue Jour (nom, statut, horaires matin/après-midi/soir, jour, nuit, total en gras), sans la colonne de validation. Les employés sous contrat ce jour-là apparaissent même sans saisie.' },
|
{ type: 'paragraph', content: 'Le PDF est organisé par site et reprend les colonnes de la vue Jour (nom, statut, horaires matin/après-midi/soir, jour, nuit, total en gras), sans la colonne de validation. Les employés sous contrat ce jour-là apparaissent même sans saisie.' },
|
||||||
{ type: 'paragraph', content: 'La colonne Statut affiche le code du type d\'absence (ex. « AT ») sur sa couleur. Une légende sous le tableau associe chaque code présent à son libellé.' },
|
{ type: 'paragraph', content: 'La colonne Statut affiche le code du type d\'absence (ex. « AT ») sur sa couleur. Une légende sous le tableau associe chaque code présent à son libellé.' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
<h1 class="text-2xl font-bold text-primary-500 lg:text-4xl">Heures</h1>
|
<h1 class="text-2xl font-bold text-primary-500 lg:text-4xl">Heures</h1>
|
||||||
<MalioButton
|
<MalioButton
|
||||||
v-if="isAdmin && viewMode === 'day'"
|
v-if="(isAdmin || isSiteManager) && viewMode === 'day'"
|
||||||
label="Export"
|
label="Export"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon-name="mdi:download"
|
icon-name="mdi:download"
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
v-model="isExportDrawerOpen"
|
v-model="isExportDrawerOpen"
|
||||||
:sites="sites"
|
:sites="sites"
|
||||||
:initial-date="selectedDate"
|
:initial-date="selectedDate"
|
||||||
:initial-site-ids="selectedSiteIds"
|
|
||||||
:is-loading="isExporting"
|
:is-loading="isExporting"
|
||||||
@submit="handleExport"
|
@submit="handleExport"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use App\State\WorkHourDayExportProvider;
|
|||||||
new QueryParameter(key: 'workDate', required: true),
|
new QueryParameter(key: 'workDate', required: true),
|
||||||
new QueryParameter(key: 'siteIds', required: true),
|
new QueryParameter(key: 'siteIds', required: true),
|
||||||
],
|
],
|
||||||
security: "is_granted('ROLE_ADMIN')"
|
security: "is_granted('ROLE_USER')"
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)]
|
)]
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ namespace App\State;
|
|||||||
|
|
||||||
use ApiPlatform\Metadata\Operation;
|
use ApiPlatform\Metadata\Operation;
|
||||||
use ApiPlatform\State\ProviderInterface;
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Entity\User;
|
||||||
use App\Repository\EmployeeRepository;
|
use App\Repository\EmployeeRepository;
|
||||||
use App\Service\WorkHours\YearlyHoursExportBuilder;
|
use App\Service\WorkHours\YearlyHoursExportBuilder;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Dompdf\Dompdf;
|
use Dompdf\Dompdf;
|
||||||
use Dompdf\Options;
|
use Dompdf\Options;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\HttpFoundation\RequestStack;
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
|
|
||||||
@@ -23,10 +26,16 @@ class WorkHourDayExportProvider implements ProviderInterface
|
|||||||
private readonly RequestStack $requestStack,
|
private readonly RequestStack $requestStack,
|
||||||
private EmployeeRepository $employeeRepository,
|
private EmployeeRepository $employeeRepository,
|
||||||
private YearlyHoursExportBuilder $exportBuilder,
|
private YearlyHoursExportBuilder $exportBuilder,
|
||||||
|
private readonly Security $security,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
|
||||||
{
|
{
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
throw new AccessDeniedHttpException('Authentication required.');
|
||||||
|
}
|
||||||
|
|
||||||
$request = $this->requestStack->getCurrentRequest();
|
$request = $this->requestStack->getCurrentRequest();
|
||||||
if (!$request) {
|
if (!$request) {
|
||||||
return new Response('Missing request.', Response::HTTP_BAD_REQUEST);
|
return new Response('Missing request.', Response::HTTP_BAD_REQUEST);
|
||||||
@@ -47,8 +56,9 @@ class WorkHourDayExportProvider implements ProviderInterface
|
|||||||
throw new UnprocessableEntityHttpException('siteIds is required.');
|
throw new UnprocessableEntityHttpException('siteIds is required.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feature réservée admin : on charge tous les employés puis on filtre.
|
// Périmètre selon le profil : admin → tous, chef de site → ses sites uniquement.
|
||||||
$employees = $this->employeeRepository->findAll();
|
// Les siteIds demandés ne peuvent donc pas déborder du scope de l'utilisateur.
|
||||||
|
$employees = $this->employeeRepository->findScoped($user);
|
||||||
|
|
||||||
// Regroupement par site (ordre displayOrder), non-conducteurs uniquement.
|
// Regroupement par site (ordre displayOrder), non-conducteurs uniquement.
|
||||||
$bySite = [];
|
$bySite = [];
|
||||||
@@ -78,7 +88,11 @@ class WorkHourDayExportProvider implements ProviderInterface
|
|||||||
$legend = [];
|
$legend = [];
|
||||||
foreach ($siteMeta as $siteId => $meta) {
|
foreach ($siteMeta as $siteId => $meta) {
|
||||||
$siteEmployees = $bySite[$siteId];
|
$siteEmployees = $bySite[$siteId];
|
||||||
usort($siteEmployees, static fn ($a, $b) => ($a->getLastName() ?? '') <=> ($b->getLastName() ?? ''));
|
// Même tri que le calendrier : ordre manuel (displayOrder) puis nom, puis prénom.
|
||||||
|
usort($siteEmployees, static function ($a, $b): int {
|
||||||
|
return [$a->getDisplayOrder(), $a->getLastName(), $a->getFirstName()]
|
||||||
|
<=> [$b->getDisplayOrder(), $b->getLastName(), $b->getFirstName()];
|
||||||
|
});
|
||||||
|
|
||||||
$rows = $this->exportBuilder->buildDayRowsForEmployees($siteEmployees, $date);
|
$rows = $this->exportBuilder->buildDayRowsForEmployees($siteEmployees, $date);
|
||||||
if ([] === $rows) {
|
if ([] === $rows) {
|
||||||
|
|||||||
Reference in New Issue
Block a user