diff --git a/frontend/components/project/ProjectDrawer.vue b/frontend/components/project/ProjectDrawer.vue index 69cbe45..7be5205 100644 --- a/frontend/components/project/ProjectDrawer.vue +++ b/frontend/components/project/ProjectDrawer.vue @@ -53,6 +53,17 @@ + +
+ +
@@ -171,6 +182,21 @@ async function handleSubmit() { } } +async function handleArchiveToggle() { + if (!props.project) return + isSubmitting.value = true + try { + const newArchived = !props.project.archived + await update(props.project.id, { archived: newArchived }, { + toastSuccessKey: newArchived ? 'projects.archived' : 'projects.unarchived', + }) + emit('saved') + isOpen.value = false + } finally { + isSubmitting.value = false + } +} + onMounted(async () => { try { giteaRepos.value = await listRepositories() diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index b6da4ce..676860a 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -27,7 +27,11 @@ "projects": { "created": "Projet créé avec succès.", "updated": "Projet mis à jour avec succès.", - "deleted": "Projet supprimé avec succès." + "deleted": "Projet supprimé avec succès.", + "archived": "Projet archivé avec succès.", + "unarchived": "Projet désarchivé avec succès.", + "showArchived": "Voir les projets archivés", + "hideArchived": "Masquer les projets archivés" }, "taskStatuses": { "created": "Statut créé avec succès.", @@ -99,6 +103,40 @@ "noTasks": "Aucune tâche", "backlog": "Backlog" }, + "dashboard": { + "title": "Tableau de bord", + "noData": "Aucune donnée", + "noPriority": "Sans priorité", + "noProject": "Sans projet", + "hoursWorked": "Heures travaillées", + "inProgress": "En cours", + "done": "Terminé", + "stats": { + "hoursThisWeek": "Heures cette semaine", + "myActiveTasks": "Mes tâches actives", + "completed": "terminée(s)", + "totalTasks": "Tâches totales", + "unassigned": "non assignée(s)", + "projects": "Projets", + "users": "utilisateur(s)" + }, + "charts": { + "hoursByDay": "Heures par jour (semaine en cours)", + "hoursByProject": "Temps par projet (semaine en cours)", + "tasksByStatus": "Tâches par statut", + "tasksByPriority": "Tâches par priorité", + "tasksByProject": "Tâches par projet" + }, + "days": { + "mon": "Lun", + "tue": "Mar", + "wed": "Mer", + "thu": "Jeu", + "fri": "Ven", + "sat": "Sam", + "sun": "Dim" + } + }, "sidebar": { "myTasks": "Mes tâches" }, diff --git a/frontend/pages/projects/index.vue b/frontend/pages/projects/index.vue index 83f027f..6a94db3 100644 --- a/frontend/pages/projects/index.vue +++ b/frontend/pages/projects/index.vue @@ -2,12 +2,24 @@

Projets

- +
+ + +
@@ -15,11 +27,18 @@ v-for="project in projects" :key="project.id" class="cursor-pointer rounded-[6px] border border-neutral-200 bg-tertiary-500 p-4 shadow-sm transition hover:shadow-md" + :class="{ 'opacity-60': project.archived }" @click="navigateTo(`/projects/${project.id}`)" >

{{ project.name }}

+ + Archivé +
@@ -66,12 +85,13 @@ const clients = ref([]) const isLoading = ref(true) const drawerOpen = ref(false) const selectedProject = ref(null) +const showArchived = ref(false) async function loadData() { isLoading.value = true try { const [p, c] = await Promise.all([ - projectService.getAll(), + projectService.getAll({ archived: showArchived.value }), clientService.getAll(), ]) projects.value = p @@ -81,6 +101,11 @@ async function loadData() { } } +function toggleArchived() { + showArchived.value = !showArchived.value + loadData() +} + function openCreate() { selectedProject.value = null drawerOpen.value = true diff --git a/frontend/services/dto/project.ts b/frontend/services/dto/project.ts index 21b5de3..c999f07 100644 --- a/frontend/services/dto/project.ts +++ b/frontend/services/dto/project.ts @@ -10,6 +10,7 @@ export type Project = { client: Client | null giteaOwner: string | null giteaRepo: string | null + archived: boolean } export type ProjectWrite = { @@ -20,4 +21,5 @@ export type ProjectWrite = { client: string | null // IRI : "/api/clients/1" ou null giteaOwner?: string | null giteaRepo?: string | null + archived?: boolean } diff --git a/frontend/services/projects.ts b/frontend/services/projects.ts index a61d98a..2a69c56 100644 --- a/frontend/services/projects.ts +++ b/frontend/services/projects.ts @@ -5,8 +5,9 @@ import { extractHydraMembers } from '~/utils/api' export function useProjectService() { const api = useApi() - async function getAll(): Promise { - const data = await api.get>('/projects') + async function getAll(params?: { archived?: boolean }): Promise { + const query = params?.archived !== undefined ? `?archived=${params.archived}` : '' + const data = await api.get>(`/projects${query}`) return extractHydraMembers(data) } @@ -20,9 +21,9 @@ export function useProjectService() { }) } - async function update(id: number, payload: Partial): Promise { + async function update(id: number, payload: Partial, options?: { toastSuccessKey?: string }): Promise { return api.patch(`/projects/${id}`, payload as Record, { - toastSuccessKey: 'projects.updated', + toastSuccessKey: options?.toastSuccessKey ?? 'projects.updated', }) } diff --git a/migrations/Version20260314075537.php b/migrations/Version20260314075537.php new file mode 100644 index 0000000..f8da958 --- /dev/null +++ b/migrations/Version20260314075537.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE project ADD archived BOOLEAN DEFAULT false NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE project DROP archived'); + } +} diff --git a/src/Entity/Project.php b/src/Entity/Project.php index b3e5c08..fd2751d 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -4,6 +4,8 @@ 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; @@ -31,6 +33,7 @@ use Symfony\Component\Validator\Constraints as Assert; 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 @@ -48,7 +51,7 @@ class Project private ?string $code = null; #[ORM\Column(length: 255)] - #[Groups(['project:read', 'project:write', 'time_entry:read'])] + #[Groups(['project:read', 'project:write', 'time_entry:read', 'task:read'])] private ?string $name = null; #[ORM\Column(type: 'text', nullable: true)] @@ -56,7 +59,7 @@ class Project private ?string $description = null; #[ORM\Column(length: 7)] - #[Groups(['project:read', 'project:write', 'time_entry:read'])] + #[Groups(['project:read', 'project:write', 'time_entry:read', 'task:read'])] private ?string $color = '#222783'; #[ORM\ManyToOne(targetEntity: Client::class, inversedBy: 'projects')] @@ -72,6 +75,10 @@ class Project #[Groups(['project:read', 'project:write', 'task:read'])] private ?string $giteaRepo = null; + #[ORM\Column] + #[Groups(['project:read', 'project:write'])] + private bool $archived = false; + public function getId(): ?int { return $this->id; @@ -165,4 +172,16 @@ class Project { 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; + } }