diff --git a/apps/main/src/components/TabloFilesSection.tsx b/apps/main/src/components/TabloFilesSection.tsx index 476c8e1..82aba48 100644 --- a/apps/main/src/components/TabloFilesSection.tsx +++ b/apps/main/src/components/TabloFilesSection.tsx @@ -10,6 +10,7 @@ import { useTabloFileNames, } from "../hooks/tablo_data"; import { useIsReadOnlyUser } from "../providers/UserStoreProvider"; +import { TypographyH3, TypographyMuted } from "@xtablo/ui/components/typography"; interface TabloFilesSectionProps { tablo: UserTablo; @@ -154,8 +155,10 @@ export const TabloFilesSection = ({ tablo, isAdmin }: TabloFilesSectionProps) => return (
-

Fichiers

-

Gérez les fichiers attachés à ce tablo

+ Fichiers + + Gérez les fichiers attachés à ce tablo +
{/* Error Banner */} diff --git a/apps/main/src/components/TabloOverviewSection.tsx b/apps/main/src/components/TabloOverviewSection.tsx index e16069b..33ad86c 100644 --- a/apps/main/src/components/TabloOverviewSection.tsx +++ b/apps/main/src/components/TabloOverviewSection.tsx @@ -1,38 +1,29 @@ import { useCallback, useMemo, useState } from "react"; import { Button } from "@xtablo/ui/components/button"; import { Input } from "@xtablo/ui/components/input"; -import { - ArrowDown, - ArrowUp, - Check, - Edit2, - Loader2, - Plus, - Trash2, - X, -} from "lucide-react"; +import { Progress } from "@xtablo/ui/components/progress"; +import { ArrowDown, ArrowUp, Check, Edit2, Loader2, Trash2, X } from "lucide-react"; import type { UserTablo } from "@xtablo/shared/types/tablos.types"; -import { TabloFilesSection } from "./TabloFilesSection"; import { useCreateEtape, useDeleteEtape, useReorderEtapes, useTabloEtapes, + useTasksByTablo, useUpdateEtape, } from "../hooks/tasks"; -import { useTablo } from "../hooks/tablos"; -import { useUser } from "../providers/UserStoreProvider"; - +import { TypographyH3, TypographyMuted, TypographyP } from "@xtablo/ui/components/typography"; +import { useTranslation } from "react-i18next"; +import { pluralize, toast } from "@xtablo/shared"; interface TabloOverviewSectionProps { tablo: UserTablo; isAdmin: boolean; } export const TabloOverviewSection = ({ tablo, isAdmin }: TabloOverviewSectionProps) => { - const { data: detailedTablo } = useTablo(tablo.id); - const { id: currentUserId } = useUser(); - + const { t } = useTranslation(); const { data: etapes = [], isLoading: isLoadingEtapes } = useTabloEtapes(tablo.id); + const { data: tasks = [] } = useTasksByTablo(tablo.id); const createEtape = useCreateEtape(); const updateEtape = useUpdateEtape(); const deleteEtape = useDeleteEtape(); @@ -42,17 +33,42 @@ export const TabloOverviewSection = ({ tablo, isAdmin }: TabloOverviewSectionPro const [editingEtapeId, setEditingEtapeId] = useState(null); const [editingTitle, setEditingTitle] = useState(""); - const isOwner = detailedTablo?.owner_id === currentUserId; - const canManageEtapes = isOwner; + const canManageEtapes = isAdmin; - const sortedEtapes = useMemo( - () => [...etapes].sort((a, b) => a.position - b.position), - [etapes] + const sortedEtapes = useMemo(() => [...etapes].sort((a, b) => a.position - b.position), [etapes]); + + // Calculate overall tablo progress + const overallProgress = useMemo(() => { + const totalTasks = tasks.length; + const doneTasks = tasks.filter((task) => task.status === "done").length; + const percentage = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0; + return { total: totalTasks, done: doneTasks, percentage }; + }, [tasks]); + + // Calculate task counts per etape + const getEtapeTaskCounts = useCallback( + (etapeId: string) => { + const etapeTasks = tasks.filter((task) => task.parent_task_id === etapeId); + const total = etapeTasks.length; + const done = etapeTasks.filter((task) => task.status === "done").length; + const ongoing = etapeTasks.filter( + (task) => task.status === "in_progress" || task.status === "in_review" + ).length; + return { total, done, ongoing }; + }, + [tasks] ); const handleCreateEtape = async () => { const title = newEtapeTitle.trim(); - if (!title) return; + if (!title) { + toast.add({ + title: "Erreur", + description: "Le nom de l'Étape est requis", + type: "error", + }); + return; + } await createEtape.mutateAsync({ tabloId: tablo.id, @@ -126,17 +142,17 @@ export const TabloOverviewSection = ({ tablo, isAdmin }: TabloOverviewSectionPro if (!sortedEtapes.length) { return (
-

+ Aucune Étape n'a encore été définie pour ce tablo. -

+ {canManageEtapes ? ( -

+ Créez votre première Étape pour structurer les tâches du tablo. -

+ ) : ( -

+ Seul le propriétaire du tablo peut ajouter des Étapes. -

+ )}
); @@ -207,10 +223,34 @@ export const TabloOverviewSection = ({ tablo, isAdmin }: TabloOverviewSectionPro
) : ( <> -

{etape.title}

-

- Position {etape.position + 1} -

+ + {etape.title} + + + Étape {etape.position + 1} + + {(() => { + const { total, done, ongoing } = getEtapeTaskCounts(etape.id); + return ( +
+ + {total}{" "} + {pluralize("tâche", total)} + + {ongoing > 0 && ( + + {ongoing} en cours + + )} + {done > 0 && ( + + {done}{" "} + {pluralize("terminée", done)} + + )} +
+ ); + })()} )} @@ -246,55 +286,58 @@ export const TabloOverviewSection = ({ tablo, isAdmin }: TabloOverviewSectionPro }; return ( -
-
-
-
-

Vue d'ensemble

-

- Configurez les Étapes du tablo pour clarifier les grandes phases de votre projet. -

-
- {canManageEtapes && ( -
- setNewEtapeTitle(event.target.value)} - placeholder="Ajouter une nouvelle Étape" - className="sm:w-64" - /> - +
+
+ Vue d'ensemble + + Configurez les Étapes du tablo pour clarifier les grandes phases de votre projet. + +
+ + {!canManageEtapes && ( +
+ Seul le propriétaire du tablo peut modifier les Étapes. Contactez l'administrateur si vous + avez besoin d'une nouvelle Étape. +
+ )} + + {/* Overall Progress */} + {overallProgress.total > 0 && ( +
+
+
+ + Progression globale + + + {overallProgress.done} sur {overallProgress.total}{" "} + {pluralize("tâche", overallProgress.total)}{" "} + {pluralize("terminée", overallProgress.done)} +
- )} -
- - {!canManageEtapes && ( -
- Seul le propriétaire du tablo peut modifier les Étapes. Contactez l'administrateur si - vous avez besoin d'une nouvelle Étape. +
{overallProgress.percentage}%
- )} - -
- +
+ )} + +
+
-
-

Fichiers du tablo

- -
+ {canManageEtapes && ( +
+ setNewEtapeTitle(event.target.value)} + placeholder="Nom de l'Étape" + className="h-9 sm:w-64" + /> + +
+ )}
); }; - diff --git a/apps/main/src/components/TabloTasksSection.tsx b/apps/main/src/components/TabloTasksSection.tsx index ed5c2f4..a31221f 100644 --- a/apps/main/src/components/TabloTasksSection.tsx +++ b/apps/main/src/components/TabloTasksSection.tsx @@ -1,7 +1,7 @@ -import { toast } from "@xtablo/shared"; +import { pluralize, toast } from "@xtablo/shared"; import { UserTablo } from "@xtablo/shared/types/tablos.types"; import type { KanbanColumn, KanbanTask, KanbanTaskInsert, TaskStatus } from "@xtablo/shared-types"; -import { ListChecks } from "lucide-react"; +import { AlertTriangle, ListChecks } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTabloMembers } from "../hooks/tablos"; import { useCreateTask, useTabloEtapes, useTasksByTablo, useUpdateTaskPositions } from "../hooks/tasks"; @@ -34,6 +34,11 @@ export const TabloTasksSection = ({ tablo }: TabloTasksSectionProps) => { [etapes] ); + // Check for tasks without parent (orphaned tasks) + const orphanedTasks = useMemo(() => { + return tasks?.filter((task) => !task.parent_task_id) || []; + }, [tasks]); + // Helper functions defined before use const initializeColumns = useCallback((tasks: KanbanTask[]): KanbanColumn[] => { const defaultColumns: KanbanColumn[] = [ @@ -160,6 +165,25 @@ export const TabloTasksSection = ({ tablo }: TabloTasksSectionProps) => {
+ {/* Warning for orphaned tasks */} + {orphanedTasks.length > 0 && ( +
+
+ +
+

+ {orphanedTasks.length} {pluralize("tâche", orphanedTasks.length)} sans Étape +

+

+ {orphanedTasks.length === 1 + ? "Cette tâche n'est associée à aucune Étape. Modifiez-la pour l'associer à une Étape." + : "Ces tâches ne sont associées à aucune Étape. Modifiez-les pour les associer à une Étape."} +

+
+
+
+ )} + {/* Kanban Board */}
{ return (amount * taxRate) / 100; diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 6fa3000..08c48ab 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -17,6 +17,7 @@ export * from "./field"; export * from "./input"; export * from "./label"; export * from "./popover"; +export * from "./progress"; export * from "./select"; export * from "./separator"; export * from "./slider"; diff --git a/packages/ui/src/components/progress.tsx b/packages/ui/src/components/progress.tsx new file mode 100644 index 0000000..3c531c7 --- /dev/null +++ b/packages/ui/src/components/progress.tsx @@ -0,0 +1,30 @@ +import * as React from "react"; +import { cn } from "@xtablo/shared"; + +const Progress = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { value?: number; max?: number } +>(({ className, value = 0, max = 100, ...props }, ref) => { + const percentage = Math.min(Math.max((value / max) * 100, 0), 100); + + return ( +
+
+
+ ); +}); + +Progress.displayName = "Progress"; + +export { Progress }; + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97157d0..cf82e0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -528,6 +528,9 @@ importers: jwt-decode: specifier: ^4.0.0 version: 4.0.0 + pluralize: + specifier: ^8.0.0 + version: 8.0.0 react: specifier: 19.0.0 version: 19.0.0 @@ -559,6 +562,9 @@ importers: '@biomejs/biome': specifier: 2.2.5 version: 2.2.5 + '@types/pluralize': + specifier: ^0.0.33 + version: 0.0.33 '@types/react': specifier: 19.0.10 version: 19.0.10 @@ -4054,6 +4060,9 @@ packages: '@types/phoenix@1.6.6': resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} + '@types/pluralize@0.0.33': + resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} + '@types/raf@3.4.3': resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} @@ -6983,6 +6992,10 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -13024,6 +13037,8 @@ snapshots: '@types/phoenix@1.6.6': {} + '@types/pluralize@0.0.33': {} + '@types/raf@3.4.3': optional: true @@ -16818,6 +16833,8 @@ snapshots: dependencies: find-up: 4.1.0 + pluralize@8.0.0: {} + possible-typed-array-names@1.1.0: {} postcss@8.5.6: