diff --git a/src/Service/TimeEntryExportService.php b/src/Service/TimeEntryExportService.php new file mode 100644 index 0000000..f078f3a --- /dev/null +++ b/src/Service/TimeEntryExportService.php @@ -0,0 +1,314 @@ + '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); + } + } +}