Release 2.0.2 (#3856)

* allow to overwrite global spreadsheet styles
* bump version
* fix deprecations in vcard download
* bump composer packages
* added help page for all registered locales
* fix menu id's, cleanup times route, fix configurable homepage redirect
* format duration without leading zero in hours (unify javascript with php behavior)
* fix active records in all screen sizes
* improved responsiveness in XS
* fixed invoice number for customer null fields
* fix javascript respects multiple recent-activity dropdowns
* show recent activities on small screens
* fix user-profile layout column in XS
* fix search is always marked as active
This commit is contained in:
Kevin Papst
2023-02-21 19:21:49 +01:00
committed by GitHub
parent e474087257
commit 25469113fd
61 changed files with 821 additions and 580 deletions

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.0.1';
public const VERSION = '2.0.2';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 20001;
public const VERSION_ID = 20002;
/**
* The software name
*/

View File

@@ -368,20 +368,39 @@ final class CustomerController extends AbstractController
$vcard = new VCard();
$contact = $customer->getContact() ?? $customer->getName();
if ($contact === null || \strlen($contact) === 0) {
$contact = 'Unknown';
}
$contact = explode(' ', $contact);
$lastname = array_pop($contact);
$firstname = \count($contact) > 0 ? $contact[0] : $lastname;
$firstname = \count($contact) > 0 ? $contact[0] : '';
$vcard->addName($lastname, $firstname);
$note = $customer->getComment();
if ($note !== null) {
$note .= PHP_EOL;
}
$vcard->addName($lastname, $firstname);
$vcard->addNote($note . $customer->getAddress());
$vcard->addAddress(null, null, null, null, null, null, Countries::getName($customer->getCountry()));
$address = $customer->getAddress();
if (!empty($note) || !empty($address)) {
$vcard->addNote($note . $address);
}
$vcard->addCompany($customer->getCompany() ?? $customer->getName());
$vcard->addEmail($customer->getEmail());
$country = $customer->getCountry();
if ($country !== null) {
$vcard->addAddress(null, null, null, null, null, null, Countries::getName($country));
}
$company = $customer->getCompany();
if ($company !== null) {
$vcard->addCompany($company);
}
$email = $customer->getEmail();
if ($email !== null) {
$vcard->addEmail($email);
}
$hasPref = false;

View File

@@ -0,0 +1,86 @@
<?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\Controller;
use App\Configuration\LocaleService;
use App\Repository\Query\BaseQuery;
use App\Utils\DataTable;
use App\Utils\LocaleFormatter;
use App\Utils\PageSetup;
use App\Utils\Pagination;
use Pagerfanta\Adapter\ArrayAdapter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Intl\Locales;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route(path: '/help')]
#[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
final class HelpController extends AbstractController
{
#[Route(path: '/locales', defaults: [], name: 'help_locales', methods: ['GET'])]
public function helpLocale(Request $request, LocaleService $service): Response
{
$table = new DataTable('help_locales', new BaseQuery());
$table->addColumn('name', ['class' => 'alwaysVisible', 'orderBy' => false]);
$table->addColumn('description', ['class' => 'd-none', 'orderBy' => false]);
$table->addColumn('language', ['class' => 'd-none w-min', 'orderBy' => false]);
$table->addColumn('date', ['class' => 'alwaysVisible w-min', 'orderBy' => false]);
$table->addColumn('time', ['class' => 'alwaysVisible w-min text-center', 'orderBy' => false]);
$table->addColumn('duration', ['class' => 'd-none w-min text-end', 'orderBy' => false]);
$table->addColumn('money', ['class' => 'w-min text-end', 'orderBy' => false, 'title' => 'rate']);
$table->addColumn('hour_24', ['class' => 'alwaysVisible w-min text-center', 'orderBy' => false]);
$table->addColumn('rtl', ['class' => 'd-none w-min text-center', 'orderBy' => false, 'title' => 'RTL']);
$page = new PageSetup('help_locales');
$page->setDataTable($table);
$page->setActionName('help_locales');
$data = $this->buildLocales($request, $service);
$pagination = new Pagination(new ArrayAdapter($data));
$pagination->setMaxPerPage(9999);
$table->setPagination($pagination);
return $this->render('help/index.html.twig', [
'page_setup' => $page,
'dataTable' => $table,
]);
}
/**
* @param Request $request
* @param LocaleService $service
* @return array<string, array<string, string|bool|null>>
*/
private function buildLocales(Request $request, LocaleService $service): array
{
$requestLocale = $request->getLocale();
$data = [];
$now = $this->getDateTimeFactory()->createDateTime();
foreach ($service->getAllLocales() as $locale) {
$formatter = new LocaleFormatter($service, $locale);
$data[$locale] = [
'language' => $locale,
'name' => Locales::getName($locale, $locale),
'description' => Locales::getName($locale, $requestLocale),
'date' => $formatter->dateShort($now),
'time' => $formatter->time($now),
'duration' => $formatter->duration(46120),
'money' => $formatter->money(2794.83, 'EUR'),
'hour_24' => $service->is24Hour($locale),
'rtl' => $service->isRightToLeft($locale),
];
}
return $data;
}
}

View File

@@ -11,6 +11,8 @@ namespace App\Controller;
use App\Configuration\LocaleService;
use App\Entity\User;
use App\Event\ConfigureMainMenuEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@@ -26,11 +28,9 @@ final class HomepageController extends AbstractController
public const DEFAULT_ROUTE = 'timesheet';
#[Route(path: '', defaults: [], name: 'homepage', methods: ['GET'])]
public function indexAction(Request $request, LocaleService $service): Response
public function indexAction(Request $request, LocaleService $service, EventDispatcherInterface $eventDispatcher): Response
{
/** @var User $user */
$user = $this->getUser();
$userRoute = $user->getPreferenceValue('login_initial_view', self::DEFAULT_ROUTE, false);
$userLanguage = $user->getLanguage();
$requestLanguage = $request->getLocale();
@@ -48,13 +48,23 @@ final class HomepageController extends AbstractController
$userLanguage = $service->getDefaultLocale();
}
$routes = [
[$userRoute, $userLanguage],
[$userRoute, $requestLanguage],
[$userRoute, User::DEFAULT_LANGUAGE],
[self::DEFAULT_ROUTE, $userLanguage],
[self::DEFAULT_ROUTE, $requestLanguage],
];
$routes = [];
$userRoute = $user->getPreferenceValue('login_initial_view');
if (\is_string($userRoute)) {
$event = new ConfigureMainMenuEvent();
$eventDispatcher->dispatch($event);
$menu = $event->findById($userRoute);
if ($menu !== null && \count($menu->getRouteArgs()) === 0 && $menu->getRoute() !== null) {
$userRoute = $menu->getRoute();
}
$routes[] = [$userRoute, $userLanguage];
$routes[] = [$userRoute, $requestLanguage];
$routes[] = [$userRoute, User::DEFAULT_LANGUAGE];
}
$routes[] = [self::DEFAULT_ROUTE, $userLanguage];
$routes[] = [self::DEFAULT_ROUTE, $requestLanguage];
foreach ($routes as $routeSettings) {
$route = $routeSettings[0];

View File

@@ -21,6 +21,7 @@ final class ConfigureMainMenuEvent extends Event
private MenuItemModel $apps;
private MenuItemModel $admin;
private MenuItemModel $system;
private ?MenuItemModel $root = null;
public function __construct()
{
@@ -30,6 +31,19 @@ final class ConfigureMainMenuEvent extends Event
$this->system = new MenuItemModel('system', 'menu.system', '', [], 'configuration');
}
public function findById(string $identifier): ?MenuItemModel
{
if ($this->root === null) {
$this->root = new MenuItemModel('root', 'root');
$this->root->addChild($this->menu);
$this->root->addChild($this->apps);
$this->root->addChild($this->admin);
$this->root->addChild($this->system);
}
return $this->root->findChild($identifier);
}
public function getMenu(): MenuItemModel
{
return $this->menu;
@@ -37,7 +51,7 @@ final class ConfigureMainMenuEvent extends Event
public function getTimesheetMenu(): ?MenuItemModel
{
return $this->menu->getChild('timesheet');
return $this->menu->getChild('times');
}
public function getInvoiceMenu(): ?MenuItemModel

View File

@@ -11,7 +11,6 @@ namespace App\EventSubscriber;
use App\Entity\User;
use App\Event\ConfigureMainMenuEvent;
use App\Utils\MenuItemModel;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -30,38 +29,25 @@ final class MenuFavoritesSubscriber implements EventSubscriberInterface
public function onMainMenuConfigure(ConfigureMainMenuEvent $menuEvent): void
{
/** @var User $user */
/** @var User|null $user */
$user = $this->security->getUser();
if (null === $user) {
return;
}
$favMenu = $menuEvent->getMenu()->findChild('favorites');
if ($favMenu === null) {
return;
}
$userFavorites = $user->getPreferenceValue('favorite_routes');
if (!\is_string($userFavorites) || trim($userFavorites) === '') {
return;
}
$root = new MenuItemModel('', '');
$root->addChild($menuEvent->getMenu());
if ($menuEvent->getAppsMenu()->hasChildren()) {
$root->addChild($menuEvent->getAppsMenu());
}
if ($menuEvent->getAdminMenu()->hasChildren()) {
$root->addChild($menuEvent->getAdminMenu());
}
if ($menuEvent->getSystemMenu()->hasChildren()) {
$root->addChild($menuEvent->getSystemMenu());
$favMenu = $menuEvent->findById('favorites');
if ($favMenu === null) {
return;
}
$userFavorites = explode(',', $userFavorites);
foreach ($userFavorites as $fav) {
$tmp = $root->findChild($fav);
$tmp = $menuEvent->findById($fav);
if ($tmp !== null && !$tmp->hasChildren()) {
$favMenu->addChild(clone $tmp);
}

View File

@@ -50,11 +50,11 @@ final class MenuSubscriber implements EventSubscriberInterface
$menu->addChild(new MenuItemModel('favorites', 'favorite_routes', null, [], 'bookmarked'));
// ------------------- timesheet menu -------------------
$times = new MenuItemModel('timesheet', 'time_tracking', null, [], 'timesheet');
$times = new MenuItemModel('times', 'time_tracking', null, [], 'timesheet');
if ($auth->isGranted('view_own_timesheet')) {
$timesheets = new MenuItemModel('times', 'my_times', 'timesheet', [], 'timesheet');
$timesheets->setChildRoutes(['times', 'timesheet_export', 'timesheet_edit', 'timesheet_create', 'timesheet_multi_update']);
$timesheets = new MenuItemModel('timesheet', 'my_times', 'timesheet', [], 'timesheet');
$timesheets->setChildRoutes(['timesheet_export', 'timesheet_edit', 'timesheet_create', 'timesheet_multi_update']);
$times->addChild($timesheets);
if ($auth->isGranted('quick-entry')) {
@@ -93,14 +93,15 @@ final class MenuSubscriber implements EventSubscriberInterface
$menu->addChild($reporting);
}
$invoice = new MenuItemModel('invoice', 'invoices', null, [], 'invoice');
// ------------------- invoice menu -------------------
$invoice = new MenuItemModel('invoices', 'invoices', null, [], 'invoice');
if ($auth->isGranted('create_invoice')) {
$invoice->addChild(new MenuItemModel('invoice', 'invoice_form.title', 'invoice', [], 'invoice'));
}
if ($auth->isGranted('view_invoice')) {
$tmpMenu = new MenuItemModel('invoice-listing', 'all_invoices', 'admin_invoice_list', [], 'list');
$tmpMenu = new MenuItemModel('invoice_listing', 'all_invoices', 'admin_invoice_list', [], 'list');
$tmpMenu->setChildRoutes(['admin_invoice_edit']);
$invoice->addChild($tmpMenu);
}

View File

@@ -19,9 +19,9 @@ use App\Form\Type\CalendarViewType;
use App\Form\Type\FavoriteMenuType;
use App\Form\Type\FirstWeekDayType;
use App\Form\Type\InitialViewType;
use App\Form\Type\LanguageType;
use App\Form\Type\SkinType;
use App\Form\Type\TimezoneType;
use App\Form\Type\UserLanguageType;
use App\Form\Type\YesNoType;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -87,7 +87,7 @@ final class UserPreferenceSubscriber implements EventSubscriberInterface
(new UserPreference(UserPreference::LOCALE, $this->systemConfiguration->getUserDefaultLanguage()))
->setOrder(250)
->setSection('locale')
->setType(LanguageType::class),
->setType(UserLanguageType::class),
(new UserPreference(UserPreference::FIRST_WEEKDAY, User::DEFAULT_FIRST_WEEKDAY))
->setOrder(300)
@@ -118,7 +118,7 @@ final class UserPreferenceSubscriber implements EventSubscriberInterface
->setOrder(710)
->setSection('behaviour')
->setOptions(['required' => false])
->addConstraint(new Length(['max' => 75]))
->addConstraint(new Length(['max' => 150]))
->setType(FavoriteMenuType::class),
(new UserPreference('daily_stats', false))

View File

@@ -23,6 +23,7 @@ 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;
@@ -37,7 +38,7 @@ use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal
* @internal means no BC promise whatsoever!
*/
abstract class AbstractSpreadsheetRenderer
{
@@ -50,12 +51,17 @@ abstract class AbstractSpreadsheetRenderer
// 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 = self::RATE_FORMAT_LEFT;
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;
/**
@@ -117,10 +123,10 @@ abstract class AbstractSpreadsheetRenderer
return $this->voter->isGranted('view_rate_other_timesheet');
}
protected function setFormattedDateTime(Worksheet $sheet, $column, $row, ?DateTime $date): void
protected function setFormattedDateTime(Worksheet $sheet, int $column, int $row, ?DateTime $date): void
{
if (null === $date) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
return;
}
@@ -128,20 +134,20 @@ abstract class AbstractSpreadsheetRenderer
$excelDate = Date::PHPToExcel($date);
if ($excelDate === false) {
$sheet->setCellValueByColumnAndRow($column, $row, $date);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $date);
return;
}
$sheet->setCellValueByColumnAndRow($column, $row, $excelDate);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $excelDate);
// TODO why is that format hardcoded and does not depend on the users locale?
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
}
protected function setFormattedTime(Worksheet $sheet, $column, $row, ?DateTime $date): void
protected function setFormattedTime(Worksheet $sheet, int $column, int $row, ?DateTime $date): void
{
if (null === $date) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
return;
}
@@ -149,19 +155,19 @@ abstract class AbstractSpreadsheetRenderer
$excelDate = Date::PHPToExcel($date);
if ($excelDate === false) {
$sheet->setCellValueByColumnAndRow($column, $row, $date);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $date);
return;
}
$sheet->setCellValueByColumnAndRow($column, $row, $excelDate);
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $excelDate);
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
}
protected function setFormattedDate(Worksheet $sheet, $column, $row, ?DateTime $date): void
protected function setFormattedDate(Worksheet $sheet, int $column, int $row, ?DateTime $date): void
{
if (null === $date) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
return;
}
@@ -169,48 +175,48 @@ abstract class AbstractSpreadsheetRenderer
$excelDate = Date::PHPToExcel($date);
if ($excelDate === false) {
$sheet->setCellValueByColumnAndRow($column, $row, $date);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $date);
return;
}
$sheet->setCellValueByColumnAndRow($column, $row, $excelDate);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $excelDate);
// TODO why is that format hardcoded and does not depend on the users locale?
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
}
protected function setDurationTotal(Worksheet $sheet, $column, $row, $startCoordinate, $endCoordinate): void
protected function setDurationTotal(Worksheet $sheet, int $column, int $row, string $startCoordinate, string $endCoordinate): void
{
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUBTOTAL(9,%s:%s)', $startCoordinate, $endCoordinate));
$style = $sheet->getStyleByColumnAndRow($column, $row);
$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, $column, $row, $duration): void
protected function setDuration(Worksheet $sheet, int $column, int $row, ?int $duration): void
{
if (null === $duration) {
$duration = 0;
}
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/%s', $duration, $this->durationBase));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode($this->durationFormat);
$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, $column, $row, $startCoordinate, $endCoordinate): void
protected function setRateTotal(Worksheet $sheet, int $column, int $row, string $startCoordinate, string $endCoordinate): void
{
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=SUBTOTAL(9,%s:%s)', $startCoordinate, $endCoordinate));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), sprintf('=SUBTOTAL(9,%s:%s)', $startCoordinate, $endCoordinate));
}
protected function setRateStyle(Worksheet $sheet, $column, $row, $rate, $currency): void
protected function setRateStyle(Worksheet $sheet, int $column, int $row, ?string $currency): void
{
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(
sprintf(self::RATE_FORMAT_LEFT, $currency)
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(
sprintf($this->rateFormat, $currency ?? '')
);
}
protected function setRate(Worksheet $sheet, $column, $row, $rate, $currency): void
protected function setRate(Worksheet $sheet, int $column, int $row, ?float $rate, ?string $currency): void
{
$sheet->setCellValueByColumnAndRow($column, $row, $rate);
$this->setRateStyle($sheet, $column, $row, $rate, $currency);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $rate ?? 0);
$this->setRateStyle($sheet, $column, $row, $currency);
}
/**
@@ -227,8 +233,8 @@ abstract class AbstractSpreadsheetRenderer
/**
* @param ExportableItem[] $exportItems
* @param TimesheetQuery $query
* @param array $columns
* @return array
* @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
{
@@ -289,7 +295,7 @@ abstract class AbstractSpreadsheetRenderer
if (null !== $entity->getUser()) {
$user = $entity->getUser()->getDisplayName();
}
$sheet->setCellValueByColumnAndRow($column, $row, $user);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $user);
};
}
@@ -300,12 +306,12 @@ abstract class AbstractSpreadsheetRenderer
if (null !== $entity->getUser()) {
$username = $entity->getUser()->getUserIdentifier();
}
$sheet->setCellValueByColumnAndRow($column, $row, $username);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $username);
};
}
if (!isset($columns['username']['header'])) {
$columns['username']['header'] = function (Worksheet $sheet, $row, $column): int {
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans('name'));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('name'));
return 1;
};
@@ -318,7 +324,7 @@ abstract class AbstractSpreadsheetRenderer
if (null !== $entity->getProject()) {
$customer = $entity->getProject()->getCustomer()->getName();
}
$sheet->setCellValueByColumnAndRow($column, $row, $customer);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $customer);
};
}
@@ -328,7 +334,7 @@ abstract class AbstractSpreadsheetRenderer
if (null !== $entity->getProject()) {
$project = $entity->getProject()->getName();
}
$sheet->setCellValueByColumnAndRow($column, $row, $project);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $project);
};
}
@@ -338,7 +344,7 @@ abstract class AbstractSpreadsheetRenderer
if (null !== $entity->getActivity()) {
$activity = $entity->getActivity()->getName();
}
$sheet->setCellValueByColumnAndRow($column, $row, $activity);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $activity);
};
}
@@ -352,7 +358,7 @@ abstract class AbstractSpreadsheetRenderer
$isColumnFormatted = false;
$columns['description']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) use (&$isColumnFormatted, $maxWidth, $wrapText, $sanitizeText) {
$cell = $sheet->getCellByColumnAndRow($column, $row);
$cell = $sheet->getCell(CellAddress::fromColumnAndRow($column, $row));
$desc = $entity->getDescription();
if ($sanitizeText && null !== $desc) {
@@ -379,20 +385,20 @@ abstract class AbstractSpreadsheetRenderer
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->setCellValueByColumnAndRow($column, $row, $this->translator->trans($exported));
$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->setCellValueByColumnAndRow($column, $row, $this->translator->trans($exported));
$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->setCellValueByColumnAndRow($column, $row, implode(',', $entity->getTagsAsArray()));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), implode(',', $entity->getTagsAsArray()));
};
}
@@ -422,7 +428,7 @@ abstract class AbstractSpreadsheetRenderer
$columns['timesheet-meta'] = [
'header' => function (Worksheet $sheet, $row, $column) use ($timesheetMetaFields): int {
foreach ($timesheetMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel()));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
}
return \count($timesheetMetaFields);
@@ -434,7 +440,7 @@ abstract class AbstractSpreadsheetRenderer
if (null !== $metaField) {
$metaFieldValue = $metaField->getValue();
}
$sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
}
return \count($timesheetMetaFields);
@@ -450,7 +456,7 @@ abstract class AbstractSpreadsheetRenderer
$columns['customer-meta'] = [
'header' => function (Worksheet $sheet, $row, $column) use ($customerMetaFields): int {
foreach ($customerMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel()));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
}
return \count($customerMetaFields);
@@ -464,7 +470,7 @@ abstract class AbstractSpreadsheetRenderer
$metaFieldValue = $metaField->getValue();
}
}
$sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
}
return \count($customerMetaFields);
@@ -477,7 +483,7 @@ abstract class AbstractSpreadsheetRenderer
$columns['project-meta'] = [
'header' => function (Worksheet $sheet, $row, $column) use ($projectMetaFields): int {
foreach ($projectMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel()));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
}
return \count($projectMetaFields);
@@ -491,7 +497,7 @@ abstract class AbstractSpreadsheetRenderer
$metaFieldValue = $metaField->getValue();
}
}
$sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
}
return \count($projectMetaFields);
@@ -504,7 +510,7 @@ abstract class AbstractSpreadsheetRenderer
$columns['activity-meta'] = [
'header' => function (Worksheet $sheet, $row, $column) use ($activityMetaFields): int {
foreach ($activityMetaFields as $metaField) {
$sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel()));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
}
return \count($activityMetaFields);
@@ -518,7 +524,7 @@ abstract class AbstractSpreadsheetRenderer
$metaFieldValue = $metaField->getValue();
}
}
$sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
}
return \count($activityMetaFields);
@@ -533,7 +539,7 @@ abstract class AbstractSpreadsheetRenderer
$columns['user-meta'] = [
'header' => function (Worksheet $sheet, $row, $column) use ($userPreferences): int {
foreach ($userPreferences as $metaField) {
$sheet->setCellValueByColumnAndRow($column++, $row, $this->translator->trans($metaField->getLabel()));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $this->translator->trans($metaField->getLabel()));
}
return \count($userPreferences);
@@ -547,7 +553,7 @@ abstract class AbstractSpreadsheetRenderer
$metaFieldValue = $metaField->getValue();
}
}
$sheet->setCellValueByColumnAndRow($column++, $row, $metaFieldValue);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column++, $row), $metaFieldValue);
}
return \count($userPreferences);
@@ -557,20 +563,20 @@ abstract class AbstractSpreadsheetRenderer
if (isset($columns['type']) && !isset($columns['type']['render'])) {
$columns['type']['render'] = function (Worksheet $sheet, int $row, int $column, ExportableItem $entity) {
$sheet->setCellValueByColumnAndRow($column, $row, $entity->getType());
$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->setCellValueByColumnAndRow($column, $row, $entity->getCategory());
$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, $row, $column): int {
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans('number'));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('number'));
return 1;
};
@@ -582,7 +588,7 @@ abstract class AbstractSpreadsheetRenderer
if (null !== $entity->getProject()) {
$customerId = $entity->getProject()->getCustomer()->getNumber();
}
$sheet->setCellValueByColumnAndRow($column, $row, $customerId);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $customerId);
};
}
}
@@ -590,7 +596,7 @@ abstract class AbstractSpreadsheetRenderer
if (isset($columns['customer_vat']) && !isset($columns['customer_vat']['render'])) {
if (!isset($columns['customer_vat']['header'])) {
$columns['customer_vat']['header'] = function (Worksheet $sheet, $row, $column): int {
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans('vat_id'));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('vat_id'));
return 1;
};
@@ -602,7 +608,7 @@ abstract class AbstractSpreadsheetRenderer
if (null !== $entity->getProject()) {
$customerVat = $entity->getProject()->getCustomer()->getVatId();
}
$sheet->setCellValueByColumnAndRow($column, $row, $customerVat);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $customerVat);
};
}
}
@@ -610,7 +616,7 @@ abstract class AbstractSpreadsheetRenderer
if (isset($columns['order_number']) && !isset($columns['order_number']['render'])) {
if (!isset($columns['order_number']['header'])) {
$columns['order_number']['header'] = function (Worksheet $sheet, $row, $column): int {
$sheet->setCellValueByColumnAndRow($column, $row, $this->translator->trans('orderNumber'));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $this->translator->trans('orderNumber'));
return 1;
};
@@ -622,7 +628,7 @@ abstract class AbstractSpreadsheetRenderer
if (null !== $entity->getProject()) {
$orderNumber = $entity->getProject()->getOrderNumber();
}
$sheet->setCellValueByColumnAndRow($column, $row, $orderNumber);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $orderNumber);
};
}
}
@@ -663,10 +669,13 @@ abstract class AbstractSpreadsheetRenderer
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->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($label));
$sheet->setCellValue(CellAddress::fromColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow), $this->translator->trans($label));
}
}
@@ -689,11 +698,11 @@ abstract class AbstractSpreadsheetRenderer
}
if (!\array_key_exists('render', $settings) || !\is_callable($settings['render'])) {
throw new \RuntimeException(sprintf('Missing renderer for export column %s', $label));
throw new \RuntimeException(sprintf('Missing or invalid renderer for export column %s', $label));
}
$amount = $settings['render']($sheet, $entryHeaderRow, $entryHeaderColumn, $exportItem);
$entryHeaderColumn += (null === $amount) ? 1 : $amount;
$entryHeaderColumn += (null === $amount) ? 1 : (int) $amount;
}
$entryHeaderRow++;
@@ -701,28 +710,28 @@ abstract class AbstractSpreadsheetRenderer
if ($this->isTotalRowSupported()) {
if (null !== $durationColumn) {
$startCoordinate = $sheet->getCellByColumnAndRow($durationColumn, 2)->getCoordinate();
$endCoordinate = $sheet->getCellByColumnAndRow($durationColumn, $entryHeaderRow - 1)->getCoordinate();
$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->getStyleByColumnAndRow($durationColumn, $entryHeaderRow);
$style = $sheet->getStyle(CellAddress::fromColumnAndRow($durationColumn, $entryHeaderRow));
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
$style->getFont()->setBold(true);
}
if (null !== $rateColumn) {
$startCoordinate = $sheet->getCellByColumnAndRow($rateColumn, 2)->getCoordinate();
$endCoordinate = $sheet->getCellByColumnAndRow($rateColumn, $entryHeaderRow - 1)->getCoordinate();
$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->getStyleByColumnAndRow($rateColumn, $entryHeaderRow);
$style = $sheet->getStyle(CellAddress::fromColumnAndRow($rateColumn, $entryHeaderRow));
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
$style->getFont()->setBold(true);
}
if (null !== $internalRateColumn) {
$startCoordinate = $sheet->getCellByColumnAndRow($internalRateColumn, 2)->getCoordinate();
$endCoordinate = $sheet->getCellByColumnAndRow($internalRateColumn, $entryHeaderRow - 1)->getCoordinate();
$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->getStyleByColumnAndRow($internalRateColumn, $entryHeaderRow);
$style = $sheet->getStyle(CellAddress::fromColumnAndRow($internalRateColumn, $entryHeaderRow));
$style->getBorders()->getTop()->setBorderStyle(Border::BORDER_THIN);
$style->getFont()->setBold(true);
}

View File

@@ -9,6 +9,7 @@
namespace App\Export\Base;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
@@ -54,16 +55,17 @@ class CsvRenderer extends AbstractSpreadsheetRenderer
return 'csv';
}
protected function setDuration(Worksheet $sheet, $column, $row, $duration): void
protected function setDuration(Worksheet $sheet, int $column, int $row, ?int $duration): void
{
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s', $duration));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), sprintf('=%s', $duration ?? 0));
}
protected function setRateStyle(Worksheet $sheet, $column, $row, $rate, $currency): void
protected function setRate(Worksheet $sheet, int $column, int $row, ?float $rate, ?string $currency): void
{
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $rate);
if ($rate === 0.00) {
return;
}
parent::setRateStyle($sheet, $column, $row, $rate, $currency);
$this->setRateStyle($sheet, $column, $row, $currency);
}
}

View File

@@ -45,6 +45,16 @@ class XlsxRenderer extends AbstractSpreadsheetRenderer
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();
@@ -75,11 +85,6 @@ class XlsxRenderer extends AbstractSpreadsheetRenderer
->getAlignment()
->setVertical(Alignment::VERTICAL_TOP)
->setHorizontal(Alignment::HORIZONTAL_LEFT);
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
$writer->save($filename);
return $filename;
}
public function getId(): string

View File

@@ -9,6 +9,7 @@
namespace App\Export\Spreadsheet\CellFormatter;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
final class ArrayFormatter implements CellFormatterInterface
@@ -16,7 +17,7 @@ final class ArrayFormatter implements CellFormatterInterface
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
{
if (null === $value) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
return;
}
@@ -25,6 +26,6 @@ final class ArrayFormatter implements CellFormatterInterface
throw new \InvalidArgumentException('Unsupported value given, only array is supported');
}
$sheet->setCellValueByColumnAndRow($column, $row, implode(';', $value));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), implode(';', $value));
}
}

View File

@@ -9,6 +9,7 @@
namespace App\Export\Spreadsheet\CellFormatter;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
final class BooleanFormatter implements CellFormatterInterface
@@ -16,7 +17,7 @@ final class BooleanFormatter implements CellFormatterInterface
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
{
if (null === $value) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
return;
}
@@ -25,6 +26,6 @@ final class BooleanFormatter implements CellFormatterInterface
throw new \InvalidArgumentException('Unsupported value given, only boolean is supported');
}
$sheet->setCellValueByColumnAndRow($column, $row, $value);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), $value);
}
}

View File

@@ -9,6 +9,7 @@
namespace App\Export\Spreadsheet\CellFormatter;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
@@ -18,7 +19,7 @@ final class DateFormatter implements CellFormatterInterface
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
{
if (null === $value) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
return;
}
@@ -27,7 +28,7 @@ final class DateFormatter implements CellFormatterInterface
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
}
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
}
}

View File

@@ -9,6 +9,7 @@
namespace App\Export\Spreadsheet\CellFormatter;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
@@ -19,7 +20,7 @@ final class DateTimeFormatter implements CellFormatterInterface
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
{
if (null === $value) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
return;
}
@@ -28,7 +29,7 @@ final class DateTimeFormatter implements CellFormatterInterface
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
}
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
}
}

View File

@@ -9,6 +9,7 @@
namespace App\Export\Spreadsheet\CellFormatter;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
final class DurationFormatter implements CellFormatterInterface
@@ -25,7 +26,7 @@ final class DurationFormatter implements CellFormatterInterface
throw new \InvalidArgumentException('Unsupported value given, only int is supported');
}
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/86400', $value));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), sprintf('=%s/86400', $value));
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
}
}

View File

@@ -9,6 +9,7 @@
namespace App\Export\Spreadsheet\CellFormatter;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
@@ -19,7 +20,7 @@ final class TimeFormatter implements CellFormatterInterface
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
{
if (null === $value) {
$sheet->setCellValueByColumnAndRow($column, $row, '');
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), '');
return;
}
@@ -28,7 +29,7 @@ final class TimeFormatter implements CellFormatterInterface
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
}
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
$sheet->setCellValue(CellAddress::fromColumnAndRow($column, $row), Date::PHPToExcel($value));
$sheet->getStyle(CellAddress::fromColumnAndRow($column, $row))->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
}
}

View File

@@ -16,6 +16,7 @@ use App\Export\Spreadsheet\CellFormatter\DateFormatter;
use App\Export\Spreadsheet\CellFormatter\DateTimeFormatter;
use App\Export\Spreadsheet\CellFormatter\DurationFormatter;
use App\Export\Spreadsheet\CellFormatter\TimeFormatter;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -65,7 +66,7 @@ class SpreadsheetExporter
$recordsHeaderRow = 1;
foreach ($columns as $settings) {
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($settings->getLabel()));
$sheet->setCellValue(CellAddress::fromColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow), $this->translator->trans($settings->getLabel()));
}
$entryHeaderRow = $recordsHeaderRow + 1;
@@ -77,7 +78,7 @@ class SpreadsheetExporter
$value = \call_user_func($settings->getAccessor(), $entry);
if (!\array_key_exists($settings->getType(), $this->formatter)) {
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $value);
$sheet->setCellValue(CellAddress::fromColumnAndRow($entryHeaderColumn, $entryHeaderRow), $value);
} else {
$formatter = $this->formatter[$settings->getType()];
$formatter->setFormattedValue($sheet, $entryHeaderColumn, $entryHeaderRow, $value);

View File

@@ -9,9 +9,10 @@
namespace App\Form\Model;
use App\Utils\EquatableInterface;
use DateTime;
final class DateRange
final class DateRange implements EquatableInterface
{
private ?DateTime $begin = null;
private ?DateTime $end = null;
@@ -49,4 +50,29 @@ final class DateRange
return $this;
}
public function isEqualTo(object $compare): bool
{
if (!$compare instanceof DateRange) {
return false;
}
if (($this->getBegin() === null && $compare->getBegin() !== null) || ($this->getBegin() !== null && $compare->getBegin() === null)) {
return false;
}
if (($this->getEnd() === null && $compare->getEnd() !== null) || ($this->getEnd() !== null && $compare->getEnd() === null)) {
return false;
}
if ($this->getBegin() !== null && $compare->getBegin() !== null && $this->getBegin()->getTimestamp() !== $compare->getBegin()->getTimestamp()) {
return false;
}
if ($this->getEnd() !== null && $compare->getEnd() !== null && $this->getEnd()->getTimestamp() !== $compare->getEnd()->getTimestamp()) {
return false;
}
return true;
}
}

View File

@@ -29,7 +29,6 @@ final class FavoriteMenuType extends AbstractType
{
$resolver->setDefaults([
'multiple' => true,
'include_reports' => false,
'filter_menus' => ['favorites', 'dashboard'],
]);
}

View File

@@ -9,20 +9,17 @@
namespace App\Form\Type;
use App\Entity\User;
use App\Event\ConfigureMainMenuEvent;
use App\Reporting\ReportingService;
use App\Utils\MenuItemModel;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
final class MenuChoiceType extends AbstractType
{
public function __construct(private EventDispatcherInterface $eventDispatcher, private ReportingService $reportingService, private TranslatorInterface $translator)
public function __construct(private EventDispatcherInterface $eventDispatcher)
{
}
@@ -30,25 +27,19 @@ final class MenuChoiceType extends AbstractType
{
$resolver->setDefaults([
'required' => true,
'include_reports' => true,
'filter_menus' => [],
]);
$resolver->setDefault('choices', function (Options $options): array {
/** @var User $user */
$user = $options['user'];
return $this->getChoices($user, $options['include_reports'], $options['filter_menus']);
return $this->getChoices($options['filter_menus']);
});
}
/**
* @param User $user
* @param bool $withReports
* @param array<string> $filter
* @return array<string, string>
*/
private function getChoices(User $user, bool $withReports, array $filter): array
private function getChoices(array $filter): array
{
$event = new ConfigureMainMenuEvent();
$this->eventDispatcher->dispatch($event);
@@ -58,13 +49,6 @@ final class MenuChoiceType extends AbstractType
$choices += $this->getChoicesFromMenu($event->getAdminMenu(), $filter);
$choices += $this->getChoicesFromMenu($event->getSystemMenu(), $filter);
if ($withReports) {
foreach ($this->reportingService->getAvailableReports($user) as $report) {
$label = $this->translator->trans('menu.reporting') . ': ' . $this->translator->trans($report->getLabel(), [], 'reporting');
$choices[$label] = $report->getId();
}
}
return $choices;
}
@@ -82,13 +66,13 @@ final class MenuChoiceType extends AbstractType
continue;
}
if (!$child->hasChildren()) {
if (\count($child->getRouteArgs()) === 0) {
if (\count($child->getRouteArgs()) === 0 && $child->getRoute() !== null) {
$choices[$child->getLabel()] = $child->getIdentifier();
}
continue;
}
foreach ($child->getChildren() as $subChild) {
if (\count($subChild->getRouteArgs()) === 0) {
if (\count($subChild->getRouteArgs()) === 0 && $subChild->getRoute() !== null) {
$choices[$subChild->getLabel()] = $subChild->getIdentifier();
}
}

View File

@@ -0,0 +1,41 @@
<?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\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Custom form field type to select the user language.
*/
final class UserLanguageType extends AbstractType
{
public function __construct(private UrlGeneratorInterface $router, private TranslatorInterface $translator)
{
}
public function configureOptions(OptionsResolver $resolver): void
{
$route = $this->router->generate('help_locales');
$message = $this->translator->trans('user.language.help');
$resolver->setDefaults([
'help_html' => true,
'help' => sprintf('<a href="%s" target="help_locales">%s</a>', $route, $message)
]);
}
public function getParent(): string
{
return LanguageType::class;
}
}

View File

@@ -53,6 +53,14 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
throw new \InvalidArgumentException('Missing invoice model, cannot calculate invoice number');
}
if ($format === 'cname' && $this->model->getCustomer()?->getName() === null) {
throw new \InvalidArgumentException('Customer has no name, replacer {cname} failed evaluation');
}
if ($format === 'cnumber' && $this->model->getCustomer()?->getNumber() === null) {
throw new \InvalidArgumentException('Customer has no number, replacer {cnumber} failed evaluation');
}
return match ($format) {
'Y' => $invoiceDate->format('Y'),
'y' => $invoiceDate->format('y'),

View File

@@ -13,6 +13,7 @@ use App\Entity\Bookmark;
use App\Entity\Team;
use App\Entity\User;
use App\Form\Model\DateRange;
use App\Utils\EquatableInterface;
use App\Utils\SearchTerm;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormErrorIterator;
@@ -330,7 +331,13 @@ class BaseQuery
$currentValue = $this->get($filter);
if (\is_object($currentValue)) {
if ($currentValue !== $expectedValue) {
if ($currentValue instanceof EquatableInterface) {
return $currentValue->isEqualTo($expectedValue);
}
// this is a loose comparison by choice, as == compares object type and content
// instead of === which only compares if both sides are the same instance
if ($currentValue != $expectedValue) { // @phpstan-ignore-line
return false;
}
} else {

View File

@@ -113,8 +113,23 @@ final class DataTable implements \Countable, \IteratorAggregate
$this->columns = $columns;
}
public function addColumn(string $name, array $column): void
/**
* Supported $column options:
* - class
* - title
* - translation_domain
* - orderBy (string|false)
* - order (desc, asc)
*
* @param string $name
* @param array $column
* @return void
*/
public function addColumn(string $name, array $column = []): void
{
if (!\array_key_exists('class', $column)) {
$column['class'] = '';
}
$this->columns[$name] = $column;
}

View 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\Utils;
interface EquatableInterface
{
public function isEqualTo(object $compare): bool;
}