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:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
86
src/Controller/HelpController.php
Normal file
86
src/Controller/HelpController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ final class FavoriteMenuType extends AbstractType
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'multiple' => true,
|
||||
'include_reports' => false,
|
||||
'filter_menus' => ['favorites', 'dashboard'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
41
src/Form/Type/UserLanguageType.php
Normal file
41
src/Form/Type/UserLanguageType.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
15
src/Utils/EquatableInterface.php
Normal file
15
src/Utils/EquatableInterface.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\Utils;
|
||||
|
||||
interface EquatableInterface
|
||||
{
|
||||
public function isEqualTo(object $compare): bool;
|
||||
}
|
||||
Reference in New Issue
Block a user