Release 2.30 (#5345)

- added missing InvoiceTemplate company, title) field validator
- graceful fallback for missing working-contract mode
- improve email test command (use configured MAIL_FROM)
- additional form types for simple usage in SystemConfiguration and UserPreferences
- allow to extend the working time query via event
This commit is contained in:
Kevin Papst
2025-02-17 08:32:22 +01:00
committed by GitHub
parent d30166e691
commit b341358d0a
20 changed files with 333 additions and 17 deletions

View File

@@ -9,6 +9,7 @@
namespace App\Command;
use App\Constants;
use App\Event\EmailEvent;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Console\Attribute\AsCommand;
@@ -19,7 +20,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Mime\Email;
#[AsCommand(name: 'kimai:mail:test', description: 'Send a test email')]
#[AsCommand(name: 'kimai:mail:test', description: 'Send a test email using MAILER_URL and MAILER_FROM')]
final class MailTestCommand extends Command
{
public function __construct(private readonly EventDispatcherInterface $dispatcher)
@@ -30,16 +31,24 @@ final class MailTestCommand extends Command
protected function configure(): void
{
$this->addArgument('to', InputArgument::REQUIRED, 'The email address to send the email to');
$this->addOption('from', null, InputOption::VALUE_OPTIONAL, 'The sender of the message', 'kimai@example.org');
$this->addOption('from', null, InputOption::VALUE_OPTIONAL, 'Deprecated: uses the MAILER_FROM env variable.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$to = $input->getArgument('to');
if (!\is_string($to) || $to === '') {
throw new \InvalidArgumentException('Need a non-empty "to" address');
}
if ($input->getOption('from') !== null) {
throw new \InvalidArgumentException('The "from" option is deprecated and will be ignored');
}
$message = new Email();
$message->to((string) $input->getArgument('to')); // @phpstan-ignore-line
$message->from((string) $input->getOption('from')); // @phpstan-ignore-line
$message->subject('Kimai test email');
$message->text('This is an email for testing the text body.');
$message->to($to);
$message->subject('Test email - ' . Constants::SOFTWARE);
$message->text('This is a test email from your time-tracker');
$this->dispatcher->dispatch(new EmailEvent($message));

View File

@@ -17,11 +17,11 @@ final class Constants
/**
* The current release version
*/
public const VERSION = '2.29.0';
public const VERSION = '2.30.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 22900;
public const VERSION_ID = 23000;
/**
* The software name
*/

View File

@@ -30,9 +30,11 @@ class InvoiceTemplate
#[Assert\Length(min: 1, max: 60)]
private ?string $name = null;
#[ORM\Column(name: 'title', type: 'string', length: 255, nullable: false)]
#[Assert\Length(max: 255)]
#[Assert\NotBlank]
private ?string $title = null;
#[ORM\Column(name: 'company', type: 'string', length: 255, nullable: false)]
#[Assert\Length(max: 255)]
#[Assert\NotBlank]
private ?string $company = null;
#[ORM\Column(name: 'vat_id', type: 'string', length: 50, nullable: true)]

View File

@@ -0,0 +1,46 @@
<?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\Event;
use App\Entity\User;
use Doctrine\ORM\QueryBuilder;
use Symfony\Contracts\EventDispatcher\Event;
final class WorkingTimeQueryStatsEvent extends Event
{
public function __construct(
private readonly QueryBuilder $queryBuilder,
private readonly User $user,
private readonly \DateTimeInterface $begin,
private readonly \DateTimeInterface $end
)
{
}
public function getQueryBuilder(): QueryBuilder
{
return $this->queryBuilder;
}
public function getUser(): User
{
return $this->user;
}
public function getBegin(): \DateTimeInterface
{
return $this->begin;
}
public function getEnd(): \DateTimeInterface
{
return $this->end;
}
}

View File

@@ -0,0 +1,38 @@
<?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\DataTransformer;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\DataTransformerInterface;
final class EntityByIdTransformer implements DataTransformerInterface // @phpstan-ignore missingType.generics
{
public function __construct(private readonly EntityRepository $repository) // @phpstan-ignore missingType.generics
{
}
public function transform(mixed $value): mixed
{
if (is_numeric($value)) {
return $this->repository->find($value);
}
return $value;
}
public function reverseTransform(mixed $value): mixed
{
if (\is_object($value) && method_exists($value, 'getId') && $value->getId() !== null) {
return (string) $value->getId();
}
return $value;
}
}

View File

@@ -0,0 +1,32 @@
<?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 App\Form\DataTransformer\EntityByIdTransformer;
use App\Repository\ActivityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
final class ActivityByIdType extends AbstractType
{
public function __construct(private readonly ActivityRepository $repository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer(new EntityByIdTransformer($this->repository));
}
public function getParent(): string
{
return ActivityType::class;
}
}

View File

@@ -0,0 +1,32 @@
<?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 App\Form\DataTransformer\EntityByIdTransformer;
use App\Repository\CustomerRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
final class CustomerByIdType extends AbstractType
{
public function __construct(private readonly CustomerRepository $repository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer(new EntityByIdTransformer($this->repository));
}
public function getParent(): string
{
return CustomerType::class;
}
}

View File

@@ -0,0 +1,32 @@
<?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 App\Form\DataTransformer\EntityByIdTransformer;
use App\Repository\ProjectRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
final class ProjectByIdType extends AbstractType
{
public function __construct(private readonly ProjectRepository $repository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer(new EntityByIdTransformer($this->repository));
}
public function getParent(): string
{
return ProjectType::class;
}
}

View File

@@ -0,0 +1,32 @@
<?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 App\Form\DataTransformer\EntityByIdTransformer;
use App\Repository\TeamRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
final class TeamByIdType extends AbstractType
{
public function __construct(private readonly TeamRepository $repository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer(new EntityByIdTransformer($this->repository));
}
public function getParent(): string
{
return TeamType::class;
}
}

View File

@@ -0,0 +1,32 @@
<?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 App\Form\DataTransformer\EntityByIdTransformer;
use App\Repository\UserRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
final class UserByIdType extends AbstractType
{
public function __construct(private readonly UserRepository $repository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer(new EntityByIdTransformer($this->repository));
}
public function getParent(): string
{
return UserType::class;
}
}

View File

@@ -10,6 +10,7 @@
namespace App\WorkingTime\Mode;
use App\Entity\User;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
final class WorkingTimeModeFactory
@@ -19,7 +20,8 @@ final class WorkingTimeModeFactory
*/
public function __construct(
#[TaggedIterator(WorkingTimeMode::class)]
private readonly iterable $modes
private readonly iterable $modes,
private readonly LoggerInterface $logger
)
{
}
@@ -39,7 +41,15 @@ final class WorkingTimeModeFactory
public function getModeForUser(User $user): WorkingTimeMode
{
return $this->getMode($user->getWorkContractMode());
try {
return $this->getMode($user->getWorkContractMode());
} catch (\InvalidArgumentException $ex) {
$this->logger->error(
\sprintf('Unknown mode "%s" requested for user %s', $user->getWorkContractMode(), $user->getId())
);
return new WorkingTimeModeNone(); // @CloudRequired
}
}
public function getMode(string $contractMode): WorkingTimeMode

View File

@@ -12,6 +12,7 @@ namespace App\WorkingTime;
use App\Entity\User;
use App\Entity\WorkingTime;
use App\Event\WorkingTimeApproveMonthEvent;
use App\Event\WorkingTimeQueryStatsEvent;
use App\Event\WorkingTimeYearEvent;
use App\Event\WorkingTimeYearSummaryEvent;
use App\Repository\TimesheetRepository;
@@ -235,6 +236,9 @@ final class WorkingTimeService
->addGroupBy('day')
;
$event = new WorkingTimeQueryStatsEvent($qb, $user, $begin, $end);
$this->eventDispatcher->dispatch($event);
$results = $qb->getQuery()->getResult();
$durations = [];