315 lines
11 KiB
PHP
315 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service;
|
|
|
|
use App\Entity\TimeEntry;
|
|
use DateTimeImmutable;
|
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
|
|
|
use function count;
|
|
|
|
class TimeEntryExportService
|
|
{
|
|
private const array DETAIL_HEADERS = [
|
|
'Date', 'Utilisateur', 'Projet', 'Tâche', 'Titre',
|
|
'Tags', 'Début', 'Fin', 'Durée (h)', 'Description',
|
|
];
|
|
|
|
private const array MONTH_NAMES = [
|
|
1 => '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);
|
|
}
|
|
}
|
|
}
|