faster spreadsheet exporter based on opensout and other export improvements (#5238)
This commit is contained in:
@@ -78,11 +78,6 @@ final class ProjectViewController extends AbstractController
|
||||
{
|
||||
$data = $this->getData($request, $service);
|
||||
|
||||
// Projektübersicht inkl. dem was Projektdetails anzeigen
|
||||
// Budget / Zeitbudget
|
||||
// Abrechenbar
|
||||
// Interner Preis
|
||||
|
||||
$content = $this->renderView('reporting/project_list_export.html.twig', $data);
|
||||
|
||||
$reader = new Html();
|
||||
|
||||
@@ -41,7 +41,7 @@ final class UserMonthController extends AbstractUserReportController
|
||||
#[Route(path: '/month_export', name: 'report_user_month_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$data = $this->getData($request);
|
||||
$data = $this->getData($request, true);
|
||||
|
||||
$content = $this->renderView('reporting/report_by_user_data.html.twig', $data);
|
||||
|
||||
@@ -53,13 +53,14 @@ final class UserMonthController extends AbstractUserReportController
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
private function getData(Request $request): array
|
||||
private function getData(Request $request, bool $export = false): array
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
|
||||
$canChangeUser = $this->canSelectUser();
|
||||
|
||||
$values = new MonthByUser();
|
||||
$values->setDecimal($export);
|
||||
$values->setUser($currentUser);
|
||||
$values->setDate($dateTimeFactory->getStartOfMonth());
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ final class UserWeekController extends AbstractUserReportController
|
||||
#[Route(path: '/week_export', name: 'report_user_week_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request): Response
|
||||
{
|
||||
$data = $this->getData($request);
|
||||
$data = $this->getData($request, true);
|
||||
|
||||
$content = $this->renderView('reporting/report_by_user_data.html.twig', $data);
|
||||
|
||||
@@ -52,13 +52,14 @@ final class UserWeekController extends AbstractUserReportController
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
private function getData(Request $request): array
|
||||
private function getData(Request $request, bool $export = false): array
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
|
||||
$canChangeUser = $this->canSelectUser();
|
||||
|
||||
$values = new WeekByUser();
|
||||
$values->setDecimal($export);
|
||||
$values->setUser($currentUser);
|
||||
$values->setDate($dateTimeFactory->getStartOfWeek());
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ final class UserYearController extends AbstractUserReportController
|
||||
#[Route(path: '/year_export', name: 'report_user_year_export', methods: ['GET', 'POST'])]
|
||||
public function export(Request $request, SystemConfiguration $systemConfiguration): Response
|
||||
{
|
||||
$data = $this->getData($request, $systemConfiguration);
|
||||
$data = $this->getData($request, $systemConfiguration, true);
|
||||
|
||||
$content = $this->renderView('reporting/report_by_user_year_export.html.twig', $data);
|
||||
|
||||
@@ -51,13 +51,14 @@ final class UserYearController extends AbstractUserReportController
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
private function getData(Request $request, SystemConfiguration $systemConfiguration): array
|
||||
private function getData(Request $request, SystemConfiguration $systemConfiguration, bool $export = false): array
|
||||
{
|
||||
$currentUser = $this->getUser();
|
||||
$dateTimeFactory = $this->getDateTimeFactory($currentUser);
|
||||
$canChangeUser = $this->canSelectUser();
|
||||
|
||||
$values = new YearByUser();
|
||||
$values->setDecimal($export);
|
||||
$values->setUser($currentUser);
|
||||
|
||||
$defaultDate = $dateTimeFactory->createStartOfYear();
|
||||
|
||||
@@ -1,809 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Base;
|
||||
|
||||
use App\Entity\ExportableItem;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Event\ActivityMetaDisplayEvent;
|
||||
use App\Event\CustomerMetaDisplayEvent;
|
||||
use App\Event\MetaDisplayEventInterface;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Event\TimesheetMetaDisplayEvent;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\ExportFilename;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Twig\LocaleFormatExtensions;
|
||||
use App\Utils\StringHelper;
|
||||
use DateTime;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @internal means no BC promise whatsoever!
|
||||
*/
|
||||
abstract class AbstractSpreadsheetRenderer
|
||||
{
|
||||
public const DATETIME_FORMAT = 'yyyy-mm-dd hh:mm';
|
||||
public const TIME_FORMAT = 'hh:mm';
|
||||
public const DURATION_FORMAT = '[hh]:mm';
|
||||
public const DURATION_DECIMAL = '#0.00';
|
||||
|
||||
// https://support.microsoft.com/de-de/office/zahlenformatcodes-5026bbd6-04bc-48cd-bf33-80f18b4eae68
|
||||
// Part 1 = positive; Part 2 = negative; Part 3 = zero; Part 4 = Text
|
||||
public const RATE_FORMAT_DEFAULT = '#.##0,00 [$%1$s];-#.##0,00 [$%1$s]';
|
||||
public const RATE_FORMAT_LEFT = '_("%1$s"* #,##0.00_);_("%1$s"* -#,##0.00;_("%1$s"* "-"??_);_(@_)';
|
||||
public const RATE_FORMAT_RIGHT = '_(* "%1$s" #,##0.00_);_(* "%1$s" -#,##0.00;_(* "%1$s" "-"??_);_(@_)';
|
||||
|
||||
/**
|
||||
* @internal used in html to excel exporter
|
||||
*/
|
||||
public const RATE_FORMAT_NO_CURRENCY = '#,##0.00;-#,##0.00';
|
||||
|
||||
/**
|
||||
* @see self:RATE_FORMAT_*
|
||||
*/
|
||||
protected string $rateFormat = self::RATE_FORMAT_LEFT;
|
||||
protected string $durationFormat = self::DURATION_FORMAT;
|
||||
protected int $durationBase = 86400;
|
||||
/**
|
||||
* @var array<string, array>
|
||||
*/
|
||||
protected array $columns = [
|
||||
'date' => [],
|
||||
'begin' => [],
|
||||
'end' => [],
|
||||
'duration' => [],
|
||||
'rate' => [],
|
||||
'rate_internal' => [
|
||||
'label' => 'internalRate', // different translation key
|
||||
],
|
||||
'user' => [
|
||||
'label' => 'name'
|
||||
],
|
||||
'username' => [],
|
||||
'accountNumber' => [
|
||||
'label' => 'account_number'
|
||||
],
|
||||
'customer' => [],
|
||||
'project' => [],
|
||||
'activity' => [],
|
||||
'description' => [
|
||||
'maxWidth' => 50,
|
||||
'wrapText' => false,
|
||||
'sanitizeDDE' => true,
|
||||
],
|
||||
'exported' => [],
|
||||
'billable' => [],
|
||||
'tags' => [],
|
||||
'hourlyRate' => [],
|
||||
'fixedRate' => [],
|
||||
'timesheet-meta' => [],
|
||||
'customer-meta' => [],
|
||||
'project-meta' => [],
|
||||
'activity-meta' => [],
|
||||
'user-meta' => [],
|
||||
'type' => [],
|
||||
'category' => [],
|
||||
'customer_number' => [],
|
||||
'customer_vat' => [],
|
||||
'order_number' => [],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator,
|
||||
protected LocaleFormatExtensions $dateExtension,
|
||||
protected EventDispatcherInterface $dispatcher,
|
||||
protected Security $voter
|
||||
) {
|
||||
}
|
||||
|
||||
protected function isRenderRate(TimesheetQuery $query): bool
|
||||
{
|
||||
if ($this->voter->getUser() === null) {
|
||||
// for command line export
|
||||
return true;
|
||||
}
|
||||
|
||||
if (null !== $query->getUser()) {
|
||||
return $this->voter->isGranted('view_rate_own_timesheet');
|
||||
}
|
||||
|
||||
return $this->voter->isGranted('view_rate_other_timesheet');
|
||||
}
|
||||
|
||||
protected function setFormattedDateTime(Worksheet $sheet, int $column, int $row, ?DateTime $date): void
|
||||
{
|
||||
if (null === $date) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$excelDate = Date::PHPToExcel($date);
|
||||
|
||||
if ($excelDate === false) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $date);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $excelDate);
|
||||
// TODO why is that format hardcoded and does not depend on the users locale?
|
||||
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
|
||||
}
|
||||
|
||||
protected function setFormattedTime(Worksheet $sheet, int $column, int $row, ?DateTime $date): void
|
||||
{
|
||||
if (null === $date) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$excelDate = Date::PHPToExcel($date);
|
||||
|
||||
if ($excelDate === false) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $date);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $excelDate);
|
||||
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
|
||||
}
|
||||
|
||||
protected function setFormattedDate(Worksheet $sheet, int $column, int $row, ?DateTime $date): void
|
||||
{
|
||||
if (null === $date) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$excelDate = Date::PHPToExcel($date);
|
||||
|
||||
if ($excelDate === false) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $date);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $excelDate);
|
||||
// TODO why is that format hardcoded and does not depend on the users locale?
|
||||
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD);
|
||||
}
|
||||
|
||||
protected function setDurationTotal(Worksheet $sheet, int $column, int $row, string $startCoordinate, string $endCoordinate): void
|
||||
{
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), \sprintf('=SUBTOTAL(9,%s:%s)', $startCoordinate, $endCoordinate));
|
||||
$style = $sheet->getStyle(CellAddress::fromColumnAndRow($column, $row));
|
||||
$style->getNumberFormat()->setFormatCode($this->durationFormat);
|
||||
}
|
||||
|
||||
protected function setDuration(Worksheet $sheet, int $column, int $row, ?int $duration): void
|
||||
{
|
||||
if (null === $duration) {
|
||||
$duration = 0;
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), \sprintf('=%s/%s', $duration, $this->durationBase));
|
||||
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode($this->durationFormat);
|
||||
}
|
||||
|
||||
protected function setRateTotal(Worksheet $sheet, int $column, int $row, string $startCoordinate, string $endCoordinate): void
|
||||
{
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), \sprintf('=SUBTOTAL(9,%s:%s)', $startCoordinate, $endCoordinate));
|
||||
}
|
||||
|
||||
protected function setRateStyle(Worksheet $sheet, int $column, int $row, ?string $currency): void
|
||||
{
|
||||
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(
|
||||
\sprintf($this->rateFormat, $currency ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
protected function setRate(Worksheet $sheet, int $column, int $row, ?float $rate, ?string $currency): void
|
||||
{
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $rate ?? 0.0);
|
||||
$this->setRateStyle($sheet, $column, $row, $currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MetaDisplayEventInterface $event
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
protected function findMetaColumns(MetaDisplayEventInterface $event): array
|
||||
{
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $event->getFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExportableItem[] $exportItems
|
||||
* @param TimesheetQuery $query
|
||||
* @param array<string, array<string, callable|int|float|false|null>> $columns
|
||||
* @return array<string, array<string, callable|int|float|false|null>>
|
||||
*/
|
||||
protected function getColumns(array $exportItems, TimesheetQuery $query, array $columns): array
|
||||
{
|
||||
if (null !== $query->getCurrentUser() && $query->getCurrentUser()->isExportDecimal()) {
|
||||
$this->durationFormat = self::DURATION_DECIMAL;
|
||||
$this->durationBase = 3600;
|
||||
}
|
||||
|
||||
$showRates = $this->isRenderRate($query);
|
||||
|
||||
if (isset($columns['date']) && !isset($columns['date']['render'])) {
|
||||
$columns['date']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$this->setFormattedDate($sheet, $column, $row, $entity->getBegin());
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['begin']) && !isset($columns['begin']['render'])) {
|
||||
$columns['begin']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$this->setFormattedTime($sheet, $column, $row, $entity->getBegin());
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['end']) && !isset($columns['end']['render'])) {
|
||||
$columns['end']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$this->setFormattedTime($sheet, $column, $row, $entity->getEnd());
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['duration']) && !isset($columns['duration']['render'])) {
|
||||
$columns['duration']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$this->setDuration($sheet, $column, $row, $entity->getDuration());
|
||||
};
|
||||
}
|
||||
|
||||
if ($showRates && isset($columns['rate']) && !isset($columns['rate']['render'])) {
|
||||
$columns['rate']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$currency = '';
|
||||
if (null !== $entity->getProject()) {
|
||||
$currency = $entity->getProject()->getCustomer()->getCurrency();
|
||||
}
|
||||
$this->setRate($sheet, $column, $row, $entity->getRate(), $currency);
|
||||
};
|
||||
}
|
||||
|
||||
if ($showRates && isset($columns['rate_internal']) && !isset($columns['rate_internal']['render'])) {
|
||||
$columns['rate_internal']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$currency = '';
|
||||
if (null !== $entity->getProject()) {
|
||||
$currency = $entity->getProject()->getCustomer()->getCurrency();
|
||||
}
|
||||
$this->setRate($sheet, $column, $row, $entity->getInternalRate(), $currency);
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['user']) && !isset($columns['user']['render'])) {
|
||||
$columns['user']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$user = '';
|
||||
if (null !== $entity->getUser()) {
|
||||
$user = $entity->getUser()->getDisplayName();
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $user);
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['username'])) {
|
||||
if (!isset($columns['username']['render'])) {
|
||||
$columns['username']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$username = '';
|
||||
if (null !== $entity->getUser()) {
|
||||
$username = $entity->getUser()->getUserIdentifier();
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $username);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($columns['accountNumber'])) {
|
||||
if (!isset($columns['accountNumber']['render'])) {
|
||||
$columns['accountNumber']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$accountNumber = '';
|
||||
if (null !== $entity->getUser()) {
|
||||
$accountNumber = $entity->getUser()->getAccountNumber();
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $accountNumber);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($columns['customer']) && !isset($columns['customer']['render'])) {
|
||||
$columns['customer']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$customer = '';
|
||||
if (null !== $entity->getProject()) {
|
||||
$customer = $entity->getProject()->getCustomer()->getName();
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $customer);
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['project']) && !isset($columns['project']['render'])) {
|
||||
$columns['project']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$project = '';
|
||||
if (null !== $entity->getProject()) {
|
||||
$project = $entity->getProject()->getName();
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $project);
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['activity']) && !isset($columns['activity']['render'])) {
|
||||
$columns['activity']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$activity = '';
|
||||
if (null !== $entity->getActivity()) {
|
||||
$activity = $entity->getActivity()->getName();
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $activity);
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['description']) && !isset($columns['description']['render'])) {
|
||||
$maxWidth = \array_key_exists('maxWidth', $columns['description']) && is_numeric($columns['description']['maxWidth']) ? (int) $columns['description']['maxWidth'] : null;
|
||||
$wrapText = \array_key_exists('wrapText', $columns['description']) ? (bool) $columns['description']['wrapText'] : false;
|
||||
$sanitizeText = \array_key_exists('sanitizeDDE', $columns['description']) ? (bool) $columns['description']['sanitizeDDE'] : true;
|
||||
|
||||
// This column has a column-only formatter to set the maximum width of a column.
|
||||
// It needs to be executed once, so we use this as a flag on when to skip it.
|
||||
$isColumnFormatted = false;
|
||||
|
||||
$columns['description']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) use (&$isColumnFormatted, $maxWidth, $wrapText, $sanitizeText) {
|
||||
$cell = $sheet->getCell(CellAddress::fromColumnAndRow($column, $row));
|
||||
$desc = $entity->getDescription();
|
||||
|
||||
if ($sanitizeText && null !== $desc) {
|
||||
$desc = StringHelper::sanitizeDDE($desc);
|
||||
}
|
||||
|
||||
$cell->setValueExplicit($desc, DataType::TYPE_STRING);
|
||||
|
||||
// Apply wrap text if configured
|
||||
if ($wrapText) {
|
||||
$cell->getStyle()->getAlignment()->setWrapText(true);
|
||||
}
|
||||
|
||||
// Apply max width, only needs to be once per column
|
||||
if (!$isColumnFormatted) {
|
||||
if (null !== $maxWidth) {
|
||||
$sheet->getColumnDimensionByColumn($column)->setWidth($maxWidth);
|
||||
}
|
||||
$isColumnFormatted = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['exported']) && !isset($columns['exported']['render'])) {
|
||||
$columns['exported']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$exported = $entity->isExported() ? 'yes' : 'no';
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans($exported));
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['billable']) && !isset($columns['billable']['render'])) {
|
||||
$columns['billable']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$exported = $entity->isBillable() ? 'yes' : 'no';
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans($exported));
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['tags']) && !isset($columns['tags']['render'])) {
|
||||
$columns['tags']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), implode(',', $entity->getTagsAsArray()));
|
||||
};
|
||||
}
|
||||
|
||||
if ($showRates && isset($columns['hourlyRate']) && !isset($columns['hourlyRate']['render'])) {
|
||||
$columns['hourlyRate']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$currency = '';
|
||||
if (null !== $entity->getProject()) {
|
||||
$currency = $entity->getProject()->getCustomer()->getCurrency();
|
||||
}
|
||||
$this->setRate($sheet, $column, $row, $entity->getHourlyRate(), $currency);
|
||||
};
|
||||
}
|
||||
|
||||
if ($showRates && isset($columns['fixedRate']) && !isset($columns['fixedRate']['render'])) {
|
||||
$columns['fixedRate']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$currency = '';
|
||||
if (null !== $entity->getProject()) {
|
||||
$currency = $entity->getProject()->getCustomer()->getCurrency();
|
||||
}
|
||||
$this->setRate($sheet, $column, $row, $entity->getFixedRate(), $currency);
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['timesheet-meta'])) {
|
||||
$timesheetMetaFields = $this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT));
|
||||
|
||||
$columns['timesheet-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($timesheetMetaFields): int {
|
||||
foreach ($timesheetMetaFields as $metaField) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||
}
|
||||
|
||||
return \count($timesheetMetaFields);
|
||||
},
|
||||
'render' => function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) use ($timesheetMetaFields): int {
|
||||
foreach ($timesheetMetaFields as $metaField) {
|
||||
$metaFieldValue = '';
|
||||
$metaField = $entity->getMetaField($metaField->getName());
|
||||
if (null !== $metaField) {
|
||||
$metaFieldValue = $metaField->getValue();
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
|
||||
}
|
||||
|
||||
return \count($timesheetMetaFields);
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($columns['customer-meta'])) {
|
||||
$customerMetaFields = $this->findMetaColumns(new CustomerMetaDisplayEvent($query->copyTo(new CustomerQuery()), CustomerMetaDisplayEvent::EXPORT));
|
||||
|
||||
$columns['customer-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($customerMetaFields): int {
|
||||
foreach ($customerMetaFields as $metaField) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||
}
|
||||
|
||||
return \count($customerMetaFields);
|
||||
},
|
||||
'render' => function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) use ($customerMetaFields): int {
|
||||
foreach ($customerMetaFields as $metaField) {
|
||||
$metaFieldValue = '';
|
||||
if (null !== $entity->getProject()) {
|
||||
$metaField = $entity->getProject()->getCustomer()->getMetaField($metaField->getName());
|
||||
if (null !== $metaField) {
|
||||
$metaFieldValue = $metaField->getValue();
|
||||
}
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
|
||||
}
|
||||
|
||||
return \count($customerMetaFields);
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($columns['project-meta'])) {
|
||||
$projectMetaFields = $this->findMetaColumns(new ProjectMetaDisplayEvent($query->copyTo(new ProjectQuery()), ProjectMetaDisplayEvent::EXPORT));
|
||||
$columns['project-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($projectMetaFields): int {
|
||||
foreach ($projectMetaFields as $metaField) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||
}
|
||||
|
||||
return \count($projectMetaFields);
|
||||
},
|
||||
'render' => function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) use ($projectMetaFields): int {
|
||||
foreach ($projectMetaFields as $metaField) {
|
||||
$metaFieldValue = '';
|
||||
if (null !== $entity->getProject()) {
|
||||
$metaField = $entity->getProject()->getMetaField($metaField->getName());
|
||||
if (null !== $metaField) {
|
||||
$metaFieldValue = $metaField->getValue();
|
||||
}
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
|
||||
}
|
||||
|
||||
return \count($projectMetaFields);
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($columns['activity-meta'])) {
|
||||
$activityMetaFields = $this->findMetaColumns(new ActivityMetaDisplayEvent($query->copyTo(new ActivityQuery()), ActivityMetaDisplayEvent::EXPORT));
|
||||
$columns['activity-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($activityMetaFields): int {
|
||||
foreach ($activityMetaFields as $metaField) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||
}
|
||||
|
||||
return \count($activityMetaFields);
|
||||
},
|
||||
'render' => function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) use ($activityMetaFields): int {
|
||||
foreach ($activityMetaFields as $metaField) {
|
||||
$metaFieldValue = '';
|
||||
if (null !== $entity->getActivity()) {
|
||||
$metaField = $entity->getActivity()->getMetaField($metaField->getName());
|
||||
if (null !== $metaField) {
|
||||
$metaFieldValue = $metaField->getValue();
|
||||
}
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
|
||||
}
|
||||
|
||||
return \count($activityMetaFields);
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($columns['user-meta'])) {
|
||||
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT);
|
||||
$this->dispatcher->dispatch($event);
|
||||
$userPreferences = $event->getPreferences();
|
||||
$columns['user-meta'] = [
|
||||
'header' => function (Worksheet $sheet, int $row, int $column) use ($userPreferences): int {
|
||||
foreach ($userPreferences as $metaField) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
|
||||
}
|
||||
|
||||
return \count($userPreferences);
|
||||
},
|
||||
'render' => function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) use ($userPreferences): int {
|
||||
foreach ($userPreferences as $preference) {
|
||||
$metaFieldValue = '';
|
||||
if (null !== $entity->getUser()) {
|
||||
$metaField = $entity->getUser()->getPreference($preference->getName());
|
||||
if (null !== $metaField) {
|
||||
$metaFieldValue = $metaField->getValue();
|
||||
}
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
|
||||
}
|
||||
|
||||
return \count($userPreferences);
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($columns['type']) && !isset($columns['type']['render'])) {
|
||||
$columns['type']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $entity->getType());
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['category']) && !isset($columns['category']['render'])) {
|
||||
$columns['category']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $entity->getCategory());
|
||||
};
|
||||
}
|
||||
|
||||
if (isset($columns['customer_number'])) {
|
||||
if (!isset($columns['customer_number']['header'])) {
|
||||
$columns['customer_number']['header'] = function (Worksheet $sheet, int $row, int $column): int {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('number'));
|
||||
|
||||
return 1;
|
||||
};
|
||||
}
|
||||
|
||||
if (!isset($columns['customer_number']['render'])) {
|
||||
$columns['customer_number']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$customerId = '';
|
||||
if (null !== $entity->getProject()) {
|
||||
$customerId = $entity->getProject()->getCustomer()->getNumber();
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $customerId);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($columns['customer_vat']) && !isset($columns['customer_vat']['render'])) {
|
||||
if (!isset($columns['customer_vat']['header'])) {
|
||||
$columns['customer_vat']['header'] = function (Worksheet $sheet, int $row, int $column): int {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('vat_id'));
|
||||
|
||||
return 1;
|
||||
};
|
||||
}
|
||||
|
||||
if (!isset($columns['customer_vat']['render'])) {
|
||||
$columns['customer_vat']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$customerVat = '';
|
||||
if (null !== $entity->getProject()) {
|
||||
$customerVat = $entity->getProject()->getCustomer()->getVatId();
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $customerVat);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($columns['order_number']) && !isset($columns['order_number']['render'])) {
|
||||
if (!isset($columns['order_number']['header'])) {
|
||||
$columns['order_number']['header'] = function (Worksheet $sheet, int $row, int $column): int {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('orderNumber'));
|
||||
|
||||
return 1;
|
||||
};
|
||||
}
|
||||
|
||||
if (!isset($columns['order_number']['render'])) {
|
||||
$columns['order_number']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
|
||||
$orderNumber = '';
|
||||
if (null !== $entity->getProject()) {
|
||||
$orderNumber = $entity->getProject()->getOrderNumber();
|
||||
}
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $orderNumber);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!$showRates) {
|
||||
$removes = ['rate', 'fixedRate', 'hourlyRate', 'rate_internal'];
|
||||
foreach ($removes as $removeMe) {
|
||||
if (\array_key_exists($removeMe, $columns)) {
|
||||
unset($columns[$removeMe]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExportableItem[] $exportItems
|
||||
* @param TimesheetQuery $query
|
||||
* @return Spreadsheet
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||
*/
|
||||
protected function fromArrayToSpreadsheet(array $exportItems, TimesheetQuery $query): Spreadsheet
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
// Set default row height to automatic, so we can specify wrap text columns later on
|
||||
// without bloating the output file as we would need to store stylesheet info for every cell.
|
||||
// LibreOffice is still not considering this flag, @see https://github.com/PHPOffice/PHPExcel/issues/588
|
||||
// with no solution implemented so nothing we can do about it there.
|
||||
$sheet->getDefaultRowDimension()->setRowHeight(-1);
|
||||
|
||||
$recordsHeaderColumn = 1;
|
||||
$recordsHeaderRow = 1;
|
||||
|
||||
$columns = $this->getColumns($exportItems, $query, $this->columns);
|
||||
|
||||
foreach ($columns as $label => $settings) {
|
||||
if (isset($settings['header'])) {
|
||||
if (!\is_callable($settings['header'])) {
|
||||
throw new \RuntimeException('Invalid header renderer given for: ' . $label);
|
||||
}
|
||||
$amount = $settings['header']($sheet, $recordsHeaderRow, $recordsHeaderColumn);
|
||||
$recordsHeaderColumn += $amount;
|
||||
} else {
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow), $this->translator->trans((\array_key_exists('label', $settings) && \is_string($settings['label'])) ? $settings['label'] : $label));
|
||||
}
|
||||
}
|
||||
|
||||
$entryHeaderRow = $recordsHeaderRow + 1;
|
||||
|
||||
$durationColumn = null;
|
||||
$rateColumn = null;
|
||||
$internalRateColumn = null;
|
||||
|
||||
foreach ($exportItems as $exportItem) {
|
||||
$entryHeaderColumn = 1;
|
||||
|
||||
foreach ($columns as $label => $settings) {
|
||||
if ($label === 'duration') {
|
||||
$durationColumn = $entryHeaderColumn;
|
||||
} elseif ($label === 'rate') {
|
||||
$rateColumn = $entryHeaderColumn;
|
||||
} elseif ($label === 'rate_internal') {
|
||||
$internalRateColumn = $entryHeaderColumn;
|
||||
}
|
||||
|
||||
if (!\array_key_exists('render', $settings) || !\is_callable($settings['render'])) {
|
||||
throw new \RuntimeException(\sprintf('Missing or invalid renderer for export column %s', $label));
|
||||
}
|
||||
|
||||
$amount = $settings['render']($sheet, $entryHeaderRow, $entryHeaderColumn, $exportItem);
|
||||
$entryHeaderColumn += (null === $amount) ? 1 : (int) $amount;
|
||||
}
|
||||
|
||||
$entryHeaderRow++;
|
||||
}
|
||||
|
||||
if ($this->isTotalRowSupported()) {
|
||||
if (null !== $durationColumn) {
|
||||
$startCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($durationColumn, 2))->getCoordinate();
|
||||
$endCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($durationColumn, $entryHeaderRow - 1))->getCoordinate();
|
||||
$this->setDurationTotal($sheet, $durationColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||
$style = $sheet->getStyle(CellAddress::fromColumnAndRow($durationColumn, $entryHeaderRow));
|
||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$style->getFont()->setBold(true);
|
||||
}
|
||||
|
||||
if (null !== $rateColumn) {
|
||||
$startCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($rateColumn, 2))->getCoordinate();
|
||||
$endCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($rateColumn, $entryHeaderRow - 1))->getCoordinate();
|
||||
$this->setRateTotal($sheet, $rateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||
$style = $sheet->getStyle(CellAddress::fromColumnAndRow($rateColumn, $entryHeaderRow));
|
||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$style->getFont()->setBold(true);
|
||||
}
|
||||
|
||||
if (null !== $internalRateColumn) {
|
||||
$startCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($internalRateColumn, 2))->getCoordinate();
|
||||
$endCoordinate = $sheet->getCell(CellAddress::fromColumnAndRow($internalRateColumn, $entryHeaderRow - 1))->getCoordinate();
|
||||
$this->setRateTotal($sheet, $internalRateColumn, $entryHeaderRow, $startCoordinate, $endCoordinate);
|
||||
$style = $sheet->getStyle(CellAddress::fromColumnAndRow($internalRateColumn, $entryHeaderRow));
|
||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$style->getFont()->setBold(true);
|
||||
}
|
||||
}
|
||||
|
||||
return $spreadsheet;
|
||||
}
|
||||
|
||||
protected function isTotalRowSupported(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExportableItem[] $exportItems
|
||||
* @param TimesheetQuery $query
|
||||
* @return Response
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||
*/
|
||||
public function render(array $exportItems, TimesheetQuery $query): Response
|
||||
{
|
||||
$spreadsheet = $this->fromArrayToSpreadsheet($exportItems, $query);
|
||||
$file = $this->saveSpreadsheet($spreadsheet);
|
||||
$filename = new ExportFilename($query);
|
||||
|
||||
return $this->getFileResponse($file, $filename->getFilename() . $this->getFileExtension());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getFileExtension(): string;
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
* @param string $filename
|
||||
* @return BinaryFileResponse
|
||||
*/
|
||||
protected function getFileResponse(string $file, string $filename): BinaryFileResponse
|
||||
{
|
||||
$response = new BinaryFileResponse($file);
|
||||
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
|
||||
|
||||
$response->headers->set('Content-Type', $this->getContentType());
|
||||
$response->headers->set('Content-Disposition', $disposition);
|
||||
$response->deleteFileAfterSend(true);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getContentType(): string;
|
||||
|
||||
/**
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
abstract protected function saveSpreadsheet(Spreadsheet $spreadsheet): string;
|
||||
}
|
||||
@@ -9,45 +9,21 @@
|
||||
|
||||
namespace App\Export\Base;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use App\Entity\ExportableItem;
|
||||
use App\Export\ExportFilename;
|
||||
use App\Export\Package\SpoutSpreadsheet;
|
||||
use App\Export\RendererInterface;
|
||||
use App\Export\TimesheetExportInterface;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use OpenSpout\Writer\CSV\Writer;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CsvRenderer extends AbstractSpreadsheetRenderer
|
||||
final class CsvRenderer implements RendererInterface, TimesheetExportInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFileExtension(): string
|
||||
use ExportTrait;
|
||||
|
||||
public function __construct(private readonly SpreadsheetRenderer $spreadsheetRenderer)
|
||||
{
|
||||
return '.csv';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getContentType(): string
|
||||
{
|
||||
return 'text/csv';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||
{
|
||||
$filename = @tempnam(sys_get_temp_dir(), 'kimai-export-csv');
|
||||
if (false === $filename) {
|
||||
throw new \Exception('Could not open temporary file');
|
||||
}
|
||||
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Csv');
|
||||
$writer->save($filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
@@ -55,17 +31,38 @@ class CsvRenderer extends AbstractSpreadsheetRenderer
|
||||
return 'csv';
|
||||
}
|
||||
|
||||
protected function setDuration(Worksheet $sheet, int $column, int $row, ?int $duration): void
|
||||
public function getTitle(): string
|
||||
{
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), \sprintf('=%s', $duration ?? 0));
|
||||
return 'csv';
|
||||
}
|
||||
|
||||
protected function setRate(Worksheet $sheet, int $column, int $row, ?float $rate, ?string $currency): void
|
||||
/**
|
||||
* @param ExportableItem[] $exportItems
|
||||
*/
|
||||
public function render(array $exportItems, TimesheetQuery $query): Response
|
||||
{
|
||||
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $rate);
|
||||
if ($rate === 0.00) {
|
||||
return;
|
||||
return $this->getFileResponse(
|
||||
$this->renderFile($exportItems, $query),
|
||||
(new ExportFilename($query))->getFilename() . '.csv',
|
||||
'text/csv'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExportableItem[] $exportItems
|
||||
*/
|
||||
public function renderFile(array $exportItems, TimesheetQuery $query): \SplFileInfo
|
||||
{
|
||||
$filename = @tempnam(sys_get_temp_dir(), 'kimai-export-csv');
|
||||
if (false === $filename) {
|
||||
throw new \Exception('Could not open temporary file');
|
||||
}
|
||||
$this->setRateStyle($sheet, $column, $row, $currency);
|
||||
|
||||
$spreadsheet = new SpoutSpreadsheet(new Writer());
|
||||
$spreadsheet->open($filename);
|
||||
|
||||
$this->spreadsheetRenderer->writeSpreadsheet($spreadsheet, $exportItems, $query);
|
||||
|
||||
return new \SplFileInfo($filename);
|
||||
}
|
||||
}
|
||||
|
||||
31
src/Export/Base/ExportTrait.php
Normal file
31
src/Export/Base/ExportTrait.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Base;
|
||||
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait ExportTrait
|
||||
{
|
||||
protected function getFileResponse(string $file, string $filename, string $contentType): BinaryFileResponse
|
||||
{
|
||||
$response = new BinaryFileResponse($file);
|
||||
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
|
||||
|
||||
$response->headers->set('Content-Type', $contentType);
|
||||
$response->headers->set('Content-Disposition', $disposition);
|
||||
$response->deleteFileAfterSend(true);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use App\Event\MetaDisplayEventInterface;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Event\TimesheetMetaDisplayEvent;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\ExportRendererInterface;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
@@ -29,29 +30,22 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
|
||||
class HtmlRenderer
|
||||
class HtmlRenderer implements ExportRendererInterface
|
||||
{
|
||||
use RendererTrait;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id = 'html';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $template = 'default.html.twig';
|
||||
private string $id = 'html';
|
||||
private string $template = 'default.html.twig';
|
||||
|
||||
public function __construct(
|
||||
protected Environment $twig,
|
||||
protected EventDispatcherInterface $dispatcher,
|
||||
private ProjectStatisticService $projectStatisticService,
|
||||
private ActivityStatisticService $activityStatisticService
|
||||
protected readonly Environment $twig,
|
||||
protected readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly ProjectStatisticService $projectStatisticService,
|
||||
private readonly ActivityStatisticService $activityStatisticService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MetaDisplayEventInterface $event
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
protected function findMetaColumns(MetaDisplayEventInterface $event): array
|
||||
@@ -75,11 +69,6 @@ class HtmlRenderer
|
||||
|
||||
/**
|
||||
* @param ExportableItem[] $timesheets
|
||||
* @param TimesheetQuery $query
|
||||
* @return Response
|
||||
* @throws \Twig\Error\LoaderError
|
||||
* @throws \Twig\Error\RuntimeError
|
||||
* @throws \Twig\Error\SyntaxError
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||
{
|
||||
@@ -123,22 +112,23 @@ class HtmlRenderer
|
||||
return '@export/' . $this->template;
|
||||
}
|
||||
|
||||
public function setTemplate(string $filename): HtmlRenderer
|
||||
public function setTemplate(string $filename): void
|
||||
{
|
||||
$this->template = $filename;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setId(string $id): HtmlRenderer
|
||||
public function setId(string $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'print';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace App\Export\Base;
|
||||
|
||||
use App\Entity\ExportableItem;
|
||||
use App\Export\ExportFilename;
|
||||
use App\Export\ExportRendererInterface;
|
||||
use App\Export\TimesheetExportInterface;
|
||||
use App\Pdf\HtmlToPdfConverter;
|
||||
use App\Pdf\PdfContext;
|
||||
use App\Pdf\PdfRendererTrait;
|
||||
@@ -21,7 +23,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
|
||||
class PDFRenderer implements DispositionInlineInterface
|
||||
class PDFRenderer implements DispositionInlineInterface, ExportRendererInterface, TimesheetExportInterface
|
||||
{
|
||||
use RendererTrait;
|
||||
use PDFRendererTrait;
|
||||
@@ -30,10 +32,19 @@ class PDFRenderer implements DispositionInlineInterface
|
||||
private string $template = 'default.pdf.twig';
|
||||
private array $pdfOptions = [];
|
||||
|
||||
public function __construct(private Environment $twig, private HtmlToPdfConverter $converter, private ProjectStatisticService $projectStatisticService)
|
||||
public function __construct(
|
||||
private readonly Environment $twig,
|
||||
private readonly HtmlToPdfConverter $converter,
|
||||
private readonly ProjectStatisticService $projectStatisticService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'pdf';
|
||||
}
|
||||
|
||||
protected function getTemplate(): string
|
||||
{
|
||||
return '@export/' . $this->template;
|
||||
|
||||
215
src/Export/Base/SpreadsheetRenderer.php
Normal file
215
src/Export/Base/SpreadsheetRenderer.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Base;
|
||||
|
||||
use App\Entity\ExportableItem;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Event\ActivityMetaDisplayEvent;
|
||||
use App\Event\CustomerMetaDisplayEvent;
|
||||
use App\Event\MetaDisplayEventInterface;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
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\DateFormatter;
|
||||
use App\Export\Package\CellFormatter\DefaultFormatter;
|
||||
use App\Export\Package\CellFormatter\DurationFormatter;
|
||||
use App\Export\Package\CellFormatter\RateFormatter;
|
||||
use App\Export\Package\CellFormatter\TextFormatter;
|
||||
use App\Export\Package\CellFormatter\TimeFormatter;
|
||||
use App\Export\Package\Column;
|
||||
use App\Export\Package\SpreadsheetPackage;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @internal means no BC promise whatsoever!
|
||||
*/
|
||||
final class SpreadsheetRenderer
|
||||
{
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator,
|
||||
protected EventDispatcherInterface $dispatcher,
|
||||
protected Security $voter
|
||||
) {
|
||||
}
|
||||
|
||||
private function isRenderRate(TimesheetQuery $query): bool
|
||||
{
|
||||
if ($this->voter->getUser() === null) {
|
||||
// for command line export
|
||||
return true;
|
||||
}
|
||||
|
||||
if (null !== $query->getUser()) {
|
||||
return $this->voter->isGranted('view_rate_own_timesheet');
|
||||
}
|
||||
|
||||
return $this->voter->isGranted('view_rate_other_timesheet');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
private function findMetaColumns(MetaDisplayEventInterface $event): array
|
||||
{
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $event->getFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExportableItem[] $exportItems
|
||||
*/
|
||||
public function writeSpreadsheet(SpreadsheetPackage $spreadsheetPackage, array $exportItems, TimesheetQuery $query): void
|
||||
{
|
||||
$columns = $this->getColumns($query);
|
||||
|
||||
$headerRow = [];
|
||||
foreach ($columns as $column) {
|
||||
$headerRow[] = $this->translator->trans($column->getHeader());
|
||||
}
|
||||
$spreadsheetPackage->setHeader($headerRow);
|
||||
|
||||
$currentRow = 1;
|
||||
foreach ($exportItems as $exportItem) {
|
||||
$cells = [];
|
||||
foreach ($columns as $column) {
|
||||
$cells[] = $column->getValue($exportItem);
|
||||
}
|
||||
$spreadsheetPackage->addRow($cells);
|
||||
$currentRow++;
|
||||
}
|
||||
|
||||
if ($currentRow > 1) {
|
||||
$totalColumns = ['duration', 'rate', 'internalRate'];
|
||||
$columnNames = range('A', 'Z');
|
||||
$totalRow = [];
|
||||
$totalColumn = 1;
|
||||
foreach ($columns as $column) {
|
||||
$formula = null;
|
||||
if (\in_array($column->getName(), $totalColumns)) {
|
||||
$columnName = $columnNames[$totalColumn - 1];
|
||||
$formula = \sprintf('=SUM(%s2:%s%s)', $columnName, $columnName, $currentRow);
|
||||
}
|
||||
$totalRow[] = $formula;
|
||||
$totalColumn++;
|
||||
}
|
||||
|
||||
$spreadsheetPackage->addRow($totalRow, ['totals' => true]);
|
||||
}
|
||||
|
||||
$spreadsheetPackage->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<Column>
|
||||
*/
|
||||
private function getColumns(TimesheetQuery $query): array
|
||||
{
|
||||
$showRates = $this->isRenderRate($query);
|
||||
|
||||
$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());
|
||||
|
||||
if ($showRates) {
|
||||
$columns[] = (new Column('currency', new DefaultFormatter()))->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('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());
|
||||
|
||||
foreach ($this->findMetaColumns(new TimesheetMetaDisplayEvent($query, TimesheetMetaDisplayEvent::EXPORT)) as $metaField) {
|
||||
if ($metaField->getName() === null) {
|
||||
continue;
|
||||
}
|
||||
$columns[] = (new Column('timesheet.meta.' . $metaField->getName(), new DefaultFormatter()))
|
||||
->withHeader($metaField->getLabel())
|
||||
->withExtractor(function (ExportableItem $exportableItem) use ($metaField) {
|
||||
return $exportableItem->getMetaField($metaField->getName())?->getValue();
|
||||
});
|
||||
}
|
||||
|
||||
foreach ($this->findMetaColumns(new CustomerMetaDisplayEvent($query->copyTo(new CustomerQuery()), CustomerMetaDisplayEvent::EXPORT)) as $metaField) {
|
||||
if ($metaField->getName() === null) {
|
||||
continue;
|
||||
}
|
||||
$columns[] = (new Column('customer.meta.' . $metaField->getName(), new DefaultFormatter()))
|
||||
->withHeader($metaField->getLabel())
|
||||
->withExtractor(function (ExportableItem $exportableItem) use ($metaField) {
|
||||
return $exportableItem->getProject()?->getCustomer()?->getMetaField($metaField->getName())?->getValue();
|
||||
});
|
||||
}
|
||||
|
||||
foreach ($this->findMetaColumns(new ProjectMetaDisplayEvent($query->copyTo(new ProjectQuery()), ProjectMetaDisplayEvent::EXPORT)) as $metaField) {
|
||||
if ($metaField->getName() === null) {
|
||||
continue;
|
||||
}
|
||||
$columns[] = (new Column('project.meta.' . $metaField->getName(), new DefaultFormatter()))
|
||||
->withHeader($metaField->getLabel())
|
||||
->withExtractor(function (ExportableItem $exportableItem) use ($metaField) {
|
||||
return $exportableItem->getProject()?->getMetaField($metaField->getName())?->getValue();
|
||||
});
|
||||
}
|
||||
|
||||
foreach ($this->findMetaColumns(new ActivityMetaDisplayEvent($query->copyTo(new ActivityQuery()), ActivityMetaDisplayEvent::EXPORT)) as $metaField) {
|
||||
if ($metaField->getName() === null) {
|
||||
continue;
|
||||
}
|
||||
$columns[] = (new Column('activity.meta.' . $metaField->getName(), new DefaultFormatter()))
|
||||
->withHeader($metaField->getLabel())
|
||||
->withExtractor(function (ExportableItem $exportableItem) use ($metaField) {
|
||||
return $exportableItem->getActivity()?->getMetaField($metaField->getName())?->getValue();
|
||||
});
|
||||
}
|
||||
|
||||
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT);
|
||||
$this->dispatcher->dispatch($event);
|
||||
foreach ($event->getPreferences() as $metaField) {
|
||||
if ($metaField->getName() === null) {
|
||||
continue;
|
||||
}
|
||||
$columns[] = (new Column('user.meta.' . $metaField->getName(), new DefaultFormatter()))
|
||||
->withHeader($metaField->getLabel())
|
||||
->withExtractor(function (ExportableItem $exportableItem) use ($metaField) {
|
||||
return $exportableItem->getUser()?->getPreference($metaField->getName())?->getValue();
|
||||
});
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
}
|
||||
@@ -9,85 +9,60 @@
|
||||
|
||||
namespace App\Export\Base;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use App\Entity\ExportableItem;
|
||||
use App\Export\ExportFilename;
|
||||
use App\Export\Package\SpoutSpreadsheet;
|
||||
use App\Export\RendererInterface;
|
||||
use App\Export\TimesheetExportInterface;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use OpenSpout\Writer\XLSX\Writer;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class XlsxRenderer extends AbstractSpreadsheetRenderer
|
||||
final class XlsxRenderer implements RendererInterface, TimesheetExportInterface
|
||||
{
|
||||
protected function isTotalRowSupported(): bool
|
||||
use ExportTrait;
|
||||
|
||||
public function __construct(private readonly SpreadsheetRenderer $spreadsheetRenderer)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return '.xlsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getContentType(): string
|
||||
{
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||
{
|
||||
$filename = @tempnam(sys_get_temp_dir(), 'kimai-export-xlsx');
|
||||
if (false === $filename) {
|
||||
throw new \Exception('Could not open temporary file');
|
||||
}
|
||||
|
||||
$this->applyStyles($spreadsheet);
|
||||
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||
$writer->save($filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
protected function applyStyles(Spreadsheet $spreadsheet): void
|
||||
{
|
||||
// Store expensive calculations for later
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$highestRow = $sheet->getHighestRow();
|
||||
$highestColumn = $sheet->getHighestColumn();
|
||||
|
||||
// Enable auto filter for header row
|
||||
$sheet->setAutoFilter('A1:' . $highestColumn . '1');
|
||||
|
||||
// Freeze first row and date & time columns for easier navigation
|
||||
$sheet->freezePane('D2');
|
||||
|
||||
foreach ($sheet->getColumnIterator() as $columnName => $column) {
|
||||
// We default to a reasonable auto-width decided by the client,
|
||||
// sadly ->getDefaultColumnDimension() is not supported so it needs
|
||||
// to be specific about what column should be auto sized.
|
||||
$col = $sheet->getColumnDimension($columnName);
|
||||
|
||||
// If no other width is specified (which defaults to -1)
|
||||
if ((int) $col->getWidth() === -1) {
|
||||
$col->setAutoSize(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Text inside cells should be top left
|
||||
$sheet
|
||||
->getStyle('A2:' . $highestColumn . $highestRow)
|
||||
->getAlignment()
|
||||
->setVertical(Alignment::VERTICAL_TOP)
|
||||
->setHorizontal(Alignment::HORIZONTAL_LEFT);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'xlsx';
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'xlsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExportableItem[] $exportItems
|
||||
*/
|
||||
public function render(array $exportItems, TimesheetQuery $query): Response
|
||||
{
|
||||
return $this->getFileResponse(
|
||||
$this->renderFile($exportItems, $query),
|
||||
(new ExportFilename($query))->getFilename() . '.xlsx',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExportableItem[] $exportItems
|
||||
*/
|
||||
public function renderFile(array $exportItems, TimesheetQuery $query): \SplFileInfo
|
||||
{
|
||||
$filename = @tempnam(sys_get_temp_dir(), 'kimai-export-xlsx');
|
||||
if (false === $filename) {
|
||||
throw new \Exception('Could not open temporary file');
|
||||
}
|
||||
|
||||
$spreadsheet = new SpoutSpreadsheet(new Writer());
|
||||
$spreadsheet->open($filename);
|
||||
|
||||
$this->spreadsheetRenderer->writeSpreadsheet($spreadsheet, $exportItems, $query);
|
||||
|
||||
return new \SplFileInfo($filename);
|
||||
}
|
||||
}
|
||||
|
||||
22
src/Export/Package/CellFormatter/ArrayFormatter.php
Normal file
22
src/Export/Package/CellFormatter/ArrayFormatter.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package\CellFormatter;
|
||||
|
||||
final class ArrayFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function formatValue(mixed $value): mixed
|
||||
{
|
||||
if (!\is_array($value)) {
|
||||
throw new \InvalidArgumentException('Only arrays are supported');
|
||||
}
|
||||
|
||||
return implode(', ', $value);
|
||||
}
|
||||
}
|
||||
26
src/Export/Package/CellFormatter/BooleanFormatter.php
Normal file
26
src/Export/Package/CellFormatter/BooleanFormatter.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package\CellFormatter;
|
||||
|
||||
final class BooleanFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function formatValue(mixed $value): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\is_scalar($value)) {
|
||||
throw new \InvalidArgumentException('Only scalar values are supported');
|
||||
}
|
||||
|
||||
return (bool) $value;
|
||||
}
|
||||
}
|
||||
15
src/Export/Package/CellFormatter/CellFormatterInterface.php
Normal file
15
src/Export/Package/CellFormatter/CellFormatterInterface.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package\CellFormatter;
|
||||
|
||||
interface CellFormatterInterface
|
||||
{
|
||||
public function formatValue(mixed $value): mixed;
|
||||
}
|
||||
26
src/Export/Package/CellFormatter/DateFormatter.php
Normal file
26
src/Export/Package/CellFormatter/DateFormatter.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package\CellFormatter;
|
||||
|
||||
final class DateFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function formatValue(mixed $value): mixed
|
||||
{
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return $value->format('Y-m-d');
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Only DateTimeInterface can be formatted');
|
||||
}
|
||||
}
|
||||
26
src/Export/Package/CellFormatter/DefaultFormatter.php
Normal file
26
src/Export/Package/CellFormatter/DefaultFormatter.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package\CellFormatter;
|
||||
|
||||
final class DefaultFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function formatValue(mixed $value): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!\is_scalar($value)) {
|
||||
throw new \InvalidArgumentException('Only scalar values are supported');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
22
src/Export/Package/CellFormatter/DurationFormatter.php
Normal file
22
src/Export/Package/CellFormatter/DurationFormatter.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package\CellFormatter;
|
||||
|
||||
final class DurationFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function formatValue(mixed $value): mixed
|
||||
{
|
||||
if (is_numeric($value)) {
|
||||
return (float) number_format($value / 3600, 2, '.', '');
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
26
src/Export/Package/CellFormatter/RateFormatter.php
Normal file
26
src/Export/Package/CellFormatter/RateFormatter.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package\CellFormatter;
|
||||
|
||||
final class RateFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function formatValue(mixed $value): mixed
|
||||
{
|
||||
if (is_numeric($value)) {
|
||||
return (float) number_format((float) $value, 2, '.', '');
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Only numeric values be formatted');
|
||||
}
|
||||
}
|
||||
28
src/Export/Package/CellFormatter/TextFormatter.php
Normal file
28
src/Export/Package/CellFormatter/TextFormatter.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package\CellFormatter;
|
||||
|
||||
use App\Utils\StringHelper;
|
||||
|
||||
final class TextFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function __construct(private readonly bool $sanitizeDde)
|
||||
{
|
||||
}
|
||||
|
||||
public function formatValue(mixed $value): mixed
|
||||
{
|
||||
if ($this->sanitizeDde && \is_string($value)) {
|
||||
$value = StringHelper::sanitizeDDE($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
26
src/Export/Package/CellFormatter/TimeFormatter.php
Normal file
26
src/Export/Package/CellFormatter/TimeFormatter.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package\CellFormatter;
|
||||
|
||||
final class TimeFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function formatValue(mixed $value): mixed
|
||||
{
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return $value->format('H:i');
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Only DateTimeInterface can be formatted');
|
||||
}
|
||||
}
|
||||
61
src/Export/Package/Column.php
Normal file
61
src/Export/Package/Column.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package;
|
||||
|
||||
use App\Entity\ExportableItem;
|
||||
use App\Export\Package\CellFormatter\CellFormatterInterface;
|
||||
|
||||
class Column
|
||||
{
|
||||
private ?string $header = null;
|
||||
private \Closure|null $extractor = null;
|
||||
|
||||
public function __construct(private readonly string $name, private readonly CellFormatterInterface $formatter)
|
||||
{
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function withHeader(?string $header): Column
|
||||
{
|
||||
$this->header = $header;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withExtractor(\Closure $extractor): Column
|
||||
{
|
||||
$this->extractor = $extractor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extract(ExportableItem $exportableItem): mixed
|
||||
{
|
||||
if ($this->extractor === null) {
|
||||
throw new \InvalidArgumentException('Missing extractor on column: ' . $this->name);
|
||||
}
|
||||
|
||||
return ($this->extractor)($exportableItem);
|
||||
}
|
||||
|
||||
public function getValue(ExportableItem $exportableItem): mixed
|
||||
{
|
||||
return $this->formatter->formatValue($this->extract($exportableItem));
|
||||
}
|
||||
|
||||
public function getHeader(): string
|
||||
{
|
||||
return $this->header ?? $this->name;
|
||||
}
|
||||
}
|
||||
131
src/Export/Package/PhpOfficeSpreadsheet.php
Normal file
131
src/Export/Package/PhpOfficeSpreadsheet.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
final class PhpOfficeSpreadsheet implements SpreadsheetPackage
|
||||
{
|
||||
private ?Spreadsheet $spreadsheet;
|
||||
private ?Worksheet $worksheet;
|
||||
private int $currentRow = 1;
|
||||
private ?string $filename = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->spreadsheet = new Spreadsheet();
|
||||
$this->worksheet = $this->spreadsheet->getActiveSheet();
|
||||
}
|
||||
|
||||
public function open(string $filename): void
|
||||
{
|
||||
$this->filename = $filename;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
if ($this->filename === null) {
|
||||
throw new \Exception('Need to call open() first before save()');
|
||||
}
|
||||
|
||||
if ($this->spreadsheet === null || $this->worksheet === null) {
|
||||
throw new \Exception('Cannot re-use spreadsheet after calling save()');
|
||||
}
|
||||
|
||||
$sheet = $this->worksheet;
|
||||
// Store expensive calculations for later
|
||||
$highestRow = $sheet->getHighestRow();
|
||||
$highestColumn = $sheet->getHighestColumn();
|
||||
|
||||
// Enable auto filter for header row
|
||||
$sheet->setAutoFilter('A1:' . $highestColumn . '1');
|
||||
|
||||
// Freeze first row and date & time columns for easier navigation
|
||||
$sheet->freezePane('D2');
|
||||
|
||||
foreach ($sheet->getColumnIterator() as $columnName => $column) {
|
||||
// We default to a reasonable auto-width decided by the client,
|
||||
// sadly ->getDefaultColumnDimension() is not supported so it needs
|
||||
// to be specific about what column should be auto sized.
|
||||
$col = $sheet->getColumnDimension($columnName);
|
||||
|
||||
// If no other width is specified (which defaults to -1)
|
||||
if ((int) $col->getWidth() === -1) {
|
||||
$col->setAutoSize(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Text inside cells should be top left
|
||||
$sheet
|
||||
->getStyle('A2:' . $highestColumn . $highestRow)
|
||||
->getAlignment()
|
||||
->setVertical(Alignment::VERTICAL_TOP)
|
||||
->setHorizontal(Alignment::HORIZONTAL_LEFT);
|
||||
|
||||
$writer = IOFactory::createWriter($this->spreadsheet, 'Xlsx');
|
||||
$writer->save($this->filename);
|
||||
|
||||
$this->spreadsheet = null;
|
||||
$this->worksheet = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $columns
|
||||
*/
|
||||
public function setHeader(array $columns): void
|
||||
{
|
||||
if ($this->worksheet === null) {
|
||||
throw new \Exception('Cannot re-use spreadsheet after calling save()');
|
||||
}
|
||||
|
||||
$counter = 1;
|
||||
foreach ($columns as $column) {
|
||||
$pos = CellAddress::fromColumnAndRow($counter, 1);
|
||||
$this->worksheet->setCellValue($pos, $column);
|
||||
$style = $this->worksheet->getStyle($pos);
|
||||
$style->getBorders()->getBottom()->setBorderStyle(Border::BORDER_THIN);
|
||||
$style->getFont()->setBold(true);
|
||||
|
||||
$counter++;
|
||||
}
|
||||
|
||||
$this->currentRow++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $columns
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function addRow(array $columns, array $options = []): void
|
||||
{
|
||||
if ($this->worksheet === null) {
|
||||
throw new \Exception('Cannot re-use spreadsheet after calling save()');
|
||||
}
|
||||
|
||||
$counter = 1;
|
||||
foreach ($columns as $column) {
|
||||
$this->worksheet->setCellValue(CellAddress::fromColumnAndRow($counter, $this->currentRow), $column);
|
||||
|
||||
if (\array_key_exists('totals', $options) && $options['totals'] === true) {
|
||||
$style = $this->worksheet->getStyle(CellAddress::fromColumnAndRow($counter, $this->currentRow));
|
||||
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
|
||||
$style->getFont()->setBold(true);
|
||||
}
|
||||
$counter++;
|
||||
}
|
||||
|
||||
$this->currentRow++;
|
||||
}
|
||||
}
|
||||
94
src/Export/Package/SpoutSpreadsheet.php
Normal file
94
src/Export/Package/SpoutSpreadsheet.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package;
|
||||
|
||||
use App\Constants;
|
||||
use OpenSpout\Common\Entity\Cell;
|
||||
use OpenSpout\Common\Entity\Row;
|
||||
use OpenSpout\Common\Entity\Style\Border;
|
||||
use OpenSpout\Common\Entity\Style\BorderPart;
|
||||
use OpenSpout\Common\Entity\Style\Color;
|
||||
use OpenSpout\Common\Entity\Style\Style;
|
||||
use OpenSpout\Writer\AbstractWriterMultiSheets;
|
||||
use OpenSpout\Writer\CSV\Writer;
|
||||
use OpenSpout\Writer\WriterInterface;
|
||||
use OpenSpout\Writer\XLSX\Entity\SheetView;
|
||||
|
||||
class SpoutSpreadsheet implements SpreadsheetPackage
|
||||
{
|
||||
public function __construct(private readonly WriterInterface $writer)
|
||||
{
|
||||
$this->writer->setCreator(Constants::SOFTWARE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $columns
|
||||
*/
|
||||
public function setHeader(array $columns): void
|
||||
{
|
||||
$tmp = [];
|
||||
foreach ($columns as $column) {
|
||||
$tmp[] = Cell::fromValue($column);
|
||||
}
|
||||
|
||||
$style = new Style();
|
||||
$style->setShouldWrapText(false);
|
||||
$style->setShouldShrinkToFit(true);
|
||||
$style->setBackgroundColor('EEEEEE');
|
||||
$style->setBorder(new Border(new BorderPart(Border::BOTTOM, Color::BLACK, Border::WIDTH_THIN, Border::STYLE_SOLID)));
|
||||
$style->setFontBold();
|
||||
|
||||
$this->writer->addRow(new Row($tmp, $style));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $columns
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function addRow(array $columns, array $options = []): void
|
||||
{
|
||||
$style = new Style();
|
||||
$style->setShouldWrapText(false);
|
||||
$style->setShouldShrinkToFit(true);
|
||||
|
||||
if (\array_key_exists('totals', $options) && $options['totals'] === true) {
|
||||
if ($this->writer instanceof Writer) {
|
||||
return;
|
||||
}
|
||||
$style->setBorder(new Border(new BorderPart(Border::TOP, Color::BLACK, Border::WIDTH_THIN, Border::STYLE_SOLID)));
|
||||
$style->setFontBold();
|
||||
}
|
||||
|
||||
$tmp = [];
|
||||
foreach ($columns as $column) {
|
||||
$tmp[] = Cell::fromValue($column); // @phpstan-ignore argument.type
|
||||
}
|
||||
|
||||
$this->writer->addRow(new Row($tmp, $style));
|
||||
}
|
||||
|
||||
public function open(string $filename): void
|
||||
{
|
||||
$this->writer->openToFile($filename);
|
||||
|
||||
if ($this->writer instanceof AbstractWriterMultiSheets) {
|
||||
$sheetView = new SheetView();
|
||||
$sheetView->setFreezeColumn('D');
|
||||
$sheetView->setFreezeRow(2);
|
||||
|
||||
$this->writer->getCurrentSheet()->setSheetView($sheetView);
|
||||
}
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->writer->close();
|
||||
}
|
||||
}
|
||||
31
src/Export/Package/SpreadsheetPackage.php
Normal file
31
src/Export/Package/SpreadsheetPackage.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Package;
|
||||
|
||||
interface SpreadsheetPackage
|
||||
{
|
||||
/**
|
||||
* Pass the temporary filename where data will be written to.
|
||||
*/
|
||||
public function open(string $filename): void;
|
||||
|
||||
public function save(): void;
|
||||
|
||||
/**
|
||||
* @param array<string> $columns
|
||||
*/
|
||||
public function setHeader(array $columns): void;
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $columns
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function addRow(array $columns, array $options = []): void;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Renderer;
|
||||
|
||||
use App\Export\Base\CsvRenderer as BaseCsvRenderer;
|
||||
use App\Export\RendererInterface;
|
||||
|
||||
final class CsvRenderer extends BaseCsvRenderer implements RendererInterface
|
||||
{
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'csv';
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Renderer;
|
||||
|
||||
use App\Export\Base\HtmlRenderer as BaseHtmlRenderer;
|
||||
use App\Export\ExportRendererInterface;
|
||||
|
||||
final class HtmlRenderer extends BaseHtmlRenderer implements ExportRendererInterface
|
||||
{
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'print';
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Export\Renderer;
|
||||
|
||||
use App\Activity\ActivityStatisticService;
|
||||
use App\Export\Base\HtmlRenderer;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Twig\Environment;
|
||||
@@ -17,10 +18,10 @@ use Twig\Environment;
|
||||
final class HtmlRendererFactory
|
||||
{
|
||||
public function __construct(
|
||||
private Environment $twig,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private ProjectStatisticService $projectStatisticService,
|
||||
private ActivityStatisticService $activityStatisticService
|
||||
private readonly Environment $twig,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly ProjectStatisticService $projectStatisticService,
|
||||
private readonly ActivityStatisticService $activityStatisticService
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Renderer;
|
||||
|
||||
use App\Export\Base\PDFRenderer as BasePDFRenderer;
|
||||
use App\Export\ExportRendererInterface;
|
||||
|
||||
final class PDFRenderer extends BasePDFRenderer implements ExportRendererInterface
|
||||
{
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'pdf';
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Export\Renderer;
|
||||
|
||||
use App\Export\Base\PDFRenderer;
|
||||
use App\Pdf\HtmlToPdfConverter;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use Twig\Environment;
|
||||
@@ -16,9 +17,9 @@ use Twig\Environment;
|
||||
final class PdfRendererFactory
|
||||
{
|
||||
public function __construct(
|
||||
private Environment $twig,
|
||||
private HtmlToPdfConverter $converter,
|
||||
private ProjectStatisticService $projectStatisticService
|
||||
private readonly Environment $twig,
|
||||
private readonly HtmlToPdfConverter $converter,
|
||||
private readonly ProjectStatisticService $projectStatisticService
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Renderer;
|
||||
|
||||
use App\Export\Base\XlsxRenderer as BaseXlsxRenderer;
|
||||
use App\Export\RendererInterface;
|
||||
|
||||
final class XlsxRenderer extends BaseXlsxRenderer implements RendererInterface
|
||||
{
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'xlsx';
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ namespace App\Export\Spreadsheet\Writer;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
|
||||
final class BinaryFileResponseWriter implements WriterInterface
|
||||
@@ -19,10 +18,9 @@ final class BinaryFileResponseWriter implements WriterInterface
|
||||
private string $prefix;
|
||||
|
||||
/**
|
||||
* @param WriterInterface $writer
|
||||
* @param string $prefix is only urlencoded but not validated and can break the response if you pass in invalid character
|
||||
*/
|
||||
public function __construct(private WriterInterface $writer, string $prefix)
|
||||
public function __construct(private readonly WriterInterface $writer, string $prefix)
|
||||
{
|
||||
$this->prefix = urlencode($prefix);
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Timesheet;
|
||||
|
||||
use App\Export\Base\CsvRenderer as BaseCsvRenderer;
|
||||
use App\Export\TimesheetExportInterface;
|
||||
|
||||
final class CsvRenderer extends BaseCsvRenderer implements TimesheetExportInterface
|
||||
{
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'csv';
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,4 @@ final class HtmlRenderer extends BaseHtmlRenderer implements TimesheetExportInte
|
||||
{
|
||||
return 'print';
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'print';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Timesheet;
|
||||
|
||||
use App\Export\Base\PDFRenderer as BasePDFRenderer;
|
||||
use App\Export\TimesheetExportInterface;
|
||||
|
||||
final class PDFRenderer extends BasePDFRenderer implements TimesheetExportInterface
|
||||
{
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'pdf';
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Timesheet;
|
||||
|
||||
use App\Export\Base\XlsxRenderer as BaseXlsxRenderer;
|
||||
use App\Export\TimesheetExportInterface;
|
||||
|
||||
final class XlsxRenderer extends BaseXlsxRenderer implements TimesheetExportInterface
|
||||
{
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'xlsx';
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,9 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
interface TimesheetExportInterface
|
||||
{
|
||||
/**
|
||||
* @param Timesheet[] $timesheets
|
||||
* @param Timesheet[] $exportItems
|
||||
*/
|
||||
public function render(array $timesheets, TimesheetQuery $query): Response;
|
||||
public function render(array $exportItems, TimesheetQuery $query): Response;
|
||||
|
||||
public function getId(): string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user