Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)
This commit is contained in:
@@ -14,172 +14,101 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_activities",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"visible","project_id"}),
|
||||
* @ORM\Index(columns={"visible","project_id","name"}),
|
||||
* @ORM\Index(columns={"visible","name"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ActivityRepository")
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
* @Serializer\VirtualProperty(
|
||||
* "ProjectName",
|
||||
* exp="object.getProject() === null ? null : object.getProject().getName()",
|
||||
* options={
|
||||
* @Serializer\SerializedName("parentTitle"),
|
||||
* @Serializer\Type(name="string"),
|
||||
* @Serializer\Groups({"Activity"})
|
||||
* }
|
||||
* )
|
||||
* @Serializer\VirtualProperty(
|
||||
* "ProjectAsId",
|
||||
* exp="object.getProject() === null ? null : object.getProject().getId()",
|
||||
* options={
|
||||
* @Serializer\SerializedName("project"),
|
||||
* @Serializer\Type(name="integer"),
|
||||
* @Serializer\Groups({"Activity", "Team", "Not_Expanded"})
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @Exporter\Order({"id", "name", "project", "budget", "timeBudget", "budgetType", "color", "visible", "comment", "billable"})
|
||||
* @Exporter\Expose("project", label="label.project", exp="object.getProject() === null ? null : object.getProject().getName()")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_activities')]
|
||||
#[ORM\Index(columns: ['visible', 'project_id'])]
|
||||
#[ORM\Index(columns: ['visible', 'project_id', 'name'])]
|
||||
#[ORM\Index(columns: ['visible', 'name'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\ActivityRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Serializer\VirtualProperty('ProjectName', exp: 'object.getProject() === null ? null : object.getProject().getName()', options: [new Serializer\SerializedName('parentTitle'), new Serializer\Type(name: 'string'), new Serializer\Groups(['Activity'])])]
|
||||
#[Serializer\VirtualProperty('ProjectAsId', exp: 'object.getProject() === null ? null : object.getProject().getId()', options: [new Serializer\SerializedName('project'), new Serializer\Type(name: 'integer'), new Serializer\Groups(['Activity', 'Team', 'Not_Expanded'])])]
|
||||
#[Exporter\Order(['id', 'name', 'project', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'comment', 'billable'])]
|
||||
#[Exporter\Expose(name: 'project', label: 'project', exp: 'object.getProject() === null ? null : object.getProject().getName()')]
|
||||
class Activity implements EntityWithMetaFields, EntityWithBudget
|
||||
{
|
||||
use BudgetTrait;
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* Internal ID
|
||||
*
|
||||
* @var int|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.id", type="integer")
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* Unique activity ID
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var Project|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Subresource", "Expanded"})
|
||||
* @SWG\Property(ref="#/definitions/Project")
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
*/
|
||||
private $project;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Project')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
#[OA\Property(ref: '#/components/schemas/Project')]
|
||||
private ?Project $project = null;
|
||||
/**
|
||||
* Name of this activity
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.name")
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=150, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=150, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 150, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 3, max: 150)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'name')]
|
||||
private ?string $name = null;
|
||||
/**
|
||||
* Description of this activity
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.comment")
|
||||
*
|
||||
* @ORM\Column(name="comment", type="text", nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
#[ORM\Column(name: 'comment', type: 'text', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'comment')]
|
||||
private ?string $comment = null;
|
||||
/**
|
||||
* Whether this activity is visible and can be used for timesheets
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.visible", type="boolean")
|
||||
*
|
||||
* @ORM\Column(name="visible", type="boolean", nullable=false, options={"default": true})
|
||||
* @Assert\NotNull()
|
||||
* Whether this activity is visible and can be selected
|
||||
*/
|
||||
private $visible = true;
|
||||
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => true])]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'visible', type: 'boolean')]
|
||||
private bool $visible = true;
|
||||
#[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'billable', type: 'boolean')]
|
||||
private bool $billable = true;
|
||||
/**
|
||||
* @var bool
|
||||
* Meta fields registered with the activity
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.billable", type="boolean")
|
||||
*
|
||||
* @ORM\Column(name="billable", type="boolean", nullable=false, options={"default": true})
|
||||
* @Assert\NotNull()
|
||||
* @var Collection<ActivityMeta>
|
||||
*/
|
||||
private $billable = true;
|
||||
#[ORM\OneToMany(targetEntity: 'App\Entity\ActivityMeta', mappedBy: 'activity', cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Activity'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\ActivityMeta>')]
|
||||
#[Serializer\SerializedName('metaFields')]
|
||||
#[Serializer\Accessor(getter: 'getVisibleMetaFields')]
|
||||
private Collection $meta;
|
||||
/**
|
||||
* Meta fields
|
||||
* Teams with access to the activity
|
||||
*
|
||||
* All visible meta (custom) fields registered with this activity
|
||||
*
|
||||
* @var ActivityMeta[]|Collection
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity"})
|
||||
* @Serializer\Type(name="array<App\Entity\ActivityMeta>")
|
||||
* @Serializer\SerializedName("metaFields")
|
||||
* @Serializer\Accessor(getter="getVisibleMetaFields")
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\ActivityMeta", mappedBy="activity", cascade={"persist"})
|
||||
* @var Collection<Team>
|
||||
*/
|
||||
private $meta;
|
||||
/**
|
||||
* Teams
|
||||
*
|
||||
* If no team is assigned, everyone can access the activity
|
||||
*
|
||||
* @var Team[]|ArrayCollection
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity"})
|
||||
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Team"))
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Team", cascade={"persist"}, inversedBy="activities")
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_activities_teams",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="activity_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
private $teams;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="invoice_text", type="text", nullable=true)
|
||||
*/
|
||||
private $invoiceText;
|
||||
#[ORM\ManyToMany(targetEntity: 'App\Entity\Team', cascade: ['persist'], inversedBy: 'activities')]
|
||||
#[ORM\JoinTable(name: 'kimai2_activities_teams')]
|
||||
#[ORM\JoinColumn(name: 'activity_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Activity'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
private Collection $teams;
|
||||
#[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
|
||||
private ?string $invoiceText = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -289,20 +218,6 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool|int|string|null
|
||||
*/
|
||||
public function getMetaFieldValue(string $name)
|
||||
{
|
||||
$field = $this->getMetaField($name);
|
||||
if ($field === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $field->getValue();
|
||||
}
|
||||
|
||||
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||
{
|
||||
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||
@@ -354,10 +269,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
||||
$this->invoiceText = $invoiceText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
@@ -15,27 +13,19 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_activities_meta",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"activity_id", "name"})
|
||||
* }
|
||||
* )
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_activities_meta')]
|
||||
#[ORM\UniqueConstraint(columns: ['activity_id', 'name'])]
|
||||
#[ORM\Entity]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class ActivityMeta implements MetaTableTypeInterface
|
||||
{
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Activity
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Activity", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $activity;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Activity', inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?Activity $activity = null;
|
||||
|
||||
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||
{
|
||||
|
||||
@@ -14,31 +14,20 @@ use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_activities_rates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "activity_id"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ActivityRateRepository")
|
||||
* @UniqueEntity({"user", "activity"}, ignoreNull=false)
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_activities_rates')]
|
||||
#[ORM\UniqueConstraint(columns: ['user_id', 'activity_id'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\ActivityRateRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity(['user', 'activity'], ignoreNull: false)]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class ActivityRate implements RateInterface
|
||||
{
|
||||
use Rate;
|
||||
|
||||
/**
|
||||
* @var Activity
|
||||
*
|
||||
* @Serializer\Exclude()
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Activity")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $activity;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Activity')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?Activity $activity = null;
|
||||
|
||||
public function setActivity(?Activity $activity): ActivityRate
|
||||
{
|
||||
|
||||
@@ -13,54 +13,35 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_bookmarks",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "name"})
|
||||
* }
|
||||
* )
|
||||
* @UniqueEntity(fields={"user", "name"})
|
||||
* @ORM\Entity(repositoryClass="App\Repository\BookmarkRepository")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_bookmarks')]
|
||||
#[ORM\UniqueConstraint(columns: ['user_id', 'name'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\BookmarkRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity(fields: ['user', 'name'])]
|
||||
class Bookmark
|
||||
{
|
||||
public const SEARCH_DEFAULT = 'search-default';
|
||||
public const COLUMN_VISIBILITY = 'column-visibility';
|
||||
public const COLUMN_VISIBILITY = 'columns';
|
||||
public const TIMESHEET = 'timesheet';
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var User
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(name="type", type="string", length=20, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=20, allowEmptyString=false)
|
||||
*/
|
||||
private $type;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(name="name", type="string", length=50, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=50, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(name="content", type="text", nullable=false)
|
||||
*/
|
||||
private $content;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'type', type: 'string', length: 20, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 2, max: 20)]
|
||||
private ?string $type = null;
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 2, max: 50)]
|
||||
private ?string $name = null;
|
||||
#[ORM\Column(name: 'content', type: 'text', nullable: false)]
|
||||
private ?string $content = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
|
||||
@@ -17,50 +17,35 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
trait BudgetTrait
|
||||
{
|
||||
/**
|
||||
* The total monetary budget, will be zero if unconfigured.
|
||||
*
|
||||
* @var float
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity_Entity", "Project_Entity", "Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.budget", type="float")
|
||||
*
|
||||
* @ORM\Column(name="budget", type="float", nullable=false)
|
||||
* @Assert\Range(min=0.00, max=900000000000.00)
|
||||
* @Assert\NotNull()
|
||||
* The total monetary budget, will be zero if not configured.
|
||||
*/
|
||||
private $budget = 0.00;
|
||||
#[ORM\Column(name: 'budget', type: 'float', nullable: false)]
|
||||
#[Assert\Range(min: 0.00, max: 900000000000.00)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Activity_Entity', 'Project_Entity', 'Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'budget', type: 'float')]
|
||||
private float $budget = 0.00;
|
||||
/**
|
||||
* The time budget in seconds, will be zero if unconfigured.
|
||||
*
|
||||
* @var int
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity_Entity", "Project_Entity", "Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.timeBudget", type="duration")
|
||||
*
|
||||
* @ORM\Column(name="time_budget", type="integer", nullable=false)
|
||||
* @Assert\Range(min=0, max=2145600000)
|
||||
* @Assert\NotNull()
|
||||
* The time budget in seconds, will be zero if not configured.
|
||||
*/
|
||||
private $timeBudget = 0;
|
||||
#[ORM\Column(name: 'time_budget', type: 'integer', nullable: false)]
|
||||
#[Assert\Range(min: 0, max: 2145600000)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Activity_Entity', 'Project_Entity', 'Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'timeBudget', type: 'duration')]
|
||||
private int $timeBudget = 0;
|
||||
/**
|
||||
* The type of budget:
|
||||
* - null = default / full time
|
||||
* - month = monthly budget
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Activity_Entity", "Project_Entity", "Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.budgetType")
|
||||
*
|
||||
* @ORM\Column(name="budget_type", type="string", length=10, nullable=true)
|
||||
*/
|
||||
private $budgetType;
|
||||
#[ORM\Column(name: 'budget_type', type: 'string', length: 10, nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Activity_Entity', 'Project_Entity', 'Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'budgetType')]
|
||||
private ?string $budgetType = null;
|
||||
|
||||
public function setBudget(float $budget): void
|
||||
{
|
||||
|
||||
@@ -18,19 +18,14 @@ use JMS\Serializer\Annotation as Serializer;
|
||||
trait ColorTrait
|
||||
{
|
||||
/**
|
||||
* The assigned color in HTML hex format, eg. #dd1d00
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.color")
|
||||
*
|
||||
* @ORM\Column(name="color", type="string", length=7, nullable=true)
|
||||
* @Constraints\HexColor()
|
||||
* The assigned color in HTML hex format, e.g. #dd1d00
|
||||
*/
|
||||
private $color = null;
|
||||
#[ORM\Column(name: 'color', type: 'string', length: 7, nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'color')]
|
||||
#[Constraints\HexColor]
|
||||
private ?string $color = null;
|
||||
|
||||
public function getColor(): ?string
|
||||
{
|
||||
|
||||
@@ -14,48 +14,23 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
trait CommentTableTypeTrait
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="message", type="text", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $message;
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $createdBy;
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(name="created_at", type="datetime", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $createdAt;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="pinned", type="boolean", nullable=false, options={"default": false})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $pinned = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTime();
|
||||
}
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\Column(name: 'message', type: 'text', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?string $message = null;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?User $createdBy = null;
|
||||
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?\DateTime $createdAt = null;
|
||||
#[ORM\Column(name: 'pinned', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||
#[Assert\NotNull]
|
||||
private bool $pinned = false;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
|
||||
@@ -13,39 +13,24 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ConfigurationRepository")
|
||||
* @ORM\Table(name="kimai2_configuration",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
* )
|
||||
* @UniqueEntity("name")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_configuration')]
|
||||
#[ORM\UniqueConstraint(columns: ['name'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\ConfigurationRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('name')]
|
||||
class Configuration
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=100, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @Assert\Length(min=2, max=100, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="value", type="text", nullable=true)
|
||||
*/
|
||||
private $value;
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Length(min: 2, max: 100)]
|
||||
private ?string $name = null;
|
||||
#[ORM\Column(name: 'value', type: 'text', length: 65535, nullable: true)]
|
||||
#[Assert\Length(max: 65535)]
|
||||
private ?string $value = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
@@ -72,15 +57,16 @@ class Configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Given $value will not be serialized before its stored, so it should be a scalar type
|
||||
* that can be casted to string.
|
||||
*
|
||||
* @param string|null|int|bool $value
|
||||
* @return Configuration
|
||||
* Given $value will not be serialized before its stored,
|
||||
* so it should be a scalar type that can be casted to string.
|
||||
*/
|
||||
public function setValue($value): Configuration
|
||||
public function setValue(string|int|bool|null $value): Configuration
|
||||
{
|
||||
if (null !== $value) {
|
||||
if (null === $value) {
|
||||
$this->value = null;
|
||||
} elseif ($value === false) {
|
||||
$this->value = '0';
|
||||
} else {
|
||||
$this->value = (string) $value;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,22 +14,17 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_customers",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"visible"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\CustomerRepository")
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*
|
||||
* @Exporter\Order({"id", "name", "company", "number", "vatId", "address", "contact","email", "phone", "mobile", "fax", "homepage", "country", "currency", "timezone", "budget", "timeBudget", "budgetType", "color", "visible", "teams", "comment", "billable"})
|
||||
* @ Exporter\Expose("teams", label="label.team", exp="object.getTeams().toArray()", type="array")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_customers')]
|
||||
#[ORM\Index(columns: ['visible'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\CustomerRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('number')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Exporter\Order(['id', 'name', 'company', 'number', 'vatId', 'address', 'contact', 'email', 'phone', 'mobile', 'fax', 'homepage', 'country', 'currency', 'timezone', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'teams', 'comment', 'billable'])]
|
||||
class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
{
|
||||
public const DEFAULT_CURRENCY = 'EUR';
|
||||
@@ -37,277 +32,162 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
use BudgetTrait;
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.id", type="integer")
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.name")
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=150, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=150, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.number")
|
||||
*
|
||||
* @ORM\Column(name="number", type="string", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
private $number;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.comment")
|
||||
*
|
||||
* @ORM\Column(name="comment", type="text", nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.visible", type="boolean")
|
||||
*
|
||||
* @ORM\Column(name="visible", type="boolean", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $visible = true;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.billable", type="boolean")
|
||||
*
|
||||
* @ORM\Column(name="billable", type="boolean", nullable=false, options={"default": true})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $billable = true;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.company")
|
||||
*
|
||||
* @ORM\Column(name="company", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $company;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.vat_id")
|
||||
*
|
||||
* @ORM\Column(name="vat_id", type="string", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
private $vatId;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.contact")
|
||||
*
|
||||
* @ORM\Column(name="contact", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $contact;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.address")
|
||||
*
|
||||
* @ORM\Column(name="address", type="text", nullable=true)
|
||||
*/
|
||||
private $address;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.country")
|
||||
*
|
||||
* @ORM\Column(name="country", type="string", length=2, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Country()
|
||||
* @Assert\Length(max=2)
|
||||
*/
|
||||
private $country;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.currency")
|
||||
*
|
||||
* @ORM\Column(name="currency", type="string", length=3, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Currency()
|
||||
* @Assert\Length(max=3)
|
||||
*/
|
||||
private $currency = self::DEFAULT_CURRENCY;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.phone")
|
||||
*
|
||||
* @ORM\Column(name="phone", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $phone;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.fax")
|
||||
*
|
||||
* @ORM\Column(name="fax", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $fax;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.mobile")
|
||||
*
|
||||
* @ORM\Column(name="mobile", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $mobile;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 150, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 3, max: 150)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'name')]
|
||||
private ?string $name;
|
||||
#[ORM\Column(name: 'number', type: 'string', length: 50, nullable: true)]
|
||||
#[Assert\Length(max: 50)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'number')]
|
||||
private ?string $number = null;
|
||||
#[ORM\Column(name: 'comment', type: 'text', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'comment')]
|
||||
private ?string $comment = null;
|
||||
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'visible', type: 'boolean')]
|
||||
private bool $visible = true;
|
||||
#[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'billable', type: 'boolean')]
|
||||
private bool $billable = true;
|
||||
#[ORM\Column(name: 'company', type: 'string', length: 100, nullable: true)]
|
||||
#[Assert\Length(max: 100)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'company')]
|
||||
private ?string $company = null;
|
||||
#[ORM\Column(name: 'vat_id', type: 'string', length: 50, nullable: true)]
|
||||
#[Assert\Length(max: 50)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'vat_id')]
|
||||
private ?string $vatId = null;
|
||||
#[ORM\Column(name: 'contact', type: 'string', length: 100, nullable: true)]
|
||||
#[Assert\Length(max: 100)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'contact')]
|
||||
private ?string $contact = null;
|
||||
#[ORM\Column(name: 'address', type: 'text', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'address')]
|
||||
private ?string $address = null;
|
||||
#[ORM\Column(name: 'country', type: 'string', length: 2, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Country]
|
||||
#[Assert\Length(max: 2)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'country')]
|
||||
private ?string $country = null;
|
||||
#[ORM\Column(name: 'currency', type: 'string', length: 3, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Currency]
|
||||
#[Assert\Length(max: 3)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[Exporter\Expose(label: 'currency')]
|
||||
private string $currency = self::DEFAULT_CURRENCY;
|
||||
#[ORM\Column(name: 'phone', type: 'string', length: 30, nullable: true)]
|
||||
#[Assert\Length(max: 30)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'phone')]
|
||||
private ?string $phone = null;
|
||||
#[ORM\Column(name: 'fax', type: 'string', length: 30, nullable: true)]
|
||||
#[Assert\Length(max: 30)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'fax')]
|
||||
private ?string $fax = null;
|
||||
#[ORM\Column(name: 'mobile', type: 'string', length: 30, nullable: true)]
|
||||
#[Assert\Length(max: 30)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'mobile')]
|
||||
private ?string $mobile = null;
|
||||
/**
|
||||
* Customers contact email
|
||||
*
|
||||
* Limited via RFC to 254 chars
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.email")
|
||||
*
|
||||
* @ORM\Column(name="email", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=254)
|
||||
*/
|
||||
private $email;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.homepage")
|
||||
*
|
||||
* @ORM\Column(name="homepage", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $homepage;
|
||||
#[ORM\Column(name: 'email', type: 'string', length: 75, nullable: true)]
|
||||
#[Assert\Length(max: 75)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'email')]
|
||||
private ?string $email = null;
|
||||
#[ORM\Column(name: 'homepage', type: 'string', length: 100, nullable: true)]
|
||||
#[Assert\Length(max: 100)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'homepage')]
|
||||
private ?string $homepage = null;
|
||||
/**
|
||||
* Timezone of begin and end
|
||||
*
|
||||
* Length was determined by a MySQL column via "use mysql;describe time_zone_name;"
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.timezone")
|
||||
*
|
||||
* @ORM\Column(name="timezone", type="string", length=64, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(max=64)
|
||||
*/
|
||||
private $timezone;
|
||||
#[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(max: 64)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'timezone')]
|
||||
private ?string $timezone = null;
|
||||
/**
|
||||
* Meta fields
|
||||
* Meta fields registered with the customer
|
||||
*
|
||||
* All visible meta (custom) fields registered with this customer
|
||||
*
|
||||
* @var CustomerMeta[]|Collection
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer"})
|
||||
* @Serializer\Type(name="array<App\Entity\CustomerMeta>")
|
||||
* @Serializer\SerializedName("metaFields")
|
||||
* @Serializer\Accessor(getter="getVisibleMetaFields")
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\CustomerMeta", mappedBy="customer", cascade={"persist"})
|
||||
* @var Collection<CustomerMeta>
|
||||
*/
|
||||
private $meta;
|
||||
#[ORM\OneToMany(targetEntity: 'App\Entity\CustomerMeta', mappedBy: 'customer', cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\CustomerMeta>')]
|
||||
#[Serializer\SerializedName('metaFields')]
|
||||
#[Serializer\Accessor(getter: 'getVisibleMetaFields')]
|
||||
private Collection $meta;
|
||||
/**
|
||||
* Teams
|
||||
* Teams with access to the customer
|
||||
*
|
||||
* If no team is assigned, everyone can access the customer
|
||||
*
|
||||
* @var Team[]|ArrayCollection
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer"})
|
||||
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Team"))
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Team", cascade={"persist"}, inversedBy="customers")
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_customers_teams",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="customer_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
* @var Collection<Team>
|
||||
*/
|
||||
private $teams;
|
||||
#[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', 'remove'], inversedBy: 'customers')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
private Collection $teams;
|
||||
/**
|
||||
* Default invoice template for this customer
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\InvoiceTemplate')]
|
||||
#[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)]
|
||||
private ?InvoiceTemplate $invoiceTemplate = null;
|
||||
#[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
|
||||
private ?string $invoiceText = null;
|
||||
|
||||
public function __construct()
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->meta = new ArrayCollection();
|
||||
$this->teams = new ArrayCollection();
|
||||
}
|
||||
@@ -317,7 +197,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setName(string $name): Customer
|
||||
public function setName(?string $name): Customer
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
@@ -435,14 +315,14 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
public function setCurrency(?string $currency): Customer
|
||||
public function setCurrency(string $currency): Customer
|
||||
{
|
||||
$this->currency = $currency;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCurrency(): ?string
|
||||
public function getCurrency(): string
|
||||
{
|
||||
return $this->currency;
|
||||
}
|
||||
@@ -519,6 +399,31 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
return $this->timezone;
|
||||
}
|
||||
|
||||
public function hasInvoiceTemplate(): bool
|
||||
{
|
||||
return $this->invoiceTemplate !== null;
|
||||
}
|
||||
|
||||
public function getInvoiceTemplate(): ?InvoiceTemplate
|
||||
{
|
||||
return $this->invoiceTemplate;
|
||||
}
|
||||
|
||||
public function setInvoiceTemplate(?InvoiceTemplate $invoiceTemplate): void
|
||||
{
|
||||
$this->invoiceTemplate = $invoiceTemplate;
|
||||
}
|
||||
|
||||
public function getInvoiceText(): ?string
|
||||
{
|
||||
return $this->invoiceText;
|
||||
}
|
||||
|
||||
public function setInvoiceText(?string $invoiceText): void
|
||||
{
|
||||
$this->invoiceText = $invoiceText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
*/
|
||||
@@ -553,20 +458,6 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool|int|string|null
|
||||
*/
|
||||
public function getMetaFieldValue(string $name)
|
||||
{
|
||||
$field = $this->getMetaField($name);
|
||||
if ($field === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $field->getValue();
|
||||
}
|
||||
|
||||
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||
{
|
||||
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||
@@ -608,10 +499,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
return $this->teams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
@@ -14,35 +12,26 @@ namespace App\Entity;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_customers_comments",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"customer_id"})
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_customers_comments')]
|
||||
#[ORM\Index(columns: ['customer_id'])]
|
||||
#[ORM\Entity]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
class CustomerComment implements CommentInterface
|
||||
{
|
||||
use CommentTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private Customer $customer;
|
||||
|
||||
public function setCustomer(Customer $customer): CustomerComment
|
||||
public function __construct(Customer $customer)
|
||||
{
|
||||
$this->createdAt = new \DateTime();
|
||||
$this->customer = $customer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomer(): ?Customer
|
||||
public function getCustomer(): Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
@@ -15,27 +13,19 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_customers_meta",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"customer_id", "name"})
|
||||
* }
|
||||
* )
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_customers_meta')]
|
||||
#[ORM\UniqueConstraint(columns: ['customer_id', 'name'])]
|
||||
#[ORM\Entity]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class CustomerMeta implements MetaTableTypeInterface
|
||||
{
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Customer', inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?Customer $customer = null;
|
||||
|
||||
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||
{
|
||||
|
||||
@@ -14,31 +14,20 @@ use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_customers_rates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "customer_id"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\CustomerRateRepository")
|
||||
* @UniqueEntity({"user", "customer"}, ignoreNull=false)
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_customers_rates')]
|
||||
#[ORM\UniqueConstraint(columns: ['user_id', 'customer_id'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\CustomerRateRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity(['user', 'customer'], ignoreNull: false)]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class CustomerRate implements RateInterface
|
||||
{
|
||||
use Rate;
|
||||
|
||||
/**
|
||||
* @var Customer
|
||||
*
|
||||
* @Serializer\Exclude()
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?Customer $customer = null;
|
||||
|
||||
public function setCustomer(?Customer $customer): CustomerRate
|
||||
{
|
||||
|
||||
97
src/Entity/ExportableItem.php
Normal file
97
src/Entity/ExportableItem.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
interface ExportableItem
|
||||
{
|
||||
/**
|
||||
* Whether this item was already exported.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isExported(): bool;
|
||||
|
||||
/**
|
||||
* Whether this item should be included in invoices.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isBillable(): bool;
|
||||
|
||||
/**
|
||||
* Returns the named meta field or null.
|
||||
*
|
||||
* @param string $name
|
||||
* @return MetaTableTypeInterface|null
|
||||
*/
|
||||
public function getMetaField(string $name): ?MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Returns all assigned tag names.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTagsAsArray(): array;
|
||||
|
||||
/**
|
||||
* Returns the amount for this item.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getAmount(): float;
|
||||
|
||||
public function getActivity(): ?Activity;
|
||||
|
||||
public function getProject(): ?Project;
|
||||
|
||||
public function getFixedRate(): ?float;
|
||||
|
||||
public function getHourlyRate(): ?float;
|
||||
|
||||
public function getRate(): float;
|
||||
|
||||
public function getInternalRate(): ?float;
|
||||
|
||||
public function getUser(): ?User;
|
||||
|
||||
public function getBegin(): ?\DateTime;
|
||||
|
||||
public function getEnd(): ?\DateTime;
|
||||
|
||||
public function getDuration(): ?int;
|
||||
|
||||
public function getDescription(): ?string;
|
||||
|
||||
/**
|
||||
* @return MetaTableTypeInterface[]
|
||||
*/
|
||||
public function getVisibleMetaFields(): array;
|
||||
|
||||
/**
|
||||
* @return Collection<MetaTableTypeInterface>
|
||||
*/
|
||||
public function getMetaFields(): Collection;
|
||||
|
||||
/**
|
||||
* A name representation for this type of item.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* A name representation for the category of this item.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCategory(): string;
|
||||
}
|
||||
@@ -18,24 +18,19 @@ use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_invoices",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"invoice_number"}),
|
||||
* @ORM\UniqueConstraint(columns={"invoice_filename"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\InvoiceRepository")
|
||||
* @UniqueEntity("invoiceNumber")
|
||||
* @UniqueEntity("invoiceFilename")
|
||||
*
|
||||
* @Exporter\Order({"id", "createdAt", "invoiceNumber", "status", "customer", "subtotal", "total", "tax", "currency", "vat", "dueDays", "dueDate", "paymentDate", "user", "invoiceFilename", "customerNumber", "comment"})
|
||||
* @Exporter\Expose("customer", label="label.customer", exp="object.getCustomer() === null ? null : object.getCustomer().getName()")
|
||||
* @Exporter\Expose("customerNumber", label="label.number", exp="object.getCustomer() === null ? null : object.getCustomer().getNumber()")
|
||||
* @Exporter\Expose("dueDate", label="invoice.due_days", type="datetime", exp="object.getDueDate() === null ? null : object.getDueDate()")
|
||||
* @Exporter\Expose("user", label="label.username", type="string", exp="object.getUser() === null ? null : object.getUser().getDisplayName()")
|
||||
* @Exporter\Expose("paymentDate", label="invoice.payment_date", type="date", exp="object.getPaymentDate() === null ? null : object.getPaymentDate()")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_invoices')]
|
||||
#[ORM\UniqueConstraint(columns: ['invoice_number'])]
|
||||
#[ORM\UniqueConstraint(columns: ['invoice_filename'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\InvoiceRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('invoiceNumber')]
|
||||
#[UniqueEntity('invoiceFilename')]
|
||||
#[Exporter\Order(['id', 'createdAt', 'invoiceNumber', 'status', 'customer', 'subtotal', 'total', 'tax', 'currency', 'vat', 'dueDays', 'dueDate', 'paymentDate', 'user', 'invoiceFilename', 'customerNumber', 'comment'])]
|
||||
#[Exporter\Expose(name: 'customer', label: 'customer', exp: 'object.getCustomer() === null ? null : object.getCustomer().getName()')]
|
||||
#[Exporter\Expose(name: 'customerNumber', label: 'number', exp: 'object.getCustomer() === null ? null : object.getCustomer().getNumber()')]
|
||||
#[Exporter\Expose(name: 'dueDate', label: 'invoice.due_days', type: 'datetime', exp: 'object.getDueDate() === null ? null : object.getDueDate()')]
|
||||
#[Exporter\Expose(name: 'user', label: 'username', type: 'string', exp: 'object.getUser() === null ? null : object.getUser().getDisplayName()')]
|
||||
#[Exporter\Expose(name: 'paymentDate', label: 'invoice.payment_date', type: 'date', exp: 'object.getPaymentDate() === null ? null : object.getPaymentDate()')]
|
||||
class Invoice implements EntityWithMetaFields
|
||||
{
|
||||
public const STATUS_PENDING = 'pending';
|
||||
@@ -44,159 +39,83 @@ class Invoice implements EntityWithMetaFields
|
||||
public const STATUS_NEW = 'new';
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*
|
||||
* @Exporter\Expose(label="label.id", type="integer")
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* Unique invoice ID
|
||||
*/
|
||||
private $id;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
#[Exporter\Expose(label: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\Column(name: 'invoice_number', type: 'string', length: 50, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Exporter\Expose(label: 'invoice.number', type: 'string')]
|
||||
private ?string $invoiceNumber = null;
|
||||
#[ORM\Column(name: 'comment', type: 'text', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer_Entity'])]
|
||||
#[Exporter\Expose(label: 'comment')]
|
||||
private ?string $comment = null;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?Customer $customer = null;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Exporter\Expose(label: 'date', type: 'datetime')]
|
||||
private ?\DateTime $createdAt = null;
|
||||
#[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: false)]
|
||||
private ?string $timezone = null;
|
||||
#[ORM\Column(name: 'total', type: 'float', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Exporter\Expose(label: 'total_rate', type: 'float')]
|
||||
private float $total = 0.00;
|
||||
#[ORM\Column(name: 'tax', type: 'float', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Exporter\Expose(label: 'invoice.tax', type: 'float')]
|
||||
private float $tax = 0.00;
|
||||
#[ORM\Column(name: 'currency', type: 'string', length: 3, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Length(max: 3)]
|
||||
#[Exporter\Expose(label: 'currency', type: 'string')]
|
||||
private ?string $currency = null;
|
||||
#[ORM\Column(name: 'due_days', type: 'integer', length: 3, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Range(min: 0, max: 999)]
|
||||
#[Exporter\Expose(label: 'due_days', type: 'integer')]
|
||||
private int $dueDays = 30;
|
||||
#[ORM\Column(name: 'vat', type: 'float', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Range(min: 0.0, max: 99.99)]
|
||||
#[Exporter\Expose(label: 'tax_rate', type: 'float')]
|
||||
private float $vat = 0.00;
|
||||
#[ORM\Column(name: 'status', type: 'string', length: 20, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Exporter\Expose(label: 'status', type: 'string')]
|
||||
private string $status = self::STATUS_NEW;
|
||||
#[ORM\Column(name: 'invoice_filename', type: 'string', length: 150, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Length(min: 1, max: 150)]
|
||||
#[Exporter\Expose(label: 'file', type: 'string')]
|
||||
private ?string $invoiceFilename = null;
|
||||
private bool $localized = false;
|
||||
#[ORM\Column(name: 'payment_date', type: 'date', nullable: true)]
|
||||
private ?\DateTime $paymentDate = null;
|
||||
/**
|
||||
* @var string
|
||||
* Meta fields registered with the invoice
|
||||
*
|
||||
* @Exporter\Expose(label="invoice.number", type="string")
|
||||
*
|
||||
* @ORM\Column(name="invoice_number", type="string", length=50, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @var Collection<InvoiceMeta>
|
||||
*/
|
||||
private $invoiceNumber;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Customer_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.comment")
|
||||
*
|
||||
* @ORM\Column(name="comment", type="text", nullable=true)
|
||||
*/
|
||||
private $comment;
|
||||
/**
|
||||
* @var Customer
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @Exporter\Expose(label="label.date", type="datetime")
|
||||
*
|
||||
* @ORM\Column(name="created_at", type="datetime", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $createdAt;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="timezone", type="string", length=64, nullable=false)
|
||||
*/
|
||||
private $timezone;
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @Exporter\Expose(label="label.total_rate", type="float")
|
||||
*
|
||||
* @ORM\Column(name="total", type="float", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $total = 0.00;
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @Exporter\Expose(label="invoice.tax", type="float")
|
||||
*
|
||||
* @ORM\Column(name="tax", type="float", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $tax = 0.00;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @Exporter\Expose(label="label.currency", type="string")
|
||||
*
|
||||
* @ORM\Column(name="currency", type="string", length=3, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @Assert\Length(max=3)
|
||||
*/
|
||||
private $currency;
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @Exporter\Expose(label="label.due_days", type="integer")
|
||||
*
|
||||
* @ORM\Column(name="due_days", type="integer", length=3, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @Assert\Range(min = 0, max = 999)
|
||||
*/
|
||||
private $dueDays = 30;
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @Exporter\Expose(label="label.tax_rate", type="float")
|
||||
*
|
||||
* @ORM\Column(name="vat", type="float", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @Assert\Range(min = 0.0, max = 99.99)
|
||||
*/
|
||||
private $vat = 0.00;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @Exporter\Expose(label="label.status", type="string")
|
||||
*
|
||||
* @ORM\Column(name="status", type="string", length=20, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $status = self::STATUS_NEW;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @Exporter\Expose(label="file", type="string")
|
||||
*
|
||||
* @ORM\Column(name="invoice_filename", type="string", length=150, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @Assert\Length(min=1, max=150, allowEmptyString=false)
|
||||
*/
|
||||
private $invoiceFilename;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $localized = false;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*
|
||||
* @ORM\Column(name="payment_date", type="date", nullable=true)
|
||||
*/
|
||||
private $paymentDate;
|
||||
/**
|
||||
* Meta fields
|
||||
*
|
||||
* All visible meta (custom) fields registered with this invoice
|
||||
*
|
||||
* @var InvoiceMeta[]|Collection
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Invoice"})
|
||||
* @Serializer\Type(name="array<App\Entity\InvoiceMeta>")
|
||||
* @Serializer\SerializedName("metaFields")
|
||||
* @Serializer\Accessor(getter="getVisibleMetaFields")
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\InvoiceMeta", mappedBy="invoice", cascade={"persist"})
|
||||
*/
|
||||
private $meta;
|
||||
#[ORM\OneToMany(targetEntity: 'App\Entity\InvoiceMeta', mappedBy: 'invoice', cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Invoice'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\InvoiceMeta>')]
|
||||
#[Serializer\SerializedName('metaFields')]
|
||||
#[Serializer\Accessor(getter: 'getVisibleMetaFields')]
|
||||
private Collection $meta;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -232,7 +151,7 @@ class Invoice implements EntityWithMetaFields
|
||||
{
|
||||
if (!$this->localized) {
|
||||
if (null !== $this->createdAt && null !== $this->timezone) {
|
||||
$this->createdAt->setTimeZone(new \DateTimeZone($this->timezone));
|
||||
$this->createdAt->setTimezone(new \DateTimeZone($this->timezone));
|
||||
}
|
||||
|
||||
$this->localized = true;
|
||||
@@ -376,11 +295,8 @@ class Invoice implements EntityWithMetaFields
|
||||
return $this->invoiceFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Exporter\Expose(label="invoice.subtotal", type="float", name="subtotal")
|
||||
* @return float|null
|
||||
*/
|
||||
public function getSubtotal(): ?float
|
||||
#[Exporter\Expose(label: 'invoice.subtotal', type: 'float', name: 'subtotal')]
|
||||
public function getSubtotal(): float
|
||||
{
|
||||
return $this->total - $this->tax;
|
||||
}
|
||||
@@ -441,20 +357,6 @@ class Invoice implements EntityWithMetaFields
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool|int|string|null
|
||||
*/
|
||||
public function getMetaFieldValue(string $name)
|
||||
{
|
||||
$field = $this->getMetaField($name);
|
||||
if ($field === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $field->getValue();
|
||||
}
|
||||
|
||||
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||
{
|
||||
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||
|
||||
@@ -1,60 +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\Entity;
|
||||
|
||||
final class InvoiceDocument
|
||||
{
|
||||
/**
|
||||
* @var \SplFileInfo
|
||||
*/
|
||||
private $file;
|
||||
|
||||
public function __construct(\SplFileInfo $file)
|
||||
{
|
||||
$this->file = $file;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
$file = $this->file->getFilename();
|
||||
|
||||
return substr($file, 0, strpos($file, '.'));
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return basename($this->getFilename());
|
||||
}
|
||||
|
||||
public function getFilename(): string
|
||||
{
|
||||
$path = $this->file->getRealPath();
|
||||
if ($path === false) {
|
||||
throw new \Exception('Invoice template got deleted from filesystem');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return $this->file->getExtension();
|
||||
}
|
||||
|
||||
public function getLastChange(): int
|
||||
{
|
||||
$modified = $this->file->getMTime();
|
||||
if ($modified === false) {
|
||||
throw new \Exception('Invoice template got deleted from filesystem');
|
||||
}
|
||||
|
||||
return $modified;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
@@ -15,27 +13,19 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_invoices_meta",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"invoice_id", "name"})
|
||||
* }
|
||||
* )
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_invoices_meta')]
|
||||
#[ORM\UniqueConstraint(columns: ['invoice_id', 'name'])]
|
||||
#[ORM\Entity]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class InvoiceMeta implements MetaTableTypeInterface
|
||||
{
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Invoice
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Invoice", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $invoice;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Invoice', inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?Invoice $invoice = null;
|
||||
|
||||
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||
{
|
||||
|
||||
@@ -13,133 +13,62 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass="App\Repository\InvoiceTemplateRepository")
|
||||
* @ORM\Table(name="kimai2_invoice_templates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
* )
|
||||
* @UniqueEntity("name")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_invoice_templates')]
|
||||
#[ORM\UniqueConstraint(columns: ['name'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\InvoiceTemplateRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('name')]
|
||||
class InvoiceTemplate
|
||||
{
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
private ?int $id = null;
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 60, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 1, max: 60)]
|
||||
private ?string $name = null;
|
||||
#[ORM\Column(name: 'title', type: 'string', length: 255, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
private ?string $title = null;
|
||||
#[ORM\Column(name: 'company', type: 'string', length: 255, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
private ?string $company = null;
|
||||
#[ORM\Column(name: 'vat_id', type: 'string', length: 50, nullable: true)]
|
||||
#[Assert\Length(max: 50)]
|
||||
private ?string $vatId = null;
|
||||
#[ORM\Column(name: 'address', type: 'text', nullable: true)]
|
||||
private ?string $address = null;
|
||||
#[ORM\Column(name: 'contact', type: 'text', nullable: true)]
|
||||
private ?string $contact = null;
|
||||
#[ORM\Column(name: 'due_days', type: 'integer', length: 3, nullable: false)]
|
||||
#[Assert\Range(min: 0, max: 999)]
|
||||
private int $dueDays = 30;
|
||||
#[ORM\Column(name: 'vat', type: 'float', nullable: false)]
|
||||
#[Assert\Range(min: 0.0, max: 99.99)]
|
||||
private float $vat = 0.00;
|
||||
#[ORM\Column(name: 'calculator', type: 'string', length: 20, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(max: 20)]
|
||||
private string $calculator = 'default';
|
||||
#[ORM\Column(name: 'number_generator', type: 'string', length: 20, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(max: 20)]
|
||||
private string $numberGenerator = 'default';
|
||||
#[ORM\Column(name: 'renderer', type: 'string', length: 20, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(max: 20)]
|
||||
private string $renderer = 'default';
|
||||
#[ORM\Column(name: 'payment_terms', type: 'text', nullable: true)]
|
||||
private ?string $paymentTerms = null;
|
||||
#[ORM\Column(name: 'payment_details', type: 'text', nullable: true)]
|
||||
private ?string $paymentDetails = null;
|
||||
/**
|
||||
* @var int|null
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* Used for translations and formatting money, numbers, dates and time.
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=60, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=1, max=60, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="title", type="string", length=255, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $title;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="company", type="string", length=255, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
private $company;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="vat_id", type="string", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
private $vatId;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="address", type="text", nullable=true)
|
||||
*/
|
||||
private $address;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="contact", type="text", nullable=true)
|
||||
*/
|
||||
private $contact;
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="due_days", type="integer", length=3, nullable=false)
|
||||
* @Assert\Range(min = 0, max = 999)
|
||||
*/
|
||||
private $dueDays = 30;
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @ORM\Column(name="vat", type="float", nullable=false)
|
||||
* @Assert\Range(min = 0.0, max = 99.99)
|
||||
*/
|
||||
private $vat = 0.00;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="calculator", type="string", length=20, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(max=20)
|
||||
*/
|
||||
private $calculator = 'default';
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="number_generator", type="string", length=20, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(max=20)
|
||||
*/
|
||||
private $numberGenerator = 'default';
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="renderer", type="string", length=20, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(max=20)
|
||||
*/
|
||||
private $renderer = 'default';
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="payment_terms", type="text", nullable=true)
|
||||
*/
|
||||
private $paymentTerms;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="payment_details", type="text", nullable=true)
|
||||
*/
|
||||
private $paymentDetails;
|
||||
/**
|
||||
* Used when rendering HTML templates.
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="decimal_duration", type="boolean", nullable=false, options={"default": false})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $decimalDuration = false;
|
||||
/**
|
||||
* Used for translations and locale dependent number and date formats.
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="language", type="string", length=6, nullable=true)
|
||||
*/
|
||||
private $language;
|
||||
#[ORM\Column(name: 'language', type: 'string', length: 6, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
private ?string $language = 'en';
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
@@ -304,16 +233,12 @@ class InvoiceTemplate
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 2.0
|
||||
*/
|
||||
public function isDecimalDuration(): bool
|
||||
{
|
||||
return $this->decimalDuration;
|
||||
}
|
||||
|
||||
public function setDecimalDuration(bool $decimalDuration): InvoiceTemplate
|
||||
{
|
||||
$this->decimalDuration = $decimalDuration;
|
||||
|
||||
return $this;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getLanguage(): ?string
|
||||
@@ -328,10 +253,7 @@ class InvoiceTemplate
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
@@ -15,99 +15,65 @@ interface MetaTableTypeInterface
|
||||
{
|
||||
/**
|
||||
* Returns the name of this entry.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getName(): ?string;
|
||||
|
||||
/**
|
||||
* Sets the name of this entry.
|
||||
*
|
||||
* @param string $name
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setName(string $name): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* @return int|bool|string|null
|
||||
*/
|
||||
public function getValue();
|
||||
public function getValue(): mixed;
|
||||
|
||||
/**
|
||||
* Value will not be serialized before its stored, so it should be a primitive type.
|
||||
*
|
||||
* @param mixed|null $value
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setValue($value): MetaTableTypeInterface;
|
||||
public function setValue(mixed $value): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Get the linked entity.
|
||||
*
|
||||
* @return EntityWithMetaFields|null
|
||||
*/
|
||||
public function getEntity(): ?EntityWithMetaFields;
|
||||
|
||||
/**
|
||||
* Set the linked entity of this entry.
|
||||
*
|
||||
* @param EntityWithMetaFields $entity
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* This will merge the current object with the values from the given $meta instance.
|
||||
* It should NOT update the name or value, but only the form settings.
|
||||
*
|
||||
* @param MetaTableTypeInterface $meta
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function merge(MetaTableTypeInterface $meta): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Whether this field can be displayed in "public" places like API results or export.
|
||||
*
|
||||
* @param bool $include
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setIsVisible(bool $include): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Whether this field can be displayed in "public" places like API results or export.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isVisible(): bool;
|
||||
|
||||
/**
|
||||
* Whether this field is required to be filled out in the form.
|
||||
*
|
||||
* @param bool $isRequired
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setIsRequired(bool $isRequired): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Whether the form field is required.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isRequired(): bool;
|
||||
|
||||
/**
|
||||
* The form type for this field.
|
||||
* If this method returns null, it will not be shown on the form.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getType(): ?string;
|
||||
|
||||
/**
|
||||
* Sets the form type.
|
||||
*
|
||||
* @param string $type
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setType(string $type): MetaTableTypeInterface;
|
||||
|
||||
@@ -120,9 +86,6 @@ interface MetaTableTypeInterface
|
||||
|
||||
/**
|
||||
* Adds a constraint to the form type.
|
||||
*
|
||||
* @param Constraint $constraint
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function addConstraint(Constraint $constraint): MetaTableTypeInterface;
|
||||
|
||||
@@ -136,23 +99,18 @@ interface MetaTableTypeInterface
|
||||
|
||||
/**
|
||||
* Returns the label shown to the end-user.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLabel(): ?string;
|
||||
|
||||
/**
|
||||
* Sets the label shown to the end-user.
|
||||
*
|
||||
* @param string $label
|
||||
* @return MetaTableTypeInterface
|
||||
* Sets or removes the label shown to the end-user.
|
||||
*/
|
||||
public function setLabel(string $label): MetaTableTypeInterface;
|
||||
public function setLabel(?string $label): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Set an array of options for the FormType.
|
||||
*
|
||||
* @param array $options
|
||||
* @param array<string, mixed> $options
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setOptions(array $options): MetaTableTypeInterface;
|
||||
@@ -160,22 +118,17 @@ interface MetaTableTypeInterface
|
||||
/**
|
||||
* Returns an array with options for the FormType.
|
||||
*
|
||||
* @return array
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOptions(): array;
|
||||
|
||||
/**
|
||||
* Sets the weight order.
|
||||
*
|
||||
* @param int $order
|
||||
* @return MetaTableTypeInterface
|
||||
* Sets the order of this meta-field.
|
||||
*/
|
||||
public function setOrder(int $order): MetaTableTypeInterface;
|
||||
|
||||
/**
|
||||
* Returns the weight order.
|
||||
*
|
||||
* @return int
|
||||
* Returns the order (default: 0).
|
||||
*/
|
||||
public function getOrder(): int;
|
||||
}
|
||||
|
||||
@@ -14,83 +14,53 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
trait MetaTableTypeTrait
|
||||
{
|
||||
/**
|
||||
* @var int|null
|
||||
*
|
||||
* @Serializer\Exclude()
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
*/
|
||||
private $id;
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[Serializer\Exclude]
|
||||
private ?int $id = null;
|
||||
/**
|
||||
* Name of the meta (custom) field
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=50, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @Assert\Length(min=2, max=50, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Length(min: 2, max: 50)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?string $name = null;
|
||||
/**
|
||||
* Value of the meta (custom) field
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="value", type="text", length=65535, nullable=true)
|
||||
* @Assert\Length(max=65535, allowEmptyString=true)
|
||||
* ATTENTION:
|
||||
* This field can be used to temporary hold data in another format (e.g. array) during form transformation,
|
||||
*/
|
||||
private $value;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="visible", type="boolean", nullable=false, options={"default": false})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $visible = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $label;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $required = false;
|
||||
|
||||
#[ORM\Column(name: 'value', type: 'text', length: 65535, nullable: true)]
|
||||
#[Assert\Length(max: 65535)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private mixed $value = null;
|
||||
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||
#[Assert\NotNull]
|
||||
private bool $visible = false;
|
||||
private ?string $label = null;
|
||||
private ?string $type = null;
|
||||
private bool $required = false;
|
||||
/**
|
||||
* @var Constraint[]
|
||||
*/
|
||||
private $constraints = [];
|
||||
|
||||
private array $constraints = [];
|
||||
/**
|
||||
* An array of options for the form element
|
||||
* @var array
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $options = [];
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $order = 0;
|
||||
private array $options = [];
|
||||
private int $order = 0;
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
@@ -108,20 +78,18 @@ trait MetaTableTypeTrait
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|bool|string|null
|
||||
*/
|
||||
public function getValue()
|
||||
public function getValue(): mixed
|
||||
{
|
||||
switch ($this->type) {
|
||||
case YesNoType::class:
|
||||
case CheckboxType::class:
|
||||
return (bool) $this->value;
|
||||
case IntegerType::class:
|
||||
return (int) $this->value;
|
||||
if ($this->value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
return match ($this->type) {
|
||||
YesNoType::class, CheckboxType::class => (\is_string($this->value) || \is_int($this->value)) ? (bool) $this->value : $this->value,
|
||||
IntegerType::class => (\is_string($this->value) || \is_int($this->value)) ? (int) $this->value : $this->value,
|
||||
NumberType::class => (\is_string($this->value) || \is_float($this->value)) ? (float) $this->value : $this->value,
|
||||
default => $this->value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,7 +98,7 @@ trait MetaTableTypeTrait
|
||||
* @param mixed $value
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setValue($value): MetaTableTypeInterface
|
||||
public function setValue(mixed $value): MetaTableTypeInterface
|
||||
{
|
||||
// unchecked checkboxes / false bool would save an empty string in the database
|
||||
// those cannot be searched in the database
|
||||
@@ -138,7 +106,11 @@ trait MetaTableTypeTrait
|
||||
switch ($this->type) {
|
||||
case YesNoType::class:
|
||||
case CheckboxType::class:
|
||||
$value = (int) $value;
|
||||
if (\is_string($value) || \is_bool($value)) {
|
||||
$value = (int) $value;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Failed converting meta-field bool value');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,13 +211,17 @@ trait MetaTableTypeTrait
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function setLabel(string $label): MetaTableTypeInterface
|
||||
public function setLabel(?string $label): MetaTableTypeInterface
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
* @return MetaTableTypeInterface
|
||||
*/
|
||||
public function setOptions(array $options): MetaTableTypeInterface
|
||||
{
|
||||
$this->options = $options;
|
||||
@@ -253,6 +229,9 @@ trait MetaTableTypeTrait
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
|
||||
@@ -15,255 +15,158 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_projects",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"customer_id","visible","name"}),
|
||||
* @ORM\Index(columns={"customer_id","visible","id"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ProjectRepository")
|
||||
* @Constraints\Project
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
* @Serializer\VirtualProperty(
|
||||
* "CustomerName",
|
||||
* exp="object.getCustomer() === null ? null : object.getCustomer().getName()",
|
||||
* options={
|
||||
* @Serializer\SerializedName("parentTitle"),
|
||||
* @Serializer\Type(name="string"),
|
||||
* @Serializer\Groups({"Project"})
|
||||
* }
|
||||
* )
|
||||
* @Serializer\VirtualProperty(
|
||||
* "CustomerAsId",
|
||||
* exp="object.getCustomer() === null ? null : object.getCustomer().getId()",
|
||||
* options={
|
||||
* @Serializer\SerializedName("customer"),
|
||||
* @Serializer\Type(name="integer"),
|
||||
* @Serializer\Groups({"Project", "Team", "Not_Expanded"})
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @Exporter\Order({"id", "name", "customer", "orderNumber", "orderDate", "start", "end", "budget", "timeBudget", "budgetType", "color", "visible", "teams", "comment", "billable"})
|
||||
* @Exporter\Expose("customer", label="label.customer", exp="object.getCustomer() === null ? null : object.getCustomer().getName()")
|
||||
* @ Exporter\Expose("teams", label="label.team", exp="object.getTeams().toArray()", type="array")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_projects')]
|
||||
#[ORM\Index(columns: ['customer_id', 'visible', 'name'])]
|
||||
#[ORM\Index(columns: ['customer_id', 'visible', 'id'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\ProjectRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Serializer\VirtualProperty('CustomerName', exp: 'object.getCustomer() === null ? null : object.getCustomer().getName()', options: [new Serializer\SerializedName('parentTitle'), new Serializer\Type(name: 'string'), new Serializer\Groups(['Project'])])]
|
||||
#[Serializer\VirtualProperty('CustomerAsId', exp: 'object.getCustomer() === null ? null : object.getCustomer().getId()', options: [new Serializer\SerializedName('customer'), new Serializer\Type(name: 'integer'), new Serializer\Groups(['Project', 'Team', 'Not_Expanded'])])]
|
||||
#[Exporter\Order(['id', 'name', 'customer', 'orderNumber', 'orderDate', 'start', 'end', 'budget', 'timeBudget', 'budgetType', 'color', 'visible', 'teams', 'comment', 'billable'])]
|
||||
#[Exporter\Expose(name: 'customer', label: 'customer', exp: 'object.getCustomer() === null ? null : object.getCustomer().getName()')]
|
||||
#[Constraints\Project]
|
||||
class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
{
|
||||
use BudgetTrait;
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* Internal ID
|
||||
*
|
||||
* @var int|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.id", type="integer")
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* Unique Project ID
|
||||
*/
|
||||
private $id;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
/**
|
||||
* Customer for this project
|
||||
*
|
||||
* @var Customer
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Subresource", "Expanded"})
|
||||
* @SWG\Property(ref="#/definitions/Customer")
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $customer;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
#[OA\Property(ref: '#/components/schemas/Customer')]
|
||||
private ?Customer $customer = null;
|
||||
/**
|
||||
* Project name
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.name")
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=150, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @Assert\Length(min=2, max=150, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 150, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Length(min: 3, max: 150)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'name')]
|
||||
private ?string $name = null;
|
||||
/**
|
||||
* Project order number
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.orderNumber")
|
||||
*
|
||||
* @ORM\Column(name="order_number", type="text", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
private $orderNumber;
|
||||
#[ORM\Column(name: 'order_number', type: 'text', length: 50, nullable: true)]
|
||||
#[Assert\Length(max: 50)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Project_Entity'])]
|
||||
#[Exporter\Expose(label: 'orderNumber')]
|
||||
private ?string $orderNumber = null;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project_Entity"})
|
||||
* @Serializer\Type(name="DateTime")
|
||||
* @Serializer\Accessor(getter="getOrderDate")
|
||||
* Order date for the project
|
||||
*
|
||||
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
|
||||
*
|
||||
* @Exporter\Expose(label="label.orderDate", type="datetime")
|
||||
*
|
||||
* @ORM\Column(name="order_date", type="datetime", nullable=true)
|
||||
*/
|
||||
private $orderDate;
|
||||
#[ORM\Column(name: 'order_date', type: 'datetime', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Project_Entity'])]
|
||||
#[Serializer\Type(name: "DateTime<'Y-m-d'>")]
|
||||
#[Serializer\Accessor(getter: 'getOrderDate')]
|
||||
#[Exporter\Expose(label: 'orderDate', type: 'datetime')]
|
||||
private ?\DateTime $orderDate = null;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project"})
|
||||
* @Serializer\Type(name="DateTime")
|
||||
* @Serializer\Accessor(getter="getStart")
|
||||
* Project start date (times before this date cannot be recorded)
|
||||
*
|
||||
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
|
||||
*
|
||||
* @Exporter\Expose(label="label.project_start", type="datetime")
|
||||
*
|
||||
* @ORM\Column(name="start", type="datetime", nullable=true)
|
||||
*/
|
||||
private $start;
|
||||
#[ORM\Column(name: 'start', type: 'datetime', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Project'])]
|
||||
#[Serializer\Type(name: "DateTime<'Y-m-d'>")]
|
||||
#[Serializer\Accessor(getter: 'getStart')]
|
||||
#[Exporter\Expose(label: 'project_start', type: 'datetime')]
|
||||
private ?\DateTime $start = null;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project"})
|
||||
* @Serializer\Type(name="DateTime")
|
||||
* @Serializer\Accessor(getter="getEnd")
|
||||
* Project end time (times after this date cannot be recorded)
|
||||
*
|
||||
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
|
||||
*
|
||||
* @Exporter\Expose(label="label.project_end", type="datetime")
|
||||
*
|
||||
* @ORM\Column(name="end", type="datetime", nullable=true)
|
||||
*/
|
||||
private $end;
|
||||
#[ORM\Column(name: 'end', type: 'datetime', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Project'])]
|
||||
#[Serializer\Type(name: "DateTime<'Y-m-d'>")]
|
||||
#[Serializer\Accessor(getter: 'getEnd')]
|
||||
#[Exporter\Expose(label: 'project_end', type: 'datetime')]
|
||||
private ?\DateTime $end = null;
|
||||
#[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: true)]
|
||||
private ?string $timezone = null;
|
||||
private bool $localized = false;
|
||||
#[ORM\Column(name: 'comment', type: 'text', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'comment')]
|
||||
private ?string $comment = null;
|
||||
/**
|
||||
* @var string|null
|
||||
* @internal used for storing the timezone for "order", "start" and "end" date
|
||||
*
|
||||
* @ORM\Column(name="timezone", type="string", length=64, nullable=true)
|
||||
* If the project is not visible, times cannot be recorded
|
||||
*/
|
||||
private $timezone;
|
||||
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'visible', type: 'boolean')]
|
||||
private bool $visible = true;
|
||||
#[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'billable', type: 'boolean')]
|
||||
private bool $billable = true;
|
||||
/**
|
||||
* @var bool
|
||||
* @internal used for having the localization state of the dates (see $timezone)
|
||||
* Meta fields registered with the project
|
||||
*
|
||||
* @var Collection<ProjectMeta>
|
||||
*/
|
||||
private $localized = false;
|
||||
#[ORM\OneToMany(targetEntity: 'App\Entity\ProjectMeta', mappedBy: 'project', cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Project'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\ProjectMeta>')]
|
||||
#[Serializer\SerializedName('metaFields')]
|
||||
#[Serializer\Accessor(getter: 'getVisibleMetaFields')]
|
||||
private Collection $meta;
|
||||
/**
|
||||
* @var string|null
|
||||
* Teams with access to the project
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.comment")
|
||||
*
|
||||
* @ORM\Column(name="comment", type="text", nullable=true)
|
||||
* @var Collection<Team>
|
||||
*/
|
||||
private $comment;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.visible", type="boolean")
|
||||
*
|
||||
* @ORM\Column(name="visible", type="boolean", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $visible = true;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.billable", type="boolean")
|
||||
*
|
||||
* @ORM\Column(name="billable", type="boolean", nullable=false, options={"default": true})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $billable = true;
|
||||
/**
|
||||
* Meta fields
|
||||
*
|
||||
* All visible meta (custom) fields registered with this project
|
||||
*
|
||||
* @var ProjectMeta[]|Collection
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project"})
|
||||
* @Serializer\Type(name="array<App\Entity\ProjectMeta>")
|
||||
* @Serializer\SerializedName("metaFields")
|
||||
* @Serializer\Accessor(getter="getVisibleMetaFields")
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\ProjectMeta", mappedBy="project", cascade={"persist"})
|
||||
*/
|
||||
private $meta;
|
||||
/**
|
||||
* Teams
|
||||
*
|
||||
* If no team is assigned, everyone can access the project (also depends on the teams of the customer)
|
||||
*
|
||||
* @var Team[]|ArrayCollection
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Project"})
|
||||
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Team"))
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Team", cascade={"persist"}, inversedBy="projects")
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_projects_teams",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="project_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
private $teams;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="invoice_text", type="text", nullable=true)
|
||||
*/
|
||||
private $invoiceText;
|
||||
#[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')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Project'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
private Collection $teams;
|
||||
#[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
|
||||
private ?string $invoiceText = null;
|
||||
/**
|
||||
* Whether this project allows booking of global activities
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="global_activities", type="boolean", nullable=false, options={"default": true})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $globalActivities = true;
|
||||
#[ORM\Column(name: 'global_activities', type: 'boolean', nullable: false, options: ['default' => true])]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private bool $globalActivities = true;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -363,15 +266,15 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
$timezone = new \DateTimeZone($this->timezone);
|
||||
|
||||
if (null !== $this->orderDate) {
|
||||
$this->orderDate->setTimeZone($timezone);
|
||||
$this->orderDate->setTimezone($timezone);
|
||||
}
|
||||
|
||||
if (null !== $this->start) {
|
||||
$this->start->setTimeZone($timezone);
|
||||
$this->start->setTimezone($timezone);
|
||||
}
|
||||
|
||||
if (null !== $this->end) {
|
||||
$this->end->setTimeZone($timezone);
|
||||
$this->end->setTimezone($timezone);
|
||||
}
|
||||
|
||||
$this->localized = true;
|
||||
@@ -475,20 +378,6 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool|int|string|null
|
||||
*/
|
||||
public function getMetaFieldValue(string $name)
|
||||
{
|
||||
$field = $this->getMetaField($name);
|
||||
if ($field === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $field->getValue();
|
||||
}
|
||||
|
||||
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||
{
|
||||
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||
@@ -558,10 +447,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
$this->invoiceText = $invoiceText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
@@ -14,35 +12,26 @@ namespace App\Entity;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_projects_comments",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"project_id"})
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_projects_comments')]
|
||||
#[ORM\Index(columns: ['project_id'])]
|
||||
#[ORM\Entity]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
class ProjectComment implements CommentInterface
|
||||
{
|
||||
use CommentTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Project
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Project')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private Project $project;
|
||||
|
||||
public function setProject(Project $project): ProjectComment
|
||||
public function __construct(Project $project)
|
||||
{
|
||||
$this->createdAt = new \DateTime();
|
||||
$this->project = $project;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProject(): ?Project
|
||||
public function getProject(): Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
@@ -15,27 +13,19 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_projects_meta",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"project_id", "name"})
|
||||
* }
|
||||
* )
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_projects_meta')]
|
||||
#[ORM\UniqueConstraint(columns: ['project_id', 'name'])]
|
||||
#[ORM\Entity]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class ProjectMeta implements MetaTableTypeInterface
|
||||
{
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Project
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Project', inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?Project $project = null;
|
||||
|
||||
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||
{
|
||||
|
||||
@@ -14,31 +14,20 @@ use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_projects_rates",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "project_id"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\ProjectRateRepository")
|
||||
* @UniqueEntity({"user", "project"}, ignoreNull=false)
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_projects_rates')]
|
||||
#[ORM\UniqueConstraint(columns: ['user_id', 'project_id'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\ProjectRateRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity(['user', 'project'], ignoreNull: false)]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class ProjectRate implements RateInterface
|
||||
{
|
||||
use Rate;
|
||||
|
||||
/**
|
||||
* @var Project
|
||||
*
|
||||
* @Serializer\Exclude()
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull
|
||||
*/
|
||||
private $project;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Project')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?Project $project = null;
|
||||
|
||||
public function setProject(?Project $project): ProjectRate
|
||||
{
|
||||
|
||||
@@ -11,62 +11,37 @@ namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
trait Rate
|
||||
{
|
||||
/**
|
||||
* @var int|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var User|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
* @SWG\Property(ref="#/definitions/User")
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=true)
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="rate", type="float", nullable=false)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $rate = 0.00;
|
||||
/**
|
||||
* @var float|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="internal_rate", type="float", nullable=true)
|
||||
*/
|
||||
private $internalRate;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="fixed", type="boolean", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $isFixed = false;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[OA\Property(ref: '#/components/schemas/User')]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'rate', type: 'float', nullable: false)]
|
||||
#[Assert\GreaterThanOrEqual(0)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private float $rate = 0.00;
|
||||
#[ORM\Column(name: 'internal_rate', type: 'float', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?float $internalRate = null;
|
||||
#[ORM\Column(name: 'fixed', type: 'boolean', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private bool $isFixed = false;
|
||||
|
||||
/**
|
||||
* Get entry id, returns null for new entities which were not persisted.
|
||||
|
||||
@@ -13,34 +13,22 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_roles",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(name="roles_name", columns={"name"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\RoleRepository")
|
||||
* @UniqueEntity("name")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_roles')]
|
||||
#[ORM\UniqueConstraint(name: 'roles_name', columns: ['name'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\RoleRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('name')]
|
||||
class Role
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=50, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=5, max=50, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
private ?int $id = null;
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 5, max: 50)]
|
||||
private ?string $name = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
@@ -54,7 +42,7 @@ class Role
|
||||
|
||||
public function setName(string $name): Role
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->name = strtoupper($name);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -13,47 +13,27 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_roles_permissions",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(name="role_permission", columns={"role_id","permission"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\RolePermissionRepository")
|
||||
* @UniqueEntity({"role", "permission"})
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_roles_permissions')]
|
||||
#[ORM\UniqueConstraint(name: 'role_permission', columns: ['role_id', 'permission'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\RolePermissionRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity(['role', 'permission'])]
|
||||
class RolePermission
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var Role
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Role")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $role;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="permission", type="string", length=50, nullable=false)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
private $permission;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @ORM\Column(name="allowed", type="boolean", nullable=false, options={"default": false})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $allowed = false;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Role')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?Role $role = null;
|
||||
#[ORM\Column(name: 'permission', type: 'string', length: 50, nullable: false)]
|
||||
#[Assert\Length(max: 50)]
|
||||
private ?string $permission = null;
|
||||
#[ORM\Column(name: 'allowed', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||
#[Assert\NotNull]
|
||||
private bool $allowed = false;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
|
||||
@@ -10,62 +10,47 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_tags",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\TagRepository")
|
||||
* @UniqueEntity("name")
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_tags')]
|
||||
#[ORM\UniqueConstraint(columns: ['name'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\TagRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('name')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class Tag
|
||||
{
|
||||
/**
|
||||
* The internal ID
|
||||
*
|
||||
* @var int
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* Internal Tag ID
|
||||
*/
|
||||
private $id;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?int $id = null;
|
||||
/**
|
||||
* The tag name
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=100, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=100, allowEmptyString=false, normalizer="trim")
|
||||
* @Assert\Regex(pattern="/,/",match=false,message="Tag name cannot contain comma")
|
||||
*/
|
||||
private $name;
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 2, max: 100, normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: '/,/', match: false, message: 'Tag name cannot contain comma')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?string $name = null;
|
||||
|
||||
use ColorTrait;
|
||||
|
||||
/**
|
||||
* @var Timesheet[]|ArrayCollection
|
||||
*
|
||||
* @Serializer\Exclude()
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Timesheet", mappedBy="tags", fetch="EXTRA_LAZY")
|
||||
* @var Collection<Timesheet>
|
||||
*/
|
||||
private $timesheets;
|
||||
#[ORM\ManyToMany(targetEntity: 'App\Entity\Timesheet', mappedBy: 'tags', fetch: 'EXTRA_LAZY')]
|
||||
private Collection $timesheets;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -89,7 +74,7 @@ class Tag
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function addTimesheet(Timesheet $timesheet)
|
||||
public function addTimesheet(Timesheet $timesheet): void
|
||||
{
|
||||
if ($this->timesheets->contains($timesheet)) {
|
||||
return;
|
||||
@@ -99,7 +84,7 @@ class Tag
|
||||
$timesheet->addTag($this);
|
||||
}
|
||||
|
||||
public function removeTimesheet(Timesheet $timesheet)
|
||||
public function removeTimesheet(Timesheet $timesheet): void
|
||||
{
|
||||
if (!$this->timesheets->contains($timesheet)) {
|
||||
return;
|
||||
@@ -109,10 +94,7 @@ class Tag
|
||||
$timesheet->removeTag($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
@@ -14,103 +14,82 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_teams",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\TeamRepository")
|
||||
* @UniqueEntity("name")
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
* @Constraints\Team
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_teams')]
|
||||
#[ORM\UniqueConstraint(columns: ['name'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\TeamRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('name')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Constraints\Team]
|
||||
class Team
|
||||
{
|
||||
/**
|
||||
* @var int|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?int $id = null;
|
||||
/**
|
||||
* Team name
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=100, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=100, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 2, max: 100)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?string $name = null;
|
||||
/**
|
||||
* All team member (including team leads)
|
||||
*
|
||||
* @var Collection<TeamMember>
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Team_Entity"})
|
||||
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/TeamMember"))
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\TeamMember", mappedBy="team", fetch="LAZY", cascade={"persist"}, orphanRemoval=true)
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\Count(min="1")
|
||||
*/
|
||||
private $members;
|
||||
#[ORM\OneToMany(targetEntity: 'App\Entity\TeamMember', mappedBy: 'team', fetch: 'LAZY', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE')]
|
||||
#[Assert\Count(min: 1)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Team_Entity'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/TeamMember'))]
|
||||
private Collection $members;
|
||||
/**
|
||||
* Customers assigned to the team
|
||||
*
|
||||
* @var Collection<Customer>
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Team_Entity"})
|
||||
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Customer"))
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Customer", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
private $customers;
|
||||
#[ORM\ManyToMany(targetEntity: 'App\Entity\Customer', mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist', 'remove'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Team_Entity'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Customer'))]
|
||||
private Collection $customers;
|
||||
/**
|
||||
* Projects assigned to the team
|
||||
*
|
||||
* @var Collection<Project>
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Team_Entity", "Expanded"})
|
||||
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Project"))
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Project", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
private $projects;
|
||||
#[ORM\ManyToMany(targetEntity: 'App\Entity\Project', mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist', 'remove'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Team_Entity', 'Expanded'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Project'))]
|
||||
private Collection $projects;
|
||||
/**
|
||||
* Activities assigned to the team
|
||||
*
|
||||
* @var Collection<Activity>
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Team_Entity", "Expanded"})
|
||||
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Activity"))
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Activity", mappedBy="teams", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
private $activities;
|
||||
#[ORM\ManyToMany(targetEntity: 'App\Entity\Activity', mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist', 'remove'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Team_Entity', 'Expanded'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Activity'))]
|
||||
private Collection $activities;
|
||||
|
||||
use ColorTrait;
|
||||
|
||||
public function __construct()
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->members = new ArrayCollection();
|
||||
$this->customers = new ArrayCollection();
|
||||
$this->projects = new ArrayCollection();
|
||||
@@ -122,7 +101,7 @@ class Team
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setName(string $name): Team
|
||||
public function setName(?string $name): Team
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
@@ -157,16 +136,16 @@ class Team
|
||||
}
|
||||
|
||||
// when using the API an invalid User ID triggers the validation too late
|
||||
if ($member->getUser() === null) {
|
||||
if (($user = $member->getUser()) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $this->findMemberByUser($member->getUser())) {
|
||||
if (null !== $this->findMemberByUser($user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->members->add($member);
|
||||
$member->getUser()->addMembership($member);
|
||||
$user->addMembership($member);
|
||||
}
|
||||
|
||||
public function hasMember(TeamMember $member): bool
|
||||
@@ -197,28 +176,6 @@ class Team
|
||||
$member->setUser(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* BE AWARE: this property is deprecated and will be removed with 2.0 - teams can have multiple teamleads since 1.15!
|
||||
*
|
||||
* @Serializer\VirtualProperty
|
||||
* @Serializer\SerializedName("teamlead"),
|
||||
* @Serializer\Groups({"Team_Entity"})
|
||||
* @SWG\Property(ref="#/definitions/User")
|
||||
*
|
||||
* @deprecated since 1.15 - will be removed with 2.0
|
||||
* @return User|null
|
||||
*/
|
||||
public function getTeamlead(): ?User
|
||||
{
|
||||
foreach ($this->members as $member) {
|
||||
if ($member->isTeamlead()) {
|
||||
return $member->getUser();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User[]
|
||||
*/
|
||||
@@ -243,15 +200,6 @@ class Team
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.15 - will be removed with 2.0
|
||||
* @param User $teamlead
|
||||
*/
|
||||
public function setTeamlead(User $teamlead): void
|
||||
{
|
||||
$this->addTeamlead($teamlead);
|
||||
}
|
||||
|
||||
public function addTeamlead(User $user): void
|
||||
{
|
||||
if (null !== ($member = $this->findMemberByUser($user))) {
|
||||
@@ -277,8 +225,6 @@ class Team
|
||||
{
|
||||
if (null !== ($member = $this->findMemberByUser($user))) {
|
||||
$member->setTeamlead(false);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,11 +272,6 @@ class Team
|
||||
/**
|
||||
* Returns all users in the team, both teamlead and normal member.
|
||||
*
|
||||
* @Serializer\VirtualProperty
|
||||
* @Serializer\SerializedName("users"),
|
||||
* @Serializer\Groups({"Team_Entity"})
|
||||
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/User"))
|
||||
*
|
||||
* @return User[]
|
||||
*/
|
||||
public function getUsers(): array
|
||||
@@ -348,7 +289,7 @@ class Team
|
||||
return $this->customers->contains($customer);
|
||||
}
|
||||
|
||||
public function addCustomer(Customer $customer)
|
||||
public function addCustomer(Customer $customer): void
|
||||
{
|
||||
if ($this->customers->contains($customer)) {
|
||||
return;
|
||||
@@ -358,7 +299,7 @@ class Team
|
||||
$customer->addTeam($this);
|
||||
}
|
||||
|
||||
public function removeCustomer(Customer $customer)
|
||||
public function removeCustomer(Customer $customer): void
|
||||
{
|
||||
if (!$this->customers->contains($customer)) {
|
||||
return;
|
||||
@@ -381,7 +322,7 @@ class Team
|
||||
return $this->projects->contains($project);
|
||||
}
|
||||
|
||||
public function addProject(Project $project)
|
||||
public function addProject(Project $project): void
|
||||
{
|
||||
if ($this->projects->contains($project)) {
|
||||
return;
|
||||
@@ -391,7 +332,7 @@ class Team
|
||||
$project->addTeam($this);
|
||||
}
|
||||
|
||||
public function removeProject(Project $project)
|
||||
public function removeProject(Project $project): void
|
||||
{
|
||||
if (!$this->projects->contains($project)) {
|
||||
return;
|
||||
@@ -414,7 +355,7 @@ class Team
|
||||
return $this->activities->contains($activity);
|
||||
}
|
||||
|
||||
public function addActivity(Activity $activity)
|
||||
public function addActivity(Activity $activity): void
|
||||
{
|
||||
if ($this->activities->contains($activity)) {
|
||||
return;
|
||||
@@ -424,7 +365,7 @@ class Team
|
||||
$activity->addTeam($this);
|
||||
}
|
||||
|
||||
public function removeActivity(Activity $activity)
|
||||
public function removeActivity(Activity $activity): void
|
||||
{
|
||||
if (!$this->activities->contains($activity)) {
|
||||
return;
|
||||
@@ -442,10 +383,7 @@ class Team
|
||||
return $this->activities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
@@ -11,63 +11,39 @@ namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_users_teams",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "team_id"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_users_teams')]
|
||||
#[ORM\UniqueConstraint(columns: ['user_id', 'team_id'])]
|
||||
#[ORM\Entity]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class TeamMember
|
||||
{
|
||||
/**
|
||||
* @var int|null
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default", "Entity", "Team_Entity"})
|
||||
* @SWG\Property(ref="#/definitions/User")
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="memberships")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var Team
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default", "Entity", "User_Entity"})
|
||||
* @SWG\Property(ref="#/definitions/Team")
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Team", inversedBy="members")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $team;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default", "Entity", "Team_Entity", "User_Entity"})
|
||||
*
|
||||
* @ORM\Column(name="teamlead", type="boolean", nullable=false, options={"default": false})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $teamlead = false;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\User', 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\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default', 'Entity', 'User_Entity'])]
|
||||
#[OA\Property(ref: '#/components/schemas/Team')]
|
||||
private ?Team $team = null;
|
||||
#[ORM\Column(name: 'teamlead', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default', 'Entity', 'Team_Entity', 'User_Entity'])]
|
||||
private bool $teamlead = false;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Export\ExportItemInterface;
|
||||
use App\Validator\Constraints as Constraints;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
@@ -18,64 +17,35 @@ use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Swagger\Annotations as SWG;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_timesheet",
|
||||
* indexes={
|
||||
* @ORM\Index(columns={"user"}),
|
||||
* @ORM\Index(columns={"activity_id"}),
|
||||
* @ORM\Index(columns={"user","start_time"}),
|
||||
* @ORM\Index(columns={"start_time"}),
|
||||
* @ORM\Index(columns={"start_time","end_time"}),
|
||||
* @ORM\Index(columns={"start_time","end_time","user"}),
|
||||
* @ORM\Index(columns={"date_tz","user"}),
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\TimesheetRepository")
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
* @Constraints\Timesheet
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
* @Serializer\VirtualProperty(
|
||||
* "ActivityAsId",
|
||||
* exp="object.getActivity() === null ? null : object.getActivity().getId()",
|
||||
* options={
|
||||
* @Serializer\SerializedName("activity"),
|
||||
* @Serializer\Type(name="integer"),
|
||||
* @Serializer\Groups({"Not_Expanded"})
|
||||
* }
|
||||
* )
|
||||
* @Serializer\VirtualProperty(
|
||||
* "ProjectAsId",
|
||||
* exp="object.getProject() === null ? null : object.getProject().getId()",
|
||||
* options={
|
||||
* @Serializer\SerializedName("project"),
|
||||
* @Serializer\Type(name="integer"),
|
||||
* @Serializer\Groups({"Not_Expanded"})
|
||||
* }
|
||||
* )
|
||||
* @Serializer\VirtualProperty(
|
||||
* "UserAsId",
|
||||
* exp="object.getUser().getId()",
|
||||
* options={
|
||||
* @Serializer\SerializedName("user"),
|
||||
* @Serializer\Type(name="integer"),
|
||||
* @Serializer\Groups({"Default"})
|
||||
* }
|
||||
* )
|
||||
* @Serializer\VirtualProperty(
|
||||
* "TagsAsArray",
|
||||
* exp="object.getTagsAsArray()",
|
||||
* options={
|
||||
* @Serializer\SerializedName("tags"),
|
||||
* @Serializer\Type(name="array<string>"),
|
||||
* @Serializer\Groups({"Default"})
|
||||
* }
|
||||
* )
|
||||
* Internal docs:
|
||||
* - IDX_4F60C6B1415614018D93D649 (for ticktac in v1)
|
||||
*/
|
||||
class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
#[ORM\Table(name: 'kimai2_timesheet')]
|
||||
#[ORM\Index(columns: ['user'], name: 'IDX_4F60C6B18D93D649')]
|
||||
#[ORM\Index(columns: ['activity_id'], name: 'IDX_4F60C6B181C06096')]
|
||||
#[ORM\Index(columns: ['user', 'start_time'], name: 'IDX_4F60C6B18D93D649502DF587')]
|
||||
#[ORM\Index(columns: ['start_time'], name: 'IDX_4F60C6B1502DF587')]
|
||||
#[ORM\Index(columns: ['start_time', 'end_time'], name: 'IDX_4F60C6B1502DF58741561401')]
|
||||
#[ORM\Index(columns: ['start_time', 'end_time', 'user'], name: 'IDX_4F60C6B1502DF587415614018D93D649')]
|
||||
#[ORM\Index(columns: ['end_time', 'user'], name: 'IDX_4F60C6B1415614018D93D649')]
|
||||
#[ORM\Index(columns: ['date_tz', 'user'], name: 'IDX_4F60C6B1BDF467148D93D649')]
|
||||
#[ORM\Index(columns: ['end_time', 'user', 'start_time'], name: 'IDX_TIMESHEET_TICKTAC')]
|
||||
#[ORM\Index(columns: ['user', 'project_id', 'activity_id'], name: 'IDX_TIMESHEET_RECENT_ACTIVITIES')]
|
||||
#[ORM\Index(columns: ['user', 'id', 'duration'], name: 'IDX_TIMESHEET_RESULT_STATS')]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\TimesheetRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Serializer\VirtualProperty('ActivityAsId', exp: 'object.getActivity() === null ? null : object.getActivity().getId()', options: [new Serializer\SerializedName('activity'), new Serializer\Type(name: 'integer'), new Serializer\Groups(['Not_Expanded'])])]
|
||||
#[Serializer\VirtualProperty('ProjectAsId', exp: 'object.getProject() === null ? null : object.getProject().getId()', options: [new Serializer\SerializedName('project'), new Serializer\Type(name: 'integer'), new Serializer\Groups(['Not_Expanded'])])]
|
||||
#[Serializer\VirtualProperty('UserAsId', exp: 'object.getUser().getId()', options: [new Serializer\SerializedName('user'), new Serializer\Type(name: 'integer'), new Serializer\Groups(['Not_Expanded'])])]
|
||||
#[Serializer\VirtualProperty('TagsAsArray', exp: 'object.getTagsAsArray()', options: [new Serializer\SerializedName('tags'), new Serializer\Type(name: 'array<string>'), new Serializer\Groups(['Default'])])]
|
||||
#[Constraints\Timesheet]
|
||||
class Timesheet implements EntityWithMetaFields, ExportableItem
|
||||
{
|
||||
/**
|
||||
* Category: Normal work-time (default category)
|
||||
@@ -104,229 +74,144 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
public const BILLABLE_DEFAULT = 'default';
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* Unique Timesheet ID
|
||||
*/
|
||||
private $id;
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?int $id = null;
|
||||
/**
|
||||
* Reflects the date in the users timezone (not in UTC).
|
||||
* Reflects the date in the user timezone (not in UTC).
|
||||
* This value is automatically set through the begin column and ONLY used in statistic queries.
|
||||
*
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(name="date_tz", type="date", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $date;
|
||||
#[ORM\Column(name: 'date_tz', type: 'date', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?DateTime $date = null;
|
||||
/**
|
||||
* @var DateTime
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
* @Serializer\Type(name="DateTime")
|
||||
* @Serializer\Accessor(getter="getBegin")
|
||||
*
|
||||
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
|
||||
*
|
||||
* @ORM\Column(name="start_time", type="datetime", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $begin;
|
||||
#[ORM\Column(name: 'start_time', type: 'datetime', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'DateTime')]
|
||||
#[Serializer\Accessor(getter: 'getBegin')]
|
||||
private ?DateTime $begin = null;
|
||||
/**
|
||||
* @var DateTime|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
* @Serializer\Type(name="DateTime")
|
||||
* @Serializer\Accessor(getter="getEnd")
|
||||
*
|
||||
* Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
|
||||
*
|
||||
* @ORM\Column(name="end_time", type="datetime", nullable=true)
|
||||
*/
|
||||
private $end;
|
||||
#[ORM\Column(name: 'end_time', type: 'datetime', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Serializer\Type(name: 'DateTime')]
|
||||
#[Serializer\Accessor(getter: 'getEnd')]
|
||||
private ?\DateTime $end = null;
|
||||
/**
|
||||
* @var string
|
||||
* @internal for storing the timezone of "begin" and "end" date
|
||||
*
|
||||
* @ORM\Column(name="timezone", type="string", length=64, nullable=false)
|
||||
* @Assert\Timezone
|
||||
*/
|
||||
private $timezone;
|
||||
#[ORM\Column(name: 'timezone', type: 'string', length: 64, nullable: false)]
|
||||
#[Assert\Timezone]
|
||||
private ?string $timezone = null;
|
||||
/**
|
||||
* @var bool
|
||||
* @internal for storing the localized state of dates (see $timezone)
|
||||
*/
|
||||
private $localized = false;
|
||||
/**
|
||||
* @var int|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="duration", type="integer", nullable=true)
|
||||
*/
|
||||
private $duration = 0;
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User")
|
||||
* @ORM\JoinColumn(name="`user`", referencedColumnName="id", onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var Activity
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Subresource", "Expanded"})
|
||||
* @SWG\Property(ref="#/definitions/ActivityExpanded")
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Activity")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $activity;
|
||||
/**
|
||||
* @var Project
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Subresource", "Expanded"})
|
||||
* @SWG\Property(ref="#/definitions/ProjectExpanded")
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Project")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="description", type="text", nullable=true)
|
||||
*/
|
||||
private $description;
|
||||
/**
|
||||
* @var float
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="rate", type="float", nullable=false)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $rate = 0.00;
|
||||
/**
|
||||
* @var float|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="internal_rate", type="float", nullable=true)
|
||||
*/
|
||||
private $internalRate;
|
||||
/**
|
||||
* @var float|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Entity"})
|
||||
*
|
||||
* @ORM\Column(name="fixed_rate", type="float", nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $fixedRate = null;
|
||||
/**
|
||||
* @var float|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Entity"})
|
||||
*
|
||||
* @ORM\Column(name="hourly_rate", type="float", nullable=true)
|
||||
* @Assert\GreaterThanOrEqual(0)
|
||||
*/
|
||||
private $hourlyRate = null;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="exported", type="boolean", nullable=false, options={"default": false})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $exported = false;
|
||||
/**
|
||||
* @var bool
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="billable", type="boolean", nullable=false, options={"default": true})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $billable = true;
|
||||
private bool $localized = false;
|
||||
#[ORM\Column(name: 'duration', type: 'integer', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?int $duration = 0;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\User')]
|
||||
#[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\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\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
#[OA\Property(ref: '#/components/schemas/ProjectExpanded')]
|
||||
private ?Project $project = null;
|
||||
#[ORM\Column(name: 'description', type: 'text', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?string $description = null;
|
||||
#[ORM\Column(name: 'rate', type: 'float', nullable: false)]
|
||||
#[Assert\GreaterThanOrEqual(0)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private float $rate = 0.00;
|
||||
#[ORM\Column(name: 'internal_rate', type: 'float', nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?float $internalRate = null;
|
||||
#[ORM\Column(name: 'fixed_rate', type: 'float', nullable: true)]
|
||||
#[Assert\GreaterThanOrEqual(0)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Entity'])]
|
||||
private ?float $fixedRate = null;
|
||||
#[ORM\Column(name: 'hourly_rate', type: 'float', nullable: true)]
|
||||
#[Assert\GreaterThanOrEqual(0)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Entity'])]
|
||||
private ?float $hourlyRate = null;
|
||||
#[ORM\Column(name: 'exported', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private bool $exported = false;
|
||||
#[ORM\Column(name: 'billable', type: 'boolean', nullable: false, options: ['default' => true])]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private bool $billable = true;
|
||||
/**
|
||||
* Internal property used to determine whether the billable field should be calculated automatically.
|
||||
* @var string
|
||||
*/
|
||||
private $billableMode = self::BILLABLE_DEFAULT;
|
||||
private string $billableMode = self::BILLABLE_DEFAULT;
|
||||
#[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])]
|
||||
#[Assert\NotNull]
|
||||
private ?string $category = self::WORK;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="category", type="string", length=10, nullable=false, options={"default": "work"})
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $category = self::WORK;
|
||||
/**
|
||||
* @var DateTime|null
|
||||
* @internal used for limiting queries, eg. via API sync
|
||||
*
|
||||
* @Gedmo\Timestampable
|
||||
* @ORM\Column(name="modified_at", type="datetime", nullable=true)
|
||||
*/
|
||||
private $modifiedAt;
|
||||
#[ORM\Column(name: 'modified_at', type: 'datetime', nullable: true)]
|
||||
#[Gedmo\Timestampable]
|
||||
private ?\DateTime $modifiedAt = null;
|
||||
/**
|
||||
* Tags
|
||||
*
|
||||
* @var Tag[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="App\Entity\Tag", inversedBy="timesheets", cascade={"persist"})
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_timesheet_tags",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="timesheet_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="tag_id", referencedColumnName="id", onDelete="CASCADE")
|
||||
* }
|
||||
* )
|
||||
* @Assert\Valid()
|
||||
* @var Collection<Tag>
|
||||
*/
|
||||
private $tags;
|
||||
#[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'])]
|
||||
#[Assert\Valid]
|
||||
private Collection $tags;
|
||||
/**
|
||||
* Meta fields
|
||||
* Meta fields registered with the timesheet
|
||||
*
|
||||
* All visible meta (custom) fields registered with this timesheet
|
||||
*
|
||||
* @var TimesheetMeta[]|Collection
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Timesheet"})
|
||||
* @Serializer\Type(name="array<App\Entity\TimesheetMeta>")
|
||||
* @Serializer\SerializedName("metaFields")
|
||||
* @Serializer\Accessor(getter="getVisibleMetaFields")
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\TimesheetMeta", mappedBy="timesheet", cascade={"persist"})
|
||||
* @var Collection<TimesheetMeta>
|
||||
*/
|
||||
private $meta;
|
||||
#[ORM\OneToMany(targetEntity: 'App\Entity\TimesheetMeta', mappedBy: 'timesheet', cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Timesheet'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\TimesheetMeta>')]
|
||||
#[Serializer\SerializedName('metaFields')]
|
||||
#[Serializer\Accessor(getter: 'getVisibleMetaFields')]
|
||||
private Collection $meta;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -355,11 +240,11 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
}
|
||||
|
||||
if (null !== $this->begin) {
|
||||
$this->begin->setTimeZone(new DateTimeZone($this->timezone));
|
||||
$this->begin->setTimezone(new DateTimeZone($this->timezone));
|
||||
}
|
||||
|
||||
if (null !== $this->end) {
|
||||
$this->end->setTimeZone(new DateTimeZone($this->timezone));
|
||||
$this->end->setTimezone(new DateTimeZone($this->timezone));
|
||||
}
|
||||
|
||||
$this->localized = true;
|
||||
@@ -372,10 +257,6 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
return $this->begin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $begin
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function setBegin(DateTime $begin): Timesheet
|
||||
{
|
||||
$this->begin = $begin;
|
||||
@@ -633,6 +514,11 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
return 'timesheet';
|
||||
}
|
||||
|
||||
public function getAmount(): float
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return $this->category;
|
||||
@@ -651,14 +537,6 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function resetRates(): void
|
||||
{
|
||||
$this->rate = 0.00;
|
||||
$this->internalRate = null;
|
||||
$this->hourlyRate = null;
|
||||
$this->fixedRate = null;
|
||||
}
|
||||
|
||||
public function isBillable(): bool
|
||||
{
|
||||
return $this->billable;
|
||||
@@ -749,20 +627,6 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool|int|string|null
|
||||
*/
|
||||
public function getMetaFieldValue(string $name)
|
||||
{
|
||||
$field = $this->getMetaField($name);
|
||||
if ($field === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $field->getValue();
|
||||
}
|
||||
|
||||
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||
{
|
||||
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
@@ -15,27 +13,19 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_timesheet_meta",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"timesheet_id", "name"})
|
||||
* }
|
||||
* )
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_timesheet_meta')]
|
||||
#[ORM\UniqueConstraint(columns: ['timesheet_id', 'name'])]
|
||||
#[ORM\Entity]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class TimesheetMeta implements MetaTableTypeInterface
|
||||
{
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
/**
|
||||
* @var Timesheet
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\Timesheet", inversedBy="meta")
|
||||
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $timesheet;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\Timesheet', inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private ?Timesheet $timesheet = null;
|
||||
|
||||
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||
{
|
||||
|
||||
@@ -19,55 +19,35 @@ use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Exception;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Swagger\Annotations as SWG;
|
||||
use KevinPapst\TablerBundle\Model\UserInterface as ThemeUserInterface;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
|
||||
use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface;
|
||||
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
|
||||
* @ORM\Table(name="kimai2_users",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"username"}),
|
||||
* @ORM\UniqueConstraint(columns={"email"})
|
||||
* }
|
||||
* )
|
||||
* @UniqueEntity("username")
|
||||
* @UniqueEntity("email")
|
||||
* @Constraints\User(groups={"UserCreate", "Registration", "Default", "Profile"})
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
* @Serializer\VirtualProperty(
|
||||
* "LanguageAsString",
|
||||
* exp="object.getLocale()",
|
||||
* options={
|
||||
* @Serializer\SerializedName("language"),
|
||||
* @Serializer\Type(name="string"),
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
* }
|
||||
* )
|
||||
* @Serializer\VirtualProperty(
|
||||
* "TimezoneAsString",
|
||||
* exp="object.getTimezone()",
|
||||
* options={
|
||||
* @Serializer\SerializedName("timezone"),
|
||||
* @Serializer\Type(name="string"),
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @Exporter\Order({"id", "username", "alias", "title", "email", "last_login", "language", "timezone", "active", "registeredAt", "roles", "teams", "color", "accountNumber"})
|
||||
* @Exporter\Expose("email", label="label.email", exp="object.getEmail()")
|
||||
* @Exporter\Expose("username", label="label.username", exp="object.getUsername()")
|
||||
* @Exporter\Expose("timezone", label="label.timezone", exp="object.getTimezone()")
|
||||
* @Exporter\Expose("language", label="label.language", exp="object.getLanguage()")
|
||||
* @Exporter\Expose("last_login", label="label.lastLogin", exp="object.getLastLogin()", type="datetime")
|
||||
* @Exporter\Expose("roles", label="label.roles", exp="object.getRoles()", type="array")
|
||||
* @ Exporter\Expose("teams", label="label.team", exp="object.getTeams()", type="array")
|
||||
* @Exporter\Expose("active", label="label.active", exp="object.isEnabled()", type="boolean")
|
||||
*/
|
||||
class User implements UserInterface, EquatableInterface, \Serializable
|
||||
#[ORM\Table(name: 'kimai2_users')]
|
||||
#[ORM\UniqueConstraint(columns: ['username'])]
|
||||
#[ORM\UniqueConstraint(columns: ['email'])]
|
||||
#[ORM\Entity(repositoryClass: 'App\Repository\UserRepository')]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[UniqueEntity('username')]
|
||||
#[UniqueEntity('email')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
#[Exporter\Order(['id', 'username', 'alias', 'title', 'email', 'last_login', 'language', 'timezone', 'active', 'registeredAt', 'roles', 'teams', 'color', 'accountNumber'])]
|
||||
#[Exporter\Expose(name: 'email', label: 'email', exp: 'object.getEmail()')]
|
||||
#[Exporter\Expose(name: 'username', label: 'username', exp: 'object.getUserIdentifier()')]
|
||||
#[Exporter\Expose(name: 'timezone', label: 'timezone', exp: 'object.getTimezone()')]
|
||||
#[Exporter\Expose(name: 'language', label: 'language', exp: 'object.getLanguage()')]
|
||||
#[Exporter\Expose(name: 'last_login', label: 'lastLogin', exp: 'object.getLastLogin()', type: 'datetime')]
|
||||
#[Exporter\Expose(name: 'roles', label: 'roles', exp: 'object.getRoles()', type: 'array')]
|
||||
#[Exporter\Expose(name: 'active', label: 'active', exp: 'object.isEnabled()', type: 'boolean')]
|
||||
#[Constraints\User(groups: ['UserCreate', 'Registration', 'Default', 'Profile'])]
|
||||
class User implements UserInterface, EquatableInterface, ThemeUserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
|
||||
{
|
||||
public const ROLE_USER = 'ROLE_USER';
|
||||
public const ROLE_TEAMLEAD = 'ROLE_TEAMLEAD';
|
||||
@@ -82,211 +62,159 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
public const AUTH_LDAP = 'ldap';
|
||||
public const AUTH_SAML = 'saml';
|
||||
|
||||
public const WIZARDS = ['intro', 'profile'];
|
||||
|
||||
/**
|
||||
* Internal ID
|
||||
*
|
||||
* @var int
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.id", type="integer")
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* Unique User ID
|
||||
*/
|
||||
private $id;
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
/**
|
||||
* The user alias will be displayed in the frontend instead of the username
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.alias")
|
||||
*
|
||||
* @ORM\Column(name="alias", type="string", length=60, nullable=true)
|
||||
* @Assert\Length(max=60)
|
||||
*/
|
||||
private $alias;
|
||||
#[ORM\Column(name: 'alias', type: 'string', length: 60, nullable: true)]
|
||||
#[Assert\Length(max: 60)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'alias')]
|
||||
private ?string $alias = null;
|
||||
/**
|
||||
* Registration date for the user
|
||||
*
|
||||
* @var DateTime|null
|
||||
*
|
||||
* @Exporter\Expose(label="profile.registration_date", type="datetime")
|
||||
*
|
||||
* @ORM\Column(name="registration_date", type="datetime", nullable=true)
|
||||
*/
|
||||
private $registeredAt;
|
||||
#[ORM\Column(name: 'registration_date', type: 'datetime', nullable: true)]
|
||||
#[Exporter\Expose(label: 'profile.registration_date', type: 'datetime')]
|
||||
private ?\DateTime $registeredAt = null;
|
||||
/**
|
||||
* An additional title for the user, like the Job position or Department
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.title")
|
||||
*
|
||||
* @ORM\Column(name="title", type="string", length=50, nullable=true)
|
||||
* @Assert\Length(max=50)
|
||||
*/
|
||||
private $title;
|
||||
#[ORM\Column(name: 'title', type: 'string', length: 50, nullable: true)]
|
||||
#[Assert\Length(max: 50)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'title')]
|
||||
private ?string $title = null;
|
||||
/**
|
||||
* URL to the user avatar, will be auto-generated if empty
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
*
|
||||
* @ORM\Column(name="avatar", type="string", length=255, nullable=true)
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $avatar;
|
||||
#[ORM\Column(name: 'avatar', type: 'string', length: 255, nullable: true)]
|
||||
#[Assert\Length(max: 255)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
private ?string $avatar = null;
|
||||
/**
|
||||
* API token (password) for this user
|
||||
*
|
||||
* @var string|null
|
||||
*
|
||||
* @ORM\Column(name="api_token", type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $apiToken;
|
||||
#[ORM\Column(name: 'api_token', type: 'string', length: 255, nullable: true)]
|
||||
private ?string $apiToken = null;
|
||||
/**
|
||||
* @var string|null
|
||||
* @internal to be set via form, must not be persisted
|
||||
*
|
||||
* @Assert\NotBlank(groups={"ApiTokenUpdate"})
|
||||
* @Assert\Length(min="8", max="60", groups={"ApiTokenUpdate"})
|
||||
*/
|
||||
private $plainApiToken;
|
||||
#[Assert\NotBlank(groups: ['ApiTokenUpdate'])]
|
||||
#[Assert\Length(min: 8, max: 60, groups: ['ApiTokenUpdate'])]
|
||||
private ?string $plainApiToken = null;
|
||||
/**
|
||||
* User preferences
|
||||
*
|
||||
* List of preferences for this user, required ones have dedicated fields/methods
|
||||
*
|
||||
* @var Collection<UserPreference>
|
||||
* This Collection can be null for one edge case ONLY:
|
||||
* if a currently logged-in user will be deleted and then refreshed from the session from one of the UserProvider
|
||||
* e.g. see LdapUserProvider::refreshUser() it might crash if $user->getPreferenceValue() is called
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\UserPreference", mappedBy="user", cascade={"persist"})
|
||||
* @var Collection<UserPreference>|null
|
||||
*/
|
||||
private $preferences;
|
||||
#[ORM\OneToMany(targetEntity: 'App\Entity\UserPreference', mappedBy: 'user', cascade: ['persist'])]
|
||||
private ?Collection $preferences = null;
|
||||
/**
|
||||
* List of all team memberships.
|
||||
*
|
||||
* @var Collection<TeamMember>
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
* @SWG\Property(ref="#/definitions/TeamMembership")
|
||||
*
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\TeamMember", mappedBy="user", fetch="LAZY", cascade={"persist"}, orphanRemoval=true)
|
||||
* @ORM\JoinColumn(onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $memberships;
|
||||
#[ORM\OneToMany(targetEntity: 'App\Entity\TeamMember', mappedBy: 'user', fetch: 'LAZY', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[OA\Property(ref: '#/components/schemas/TeamMembership')]
|
||||
private Collection $memberships;
|
||||
/**
|
||||
* The type of authentication used by the user (e.g. "kimai", "ldap", "saml")
|
||||
*
|
||||
* @var string|null
|
||||
* @internal for internal usage only
|
||||
*
|
||||
* @ORM\Column(name="auth", type="string", length=20, nullable=true)
|
||||
* @Assert\Length(max=20)
|
||||
*/
|
||||
private $auth = self::AUTH_INTERNAL;
|
||||
#[ORM\Column(name: 'auth', type: 'string', length: 20, nullable: true)]
|
||||
#[Assert\Length(max: 20)]
|
||||
private ?string $auth = self::AUTH_INTERNAL;
|
||||
/**
|
||||
* This flag will be initialized in UserEnvironmentSubscriber.
|
||||
*
|
||||
* @var bool|null
|
||||
* @internal has no database mapping as the value is calculated from a permission
|
||||
*/
|
||||
private $isAllowedToSeeAllData = null;
|
||||
/**
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @var string
|
||||
* @ORM\Column(name="username", type="string", length=180)
|
||||
* @Assert\NotBlank(groups={"Registration", "UserCreate", "Profile"})
|
||||
* @Assert\Length(min="2", max="60", groups={"Registration", "UserCreate", "Profile"})
|
||||
*/
|
||||
private $username;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(name="email", type="string", length=180)
|
||||
* @Assert\NotBlank(groups={"Registration", "UserCreate", "Profile"})
|
||||
* @Assert\Length(min="2", max="180")
|
||||
* @Assert\Email(groups={"Registration", "UserCreate", "Profile"})
|
||||
*/
|
||||
private $email;
|
||||
/**
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @Exporter\Expose(label="label.account_number")
|
||||
*
|
||||
* @var string|null
|
||||
* @ORM\Column(name="account", type="string", length=30, nullable=true)
|
||||
* @Assert\Length(allowEmptyString=true, max="30", groups={"Registration", "UserCreate", "Profile"})
|
||||
*/
|
||||
private $accountNumber;
|
||||
/**
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @var bool
|
||||
* @ORM\Column(name="enabled", type="boolean")
|
||||
*/
|
||||
private $enabled = false;
|
||||
private ?bool $isAllowedToSeeAllData = null;
|
||||
#[ORM\Column(name: 'username', type: 'string', length: 180, nullable: false)]
|
||||
#[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||
#[Assert\Length(min: 2, max: 60, groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?string $username = null;
|
||||
#[ORM\Column(name: 'email', type: 'string', length: 180, nullable: false)]
|
||||
#[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||
#[Assert\Length(min: 2, max: 180)]
|
||||
#[Assert\Email(groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||
private ?string $email = null;
|
||||
#[ORM\Column(name: 'account', type: 'string', length: 30, nullable: true)]
|
||||
#[Assert\Length(max: 30, groups: ['Registration', 'UserCreate', 'Profile'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'account_number')]
|
||||
private ?string $accountNumber = null;
|
||||
#[ORM\Column(name: 'enabled', type: 'boolean', nullable: false)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private bool $enabled = false;
|
||||
/**
|
||||
* Encrypted password. Must be persisted.
|
||||
*
|
||||
* @var string
|
||||
* @ORM\Column(name="password", type="string")
|
||||
*/
|
||||
private $password;
|
||||
#[ORM\Column(name: 'password', type: 'string', nullable: false)]
|
||||
private ?string $password = null;
|
||||
/**
|
||||
* Plain password. Used for model validation, not persisted.
|
||||
*
|
||||
* TODO make the password rules configurable
|
||||
*
|
||||
* @var string|null
|
||||
* @Assert\NotBlank(groups={"Registration", "PasswordUpdate", "UserCreate"})
|
||||
* @Assert\Length(min="8", max="60", groups={"Registration", "PasswordUpdate", "UserCreate", "ResetPassword", "ChangePassword"})
|
||||
*/
|
||||
private $plainPassword;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
* @ORM\Column(name="last_login", type="datetime", nullable=true)
|
||||
*/
|
||||
private $lastLogin;
|
||||
#[Assert\NotBlank(groups: ['Registration', 'PasswordUpdate', 'UserCreate'])]
|
||||
#[Assert\Length(min: 8, max: 60, groups: ['Registration', 'PasswordUpdate', 'UserCreate', 'ResetPassword', 'ChangePassword'])]
|
||||
private ?string $plainPassword = null;
|
||||
#[ORM\Column(name: 'last_login', type: 'datetime', nullable: true)]
|
||||
private ?DateTime $lastLogin = null;
|
||||
/**
|
||||
* Random string sent to the user email address in order to verify it.
|
||||
*
|
||||
* @var string|null
|
||||
* @ORM\Column(name="confirmation_token", type="string", length=180, unique=true, nullable=true)
|
||||
*/
|
||||
private $confirmationToken;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
* @ORM\Column(name="password_requested_at", type="datetime", nullable=true)
|
||||
*/
|
||||
private $passwordRequestedAt;
|
||||
#[ORM\Column(name: 'confirmation_token', type: 'string', length: 180, unique: true, nullable: true)]
|
||||
private ?string $confirmationToken = null;
|
||||
#[ORM\Column(name: 'password_requested_at', type: 'datetime', nullable: true)]
|
||||
private ?\DateTime $passwordRequestedAt = null;
|
||||
/**
|
||||
* List of all role names
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
* @Serializer\Type("array<string>")
|
||||
*
|
||||
* @var array
|
||||
* @ORM\Column(name="roles", type="array")
|
||||
* @Constraints\Role(groups={"RolesUpdate"})
|
||||
*/
|
||||
private $roles = [];
|
||||
#[ORM\Column(name: 'roles', type: 'array', nullable: false)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[Serializer\Type('array<string>')]
|
||||
#[Constraints\Role(groups: ['RolesUpdate'])]
|
||||
private array $roles = [];
|
||||
/**
|
||||
* If not empty two-factor authentication is enabled.
|
||||
*/
|
||||
#[ORM\Column(name: 'totp_secret', type: 'string', nullable: true)]
|
||||
private ?string $totpSecret;
|
||||
#[ORM\Column(name: 'totp_enabled', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||
private bool $totpEnabled = false;
|
||||
#[ORM\Column(name: 'system_account', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||
private bool $systemAccount = false;
|
||||
|
||||
use ColorTrait;
|
||||
|
||||
@@ -377,14 +305,13 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
/**
|
||||
* Read-only list of of all visible user preferences.
|
||||
*
|
||||
* @Serializer\VirtualProperty
|
||||
* @Serializer\SerializedName("preferences"),
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/UserPreference"))
|
||||
*
|
||||
* @internal only for API usage
|
||||
* @return UserPreference[]
|
||||
*/
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('preferences')]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/UserPreference'))]
|
||||
public function getVisiblePreferences(): array
|
||||
{
|
||||
// hide all internal preferences, which are either available in other fields
|
||||
@@ -393,14 +320,13 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
UserPreference::TIMEZONE,
|
||||
UserPreference::LOCALE,
|
||||
UserPreference::SKIN,
|
||||
'calendar.initial_view',
|
||||
'login.initial_view',
|
||||
'reporting.initial_view',
|
||||
'theme.collapsed_sidebar',
|
||||
'theme.layout',
|
||||
'theme.update_browser_title',
|
||||
'timesheet.daily_stats',
|
||||
'timesheet.export_decimal',
|
||||
'calendar_initial_view',
|
||||
'login_initial_view',
|
||||
'collapsed_sidebar', // TODO @2.1 removed with 2.0, can be deleted with 2.1
|
||||
'layout', // TODO @2.1 removed with 2.0, can be deleted with 2.1
|
||||
'update_browser_title',
|
||||
'daily_stats',
|
||||
'export_decimal',
|
||||
];
|
||||
|
||||
$all = [];
|
||||
@@ -438,15 +364,14 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param bool|int|string|null $value
|
||||
* @param bool|int|string|float|null $value
|
||||
*/
|
||||
public function setPreferenceValue(string $name, $value = null)
|
||||
{
|
||||
$pref = $this->getPreference($name);
|
||||
|
||||
if (null === $pref) {
|
||||
$pref = new UserPreference();
|
||||
$pref->setName($name);
|
||||
$pref = new UserPreference($name);
|
||||
$this->addPreference($pref);
|
||||
}
|
||||
|
||||
@@ -455,14 +380,12 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
|
||||
public function getPreference(string $name): ?UserPreference
|
||||
{
|
||||
// this code will be triggered, if a currently logged-in user will be deleted and then refreshed from the session
|
||||
// via one of the UserProvider - e.g. see LdapUserProvider::refreshUser() which calls $user->getPreferenceValue()
|
||||
if ($this->preferences === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($this->preferences as $preference) {
|
||||
if ($preference->getName() === $name) {
|
||||
if ($preference->matches($name)) {
|
||||
return $preference;
|
||||
}
|
||||
}
|
||||
@@ -470,25 +393,19 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getTimeFormat(): string
|
||||
{
|
||||
if ($this->is24Hour()) {
|
||||
return 'H:i';
|
||||
}
|
||||
|
||||
return 'h:i A';
|
||||
}
|
||||
|
||||
public function is24Hour(): bool
|
||||
{
|
||||
return (bool) $this->getPreferenceValue(UserPreference::HOUR_24, true, false);
|
||||
}
|
||||
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('language')]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[OA\Property(type: 'string')]
|
||||
public function getLocale(): string
|
||||
{
|
||||
return $this->getPreferenceValue(UserPreference::LOCALE, User::DEFAULT_LANGUAGE, false);
|
||||
}
|
||||
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('timezone')]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[OA\Property(type: 'string')]
|
||||
public function getTimezone(): string
|
||||
{
|
||||
return $this->getPreferenceValue(UserPreference::TIMEZONE, date_default_timezone_get(), false);
|
||||
@@ -517,14 +434,9 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
return $this->getPreferenceValue(UserPreference::FIRST_WEEKDAY, User::DEFAULT_FIRST_WEEKDAY, false);
|
||||
}
|
||||
|
||||
public function isSmallLayout(): bool
|
||||
{
|
||||
return $this->getPreferenceValue('theme.layout', 'fixed', false) === 'boxed';
|
||||
}
|
||||
|
||||
public function isExportDecimal(): bool
|
||||
{
|
||||
return (bool) $this->getPreferenceValue('timesheet.export_decimal', false, false);
|
||||
return (bool) $this->getPreferenceValue('export_decimal', false, false);
|
||||
}
|
||||
|
||||
public function setTimezone(?string $timezone)
|
||||
@@ -537,11 +449,11 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $default
|
||||
* @param bool|int|float|string|null $default
|
||||
* @param bool $allowNull
|
||||
* @return bool|int|string|null
|
||||
* @return bool|int|float|string|null
|
||||
*/
|
||||
public function getPreferenceValue(string $name, $default = null, bool $allowNull = true)
|
||||
public function getPreferenceValue(string $name, mixed $default = null, bool $allowNull = true): bool|int|float|string|null
|
||||
{
|
||||
$preference = $this->getPreference($name);
|
||||
if (null === $preference) {
|
||||
@@ -553,15 +465,6 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
return $allowNull ? $value : ($value ?? $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return bool|int|string|null
|
||||
*/
|
||||
public function getMetaFieldValue(string $name)
|
||||
{
|
||||
return $this->getPreferenceValue($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserPreference $preference
|
||||
* @return User
|
||||
@@ -593,16 +496,16 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
}
|
||||
|
||||
// when using the API an invalid Team ID triggers the validation too late
|
||||
if ($member->getTeam() === null) {
|
||||
if (($team = $member->getTeam()) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $this->findMemberByTeam($member->getTeam())) {
|
||||
if (null !== $this->findMemberByTeam($team)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->memberships->add($member);
|
||||
$member->getTeam()->addMember($member);
|
||||
$team->addMember($member);
|
||||
}
|
||||
|
||||
private function findMemberByTeam(Team $team): ?TeamMember
|
||||
@@ -688,13 +591,12 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
/**
|
||||
* List of all teams, this user is part of
|
||||
*
|
||||
* @Serializer\VirtualProperty
|
||||
* @Serializer\SerializedName("teams"),
|
||||
* @Serializer\Groups({"User_Entity"})
|
||||
* @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Team"))
|
||||
*
|
||||
* @return Team[]
|
||||
*/
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('teams')]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
public function getTeams(): iterable
|
||||
{
|
||||
$teams = [];
|
||||
@@ -802,7 +704,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
return $this->getAlias();
|
||||
}
|
||||
|
||||
return $this->getUsername();
|
||||
return $this->getUserIdentifier();
|
||||
}
|
||||
|
||||
public function getAuth(): ?string
|
||||
@@ -846,35 +748,49 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function eraseCredentials()
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
$this->plainPassword = null;
|
||||
$this->plainApiToken = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getUsername()
|
||||
public function hasUsername(): bool
|
||||
{
|
||||
return $this->username !== null;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @internal only here to satisfy the theme interface
|
||||
*/
|
||||
public function getEmail()
|
||||
public function getIdentifier(): string
|
||||
{
|
||||
return $this->getUsername();
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return $this->getUsername();
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function hasEmail(): bool
|
||||
{
|
||||
return $this->email !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPassword()
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
@@ -888,7 +804,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
{
|
||||
if ($this->lastLogin !== null) {
|
||||
// make sure to use the users own timezone
|
||||
$this->lastLogin->setTimeZone(new \DateTimeZone($this->getTimezone()));
|
||||
$this->lastLogin->setTimezone(new \DateTimeZone($this->getTimezone()));
|
||||
}
|
||||
|
||||
return $this->lastLogin;
|
||||
@@ -902,7 +818,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRoles()
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = $this->roles;
|
||||
|
||||
@@ -941,14 +857,17 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUsername($username): User
|
||||
public function setUsername(string $username): void
|
||||
{
|
||||
$this->username = $username;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEmail($email): User
|
||||
public function setUserIdentifier(string $identifier): void
|
||||
{
|
||||
$this->setUsername($identifier);
|
||||
}
|
||||
|
||||
public function setEmail(?string $email): User
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
@@ -1016,7 +935,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
{
|
||||
$date = $this->getPasswordRequestedAt();
|
||||
|
||||
if ($date === null || !($date instanceof DateTime)) {
|
||||
if (!($date instanceof \DateTimeInterface)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1034,7 +953,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isEqualTo(UserInterface $user)
|
||||
public function isEqualTo(UserInterface $user): bool
|
||||
{
|
||||
if (!$user instanceof self) {
|
||||
return false;
|
||||
@@ -1044,7 +963,7 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->username !== $user->getUsername()) {
|
||||
if ($this->username !== $user->getUserIdentifier()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1062,20 +981,6 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
return serialize([
|
||||
$this->password,
|
||||
$this->username,
|
||||
$this->enabled,
|
||||
$this->id,
|
||||
$this->email,
|
||||
]);
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
if (!\array_key_exists('id', $data)) {
|
||||
@@ -1088,43 +993,15 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
$this->password = $data['password'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function unserialize($serialized)
|
||||
{
|
||||
$data = unserialize($serialized);
|
||||
|
||||
// unserialize a user object from <= 1.14
|
||||
if (8 === \count($data)) {
|
||||
unset($data[1], $data[2], $data[7]);
|
||||
$data = array_values($data);
|
||||
}
|
||||
|
||||
list(
|
||||
$this->password,
|
||||
$this->username,
|
||||
$this->enabled,
|
||||
$this->id,
|
||||
$this->email) = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSalt()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getDisplayName();
|
||||
}
|
||||
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('initials')]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[OA\Property(type: 'string')]
|
||||
public function getInitials(): string
|
||||
{
|
||||
$length = 2;
|
||||
@@ -1173,6 +1050,83 @@ class User implements UserInterface, EquatableInterface, \Serializable
|
||||
|
||||
public function isSystemAccount(): bool
|
||||
{
|
||||
return $this->systemAccount;
|
||||
}
|
||||
|
||||
public function setSystemAccount(bool $isSystemAccount): void
|
||||
{
|
||||
$this->systemAccount = $isSystemAccount;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->getDisplayName();
|
||||
}
|
||||
|
||||
public function hasSeenWizard(string $wizard): bool
|
||||
{
|
||||
$wizards = $this->getPreferenceValue('__wizards__');
|
||||
|
||||
if (\is_string($wizards)) {
|
||||
$wizards = explode(',', $wizards);
|
||||
|
||||
return \in_array($wizard, $wizards);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function setWizardAsSeen(string $wizard): void
|
||||
{
|
||||
$wizards = $this->getPreferenceValue('__wizards__');
|
||||
$values = [];
|
||||
|
||||
if (\is_string($wizards)) {
|
||||
$values = explode(',', $wizards);
|
||||
}
|
||||
|
||||
if (\in_array($wizard, $values)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$values[] = $wizard;
|
||||
$this->setPreferenceValue('__wizards__', implode(',', array_filter($values)));
|
||||
}
|
||||
|
||||
// --------------- 2 Factor Authentication ---------------
|
||||
|
||||
public function setTotpSecret(?string $secret): void
|
||||
{
|
||||
$this->totpSecret = $secret;
|
||||
}
|
||||
|
||||
public function hasTotpSecret(): bool
|
||||
{
|
||||
return $this->totpSecret !== null;
|
||||
}
|
||||
|
||||
public function isTotpAuthenticationEnabled(): bool
|
||||
{
|
||||
return $this->totpEnabled;
|
||||
}
|
||||
|
||||
public function enableTotpAuthentication(): void
|
||||
{
|
||||
$this->totpEnabled = true;
|
||||
}
|
||||
|
||||
public function disableTotpAuthentication(): void
|
||||
{
|
||||
$this->totpEnabled = false;
|
||||
}
|
||||
|
||||
public function getTotpAuthenticationUsername(): string
|
||||
{
|
||||
return $this->getUserIdentifier();
|
||||
}
|
||||
|
||||
public function getTotpAuthenticationConfiguration(): TotpConfigurationInterface
|
||||
{
|
||||
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,90 +14,61 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="kimai2_user_preferences",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"user_id", "name"})
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @Serializer\ExclusionPolicy("all")
|
||||
*/
|
||||
#[ORM\Table(name: 'kimai2_user_preferences')]
|
||||
#[ORM\UniqueConstraint(columns: ['user_id', 'name'])]
|
||||
#[ORM\Entity]
|
||||
#[ORM\ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
#[Serializer\ExclusionPolicy('all')]
|
||||
class UserPreference
|
||||
{
|
||||
public const HOURLY_RATE = 'hourly_rate';
|
||||
public const INTERNAL_RATE = 'internal_rate';
|
||||
public const SKIN = 'skin';
|
||||
public const LOCALE = 'language';
|
||||
public const HOUR_24 = 'hours_24';
|
||||
public const TIMEZONE = 'timezone';
|
||||
public const FIRST_WEEKDAY = 'first_weekday';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="preferences")
|
||||
* @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
|
||||
* @Assert\NotNull()
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=50, nullable=false)
|
||||
* @Assert\NotNull()
|
||||
* @Assert\Length(min=2, max=50, allowEmptyString=false)
|
||||
*/
|
||||
private $name;
|
||||
/**
|
||||
* @var string|null
|
||||
*
|
||||
* @Serializer\Expose()
|
||||
* @Serializer\Groups({"Default"})
|
||||
*
|
||||
* @ORM\Column(name="value", type="string", length=255, nullable=true)
|
||||
*/
|
||||
private $value;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $enabled = true;
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(name: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Entity\User', inversedBy: 'preferences')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 50, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Length(min: 2, max: 50)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private string $name;
|
||||
#[ORM\Column(name: 'value', type: 'string', length: 255, nullable: true)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?string $value;
|
||||
private ?string $type = null;
|
||||
private bool $enabled = true;
|
||||
/**
|
||||
* @var Constraint[]
|
||||
*/
|
||||
private $constraints = [];
|
||||
private array $constraints = [];
|
||||
/**
|
||||
* An array of options for the form element
|
||||
* @var array
|
||||
*/
|
||||
private $options = [];
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $order = 1000;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $section = 'default';
|
||||
private array $options = [];
|
||||
private int $order = 1000;
|
||||
private string $section = 'default';
|
||||
|
||||
public function __construct(string $name, string|int|float|bool|null $value = null)
|
||||
{
|
||||
$this->name = $this->sanitizeName($name);
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
@@ -129,35 +100,32 @@ class UserPreference
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
return $this->sanitizeName($this->name);
|
||||
}
|
||||
|
||||
public function setName(string $name): UserPreference
|
||||
public function matches(string $name): bool
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
return $this->sanitizeName($name) === $this->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValue()
|
||||
public function sanitizeName(?string $name): string
|
||||
{
|
||||
switch ($this->type) {
|
||||
case YesNoType::class:
|
||||
case CheckboxType::class:
|
||||
return (bool) $this->value;
|
||||
case IntegerType::class:
|
||||
return (int) $this->value;
|
||||
}
|
||||
return str_replace(['.', '-'], '_', $name);
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
public function getValue(): bool|int|float|string|null
|
||||
{
|
||||
return match ($this->type) {
|
||||
YesNoType::class, CheckboxType::class => (bool) $this->value,
|
||||
IntegerType::class => (int) $this->value,
|
||||
NumberType::class => (float) $this->value,
|
||||
default => $this->value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given $value will not be serialized before its stored, so it should be one of the types:
|
||||
* integer, string or boolean
|
||||
* integer, float, string, boolean or null
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return UserPreference
|
||||
@@ -228,7 +196,7 @@ class UserPreference
|
||||
/**
|
||||
* @return Constraint[]
|
||||
*/
|
||||
public function getConstraints()
|
||||
public function getConstraints(): array
|
||||
{
|
||||
return $this->constraints;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user