Release 2.0.14 (#3963)
- show customer totals in single-user reports - support custom fonts in export / invoice templates - require daterange for export and invoice searches - improve help label for user locale - show decimal formats in help controller - cache tag amount per request - composer updates - improved code styles - added default label to date picker - new macros & use own macro
This commit is contained in:
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.0.13';
|
||||
public const VERSION = '2.0.14';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 20013;
|
||||
public const VERSION_ID = 20014;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -307,8 +307,8 @@ final class DoctorController extends AbstractController
|
||||
}
|
||||
|
||||
$phpInfo = $phpinfo['phpinfo'];
|
||||
unset($phpInfo[0]);
|
||||
unset($phpInfo[1]);
|
||||
array_pop($phpInfo);
|
||||
array_shift($phpInfo);
|
||||
|
||||
return $phpInfo;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ final class HelpController extends AbstractController
|
||||
$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('decimal', ['class' => 'd-none w-min text-end', 'orderBy' => false, 'title' => 'Decimal']);
|
||||
$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']);
|
||||
@@ -75,6 +76,7 @@ final class HelpController extends AbstractController
|
||||
'date' => $formatter->dateShort($now),
|
||||
'time' => $formatter->time($now),
|
||||
'duration' => $formatter->duration(46120),
|
||||
'decimal' => $formatter->durationDecimal(46120),
|
||||
'money' => $formatter->money(2794.83, 'EUR'),
|
||||
'hour_24' => $service->is24Hour($locale),
|
||||
'rtl' => $service->isRightToLeft($locale),
|
||||
|
||||
@@ -113,6 +113,24 @@ abstract class AbstractUserReportController extends AbstractController
|
||||
$data[$project->getId()]['project'] = $project;
|
||||
}
|
||||
|
||||
return $data;
|
||||
$customers = [];
|
||||
foreach ($data as $id => $row) {
|
||||
$customerId = (string) $row['project']->getCustomer()->getId();
|
||||
if (!\array_key_exists($customerId, $customers)) {
|
||||
$customers[$customerId] = [
|
||||
'customer' => $row['project']->getCustomer(),
|
||||
'projects' => [],
|
||||
'duration' => 0,
|
||||
'rate' => 0.0,
|
||||
'internalRate' => 0.0,
|
||||
];
|
||||
}
|
||||
$customers[$customerId]['projects'][$id] = $row;
|
||||
$customers[$customerId]['duration'] += $row['duration'];
|
||||
$customers[$customerId]['rate'] += $row['rate'];
|
||||
$customers[$customerId]['internalRate'] += $row['internalRate'];
|
||||
}
|
||||
|
||||
return $customers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,19 @@ namespace App\Event;
|
||||
*/
|
||||
final class ActivityDetailControllerEvent extends AbstractActivityEvent
|
||||
{
|
||||
private $controller = [];
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private array $controller = [];
|
||||
|
||||
public function addController(string $controller): void
|
||||
{
|
||||
$this->controller[] = $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getController(): array
|
||||
{
|
||||
return $this->controller;
|
||||
|
||||
@@ -16,13 +16,19 @@ namespace App\Event;
|
||||
*/
|
||||
final class CustomerDetailControllerEvent extends AbstractCustomerEvent
|
||||
{
|
||||
private $controller = [];
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private array $controller = [];
|
||||
|
||||
public function addController(string $controller): void
|
||||
{
|
||||
$this->controller[] = $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getController(): array
|
||||
{
|
||||
return $this->controller;
|
||||
|
||||
@@ -16,13 +16,19 @@ namespace App\Event;
|
||||
*/
|
||||
final class ProjectDetailControllerEvent extends AbstractProjectEvent
|
||||
{
|
||||
private $controller = [];
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private array $controller = [];
|
||||
|
||||
public function addController(string $controller): void
|
||||
{
|
||||
$this->controller[] = $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getController(): array
|
||||
{
|
||||
return $this->controller;
|
||||
|
||||
@@ -25,7 +25,7 @@ final class ExportToolbarForm extends AbstractType
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||
$this->addDateRange($builder, ['timezone' => $options['timezone']], false, true);
|
||||
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
|
||||
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
|
||||
$this->addActivitySelect($builder, [], true, true, false);
|
||||
|
||||
@@ -26,7 +26,7 @@ final class InvoiceToolbarForm extends AbstractType
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||
$this->addDateRange($builder, ['timezone' => $options['timezone']], false, true);
|
||||
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
|
||||
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
|
||||
$this->addActivitySelect($builder, [], true, true, false);
|
||||
|
||||
@@ -155,7 +155,7 @@ trait ToolbarFormTrait
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addDateRange(FormBuilderInterface $builder, array $options, $allowEmpty = true, $required = false): void
|
||||
protected function addDateRange(FormBuilderInterface $builder, array $options, bool $allowEmpty = true, bool $required = false): void
|
||||
{
|
||||
$params = [
|
||||
'required' => $required,
|
||||
|
||||
@@ -57,6 +57,7 @@ class DatePickerType extends AbstractType
|
||||
$formFormat = $converter->convert($format);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'label' => 'date',
|
||||
'widget' => 'single_text',
|
||||
'html5' => false,
|
||||
'format' => $formFormat,
|
||||
|
||||
@@ -16,6 +16,8 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
final class TagsType extends AbstractType
|
||||
{
|
||||
private ?int $count = null;
|
||||
|
||||
public function __construct(
|
||||
private AuthorizationCheckerInterface $auth,
|
||||
private TagRepository $repository
|
||||
@@ -31,7 +33,11 @@ final class TagsType extends AbstractType
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
if ($this->repository->count([]) > TagRepository::MAX_AMOUNT_SELECT) {
|
||||
if ($this->count === null) {
|
||||
$this->count = $this->repository->count([]);
|
||||
}
|
||||
|
||||
if ($this->count > TagRepository::MAX_AMOUNT_SELECT) {
|
||||
return TagsInputType::class;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,11 @@ final class UserLanguageType extends AbstractType
|
||||
{
|
||||
$route = $this->router->generate('help_locales');
|
||||
$message = $this->translator->trans('user.language.help');
|
||||
$moreLink = $this->translator->trans('help_locales');
|
||||
|
||||
$resolver->setDefaults([
|
||||
'help_html' => true,
|
||||
'help' => sprintf('<a href="%s" target="help_locales">%s</a>', $route, $message)
|
||||
'help' => sprintf('%2$s <a href="%1$s" target="help_locales">%3$s</a>', $route, $message, $moreLink)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ final class MPdfConverter implements HtmlToPdfConverter
|
||||
$fonts = new FontVariables();
|
||||
$allowed = [
|
||||
'mode', 'format', 'default_font_size', 'default_font', 'margin_left', 'margin_right', 'margin_top',
|
||||
'margin_bottom', 'margin_header', 'margin_footer', 'orientation',
|
||||
'margin_bottom', 'margin_header', 'margin_footer', 'orientation', 'fonts',
|
||||
];
|
||||
|
||||
$filtered = array_filter($options, function ($key) use ($allowed, $configs, $fonts): bool {
|
||||
@@ -61,15 +61,7 @@ final class MPdfConverter implements HtmlToPdfConverter
|
||||
['tempDir' => $this->cacheDirectory, 'exposeVersion' => false]
|
||||
);
|
||||
|
||||
$mpdf = new Mpdf($sanitized);
|
||||
$mpdf->creator = Constants::SOFTWARE;
|
||||
|
||||
if (\array_key_exists('fonts', $options)) {
|
||||
$mpdf->AddFontDirectory($this->fileHelper->getDataDirectory('fonts'));
|
||||
foreach ($options['fonts'] as $fontName => $fontConfig) {
|
||||
$mpdf->AddFont($fontName, $fontConfig);
|
||||
}
|
||||
}
|
||||
$mpdf = $this->initMpdf($sanitized);
|
||||
|
||||
// some OS'es do not follow the PHP default settings
|
||||
if ((int) \ini_get('pcre.backtrack_limit') < 1000000) {
|
||||
@@ -99,4 +91,47 @@ final class MPdfConverter implements HtmlToPdfConverter
|
||||
|
||||
return $mpdf->Output('', Destination::STRING_RETURN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<mixed>> $options
|
||||
* @return Mpdf
|
||||
*/
|
||||
private function initMpdf(array $options): Mpdf
|
||||
{
|
||||
$options['fontDir'] = $this->getFontDirectories();
|
||||
$options['fontdata'] = $this->mergeFontData($options);
|
||||
|
||||
$mpdf = new Mpdf($options);
|
||||
$mpdf->creator = Constants::SOFTWARE;
|
||||
|
||||
return $mpdf;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
private function getFontDirectories(): array
|
||||
{
|
||||
$defaultConfig = (new ConfigVariables())->getDefaults();
|
||||
$fontDirectories = $defaultConfig['fontDir'];
|
||||
$fontDirectories[] = rtrim($this->fileHelper->getDataDirectory('fonts'), DIRECTORY_SEPARATOR);
|
||||
|
||||
return $fontDirectories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<mixed>> $options
|
||||
* @return array<string, array<mixed>>
|
||||
*/
|
||||
private function mergeFontData(array $options): array
|
||||
{
|
||||
$defaultFontConfig = (new FontVariables())->getDefaults();
|
||||
$fontData = $defaultFontConfig['fontdata'];
|
||||
|
||||
if (\array_key_exists('fonts', $options)) {
|
||||
$fontData = array_merge($fontData, $options['fonts']);
|
||||
}
|
||||
|
||||
return $fontData;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user