Release 2.0.25 (#4066)
* added support for hourly rate column in detail table * allow to register icon in extension * allow to show QR code secret * new translations * bump theme and packages * fix validation for invoice-document-filenames with uppercase character * max upload size 1MB * fix last month in daterange-picker in certain situations, more years in quick-select * remove unused package-versions-deprecated * link preferences from contract warning message * added page_setup page layout * prevent DROP TABLE in addSQL() and replace drop table with schema call * added azuyalabs/yasumi
This commit is contained in:
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.0.24';
|
||||
public const VERSION = '2.0.25';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 20024;
|
||||
public const VERSION_ID = 20025;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -64,8 +64,10 @@ final class ContractController extends AbstractController
|
||||
$yearDate = $values->getDate();
|
||||
$year = $workingTimeService->getYear($profile, $yearDate);
|
||||
|
||||
$page = new PageSetup('status');
|
||||
$page = new PageSetup('work_times');
|
||||
$page->setHelp('contract.html');
|
||||
$page->setActionName('contract');
|
||||
$page->setPaginationForm($form);
|
||||
|
||||
// additional boxes by plugins
|
||||
$controllerEvent = new WorkContractDetailControllerEvent($year);
|
||||
|
||||
@@ -120,27 +120,25 @@ final class DoctorController extends AbstractController
|
||||
/** @var array<string, string> $versions */
|
||||
$versions = [];
|
||||
|
||||
if (class_exists(InstalledVersions::class)) {
|
||||
$rootPackage = InstalledVersions::getRootPackage()['name'];
|
||||
foreach (InstalledVersions::getInstalledPackages() as $package) {
|
||||
$versions[$package] = InstalledVersions::getPrettyVersion($package);
|
||||
$rootPackage = InstalledVersions::getRootPackage()['name'];
|
||||
foreach (InstalledVersions::getInstalledPackages() as $package) {
|
||||
$versions[$package] = InstalledVersions::getPrettyVersion($package);
|
||||
}
|
||||
|
||||
// remove kimai from the package list
|
||||
$versions = array_filter($versions, function ($version, $name) use ($rootPackage): bool {
|
||||
if ($name === $rootPackage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove kimai from the package list
|
||||
$versions = array_filter($versions, function ($version, $name) use ($rootPackage): bool {
|
||||
if ($name === $rootPackage) {
|
||||
return false;
|
||||
}
|
||||
if ($version === null || $version === '*') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($version === null || $version === '*') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
return true;
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
ksort($versions);
|
||||
}
|
||||
ksort($versions);
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
@@ -413,6 +413,7 @@ final class ProfileController extends AbstractController
|
||||
'form' => $form->createView(),
|
||||
'deactivate' => $this->getTwoFactorDeactivationForm($profile)->createView(),
|
||||
'qr_code' => $result,
|
||||
'secret' => $profile->getTotpSecret(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Event\CustomerUpdatePostEvent;
|
||||
use App\Event\CustomerUpdatePreEvent;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Utils\NumberGenerator;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
@@ -107,6 +108,19 @@ final class CustomerService
|
||||
return $this->repository->findOneBy(['number' => $number]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<Customer>
|
||||
*/
|
||||
public function findCustomer(CustomerQuery $query): iterable
|
||||
{
|
||||
return $this->repository->getCustomersForQuery($query);
|
||||
}
|
||||
|
||||
public function countCustomer(bool $visible = true): int
|
||||
{
|
||||
return $this->repository->countCustomer($visible);
|
||||
}
|
||||
|
||||
public function calculateNextCustomerNumber(): string
|
||||
{
|
||||
$format = $this->configuration->find('customer.number_format');
|
||||
|
||||
@@ -64,4 +64,20 @@ abstract class AbstractMigration extends BaseAbstractMigration
|
||||
{
|
||||
$this->addSql('#prevent empty warning - no SQL to execute');
|
||||
}
|
||||
|
||||
/**
|
||||
* I don't know how often I accidentally dropped database tables,
|
||||
* because a generated "left-over" migration was executed.
|
||||
*
|
||||
* @param mixed[] $params
|
||||
* @param mixed[] $types
|
||||
*/
|
||||
protected function addSql(string $sql, array $params = [], array $types = []): void
|
||||
{
|
||||
if (str_starts_with($sql, 'DROP TABLE ')) {
|
||||
throw new \InvalidArgumentException('Cannot use addSql() with DROP TABLE');
|
||||
}
|
||||
|
||||
parent::addSql($sql, $params, $types);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
* If not empty two-factor authentication is enabled.
|
||||
*/
|
||||
#[ORM\Column(name: 'totp_secret', type: 'string', nullable: true)]
|
||||
private ?string $totpSecret;
|
||||
private ?string $totpSecret = null;
|
||||
#[ORM\Column(name: 'totp_enabled', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||
private bool $totpEnabled = false;
|
||||
#[ORM\Column(name: 'system_account', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||
@@ -1109,6 +1109,11 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return $this->totpSecret !== null;
|
||||
}
|
||||
|
||||
public function getTotpSecret(): ?string
|
||||
{
|
||||
return $this->totpSecret;
|
||||
}
|
||||
|
||||
public function isTotpAuthenticationEnabled(): bool
|
||||
{
|
||||
return $this->totpEnabled;
|
||||
|
||||
@@ -65,6 +65,7 @@ final class InvoiceDocumentUploadForm extends AbstractType
|
||||
new File([
|
||||
'mimeTypes' => $mimetypes,
|
||||
'mimeTypesMessage' => 'This file type is not allowed',
|
||||
'maxSize' => '1024k',
|
||||
]),
|
||||
new Callback([$this, 'validateDocument'])
|
||||
],
|
||||
@@ -117,7 +118,7 @@ final class InvoiceDocumentUploadForm extends AbstractType
|
||||
|
||||
$safeFilename = transliterator_transliterate(self::FILENAME_RULE, $nameWithoutExtension);
|
||||
|
||||
if ($safeFilename !== $nameWithoutExtension) {
|
||||
if ($safeFilename === false || strtolower($safeFilename) !== strtolower($nameWithoutExtension)) {
|
||||
$context->buildViolation('This invoice document cannot be used, filename may only contain the following ascii character: %character%')
|
||||
->setParameters(['%character%' => 'A-Z a-z 0-9 _ -'])
|
||||
->setTranslationDomain('validators')
|
||||
|
||||
@@ -17,8 +17,9 @@ final class Configuration
|
||||
{
|
||||
private ?string $label = null;
|
||||
private string $translationDomain = 'messages';
|
||||
private string|int|null|bool|array $value = null;
|
||||
private string|int|null|bool|float $value = null;
|
||||
private ?string $type = null;
|
||||
/** @var array<string, mixed> */
|
||||
private array $options = [];
|
||||
private bool $enabled = true;
|
||||
private bool $required = true;
|
||||
@@ -36,12 +37,12 @@ final class Configuration
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getValue(): string|int|null|bool|object
|
||||
public function getValue(): string|int|null|bool|float
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function setValue(string|int|null|bool|object $value): Configuration
|
||||
public function setValue(string|int|null|bool|float $value): Configuration
|
||||
{
|
||||
if ($this->type === CheckboxType::class || $this->type === YesNoType::class) {
|
||||
$value = (bool) $value;
|
||||
@@ -131,11 +132,17 @@ final class Configuration
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function setOptions(array $options): Configuration
|
||||
{
|
||||
$this->options = $options;
|
||||
|
||||
@@ -61,18 +61,24 @@ final class DateRangeType extends AbstractType
|
||||
$user = $options['user'];
|
||||
$factory = DateTimeFactory::createByUser($user);
|
||||
|
||||
$view->vars['ranges'] = [
|
||||
'today' => [$factory->createDateTime('00:00:00'), $factory->createDateTime('23:59:59')],
|
||||
'yesterday' => [$factory->createDateTime('-1 day 00:00:00'), $factory->createDateTime('-1 day 23:59:59')],
|
||||
'thisWeek' => [$factory->getStartOfWeek(), $factory->getEndOfWeek()],
|
||||
'lastWeek' => [$factory->getStartOfWeek('-1 week'), $factory->getEndOfWeek('-1 week')],
|
||||
'thisMonth' => [$factory->getStartOfMonth(), $factory->getEndOfMonth()],
|
||||
'lastMonth' => [$factory->getStartOfMonth('-1 month'), $factory->getEndOfMonth('-1 month')],
|
||||
'thisYear' => [$factory->createStartOfYear(), $factory->createEndOfYear()],
|
||||
'thisYearUntilNow' => [$factory->createStartOfYear(), $factory->createDateTime('23:59:59')],
|
||||
'lastYear' => [$factory->createStartOfYear('-1 year'), $factory->createEndOfYear('-1 year')],
|
||||
'allTime' => [null, null],
|
||||
$ranges = [
|
||||
'daterangepicker.allTime' => [null, null],
|
||||
'daterangepicker.today' => [$factory->createDateTime('00:00:00'), $factory->createDateTime('23:59:59')],
|
||||
'daterangepicker.yesterday' => [$factory->createDateTime('-1 day 00:00:00'), $factory->createDateTime('-1 day 23:59:59')],
|
||||
'daterangepicker.thisWeek' => [$factory->getStartOfWeek(), $factory->getEndOfWeek()],
|
||||
'daterangepicker.lastWeek' => [$factory->getStartOfWeek('-1 week'), $factory->getEndOfWeek('-1 week')],
|
||||
'daterangepicker.thisMonth' => [$factory->getStartOfMonth(), $factory->getEndOfMonth()],
|
||||
'daterangepicker.lastMonth' => [$factory->getStartOfLastMonth(), $factory->getEndOfLastMonth()],
|
||||
'daterangepicker.thisYearUntilNow' => [$factory->createStartOfYear(), $factory->createDateTime('23:59:59')],
|
||||
];
|
||||
|
||||
$thisYear = (int) $factory->createStartOfYear()->format('Y');
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$year = $thisYear - $i;
|
||||
$ranges[$year] = [$year . '-01-01', $year . '-12-31'];
|
||||
}
|
||||
|
||||
$view->vars['ranges'] = $ranges;
|
||||
$view->vars['rangeFormat'] = $options['format'];
|
||||
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||
|
||||
@@ -14,6 +14,17 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
|
||||
abstract class AbstractPluginExtension extends Extension
|
||||
{
|
||||
protected function registerIcon(ContainerBuilder $container, string $name, string $icon): void
|
||||
{
|
||||
$container->setParameter(
|
||||
'tabler_bundle.icons',
|
||||
array_merge(
|
||||
$container->getParameter('tabler_bundle.icons'),
|
||||
[$name => $icon]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function registerBundleConfiguration(ContainerBuilder $container, array $configs): void
|
||||
{
|
||||
$bundleConfig = [$this->getAlias() => $configs];
|
||||
|
||||
@@ -48,6 +48,14 @@ final class DateTimeFactory
|
||||
return $date;
|
||||
}
|
||||
|
||||
public function getStartOfLastMonth(): DateTimeInterface
|
||||
{
|
||||
$newDate = $this->createDateTime('first day of -1 month');
|
||||
$newDate->setTime(0, 0, 0);
|
||||
|
||||
return $newDate;
|
||||
}
|
||||
|
||||
private function getDate(DateTimeInterface|string|null $date = null): DateTime
|
||||
{
|
||||
if ($date === null) {
|
||||
@@ -105,6 +113,14 @@ final class DateTimeFactory
|
||||
return $date;
|
||||
}
|
||||
|
||||
public function getEndOfLastMonth(): DateTimeInterface
|
||||
{
|
||||
$newDate = $this->createDateTime('last day of -1 month');
|
||||
$newDate->setTime(23, 59, 59);
|
||||
|
||||
return $newDate;
|
||||
}
|
||||
|
||||
private function createWeekDateTime($year, $week, $day, $hour, $minute, $second)
|
||||
{
|
||||
$date = new DateTime('now', $this->getTimezone());
|
||||
|
||||
@@ -148,7 +148,7 @@ final class DataTable implements \Countable, \IteratorAggregate
|
||||
|
||||
public function hasConfiguration(): bool
|
||||
{
|
||||
return $this->configuration;
|
||||
return $this->configuration && \count($this->columns) > 0;
|
||||
}
|
||||
|
||||
public function getPaginationRoute(): ?string
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Utils;
|
||||
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
|
||||
final class PageSetup
|
||||
{
|
||||
private ?string $help = null;
|
||||
@@ -17,11 +20,27 @@ final class PageSetup
|
||||
private string $translationDomain = 'messages';
|
||||
private array $actionPayload = [];
|
||||
private ?DataTable $dataTable = null;
|
||||
private ?FormInterface $paginationForm = null;
|
||||
|
||||
public function __construct(private string $title)
|
||||
{
|
||||
}
|
||||
|
||||
public function setPaginationForm(FormInterface $paginationForm): void
|
||||
{
|
||||
$this->paginationForm = $paginationForm;
|
||||
}
|
||||
|
||||
public function hasPaginationForm(): bool
|
||||
{
|
||||
return $this->paginationForm !== null;
|
||||
}
|
||||
|
||||
public function getPaginationForm(): ?FormView
|
||||
{
|
||||
return $this->paginationForm?->createView();
|
||||
}
|
||||
|
||||
public function hasDataTable(): bool
|
||||
{
|
||||
return $this->dataTable !== null;
|
||||
|
||||
Reference in New Issue
Block a user