Ajout des notification + page employé (#6)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled

| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

Reviewed-on: #6
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #6.
This commit is contained in:
2026-03-10 12:35:17 +00:00
committed by Autin
parent ae42c70d50
commit f493ea237b
126 changed files with 9215 additions and 935 deletions

View File

@@ -52,7 +52,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
paginationEnabled: false,
)]
#[ApiFilter(DateFilter::class, properties: ['startDate', 'endDate'])]
#[ApiFilter(SearchFilter::class, properties: ['employee.site' => 'exact'])]
#[ApiFilter(SearchFilter::class, properties: ['employee' => 'exact', 'employee.site' => 'exact'])]
#[ORM\Entity(repositoryClass: AbsenceRepository::class)]
#[ORM\Table(name: 'absences')]
class Absence

View File

@@ -6,6 +6,7 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use App\Dto\Employees\ContractHistoryItem;
use App\Enum\ContractNature;
use App\Repository\EmployeeRepository;
use App\State\EmployeeWriteProcessor;
@@ -74,6 +75,12 @@ class Employee
#[Groups(['employee:write'])]
private ?string $contractEndDate = null;
#[Groups(['employee:write'])]
private ?bool $contractPaidLeaveSettled = null;
#[Groups(['employee:write'])]
private ?string $contractComment = null;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
@@ -109,6 +116,15 @@ class Employee
return $this;
}
#[Groups(['employee:read'])]
public function getInitials(): string
{
$first = mb_strtoupper(mb_substr(trim($this->firstName), 0, 1));
$last = mb_strtoupper(mb_substr(trim($this->lastName), 0, 1));
return $first.$last;
}
public function getSite(): ?Site
{
return $this->site;
@@ -186,6 +202,30 @@ class Employee
return $this;
}
public function getContractPaidLeaveSettled(): ?bool
{
return $this->contractPaidLeaveSettled;
}
public function setContractPaidLeaveSettled(?bool $contractPaidLeaveSettled): self
{
$this->contractPaidLeaveSettled = $contractPaidLeaveSettled;
return $this;
}
public function getContractComment(): ?string
{
return $this->contractComment;
}
public function setContractComment(?string $contractComment): self
{
$this->contractComment = $contractComment;
return $this;
}
#[Groups(['employee:read'])]
public function getCurrentContractNature(): string
{
@@ -204,6 +244,36 @@ class Employee
return $this->resolveCurrentContractPeriod()?->getEndDate()?->format('Y-m-d');
}
/**
* @return list<ContractHistoryItem>
*/
#[Groups(['employee:read'])]
public function getContractHistory(): array
{
$periods = $this->contractPeriods->toArray();
usort(
$periods,
static fn (EmployeeContractPeriod $a, EmployeeContractPeriod $b): int => $b->getStartDate() <=> $a->getStartDate()
);
return array_map(
static function (EmployeeContractPeriod $period): ContractHistoryItem {
$contract = $period->getContract();
return new ContractHistoryItem(
contractId: $contract?->getId(),
contractName: $contract?->getName(),
weeklyHours: $contract?->getWeeklyHours(),
contractNature: $period->getContractNatureEnum()->value,
startDate: $period->getStartDate()->format('Y-m-d'),
endDate: $period->getEndDate()?->format('Y-m-d'),
comment: $period->getComment(),
);
},
$periods
);
}
private function resolveCurrentContractPeriod(): ?EmployeeContractPeriod
{
$today = new DateTimeImmutable('today');

View File

@@ -37,6 +37,12 @@ class EmployeeContractPeriod
#[ORM\Column(type: 'string', length: 20, options: ['default' => ContractNature::CDI->value])]
private string $contractNature = ContractNature::CDI->value;
#[ORM\Column(type: 'boolean', options: ['default' => false])]
private bool $paidLeaveSettled = false;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $comment = null;
#[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $createdAt;
@@ -121,4 +127,28 @@ class EmployeeContractPeriod
{
return $this->createdAt;
}
public function isPaidLeaveSettled(): bool
{
return $this->paidLeaveSettled;
}
public function setPaidLeaveSettled(bool $paidLeaveSettled): self
{
$this->paidLeaveSettled = $paidLeaveSettled;
return $this;
}
public function getComment(): ?string
{
return $this->comment;
}
public function setComment(?string $comment): self
{
$this->comment = $comment;
return $this;
}
}

View File

@@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Enum\LeaveRuleCode;
use App\Repository\EmployeeLeaveBalanceRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: EmployeeLeaveBalanceRepository::class)]
#[ORM\Table(name: 'employee_leave_balances', options: ['comment' => 'Soldes de conges par employe et exercice (ouverture, mouvements, cloture).'])]
#[ORM\UniqueConstraint(name: 'uniq_employee_leave_balance', columns: ['employee_id', 'rule_code', 'year'])]
#[ORM\Index(columns: ['employee_id', 'year'], name: 'idx_leave_balance_employee_year')]
class EmployeeLeaveBalance
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Employee::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private ?Employee $employee = null;
#[ORM\Column(length: 64, options: ['comment' => 'Code de regle de calcul des conges (CDI_CDD_NON_FORFAIT, FORFAIT_218, ...).'])]
private string $ruleCode = '';
#[ORM\Column(type: 'integer', options: ['comment' => 'Annee d exercice de reference (ex: 2026).'])]
private int $year = 0;
#[ORM\Column(type: 'float', options: ['comment' => 'Report N-1 en jours a l ouverture de l exercice.'])]
private float $openingDays = 0.0;
#[ORM\Column(type: 'float', options: ['comment' => 'Report N-1 en samedis a l ouverture (0 pour forfait).'])]
private float $openingSaturdays = 0.0;
#[ORM\Column(type: 'float', options: ['comment' => 'Droits jours acquis sur l exercice courant.'])]
private float $accruedDays = 0.0;
#[ORM\Column(type: 'float', options: ['comment' => 'Droits samedis acquis sur l exercice courant.'])]
private float $accruedSaturdays = 0.0;
#[ORM\Column(type: 'float', options: ['comment' => 'Jours de conges consommes sur l exercice.'])]
private float $takenDays = 0.0;
#[ORM\Column(type: 'float', options: ['comment' => 'Samedis consommes sur l exercice.'])]
private float $takenSaturdays = 0.0;
#[ORM\Column(type: 'float', options: ['comment' => 'Solde de cloture jours sur l exercice.'])]
private float $closingDays = 0.0;
#[ORM\Column(type: 'float', options: ['comment' => 'Solde de cloture samedis sur l exercice.'])]
private float $closingSaturdays = 0.0;
#[ORM\Column(type: 'float', options: ['default' => 0, 'comment' => 'Jours de fractionnement saisis par la RH.'])]
private float $fractionedDays = 0.0;
#[ORM\Column(type: 'boolean', options: ['default' => false, 'comment' => 'Indique si le solde de l exercice est fige (verrouille RH).'])]
private bool $isLocked = false;
#[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $createdAt;
#[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $updatedAt;
public function __construct()
{
$now = new DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
}
public function getId(): ?int
{
return $this->id;
}
public function getEmployee(): ?Employee
{
return $this->employee;
}
public function setEmployee(Employee $employee): self
{
$this->employee = $employee;
return $this;
}
public function getRuleCode(): string
{
return $this->ruleCode;
}
public function setRuleCode(LeaveRuleCode|string $ruleCode): self
{
$this->ruleCode = $ruleCode instanceof LeaveRuleCode ? $ruleCode->value : $ruleCode;
return $this;
}
public function getYear(): int
{
return $this->year;
}
public function setYear(int $year): self
{
$this->year = $year;
return $this;
}
public function getOpeningDays(): float
{
return $this->openingDays;
}
public function setOpeningDays(float $openingDays): self
{
$this->openingDays = $openingDays;
return $this;
}
public function getOpeningSaturdays(): float
{
return $this->openingSaturdays;
}
public function setOpeningSaturdays(float $openingSaturdays): self
{
$this->openingSaturdays = $openingSaturdays;
return $this;
}
public function getAccruedDays(): float
{
return $this->accruedDays;
}
public function setAccruedDays(float $accruedDays): self
{
$this->accruedDays = $accruedDays;
return $this;
}
public function getAccruedSaturdays(): float
{
return $this->accruedSaturdays;
}
public function setAccruedSaturdays(float $accruedSaturdays): self
{
$this->accruedSaturdays = $accruedSaturdays;
return $this;
}
public function getTakenDays(): float
{
return $this->takenDays;
}
public function setTakenDays(float $takenDays): self
{
$this->takenDays = $takenDays;
return $this;
}
public function getTakenSaturdays(): float
{
return $this->takenSaturdays;
}
public function setTakenSaturdays(float $takenSaturdays): self
{
$this->takenSaturdays = $takenSaturdays;
return $this;
}
public function getClosingDays(): float
{
return $this->closingDays;
}
public function setClosingDays(float $closingDays): self
{
$this->closingDays = $closingDays;
return $this;
}
public function getClosingSaturdays(): float
{
return $this->closingSaturdays;
}
public function setClosingSaturdays(float $closingSaturdays): self
{
$this->closingSaturdays = $closingSaturdays;
return $this;
}
public function getFractionedDays(): float
{
return $this->fractionedDays;
}
public function setFractionedDays(float $fractionedDays): self
{
$this->fractionedDays = $fractionedDays;
return $this;
}
public function isLocked(): bool
{
return $this->isLocked;
}
public function setIsLocked(bool $isLocked): self
{
$this->isLocked = $isLocked;
return $this;
}
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): DateTimeImmutable
{
return $this->updatedAt;
}
public function touch(): self
{
$this->updatedAt = new DateTimeImmutable();
return $this;
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\EmployeeRttBalanceRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: EmployeeRttBalanceRepository::class)]
#[ORM\Table(name: 'employee_rtt_balances', options: ['comment' => 'Soldes RTT par employe et exercice (report N-1).'])]
#[ORM\UniqueConstraint(name: 'uniq_employee_rtt_balance', columns: ['employee_id', 'year'])]
#[ORM\Index(columns: ['employee_id', 'year'], name: 'idx_rtt_balance_employee_year')]
class EmployeeRttBalance
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Employee::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private ?Employee $employee = null;
#[ORM\Column(type: 'integer', options: ['comment' => 'Annee d exercice (year = annee de fin, ex: 2026 = 01/06/2025 -> 31/05/2026).'])]
private int $year = 0;
#[ORM\Column(type: 'integer', options: ['comment' => 'Report N-1 en minutes (solde d ouverture).'])]
private int $openingMinutes = 0;
#[ORM\Column(type: 'boolean', options: ['default' => false, 'comment' => 'Indique si le solde est fige (verrouille RH).'])]
private bool $isLocked = false;
#[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $createdAt;
#[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $updatedAt;
public function __construct()
{
$now = new DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
}
public function getId(): ?int
{
return $this->id;
}
public function getEmployee(): ?Employee
{
return $this->employee;
}
public function setEmployee(Employee $employee): self
{
$this->employee = $employee;
return $this;
}
public function getYear(): int
{
return $this->year;
}
public function setYear(int $year): self
{
$this->year = $year;
return $this;
}
public function getOpeningMinutes(): int
{
return $this->openingMinutes;
}
public function setOpeningMinutes(int $openingMinutes): self
{
$this->openingMinutes = $openingMinutes;
return $this;
}
public function isLocked(): bool
{
return $this->isLocked;
}
public function setIsLocked(bool $isLocked): self
{
$this->isLocked = $isLocked;
return $this;
}
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): DateTimeImmutable
{
return $this->updatedAt;
}
public function touch(): self
{
$this->updatedAt = new DateTimeImmutable();
return $this;
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\EmployeeRttPaymentRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: EmployeeRttPaymentRepository::class)]
#[ORM\Table(name: 'employee_rtt_payments', options: ['comment' => 'Paiements RTT par employe, mois et exercice.'])]
#[ORM\Index(columns: ['employee_id', 'year'], name: 'idx_rtt_payment_employee_year')]
class EmployeeRttPayment
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Employee::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private ?Employee $employee = null;
#[ORM\Column(type: 'integer', options: ['comment' => 'Annee d exercice.'])]
private int $year = 0;
#[ORM\Column(type: 'integer', options: ['comment' => 'Mois du paiement.'])]
private int $month = 0;
#[ORM\Column(type: 'integer', options: ['comment' => 'Duree en minutes.'])]
private int $minutes = 0;
#[ORM\Column(type: 'string', length: 10, options: ['comment' => 'Taux applique.'])]
private string $rate = '';
#[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $createdAt;
#[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $updatedAt;
public function __construct()
{
$now = new DateTimeImmutable();
$this->createdAt = $now;
$this->updatedAt = $now;
}
public function getId(): ?int
{
return $this->id;
}
public function getEmployee(): ?Employee
{
return $this->employee;
}
public function setEmployee(Employee $employee): self
{
$this->employee = $employee;
return $this;
}
public function getYear(): int
{
return $this->year;
}
public function setYear(int $year): self
{
$this->year = $year;
return $this;
}
public function getMonth(): int
{
return $this->month;
}
public function setMonth(int $month): self
{
$this->month = $month;
return $this;
}
public function getMinutes(): int
{
return $this->minutes;
}
public function setMinutes(int $minutes): self
{
$this->minutes = $minutes;
return $this;
}
public function getRate(): string
{
return $this->rate;
}
public function setRate(string $rate): self
{
$this->rate = $rate;
return $this;
}
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): DateTimeImmutable
{
return $this->updatedAt;
}
public function touch(): self
{
$this->updatedAt = new DateTimeImmutable();
return $this;
}
}

196
src/Entity/Notification.php Normal file
View File

@@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use App\Repository\NotificationRepository;
use App\State\MarkAllNotificationsReadProcessor;
use App\State\NotificationHistoryProvider;
use App\State\NotificationTodayProvider;
use App\State\UnreadNotificationsProvider;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new Get(
uriTemplate: '/notifications/{id}',
uriVariables: ['id'],
requirements: ['id' => '\d+'],
normalizationContext: ['groups' => ['notification:read']],
security: "is_granted('ROLE_USER')"
),
new GetCollection(
uriTemplate: '/notifications/unread',
normalizationContext: ['groups' => ['notification:read']],
security: "is_granted('ROLE_USER')",
provider: UnreadNotificationsProvider::class,
paginationEnabled: false
),
new GetCollection(
uriTemplate: '/notifications/today',
normalizationContext: ['groups' => ['notification:read']],
security: "is_granted('ROLE_USER')",
provider: NotificationTodayProvider::class,
paginationEnabled: false
),
new GetCollection(
uriTemplate: '/notifications/history',
normalizationContext: ['groups' => ['notification:read']],
security: "is_granted('ROLE_USER')",
provider: NotificationHistoryProvider::class,
paginationEnabled: false
),
new Post(
uriTemplate: '/notifications/mark-all-read',
security: "is_granted('ROLE_USER')",
input: false,
output: false,
read: false,
processor: MarkAllNotificationsReadProcessor::class
),
]
)]
#[ORM\Entity(repositoryClass: NotificationRepository::class)]
#[ORM\Table(name: 'notifications')]
#[ORM\Index(columns: ['recipient_id', 'is_read', 'created_at'], name: 'idx_notifications_recipient_read_created')]
class Notification
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
#[Groups(['notification:read'])]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private ?User $recipient = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?User $actor = null;
#[ORM\Column(type: 'text')]
#[Groups(['notification:read'])]
private string $message = '';
#[ORM\Column(type: 'string', length: 60, options: ['default' => ''])]
#[Groups(['notification:read'])]
private string $category = '';
#[ORM\Column(type: 'string', length: 255, options: ['default' => ''])]
#[Groups(['notification:read'])]
private string $target = '';
#[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['notification:read'])]
private bool $isRead = false;
#[ORM\Column(type: 'datetime_immutable')]
#[Groups(['notification:read'])]
private DateTimeImmutable $createdAt;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getRecipient(): ?User
{
return $this->recipient;
}
public function setRecipient(?User $recipient): self
{
$this->recipient = $recipient;
return $this;
}
public function getActor(): ?User
{
return $this->actor;
}
public function setActor(?User $actor): self
{
$this->actor = $actor;
return $this;
}
#[Groups(['notification:read'])]
public function getActorName(): string
{
return $this->actor?->getUsername() ?? '';
}
public function getMessage(): string
{
return $this->message;
}
public function setMessage(string $message): self
{
$this->message = $message;
return $this;
}
public function getCategory(): string
{
return $this->category;
}
public function setCategory(string $category): self
{
$this->category = $category;
return $this;
}
public function getTarget(): string
{
return $this->target;
}
public function setTarget(string $target): self
{
$this->target = $target;
return $this;
}
public function isRead(): bool
{
return $this->isRead;
}
public function getIsRead(): bool
{
return $this->isRead;
}
public function setIsRead(bool $isRead): self
{
$this->isRead = $isRead;
return $this;
}
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
}

View File

@@ -10,6 +10,7 @@ use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\UserRepository;
use App\State\CurrentUserProvider;
use App\State\UserPasswordHasherProcessor;
use Doctrine\Common\Collections\ArrayCollection;
@@ -52,7 +53,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
),
]
)]
#[ORM\Entity]
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'users')]
#[ORM\UniqueConstraint(name: 'uniq_users_username', fields: ['username'])]
class User implements UserInterface, PasswordAuthenticatedUserInterface

View File

@@ -14,6 +14,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use App\Repository\WorkHourRepository;
use App\State\WorkHourSiteValidationProcessor;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
@@ -106,6 +107,10 @@ class WorkHour
#[Groups(['work_hour:read', 'work_hour:site_validate'])]
private bool $isSiteValid = false;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
#[Groups(['work_hour:read'])]
private ?DateTimeImmutable $updatedAt = null;
public function getId(): ?int
{
return $this->id;
@@ -274,4 +279,16 @@ class WorkHour
return $this;
}
public function getUpdatedAt(): ?DateTimeImmutable
{
return $this->updatedAt;
}
public function setUpdatedAt(?DateTimeImmutable $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
}