diff --git a/src/Export/Base/CsvRenderer.php b/src/Export/Base/CsvRenderer.php index b54bcb5c..83f76779 100644 --- a/src/Export/Base/CsvRenderer.php +++ b/src/Export/Base/CsvRenderer.php @@ -11,6 +11,7 @@ namespace App\Export\Base; use App\Entity\ExportableItem; use App\Export\ExportFilename; +use App\Export\Package\CellFormatter\DateStringFormatter; use App\Export\Package\SpoutSpreadsheet; use App\Export\RendererInterface; use App\Export\TimesheetExportInterface; @@ -65,6 +66,7 @@ final class CsvRenderer implements RendererInterface, TimesheetExportInterface $spreadsheet = new SpoutSpreadsheet(new Writer($options)); $spreadsheet->open($filename); + $this->spreadsheetRenderer->registerFormatter('date', new DateStringFormatter()); $this->spreadsheetRenderer->writeSpreadsheet($spreadsheet, $exportItems, $query); return new \SplFileInfo($filename); diff --git a/src/Export/Base/SpreadsheetRenderer.php b/src/Export/Base/SpreadsheetRenderer.php index 3088b3ed..16e2f5a7 100644 --- a/src/Export/Base/SpreadsheetRenderer.php +++ b/src/Export/Base/SpreadsheetRenderer.php @@ -19,6 +19,7 @@ use App\Event\TimesheetMetaDisplayEvent; use App\Event\UserPreferenceDisplayEvent; use App\Export\Package\CellFormatter\ArrayFormatter; use App\Export\Package\CellFormatter\BooleanFormatter; +use App\Export\Package\CellFormatter\CellFormatterInterface; use App\Export\Package\CellFormatter\DateFormatter; use App\Export\Package\CellFormatter\DefaultFormatter; use App\Export\Package\CellFormatter\DurationFormatter; @@ -40,6 +41,11 @@ use Symfony\Contracts\Translation\TranslatorInterface; */ final class SpreadsheetRenderer { + /** + * @var array + */ + private array $formatter = []; + public function __construct( protected TranslatorInterface $translator, protected EventDispatcherInterface $dispatcher, @@ -115,6 +121,25 @@ final class SpreadsheetRenderer $spreadsheetPackage->save(); } + public function registerFormatter(string $name, CellFormatterInterface $cellFormatter): void + { + $this->formatter[$name] = $cellFormatter; + } + + private function getFormatter(string $name): CellFormatterInterface + { + if (\array_key_exists($name, $this->formatter)) { + return $this->formatter[$name]; + } + + return match ($name) { + 'date' => new DateFormatter(), + 'time' => new TimeFormatter(), + 'duration' => new DurationFormatter(), + default => new DefaultFormatter() + }; + } + /** * @return array */ @@ -124,40 +149,40 @@ final class SpreadsheetRenderer $columns = []; - $columns[] = (new Column('date', new DateFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getBegin()); - $columns[] = (new Column('begin', new TimeFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getBegin()); - $columns[] = (new Column('end', new TimeFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getEnd()); - $columns[] = (new Column('duration', new DurationFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getDuration()); + $columns[] = (new Column('date', $this->getFormatter('date')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getBegin()); + $columns[] = (new Column('begin', $this->getFormatter('time')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getBegin()); + $columns[] = (new Column('end', $this->getFormatter('time')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getEnd()); + $columns[] = (new Column('duration', $this->getFormatter('duration')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getDuration()); if ($showRates) { - $columns[] = (new Column('currency', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getCurrency()); + $columns[] = (new Column('currency', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getCurrency()); $columns[] = (new Column('rate', new RateFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getRate()); $columns[] = (new Column('internalRate', new RateFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getInternalRate()); $columns[] = (new Column('hourlyRate', new RateFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getHourlyRate()); $columns[] = (new Column('fixedRate', new RateFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getFixedRate()); } - $columns[] = (new Column('username', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getDisplayName()); - $columns[] = (new Column('account_number', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getAccountNumber()); - $columns[] = (new Column('customer', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getName()); - $columns[] = (new Column('project', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getName()); - $columns[] = (new Column('activity', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getActivity()?->getName()); + $columns[] = (new Column('username', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getDisplayName()); + $columns[] = (new Column('account_number', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getUser()?->getAccountNumber()); + $columns[] = (new Column('customer', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getName()); + $columns[] = (new Column('project', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getName()); + $columns[] = (new Column('activity', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getActivity()?->getName()); $columns[] = (new Column('description', new TextFormatter(true)))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getDescription()); //$columns[] = (new Column('exported', new BooleanFormatter()))->withExtractor(fn(ExportableItem $exportableItem) => $exportableItem->isExported()); $columns[] = (new Column('billable', new BooleanFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->isBillable()); $columns[] = (new Column('tags', new ArrayFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getTagsAsArray()); - $columns[] = (new Column('type', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getType()); - $columns[] = (new Column('category', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getCategory()); - $columns[] = (new Column('number', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getNumber()); - $columns[] = (new Column('project_number', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getNumber()); - $columns[] = (new Column('vat_id', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getVatId()); - $columns[] = (new Column('orderNumber', new DefaultFormatter()))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getOrderNumber()); + $columns[] = (new Column('type', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getType()); + $columns[] = (new Column('category', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getCategory()); + $columns[] = (new Column('number', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getNumber()); + $columns[] = (new Column('project_number', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getNumber()); + $columns[] = (new Column('vat_id', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getCustomer()?->getVatId()); + $columns[] = (new Column('orderNumber', $this->getFormatter('default')))->withExtractor(fn (ExportableItem $exportableItem) => $exportableItem->getProject()?->getOrderNumber()); foreach ($this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT)) as $metaField) { if ($metaField->getName() === null) { continue; } - $columns[] = (new Column('timesheet.meta.' . $metaField->getName(), new DefaultFormatter())) + $columns[] = (new Column('timesheet.meta.' . $metaField->getName(), $this->getFormatter('default'))) ->withHeader($metaField->getLabel()) ->withExtractor(function (ExportableItem $exportableItem) use ($metaField) { return $exportableItem->getMetaField($metaField->getName())?->getValue(); @@ -168,7 +193,7 @@ final class SpreadsheetRenderer if ($metaField->getName() === null) { continue; } - $columns[] = (new Column('customer.meta.' . $metaField->getName(), new DefaultFormatter())) + $columns[] = (new Column('customer.meta.' . $metaField->getName(), $this->getFormatter('default'))) ->withHeader($metaField->getLabel()) ->withExtractor(function (ExportableItem $exportableItem) use ($metaField) { return $exportableItem->getProject()?->getCustomer()?->getMetaField($metaField->getName())?->getValue(); @@ -179,7 +204,7 @@ final class SpreadsheetRenderer if ($metaField->getName() === null) { continue; } - $columns[] = (new Column('project.meta.' . $metaField->getName(), new DefaultFormatter())) + $columns[] = (new Column('project.meta.' . $metaField->getName(), $this->getFormatter('default'))) ->withHeader($metaField->getLabel()) ->withExtractor(function (ExportableItem $exportableItem) use ($metaField) { return $exportableItem->getProject()?->getMetaField($metaField->getName())?->getValue(); @@ -190,7 +215,7 @@ final class SpreadsheetRenderer if ($metaField->getName() === null) { continue; } - $columns[] = (new Column('activity.meta.' . $metaField->getName(), new DefaultFormatter())) + $columns[] = (new Column('activity.meta.' . $metaField->getName(), $this->getFormatter('default'))) ->withHeader($metaField->getLabel()) ->withExtractor(function (ExportableItem $exportableItem) use ($metaField) { return $exportableItem->getActivity()?->getMetaField($metaField->getName())?->getValue(); @@ -203,7 +228,7 @@ final class SpreadsheetRenderer if ($metaField->getName() === null) { continue; } - $columns[] = (new Column('user.meta.' . $metaField->getName(), new DefaultFormatter())) + $columns[] = (new Column('user.meta.' . $metaField->getName(), $this->getFormatter('default'))) ->withHeader($metaField->getLabel()) ->withExtractor(function (ExportableItem $exportableItem) use ($metaField) { return $exportableItem->getUser()?->getPreference($metaField->getName())?->getValue(); diff --git a/src/Export/Package/CellFormatter/DateFormatter.php b/src/Export/Package/CellFormatter/DateFormatter.php index 22f029f1..3240070d 100644 --- a/src/Export/Package/CellFormatter/DateFormatter.php +++ b/src/Export/Package/CellFormatter/DateFormatter.php @@ -14,7 +14,7 @@ final class DateFormatter implements CellFormatterInterface public function formatValue(mixed $value): mixed { if ($value instanceof \DateTimeInterface) { - return $value->format('Y-m-d'); + return $value; } if ($value === null) { diff --git a/src/Export/Package/CellFormatter/DateStringFormatter.php b/src/Export/Package/CellFormatter/DateStringFormatter.php new file mode 100644 index 00000000..bd92b04b --- /dev/null +++ b/src/Export/Package/CellFormatter/DateStringFormatter.php @@ -0,0 +1,26 @@ +format('Y-m-d'); + } + + if ($value === null) { + return null; + } + + throw new \InvalidArgumentException('Only DateTimeInterface can be formatted'); + } +} diff --git a/src/Export/Package/SpoutSpreadsheet.php b/src/Export/Package/SpoutSpreadsheet.php index ecc0eff8..1c836e2c 100644 --- a/src/Export/Package/SpoutSpreadsheet.php +++ b/src/Export/Package/SpoutSpreadsheet.php @@ -23,9 +23,12 @@ use OpenSpout\Writer\XLSX\Entity\SheetView; class SpoutSpreadsheet implements SpreadsheetPackage { + private Style $dateStyle; + public function __construct(private readonly WriterInterface $writer) { $this->writer->setCreator(Constants::SOFTWARE); + $this->dateStyle = (new Style())->setFormat('yyyy-mm-dd'); } /** @@ -68,7 +71,11 @@ class SpoutSpreadsheet implements SpreadsheetPackage $tmp = []; foreach ($columns as $column) { - $tmp[] = Cell::fromValue($column); // @phpstan-ignore argument.type + if ($column instanceof \DateTimeInterface) { + $tmp[] = Cell::fromValue($column, $this->dateStyle); + } else { + $tmp[] = Cell::fromValue($column); // @phpstan-ignore argument.type + } } $this->writer->addRow(new Row($tmp, $style)); diff --git a/tests/Export/Package/CellFormatter/DateFormatterTest.php b/tests/Export/Package/CellFormatter/DateFormatterTest.php index a9ae5c79..1882c176 100644 --- a/tests/Export/Package/CellFormatter/DateFormatterTest.php +++ b/tests/Export/Package/CellFormatter/DateFormatterTest.php @@ -22,7 +22,7 @@ class DateFormatterTest extends TestCase $formatter = new DateFormatter(); $date = new \DateTime('2023-10-01'); $result = $formatter->formatValue($date); - self::assertEquals('2023-10-01', $result); + $this->assertInstanceOf(\DateTimeInterface::class, $result); } public function testFormatValueReturnsNullForNullValue(): void diff --git a/tests/Export/Package/CellFormatter/DateStringFormatterTest.php b/tests/Export/Package/CellFormatter/DateStringFormatterTest.php new file mode 100644 index 00000000..12939360 --- /dev/null +++ b/tests/Export/Package/CellFormatter/DateStringFormatterTest.php @@ -0,0 +1,44 @@ +formatValue($date); + $this->assertIsString($result); + $this->assertEquals('2023-10-01', $result); + } + + public function testFormatValueReturnsNullForNullValue(): void + { + $formatter = new DateStringFormatter(); + $result = $formatter->formatValue(null); + self::assertNull($result); + } + + public function testFormatValueThrowsExceptionForNonDateTime(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Only DateTimeInterface can be formatted'); + + $formatter = new DateStringFormatter(); + $formatter->formatValue('not a date'); + } +}