diff --git a/README.md b/README.md
index bdff6aea..967e7b5b 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
-
+
diff --git a/src/Constants.php b/src/Constants.php
index f308a849..049de959 100644
--- a/src/Constants.php
+++ b/src/Constants.php
@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
- public const VERSION = '2.0.30';
+ public const VERSION = '2.0.31';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
- public const VERSION_ID = 20030;
+ public const VERSION_ID = 20031;
/**
* The software name
*/
diff --git a/src/Entity/Activity.php b/src/Entity/Activity.php
index 1429f107..430a0ed4 100644
--- a/src/Entity/Activity.php
+++ b/src/Entity/Activity.php
@@ -43,8 +43,8 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'id', type: 'integer')]
private ?int $id = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Project')]
- #[ORM\JoinColumn(onDelete: 'CASCADE')]
+ #[ORM\ManyToOne(targetEntity: Project::class)]
+ #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Subresource', 'Expanded'])]
#[OA\Property(ref: '#/components/schemas/ProjectExpanded')]
@@ -87,7 +87,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
*
* @var Collection
*/
- #[ORM\OneToMany(targetEntity: 'App\Entity\ActivityMeta', mappedBy: 'activity', cascade: ['persist'])]
+ #[ORM\OneToMany(targetEntity: ActivityMeta::class, mappedBy: 'activity', cascade: ['persist'])]
#[Serializer\Expose]
#[Serializer\Groups(['Activity'])]
#[Serializer\Type(name: 'array')]
@@ -102,7 +102,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
#[ORM\JoinTable(name: 'kimai2_activities_teams')]
#[ORM\JoinColumn(name: 'activity_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
- #[ORM\ManyToMany(targetEntity: 'App\Entity\Team', cascade: ['persist'], inversedBy: 'activities')]
+ #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'activities')]
#[Serializer\Expose]
#[Serializer\Groups(['Activity'])]
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
diff --git a/src/Entity/ActivityMeta.php b/src/Entity/ActivityMeta.php
index 0631fcd5..f4f1561a 100644
--- a/src/Entity/ActivityMeta.php
+++ b/src/Entity/ActivityMeta.php
@@ -22,7 +22,7 @@ class ActivityMeta implements MetaTableTypeInterface
{
use MetaTableTypeTrait;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Activity', inversedBy: 'meta')]
+ #[ORM\ManyToOne(targetEntity: Activity::class, inversedBy: 'meta')]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?Activity $activity = null;
diff --git a/src/Entity/ActivityRate.php b/src/Entity/ActivityRate.php
index 033d8405..ff4de09b 100644
--- a/src/Entity/ActivityRate.php
+++ b/src/Entity/ActivityRate.php
@@ -24,7 +24,7 @@ class ActivityRate implements RateInterface
{
use Rate;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Activity')]
+ #[ORM\ManyToOne(targetEntity: Activity::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?Activity $activity = null;
diff --git a/src/Entity/Bookmark.php b/src/Entity/Bookmark.php
index 9c9b7846..25af24a7 100644
--- a/src/Entity/Bookmark.php
+++ b/src/Entity/Bookmark.php
@@ -28,7 +28,7 @@ class Bookmark
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
+ #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?User $user = null;
diff --git a/src/Entity/CommentTableTypeTrait.php b/src/Entity/CommentTableTypeTrait.php
index 64208f45..314e4b86 100644
--- a/src/Entity/CommentTableTypeTrait.php
+++ b/src/Entity/CommentTableTypeTrait.php
@@ -21,7 +21,7 @@ trait CommentTableTypeTrait
#[ORM\Column(name: 'message', type: 'text', nullable: false)]
#[Assert\NotNull]
private ?string $message = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
+ #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?User $createdBy = null;
diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php
index 4b9de8d6..0289cbf5 100644
--- a/src/Entity/Customer.php
+++ b/src/Entity/Customer.php
@@ -156,7 +156,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
*
* @var Collection
*/
- #[ORM\OneToMany(targetEntity: 'App\Entity\CustomerMeta', mappedBy: 'customer', cascade: ['persist'])]
+ #[ORM\OneToMany(targetEntity: CustomerMeta::class, mappedBy: 'customer', cascade: ['persist'])]
#[Serializer\Expose]
#[Serializer\Groups(['Customer'])]
#[Serializer\Type(name: 'array')]
@@ -171,7 +171,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
#[ORM\JoinTable(name: 'kimai2_customers_teams')]
#[ORM\JoinColumn(name: 'customer_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
- #[ORM\ManyToMany(targetEntity: 'App\Entity\Team', cascade: ['persist'], inversedBy: 'customers')]
+ #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'customers')]
#[Serializer\Expose]
#[Serializer\Groups(['Customer'])]
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
@@ -179,7 +179,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
/**
* Default invoice template for this customer
*/
- #[ORM\ManyToOne(targetEntity: 'App\Entity\InvoiceTemplate')]
+ #[ORM\ManyToOne(targetEntity: InvoiceTemplate::class)]
#[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)]
private ?InvoiceTemplate $invoiceTemplate = null;
#[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
diff --git a/src/Entity/CustomerComment.php b/src/Entity/CustomerComment.php
index fea10ce8..012e9770 100644
--- a/src/Entity/CustomerComment.php
+++ b/src/Entity/CustomerComment.php
@@ -20,7 +20,7 @@ class CustomerComment implements CommentInterface
{
use CommentTableTypeTrait;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')]
+ #[ORM\ManyToOne(targetEntity: Customer::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private Customer $customer;
diff --git a/src/Entity/CustomerMeta.php b/src/Entity/CustomerMeta.php
index 8bee41e4..f396e82a 100644
--- a/src/Entity/CustomerMeta.php
+++ b/src/Entity/CustomerMeta.php
@@ -22,7 +22,7 @@ class CustomerMeta implements MetaTableTypeInterface
{
use MetaTableTypeTrait;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Customer', inversedBy: 'meta')]
+ #[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: 'meta')]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?Customer $customer = null;
diff --git a/src/Entity/CustomerRate.php b/src/Entity/CustomerRate.php
index 48062776..4d747459 100644
--- a/src/Entity/CustomerRate.php
+++ b/src/Entity/CustomerRate.php
@@ -24,7 +24,7 @@ class CustomerRate implements RateInterface
{
use Rate;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')]
+ #[ORM\ManyToOne(targetEntity: Customer::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?Customer $customer = null;
diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php
index 2bc15fca..c5188b0e 100644
--- a/src/Entity/Invoice.php
+++ b/src/Entity/Invoice.php
@@ -55,11 +55,11 @@ class Invoice implements EntityWithMetaFields
#[Serializer\Groups(['Customer_Entity'])]
#[Exporter\Expose(label: 'comment')]
private ?string $comment = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')]
+ #[ORM\ManyToOne(targetEntity: Customer::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?Customer $customer = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
+ #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?User $user = null;
@@ -109,7 +109,7 @@ class Invoice implements EntityWithMetaFields
*
* @var Collection
*/
- #[ORM\OneToMany(targetEntity: 'App\Entity\InvoiceMeta', mappedBy: 'invoice', cascade: ['persist'])]
+ #[ORM\OneToMany(targetEntity: InvoiceMeta::class, mappedBy: 'invoice', cascade: ['persist'])]
#[Serializer\Expose]
#[Serializer\Groups(['Invoice'])]
#[Serializer\Type(name: 'array')]
diff --git a/src/Entity/InvoiceMeta.php b/src/Entity/InvoiceMeta.php
index 6c7029ad..d41db1e9 100644
--- a/src/Entity/InvoiceMeta.php
+++ b/src/Entity/InvoiceMeta.php
@@ -22,7 +22,7 @@ class InvoiceMeta implements MetaTableTypeInterface
{
use MetaTableTypeTrait;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Invoice', inversedBy: 'meta')]
+ #[ORM\ManyToOne(targetEntity: Invoice::class, inversedBy: 'meta')]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?Invoice $invoice = null;
diff --git a/src/Entity/Project.php b/src/Entity/Project.php
index 7bc1b781..f26e5b8d 100644
--- a/src/Entity/Project.php
+++ b/src/Entity/Project.php
@@ -47,7 +47,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
/**
* Customer for this project
*/
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')]
+ #[ORM\ManyToOne(targetEntity: Customer::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
@@ -137,7 +137,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
*
* @var Collection
*/
- #[ORM\OneToMany(targetEntity: 'App\Entity\ProjectMeta', mappedBy: 'project', cascade: ['persist'])]
+ #[ORM\OneToMany(targetEntity: ProjectMeta::class, mappedBy: 'project', cascade: ['persist'])]
#[Serializer\Expose]
#[Serializer\Groups(['Project'])]
#[Serializer\Type(name: 'array')]
@@ -152,7 +152,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
#[ORM\JoinTable(name: 'kimai2_projects_teams')]
#[ORM\JoinColumn(name: 'project_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
- #[ORM\ManyToMany(targetEntity: 'App\Entity\Team', cascade: ['persist'], inversedBy: 'projects')]
+ #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'projects')]
#[Serializer\Expose]
#[Serializer\Groups(['Project'])]
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
diff --git a/src/Entity/ProjectComment.php b/src/Entity/ProjectComment.php
index 33c01985..ddba3cd2 100644
--- a/src/Entity/ProjectComment.php
+++ b/src/Entity/ProjectComment.php
@@ -20,7 +20,7 @@ class ProjectComment implements CommentInterface
{
use CommentTableTypeTrait;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Project')]
+ #[ORM\ManyToOne(targetEntity: Project::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private Project $project;
diff --git a/src/Entity/ProjectMeta.php b/src/Entity/ProjectMeta.php
index bcb0c341..89e7fac2 100644
--- a/src/Entity/ProjectMeta.php
+++ b/src/Entity/ProjectMeta.php
@@ -22,7 +22,7 @@ class ProjectMeta implements MetaTableTypeInterface
{
use MetaTableTypeTrait;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Project', inversedBy: 'meta')]
+ #[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'meta')]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?Project $project = null;
diff --git a/src/Entity/ProjectRate.php b/src/Entity/ProjectRate.php
index 0a21b852..7bdc8695 100644
--- a/src/Entity/ProjectRate.php
+++ b/src/Entity/ProjectRate.php
@@ -24,7 +24,7 @@ class ProjectRate implements RateInterface
{
use Rate;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Project')]
+ #[ORM\ManyToOne(targetEntity: Project::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?Project $project = null;
diff --git a/src/Entity/Rate.php b/src/Entity/Rate.php
index 76259208..15e1c31b 100644
--- a/src/Entity/Rate.php
+++ b/src/Entity/Rate.php
@@ -22,7 +22,7 @@ trait Rate
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?int $id = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
+ #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
diff --git a/src/Entity/RolePermission.php b/src/Entity/RolePermission.php
index 41643e8f..5c18e179 100644
--- a/src/Entity/RolePermission.php
+++ b/src/Entity/RolePermission.php
@@ -24,7 +24,7 @@ class RolePermission
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Role')]
+ #[ORM\ManyToOne(targetEntity: Role::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?Role $role = null;
diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php
index 35cbe8e3..febc8546 100644
--- a/src/Entity/Tag.php
+++ b/src/Entity/Tag.php
@@ -54,7 +54,7 @@ class Tag
/**
* @var Collection
*/
- #[ORM\ManyToMany(targetEntity: 'App\Entity\Timesheet', mappedBy: 'tags', fetch: 'EXTRA_LAZY')]
+ #[ORM\ManyToMany(targetEntity: Timesheet::class, mappedBy: 'tags', fetch: 'EXTRA_LAZY')]
private Collection $timesheets;
public function __construct()
diff --git a/src/Entity/TeamMember.php b/src/Entity/TeamMember.php
index 571d494a..946ceb14 100644
--- a/src/Entity/TeamMember.php
+++ b/src/Entity/TeamMember.php
@@ -25,14 +25,14 @@ class TeamMember
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
private ?int $id = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\User', inversedBy: 'memberships')]
+ #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'memberships')]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Default', 'Entity', 'Team_Entity'])]
#[OA\Property(ref: '#/components/schemas/User')]
private ?User $user = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Team', inversedBy: 'members')]
+ #[ORM\ManyToOne(targetEntity: Team::class, inversedBy: 'members')]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php
index ed5d93f0..253c1f3d 100644
--- a/src/Entity/Timesheet.php
+++ b/src/Entity/Timesheet.php
@@ -127,21 +127,21 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?int $duration = 0;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
+ #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Subresource', 'Expanded'])]
#[OA\Property(ref: '#/components/schemas/User')]
private ?User $user = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Activity')]
+ #[ORM\ManyToOne(targetEntity: Activity::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
#[Serializer\Groups(['Subresource', 'Expanded'])]
#[OA\Property(ref: '#/components/schemas/ActivityExpanded')]
private ?Activity $activity = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Project')]
+ #[ORM\ManyToOne(targetEntity: Project::class)]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
#[Serializer\Expose]
@@ -203,7 +203,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
#[ORM\JoinTable(name: 'kimai2_timesheet_tags')]
#[ORM\JoinColumn(name: 'timesheet_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
#[ORM\InverseJoinColumn(name: 'tag_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
- #[ORM\ManyToMany(targetEntity: 'App\Entity\Tag', inversedBy: 'timesheets', cascade: ['persist'])]
+ #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'timesheets', cascade: ['persist'])]
#[Assert\Valid]
private Collection $tags;
/**
@@ -211,7 +211,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
*
* @var Collection
*/
- #[ORM\OneToMany(targetEntity: 'App\Entity\TimesheetMeta', mappedBy: 'timesheet', cascade: ['persist'])]
+ #[ORM\OneToMany(targetEntity: TimesheetMeta::class, mappedBy: 'timesheet', cascade: ['persist'])]
#[Serializer\Expose]
#[Serializer\Groups(['Timesheet'])]
#[Serializer\Type(name: 'array')]
diff --git a/src/Entity/TimesheetMeta.php b/src/Entity/TimesheetMeta.php
index 559251c7..4e116f64 100644
--- a/src/Entity/TimesheetMeta.php
+++ b/src/Entity/TimesheetMeta.php
@@ -22,7 +22,7 @@ class TimesheetMeta implements MetaTableTypeInterface
{
use MetaTableTypeTrait;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\Timesheet', inversedBy: 'meta')]
+ #[ORM\ManyToOne(targetEntity: Timesheet::class, inversedBy: 'meta')]
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
#[Assert\NotNull]
private ?Timesheet $timesheet = null;
diff --git a/src/Entity/User.php b/src/Entity/User.php
index 3b827fe4..3770f546 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -159,7 +159,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
#[ORM\Column(name: 'username', type: 'string', length: 180, nullable: false)]
#[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
#[Assert\Regex(pattern: '/\//', match: false, groups: ['Registration', 'UserCreate', 'Profile'])]
- #[Assert\Length(min: 2, max: 60, groups: ['Registration', 'UserCreate', 'Profile'])]
+ #[Assert\Length(min: 2, max: 64, groups: ['Registration', 'UserCreate', 'Profile'])]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
private ?string $username = null;
@@ -1253,9 +1253,9 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
$this->setPreferenceValue(UserPreference::PUBLIC_HOLIDAY_GROUP, $group);
}
- public function setHolidaysPerYear(int $holidays): void
+ public function setHolidaysPerYear(?int $holidays): void
{
- $this->setPreferenceValue(UserPreference::HOLIDAYS_PER_YEAR, $holidays);
+ $this->setPreferenceValue(UserPreference::HOLIDAYS_PER_YEAR, $holidays ?? 0);
}
public function hasContractSettings(): bool
diff --git a/src/Entity/UserPreference.php b/src/Entity/UserPreference.php
index 770c2f25..9835a52b 100644
--- a/src/Entity/UserPreference.php
+++ b/src/Entity/UserPreference.php
@@ -46,7 +46,7 @@ class UserPreference
#[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')]
private ?int $id = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\User', inversedBy: 'preferences')]
+ #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'preferences')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull]
private ?User $user = null;
diff --git a/src/Entity/WorkingTime.php b/src/Entity/WorkingTime.php
index 0898c026..f2e62902 100644
--- a/src/Entity/WorkingTime.php
+++ b/src/Entity/WorkingTime.php
@@ -25,7 +25,7 @@ class WorkingTime
#[ORM\GeneratedValue]
#[ORM\Column(name: 'id', type: 'integer')]
private ?int $id = null;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
+ #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Assert\NotNull]
private ?User $user = null;
@@ -38,7 +38,7 @@ class WorkingTime
#[ORM\Column(name: 'actual', type: 'integer', nullable: false)]
#[Assert\NotNull]
private int $actualTime = 0;
- #[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
+ #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'approved_by', nullable: true, onDelete: 'SET NULL')]
private ?User $approvedBy = null;
#[ORM\Column(name: 'approved_at', type: 'datetime', nullable: true)]
diff --git a/src/Form/Type/DateTimePickerType.php b/src/Form/Type/DateTimePickerType.php
index 5afb8498..242f6ad1 100644
--- a/src/Form/Type/DateTimePickerType.php
+++ b/src/Form/Type/DateTimePickerType.php
@@ -11,6 +11,7 @@ namespace App\Form\Type;
use App\API\BaseApiController;
use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
@@ -22,10 +23,6 @@ class DateTimePickerType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
- $parts = ['year', 'month', 'day', 'hour', 'minute'];
- $dateParts = ['year', 'month', 'day'];
- $timeParts = ['hour', 'minute'];
-
// Only pass a subset of the options to children
$dateOptions = array_intersect_key($options, array_flip([
'years',
@@ -37,6 +34,8 @@ class DateTimePickerType extends AbstractType
'translation_domain',
'invalid_message',
'invalid_message_parameters',
+ 'model_timezone',
+ 'view_timezone',
]));
$timeOptions = array_intersect_key($options, array_flip([
@@ -45,6 +44,8 @@ class DateTimePickerType extends AbstractType
'translation_domain',
'invalid_message',
'invalid_message_parameters',
+ 'model_timezone',
+ 'view_timezone',
]));
if (false === $options['label']) {
@@ -63,6 +64,10 @@ class DateTimePickerType extends AbstractType
$dateOptions['input'] = $timeOptions['input'] = 'array';
$dateOptions['error_bubbling'] = $timeOptions['error_bubbling'] = true;
+ $dateParts = ['year', 'month', 'day'];
+ $timeParts = ['hour', 'minute'];
+ $parts = array_merge($dateParts, $timeParts);
+
$builder
->addViewTransformer(new DataTransformerChain([
new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts),
@@ -70,6 +75,29 @@ class DateTimePickerType extends AbstractType
'date' => $dateParts,
'time' => $timeParts,
]),
+ new CallbackTransformer(
+ function ($transform) {
+ return $transform;
+ },
+ function ($reverseTransform) {
+ if (\array_key_exists('date', $reverseTransform) && $reverseTransform['date'] === null) {
+ $reverseTransform['time'] = [
+ 'year' => '',
+ 'month' => '',
+ 'day' => '',
+ ];
+ }
+ // happened in DateTimePickerType - made it impossible to create an empty DateTime
+ if (\array_key_exists('time', $reverseTransform) && $reverseTransform['time'] === null) {
+ $reverseTransform['time'] = [
+ 'hour' => '',
+ 'minute' => '',
+ ];
+ }
+
+ return $reverseTransform;
+ }
+ ),
]))
->add('date', DatePickerType::class, $dateOptions)
->add('time', TimePickerType::class, $timeOptions)
diff --git a/src/Form/Type/TimePickerType.php b/src/Form/Type/TimePickerType.php
index 01704abf..f2af56d8 100644
--- a/src/Form/Type/TimePickerType.php
+++ b/src/Form/Type/TimePickerType.php
@@ -51,9 +51,17 @@ final class TimePickerType extends AbstractType
// DateTimePickerType
if ($options['input'] === 'array' && \is_array($data)) {
+ if (!\array_key_exists('hour', $data) || $data['hour'] === '' || $data['hour'] === null) {
+ return null;
+ }
+
+ if (!\array_key_exists('minute', $data) || $data['minute'] === '' || $data['minute'] === null) {
+ return null;
+ }
+
$now = new \DateTime('now', new \DateTimeZone($options['model_timezone']));
- $hour = $data['hour'] === '' || !is_numeric($data['hour']) ? 0 : (int) $data['hour'];
- $minute = $data['minute'] === '' || !is_numeric($data['minute']) ? 0 : (int) $data['minute'];
+ $hour = !is_numeric($data['hour']) ? 0 : (int) $data['hour'];
+ $minute = !is_numeric($data['minute']) ? 0 : (int) $data['minute'];
$now->setTime($hour, $minute, 0);
$data = $now;
}
diff --git a/src/Utils/DataTable.php b/src/Utils/DataTable.php
index 30ab8109..65dc06a1 100644
--- a/src/Utils/DataTable.php
+++ b/src/Utils/DataTable.php
@@ -9,7 +9,7 @@
namespace App\Utils;
-use App\Form\MultiUpdate\MultiUpdateTable;
+use App\Form\MultiUpdate\MultiUpdateTableDTO;
use App\Repository\Query\BaseQuery;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
@@ -20,7 +20,7 @@ final class DataTable implements \Countable, \IteratorAggregate
private ?Pagination $pagination = null;
private ?FormInterface $searchForm = null;
/**
- * @var FormInterface|null
+ * @var FormInterface|null
*/
private ?FormInterface $batchForm = null;
private array $columns = [];
@@ -84,7 +84,7 @@ final class DataTable implements \Countable, \IteratorAggregate
}
/**
- * @param FormInterface|null $batchForm
+ * @param FormInterface|null $batchForm
* @return void
*/
public function setBatchForm(?FormInterface $batchForm): void
diff --git a/src/WorkingTime/Model/DayAddon.php b/src/WorkingTime/Model/DayAddon.php
index 8a2ec956..65055543 100644
--- a/src/WorkingTime/Model/DayAddon.php
+++ b/src/WorkingTime/Model/DayAddon.php
@@ -11,14 +11,10 @@ namespace App\WorkingTime\Model;
final class DayAddon
{
- private string $title;
- private int $duration;
private bool $billable = true;
- public function __construct(string $title, int $duration)
+ public function __construct(private string $title, private int $duration, private int $visibleDuration)
{
- $this->title = $title;
- $this->duration = $duration;
}
public function getTitle(): string
@@ -40,4 +36,9 @@ final class DayAddon
{
$this->billable = $billable;
}
+
+ public function getVisibleDuration(): int
+ {
+ return $this->visibleDuration;
+ }
}
diff --git a/templates/contract/status.html.twig b/templates/contract/status.html.twig
index b576978b..8d47eef0 100644
--- a/templates/contract/status.html.twig
+++ b/templates/contract/status.html.twig
@@ -226,9 +226,9 @@
{% if day.hasAddons() %}
{% set statusTitle = '' %}
{% for addon in day.getAddons() %}
- {% set statusTitle = statusTitle ~ (addon.title|trans) ~ ' (' ~ addon.duration|duration(decimal) ~ ')' %}
+ {% set statusTitle = statusTitle ~ (addon.title|trans) ~ ' (' ~ addon.visibleDuration|duration(decimal) ~ ')' %}
{% if not loop.last %}
- {% set statusTitle = statusTitle ~ '
' %}
+ {% set statusTitle = statusTitle ~ ', ' %}
{% endif %}
{% endfor %}
diff --git a/tests/Controller/Security/SelfRegistrationControllerTest.php b/tests/Controller/Security/SelfRegistrationControllerTest.php
index 7a2767eb..06cc49dd 100644
--- a/tests/Controller/Security/SelfRegistrationControllerTest.php
+++ b/tests/Controller/Security/SelfRegistrationControllerTest.php
@@ -62,7 +62,7 @@ class SelfRegistrationControllerTest extends ControllerBaseTest
$this->assertStringContainsString('assertStringContainsString('id="user_registration_form_email" name="user_registration_form[email]" required="required"', $content);
$this->assertStringContainsString('assertStringContainsString('id="user_registration_form_username" name="user_registration_form[username]" required="required" maxlength="60" pattern="', $content);
+ $this->assertStringContainsString('id="user_registration_form_username" name="user_registration_form[username]" required="required" maxlength="64" pattern="', $content);
$this->assertStringContainsString('assertStringContainsString('id="user_registration_form_plainPassword_first" name="user_registration_form[plainPassword][first]" required="required"', $content);
$this->assertStringContainsString('id="user_registration_form_plainPassword_second" name="user_registration_form[plainPassword][second]" required="required"', $content);
diff --git a/tests/Entity/UserTest.php b/tests/Entity/UserTest.php
index f7249ed0..ebba943b 100644
--- a/tests/Entity/UserTest.php
+++ b/tests/Entity/UserTest.php
@@ -91,6 +91,22 @@ class UserTest extends TestCase
{
$user = new User();
+ $monday = new \DateTime('2023-05-08 12:00:00', new \DateTimeZone('Europe/Berlin'));
+ $tuesday = new \DateTime('2023-05-09 12:00:00', new \DateTimeZone('Europe/Berlin'));
+ $wednesday = new \DateTime('2023-05-10 12:00:00', new \DateTimeZone('Europe/Berlin'));
+ $thursday = new \DateTime('2023-05-11 12:00:00', new \DateTimeZone('Europe/Berlin'));
+ $friday = new \DateTime('2023-05-12 12:00:00', new \DateTimeZone('Europe/Berlin'));
+ $saturday = new \DateTime('2023-05-13 12:00:00', new \DateTimeZone('Europe/Berlin'));
+ $sunday = new \DateTime('2023-05-14 12:00:00', new \DateTimeZone('Europe/Berlin'));
+
+ self::assertFalse($user->isWorkDay($monday));
+ self::assertFalse($user->isWorkDay($tuesday));
+ self::assertFalse($user->isWorkDay($wednesday));
+ self::assertFalse($user->isWorkDay($thursday));
+ self::assertFalse($user->isWorkDay($friday));
+ self::assertFalse($user->isWorkDay($saturday));
+ self::assertFalse($user->isWorkDay($sunday));
+
$user->setWorkHoursMonday(7200);
self::assertTrue($user->hasWorkHourConfiguration());
$user->setWorkHoursTuesday(7300);
@@ -111,13 +127,21 @@ class UserTest extends TestCase
self::assertEquals(7800, $user->getWorkHoursSunday());
self::assertEquals(10, $user->getHolidaysPerYear());
- self::assertEquals(7200, $user->getWorkHoursForDay(new \DateTime('2023-05-08 12:00:00', new \DateTimeZone('Europe/Berlin'))));
- self::assertEquals(7300, $user->getWorkHoursForDay(new \DateTime('2023-05-09 12:00:00', new \DateTimeZone('Europe/Berlin'))));
- self::assertEquals(7400, $user->getWorkHoursForDay(new \DateTime('2023-05-10 12:00:00', new \DateTimeZone('Europe/Berlin'))));
- self::assertEquals(7500, $user->getWorkHoursForDay(new \DateTime('2023-05-11 12:00:00', new \DateTimeZone('Europe/Berlin'))));
- self::assertEquals(7600, $user->getWorkHoursForDay(new \DateTime('2023-05-12 12:00:00', new \DateTimeZone('Europe/Berlin'))));
- self::assertEquals(7700, $user->getWorkHoursForDay(new \DateTime('2023-05-13 12:00:00', new \DateTimeZone('Europe/Berlin'))));
- self::assertEquals(7800, $user->getWorkHoursForDay(new \DateTime('2023-05-14 12:00:00', new \DateTimeZone('Europe/Berlin'))));
+ self::assertEquals(7200, $user->getWorkHoursForDay($monday));
+ self::assertEquals(7300, $user->getWorkHoursForDay($tuesday));
+ self::assertEquals(7400, $user->getWorkHoursForDay($wednesday));
+ self::assertEquals(7500, $user->getWorkHoursForDay($thursday));
+ self::assertEquals(7600, $user->getWorkHoursForDay($friday));
+ self::assertEquals(7700, $user->getWorkHoursForDay($saturday));
+ self::assertEquals(7800, $user->getWorkHoursForDay($sunday));
+
+ self::assertTrue($user->isWorkDay($monday));
+ self::assertTrue($user->isWorkDay($tuesday));
+ self::assertTrue($user->isWorkDay($wednesday));
+ self::assertTrue($user->isWorkDay($thursday));
+ self::assertTrue($user->isWorkDay($friday));
+ self::assertTrue($user->isWorkDay($saturday));
+ self::assertTrue($user->isWorkDay($sunday));
$user->setPublicHolidayGroup('10');
self::assertEquals('10', $user->getPublicHolidayGroup());
diff --git a/tests/Entity/UserValidationTest.php b/tests/Entity/UserValidationTest.php
index 391426a2..b8d14e9f 100644
--- a/tests/Entity/UserValidationTest.php
+++ b/tests/Entity/UserValidationTest.php
@@ -25,14 +25,14 @@ class UserValidationTest extends KernelTestCase
return [
['', ''],
['x', 'test@'], // too short username
- [str_pad('#', 61, '-'), 'test@x.'], // too long username
+ [str_pad('#', 65, '-'), 'test@x.'], // too long username
];
}
/**
* @dataProvider getInvalidTestData
*/
- public function testInvalidValues($username, $email, $roles = [])
+ public function testInvalidValues($username, $email, $roles = []): void
{
$defaultFields = [
'username', 'email'
@@ -49,7 +49,7 @@ class UserValidationTest extends KernelTestCase
$this->assertHasViolationForField($user, $defaultFields, ['Profile']);
}
- public function testInvalidRoles()
+ public function testInvalidRoles(): void
{
$user = new User();
$user->setUserIdentifier('foo');
@@ -59,7 +59,7 @@ class UserValidationTest extends KernelTestCase
$this->assertHasViolationForField($user, ['roles'], ['RolesUpdate']);
}
- public function testValidRoles()
+ public function testValidRoles(): void
{
$user = new User();
$user->setUserIdentifier('foo');
@@ -73,14 +73,14 @@ class UserValidationTest extends KernelTestCase
{
return [
[str_pad('#', 8, '-'), 'test@x.x'], // shortest possible username
- [str_pad('#', 60, '-'), 'test@x.x'], // longest possible username
+ [str_pad('#', 64, '-'), 'test@x.x'], // longest possible username
];
}
/**
* @dataProvider getValidTestData
*/
- public function testValidValues($username, $email, $roles = [])
+ public function testValidValues($username, $email, $roles = []): void
{
$user = new User();
$user->setUserIdentifier($username);
diff --git a/tests/WorkingTime/Model/DayAddonTest.php b/tests/WorkingTime/Model/DayAddonTest.php
new file mode 100644
index 00000000..2f468560
--- /dev/null
+++ b/tests/WorkingTime/Model/DayAddonTest.php
@@ -0,0 +1,34 @@
+getTitle());
+ self::assertEquals(7200, $sut->getDuration());
+ self::assertEquals(0, $sut->getVisibleDuration());
+ self::assertTrue($sut->isBillable());
+
+ $sut->setBillable(false);
+ self::assertFalse($sut->isBillable());
+
+ $sut->setBillable(true);
+ self::assertTrue($sut->isBillable());
+ }
+}
diff --git a/tests/phpstan.neon b/tests/phpstan.neon
index d76a5b59..ec8e43ed 100644
--- a/tests/phpstan.neon
+++ b/tests/phpstan.neon
@@ -4472,16 +4472,6 @@ parameters:
count: 1
path: Entity/UserValidationTest.php
- -
- message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testInvalidRoles\\(\\) has no return type specified\\.$#"
- count: 1
- path: Entity/UserValidationTest.php
-
- -
- message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testInvalidValues\\(\\) has no return type specified\\.$#"
- count: 1
- path: Entity/UserValidationTest.php
-
-
message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testInvalidValues\\(\\) has parameter \\$email with no type specified\\.$#"
count: 1
@@ -4497,16 +4487,6 @@ parameters:
count: 1
path: Entity/UserValidationTest.php
- -
- message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testValidRoles\\(\\) has no return type specified\\.$#"
- count: 1
- path: Entity/UserValidationTest.php
-
- -
- message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testValidValues\\(\\) has no return type specified\\.$#"
- count: 1
- path: Entity/UserValidationTest.php
-
-
message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testValidValues\\(\\) has parameter \\$email with no type specified\\.$#"
count: 1