0
0
Fork 0
mirror of https://github.com/kevinpapst/kimai2.git synced 2025-03-16 22:13:30 +00:00

Release 2.27 ()

This commit is contained in:
Kevin Papst 2024-12-22 22:50:42 +01:00 committed by GitHub
parent 4fdfb6f478
commit 4332ef95a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 354 additions and 256 deletions

View file

@ -3032,11 +3032,6 @@ parameters:
count: 1
path: src/Invoice/Hydrator/InvoiceModelActivityHydrator.php
-
message: "#^Cannot call method getEnd\\(\\) on App\\\\Repository\\\\Query\\\\InvoiceQuery\\|null\\.$#"
count: 1
path: src/Invoice/Hydrator/InvoiceModelCustomerHydrator.php
-
message: "#^Method App\\\\Invoice\\\\Hydrator\\\\InvoiceModelCustomerHydrator\\:\\:getBudgetValues\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1

View file

@ -11,13 +11,13 @@ namespace App\Configuration;
final class MailConfiguration
{
public function __construct(private string $mailFrom)
public function __construct(private readonly string $mailFrom)
{
}
public function getFromAddress(): ?string
{
if (empty($this->mailFrom)) {
if (trim($this->mailFrom) === '') {
return null;
}

View file

@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '2.26.0';
public const VERSION = '2.27.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 22600;
public const VERSION_ID = 22700;
/**
* The software name
*/

View file

@ -16,7 +16,6 @@ use App\Model\CustomerBudgetStatisticModel;
use App\Model\CustomerStatistic;
use App\Repository\TimesheetRepository;
use App\Timesheet\DateTimeFactory;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\DBAL\Types\Types;
@ -36,7 +35,7 @@ class CustomerStatisticService
/**
* WARNING: this method does not respect the budget type. Your results will always be with the "full lifetime data" or the "selected date-range".
*/
public function getCustomerStatistics(Customer $customer, ?DateTime $begin = null, ?DateTime $end = null): CustomerStatistic
public function getCustomerStatistics(Customer $customer, ?DateTimeInterface $begin = null, ?DateTimeInterface $end = null): CustomerStatistic
{
$statistics = $this->getBudgetStatistic([$customer], $begin, $end);
$event = new CustomerStatisticEvent($customer, array_pop($statistics), $begin, $end);
@ -51,7 +50,7 @@ class CustomerStatisticService
$stats->setStatisticTotal($this->getCustomerStatistics($customer));
$begin = null;
$end = DateTime::createFromInterface($today);
$end = DateTimeImmutable::createFromInterface($today);
if ($customer->isMonthlyBudget()) {
$dateFactory = new DateTimeFactory($today->getTimezone());

View file

@ -14,7 +14,7 @@ use App\Model\CustomerStatistic;
final class CustomerStatisticEvent extends AbstractCustomerEvent
{
public function __construct(Customer $customer, private CustomerStatistic $statistic, private ?\DateTime $begin = null, private ?\DateTime $end = null)
public function __construct(Customer $customer, private readonly CustomerStatistic $statistic, private readonly ?\DateTimeInterface $begin = null, private readonly ?\DateTimeInterface $end = null)
{
parent::__construct($customer);
}
@ -24,12 +24,12 @@ final class CustomerStatisticEvent extends AbstractCustomerEvent
return $this->statistic;
}
public function getBegin(): ?\DateTime
public function getBegin(): ?\DateTimeInterface
{
return $this->begin;
}
public function getEnd(): ?\DateTime
public function getEnd(): ?\DateTimeInterface
{
return $this->end;
}

View file

@ -15,10 +15,14 @@ use Symfony\Contracts\EventDispatcher\Event;
/**
* Working time for every day of the given year.
* Will be reflected in the working-time summary row.
*
* Only to be used with already approved entries.
*
* Can be locked before, but also can be locked by the system.
*/
final class WorkingTimeYearEvent extends Event
{
public function __construct(private Year $year, private \DateTimeInterface $until)
public function __construct(private readonly Year $year, private readonly \DateTimeInterface $until)
{
}

View file

@ -266,7 +266,7 @@ trait ToolbarFormTrait
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($name, $multiProject, $activityOptions) {
function (FormEvent $event) use ($name, $multiProject, $activityOptions, $options) {
/** @var array<string, mixed> $data */
$data = $event->getData();
$event->getForm()->add($name, ActivityType::class, array_merge($activityOptions, [
@ -299,7 +299,7 @@ trait ToolbarFormTrait
return $repo->getQueryBuilderForFormType($query);
},
]));
], $options));
}
);
}

View file

@ -18,7 +18,7 @@ final class InvoiceModelActivityHydrator implements InvoiceModelHydrator
{
use BudgetHydratorTrait;
public function __construct(private ActivityStatisticService $activityStatistic)
public function __construct(private readonly ActivityStatisticService $activityStatistic)
{
}
@ -70,8 +70,9 @@ final class InvoiceModelActivityHydrator implements InvoiceModelHydrator
$prefix . 'invoice_text' => $activity->getInvoiceText() ?? '',
];
if ($model->getQuery()?->getEnd() !== null) {
$statistic = $this->activityStatistic->getBudgetStatisticModel($activity, $model->getQuery()->getEnd());
$end = $model->getQuery()?->getEnd();
if ($end !== null) {
$statistic = $this->activityStatistic->getBudgetStatisticModel($activity, $end);
$values = array_merge($values, $this->getBudgetValues($prefix, $statistic, $model));
}

View file

@ -17,7 +17,7 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
{
use BudgetHydratorTrait;
public function __construct(private CustomerStatisticService $customerStatisticService)
public function __construct(private readonly CustomerStatisticService $customerStatisticService)
{
}
@ -29,34 +29,37 @@ final class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
return [];
}
$prefix = 'customer.';
$values = [
'customer.id' => $customer->getId(),
'customer.address' => $customer->getAddress() ?? '',
'customer.name' => $customer->getName() ?? '',
'customer.contact' => $customer->getContact() ?? '',
'customer.company' => $customer->getCompany() ?? '',
'customer.vat' => $customer->getVatId() ?? '', // deprecated since 2.0.15
'customer.vat_id' => $customer->getVatId() ?? '',
'customer.number' => $customer->getNumber() ?? '',
'customer.country' => $customer->getCountry(),
'customer.homepage' => $customer->getHomepage() ?? '',
'customer.comment' => $customer->getComment() ?? '',
'customer.email' => $customer->getEmail() ?? '',
'customer.fax' => $customer->getFax() ?? '',
'customer.phone' => $customer->getPhone() ?? '',
'customer.mobile' => $customer->getMobile() ?? '',
'customer.invoice_text' => $customer->getInvoiceText() ?? '',
$prefix . 'id' => $customer->getId(),
$prefix . 'address' => $customer->getAddress() ?? '',
$prefix . 'name' => $customer->getName() ?? '',
$prefix . 'contact' => $customer->getContact() ?? '',
$prefix . 'company' => $customer->getCompany() ?? '',
$prefix . 'vat' => $customer->getVatId() ?? '', // deprecated since 2.0.15
$prefix . 'vat_id' => $customer->getVatId() ?? '',
$prefix . 'number' => $customer->getNumber() ?? '',
$prefix . 'country' => $customer->getCountry(),
$prefix . 'homepage' => $customer->getHomepage() ?? '',
$prefix . 'comment' => $customer->getComment() ?? '',
$prefix . 'email' => $customer->getEmail() ?? '',
$prefix . 'fax' => $customer->getFax() ?? '',
$prefix . 'phone' => $customer->getPhone() ?? '',
$prefix . 'mobile' => $customer->getMobile() ?? '',
$prefix . 'invoice_text' => $customer->getInvoiceText() ?? '',
];
/** @var \DateTime $end */
$end = $model->getQuery()->getEnd();
$statistic = $this->customerStatisticService->getBudgetStatisticModel($customer, $end);
$end = $model->getQuery()?->getEnd();
if ($end !== null) {
$statistic = $this->customerStatisticService->getBudgetStatisticModel($customer, $end);
$values = array_merge($values, $this->getBudgetValues('customer.', $statistic, $model));
$values = array_merge($values, $this->getBudgetValues($prefix, $statistic, $model));
}
foreach ($customer->getMetaFields() as $metaField) {
$values = array_merge($values, [
'customer.meta.' . $metaField->getName() => $metaField->getValue(),
$prefix . 'meta.' . $metaField->getName() => $metaField->getValue(),
]);
}

View file

@ -18,7 +18,7 @@ final class InvoiceModelProjectHydrator implements InvoiceModelHydrator
{
use BudgetHydratorTrait;
public function __construct(private ProjectStatisticService $projectStatistic)
public function __construct(private readonly ProjectStatisticService $projectStatistic)
{
}
@ -83,8 +83,9 @@ final class InvoiceModelProjectHydrator implements InvoiceModelHydrator
$prefix . 'invoice_text' => $project->getInvoiceText() ?? '',
];
if ($model->getQuery()?->getEnd() !== null) {
$statistic = $this->projectStatistic->getBudgetStatisticModel($project, $model->getQuery()->getEnd());
$end = $model->getQuery()?->getEnd();
if ($end !== null) {
$statistic = $this->projectStatistic->getBudgetStatisticModel($project, $end);
$values = array_merge($values, $this->getBudgetValues($prefix, $statistic, $model));
}

View file

@ -12,17 +12,19 @@ namespace App\Invoice\Renderer;
use App\Invoice\InvoiceModel;
use App\Invoice\RendererInterface;
use App\Model\InvoiceDocument;
use App\Twig\TwigRendererTrait;
use App\Twig\LocaleFormatExtensions;
use App\Twig\SecurityPolicy\InvoicePolicy;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Twig\Environment;
use Twig\Extension\SandboxExtension;
/**
* @internal
*/
abstract class AbstractTwigRenderer implements RendererInterface
{
use TwigRendererTrait;
public function __construct(private Environment $twig)
public function __construct(private readonly Environment $twig)
{
}
@ -45,6 +47,65 @@ abstract class AbstractTwigRenderer implements RendererInterface
'entries' => $entries
], $options);
// cloning twig, because we don't want to change the
return $this->renderTwigTemplateWithLanguage($this->twig, $template, $options, $language, $formatLocale);
}
private function renderTwigTemplateWithLanguage(Environment $twig, string $template, array $options = [], ?string $language = null, ?string $formatLocale = null): string
{
$previousTranslation = null;
$previousFormatLocale = null;
if ($language !== null) {
$previousTranslation = $this->switchTranslationLocale($twig, $language);
}
if ($formatLocale !== null) {
$previousFormatLocale = $this->switchFormatLocale($twig, $formatLocale);
}
if (!$twig->hasExtension(SandboxExtension::class)) {
$twig->addExtension(new SandboxExtension(new InvoicePolicy()));
}
$sandbox = $twig->getExtension(SandboxExtension::class);
$sandbox->enableSandbox();
$content = $twig->render($template, $options);
$sandbox->disableSandbox();
if ($previousTranslation !== null) {
$this->switchTranslationLocale($twig, $previousTranslation);
}
if ($previousFormatLocale !== null) {
$this->switchFormatLocale($twig, $previousFormatLocale);
}
return $content;
}
private function switchTranslationLocale(Environment $twig, string $language): string
{
/** @var TranslationExtension $extension */
$extension = $twig->getExtension(TranslationExtension::class);
$translator = $extension->getTranslator();
if (!$translator instanceof LocaleAwareInterface) {
throw new \Exception('Translator is expected to be of type LocaleAwareInterface');
}
$previous = $translator->getLocale();
$translator->setLocale($language);
return $previous;
}
private function switchFormatLocale(Environment $twig, string $language): string
{
/** @var LocaleFormatExtensions $extension */
$extension = $twig->getExtension(LocaleFormatExtensions::class);
$previous = $extension->getLocale();
$extension->setLocale($language);
return $previous;
}
}

View file

@ -10,6 +10,7 @@
namespace App\Mail;
use App\Configuration\MailConfiguration;
use App\Constants;
use App\Entity\User;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\MailerInterface;
@ -36,7 +37,7 @@ final class KimaiMailer implements MailerInterface
if ($fallback === null) {
throw new \RuntimeException('Missing email "from" address');
}
$message->from(new Address($fallback, 'Kimai'));
$message->from(new Address($fallback, Constants::SOFTWARE));
}
$this->mailer->send($message);

View file

@ -199,7 +199,7 @@ class ProjectStatisticService
$stats->setStatisticTotal($this->getProjectStatistics($project));
$begin = null;
$end = $today;
$end = DateTimeImmutable::createFromInterface($today);
if ($project->isMonthlyBudget()) {
$dateFactory = new DateTimeFactory($today->getTimezone());

View file

@ -114,7 +114,7 @@ class TagRepository extends EntityRepository
$qb
->resetDQLPart('select')
->resetDQLPart('orderBy')
->select($qb->expr()->count('tag.id'))
->select($qb->expr()->count('tag'))
;
/** @var int<0, max> $counter */
$counter = (int) $qb->getQuery()->getSingleScalarResult();

View file

@ -459,7 +459,7 @@ class TimesheetRepository extends EntityRepository
$qb
->resetDQLPart('select')
->resetDQLPart('orderBy')
->select($qb->expr()->count('t.id'))
->select($qb->expr()->count('t'))
;
return (int) $qb->getQuery()->getSingleScalarResult(); // @phpstan-ignore-line
@ -868,7 +868,7 @@ class TimesheetRepository extends EntityRepository
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select($qb->expr()->count('t.id'))
->select($qb->expr()->count('t'))
->from(Timesheet::class, 't')
;

View file

@ -1,78 +0,0 @@
<?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\Twig;
use App\Twig\SecurityPolicy\InvoicePolicy;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Twig\Environment;
use Twig\Extension\SandboxExtension;
/**
* @internal
*/
trait TwigRendererTrait
{
protected function renderTwigTemplateWithLanguage(Environment $twig, string $template, array $options = [], ?string $language = null, ?string $formatLocale = null): string
{
$previousTranslation = null;
$previousFormatLocale = null;
if ($language !== null) {
$previousTranslation = $this->switchTranslationLocale($twig, $language);
}
if ($formatLocale !== null) {
$previousFormatLocale = $this->switchFormatLocale($twig, $formatLocale);
}
// enable basic security measures
if (!$twig->hasExtension(SandboxExtension::class)) {
$sandbox = new SandboxExtension(new InvoicePolicy());
$sandbox->enableSandbox();
$twig->addExtension($sandbox);
}
$content = $twig->render($template, $options);
if ($previousTranslation !== null) {
$this->switchTranslationLocale($twig, $previousTranslation);
}
if ($previousFormatLocale !== null) {
$this->switchFormatLocale($twig, $previousFormatLocale);
}
return $content;
}
protected function switchTranslationLocale(Environment $twig, string $language): string
{
/** @var TranslationExtension $extension */
$extension = $twig->getExtension(TranslationExtension::class);
$translator = $extension->getTranslator();
if (!$translator instanceof LocaleAwareInterface) {
throw new \Exception('Translator is expected to be of type LocaleAwareInterface');
}
$previous = $translator->getLocale();
$translator->setLocale($language);
return $previous;
}
protected function switchFormatLocale(Environment $twig, string $language): string
{
/** @var LocaleFormatExtensions $extension */
$extension = $twig->getExtension(LocaleFormatExtensions::class);
$previous = $extension->getLocale();
$extension->setLocale($language);
return $previous;
}
}

View file

@ -11,6 +11,7 @@ namespace App\Utils;
use App\Repository\Query\BaseQuery;
use Pagerfanta\Adapter\AdapterInterface;
use Pagerfanta\Adapter\ArrayAdapter;
use Pagerfanta\Pagerfanta;
final class Pagination extends Pagerfanta
@ -19,6 +20,10 @@ final class Pagination extends Pagerfanta
{
parent::__construct($adapter);
if ($adapter instanceof ArrayAdapter && ($size = $adapter->getNbResults()) > 0) {
$this->setMaxPerPage($size);
}
if ($query === null || !$query->isApiCall()) {
$this->setNormalizeOutOfRangePages(true);
}

View file

@ -15,9 +15,15 @@
</h3>
</div>
<div class="card-body">
{{ form_errors(form) }}
{% block form_body %}
{{ form_rest(form) }}
{% block form_body_outer %}
{% block form_body_pre %}
{{ form_errors(form) }}
{% endblock %}
{% block form_body %}
{{ form_rest(form) }}
{% endblock %}
{% block form_body_post %}
{% endblock %}
{% endblock %}
</div>
<div class="card-footer">

View file

@ -16,9 +16,15 @@
</div>
<div class="modal-body">
{% block modal_body %}
{{ form_errors(form) }}
{% block form_body %}
{{ form_rest(form) }}
{% block form_body_outer %}
{% block form_body_pre %}
{{ form_errors(form) }}
{% endblock %}
{% block form_body %}
{{ form_rest(form) }}
{% endblock %}
{% block form_body_post %}
{% endblock %}
{% endblock %}
{% endblock %}
</div>

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\Tests\Ldap;
use App\Ldap\LdapBadge;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Ldap\LdapBadge
*/
class LdapBadgeTest extends TestCase
{
public function testMarkResolvedSetsResolvedToTrue(): void
{
$badge = new LdapBadge();
$badge->markResolved();
self::assertTrue($badge->isResolved());
}
public function testIsResolvedReturnsFalseInitially(): void
{
$badge = new LdapBadge();
self::assertFalse($badge->isResolved());
}
}

View file

@ -22,16 +22,19 @@ use Symfony\Component\Mime\Email;
*/
class KimaiMailerTest extends TestCase
{
public function getSut(): KimaiMailer
public function getSut(?MailerInterface $mailer = null): KimaiMailer
{
$config = new MailConfiguration('zippel@example.com');
$mailer = $this->createMock(MailerInterface::class);
if ($mailer === null) {
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects(self::once())->method('send');
}
return new KimaiMailer($config, $mailer);
}
public function testSendSetsFrom(): void
public function testSendSetsFromHeaderFromFallback(): void
{
$user = new User();
$user->setUserIdentifier('Testing');
@ -47,4 +50,83 @@ class KimaiMailerTest extends TestCase
self::assertEquals([new Address('zippel@example.com', 'Kimai')], $message->getFrom());
}
public function testSendToUserSetsFromHeaderFromFallback(): void
{
$user = new User();
$user->setUserIdentifier('Testing');
$user->setEmail('foo@example.com');
$user->setAlias('Super User');
$user->setEnabled(true);
$mailer = $this->getSut();
$message = new Email();
$mailer->sendToUser($user, $message);
self::assertEquals([new Address('zippel@example.com', 'Kimai')], $message->getFrom());
}
public function testSendToUserSendsEmailWhenUserIsEnabledAndHasEmail(): void
{
$user = $this->createMock(User::class);
$user->method('isEnabled')->willReturn(true);
$user->method('getEmail')->willReturn('foo-bar@example.com');
$email = new Email();
self::assertEquals([], $email->getTo());
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects(self::once())->method('send')->with($email);
$sut = $this->getSut($mailer);
$sut->sendToUser($user, $email);
self::assertEquals([new Address('zippel@example.com', 'Kimai')], $email->getFrom());
self::assertEquals([new Address('foo-bar@example.com')], $email->getTo());
}
public function testSendToUserDoesNotSendEmailWhenUserIsDisabled(): void
{
$user = $this->createMock(User::class);
$user->method('isEnabled')->willReturn(false);
$user->method('getEmail')->willReturn('user@example.com');
$email = new Email();
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects(self::never())->method('send');
$sut = $this->getSut($mailer);
$sut->sendToUser($user, $email);
}
public function testSendToUserDoesNotSendEmailWhenUserHasNoEmail(): void
{
$user = $this->createMock(User::class);
$user->method('isEnabled')->willReturn(true);
$user->method('getEmail')->willReturn(null);
$email = new Email();
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects(self::never())->method('send');
$sut = $this->getSut($mailer);
$sut->sendToUser($user, $email);
}
public function testSThrowsOnEmptyFromAddress(): void
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Missing email "from" address');
$user = $this->createMock(User::class);
$user->method('isEnabled')->willReturn(true);
$user->method('getEmail')->willReturn('test@example.com');
$email = new Email();
$config = new MailConfiguration('');
$mailer = $this->createMock(MailerInterface::class);
$sut = new KimaiMailer($config, $mailer);
$sut->sendToUser($user, $email);
}
}

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\Tests\Plugin;
use App\Plugin\Package;
use App\Plugin\PluginMetadata;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Plugin\Package
*/
class PackageTest extends TestCase
{
public function testGetPackageFileReturnsCorrectFile(): void
{
$fileInfo = new \SplFileInfo('path/to/package.zip');
$metadata = $this->createMock(PluginMetadata::class);
$package = new Package($fileInfo, $metadata);
self::assertSame($fileInfo, $package->getPackageFile());
}
public function testGetMetadataReturnsCorrectMetadata(): void
{
$fileInfo = new \SplFileInfo('path/to/package.zip');
$metadata = $this->createMock(PluginMetadata::class);
$package = new Package($fileInfo, $metadata);
self::assertSame($metadata, $package->getMetadata());
}
}

View file

@ -65,6 +65,7 @@ class PaginationExtensionTest extends TestCase
$values = array_fill(0, 151, 'blub');
$pagerfanta = new Pagination(new ArrayAdapter($values));
$pagerfanta->setMaxPerPage(10);
$result = $sut->renderPagination($pagerfanta, [
'css_container_class' => 'pagination pagination-sm inline',
'routeName' => 'project_activities',
@ -98,6 +99,7 @@ class PaginationExtensionTest extends TestCase
$values = array_fill(0, 151, 'blub');
$pagerfanta = new Pagination(new ArrayAdapter($values));
$pagerfanta->setMaxPerPage(10);
$result = $sut->renderPagination($pagerfanta, [
'css_container_class' => 'pagination pagination-sm inline',
'routeName' => 'project_activities',
@ -106,6 +108,26 @@ class PaginationExtensionTest extends TestCase
$this->assertPaginationHtml($result);
}
public function testRenderPaginationWithoutPageSize(): void
{
$sut = $this->getSut();
$values = array_fill(0, 151, 'blub');
$pagerfanta = new Pagination(new ArrayAdapter($values));
$result = $sut->renderPagination($pagerfanta, [
'css_container_class' => 'pagination pagination-sm inline',
'routeName' => 'project_activities',
'routeParams' => ['id' => 137]
]);
$expected =
'<ul class="pagination pagination-sm inline"><li class="page-item disabled"><span class="page-link pagination-link"><i class="fas fa-chevron-left"></i></span></li>' .
'<li class="page-item active"><a class="page-link pagination-link" href="project_activities?id=137&page=1">1</a></li>' .
'<li class="page-item disabled"><span class="page-link pagination-link"><i class="fas fa-chevron-right"></i></span></li></ul>';
self::assertEquals($expected, $result);
}
public function testRenderPaginationWithoutRouteName(): void
{
$this->expectException(\Exception::class);

View file

@ -72,7 +72,7 @@ Bitte überprüfen Sie diese unter: %url%</target>
</trans-unit>
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
<source>approval_rejected_message</source>
<target state="translated">Ihre Genehmigungsanfrage wurde von %user% abgelehnt.
<target state="translated">Ihre Genehmigungsanfrage wurde von %created_by% abgelehnt.
%list%
@ -84,7 +84,7 @@ Bitte überprüfen Sie ihre Anfragen unter: %url%</target>
</trans-unit>
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
<source>approval_approved_message</source>
<target state="translated">Ihre Anfrage wurde von %user% genehmigt.
<target state="translated">Ihre Anfrage wurde von %created_by% genehmigt.
%list%

View file

@ -72,7 +72,7 @@ Please review them at: %url%</target>
</trans-unit>
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
<source>approval_rejected_message</source>
<target state="translated">Your authorisation request has been rejected by %user%.
<target state="translated">Your authorisation request has been rejected by %created_by%.
%list%
@ -84,7 +84,7 @@ Please review your requests at: %url%</target>
</trans-unit>
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
<source>approval_approved_message</source>
<target state="translated">Your request has been approved by %user%.
<target state="translated">Your request has been approved by %created_by%.
%list%

View file

@ -64,7 +64,7 @@ Por favor revísalos en: %url%</target>
</trans-unit>
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve" approved="no">
<source>approval_rejected_message</source>
<target state="translated">Su solicitud de autorización ha sido rechazada por %user%.
<target state="translated">Su solicitud de autorización ha sido rechazada por %created_by%.
%list%
@ -80,7 +80,7 @@ Por favor revise sus solicitudes en: %url%</target>
</trans-unit>
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve" approved="no">
<source>approval_approved_message</source>
<target state="translated">Su solicitud ha sido aprobada por %user%.
<target state="translated">Su solicitud ha sido aprobada por %created_by%.
%list%

View file

@ -68,7 +68,7 @@ Please review them at: %url%</source>
</trans-unit>
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve" approved="yes">
<source>approval_rejected_message</source>
<target state="final">בקשת האימות נדחתה על ידי המשתמש %user%.
<target state="final">בקשת האימות נדחתה על ידי המשתמש %created_by%.
%list%
@ -84,7 +84,7 @@ Please review them at: %url%</source>
</trans-unit>
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve" approved="yes">
<source>approval_approved_message</source>
<target state="final">הבקשה שלך אושרה על ידי %user%.
<target state="final">הבקשה שלך אושרה על ידי %created_by%.
%list%

View file

@ -72,7 +72,7 @@ Pregledaj ih na: %url%</target>
</trans-unit>
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
<source>approval_rejected_message</source>
<target state="translated">%user% je odbio/la tvoj zahtjev za autorizaciju.
<target state="translated">%created_by% je odbio/la tvoj zahtjev za autorizaciju.
%list%
@ -84,7 +84,7 @@ Pregledaj tvoje zahtjeve na: %url%</target>
</trans-unit>
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
<source>approval_approved_message</source>
<target state="translated">%user% je odobrio/la tvoj zahtjev.
<target state="translated">%created_by% je odobrio/la tvoj zahtjev.
%list%

View file

@ -72,7 +72,7 @@ Silakan tinjau di: %url%</target>
</trans-unit>
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
<source>approval_rejected_message</source>
<target state="translated">Permintaan otorisasi Anda telah ditolak oleh %user%.
<target state="translated">Permintaan otorisasi Anda telah ditolak oleh %created_by%.
%list%
@ -84,7 +84,7 @@ Silakan tinjau permintaan Anda di: %url%</target>
</trans-unit>
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
<source>approval_approved_message</source>
<target state="translated">Permintaan Anda telah disetujui oleh %user%.
<target state="translated">Permintaan Anda telah disetujui oleh %created_by%.
%list%

View file

@ -64,7 +64,7 @@ Controllali qui: %url%</target>
</trans-unit>
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve" approved="yes">
<source>approval_approved_message</source>
<target state="final">La tua richiesta è stata approvata da %user%.
<target state="final">La tua richiesta è stata approvata da %created_by%.
%list%
@ -72,7 +72,7 @@ Puoi trovare tutte le tue richieste qui: %url%</target>
</trans-unit>
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve" approved="yes">
<source>approval_rejected_message</source>
<target state="final">La tua richiesta di autorizzazione è stata rifiutata da %user%.
<target state="final">La tua richiesta di autorizzazione è stata rifiutata da %created_by%.
%list%

View file

@ -72,7 +72,7 @@ Favor revisá-los em: %url%</target>
</trans-unit>
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve" approved="no">
<source>approval_approved_message</source>
<target state="translated">Sua solicitação foi aprovada pelo %user%.
<target state="translated">Sua solicitação foi aprovada pelo %created_by%.
%list%
@ -84,7 +84,7 @@ Você pode encontrar todas as suas solicitações em: %url%</target>
</trans-unit>
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve" approved="no">
<source>approval_rejected_message</source>
<target state="translated">Sua solicitação de autorização foi recusada pelo %user%.
<target state="translated">Sua solicitação de autorização foi recusada pelo %created_by%.
%list%

View file

@ -72,7 +72,7 @@ Please review them at: %url%</source>
</trans-unit>
<trans-unit id="U.JjI9R" resname="approval_rejected_message" xml:space="preserve">
<source>approval_rejected_message</source>
<target state="translated">Ваш запит на авторизацію %user% скасовує.
<target state="translated">Ваш запит на авторизацію %created_by% скасовує.
%list%
@ -84,7 +84,7 @@ Please review them at: %url%</source>
</trans-unit>
<trans-unit id="4nn7Vi1" resname="approval_approved_message" xml:space="preserve">
<source>approval_approved_message</source>
<target state="translated">%user% схвалює Ваш запит.
<target state="translated">%created_by% схвалює Ваш запит.
%list%

View file

@ -1462,10 +1462,6 @@
<source>complete_month.help</source>
<target state="translated">Toto uzamkne všechny dny roku předcházející vybrané datum. Uživatel dál nebude moci vytvářet ani upravovat časy pro uzamčené období.</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Odpracované hodiny</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source>
<target state="translated">Pro tohoto uživatele nebyly v nastavení pracovní smlouvy nakonfigurovány žádné cílové hodiny.</target>

View file

@ -1026,10 +1026,6 @@
<source>Review</source>
<target state="translated">Gennemse</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
<source>work_times_result</source>
<target state="translated">Timer arbejdet</target>
</trans-unit>
<trans-unit id="0ZWvn12" resname="notifications.welcome" xml:space="preserve">
<source>notifications.welcome</source>
<target state="translated">Tak for at du har tilmedt dig notifikationer!</target>

View file

@ -650,10 +650,6 @@
<source>work_times_is</source>
<target>Geleistete Stunden</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target>Geleistete Stunden</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
<source>confirmed_by_at</source>
<target>Bestätigt von %user% am %date%</target>
@ -1938,6 +1934,14 @@
<source>Log out of all devices</source>
<target>Von allen Geräten abmelden</target>
</trans-unit>
<trans-unit id="9qIU96X" resname="result">
<source>result</source>
<target>Ergebnis</target>
</trans-unit>
<trans-unit id="wvt4jH3" resname="training">
<source>training</source>
<target>Fortbildung</target>
</trans-unit>
</body>
</file>
</xliff>

View file

@ -650,10 +650,6 @@
<source>work_times_is</source>
<target>Hours worked</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target>Hours worked</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
<source>confirmed_by_at</source>
<target>Confirmed by %user% at %date%</target>
@ -1938,6 +1934,14 @@
<source>Log out of all devices</source>
<target>Log out of all devices</target>
</trans-unit>
<trans-unit id="9qIU96X" resname="result">
<source>result</source>
<target>Result</target>
</trans-unit>
<trans-unit id="wvt4jH3" resname="training">
<source>training</source>
<target>Training</target>
</trans-unit>
</body>
</file>
</xliff>

View file

@ -1534,10 +1534,6 @@
<source>receive_from</source>
<target state="translated">Recibir de %name%</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Horas trabajadas</target>
</trans-unit>
<trans-unit id="yJg6P3m" resname="holiday.intro" xml:space="preserve">
<source>You have submitted %used% of your available %days% vacation days so far.</source>
<target state="translated">Has utilizado %used% de tus %days% días disponibles de vacaciones.</target>

View file

@ -1406,10 +1406,6 @@
<source>favorite_routes</source>
<target state="translated">Suosikit</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Työskennellyt tunnit</target>
</trans-unit>
<trans-unit id="yJg6P3m" resname="holiday.intro" xml:space="preserve">
<source>You have submitted %used% of your available %days% vacation days so far.</source>
<target state="needs-translation">Olet käyttänyt %used% vapaata saatavilla olevista %days% päivästä.</target>

View file

@ -1498,10 +1498,6 @@
<source>manual_bookings.work_contract_intro</source>
<target state="translated">Les réservations manuelles ne peuvent être ni modifiées ni supprimées!</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Heures travaillées</target>
</trans-unit>
<trans-unit id="t1r372x" resname="invisible">
<source>invisible</source>
<target state="translated">Invisible</target>

View file

@ -1446,10 +1446,6 @@
<source>work_times_is</source>
<target state="final">שעות עבודה בפועל</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve" approved="yes">
<source>work_times_result</source>
<target state="final">שעות עבודה</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
<source>confirmed_by_at</source>
<target state="translated">אושר על ידי %user% ב־%date%</target>

View file

@ -1442,10 +1442,6 @@
<source>manual_bookings</source>
<target state="translated">Ručne rezervacije</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
<source>work_times_result</source>
<target state="translated">Odrađeni sati</target>
</trans-unit>
<trans-unit id="31dFXhR" resname="work_times_is" xml:space="preserve">
<source>work_times_is</source>
<target state="translated">Odrađeni sati</target>

View file

@ -650,10 +650,6 @@
<source>work_times_should.none_configured</source>
<target state="translated">Tidak ada target jam yang ditetapkan pada pengguna ini dalam setelan kontrak pekerjaan.</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
<source>work_times_result</source>
<target state="translated">Jam bekerja</target>
</trans-unit>
<trans-unit id="1c0EQaz" resname="profile.registration_date" xml:space="preserve">
<source>profile.registration_date</source>
<target state="translated">Teregistrasi di</target>

View file

@ -1482,10 +1482,6 @@
<source>confirmed_by_at</source>
<target state="final">Confermato da %user% il %date%</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result" approved="yes">
<source>work_times_result</source>
<target state="final">Ore lavorate</target>
</trans-unit>
<trans-unit id="wZRRqg0" resname="manual_bookings" approved="yes">
<source>manual_bookings</source>
<target state="final">Prenotazioni manuali</target>

View file

@ -1430,10 +1430,6 @@
<source>work_times_is</source>
<target state="translated">Gewerkte uren</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Gewerkte uren</target>
</trans-unit>
<trans-unit id="wZRRqg0" resname="manual_bookings">
<source>manual_bookings</source>
<target state="translated">Handmatige boekingen</target>

View file

@ -1574,10 +1574,6 @@
<source>work_times_is</source>
<target state="translated">Przepracowane godziny</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
<source>work_times_result</source>
<target state="translated">Przepracowane godziny</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at" xml:space="preserve">
<source>confirmed_by_at</source>
<target state="translated">Potwierdzone przez %user% dnia %date%</target>

View file

@ -1630,10 +1630,6 @@
<source>work_times_is</source>
<target state="translated">Horas realizadas</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Horas trabalhadas</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
<source>confirmed_by_at</source>
<target state="translated">Confirmado por %user% em %date%</target>

View file

@ -1474,10 +1474,6 @@
<source>completed_month_pdf</source>
<target state="translated">PDF do mês concluído</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Horas trabalhadas</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source>
<target state="translated">Nenhuma hora-alvo foi configurada para esse usuário nas configurações do contrato de trabalho.</target>

View file

@ -1406,10 +1406,6 @@
<source>work_times_is</source>
<target state="needs-translation">Timpul de lucru este</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
<source>work_times_result</source>
<target state="translated">Ore lucrate</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at" xml:space="preserve">
<source>confirmed_by_at</source>
<target state="translated">Confirmat de %user% la %date%</target>

View file

@ -1354,10 +1354,6 @@
<source>Expected number of hours</source>
<target state="translated">Предполагаемые часы</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Отработанные часы</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
<source>confirmed_by_at</source>
<target state="needs-translation">Подтверждено %user% в %date%</target>

View file

@ -1170,10 +1170,6 @@
<source>export.warn_result_amount</source>
<target state="translated">Vaše vyhľadávanie vedie k %count% výsledkom. Pokiaľ sa export nepodarí, musíte zúžiť vaše vyhľadávanie.</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Odpracované hodiny</target>
</trans-unit>
<trans-unit id="s9NPI3v" resname="work_times_should.none_configured">
<source>work_times_should.none_configured</source>
<target state="translated">Pro tohoto uživateľa neboli v nastaveniach pracovnej zmluvy nakonfigurované žiadne cielové hodiny.</target>

View file

@ -1434,10 +1434,6 @@
<source>confirmed_by_at</source>
<target state="translated">Bekräftad %date% av %user%</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Timmar arbetade</target>
</trans-unit>
<trans-unit id="opDmYAa" resname="status.approved">
<source>status.approved</source>
<target state="translated">Godkänd</target>

View file

@ -1470,10 +1470,6 @@
<source>work_times_should.none_configured</source>
<target state="translated">İş sözleşmesi ayarlarında bu kullanıcı için herhangi bir hedef saat yapılandırılmamış.</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Çalışılan saatler</target>
</trans-unit>
<trans-unit id="znIJiOw" resname="manual_bookings.duration_help">
<source>manual_bookings.duration_help</source>
<target state="translated">Süre hem pozitif (is-time artması) hem de negatif (is-time azalması) olabilir.</target>

View file

@ -1438,10 +1438,6 @@
<source>Expected number of hours</source>
<target state="translated">Очікувані години</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">Відпрацьовані години</target>
</trans-unit>
<trans-unit id="wZRRqg0" resname="manual_bookings">
<source>manual_bookings</source>
<target state="translated">Ручне бронювання</target>

View file

@ -1310,10 +1310,6 @@
<source>evaluation</source>
<target state="translated">Đánh giá</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve">
<source>work_times_result</source>
<target state="translated">Số giờ làm việc</target>
</trans-unit>
<trans-unit id="31dFXhR" resname="work_times_is" xml:space="preserve">
<source>work_times_is</source>
<target state="needs-translation">Giờ làm việc thực tế</target>

View file

@ -1478,10 +1478,6 @@
<source>manual_bookings.duration_help</source>
<target state="translated">持续时间既可以是正的(增加的是时间)也可以是负的(减少的是时间)。</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result">
<source>work_times_result</source>
<target state="translated">工作时间</target>
</trans-unit>
<trans-unit id="8yqaKzX" resname="confirmed_by_at">
<source>confirmed_by_at</source>
<target state="translated">由 %user% 在 %date% 确认</target>

View file

@ -1410,10 +1410,6 @@
<source>unit_price</source>
<target state="final">單價</target>
</trans-unit>
<trans-unit id="uHrZlbq" resname="work_times_result" xml:space="preserve" approved="yes">
<source>work_times_result</source>
<target state="final">工作時數</target>
</trans-unit>
<trans-unit id="XE01Ar1" resname="parental" xml:space="preserve" approved="yes">
<source>parental</source>
<target state="final">育嬰假</target>