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;
+ }
}