'Janvier', 2 => 'Février', 3 => 'Mars', 4 => 'Avril', 5 => 'Mai', 6 => 'Juin', 7 => 'Juillet', 8 => 'Août', 9 => 'Septembre', 10 => 'Octobre', 11 => 'Novembre', 12 => 'Décembre', ]; /** * @param TimeEntry[] $timeEntries * * @return string Path to the generated temp file */ public function generate(array $timeEntries, DateTimeImmutable $from, DateTimeImmutable $to): string { $spreadsheet = new Spreadsheet(); $this->buildDetailSheet($spreadsheet, $timeEntries); $this->buildProjectRecapSheet($spreadsheet, $timeEntries); $this->buildMonthRecapSheet($spreadsheet, $timeEntries, $from, $to); $spreadsheet->setActiveSheetIndex(0); $tempFile = tempnam(sys_get_temp_dir(), 'export_temps_').'.xlsx'; $writer = new Xlsx($spreadsheet); $writer->save($tempFile); return $tempFile; } /** * @param TimeEntry[] $timeEntries */ private function buildDetailSheet(Spreadsheet $spreadsheet, array $timeEntries): void { $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Détail'); // Headers foreach (self::DETAIL_HEADERS as $col => $header) { $colLetter = Coordinate::stringFromColumnIndex($col + 1); $sheet->setCellValue("{$colLetter}1", $header); } $this->boldRow($sheet, 1, count(self::DETAIL_HEADERS)); // Data rows $row = 2; foreach ($timeEntries as $entry) { $duration = $this->computeDuration($entry); $task = $entry->getTask(); $taskLabel = ''; if (null !== $task) { $project = $task->getProject(); $code = $project?->getCode() ?? ''; $taskLabel = $code.'-'.$task->getNumber().' - '.$task->getTitle(); } $tagLabels = $entry->getTags()->map(fn ($t) => $t->getLabel() ?? '')->toArray(); $sheet->setCellValue("A{$row}", $entry->getStartedAt()->format('Y-m-d')); $sheet->setCellValue("B{$row}", $entry->getUser()?->getUsername() ?? ''); $sheet->setCellValue("C{$row}", $entry->getProject()?->getName() ?? ''); $sheet->setCellValue("D{$row}", $taskLabel); $sheet->setCellValue("E{$row}", $entry->getTitle() ?? ''); $sheet->setCellValue("F{$row}", implode(', ', $tagLabels)); $sheet->setCellValue("G{$row}", $entry->getStartedAt()->format('H:i')); $sheet->setCellValue("H{$row}", $entry->getStoppedAt()?->format('H:i') ?? ''); $sheet->setCellValue("I{$row}", round($duration, 2)); $sheet->setCellValue("J{$row}", $entry->getDescription() ?? ''); ++$row; } // Total row if ($row > 2) { $sheet->setCellValue("H{$row}", 'Total'); $sheet->getStyle("H{$row}")->getFont()->setBold(true); $sheet->setCellValue("I{$row}", '=SUM(I2:I'.($row - 1).')'); $sheet->getStyle("I{$row}")->getFont()->setBold(true); } // Auto-size columns foreach (range('A', 'J') as $col) { $sheet->getColumnDimension($col)->setAutoSize(true); } } /** * @param TimeEntry[] $timeEntries */ private function buildProjectRecapSheet(Spreadsheet $spreadsheet, array $timeEntries): void { $sheet = $spreadsheet->createSheet(); $sheet->setTitle('Récap par projet'); // Aggregate: user → project → hours $data = []; $projects = []; $users = []; foreach ($timeEntries as $entry) { $userName = $entry->getUser()?->getUsername() ?? 'Inconnu'; $projectName = $entry->getProject()?->getName() ?? 'Sans projet'; $duration = $this->computeDuration($entry); $users[$userName] = true; $projects[$projectName] = true; $data[$userName][$projectName] = ($data[$userName][$projectName] ?? 0) + $duration; } ksort($users); ksort($projects); $projectList = array_keys($projects); $userList = array_keys($users); // Headers $sheet->setCellValue('A1', 'Utilisateur'); $col = 2; foreach ($projectList as $project) { $letter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$letter}1", $project); ++$col; } $totalLetter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$totalLetter}1", 'Total'); $this->boldRow($sheet, 1, $col); // Data rows $row = 2; foreach ($userList as $user) { $sheet->setCellValue("A{$row}", $user); $col = 2; $userTotal = 0; foreach ($projectList as $project) { $val = round($data[$user][$project] ?? 0, 2); $letter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$letter}{$row}", $val); $userTotal += $val; ++$col; } $letter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$letter}{$row}", round($userTotal, 2)); $sheet->getStyle("{$letter}{$row}")->getFont()->setBold(true); ++$row; } // Total row $sheet->setCellValue("A{$row}", 'Total'); $sheet->getStyle("A{$row}")->getFont()->setBold(true); $col = 2; foreach ($projectList as $project) { $projectTotal = 0; foreach ($userList as $user) { $projectTotal += $data[$user][$project] ?? 0; } $letter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$letter}{$row}", round($projectTotal, 2)); $sheet->getStyle("{$letter}{$row}")->getFont()->setBold(true); ++$col; } // Grand total $grandTotal = 0; foreach ($data as $userData) { foreach ($userData as $hours) { $grandTotal += $hours; } } $letter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$letter}{$row}", round($grandTotal, 2)); $sheet->getStyle("{$letter}{$row}")->getFont()->setBold(true); // Auto-size for ($c = 1; $c <= $col; ++$c) { $sheet->getColumnDimension(Coordinate::stringFromColumnIndex($c))->setAutoSize(true); } } /** * @param TimeEntry[] $timeEntries */ private function buildMonthRecapSheet(Spreadsheet $spreadsheet, array $timeEntries, DateTimeImmutable $from, DateTimeImmutable $to): void { $sheet = $spreadsheet->createSheet(); $sheet->setTitle('Récap par mois'); // Build month columns from the date range $months = []; $current = $from->modify('first day of this month'); $end = $to->modify('first day of this month'); while ($current <= $end) { $key = $current->format('Y-m'); $label = self::MONTH_NAMES[(int) $current->format('n')].' '.$current->format('Y'); $months[$key] = $label; $current = $current->modify('+1 month'); } // Aggregate: user → month-key → hours $data = []; $users = []; foreach ($timeEntries as $entry) { $userName = $entry->getUser()?->getUsername() ?? 'Inconnu'; $monthKey = $entry->getStartedAt()->format('Y-m'); $duration = $this->computeDuration($entry); $users[$userName] = true; $data[$userName][$monthKey] = ($data[$userName][$monthKey] ?? 0) + $duration; } ksort($users); $userList = array_keys($users); $monthKeys = array_keys($months); // Headers $sheet->setCellValue('A1', 'Utilisateur'); $col = 2; foreach ($months as $label) { $letter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$letter}1", $label); ++$col; } $totalLetter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$totalLetter}1", 'Total'); $this->boldRow($sheet, 1, $col); // Data rows $row = 2; foreach ($userList as $user) { $sheet->setCellValue("A{$row}", $user); $col = 2; $userTotal = 0; foreach ($monthKeys as $monthKey) { $val = round($data[$user][$monthKey] ?? 0, 2); $letter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$letter}{$row}", $val); $userTotal += $val; ++$col; } $letter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$letter}{$row}", round($userTotal, 2)); $sheet->getStyle("{$letter}{$row}")->getFont()->setBold(true); ++$row; } // Total row $sheet->setCellValue("A{$row}", 'Total'); $sheet->getStyle("A{$row}")->getFont()->setBold(true); $col = 2; foreach ($monthKeys as $monthKey) { $monthTotal = 0; foreach ($userList as $user) { $monthTotal += $data[$user][$monthKey] ?? 0; } $letter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$letter}{$row}", round($monthTotal, 2)); $sheet->getStyle("{$letter}{$row}")->getFont()->setBold(true); ++$col; } $grandTotal = 0; foreach ($data as $userData) { foreach ($userData as $hours) { $grandTotal += $hours; } } $letter = Coordinate::stringFromColumnIndex($col); $sheet->setCellValue("{$letter}{$row}", round($grandTotal, 2)); $sheet->getStyle("{$letter}{$row}")->getFont()->setBold(true); // Auto-size for ($c = 1; $c <= $col; ++$c) { $sheet->getColumnDimension(Coordinate::stringFromColumnIndex($c))->setAutoSize(true); } } private function computeDuration(TimeEntry $entry): float { $start = $entry->getStartedAt(); $end = $entry->getStoppedAt(); if (null === $start || null === $end) { return 0; } return ($end->getTimestamp() - $start->getTimestamp()) / 3600; } private function boldRow(Worksheet $sheet, int $row, int $colCount): void { for ($c = 1; $c <= $colCount; ++$c) { $letter = Coordinate::stringFromColumnIndex($c); $sheet->getStyle("{$letter}{$row}")->getFont()->setBold(true); } } }