Add Étapes (Steps) tab to project detail page
- New "Étapes" tab with ListChecksIcon showing parent tasks (is_parent: true) - Each étape is an expandable card with: numbered purple icon, title, description, status badge, progress bar (done/total child tasks) - Expanding reveals child tasks grouped under each étape with done/pending indicators and status badges - Uses useTabloEtapes hook to fetch parent tasks ordered by position - Empty state when no étapes exist - Full dark mode support Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
afe47554c8
commit
bf9cabe710
1 changed files with 157 additions and 3 deletions
|
|
@ -1,15 +1,18 @@
|
|||
import { cn, toast } from "@xtablo/shared";
|
||||
import type { UserTablo } from "@xtablo/shared/types/tablos.types";
|
||||
import type { KanbanTask } from "@xtablo/shared-types";
|
||||
import type { Etape, KanbanTask } from "@xtablo/shared-types";
|
||||
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
|
||||
import {
|
||||
CalendarIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
CircleCheckIcon,
|
||||
EllipsisVerticalIcon,
|
||||
FileTextIcon,
|
||||
FolderIcon,
|
||||
KanbanIcon,
|
||||
LayoutDashboardIcon,
|
||||
ListChecksIcon,
|
||||
MapIcon,
|
||||
MessageCircleIcon,
|
||||
PlusIcon,
|
||||
|
|
@ -26,7 +29,7 @@ import { TabloDiscussionSection } from "../components/TabloDiscussionSection";
|
|||
import { TabloEventsSection } from "../components/TabloEventsSection";
|
||||
import { TabloFilesSection } from "../components/TabloFilesSection";
|
||||
import { TabloTasksSection } from "../components/TabloTasksSection";
|
||||
import { useAllTasks } from "../hooks/tasks";
|
||||
import { useAllTasks, useTabloEtapes } from "../hooks/tasks";
|
||||
import { useTabloFileNames } from "../hooks/tablo_data";
|
||||
import { useTablosList } from "../hooks/tablos";
|
||||
|
||||
|
|
@ -69,7 +72,8 @@ type TabSection =
|
|||
| "files"
|
||||
| "discussion"
|
||||
| "events"
|
||||
| "tasks";
|
||||
| "tasks"
|
||||
| "etapes";
|
||||
|
||||
const TABS: {
|
||||
id: TabSection;
|
||||
|
|
@ -78,6 +82,7 @@ const TABS: {
|
|||
disabled?: boolean;
|
||||
}[] = [
|
||||
{ id: "overview", label: "Aperçu", icon: LayoutDashboardIcon },
|
||||
{ id: "etapes", label: "Étapes", icon: ListChecksIcon },
|
||||
{ id: "tasks", label: "Tâches", icon: KanbanIcon },
|
||||
{ id: "files", label: "Fichiers", icon: FolderIcon },
|
||||
{ id: "discussion", label: "Discussion", icon: MessageCircleIcon },
|
||||
|
|
@ -127,6 +132,9 @@ export const TabloDetailsPage = () => {
|
|||
(t) => t.tablo_id === tabloId,
|
||||
);
|
||||
|
||||
// Etapes (parent tasks) for this tablo
|
||||
const { data: etapes = [] } = useTabloEtapes(tabloId);
|
||||
|
||||
// Files for this tablo (used in overview)
|
||||
const { data: filesData } = useTabloFileNames(tabloId ?? "");
|
||||
const fileNames = (filesData?.fileNames ?? []).filter(
|
||||
|
|
@ -449,7 +457,153 @@ export const TabloDetailsPage = () => {
|
|||
{activeSection === "events" && (
|
||||
<TabloEventsSection tablo={tablo} isAdmin={isAdmin} />
|
||||
)}
|
||||
|
||||
{activeSection === "etapes" && (
|
||||
<EtapesSection etapes={etapes} tabloTasks={tabloTasks} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Etapes (Steps) section ─────────────────────────────────────────────────
|
||||
|
||||
function EtapesSection({
|
||||
etapes,
|
||||
tabloTasks,
|
||||
}: {
|
||||
etapes: Etape[];
|
||||
tabloTasks: KanbanTask[];
|
||||
}) {
|
||||
const [expandedEtapes, setExpandedEtapes] = useState<Set<string>>(
|
||||
new Set(etapes.map((e) => e.id)),
|
||||
);
|
||||
|
||||
const toggleEtape = (id: string) => {
|
||||
setExpandedEtapes((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const statusConfig: Record<string, { label: string; color: string }> = {
|
||||
todo: { label: "À faire", color: "bg-blue-100 text-blue-700 dark:bg-blue-950/30 dark:text-blue-400" },
|
||||
in_progress: { label: "En cours", color: "bg-yellow-100 text-yellow-700 dark:bg-yellow-950/30 dark:text-yellow-400" },
|
||||
in_review: { label: "Vérification", color: "bg-purple-100 text-purple-700 dark:bg-purple-950/30 dark:text-purple-400" },
|
||||
done: { label: "Terminé", color: "bg-green-100 text-green-700 dark:bg-green-950/30 dark:text-green-400" },
|
||||
};
|
||||
|
||||
if (etapes.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-center">
|
||||
<ListChecksIcon className="w-12 h-12 text-gray-300 dark:text-gray-600 mb-4" />
|
||||
<p className="text-gray-500 dark:text-gray-400 text-lg font-medium">Aucune étape</p>
|
||||
<p className="text-gray-400 dark:text-gray-500 text-sm mt-1">Les étapes permettent de structurer votre projet en grandes phases</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{etapes.map((etape, index) => {
|
||||
const childTasks = tabloTasks.filter((t) => t.parent_task_id === etape.id);
|
||||
const doneCount = childTasks.filter((t) => t.status === "done").length;
|
||||
const totalCount = childTasks.length;
|
||||
const progressPct = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0;
|
||||
const isExpanded = expandedEtapes.has(etape.id);
|
||||
const status = statusConfig[etape.status] ?? statusConfig.todo;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={etape.id}
|
||||
className="bg-white dark:bg-card rounded-xl border border-gray-100 dark:border-gray-700 shadow-sm overflow-hidden"
|
||||
>
|
||||
{/* Etape header */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleEtape(etape.id)}
|
||||
className="w-full flex items-center gap-4 px-5 py-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors text-left"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDownIcon className="w-5 h-5 text-gray-400 shrink-0" />
|
||||
) : (
|
||||
<ChevronRightIcon className="w-5 h-5 text-gray-400 shrink-0" />
|
||||
)}
|
||||
|
||||
<div className="w-8 h-8 rounded-lg bg-[#F4F3FF] dark:bg-purple-900/20 flex items-center justify-center shrink-0">
|
||||
<span className="text-sm font-bold text-[#7F56D9] dark:text-purple-400">
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100 truncate">
|
||||
{etape.title}
|
||||
</h3>
|
||||
{etape.description && (
|
||||
<p className="text-sm text-muted-foreground truncate mt-0.5">
|
||||
{etape.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className={cn("px-2.5 py-1 rounded-full text-xs font-medium shrink-0", status.color)}>
|
||||
{status.label}
|
||||
</span>
|
||||
|
||||
{totalCount > 0 && (
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="w-16 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-green-500 rounded-full transition-all" style={{ width: `${progressPct}%` }} />
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{doneCount}/{totalCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Child tasks */}
|
||||
{isExpanded && childTasks.length > 0 && (
|
||||
<div className="border-t border-gray-100 dark:border-gray-700 divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{childTasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className="flex items-center gap-3 px-5 py-3 pl-16 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
{task.status === "done" ? (
|
||||
<CircleCheckIcon className="w-4 h-4 text-green-500 shrink-0" />
|
||||
) : (
|
||||
<div className="w-4 h-4 rounded-full border-2 border-gray-300 dark:border-gray-600 shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm flex-1 truncate",
|
||||
task.status === "done" ? "line-through text-gray-400" : "text-gray-900 dark:text-gray-100",
|
||||
)}
|
||||
>
|
||||
{task.title}
|
||||
</span>
|
||||
{task.status && (
|
||||
<span className={cn("px-2 py-0.5 rounded-full text-[10px] font-medium shrink-0", (statusConfig[task.status] ?? statusConfig.todo).color)}>
|
||||
{(statusConfig[task.status] ?? statusConfig.todo).label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded && childTasks.length === 0 && (
|
||||
<div className="border-t border-gray-100 dark:border-gray-700 px-5 py-4 pl-16 text-sm text-muted-foreground">
|
||||
Aucune tâche dans cette étape
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue