feat(absences) : fondation backend du module de gestion des absences
Module type Payfit (étapes 1+2 de la spec V1) : demande d'absence, validation admin, soldes à jour. - Enums : AbsenceType, AbsenceStatus, HalfDay, ContractType, FamilySituation - Entités : AbsencePolicy, AbsenceBalance, AbsenceRequest + champs RH sur User - Services : PublicHolidayProvider (fériés FR métropole en PHP pur, Computus), AbsenceDayCalculator (décompte jours ouvrés/ouvrables + demi-journées, TDD), AbsenceBalanceService (périodes + pending/taken/recrédit) - API Platform : providers/processors (création, approve/reject/cancel) + RBAC me/admin, contrôleurs preview (dry-run), upload/download justificatif, calendrier - Migrations : une par table + colonnes RH user (DEFAULT puis DROP DEFAULT) - Fixtures : 5 policies par défaut, salariés démo, soldes et demandes - Tests unitaires : PublicHolidayProvider, AbsenceDayCalculator (12 tests) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use App\Enum\AbsenceType;
|
||||
use App\Repository\AbsenceBalanceRepository;
|
||||
use App\State\AbsenceBalanceProvider;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
/**
|
||||
* Per-employee, per-type leave balance for a given reference period.
|
||||
*/
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
paginationEnabled: false,
|
||||
security: "is_granted('ROLE_USER')",
|
||||
provider: AbsenceBalanceProvider::class,
|
||||
),
|
||||
new Get(
|
||||
security: "is_granted('ROLE_USER')",
|
||||
provider: AbsenceBalanceProvider::class,
|
||||
),
|
||||
new Patch(security: "is_granted('ROLE_ADMIN')"),
|
||||
],
|
||||
normalizationContext: ['groups' => ['absence_balance:read']],
|
||||
denormalizationContext: ['groups' => ['absence_balance:write']],
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: AbsenceBalanceRepository::class)]
|
||||
#[ORM\Table(name: 'absence_balance')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_absence_balance_user_type_period', columns: ['user_id', 'type', 'period'])]
|
||||
class AbsenceBalance
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['absence_balance:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Groups(['absence_balance:read'])]
|
||||
private ?User $user = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 32, enumType: AbsenceType::class)]
|
||||
#[Groups(['absence_balance:read'])]
|
||||
private AbsenceType $type;
|
||||
|
||||
/** Reference period, e.g. "2025-2026" for paid leave or "2025" for yearly. */
|
||||
#[ORM\Column(length: 16)]
|
||||
#[Groups(['absence_balance:read'])]
|
||||
private ?string $period = null;
|
||||
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[Groups(['absence_balance:read', 'absence_balance:write'])]
|
||||
private float $acquired = 0.0;
|
||||
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[Groups(['absence_balance:read', 'absence_balance:write'])]
|
||||
private float $taken = 0.0;
|
||||
|
||||
/** Sum of days in PENDING requests, for information. */
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[Groups(['absence_balance:read'])]
|
||||
private float $pending = 0.0;
|
||||
|
||||
#[Groups(['absence_balance:read'])]
|
||||
public function getAvailable(): float
|
||||
{
|
||||
return $this->acquired - $this->taken;
|
||||
}
|
||||
|
||||
#[Groups(['absence_balance:read'])]
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->type->label();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): static
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): AbsenceType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(AbsenceType $type): static
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPeriod(): ?string
|
||||
{
|
||||
return $this->period;
|
||||
}
|
||||
|
||||
public function setPeriod(string $period): static
|
||||
{
|
||||
$this->period = $period;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAcquired(): float
|
||||
{
|
||||
return $this->acquired;
|
||||
}
|
||||
|
||||
public function setAcquired(float $acquired): static
|
||||
{
|
||||
$this->acquired = $acquired;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTaken(): float
|
||||
{
|
||||
return $this->taken;
|
||||
}
|
||||
|
||||
public function setTaken(float $taken): static
|
||||
{
|
||||
$this->taken = $taken;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPending(): float
|
||||
{
|
||||
return $this->pending;
|
||||
}
|
||||
|
||||
public function setPending(float $pending): static
|
||||
{
|
||||
$this->pending = $pending;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use App\Enum\AbsenceType;
|
||||
use App\Repository\AbsencePolicyRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
/**
|
||||
* Per-type configuration of absence rules. Overrides the legal defaults and
|
||||
* lets an admin tune days/year, days/event, notice period, etc.
|
||||
*/
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
paginationEnabled: false,
|
||||
security: "is_granted('ROLE_USER')",
|
||||
),
|
||||
new Get(security: "is_granted('ROLE_USER')"),
|
||||
new Patch(security: "is_granted('ROLE_ADMIN')"),
|
||||
],
|
||||
normalizationContext: ['groups' => ['absence_policy:read']],
|
||||
denormalizationContext: ['groups' => ['absence_policy:write']],
|
||||
order: ['type' => 'ASC'],
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: AbsencePolicyRepository::class)]
|
||||
#[ORM\Table(name: 'absence_policy')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_absence_policy_type', columns: ['type'])]
|
||||
class AbsencePolicy
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['absence_policy:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 32, enumType: AbsenceType::class)]
|
||||
#[Groups(['absence_policy:read', 'absence_balance:read', 'absence_request:read'])]
|
||||
private AbsenceType $type;
|
||||
|
||||
/** Yearly entitlement (e.g. 25 for paid leave); null when not relevant. */
|
||||
#[ORM\Column(type: Types::FLOAT, nullable: true)]
|
||||
#[Groups(['absence_policy:read', 'absence_policy:write'])]
|
||||
private ?float $daysPerYear = null;
|
||||
|
||||
/** Days granted per event (e.g. 4 for marriage); null when not relevant. */
|
||||
#[ORM\Column(type: Types::FLOAT, nullable: true)]
|
||||
#[Groups(['absence_policy:read', 'absence_policy:write'])]
|
||||
private ?float $daysPerEvent = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['absence_policy:read', 'absence_policy:write'])]
|
||||
private bool $justificationRequired = false;
|
||||
|
||||
/** Minimum notice period in days (e.g. 30 for paid leave, 0 for sick leave). */
|
||||
#[ORM\Column]
|
||||
#[Groups(['absence_policy:read', 'absence_policy:write'])]
|
||||
private int $noticeDays = 0;
|
||||
|
||||
/** true => "jours ouvrés" (Mon-Fri), false => "jours ouvrables" (Mon-Sat). */
|
||||
#[ORM\Column]
|
||||
#[Groups(['absence_policy:read', 'absence_policy:write'])]
|
||||
private bool $countWorkingDaysOnly = true;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['absence_policy:read', 'absence_policy:write'])]
|
||||
private bool $active = true;
|
||||
|
||||
#[Groups(['absence_policy:read'])]
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->type->label();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getType(): AbsenceType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(AbsenceType $type): static
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDaysPerYear(): ?float
|
||||
{
|
||||
return $this->daysPerYear;
|
||||
}
|
||||
|
||||
public function setDaysPerYear(?float $daysPerYear): static
|
||||
{
|
||||
$this->daysPerYear = $daysPerYear;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDaysPerEvent(): ?float
|
||||
{
|
||||
return $this->daysPerEvent;
|
||||
}
|
||||
|
||||
public function setDaysPerEvent(?float $daysPerEvent): static
|
||||
{
|
||||
$this->daysPerEvent = $daysPerEvent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isJustificationRequired(): bool
|
||||
{
|
||||
return $this->justificationRequired;
|
||||
}
|
||||
|
||||
public function setJustificationRequired(bool $justificationRequired): static
|
||||
{
|
||||
$this->justificationRequired = $justificationRequired;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNoticeDays(): int
|
||||
{
|
||||
return $this->noticeDays;
|
||||
}
|
||||
|
||||
public function setNoticeDays(int $noticeDays): static
|
||||
{
|
||||
$this->noticeDays = $noticeDays;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isCountWorkingDaysOnly(): bool
|
||||
{
|
||||
return $this->countWorkingDaysOnly;
|
||||
}
|
||||
|
||||
public function setCountWorkingDaysOnly(bool $countWorkingDaysOnly): static
|
||||
{
|
||||
$this->countWorkingDaysOnly = $countWorkingDaysOnly;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
public function setActive(bool $active): static
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Enum\AbsenceStatus;
|
||||
use App\Enum\AbsenceType;
|
||||
use App\Enum\HalfDay;
|
||||
use App\Repository\AbsenceRequestRepository;
|
||||
use App\State\AbsenceCancelProcessor;
|
||||
use App\State\AbsenceRequestProcessor;
|
||||
use App\State\AbsenceRequestProvider;
|
||||
use App\State\AbsenceReviewProcessor;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
paginationEnabled: false,
|
||||
security: "is_granted('ROLE_USER')",
|
||||
provider: AbsenceRequestProvider::class,
|
||||
),
|
||||
new Get(
|
||||
security: "is_granted('ROLE_USER')",
|
||||
provider: AbsenceRequestProvider::class,
|
||||
),
|
||||
new Post(
|
||||
security: "is_granted('ROLE_USER')",
|
||||
processor: AbsenceRequestProcessor::class,
|
||||
),
|
||||
new Patch(
|
||||
uriTemplate: '/absence_requests/{id}/approve',
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
processor: AbsenceReviewProcessor::class,
|
||||
provider: AbsenceRequestProvider::class,
|
||||
),
|
||||
new Patch(
|
||||
uriTemplate: '/absence_requests/{id}/reject',
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
processor: AbsenceReviewProcessor::class,
|
||||
provider: AbsenceRequestProvider::class,
|
||||
),
|
||||
new Patch(
|
||||
uriTemplate: '/absence_requests/{id}/cancel',
|
||||
security: "is_granted('ROLE_USER')",
|
||||
processor: AbsenceCancelProcessor::class,
|
||||
provider: AbsenceRequestProvider::class,
|
||||
),
|
||||
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
||||
],
|
||||
normalizationContext: ['groups' => ['absence_request:read']],
|
||||
denormalizationContext: ['groups' => ['absence_request:write']],
|
||||
order: ['createdAt' => 'DESC'],
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: AbsenceRequestRepository::class)]
|
||||
#[ORM\Table(name: 'absence_request')]
|
||||
class AbsenceRequest
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['absence_request:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Groups(['absence_request:read'])]
|
||||
private ?User $user = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 32, enumType: AbsenceType::class)]
|
||||
#[Groups(['absence_request:read', 'absence_request:write'])]
|
||||
#[Assert\NotNull]
|
||||
private ?AbsenceType $type = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
#[Groups(['absence_request:read', 'absence_request:write'])]
|
||||
#[Assert\NotNull]
|
||||
private ?DateTimeImmutable $startDate = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
#[Groups(['absence_request:read', 'absence_request:write'])]
|
||||
#[Assert\NotNull]
|
||||
private ?DateTimeImmutable $endDate = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 16, nullable: true, enumType: HalfDay::class)]
|
||||
#[Groups(['absence_request:read', 'absence_request:write'])]
|
||||
private ?HalfDay $startHalfDay = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 16, nullable: true, enumType: HalfDay::class)]
|
||||
#[Groups(['absence_request:read', 'absence_request:write'])]
|
||||
private ?HalfDay $endHalfDay = null;
|
||||
|
||||
/** Number of deducted days, computed server-side at creation. */
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[Groups(['absence_request:read'])]
|
||||
private float $countedDays = 0.0;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
#[Groups(['absence_request:read', 'absence_request:write'])]
|
||||
private ?string $reason = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['absence_request:read'])]
|
||||
private ?string $justificationFileName = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 16, enumType: AbsenceStatus::class)]
|
||||
#[Groups(['absence_request:read'])]
|
||||
private AbsenceStatus $status = AbsenceStatus::Pending;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
#[Groups(['absence_request:read', 'absence_request:write'])]
|
||||
private ?string $rejectionReason = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
#[Groups(['absence_request:read'])]
|
||||
private ?DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
#[Groups(['absence_request:read'])]
|
||||
private ?DateTimeImmutable $reviewedAt = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
#[Groups(['absence_request:read'])]
|
||||
private ?User $reviewedBy = null;
|
||||
|
||||
#[Groups(['absence_request:read'])]
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->type?->label();
|
||||
}
|
||||
|
||||
#[Groups(['absence_request:read'])]
|
||||
public function getJustificationUrl(): ?string
|
||||
{
|
||||
if (null === $this->justificationFileName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return '/api/absence_requests/'.$this->id.'/justificatif';
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(?User $user): static
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): ?AbsenceType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(?AbsenceType $type): static
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStartDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->startDate;
|
||||
}
|
||||
|
||||
public function setStartDate(?DateTimeImmutable $startDate): static
|
||||
{
|
||||
$this->startDate = $startDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEndDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->endDate;
|
||||
}
|
||||
|
||||
public function setEndDate(?DateTimeImmutable $endDate): static
|
||||
{
|
||||
$this->endDate = $endDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStartHalfDay(): ?HalfDay
|
||||
{
|
||||
return $this->startHalfDay;
|
||||
}
|
||||
|
||||
public function setStartHalfDay(?HalfDay $startHalfDay): static
|
||||
{
|
||||
$this->startHalfDay = $startHalfDay;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEndHalfDay(): ?HalfDay
|
||||
{
|
||||
return $this->endHalfDay;
|
||||
}
|
||||
|
||||
public function setEndHalfDay(?HalfDay $endHalfDay): static
|
||||
{
|
||||
$this->endHalfDay = $endHalfDay;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCountedDays(): float
|
||||
{
|
||||
return $this->countedDays;
|
||||
}
|
||||
|
||||
public function setCountedDays(float $countedDays): static
|
||||
{
|
||||
$this->countedDays = $countedDays;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReason(): ?string
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
|
||||
public function setReason(?string $reason): static
|
||||
{
|
||||
$this->reason = $reason;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getJustificationFileName(): ?string
|
||||
{
|
||||
return $this->justificationFileName;
|
||||
}
|
||||
|
||||
public function setJustificationFileName(?string $justificationFileName): static
|
||||
{
|
||||
$this->justificationFileName = $justificationFileName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): AbsenceStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(AbsenceStatus $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRejectionReason(): ?string
|
||||
{
|
||||
return $this->rejectionReason;
|
||||
}
|
||||
|
||||
public function setRejectionReason(?string $rejectionReason): static
|
||||
{
|
||||
$this->rejectionReason = $rejectionReason;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(DateTimeImmutable $createdAt): static
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReviewedAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->reviewedAt;
|
||||
}
|
||||
|
||||
public function setReviewedAt(?DateTimeImmutable $reviewedAt): static
|
||||
{
|
||||
$this->reviewedAt = $reviewedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReviewedBy(): ?User
|
||||
{
|
||||
return $this->reviewedBy;
|
||||
}
|
||||
|
||||
public function setReviewedBy(?User $reviewedBy): static
|
||||
{
|
||||
$this->reviewedBy = $reviewedBy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
+173
-3
@@ -10,6 +10,8 @@ use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Enum\ContractType;
|
||||
use App\Enum\FamilySituation;
|
||||
use App\Repository\UserRepository;
|
||||
use App\State\MeProvider;
|
||||
use App\State\UserPasswordHasherProcessor;
|
||||
@@ -48,11 +50,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['me:read', 'task:read', 'user:list', 'time_entry:read', 'client_ticket:read'])]
|
||||
#[Groups(['me:read', 'task:read', 'user:list', 'time_entry:read', 'client_ticket:read', 'absence_request:read', 'absence_balance:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 180, unique: true)]
|
||||
#[Groups(['me:read', 'task:read', 'user:list', 'user:write', 'time_entry:read', 'client_ticket:read'])]
|
||||
#[Groups(['me:read', 'task:read', 'user:list', 'user:write', 'time_entry:read', 'client_ticket:read', 'absence_request:read', 'absence_balance:read'])]
|
||||
private ?string $username = null;
|
||||
|
||||
/** @var list<string> */
|
||||
@@ -87,6 +89,54 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private Collection $allowedProjects;
|
||||
|
||||
// --- HR / absence management fields ---
|
||||
|
||||
/** Whether this user is an employee subject to absence management. */
|
||||
#[ORM\Column]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private bool $isEmployee = false;
|
||||
|
||||
/** Hiring date — start of paid-leave acquisition. */
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private ?DateTimeImmutable $hireDate = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private ?DateTimeImmutable $endDate = null;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 16, nullable: true, enumType: ContractType::class)]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private ?ContractType $contractType = null;
|
||||
|
||||
/** Work-time ratio: 1.0 = full time, 0.8 = 4 days out of 5. */
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private float $workTimeRatio = 1.0;
|
||||
|
||||
/** Yearly paid-leave entitlement in worked days (default 25 = jours ouvrés). */
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private float $annualLeaveDays = 25.0;
|
||||
|
||||
/** Reference period start as MM-DD (default 06-01, 1st of June). */
|
||||
#[ORM\Column(length: 5)]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private string $referencePeriodStart = '06-01';
|
||||
|
||||
/** Paid-leave already acquired when the module is rolled out. */
|
||||
#[ORM\Column(type: Types::FLOAT)]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private float $initialLeaveBalance = 0.0;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 16, nullable: true, enumType: FamilySituation::class)]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private ?FamilySituation $familySituation = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['me:read', 'user:list', 'user:write'])]
|
||||
private int $nbChildren = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
@@ -217,7 +267,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Groups(['me:read', 'task:read', 'user:list', 'time_entry:read', 'client_ticket:read'])]
|
||||
#[Groups(['me:read', 'task:read', 'user:list', 'time_entry:read', 'client_ticket:read', 'absence_request:read', 'absence_balance:read'])]
|
||||
public function getAvatarUrl(): ?string
|
||||
{
|
||||
if (null === $this->avatarFileName) {
|
||||
@@ -243,4 +293,124 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
$this->plainPassword = null;
|
||||
}
|
||||
|
||||
public function isEmployee(): bool
|
||||
{
|
||||
return $this->isEmployee;
|
||||
}
|
||||
|
||||
public function setIsEmployee(bool $isEmployee): static
|
||||
{
|
||||
$this->isEmployee = $isEmployee;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHireDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->hireDate;
|
||||
}
|
||||
|
||||
public function setHireDate(?DateTimeImmutable $hireDate): static
|
||||
{
|
||||
$this->hireDate = $hireDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEndDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->endDate;
|
||||
}
|
||||
|
||||
public function setEndDate(?DateTimeImmutable $endDate): static
|
||||
{
|
||||
$this->endDate = $endDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContractType(): ?ContractType
|
||||
{
|
||||
return $this->contractType;
|
||||
}
|
||||
|
||||
public function setContractType(?ContractType $contractType): static
|
||||
{
|
||||
$this->contractType = $contractType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWorkTimeRatio(): float
|
||||
{
|
||||
return $this->workTimeRatio;
|
||||
}
|
||||
|
||||
public function setWorkTimeRatio(float $workTimeRatio): static
|
||||
{
|
||||
$this->workTimeRatio = $workTimeRatio;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAnnualLeaveDays(): float
|
||||
{
|
||||
return $this->annualLeaveDays;
|
||||
}
|
||||
|
||||
public function setAnnualLeaveDays(float $annualLeaveDays): static
|
||||
{
|
||||
$this->annualLeaveDays = $annualLeaveDays;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReferencePeriodStart(): string
|
||||
{
|
||||
return $this->referencePeriodStart;
|
||||
}
|
||||
|
||||
public function setReferencePeriodStart(string $referencePeriodStart): static
|
||||
{
|
||||
$this->referencePeriodStart = $referencePeriodStart;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getInitialLeaveBalance(): float
|
||||
{
|
||||
return $this->initialLeaveBalance;
|
||||
}
|
||||
|
||||
public function setInitialLeaveBalance(float $initialLeaveBalance): static
|
||||
{
|
||||
$this->initialLeaveBalance = $initialLeaveBalance;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFamilySituation(): ?FamilySituation
|
||||
{
|
||||
return $this->familySituation;
|
||||
}
|
||||
|
||||
public function setFamilySituation(?FamilySituation $familySituation): static
|
||||
{
|
||||
$this->familySituation = $familySituation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNbChildren(): int
|
||||
{
|
||||
return $this->nbChildren;
|
||||
}
|
||||
|
||||
public function setNbChildren(int $nbChildren): static
|
||||
{
|
||||
$this->nbChildren = $nbChildren;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user