Migration modular monolith DDD (0.1 → 3.3) (#17)
Auto Tag Develop / tag (push) Successful in 9s

## Migration modular monolith DDD — Lesstime (0.1 → 3.3)

Cette MR regroupe l'intégralité de la refonte en monolithe modulaire (strangler progressif, additif). Elle remplace les MR stackées de Phase 1 (#12–#16), désormais incluses ici.

**Ne pas merger avant validation fonctionnelle** : branche destinée à être testée telle quelle.

### Périmètre — 9 modules sous `src/Module/`
| Phase | Module | Contenu |
|------|--------|---------|
| 0.1 | (socle) | infrastructure modulaire, `ModuleInterface`, mapping Doctrine par module |
| 0.2 | (socle front) | auto-détection des layers Nuxt sous `frontend/modules/*` |
| 1.1 | **Core** | Identité (User/Auth), Notifications, Notifier |
| 1.2 | Core | RBAC fin (permissions `module.resource.action`, sidebar gated) |
| 1.3 | Core | Audit log (`#[Auditable]`, listener, provider DBAL) |
| 2.1 | **TimeTracking** | TimeEntry + MCP + export |
| 2.2 | **ProjectManagement** | cœur métier Projets/Tâches + 38 MCP tools |
| 2.3 | **Absence** | demandes, soldes, policies, justificatifs |
| 2.4 | **Directory** | Clients (migrés) + **Prospects** (nouveau, conversion → Client) |
| 2.5 | **Mail** | intégration IMAP OVH + liens tâches |
| 2.6 | **Integration** | Gitea / BookStack / Zimbra / Share |
| 3.1 | **Reporting** | rapports transverses (DBAL read-only, 0 import inter-module) |
| 3.2 | **ClientPortal** | portail client (ROLE_CLIENT cloisonné, tickets, notifications) |
| 3.3 | (finition) | nettoyage legacy — `src/Entity` vide, app 100% modulaire |

### Architecture
- Découplage inter-modules par **contrats** (`UserInterface`, `ProjectInterface`, `TaskInterface`, `TaskTagInterface`, `ClientInterface`, `ClientTicketInterface`, `LeaveProfileInterface`) + `resolve_target_entities` 100% modulaire (aucune cible legacy).
- Repositories : interface `Domain/Repository` + implémentation `Infrastructure/Doctrine`, bindées.
- Reporting en DBAL read-only pur (aucun import d'entité d'un autre module).
- Chaque migration de module : déplacement à comportement préservé (API publique et noms d'outils MCP inchangés), migrations **additives** uniquement (zéro destructif).

### Sécurité
- ROLE_CLIENT cloisonné : un utilisateur client n'accède qu'à `/portal` et à ses propres tickets (filtrés par `allowedProjects`), interdit sur toute l'API interne.
- Correctif : interdiction pour un client de créer un lien vers le partage SMB (upload uniquement).

### QA non-régression (branche reconstruite from scratch)
- Migrations from scratch + fixtures : OK.
- Compilation dev + prod : OK.
- **180 tests PHPUnit verts**, php-cs-fixer clean, ~96 routes, **66 outils MCP** tous sous `App\Module\*`.
- Smoke test runtime multi-rôles (admin / ROLE_USER / ROLE_CLIENT) : 44 vérifications HTTP, **0 écart**, cloisonnement client étanche.
- Build Nuxt OK, 9 layers, 0 import legacy résiduel.

### Points à arbitrer (hors périmètre de cette migration)
- Durcissement MCP/IDOR pré-existant (`userId` explicite sans scoping sur certains tools TimeTracking/Absence/TaskDocument) — ticket dédié recommandé.
- Validation fonctionnelle de **Prospect** et **ClientPortal** (conçus depuis les specs disque).
- **Harmonisation visuelle Malio finale** (3.3) — finition esthétique inter-modules laissée au PO.

---

## ⚠️ Déploiement / migration des données — à ne pas oublier

### 1. Resynchroniser les séquences PostgreSQL après tout import/restore de dump
Si la prod (ou tout environnement) est **montée depuis un dump** (`pg_restore` / `COPY`), les lignes sont chargées avec leurs `id` explicites **sans avancer les séquences** → au premier `INSERT` : `duplicate key value violates unique constraint "..._pkey"` (constaté en local sur `notification`, `task`, `time_entry`…).

À lancer **juste après chaque restore/import** :

```sql
DO $$
DECLARE r RECORD; maxid BIGINT; seq TEXT;
BEGIN
  FOR r IN SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public'
  LOOP
    seq := pg_get_serial_sequence(quote_ident(r.table_name), r.column_name);
    IF seq IS NOT NULL THEN
      EXECUTE format('SELECT COALESCE(MAX(%I),0) FROM %I', r.column_name, r.table_name) INTO maxid;
      PERFORM setval(seq, GREATEST(maxid,1), maxid > 0);
    END IF;
  END LOOP;
END $$;
```

> Ne concerne **pas** une prod qui tourne déjà (séquences avancées organiquement) — uniquement le cas restore/import. Idempotent, sans risque.

### 2. Fix dénormalisation des collections typées-contrat (code, inclus dans la branche)
Les relations **to-many** typées par une interface `Shared\Domain\Contract\*` (`TimeEntry::tags` → `TaskTagInterface`, `Task::collaborators` → `UserInterface`) étaient **indénormalisables par API Platform** (mono-valué OK via IRI, collection KO) → **tout POST/PATCH portant une telle collection renvoyait 400/500**. Corrigé par un dénormaliseur générique `ContractRelationDenormalizer` (réutilise `resolve_target_entities`, zéro couplage par-entité) + test fonctionnel de non-régression.

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
2026-06-23 13:50:42 +00:00
parent d0a49322e1
commit 8313c759c6
622 changed files with 24802 additions and 2864 deletions
-208
View File
@@ -1,208 +0,0 @@
<?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;
/** Days acquired during the *previous* reference period (Congés N-1): fully available to take. */
#[ORM\Column(type: Types::FLOAT)]
#[Groups(['absence_balance:read', 'absence_balance:write'])]
private float $acquired = 0.0;
/** Days being accrued during the *current* reference period (Congés N): "en cours d'acquisition". */
#[ORM\Column(type: Types::FLOAT)]
#[Groups(['absence_balance:read', 'absence_balance:write'])]
private float $acquiring = 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;
/** Last month (format YYYY-MM) for which the monthly accrual was applied. */
#[ORM\Column(length: 7, nullable: true)]
private ?string $lastAccruedMonth = null;
/** Total entitlement for the period, both finalized (N-1) and in-progress (N). */
#[Groups(['absence_balance:read'])]
public function getAcquiredTotal(): float
{
return $this->acquired + $this->acquiring;
}
/**
* Days the employee can still take: in this organisation the days being
* accrued (N) are posable too, so they count towards what is available.
*/
#[Groups(['absence_balance:read'])]
public function getAvailable(): float
{
return $this->acquired + $this->acquiring - $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 getAcquiring(): float
{
return $this->acquiring;
}
public function setAcquiring(float $acquiring): static
{
$this->acquiring = $acquiring;
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;
}
public function getLastAccruedMonth(): ?string
{
return $this->lastAccruedMonth;
}
public function setLastAccruedMonth(?string $lastAccruedMonth): static
{
$this->lastAccruedMonth = $lastAccruedMonth;
return $this;
}
}
-171
View File
@@ -1,171 +0,0 @@
<?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;
}
}
-326
View File
@@ -1,326 +0,0 @@
<?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;
}
}
-74
View File
@@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\BookStackConfigurationRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: BookStackConfigurationRepository::class)]
class BookStackConfiguration
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
#[Assert\Url]
private ?string $url = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $encryptedTokenId = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $encryptedTokenSecret = null;
public function getId(): ?int
{
return $this->id;
}
public function getUrl(): ?string
{
return $this->url;
}
public function setUrl(?string $url): static
{
$this->url = $url;
return $this;
}
public function getEncryptedTokenId(): ?string
{
return $this->encryptedTokenId;
}
public function setEncryptedTokenId(?string $encryptedTokenId): static
{
$this->encryptedTokenId = $encryptedTokenId;
return $this;
}
public function getEncryptedTokenSecret(): ?string
{
return $this->encryptedTokenSecret;
}
public function setEncryptedTokenSecret(?string $encryptedTokenSecret): static
{
$this->encryptedTokenSecret = $encryptedTokenSecret;
return $this;
}
public function hasToken(): bool
{
return null !== $this->encryptedTokenId && null !== $this->encryptedTokenSecret;
}
}
-155
View File
@@ -1,155 +0,0 @@
<?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\Repository\ClientRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['client:read']],
denormalizationContext: ['groups' => ['client:write']],
order: ['name' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: ClientRepository::class)]
class Client
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['client:read', 'project:read', 'user:list'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['client:read', 'client:write', 'project:read', 'user:list'])]
private ?string $name = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['client:read', 'client:write'])]
private ?string $email = null;
#[ORM\Column(length: 50, nullable: true)]
#[Groups(['client:read', 'client:write'])]
private ?string $phone = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['client:read', 'client:write'])]
private ?string $street = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['client:read', 'client:write'])]
private ?string $city = null;
#[ORM\Column(length: 20, nullable: true)]
#[Groups(['client:read', 'client:write'])]
private ?string $postalCode = null;
/** @var Collection<int, Project> */
#[ORM\OneToMany(targetEntity: Project::class, mappedBy: 'client')]
private Collection $projects;
public function __construct()
{
$this->projects = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): static
{
$this->email = $email;
return $this;
}
public function getPhone(): ?string
{
return $this->phone;
}
public function setPhone(?string $phone): static
{
$this->phone = $phone;
return $this;
}
public function getStreet(): ?string
{
return $this->street;
}
public function setStreet(?string $street): static
{
$this->street = $street;
return $this;
}
public function getCity(): ?string
{
return $this->city;
}
public function setCity(?string $city): static
{
$this->city = $city;
return $this;
}
public function getPostalCode(): ?string
{
return $this->postalCode;
}
public function setPostalCode(?string $postalCode): static
{
$this->postalCode = $postalCode;
return $this;
}
/** @return Collection<int, Project> */
public function getProjects(): Collection
{
return $this->projects;
}
}
-59
View File
@@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\GiteaConfigurationRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: GiteaConfigurationRepository::class)]
class GiteaConfiguration
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
#[Assert\Url]
private ?string $url = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $encryptedToken = null;
public function getId(): ?int
{
return $this->id;
}
public function getUrl(): ?string
{
return $this->url;
}
public function setUrl(?string $url): static
{
$this->url = $url;
return $this;
}
public function getEncryptedToken(): ?string
{
return $this->encryptedToken;
}
public function setEncryptedToken(?string $encryptedToken): static
{
$this->encryptedToken = $encryptedToken;
return $this;
}
public function hasToken(): bool
{
return null !== $this->encryptedToken;
}
}
-193
View File
@@ -1,193 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\MailConfigurationRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MailConfigurationRepository::class)]
#[ORM\Table(name: 'mail_configuration')]
class MailConfiguration
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 10)]
private string $protocol = 'imap';
#[ORM\Column(length: 255, nullable: true)]
private ?string $imapHost = null;
#[ORM\Column]
private int $imapPort = 993;
#[ORM\Column(length: 10)]
private string $imapEncryption = 'ssl';
#[ORM\Column(length: 255, nullable: true)]
private ?string $smtpHost = null;
#[ORM\Column]
private int $smtpPort = 465;
#[ORM\Column(length: 10)]
private string $smtpEncryption = 'ssl';
#[ORM\Column(length: 255, nullable: true)]
private ?string $username = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $encryptedPassword = null;
#[ORM\Column(length: 255)]
private string $sentFolderPath = 'Sent';
#[ORM\Column(type: 'boolean')]
private bool $enabled = false;
public function getId(): ?int
{
return $this->id;
}
public function getProtocol(): string
{
return $this->protocol;
}
public function setProtocol(string $protocol): static
{
$this->protocol = $protocol;
return $this;
}
public function getImapHost(): ?string
{
return $this->imapHost;
}
public function setImapHost(?string $imapHost): static
{
$this->imapHost = $imapHost;
return $this;
}
public function getImapPort(): int
{
return $this->imapPort;
}
public function setImapPort(int $imapPort): static
{
$this->imapPort = $imapPort;
return $this;
}
public function getImapEncryption(): string
{
return $this->imapEncryption;
}
public function setImapEncryption(string $imapEncryption): static
{
$this->imapEncryption = $imapEncryption;
return $this;
}
public function getSmtpHost(): ?string
{
return $this->smtpHost;
}
public function setSmtpHost(?string $smtpHost): static
{
$this->smtpHost = $smtpHost;
return $this;
}
public function getSmtpPort(): int
{
return $this->smtpPort;
}
public function setSmtpPort(int $smtpPort): static
{
$this->smtpPort = $smtpPort;
return $this;
}
public function getSmtpEncryption(): string
{
return $this->smtpEncryption;
}
public function setSmtpEncryption(string $smtpEncryption): static
{
$this->smtpEncryption = $smtpEncryption;
return $this;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(?string $username): static
{
$this->username = $username;
return $this;
}
public function getEncryptedPassword(): ?string
{
return $this->encryptedPassword;
}
public function setEncryptedPassword(?string $encryptedPassword): static
{
$this->encryptedPassword = $encryptedPassword;
return $this;
}
public function getSentFolderPath(): string
{
return $this->sentFolderPath;
}
public function setSentFolderPath(string $sentFolderPath): static
{
$this->sentFolderPath = $sentFolderPath;
return $this;
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function setEnabled(bool $enabled): static
{
$this->enabled = $enabled;
return $this;
}
public function hasPassword(): bool
{
return null !== $this->encryptedPassword;
}
}
-115
View File
@@ -1,115 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\MailFolderRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MailFolderRepository::class)]
#[ORM\Table(name: 'mail_folder')]
#[ORM\Index(columns: ['parent_path'], name: 'idx_mail_folder_parent_path')]
class MailFolder
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 500, unique: true)]
private string $path;
#[ORM\Column(length: 255)]
private string $displayName;
#[ORM\Column(length: 500, nullable: true)]
private ?string $parentPath = null;
#[ORM\Column]
private int $unreadCount = 0;
#[ORM\Column]
private int $totalCount = 0;
#[ORM\Column(type: 'datetimetz_immutable', nullable: true)]
private ?DateTimeImmutable $lastSyncedAt = null;
public function getId(): ?int
{
return $this->id;
}
public function getPath(): string
{
return $this->path;
}
public function setPath(string $path): static
{
$this->path = $path;
return $this;
}
public function getDisplayName(): string
{
return $this->displayName;
}
public function setDisplayName(string $displayName): static
{
$this->displayName = $displayName;
return $this;
}
public function getParentPath(): ?string
{
return $this->parentPath;
}
public function setParentPath(?string $parentPath): static
{
$this->parentPath = $parentPath;
return $this;
}
public function getUnreadCount(): int
{
return $this->unreadCount;
}
public function setUnreadCount(int $unreadCount): static
{
$this->unreadCount = $unreadCount;
return $this;
}
public function getTotalCount(): int
{
return $this->totalCount;
}
public function setTotalCount(int $totalCount): static
{
$this->totalCount = $totalCount;
return $this;
}
public function getLastSyncedAt(): ?DateTimeImmutable
{
return $this->lastSyncedAt;
}
public function setLastSyncedAt(?DateTimeImmutable $lastSyncedAt): static
{
$this->lastSyncedAt = $lastSyncedAt;
return $this;
}
}
-239
View File
@@ -1,239 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\MailMessageRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MailMessageRepository::class)]
#[ORM\Table(name: 'mail_message')]
#[ORM\UniqueConstraint(name: 'uq_mail_message_folder_uid', columns: ['folder_id', 'uid'])]
#[ORM\Index(columns: ['sent_at'], name: 'idx_mail_message_sent_at')]
#[ORM\Index(columns: ['is_read'], name: 'idx_mail_message_is_read')]
#[ORM\Index(columns: ['message_id'], name: 'idx_mail_message_message_id')]
class MailMessage
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 500)]
private string $messageId;
#[ORM\ManyToOne(targetEntity: MailFolder::class)]
#[ORM\JoinColumn(name: 'folder_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
private MailFolder $folder;
#[ORM\Column]
private int $uid;
#[ORM\Column(length: 500, nullable: true)]
private ?string $subject = null;
#[ORM\Column(length: 255)]
private string $fromAddress;
#[ORM\Column(length: 255, nullable: true)]
private ?string $fromName = null;
#[ORM\Column(type: 'json')]
private array $toAddresses = [];
#[ORM\Column(type: 'json', nullable: true)]
private ?array $ccAddresses = null;
#[ORM\Column(type: 'datetimetz_immutable')]
private DateTimeImmutable $sentAt;
#[ORM\Column(type: 'boolean')]
private bool $isRead = false;
#[ORM\Column(type: 'boolean')]
private bool $isFlagged = false;
#[ORM\Column(type: 'boolean')]
private bool $hasAttachments = false;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $snippet = null;
#[ORM\Column(type: 'datetimetz_immutable')]
private DateTimeImmutable $syncedAt;
public function getId(): ?int
{
return $this->id;
}
public function getMessageId(): string
{
return $this->messageId;
}
public function setMessageId(string $messageId): static
{
$this->messageId = $messageId;
return $this;
}
public function getFolder(): MailFolder
{
return $this->folder;
}
public function setFolder(MailFolder $folder): static
{
$this->folder = $folder;
return $this;
}
public function getUid(): int
{
return $this->uid;
}
public function setUid(int $uid): static
{
$this->uid = $uid;
return $this;
}
public function getSubject(): ?string
{
return $this->subject;
}
public function setSubject(?string $subject): static
{
$this->subject = $subject;
return $this;
}
public function getFromAddress(): string
{
return $this->fromAddress;
}
public function setFromAddress(string $fromAddress): static
{
$this->fromAddress = $fromAddress;
return $this;
}
public function getFromName(): ?string
{
return $this->fromName;
}
public function setFromName(?string $fromName): static
{
$this->fromName = $fromName;
return $this;
}
public function getToAddresses(): array
{
return $this->toAddresses;
}
public function setToAddresses(array $toAddresses): static
{
$this->toAddresses = $toAddresses;
return $this;
}
public function getCcAddresses(): ?array
{
return $this->ccAddresses;
}
public function setCcAddresses(?array $ccAddresses): static
{
$this->ccAddresses = $ccAddresses;
return $this;
}
public function getSentAt(): DateTimeImmutable
{
return $this->sentAt;
}
public function setSentAt(DateTimeImmutable $sentAt): static
{
$this->sentAt = $sentAt;
return $this;
}
public function isRead(): bool
{
return $this->isRead;
}
public function setIsRead(bool $isRead): static
{
$this->isRead = $isRead;
return $this;
}
public function isFlagged(): bool
{
return $this->isFlagged;
}
public function setIsFlagged(bool $isFlagged): static
{
$this->isFlagged = $isFlagged;
return $this;
}
public function hasAttachments(): bool
{
return $this->hasAttachments;
}
public function setHasAttachments(bool $hasAttachments): static
{
$this->hasAttachments = $hasAttachments;
return $this;
}
public function getSnippet(): ?string
{
return $this->snippet;
}
public function setSnippet(?string $snippet): static
{
$this->snippet = $snippet;
return $this;
}
public function getSyncedAt(): DateTimeImmutable
{
return $this->syncedAt;
}
public function setSyncedAt(DateTimeImmutable $syncedAt): static
{
$this->syncedAt = $syncedAt;
return $this;
}
}
-143
View File
@@ -1,143 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use App\Repository\NotificationRepository;
use App\State\NotificationProvider;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(
provider: NotificationProvider::class,
security: "is_granted('IS_AUTHENTICATED_FULLY')",
),
new Patch(
security: "is_granted('IS_AUTHENTICATED_FULLY') and object.getUser() == user",
),
],
normalizationContext: ['groups' => ['notification:read']],
denormalizationContext: ['groups' => ['notification:write']],
order: ['createdAt' => 'DESC'],
)]
#[ORM\Entity(repositoryClass: NotificationRepository::class)]
#[ORM\Index(columns: ['user_id'], name: 'idx_notification_user')]
#[ORM\Index(columns: ['user_id', 'is_read'], name: 'idx_notification_user_read')]
class Notification
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['notification:read'])]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Groups(['notification:read'])]
private ?User $user = null;
#[ORM\Column(length: 50)]
#[Groups(['notification:read'])]
private ?string $type = null;
#[ORM\Column(length: 255)]
#[Groups(['notification:read'])]
private ?string $title = null;
#[ORM\Column(type: Types::TEXT)]
#[Groups(['notification:read'])]
private ?string $message = null;
#[ORM\Column]
#[Groups(['notification:read', 'notification:write'])]
private bool $isRead = false;
#[ORM\Column]
#[Groups(['notification:read'])]
private ?DateTimeImmutable $createdAt = null;
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(): ?string
{
return $this->type;
}
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getMessage(): ?string
{
return $this->message;
}
public function setMessage(string $message): static
{
$this->message = $message;
return $this;
}
public function isRead(): bool
{
return $this->isRead;
}
public function setIsRead(bool $isRead): static
{
$this->isRead = $isRead;
return $this;
}
public function getCreatedAt(): ?DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
}
-270
View File
@@ -1,270 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\ApiResource\SwitchWorkflowOutput;
use App\Repository\ProjectRepository;
use App\State\SwitchProjectWorkflowProcessor;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
operations: [
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(
security: "is_granted('ROLE_ADMIN')",
denormalizationContext: ['groups' => ['project:write', 'project:create']],
),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
new Post(
uriTemplate: '/projects/{id}/switch-workflow',
uriVariables: ['id' => new Link(fromClass: Project::class)],
security: "is_granted('ROLE_ADMIN')",
input: false,
output: SwitchWorkflowOutput::class,
normalizationContext: ['groups' => ['switch_workflow:read']],
processor: SwitchProjectWorkflowProcessor::class,
read: true,
deserialize: false,
validate: false,
name: 'switch_workflow',
),
],
normalizationContext: ['groups' => ['project:read']],
denormalizationContext: ['groups' => ['project:write']],
order: ['name' => 'ASC'],
)]
#[ApiFilter(BooleanFilter::class, properties: ['archived'])]
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
#[UniqueEntity(fields: ['code'], message: 'Ce code de projet est déjà utilisé.')]
class Project
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['project:read', 'time_entry:read', 'task:read', 'me:read', 'user:list'])]
private ?int $id = null;
#[ORM\Column(length: 10, unique: true)]
#[Groups(['project:read', 'project:create', 'task:read'])]
#[Assert\NotBlank]
#[Assert\Regex(pattern: '/^[A-Z]{2,10}$/', message: 'Le code doit contenir entre 2 et 10 lettres majuscules.')]
private ?string $code = null;
#[ORM\Column(length: 255)]
#[Groups(['project:read', 'project:write', 'time_entry:read', 'task:read', 'me:read', 'user:list'])]
private ?string $name = null;
#[ORM\Column(type: 'text', nullable: true)]
#[Groups(['project:read', 'project:write'])]
private ?string $description = null;
#[ORM\Column(length: 7)]
#[Groups(['project:read', 'project:write', 'time_entry:read', 'task:read'])]
private ?string $color = '#222783';
#[ORM\ManyToOne(targetEntity: Client::class, inversedBy: 'projects')]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['project:read', 'project:write'])]
private ?Client $client = null;
#[ORM\ManyToOne(targetEntity: Workflow::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'RESTRICT')]
#[Groups(['project:read', 'project:write', 'task:read'])]
#[Assert\NotNull(message: 'Un projet doit avoir un workflow.')]
private ?Workflow $workflow = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['project:read', 'project:write', 'task:read'])]
private ?string $giteaOwner = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['project:read', 'project:write', 'task:read'])]
private ?string $giteaRepo = null;
#[ORM\Column(nullable: true)]
#[Groups(['project:read', 'project:write', 'task:read'])]
private ?int $bookstackShelfId = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['project:read', 'project:write'])]
private ?string $bookstackShelfName = null;
#[ORM\Column]
#[Groups(['project:read', 'project:write'])]
private bool $archived = false;
/** @var Collection<int, Task> */
#[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'project')]
private Collection $tasks;
public function __construct()
{
$this->tasks = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): static
{
$this->code = $code;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): static
{
$this->color = $color;
return $this;
}
public function getClient(): ?Client
{
return $this->client;
}
public function setClient(?Client $client): static
{
$this->client = $client;
return $this;
}
public function getGiteaOwner(): ?string
{
return $this->giteaOwner;
}
public function setGiteaOwner(?string $giteaOwner): static
{
$this->giteaOwner = $giteaOwner;
return $this;
}
public function getGiteaRepo(): ?string
{
return $this->giteaRepo;
}
public function setGiteaRepo(?string $giteaRepo): static
{
$this->giteaRepo = $giteaRepo;
return $this;
}
public function hasGiteaRepo(): bool
{
return null !== $this->giteaOwner && null !== $this->giteaRepo;
}
public function isArchived(): bool
{
return $this->archived;
}
public function setArchived(bool $archived): static
{
$this->archived = $archived;
return $this;
}
public function getBookstackShelfId(): ?int
{
return $this->bookstackShelfId;
}
public function setBookstackShelfId(?int $bookstackShelfId): static
{
$this->bookstackShelfId = $bookstackShelfId;
return $this;
}
public function getBookstackShelfName(): ?string
{
return $this->bookstackShelfName;
}
public function setBookstackShelfName(?string $bookstackShelfName): static
{
$this->bookstackShelfName = $bookstackShelfName;
return $this;
}
public function getWorkflow(): ?Workflow
{
return $this->workflow;
}
public function setWorkflow(Workflow $workflow): static
{
$this->workflow = $workflow;
return $this;
}
#[Groups(['project:read'])]
public function getTaskCount(): int
{
return $this->tasks->count();
}
}
-139
View File
@@ -1,139 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\ShareConfigurationRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ShareConfigurationRepository::class)]
class ShareConfiguration
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $host = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $shareName = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $basePath = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $domain = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $username = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $encryptedPassword = null;
#[ORM\Column(type: 'boolean')]
private bool $enabled = false;
public function getId(): ?int
{
return $this->id;
}
public function getHost(): ?string
{
return $this->host;
}
public function setHost(?string $host): static
{
$this->host = $host;
return $this;
}
public function getShareName(): ?string
{
return $this->shareName;
}
public function setShareName(?string $shareName): static
{
$this->shareName = $shareName;
return $this;
}
public function getBasePath(): ?string
{
return $this->basePath;
}
public function setBasePath(?string $basePath): static
{
$this->basePath = $basePath;
return $this;
}
public function getDomain(): ?string
{
return $this->domain;
}
public function setDomain(?string $domain): static
{
$this->domain = $domain;
return $this;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(?string $username): static
{
$this->username = $username;
return $this;
}
public function getEncryptedPassword(): ?string
{
return $this->encryptedPassword;
}
public function setEncryptedPassword(?string $encryptedPassword): static
{
$this->encryptedPassword = $encryptedPassword;
return $this;
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function setEnabled(bool $enabled): static
{
$this->enabled = $enabled;
return $this;
}
public function hasPassword(): bool
{
return null !== $this->encryptedPassword;
}
public function isUsable(): bool
{
return $this->enabled
&& null !== $this->host && '' !== $this->host
&& null !== $this->shareName && '' !== $this->shareName;
}
}
-486
View File
@@ -1,486 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
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\Repository\TaskRepository;
use App\State\TaskCalendarProcessor;
use App\State\TaskNumberProcessor;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ApiResource(
operations: [
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')", processor: TaskNumberProcessor::class),
new Patch(security: "is_granted('ROLE_ADMIN')", processor: TaskCalendarProcessor::class),
new Delete(security: "is_granted('ROLE_ADMIN')", processor: TaskCalendarProcessor::class),
],
normalizationContext: ['groups' => ['task:read']],
denormalizationContext: ['groups' => ['task:write']],
order: ['id' => 'DESC'],
)]
#[ApiFilter(SearchFilter::class, properties: ['project' => 'exact', 'group' => 'exact', 'assignee' => 'exact', 'collaborators' => 'exact', 'priority' => 'exact', 'effort' => 'exact', 'tags' => 'exact', 'status' => 'exact'])]
#[ApiFilter(DateFilter::class, properties: ['scheduledStart', 'scheduledEnd', 'deadline'])]
#[ApiFilter(BooleanFilter::class, properties: ['archived', 'syncToCalendar'])]
#[ApiFilter(OrderFilter::class, properties: ['scheduledStart', 'deadline'])]
#[ORM\Entity(repositoryClass: TaskRepository::class)]
#[ORM\Table(name: 'task')]
#[ORM\UniqueConstraint(name: 'uniq_task_project_number', columns: ['project_id', 'number'])]
class Task
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task:read'])]
private ?int $id = null;
#[ORM\Column(type: 'integer')]
#[Groups(['task:read'])]
private ?int $number = null;
#[ORM\Column(length: 255)]
#[Groups(['task:read', 'task:write'])]
private ?string $title = null;
#[ORM\Column(type: 'text', nullable: true)]
#[Groups(['task:read', 'task:write'])]
private ?string $description = null;
#[ORM\ManyToOne(targetEntity: TaskStatus::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task:read', 'task:write'])]
private ?TaskStatus $status = null;
#[ORM\ManyToOne(targetEntity: TaskEffort::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task:read', 'task:write'])]
private ?TaskEffort $effort = null;
#[ORM\ManyToOne(targetEntity: TaskPriority::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task:read', 'task:write'])]
private ?TaskPriority $priority = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task:read', 'task:write'])]
private ?User $assignee = null;
/** @var Collection<int, User> */
#[ORM\ManyToMany(targetEntity: User::class)]
#[ORM\JoinTable(
name: 'task_collaborator',
joinColumns: [new ORM\JoinColumn(name: 'task_id', referencedColumnName: 'id', onDelete: 'CASCADE')],
inverseJoinColumns: [new ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', onDelete: 'CASCADE')],
)]
#[Groups(['task:read', 'task:write'])]
private Collection $collaborators;
#[ORM\ManyToOne(targetEntity: TaskGroup::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task:read', 'task:write'])]
private ?TaskGroup $group = null;
#[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'tasks')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Groups(['task:read', 'task:write'])]
private ?Project $project = null;
/** @var Collection<int, TaskTag> */
#[ORM\ManyToMany(targetEntity: TaskTag::class)]
#[ORM\JoinTable(
name: 'task_task_type',
joinColumns: [new ORM\JoinColumn(name: 'task_id', referencedColumnName: 'id')],
inverseJoinColumns: [new ORM\JoinColumn(name: 'task_type_id', referencedColumnName: 'id')],
)]
#[Groups(['task:read', 'task:write'])]
private Collection $tags;
#[ORM\Column(type: 'boolean')]
#[Groups(['task:read', 'task:write'])]
private bool $archived = false;
/** @var Collection<int, TaskDocument> */
#[ORM\OneToMany(targetEntity: TaskDocument::class, mappedBy: 'task', cascade: ['remove'])]
#[Groups(['task:read'])]
private Collection $documents;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
#[Groups(['task:read', 'task:write'])]
private ?DateTimeImmutable $scheduledStart = null;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
#[Groups(['task:read', 'task:write'])]
private ?DateTimeImmutable $scheduledEnd = null;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
#[Groups(['task:read', 'task:write'])]
private ?DateTimeImmutable $deadline = null;
#[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['task:read', 'task:write'])]
private bool $syncToCalendar = false;
#[ORM\Column(length: 255, nullable: true)]
private ?string $calendarEventUid = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $calendarTodoUid = null;
#[ORM\Column(type: 'text', nullable: true)]
#[Groups(['task:read'])]
private ?string $calendarSyncError = null;
#[ORM\ManyToOne(targetEntity: TaskRecurrence::class, inversedBy: 'tasks')]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task:read', 'task:write'])]
private ?TaskRecurrence $recurrence = null;
public function __construct()
{
$this->tags = new ArrayCollection();
$this->documents = new ArrayCollection();
$this->collaborators = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getNumber(): ?int
{
return $this->number;
}
public function setNumber(int $number): static
{
$this->number = $number;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getStatus(): ?TaskStatus
{
return $this->status;
}
public function setStatus(?TaskStatus $status): static
{
$this->status = $status;
return $this;
}
public function getEffort(): ?TaskEffort
{
return $this->effort;
}
public function setEffort(?TaskEffort $effort): static
{
$this->effort = $effort;
return $this;
}
public function getPriority(): ?TaskPriority
{
return $this->priority;
}
public function setPriority(?TaskPriority $priority): static
{
$this->priority = $priority;
return $this;
}
public function getAssignee(): ?User
{
return $this->assignee;
}
public function setAssignee(?User $assignee): static
{
$this->assignee = $assignee;
return $this;
}
/** @return Collection<int, User> */
public function getCollaborators(): Collection
{
return $this->collaborators;
}
public function addCollaborator(User $user): static
{
if (!$this->collaborators->contains($user)) {
$this->collaborators->add($user);
}
return $this;
}
public function removeCollaborator(User $user): static
{
$this->collaborators->removeElement($user);
return $this;
}
public function getGroup(): ?TaskGroup
{
return $this->group;
}
public function setGroup(?TaskGroup $group): static
{
$this->group = $group;
return $this;
}
public function getProject(): ?Project
{
return $this->project;
}
public function setProject(?Project $project): static
{
$this->project = $project;
return $this;
}
/** @return Collection<int, TaskTag> */
public function getTags(): Collection
{
return $this->tags;
}
public function addTag(TaskTag $tag): static
{
if (!$this->tags->contains($tag)) {
$this->tags->add($tag);
}
return $this;
}
public function removeTag(TaskTag $tag): static
{
$this->tags->removeElement($tag);
return $this;
}
public function isArchived(): bool
{
return $this->archived;
}
public function setArchived(bool $archived): static
{
$this->archived = $archived;
return $this;
}
/** @return Collection<int, TaskDocument> */
public function getDocuments(): Collection
{
return $this->documents;
}
public function getScheduledStart(): ?DateTimeImmutable
{
return $this->scheduledStart;
}
public function setScheduledStart(?DateTimeImmutable $scheduledStart): static
{
$this->scheduledStart = $scheduledStart;
return $this;
}
public function getScheduledEnd(): ?DateTimeImmutable
{
return $this->scheduledEnd;
}
public function setScheduledEnd(?DateTimeImmutable $scheduledEnd): static
{
$this->scheduledEnd = $scheduledEnd;
return $this;
}
public function getDeadline(): ?DateTimeImmutable
{
return $this->deadline;
}
public function setDeadline(?DateTimeImmutable $deadline): static
{
$this->deadline = $deadline;
return $this;
}
public function isSyncToCalendar(): bool
{
return $this->syncToCalendar;
}
public function setSyncToCalendar(bool $syncToCalendar): static
{
$this->syncToCalendar = $syncToCalendar;
return $this;
}
public function getCalendarEventUid(): ?string
{
return $this->calendarEventUid;
}
public function setCalendarEventUid(?string $calendarEventUid): static
{
$this->calendarEventUid = $calendarEventUid;
return $this;
}
public function getCalendarTodoUid(): ?string
{
return $this->calendarTodoUid;
}
public function setCalendarTodoUid(?string $calendarTodoUid): static
{
$this->calendarTodoUid = $calendarTodoUid;
return $this;
}
public function getCalendarSyncError(): ?string
{
return $this->calendarSyncError;
}
public function setCalendarSyncError(?string $calendarSyncError): static
{
$this->calendarSyncError = $calendarSyncError;
return $this;
}
public function getRecurrence(): ?TaskRecurrence
{
return $this->recurrence;
}
public function setRecurrence(?TaskRecurrence $recurrence): static
{
$this->recurrence = $recurrence;
return $this;
}
#[Assert\Callback]
public function validateScheduledDates(ExecutionContextInterface $context): void
{
if ((null === $this->scheduledStart) !== (null === $this->scheduledEnd)) {
$context->buildViolation('scheduledStart and scheduledEnd must both be set or both be null.')
->atPath('scheduledEnd')
->addViolation()
;
}
if (null !== $this->scheduledStart && null !== $this->scheduledEnd
&& $this->scheduledEnd <= $this->scheduledStart) {
$context->buildViolation('scheduledEnd must be after scheduledStart.')
->atPath('scheduledEnd')
->addViolation()
;
}
}
#[Assert\Callback]
public function validateCollaborators(ExecutionContextInterface $context): void
{
if (null !== $this->assignee && $this->collaborators->contains($this->assignee)) {
$context->buildViolation('The assignee cannot also be a collaborator.')
->atPath('collaborators')
->addViolation()
;
}
}
#[Assert\Callback]
public function validateStatusBelongsToProjectWorkflow(ExecutionContextInterface $context): void
{
if (null === $this->status || null === $this->project) {
return;
}
$projectWorkflow = $this->project->getWorkflow();
$statusWorkflow = $this->status->getWorkflow();
if (null === $projectWorkflow || null === $statusWorkflow) {
return;
}
if ($projectWorkflow->getId() !== $statusWorkflow->getId()) {
$context->buildViolation('Status does not belong to this project\'s workflow.')
->atPath('status')
->addViolation()
;
}
}
}
-115
View File
@@ -1,115 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\TaskBookStackLinkRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: TaskBookStackLinkRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_task_bookstack_link', columns: ['task_id', 'bookstack_id', 'bookstack_type'])]
class TaskBookStackLink
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Task::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private Task $task;
#[ORM\Column]
private int $bookstackId;
#[ORM\Column(length: 10)]
private string $bookstackType;
#[ORM\Column(length: 255)]
private string $title;
#[ORM\Column(length: 500)]
#[Assert\Url]
private string $url;
#[ORM\Column]
private DateTimeImmutable $createdAt;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getTask(): Task
{
return $this->task;
}
public function setTask(Task $task): static
{
$this->task = $task;
return $this;
}
public function getBookstackId(): int
{
return $this->bookstackId;
}
public function setBookstackId(int $bookstackId): static
{
$this->bookstackId = $bookstackId;
return $this;
}
public function getBookstackType(): string
{
return $this->bookstackType;
}
public function setBookstackType(string $bookstackType): static
{
$this->bookstackType = $bookstackType;
return $this;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getUrl(): string
{
return $this->url;
}
public function setUrl(string $url): static
{
$this->url = $url;
return $this;
}
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
}
-190
View File
@@ -1,190 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use App\EventListener\TaskDocumentListener;
use App\State\TaskDocumentProcessor;
use App\State\TaskDocumentProvider;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')", provider: TaskDocumentProvider::class),
new Get(security: "is_granted('ROLE_USER')", provider: TaskDocumentProvider::class),
new Post(
security: "is_granted('ROLE_ADMIN')",
processor: TaskDocumentProcessor::class,
deserialize: false,
),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_document:read']],
denormalizationContext: ['groups' => ['task_document:write']],
order: ['id' => 'DESC'],
)]
#[ApiFilter(SearchFilter::class, properties: ['task' => 'exact'])]
#[ORM\Entity]
#[ORM\EntityListeners([TaskDocumentListener::class])]
class TaskDocument
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_document:read', 'task:read'])]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Task::class, inversedBy: 'documents')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Groups(['task_document:read', 'task_document:write'])]
private ?Task $task = null;
#[ORM\Column(length: 255)]
#[Groups(['task_document:read', 'task:read'])]
private ?string $originalName = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['task_document:read', 'task:read'])]
private ?string $fileName = null;
/**
* Chemin relatif sur le partage SMB lorsque le document est un lien vers un fichier du partage
* (au lieu d'un fichier uploadé stocké sur disque). Mutuellement exclusif avec fileName.
*/
#[ORM\Column(length: 1024, nullable: true)]
#[Groups(['task_document:read', 'task:read'])]
private ?string $sharePath = null;
#[ORM\Column(length: 100)]
#[Groups(['task_document:read', 'task:read'])]
private ?string $mimeType = null;
#[ORM\Column]
#[Groups(['task_document:read', 'task:read'])]
private ?int $size = null;
#[ORM\Column(type: 'datetime_immutable')]
#[Groups(['task_document:read', 'task:read'])]
private ?DateTimeImmutable $createdAt = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['task_document:read', 'task:read'])]
private ?User $uploadedBy = null;
public function getId(): ?int
{
return $this->id;
}
public function getTask(): ?Task
{
return $this->task;
}
public function setTask(?Task $task): static
{
$this->task = $task;
return $this;
}
public function getOriginalName(): ?string
{
return $this->originalName;
}
public function setOriginalName(string $originalName): static
{
$this->originalName = $originalName;
return $this;
}
public function getFileName(): ?string
{
return $this->fileName;
}
public function setFileName(?string $fileName): static
{
$this->fileName = $fileName;
return $this;
}
public function getSharePath(): ?string
{
return $this->sharePath;
}
public function setSharePath(?string $sharePath): static
{
$this->sharePath = $sharePath;
return $this;
}
public function isShareLink(): bool
{
return null !== $this->sharePath;
}
public function getMimeType(): ?string
{
return $this->mimeType;
}
public function setMimeType(string $mimeType): static
{
$this->mimeType = $mimeType;
return $this;
}
public function getSize(): ?int
{
return $this->size;
}
public function setSize(int $size): static
{
$this->size = $size;
return $this;
}
public function getCreatedAt(): ?DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUploadedBy(): ?User
{
return $this->uploadedBy;
}
public function setUploadedBy(?User $uploadedBy): static
{
$this->uploadedBy = $uploadedBy;
return $this;
}
}
-58
View File
@@ -1,58 +0,0 @@
<?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\Repository\TaskEffortRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_effort:read']],
denormalizationContext: ['groups' => ['task_effort:write']],
order: ['label' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: TaskEffortRepository::class)]
class TaskEffort
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_effort:read', 'task:read'])]
private ?int $id = null;
#[ORM\Column(length: 50)]
#[Groups(['task_effort:read', 'task_effort:write', 'task:read'])]
private ?string $label = null;
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): static
{
$this->label = $label;
return $this;
}
}
-128
View File
@@ -1,128 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
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\Repository\TaskGroupRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_group:read']],
denormalizationContext: ['groups' => ['task_group:write']],
order: ['title' => 'ASC'],
)]
#[ApiFilter(SearchFilter::class, properties: ['project' => 'exact'])]
#[ApiFilter(BooleanFilter::class, properties: ['archived'])]
#[ORM\Entity(repositoryClass: TaskGroupRepository::class)]
class TaskGroup
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_group:read', 'task:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['task_group:read', 'task_group:write', 'task:read'])]
private ?string $title = null;
#[ORM\Column(type: 'text', nullable: true)]
#[Groups(['task_group:read', 'task_group:write'])]
private ?string $description = null;
#[ORM\Column(length: 7)]
#[Groups(['task_group:read', 'task_group:write', 'task:read'])]
private ?string $color = '#222783';
#[ORM\ManyToOne(targetEntity: Project::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Groups(['task_group:read', 'task_group:write'])]
private ?Project $project = null;
#[ORM\Column(type: 'boolean')]
#[Groups(['task_group:read', 'task_group:write', 'task:read'])]
private bool $archived = false;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): static
{
$this->color = $color;
return $this;
}
public function getProject(): ?Project
{
return $this->project;
}
public function setProject(?Project $project): static
{
$this->project = $project;
return $this;
}
public function isArchived(): bool
{
return $this->archived;
}
public function setArchived(bool $archived): static
{
$this->archived = $archived;
return $this;
}
}
-88
View File
@@ -1,88 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\TaskMailLinkRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TaskMailLinkRepository::class)]
#[ORM\Table(name: 'task_mail_link')]
#[ORM\UniqueConstraint(name: 'uq_task_mail_link', columns: ['task_id', 'mail_message_id'])]
class TaskMailLink
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Task::class)]
#[ORM\JoinColumn(name: 'task_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
private Task $task;
#[ORM\ManyToOne(targetEntity: MailMessage::class)]
#[ORM\JoinColumn(name: 'mail_message_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
private MailMessage $mailMessage;
#[ORM\Column(type: 'datetimetz_immutable')]
private DateTimeImmutable $linkedAt;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'linked_by_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
private ?User $linkedBy = null;
public function getId(): ?int
{
return $this->id;
}
public function getTask(): Task
{
return $this->task;
}
public function setTask(Task $task): static
{
$this->task = $task;
return $this;
}
public function getMailMessage(): MailMessage
{
return $this->mailMessage;
}
public function setMailMessage(MailMessage $mailMessage): static
{
$this->mailMessage = $mailMessage;
return $this;
}
public function getLinkedAt(): DateTimeImmutable
{
return $this->linkedAt;
}
public function setLinkedAt(DateTimeImmutable $linkedAt): static
{
$this->linkedAt = $linkedAt;
return $this;
}
public function getLinkedBy(): ?User
{
return $this->linkedBy;
}
public function setLinkedBy(?User $linkedBy): static
{
$this->linkedBy = $linkedBy;
return $this;
}
}
-74
View File
@@ -1,74 +0,0 @@
<?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\Repository\TaskPriorityRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_priority:read']],
denormalizationContext: ['groups' => ['task_priority:write']],
order: ['label' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: TaskPriorityRepository::class)]
class TaskPriority
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_priority:read', 'task:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['task_priority:read', 'task_priority:write', 'task:read'])]
private ?string $label = null;
#[ORM\Column(length: 7)]
#[Groups(['task_priority:read', 'task_priority:write', 'task:read'])]
private ?string $color = '#222783';
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): static
{
$this->label = $label;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): static
{
$this->color = $color;
return $this;
}
}
-197
View File
@@ -1,197 +0,0 @@
<?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\RecurrenceType;
use App\Repository\TaskRecurrenceRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_recurrence:read']],
denormalizationContext: ['groups' => ['task_recurrence:write']],
)]
#[ORM\Entity(repositoryClass: TaskRecurrenceRepository::class)]
class TaskRecurrence
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_recurrence:read', 'task:read'])]
private ?int $id = null;
#[ORM\Column(type: 'string', enumType: RecurrenceType::class)]
#[Groups(['task_recurrence:read', 'task_recurrence:write', 'task:read'])]
private ?RecurrenceType $type = null;
#[ORM\Column(type: 'integer')]
#[Groups(['task_recurrence:read', 'task_recurrence:write', 'task:read'])]
private int $interval = 1;
#[ORM\Column(type: 'json', nullable: true)]
#[Groups(['task_recurrence:read', 'task_recurrence:write', 'task:read'])]
private ?array $daysOfWeek = null;
#[ORM\Column(type: 'integer', nullable: true)]
#[Groups(['task_recurrence:read', 'task_recurrence:write', 'task:read'])]
private ?int $dayOfMonth = null;
#[ORM\Column(type: 'integer', nullable: true)]
#[Groups(['task_recurrence:read', 'task_recurrence:write', 'task:read'])]
private ?int $weekOfMonth = null;
#[ORM\Column(type: 'date_immutable', nullable: true)]
#[Groups(['task_recurrence:read', 'task_recurrence:write', 'task:read'])]
private ?DateTimeImmutable $endDate = null;
#[ORM\Column(type: 'integer', nullable: true)]
#[Groups(['task_recurrence:read', 'task_recurrence:write', 'task:read'])]
private ?int $maxOccurrences = null;
#[ORM\Column(type: 'integer')]
#[Groups(['task_recurrence:read'])]
private int $occurrenceCount = 0;
#[ORM\Version]
#[ORM\Column(type: 'integer')]
private int $version = 1;
/** @var Collection<int, Task> */
#[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'recurrence')]
private Collection $tasks;
public function __construct()
{
$this->tasks = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getType(): ?RecurrenceType
{
return $this->type;
}
public function setType(RecurrenceType $type): static
{
$this->type = $type;
return $this;
}
public function getInterval(): int
{
return $this->interval;
}
public function setInterval(int $interval): static
{
$this->interval = $interval;
return $this;
}
public function getDaysOfWeek(): ?array
{
return $this->daysOfWeek;
}
public function setDaysOfWeek(?array $daysOfWeek): static
{
$this->daysOfWeek = $daysOfWeek;
return $this;
}
public function getDayOfMonth(): ?int
{
return $this->dayOfMonth;
}
public function setDayOfMonth(?int $dayOfMonth): static
{
$this->dayOfMonth = $dayOfMonth;
return $this;
}
public function getWeekOfMonth(): ?int
{
return $this->weekOfMonth;
}
public function setWeekOfMonth(?int $weekOfMonth): static
{
$this->weekOfMonth = $weekOfMonth;
return $this;
}
public function getEndDate(): ?DateTimeImmutable
{
return $this->endDate;
}
public function setEndDate(?DateTimeImmutable $endDate): static
{
$this->endDate = $endDate;
return $this;
}
public function getMaxOccurrences(): ?int
{
return $this->maxOccurrences;
}
public function setMaxOccurrences(?int $maxOccurrences): static
{
$this->maxOccurrences = $maxOccurrences;
return $this;
}
public function getOccurrenceCount(): int
{
return $this->occurrenceCount;
}
public function getVersion(): int
{
return $this->version;
}
/** @return Collection<int, Task> */
public function getTasks(): Collection
{
return $this->tasks;
}
public function incrementOccurrenceCount(): static
{
++$this->occurrenceCount;
return $this;
}
}
-143
View File
@@ -1,143 +0,0 @@
<?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\StatusCategory;
use App\Repository\TaskStatusRepository;
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')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_status:read']],
denormalizationContext: ['groups' => ['task_status:write']],
order: ['position' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: TaskStatusRepository::class)]
class TaskStatus
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_status:read', 'task:read', 'workflow:read', 'project:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['task_status:read', 'task_status:write', 'task:read', 'workflow:read', 'project:read'])]
private ?string $label = null;
#[ORM\Column(length: 7)]
#[Groups(['task_status:read', 'task_status:write', 'task:read', 'workflow:read', 'project:read'])]
private ?string $color = '#222783';
#[ORM\Column]
#[Groups(['task_status:read', 'task_status:write', 'task:read', 'workflow:read', 'project:read'])]
private ?int $position = 0;
#[ORM\Column(type: 'boolean')]
#[Groups(['task_status:read', 'task_status:write', 'task:read', 'workflow:read', 'project:read'])]
private bool $isFinal = false;
#[ORM\ManyToOne(targetEntity: Workflow::class, inversedBy: 'statuses')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Groups(['task_status:read', 'task_status:write', 'task:read'])]
#[Assert\NotNull]
private ?Workflow $workflow = null;
#[ORM\Column(type: 'string', length: 32, enumType: StatusCategory::class)]
#[Groups(['task_status:read', 'task_status:write', 'task:read', 'workflow:read', 'project:read'])]
#[Assert\NotNull]
private ?StatusCategory $category = null;
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): static
{
$this->label = $label;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): static
{
$this->color = $color;
return $this;
}
public function getPosition(): ?int
{
return $this->position;
}
public function setPosition(int $position): static
{
$this->position = $position;
return $this;
}
public function getIsFinal(): bool
{
return $this->isFinal;
}
public function setIsFinal(bool $isFinal): static
{
$this->isFinal = $isFinal;
return $this;
}
public function getWorkflow(): ?Workflow
{
return $this->workflow;
}
public function setWorkflow(?Workflow $workflow): static
{
$this->workflow = $workflow;
return $this;
}
public function getCategory(): ?StatusCategory
{
return $this->category;
}
public function setCategory(StatusCategory $category): static
{
$this->category = $category;
return $this;
}
}
-75
View File
@@ -1,75 +0,0 @@
<?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\Repository\TaskTagRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
normalizationContext: ['groups' => ['task_tag:read']],
denormalizationContext: ['groups' => ['task_tag:write']],
order: ['label' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: TaskTagRepository::class)]
#[ORM\Table(name: 'task_type')]
class TaskTag
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_tag:read', 'task:read', 'time_entry:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['task_tag:read', 'task_tag:write', 'task:read', 'time_entry:read'])]
private ?string $label = null;
#[ORM\Column(length: 7)]
#[Groups(['task_tag:read', 'task_tag:write', 'task:read', 'time_entry:read'])]
private ?string $color = '#222783';
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabel(string $label): static
{
$this->label = $label;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): static
{
$this->color = $color;
return $this;
}
}
-220
View File
@@ -1,220 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
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\Repository\TimeEntryRepository;
use App\State\ActiveTimeEntryProvider;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(security: "is_granted('ROLE_USER')"),
new GetCollection(
name: 'time_entries_range',
uriTemplate: '/time_entries/range',
description: 'List time entries for a bounded date range without pagination (used by the time-tracking calendar)',
paginationEnabled: false,
security: "is_granted('ROLE_USER')",
),
new GetCollection(
name: 'active_time_entry',
uriTemplate: '/time_entries/active',
provider: ActiveTimeEntryProvider::class,
description: 'Get the active timer for the current user',
paginationEnabled: false,
security: "is_granted('ROLE_USER')",
),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_USER')"),
new Patch(security: "is_granted('ROLE_ADMIN') or object.getUser() == user"),
new Delete(security: "is_granted('ROLE_ADMIN') or object.getUser() == user"),
],
normalizationContext: ['groups' => ['time_entry:read']],
denormalizationContext: ['groups' => ['time_entry:write']],
order: ['startedAt' => 'DESC'],
)]
#[ApiFilter(SearchFilter::class, properties: ['user' => 'exact', 'project' => 'exact', 'tags' => 'exact'])]
#[ApiFilter(DateFilter::class, properties: ['startedAt'])]
#[ORM\Entity(repositoryClass: TimeEntryRepository::class)]
#[ORM\UniqueConstraint(name: 'uniq_active_timer', columns: ['user_id'], options: ['where' => '(stopped_at IS NULL)'])]
class TimeEntry
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['time_entry:read'])]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['time_entry:read', 'time_entry:write'])]
private ?string $title = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
#[Groups(['time_entry:read', 'time_entry:write'])]
private ?string $description = null;
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE)]
#[Groups(['time_entry:read', 'time_entry:write'])]
private ?DateTimeImmutable $startedAt = null;
#[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: true)]
#[Groups(['time_entry:read', 'time_entry:write'])]
private ?DateTimeImmutable $stoppedAt = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Groups(['time_entry:read', 'time_entry:write'])]
private ?User $user = null;
#[ORM\ManyToOne(targetEntity: Project::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['time_entry:read', 'time_entry:write'])]
private ?Project $project = null;
#[ORM\ManyToOne(targetEntity: Task::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
#[Groups(['time_entry:read', 'time_entry:write'])]
private ?Task $task = null;
/** @var Collection<int, TaskTag> */
#[ORM\ManyToMany(targetEntity: TaskTag::class)]
#[ORM\JoinTable(
name: 'time_entry_task_type',
joinColumns: [new ORM\JoinColumn(name: 'time_entry_id', referencedColumnName: 'id')],
inverseJoinColumns: [new ORM\JoinColumn(name: 'task_type_id', referencedColumnName: 'id')],
)]
#[Groups(['time_entry:read', 'time_entry:write'])]
private Collection $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(?string $title): static
{
$this->title = $title;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getStartedAt(): ?DateTimeImmutable
{
return $this->startedAt;
}
public function setStartedAt(DateTimeImmutable $startedAt): static
{
$this->startedAt = $startedAt;
return $this;
}
public function getStoppedAt(): ?DateTimeImmutable
{
return $this->stoppedAt;
}
public function setStoppedAt(?DateTimeImmutable $stoppedAt): static
{
$this->stoppedAt = $stoppedAt;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): static
{
$this->user = $user;
return $this;
}
public function getProject(): ?Project
{
return $this->project;
}
public function setProject(?Project $project): static
{
$this->project = $project;
return $this;
}
public function getTask(): ?Task
{
return $this->task;
}
public function setTask(?Task $task): static
{
$this->task = $task;
return $this;
}
/** @return Collection<int, TaskTag> */
public function getTags(): Collection
{
return $this->tags;
}
public function addTag(TaskTag $tag): static
{
if (!$this->tags->contains($tag)) {
$this->tags->add($tag);
}
return $this;
}
public function removeTag(TaskTag $tag): static
{
$this->tags->removeElement($tag);
return $this;
}
}
-375
View File
@@ -1,375 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiProperty;
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\ContractType;
use App\Repository\UserRepository;
use App\State\MeProvider;
use App\State\UserPasswordHasherProcessor;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new Get(
uriTemplate: '/me',
provider: MeProvider::class,
normalizationContext: ['groups' => ['me:read']],
),
new Get(
normalizationContext: ['groups' => ['user:list']],
),
new GetCollection(
paginationEnabled: false,
normalizationContext: ['groups' => ['user:list']],
),
new Post(security: "is_granted('ROLE_ADMIN')", processor: UserPasswordHasherProcessor::class),
new Patch(security: "is_granted('ROLE_ADMIN')", processor: UserPasswordHasherProcessor::class),
new Delete(security: "is_granted('ROLE_ADMIN')"),
],
denormalizationContext: ['groups' => ['user:write']],
)]
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['me:read', 'task:read', 'user:list', 'time_entry: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', 'absence_request:read', 'absence_balance:read'])]
private ?string $username = null;
#[ORM\Column(length: 100, nullable: true)]
#[Groups(['me:read', 'user:list', 'user:write'])]
private ?string $firstName = null;
#[ORM\Column(length: 100, nullable: true)]
#[Groups(['me:read', 'user:list', 'user:write'])]
private ?string $lastName = null;
/** @var list<string> */
#[ORM\Column]
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
#[Groups(['me:read', 'user:list', 'user:write'])]
private array $roles = [];
#[ORM\Column]
private ?string $password = null;
#[Groups(['user:write'])]
private ?string $plainPassword = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
private ?DateTimeImmutable $createdAt = null;
#[ORM\Column(length: 64, unique: true, nullable: true)]
#[Groups(['me:read'])]
private ?string $apiToken = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $avatarFileName = null;
// --- HR / absence management fields (readable only by an admin or the user themselves) ---
/** Whether this user is an employee subject to absence management. */
#[ORM\Column]
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
#[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)]
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
#[Groups(['me:read', 'user:list', 'user:write'])]
private ?DateTimeImmutable $hireDate = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
#[Groups(['me:read', 'user:list', 'user:write'])]
private ?DateTimeImmutable $endDate = null;
#[ORM\Column(type: Types::STRING, length: 16, nullable: true, enumType: ContractType::class)]
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
#[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)]
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
#[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)]
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
#[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)]
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
#[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)]
#[ApiProperty(security: "is_granted('ROLE_ADMIN') or object == user")]
#[Groups(['me:read', 'user:list', 'user:write'])]
private float $initialLeaveBalance = 0.0;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(string $username): static
{
$this->username = $username;
return $this;
}
public function getFirstName(): ?string
{
return $this->firstName;
}
public function setFirstName(?string $firstName): static
{
$this->firstName = $firstName;
return $this;
}
public function getLastName(): ?string
{
return $this->lastName;
}
public function setLastName(?string $lastName): static
{
$this->lastName = $lastName;
return $this;
}
public function getUserIdentifier(): string
{
return (string) $this->username;
}
/** @return list<string> */
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return array_values(array_unique($roles));
}
/** @param list<string> $roles */
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
public function getCreatedAt(): ?DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getApiToken(): ?string
{
return $this->apiToken;
}
public function setApiToken(?string $apiToken): static
{
$this->apiToken = $apiToken;
return $this;
}
public function getAvatarFileName(): ?string
{
return $this->avatarFileName;
}
public function setAvatarFileName(?string $avatarFileName): static
{
$this->avatarFileName = $avatarFileName;
return $this;
}
#[Groups(['me:read', 'task:read', 'user:list', 'time_entry:read', 'absence_request:read', 'absence_balance:read'])]
public function getAvatarUrl(): ?string
{
if (null === $this->avatarFileName) {
return null;
}
return '/api/users/'.$this->id.'/avatar';
}
public function getPlainPassword(): ?string
{
return $this->plainPassword;
}
public function setPlainPassword(?string $plainPassword): static
{
$this->plainPassword = $plainPassword;
return $this;
}
public function eraseCredentials(): void
{
$this->plainPassword = null;
}
public function getIsEmployee(): 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;
}
}
-131
View File
@@ -1,131 +0,0 @@
<?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\Repository\WorkflowRepository;
use App\State\WorkflowDeleteProcessor;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
operations: [
new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')", processor: WorkflowDeleteProcessor::class),
],
normalizationContext: ['groups' => ['workflow:read']],
denormalizationContext: ['groups' => ['workflow:write']],
order: ['position' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: WorkflowRepository::class)]
#[UniqueEntity(fields: ['name'], message: 'Ce nom de workflow est déjà utilisé.')]
class Workflow
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['workflow:read', 'project:read', 'task_status:read'])]
private ?int $id = null;
#[ORM\Column(length: 255, unique: true)]
#[Groups(['workflow:read', 'workflow:write', 'project:read'])]
#[Assert\NotBlank]
private ?string $name = null;
#[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['workflow:read', 'workflow:write'])]
private bool $isDefault = false;
#[ORM\Column(type: 'integer', options: ['default' => 0])]
#[Groups(['workflow:read', 'workflow:write'])]
private int $position = 0;
/** @var Collection<int, TaskStatus> */
#[ORM\OneToMany(targetEntity: TaskStatus::class, mappedBy: 'workflow', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['position' => 'ASC'])]
#[Groups(['workflow:read', 'project:read'])]
private Collection $statuses;
public function __construct()
{
$this->statuses = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function isDefault(): bool
{
return $this->isDefault;
}
public function setIsDefault(bool $isDefault): static
{
$this->isDefault = $isDefault;
return $this;
}
public function getPosition(): int
{
return $this->position;
}
public function setPosition(int $position): static
{
$this->position = $position;
return $this;
}
/** @return Collection<int, TaskStatus> */
public function getStatuses(): Collection
{
return $this->statuses;
}
public function addStatus(TaskStatus $status): static
{
if (!$this->statuses->contains($status)) {
$this->statuses->add($status);
$status->setWorkflow($this);
}
return $this;
}
public function removeStatus(TaskStatus $status): static
{
$this->statuses->removeElement($status);
return $this;
}
}
-104
View File
@@ -1,104 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\ZimbraConfigurationRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: ZimbraConfigurationRepository::class)]
class ZimbraConfiguration
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
#[Assert\Url]
private ?string $serverUrl = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $username = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $encryptedPassword = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $calendarPath = null;
#[ORM\Column(type: 'boolean')]
private bool $enabled = false;
public function getId(): ?int
{
return $this->id;
}
public function getServerUrl(): ?string
{
return $this->serverUrl;
}
public function setServerUrl(?string $serverUrl): static
{
$this->serverUrl = $serverUrl;
return $this;
}
public function getUsername(): ?string
{
return $this->username;
}
public function setUsername(?string $username): static
{
$this->username = $username;
return $this;
}
public function getEncryptedPassword(): ?string
{
return $this->encryptedPassword;
}
public function setEncryptedPassword(?string $encryptedPassword): static
{
$this->encryptedPassword = $encryptedPassword;
return $this;
}
public function getCalendarPath(): ?string
{
return $this->calendarPath;
}
public function setCalendarPath(?string $calendarPath): static
{
$this->calendarPath = $calendarPath;
return $this;
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function setEnabled(bool $enabled): static
{
$this->enabled = $enabled;
return $this;
}
public function hasPassword(): bool
{
return null !== $this->encryptedPassword;
}
}