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:
@@ -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));
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)]
|
||||
|
||||
46
src/Event/WorkingTimeQueryStatsEvent.php
Normal file
46
src/Event/WorkingTimeQueryStatsEvent.php
Normal 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;
|
||||
}
|
||||
}
|
||||
38
src/Form/DataTransformer/EntityByIdTransformer.php
Normal file
38
src/Form/DataTransformer/EntityByIdTransformer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
32
src/Form/Type/ActivityByIdType.php
Normal file
32
src/Form/Type/ActivityByIdType.php
Normal 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;
|
||||
}
|
||||
}
|
||||
32
src/Form/Type/CustomerByIdType.php
Normal file
32
src/Form/Type/CustomerByIdType.php
Normal 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;
|
||||
}
|
||||
}
|
||||
32
src/Form/Type/ProjectByIdType.php
Normal file
32
src/Form/Type/ProjectByIdType.php
Normal 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;
|
||||
}
|
||||
}
|
||||
32
src/Form/Type/TeamByIdType.php
Normal file
32
src/Form/Type/TeamByIdType.php
Normal 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;
|
||||
}
|
||||
}
|
||||
32
src/Form/Type/UserByIdType.php
Normal file
32
src/Form/Type/UserByIdType.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user