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:
@@ -24,7 +24,8 @@
|
|||||||
"ext-xml": "*",
|
"ext-xml": "*",
|
||||||
"ext-xsl": "*",
|
"ext-xsl": "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"composer/package-versions-deprecated": "^1.8",
|
"composer-runtime-api": "^2.0",
|
||||||
|
"azuyalabs/yasumi": "^2.6",
|
||||||
"composer/semver": "^3.3",
|
"composer/semver": "^3.3",
|
||||||
"doctrine/doctrine-bundle": "^2.7",
|
"doctrine/doctrine-bundle": "^2.7",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
"friendsofsymfony/rest-bundle": "^3.0",
|
"friendsofsymfony/rest-bundle": "^3.0",
|
||||||
"gedmo/doctrine-extensions": "^3.6",
|
"gedmo/doctrine-extensions": "^3.6",
|
||||||
"jms/serializer-bundle": "^5.0",
|
"jms/serializer-bundle": "^5.0",
|
||||||
"kevinpapst/tabler-bundle": "^0.19",
|
"kevinpapst/tabler-bundle": "^0.20",
|
||||||
"league/csv": "^9.4",
|
"league/csv": "^9.4",
|
||||||
"mpdf/mpdf": "^8.0",
|
"mpdf/mpdf": "^8.0",
|
||||||
"nelmio/api-doc-bundle": "^4.0",
|
"nelmio/api-doc-bundle": "^4.0",
|
||||||
@@ -104,7 +105,6 @@
|
|||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
"composer/package-versions-deprecated": true,
|
|
||||||
"symfony/flex": true,
|
"symfony/flex": true,
|
||||||
"symfony/runtime": true
|
"symfony/runtime": true
|
||||||
},
|
},
|
||||||
|
|||||||
678
composer.lock
generated
678
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -78,6 +78,7 @@ tabler:
|
|||||||
filter: fas fa-filter
|
filter: fas fa-filter
|
||||||
help: far fa-question-circle
|
help: far fa-question-circle
|
||||||
home: fas fa-home
|
home: fas fa-home
|
||||||
|
import: fas fa-file-import
|
||||||
invoice: fas fa-file-contract
|
invoice: fas fa-file-contract
|
||||||
invoice-template: fas fa-file-signature
|
invoice-template: fas fa-file-signature
|
||||||
left: fas fa-chevron-left
|
left: fas fa-chevron-left
|
||||||
|
|||||||
@@ -41,6 +41,6 @@ final class Version20210316224358 extends AbstractMigration
|
|||||||
|
|
||||||
public function down(Schema $schema): void
|
public function down(Schema $schema): void
|
||||||
{
|
{
|
||||||
$this->addSql('DROP TABLE kimai2_bookmarks');
|
$schema->dropTable('kimai2_bookmarks');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
phpstan.neon
35
phpstan.neon
@@ -1391,11 +1391,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Controller/Security/SelfRegistrationController.php
|
path: src/Controller/Security/SelfRegistrationController.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$value of method App\\\\Form\\\\Model\\\\Configuration\\:\\:setValue\\(\\) expects bool\\|int\\|object\\|string\\|null, bool\\|float\\|int\\|string given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Controller/SystemConfigurationController.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot call method getData\\(\\) on Symfony\\\\Component\\\\Form\\\\FormInterface\\|null\\.$#"
|
message: "#^Cannot call method getData\\(\\) on Symfony\\\\Component\\\\Form\\\\FormInterface\\|null\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -3006,36 +3001,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Form/InvoiceDocumentUploadForm.php
|
path: src/Form/InvoiceDocumentUploadForm.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Form\\\\Model\\\\Configuration\\:\\:getOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Form/Model/Configuration.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Form\\\\Model\\\\Configuration\\:\\:getValue\\(\\) should return bool\\|int\\|object\\|string\\|null but returns array\\|bool\\|int\\|string\\|null\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Form/Model/Configuration.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Form\\\\Model\\\\Configuration\\:\\:setOptions\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Form/Model/Configuration.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Property App\\\\Form\\\\Model\\\\Configuration\\:\\:\\$options type has no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Form/Model/Configuration.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Property App\\\\Form\\\\Model\\\\Configuration\\:\\:\\$value \\(array\\|bool\\|int\\|string\\|null\\) does not accept bool\\|int\\|object\\|string\\|null\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Form/Model/Configuration.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Property App\\\\Form\\\\Model\\\\Configuration\\:\\:\\$value type has no value type specified in iterable type array\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Form/Model/Configuration.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Form\\\\Model\\\\MultiUserTimesheet\\:\\:getTeams\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
|
message: "#^Method App\\\\Form\\\\Model\\\\MultiUserTimesheet\\:\\:getTeams\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '2.0.24';
|
public const VERSION = '2.0.25';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* The current release: major * 10000 + minor * 100 + patch
|
||||||
*/
|
*/
|
||||||
public const VERSION_ID = 20024;
|
public const VERSION_ID = 20025;
|
||||||
/**
|
/**
|
||||||
* The software name
|
* The software name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ final class ContractController extends AbstractController
|
|||||||
$yearDate = $values->getDate();
|
$yearDate = $values->getDate();
|
||||||
$year = $workingTimeService->getYear($profile, $yearDate);
|
$year = $workingTimeService->getYear($profile, $yearDate);
|
||||||
|
|
||||||
$page = new PageSetup('status');
|
$page = new PageSetup('work_times');
|
||||||
$page->setHelp('contract.html');
|
$page->setHelp('contract.html');
|
||||||
|
$page->setActionName('contract');
|
||||||
|
$page->setPaginationForm($form);
|
||||||
|
|
||||||
// additional boxes by plugins
|
// additional boxes by plugins
|
||||||
$controllerEvent = new WorkContractDetailControllerEvent($year);
|
$controllerEvent = new WorkContractDetailControllerEvent($year);
|
||||||
|
|||||||
@@ -120,27 +120,25 @@ final class DoctorController extends AbstractController
|
|||||||
/** @var array<string, string> $versions */
|
/** @var array<string, string> $versions */
|
||||||
$versions = [];
|
$versions = [];
|
||||||
|
|
||||||
if (class_exists(InstalledVersions::class)) {
|
$rootPackage = InstalledVersions::getRootPackage()['name'];
|
||||||
$rootPackage = InstalledVersions::getRootPackage()['name'];
|
foreach (InstalledVersions::getInstalledPackages() as $package) {
|
||||||
foreach (InstalledVersions::getInstalledPackages() as $package) {
|
$versions[$package] = InstalledVersions::getPrettyVersion($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
|
if ($version === null || $version === '*') {
|
||||||
$versions = array_filter($versions, function ($version, $name) use ($rootPackage): bool {
|
return false;
|
||||||
if ($name === $rootPackage) {
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($version === null || $version === '*') {
|
return true;
|
||||||
return false;
|
}, ARRAY_FILTER_USE_BOTH);
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
ksort($versions);
|
||||||
}, ARRAY_FILTER_USE_BOTH);
|
|
||||||
|
|
||||||
ksort($versions);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $versions;
|
return $versions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ final class ProfileController extends AbstractController
|
|||||||
'form' => $form->createView(),
|
'form' => $form->createView(),
|
||||||
'deactivate' => $this->getTwoFactorDeactivationForm($profile)->createView(),
|
'deactivate' => $this->getTwoFactorDeactivationForm($profile)->createView(),
|
||||||
'qr_code' => $result,
|
'qr_code' => $result,
|
||||||
|
'secret' => $profile->getTotpSecret(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use App\Event\CustomerMetaDefinitionEvent;
|
|||||||
use App\Event\CustomerUpdatePostEvent;
|
use App\Event\CustomerUpdatePostEvent;
|
||||||
use App\Event\CustomerUpdatePreEvent;
|
use App\Event\CustomerUpdatePreEvent;
|
||||||
use App\Repository\CustomerRepository;
|
use App\Repository\CustomerRepository;
|
||||||
|
use App\Repository\Query\CustomerQuery;
|
||||||
use App\Utils\NumberGenerator;
|
use App\Utils\NumberGenerator;
|
||||||
use App\Validator\ValidationFailedException;
|
use App\Validator\ValidationFailedException;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
@@ -107,6 +108,19 @@ final class CustomerService
|
|||||||
return $this->repository->findOneBy(['number' => $number]);
|
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
|
public function calculateNextCustomerNumber(): string
|
||||||
{
|
{
|
||||||
$format = $this->configuration->find('customer.number_format');
|
$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');
|
$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.
|
* If not empty two-factor authentication is enabled.
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(name: 'totp_secret', type: 'string', nullable: true)]
|
#[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])]
|
#[ORM\Column(name: 'totp_enabled', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||||
private bool $totpEnabled = false;
|
private bool $totpEnabled = false;
|
||||||
#[ORM\Column(name: 'system_account', type: 'boolean', nullable: false, options: ['default' => 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;
|
return $this->totpSecret !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getTotpSecret(): ?string
|
||||||
|
{
|
||||||
|
return $this->totpSecret;
|
||||||
|
}
|
||||||
|
|
||||||
public function isTotpAuthenticationEnabled(): bool
|
public function isTotpAuthenticationEnabled(): bool
|
||||||
{
|
{
|
||||||
return $this->totpEnabled;
|
return $this->totpEnabled;
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ final class InvoiceDocumentUploadForm extends AbstractType
|
|||||||
new File([
|
new File([
|
||||||
'mimeTypes' => $mimetypes,
|
'mimeTypes' => $mimetypes,
|
||||||
'mimeTypesMessage' => 'This file type is not allowed',
|
'mimeTypesMessage' => 'This file type is not allowed',
|
||||||
|
'maxSize' => '1024k',
|
||||||
]),
|
]),
|
||||||
new Callback([$this, 'validateDocument'])
|
new Callback([$this, 'validateDocument'])
|
||||||
],
|
],
|
||||||
@@ -117,7 +118,7 @@ final class InvoiceDocumentUploadForm extends AbstractType
|
|||||||
|
|
||||||
$safeFilename = transliterator_transliterate(self::FILENAME_RULE, $nameWithoutExtension);
|
$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%')
|
$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 _ -'])
|
->setParameters(['%character%' => 'A-Z a-z 0-9 _ -'])
|
||||||
->setTranslationDomain('validators')
|
->setTranslationDomain('validators')
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ final class Configuration
|
|||||||
{
|
{
|
||||||
private ?string $label = null;
|
private ?string $label = null;
|
||||||
private string $translationDomain = 'messages';
|
private string $translationDomain = 'messages';
|
||||||
private string|int|null|bool|array $value = null;
|
private string|int|null|bool|float $value = null;
|
||||||
private ?string $type = null;
|
private ?string $type = null;
|
||||||
|
/** @var array<string, mixed> */
|
||||||
private array $options = [];
|
private array $options = [];
|
||||||
private bool $enabled = true;
|
private bool $enabled = true;
|
||||||
private bool $required = true;
|
private bool $required = true;
|
||||||
@@ -36,12 +37,12 @@ final class Configuration
|
|||||||
return $this->name;
|
return $this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getValue(): string|int|null|bool|object
|
public function getValue(): string|int|null|bool|float
|
||||||
{
|
{
|
||||||
return $this->value;
|
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) {
|
if ($this->type === CheckboxType::class || $this->type === YesNoType::class) {
|
||||||
$value = (bool) $value;
|
$value = (bool) $value;
|
||||||
@@ -131,11 +132,17 @@ final class Configuration
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
public function getOptions(): array
|
public function getOptions(): array
|
||||||
{
|
{
|
||||||
return $this->options;
|
return $this->options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $options
|
||||||
|
*/
|
||||||
public function setOptions(array $options): Configuration
|
public function setOptions(array $options): Configuration
|
||||||
{
|
{
|
||||||
$this->options = $options;
|
$this->options = $options;
|
||||||
|
|||||||
@@ -61,18 +61,24 @@ final class DateRangeType extends AbstractType
|
|||||||
$user = $options['user'];
|
$user = $options['user'];
|
||||||
$factory = DateTimeFactory::createByUser($user);
|
$factory = DateTimeFactory::createByUser($user);
|
||||||
|
|
||||||
$view->vars['ranges'] = [
|
$ranges = [
|
||||||
'today' => [$factory->createDateTime('00:00:00'), $factory->createDateTime('23:59:59')],
|
'daterangepicker.allTime' => [null, null],
|
||||||
'yesterday' => [$factory->createDateTime('-1 day 00:00:00'), $factory->createDateTime('-1 day 23:59:59')],
|
'daterangepicker.today' => [$factory->createDateTime('00:00:00'), $factory->createDateTime('23:59:59')],
|
||||||
'thisWeek' => [$factory->getStartOfWeek(), $factory->getEndOfWeek()],
|
'daterangepicker.yesterday' => [$factory->createDateTime('-1 day 00:00:00'), $factory->createDateTime('-1 day 23:59:59')],
|
||||||
'lastWeek' => [$factory->getStartOfWeek('-1 week'), $factory->getEndOfWeek('-1 week')],
|
'daterangepicker.thisWeek' => [$factory->getStartOfWeek(), $factory->getEndOfWeek()],
|
||||||
'thisMonth' => [$factory->getStartOfMonth(), $factory->getEndOfMonth()],
|
'daterangepicker.lastWeek' => [$factory->getStartOfWeek('-1 week'), $factory->getEndOfWeek('-1 week')],
|
||||||
'lastMonth' => [$factory->getStartOfMonth('-1 month'), $factory->getEndOfMonth('-1 month')],
|
'daterangepicker.thisMonth' => [$factory->getStartOfMonth(), $factory->getEndOfMonth()],
|
||||||
'thisYear' => [$factory->createStartOfYear(), $factory->createEndOfYear()],
|
'daterangepicker.lastMonth' => [$factory->getStartOfLastMonth(), $factory->getEndOfLastMonth()],
|
||||||
'thisYearUntilNow' => [$factory->createStartOfYear(), $factory->createDateTime('23:59:59')],
|
'daterangepicker.thisYearUntilNow' => [$factory->createStartOfYear(), $factory->createDateTime('23:59:59')],
|
||||||
'lastYear' => [$factory->createStartOfYear('-1 year'), $factory->createEndOfYear('-1 year')],
|
|
||||||
'allTime' => [null, null],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$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['rangeFormat'] = $options['format'];
|
||||||
|
|
||||||
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
$view->vars['attr'] = array_merge($view->vars['attr'], [
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
|||||||
|
|
||||||
abstract class AbstractPluginExtension extends 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
|
protected function registerBundleConfiguration(ContainerBuilder $container, array $configs): void
|
||||||
{
|
{
|
||||||
$bundleConfig = [$this->getAlias() => $configs];
|
$bundleConfig = [$this->getAlias() => $configs];
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ final class DateTimeFactory
|
|||||||
return $date;
|
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
|
private function getDate(DateTimeInterface|string|null $date = null): DateTime
|
||||||
{
|
{
|
||||||
if ($date === null) {
|
if ($date === null) {
|
||||||
@@ -105,6 +113,14 @@ final class DateTimeFactory
|
|||||||
return $date;
|
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)
|
private function createWeekDateTime($year, $week, $day, $hour, $minute, $second)
|
||||||
{
|
{
|
||||||
$date = new DateTime('now', $this->getTimezone());
|
$date = new DateTime('now', $this->getTimezone());
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ final class DataTable implements \Countable, \IteratorAggregate
|
|||||||
|
|
||||||
public function hasConfiguration(): bool
|
public function hasConfiguration(): bool
|
||||||
{
|
{
|
||||||
return $this->configuration;
|
return $this->configuration && \count($this->columns) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPaginationRoute(): ?string
|
public function getPaginationRoute(): ?string
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
|
|
||||||
namespace App\Utils;
|
namespace App\Utils;
|
||||||
|
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\Form\FormView;
|
||||||
|
|
||||||
final class PageSetup
|
final class PageSetup
|
||||||
{
|
{
|
||||||
private ?string $help = null;
|
private ?string $help = null;
|
||||||
@@ -17,11 +20,27 @@ final class PageSetup
|
|||||||
private string $translationDomain = 'messages';
|
private string $translationDomain = 'messages';
|
||||||
private array $actionPayload = [];
|
private array $actionPayload = [];
|
||||||
private ?DataTable $dataTable = null;
|
private ?DataTable $dataTable = null;
|
||||||
|
private ?FormInterface $paginationForm = null;
|
||||||
|
|
||||||
public function __construct(private string $title)
|
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
|
public function hasDataTable(): bool
|
||||||
{
|
{
|
||||||
return $this->dataTable !== null;
|
return $this->dataTable !== null;
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
"behat/transliterator": {
|
"behat/transliterator": {
|
||||||
"version": "v1.2.0"
|
"version": "v1.2.0"
|
||||||
},
|
},
|
||||||
"composer/package-versions-deprecated": {
|
|
||||||
"version": "1.11.99.5"
|
|
||||||
},
|
|
||||||
"composer/pcre": {
|
"composer/pcre": {
|
||||||
"version": "1.0.1"
|
"version": "1.0.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
{% extends 'base.html.twig' %}
|
{% extends 'page_setup.html.twig' %}
|
||||||
{% import "macros/widgets.html.twig" as widgets %}
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
{% set withWorkHourConfiguration = user.hasWorkHourConfiguration() %}
|
{% set withWorkHourConfiguration = user.hasWorkHourConfiguration() %}
|
||||||
|
|
||||||
{% if form is defined %}
|
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-body">
|
|
||||||
{{ form_start(form, {'attr': {'class': 'form-reporting', 'id': 'report-form'}}) }}
|
|
||||||
<div class="btn-list w-100">
|
|
||||||
{{ form_widget(form.date) }}
|
|
||||||
{% if form.user is defined %}
|
|
||||||
{{ form_widget(form.user, {'label': false}) }}
|
|
||||||
{% elseif app.user != user %}
|
|
||||||
{{ widgets.username(user) }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{{ form_end(form) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if not withWorkHourConfiguration %}
|
{% if not withWorkHourConfiguration %}
|
||||||
{{ widgets.alert('info', 'work_times_should.none_configured'|trans) }}
|
{% set warningMsg %}
|
||||||
|
{{ 'work_times_should.none_configured'|trans }}
|
||||||
|
{% if is_granted('contract', user) %}
|
||||||
|
<a href="{{ path('user_profile_contract', {'username': user.getUserIdentifier()}) }}" class="alert-link">{{ 'action.edit'|trans }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endset %}
|
||||||
|
{% from '@theme/components/alert.html.twig' import alert %}
|
||||||
|
{{ alert({type: 'info', description: warningMsg|raw}) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if summaries|length > 0 %}
|
{% if summaries|length > 0 %}
|
||||||
@@ -299,13 +289,6 @@
|
|||||||
|
|
||||||
document.addEventListener('kimai.initialized', function() {
|
document.addEventListener('kimai.initialized', function() {
|
||||||
KimaiReloadPageWidget.create('kimai.workingTimesUpdate', true);
|
KimaiReloadPageWidget.create('kimai.workingTimesUpdate', true);
|
||||||
|
|
||||||
const contractForm = document.getElementById('report-form');
|
|
||||||
if (contractForm !== null) {
|
|
||||||
contractForm.addEventListener('change', () => {
|
|
||||||
contractForm.submit();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
{% import "macros/widgets.html.twig" as widgets %}
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
{% import "macros/datatables.html.twig" as tables %}
|
{% import "macros/datatables.html.twig" as tables %}
|
||||||
|
|
||||||
|
{% if dataTable is not defined and page_setup is defined and page_setup.hasDataTable() %}
|
||||||
|
{% set dataTable = page_setup.getDataTable() %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block main_before %}
|
{% block main_before %}
|
||||||
{{ tables.configuration(dataTable) }}
|
{{ tables.configuration(dataTable) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{% set showUserColumn = showUserColumn ?? true %}
|
{% set showUserColumn = showUserColumn ?? true %}
|
||||||
{% set showInternalRate = showInternalRate ?? false %}
|
{% set showInternalRate = showInternalRate ?? false %}
|
||||||
{% set showRateColumn = showRateColumn ?? is_granted('view_rate_other_timesheet') %}
|
{% set showRateColumn = showRateColumn ?? is_granted('view_rate_other_timesheet') %}
|
||||||
|
{% set showHourlyRateColumn = showRateColumn and (showHourlyRateColumn ?? false) %}
|
||||||
|
{% set showSummaryHourlyRate = showRateColumn and (showSummaryHourlyRate ?? false) %}
|
||||||
{% set showRateBudget = showRateBudget ?? false %}
|
{% set showRateBudget = showRateBudget ?? false %}
|
||||||
{% set showTimeBudget = showTimeBudget ?? false %}
|
{% set showTimeBudget = showTimeBudget ?? false %}
|
||||||
{% set showCustomerSummary = showCustomerSummary ?? true %}
|
{% set showCustomerSummary = showCustomerSummary ?? true %}
|
||||||
{% set showTotalSummary = showTotalSummary ?? true %}
|
{% set showTotalSummary = showTotalSummary ?? true %}
|
||||||
{% set showDateTimeShort = showDateTimeShort ?? false %}
|
{% set showDateTimeShort = showDateTimeShort ?? false %}
|
||||||
{% set showSummaryHourlyRate = showSummaryHourlyRate ?? false %}
|
|
||||||
{% set now = create_date('now', app.user) %}
|
{% set now = create_date('now', app.user) %}
|
||||||
{% set decimal = decimal ?? false %}
|
{% set decimal = decimal ?? false %}
|
||||||
{# this is only triggered, if a user exports from his personal timesheet screen #}
|
{# this is only triggered, if a user exports from his personal timesheet screen #}
|
||||||
@@ -228,6 +229,9 @@ mpdf-->
|
|||||||
<th>{{ 'user'|trans }}</th>
|
<th>{{ 'user'|trans }}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th>{{ 'description'|trans }}</th>
|
<th>{{ 'description'|trans }}</th>
|
||||||
|
{% if showHourlyRateColumn %}
|
||||||
|
<th class="center">{{ 'hourlyRate'|trans }}</th>
|
||||||
|
{% endif %}
|
||||||
<th class="center">{{ 'duration'|trans }}</th>
|
<th class="center">{{ 'duration'|trans }}</th>
|
||||||
{% if showRateColumn %}
|
{% if showRateColumn %}
|
||||||
{% if showInternalRate %}
|
{% if showInternalRate %}
|
||||||
@@ -241,10 +245,11 @@ mpdf-->
|
|||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<!-- CONTENT_PART -->
|
<!-- CONTENT_PART -->
|
||||||
{% set duration = duration + entry.duration %}
|
{% set duration = duration + entry.duration %}
|
||||||
|
{% set entryCurrency = entry.project.customer.currency %}
|
||||||
{% if currency is same as(false) %}
|
{% if currency is same as(false) %}
|
||||||
{% set currency = entry.project.customer.currency %}
|
{% set currency = entryCurrency %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if currency is not same as(entry.project.customer.currency) %}
|
{% if currency is not same as(entryCurrency) %}
|
||||||
{% set currency = null %}
|
{% set currency = null %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">
|
<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">
|
||||||
@@ -266,14 +271,29 @@ mpdf-->
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</td>
|
</td>
|
||||||
<td class="duration">{{ entry.duration|duration(decimal) }}</td>
|
{% if showHourlyRateColumn %}
|
||||||
|
<td class="cost">
|
||||||
|
{% if entry.fixedRate %}
|
||||||
|
-
|
||||||
|
{% else %}
|
||||||
|
{{ entry.hourlyRate|money(entryCurrency) }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td class="duration">
|
||||||
|
{% if entry.duration == 0 %}
|
||||||
|
-
|
||||||
|
{% else %}
|
||||||
|
{{ entry.duration|duration(decimal) }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
{% if showRateColumn %}
|
{% if showRateColumn %}
|
||||||
{# no check for is_granted('view_rate', entry) because it is only available for timesheets,
|
{# no check for is_granted('view_rate', entry) because it is only available for timesheets,
|
||||||
but maybe missing for other potential export repositories #}
|
but maybe missing for other potential export repositories #}
|
||||||
{% set rate = rate + entry.rate %}
|
{% set rate = rate + entry.rate %}
|
||||||
{% set rateInternal = rateInternal + entry.internalRate %}
|
{% set rateInternal = rateInternal + entry.internalRate %}
|
||||||
{% set entryRate = entry.rate|money(entry.project.customer.currency) %}
|
{% set entryRate = entry.rate|money(entryCurrency) %}
|
||||||
{% set entryRateInternal = entry.internalRate|money(entry.project.customer.currency) %}
|
{% set entryRateInternal = entry.internalRate|money(entryCurrency) %}
|
||||||
{% if showInternalRate %}
|
{% if showInternalRate %}
|
||||||
<td class="cost">{{ entryRateInternal }}</td>
|
<td class="cost">{{ entryRateInternal }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -287,6 +307,9 @@ mpdf-->
|
|||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">{{ 'sum.total'|trans }}</td>
|
<td colspan="2">{{ 'sum.total'|trans }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if showHourlyRateColumn %}
|
||||||
|
<td class="totals cost"></td>
|
||||||
|
{% endif %}
|
||||||
<td class="totals duration">{{ duration|duration(decimal) }}</td>
|
<td class="totals duration">{{ duration|duration(decimal) }}</td>
|
||||||
{% if showRateColumn %}
|
{% if showRateColumn %}
|
||||||
{% if showInternalRate %}
|
{% if showInternalRate %}
|
||||||
|
|||||||
@@ -174,7 +174,7 @@
|
|||||||
<button type="button" class="btn dropdown-toggle btn-duration-preset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
|
<button type="button" class="btn dropdown-toggle btn-duration-preset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
|
||||||
<div class="dropdown-menu dropdown-menu-start pre-scrollable">
|
<div class="dropdown-menu dropdown-menu-start pre-scrollable">
|
||||||
{% for title, range in ranges %}
|
{% for title, range in ranges %}
|
||||||
<a class="dropdown-item" href="#" data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{% if range[0] is not null %}{{ range[0]|format_date('short', rangeFormat) }}{{ attr['data-separator'] }}{{ range[1]|format_date('short', rangeFormat) }}{% endif %}" data-event="change keyup">{{ ('daterangepicker.' ~ title)|trans({}, 'daterangepicker') }}</a>
|
<a class="dropdown-item" href="#" data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{% if range[0] is not null %}{{ range[0]|format_date('short', rangeFormat) }}{{ attr['data-separator'] }}{{ range[1]|format_date('short', rangeFormat) }}{% endif %}" data-event="change keyup">{{ title|trans({}, 'daterangepicker') }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -261,7 +261,6 @@
|
|||||||
{%- endblock secret_widget %}
|
{%- endblock secret_widget %}
|
||||||
|
|
||||||
{% block yearpicker_widget -%}
|
{% block yearpicker_widget -%}
|
||||||
{%- set functionName = 'year_' ~ form.vars.id %}
|
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a class="btn btn-left btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ previousYear|date_short }}"
|
<a class="btn btn-left btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ previousYear|date_short }}"
|
||||||
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ previousYear|report_date }}" data-event-bubbles="change">
|
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ previousYear|report_date }}" data-event-bubbles="change">
|
||||||
@@ -286,7 +285,6 @@
|
|||||||
{%- endblock yearpicker_widget %}
|
{%- endblock yearpicker_widget %}
|
||||||
|
|
||||||
{% block monthpicker_widget -%}
|
{% block monthpicker_widget -%}
|
||||||
{%- set functionName = 'month_' ~ form.vars.id %}
|
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a class="btn btn-left btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ previousMonth|month_name(true) }}"
|
<a class="btn btn-left btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ previousMonth|month_name(true) }}"
|
||||||
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ previousMonth|report_date }}" data-event-bubbles="change">
|
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ previousMonth|report_date }}" data-event-bubbles="change">
|
||||||
@@ -305,7 +303,6 @@
|
|||||||
{%- endblock monthpicker_widget %}
|
{%- endblock monthpicker_widget %}
|
||||||
|
|
||||||
{% block weekpicker_widget -%}
|
{% block weekpicker_widget -%}
|
||||||
{%- set functionName = 'week_' ~ form.vars.id %}
|
|
||||||
<div class="btn-group week-picker-btn-group {{ attr.class ?? '' }}">
|
<div class="btn-group week-picker-btn-group {{ attr.class ?? '' }}">
|
||||||
<a class="btn btn-left btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': previousWeek|date_format('W')}) }}"
|
<a class="btn btn-left btn-icon" href="#" data-toggle="tooltip" data-placement="top" title="{{ 'stats.workingTimeWeek'|trans({'%week%': previousWeek|date_format('W')}) }}"
|
||||||
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ previousWeek|report_date }}" data-event-bubbles="change">
|
data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ previousWeek|report_date }}" data-event-bubbles="change">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
{{- datatable_column_class(dataTable.getTableName(), column) -}}
|
{{- datatable_column_class(dataTable.getTableName(), column) -}}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{# compare templates/page_setup.html.twig block table_actions #}
|
||||||
{% macro actions(dataTable) %}
|
{% macro actions(dataTable) %}
|
||||||
{% set btnClass = '' %}
|
{% set btnClass = '' %}
|
||||||
{% set bgClass = '' %}
|
{% set bgClass = '' %}
|
||||||
|
|||||||
28
templates/page_setup.html.twig
Normal file
28
templates/page_setup.html.twig
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
|
||||||
|
{% block table_actions %}
|
||||||
|
{# compare templates/macros/datatables.html.twig macro actions(dataTable) #}
|
||||||
|
{% if page_setup is defined and page_setup.hasPaginationForm() %}
|
||||||
|
<div class="btn-list">
|
||||||
|
{% set form = page_setup.getPaginationForm() %}
|
||||||
|
{{ form_start(form, {'attr': {'onchange': 'this.submit()'}}) }}
|
||||||
|
<div class="btn-list w-100 flex-nowrap">
|
||||||
|
{% if form.date is defined %}
|
||||||
|
{{ form_widget(form.date) }}
|
||||||
|
{% endif %}
|
||||||
|
{% if form.user is defined %}
|
||||||
|
{{ form_widget(form.user) }}
|
||||||
|
{% elseif user is defined and app.user != user %}
|
||||||
|
{{ widgets.username(user) }}
|
||||||
|
{% endif %}
|
||||||
|
{% for field in form %}
|
||||||
|
{% if not field.rendered %}
|
||||||
|
{{ form_widget(field) }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{{ form_end(form) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
|
{% extends kimai_context.modalRequest ? 'form.html.twig' : 'base.html.twig' %}
|
||||||
{% import "macros/widgets.html.twig" as widgets %}
|
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
{{ include(kimai_context.modalRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig', {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
{{ 'profile.2fa_intro'|trans }}
|
{{ 'profile.2fa_intro'|trans }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<img alt="TOTP QR Code" style="max-width: 200px; max-height: 200px;" src="{{ qr_code.dataUri }}" />
|
<img onclick="document.getElementById('totp_secret').classList.toggle('d-none');" data-toggle="tooltip" title="Click to show code" alt="TOTP QR Code" style="max-width: 200px; max-height: 200px;" src="{{ qr_code.dataUri }}" />
|
||||||
|
<span id="totp_secret" style="display: block" class="d-none">{{ secret }}</span>
|
||||||
</p>
|
</p>
|
||||||
{% if user.totpAuthenticationEnabled %}
|
{% if user.totpAuthenticationEnabled %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ class ProfileControllerTest extends ControllerBaseTest
|
|||||||
$content = $client->getResponse()->getContent();
|
$content = $client->getResponse()->getContent();
|
||||||
$this->assertNotFalse($content);
|
$this->assertNotFalse($content);
|
||||||
|
|
||||||
$this->assertStringContainsString('<img alt="TOTP QR Code" style="max-width: 200px; max-height: 200px;" src="', $content);
|
$this->assertStringContainsString(' data-toggle="tooltip" title="Click to show code" alt="TOTP QR Code" style="max-width: 200px; max-height: 200px;" src="', $content);
|
||||||
$this->assertStringContainsString('<form name="user_two_factor" method="post" action="' . $formUrl . '" id="user_two_factor_form">', $content);
|
$this->assertStringContainsString('<form name="user_two_factor" method="post" action="' . $formUrl . '" id="user_two_factor_form">', $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,7 +503,7 @@ class ProfileControllerTest extends ControllerBaseTest
|
|||||||
$this->assertNotFalse($content);
|
$this->assertNotFalse($content);
|
||||||
$formUrl = $this->createUrl('/profile/' . UserFixtures::USERNAME_USER . '/2fa');
|
$formUrl = $this->createUrl('/profile/' . UserFixtures::USERNAME_USER . '/2fa');
|
||||||
|
|
||||||
$this->assertStringContainsString('<img alt="TOTP QR Code" style="max-width: 200px; max-height: 200px;" src="', $content);
|
$this->assertStringContainsString(' data-toggle="tooltip" title="Click to show code" alt="TOTP QR Code" style="max-width: 200px; max-height: 200px;" src="', $content);
|
||||||
$this->assertStringContainsString('<form name="user_two_factor" method="post" action="' . $formUrl . '" id="user_two_factor_form">', $content);
|
$this->assertStringContainsString('<form name="user_two_factor" method="post" action="' . $formUrl . '" id="user_two_factor_form">', $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ class UserTest extends TestCase
|
|||||||
self::assertNull($user->getApiToken());
|
self::assertNull($user->getApiToken());
|
||||||
self::assertNull($user->getPlainApiToken());
|
self::assertNull($user->getPlainApiToken());
|
||||||
self::assertNull($user->getPasswordRequestedAt());
|
self::assertNull($user->getPasswordRequestedAt());
|
||||||
|
self::assertFalse($user->hasTotpSecret());
|
||||||
|
self::assertNull($user->getTotpSecret());
|
||||||
self::assertEquals(User::DEFAULT_LANGUAGE, $user->getLocale());
|
self::assertEquals(User::DEFAULT_LANGUAGE, $user->getLocale());
|
||||||
self::assertFalse($user->hasTeamAssignment());
|
self::assertFalse($user->hasTeamAssignment());
|
||||||
self::assertFalse($user->canSeeAllData());
|
self::assertFalse($user->canSeeAllData());
|
||||||
@@ -59,6 +61,9 @@ class UserTest extends TestCase
|
|||||||
$user->setApiToken('nbvfdswe34567ujko098765rerfghbgvfcdsert');
|
$user->setApiToken('nbvfdswe34567ujko098765rerfghbgvfcdsert');
|
||||||
self::assertEquals('nbvfdswe34567ujko098765rerfghbgvfcdsert', $user->getApiToken());
|
self::assertEquals('nbvfdswe34567ujko098765rerfghbgvfcdsert', $user->getApiToken());
|
||||||
|
|
||||||
|
$user->setTotpSecret('ertzuio878t6rtdrjfcghvjkiu87');
|
||||||
|
self::assertEquals('ertzuio878t6rtdrjfcghvjkiu87', $user->getTotpSecret());
|
||||||
|
|
||||||
$user->setPlainApiToken('https://www.gravatar.com/avatar/nbvfdswe34567ujko098765rerfghbgvfcdsert');
|
$user->setPlainApiToken('https://www.gravatar.com/avatar/nbvfdswe34567ujko098765rerfghbgvfcdsert');
|
||||||
self::assertEquals('https://www.gravatar.com/avatar/nbvfdswe34567ujko098765rerfghbgvfcdsert', $user->getPlainApiToken());
|
self::assertEquals('https://www.gravatar.com/avatar/nbvfdswe34567ujko098765rerfghbgvfcdsert', $user->getPlainApiToken());
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,14 @@
|
|||||||
<source>Remove widget</source>
|
<source>Remove widget</source>
|
||||||
<target>Widget entfernen</target>
|
<target>Widget entfernen</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="5vJvarK" resname="syncronisation">
|
||||||
|
<source>syncronisation</source>
|
||||||
|
<target>Syncronisierung</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2UL2SIZ" resname="import">
|
||||||
|
<source>import</source>
|
||||||
|
<target>Import</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -118,6 +118,14 @@
|
|||||||
<source>Remove widget</source>
|
<source>Remove widget</source>
|
||||||
<target>Remove widget</target>
|
<target>Remove widget</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="5vJvarK" resname="syncronisation">
|
||||||
|
<source>syncronisation</source>
|
||||||
|
<target>Syncronisation</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2UL2SIZ" resname="import">
|
||||||
|
<source>import</source>
|
||||||
|
<target>Import</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -1537,6 +1537,38 @@
|
|||||||
<source>time</source>
|
<source>time</source>
|
||||||
<target>Uhrzeit</target>
|
<target>Uhrzeit</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="95DUED0" resname="send_to">
|
||||||
|
<source>send_to</source>
|
||||||
|
<target>Senden an %name%</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5buBUCm" resname="receive_from">
|
||||||
|
<source>receive_from</source>
|
||||||
|
<target>Empfangen von %name%</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="fWuGTog" resname="public_holiday">
|
||||||
|
<source>public_holiday</source>
|
||||||
|
<target>Feiertag</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="zlmddHB" resname="public_holidays">
|
||||||
|
<source>public_holidays</source>
|
||||||
|
<target>Feiertage</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="u4lbYD5" resname="public_holiday.group">
|
||||||
|
<source>public_holiday.group</source>
|
||||||
|
<target>Feiertags-Gruppe</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="GIgQHa4" resname="absence">
|
||||||
|
<source>absence</source>
|
||||||
|
<target>Abwesenheit</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="4PMHpIp" resname="time_off">
|
||||||
|
<source>time_off</source>
|
||||||
|
<target>Freizeitausgleich</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="N6juwc4" resname="default">
|
||||||
|
<source>default</source>
|
||||||
|
<target>Standard</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -1537,6 +1537,38 @@
|
|||||||
<source>time</source>
|
<source>time</source>
|
||||||
<target>Time</target>
|
<target>Time</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="95DUED0" resname="send_to">
|
||||||
|
<source>send_to</source>
|
||||||
|
<target>Send to %name%</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5buBUCm" resname="receive_from">
|
||||||
|
<source>receive_from</source>
|
||||||
|
<target>Receive from %name%</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="fWuGTog" resname="public_holiday">
|
||||||
|
<source>public_holiday</source>
|
||||||
|
<target>Public holiday</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="zlmddHB" resname="public_holidays">
|
||||||
|
<source>public_holidays</source>
|
||||||
|
<target>Public holidays</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="u4lbYD5" resname="public_holiday.group">
|
||||||
|
<source>public_holiday.group</source>
|
||||||
|
<target>Holiday group</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="GIgQHa4" resname="absence">
|
||||||
|
<source>absence</source>
|
||||||
|
<target>Absence</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="4PMHpIp" resname="time_off">
|
||||||
|
<source>time_off</source>
|
||||||
|
<target>Time off</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="N6juwc4" resname="default">
|
||||||
|
<source>default</source>
|
||||||
|
<target>Default</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
Reference in New Issue
Block a user