229 lines
7 KiB
TypeScript
229 lines
7 KiB
TypeScript
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 { 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";
|
|
import { getEtapeColor } from "../utils/etapeColors";
|
|
import { KanbanBoard } from "./kanban/KanbanBoard";
|
|
import { TaskModal } from "./kanban/TaskModal";
|
|
|
|
interface TabloTasksSectionProps {
|
|
tablo: UserTablo;
|
|
isAdmin: boolean;
|
|
}
|
|
|
|
export const TabloTasksSection = ({ tablo }: TabloTasksSectionProps) => {
|
|
const { data: members = [] } = useTabloMembers(tablo.id);
|
|
const [columns, setColumns] = useState<KanbanColumn[]>([]);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [selectedTask, setSelectedTask] = useState<KanbanTask | null>(null);
|
|
const [modalStatus, setModalStatus] = useState<TaskStatus>("todo");
|
|
|
|
const { data: tasks } = useTasksByTablo(tablo.id);
|
|
const { data: etapes = [] } = useTabloEtapes(tablo.id);
|
|
const { mutate: updateTaskPositions } = useUpdateTaskPositions();
|
|
const { mutate: createTask } = useCreateTask();
|
|
|
|
const etapeTitleMap = useMemo(
|
|
() =>
|
|
etapes.reduce<Record<string, string>>((map, etape) => {
|
|
map[etape.id] = etape.title;
|
|
return map;
|
|
}, {}),
|
|
[etapes]
|
|
);
|
|
|
|
const etapeColorMap = useMemo(
|
|
() =>
|
|
etapes.reduce<Record<string, ReturnType<typeof getEtapeColor>>>((map, etape) => {
|
|
map[etape.id] = getEtapeColor(etape.position);
|
|
return map;
|
|
}, {}),
|
|
[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[] = [
|
|
{
|
|
id: "todo",
|
|
title: "À faire",
|
|
status: "todo",
|
|
position: 0,
|
|
tasks: tasks.filter((task) => task.status === "todo"),
|
|
},
|
|
{
|
|
id: "in_progress",
|
|
title: "En cours",
|
|
status: "in_progress",
|
|
position: 1,
|
|
tasks: tasks.filter((task) => task.status === "in_progress"),
|
|
},
|
|
{
|
|
id: "in_review",
|
|
title: "Vérification",
|
|
status: "in_review",
|
|
position: 2,
|
|
tasks: tasks.filter((task) => task.status === "in_review"),
|
|
},
|
|
{
|
|
id: "done",
|
|
title: "Terminé",
|
|
status: "done",
|
|
position: 3,
|
|
tasks: tasks.filter((task) => task.status === "done"),
|
|
},
|
|
];
|
|
return defaultColumns;
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setColumns(initializeColumns(tasks ?? []));
|
|
}, [initializeColumns, tasks]);
|
|
|
|
const handleAddTask = (status: TaskStatus) => {
|
|
setSelectedTask(null);
|
|
setModalStatus(status);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleCreateTask = (taskData: {
|
|
title: string;
|
|
description: string;
|
|
assignee_id?: string;
|
|
status: TaskStatus;
|
|
parent_task_id?: string | null;
|
|
}) => {
|
|
const newTask: KanbanTaskInsert = {
|
|
id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
title: taskData.title,
|
|
description: taskData.description,
|
|
status: taskData.status,
|
|
assignee_id: taskData.assignee_id,
|
|
tablo_id: tablo.id,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
position: 0,
|
|
parent_task_id: taskData.parent_task_id ?? null,
|
|
};
|
|
|
|
createTask(newTask);
|
|
|
|
// setColumns((prevColumns) =>
|
|
// prevColumns.map((column: KanbanColumn) => {
|
|
// if (column.status === (taskData.status as TaskStatus)) {
|
|
// return {
|
|
// ...column,
|
|
// tasks: [newTask, ...column.tasks],
|
|
// };
|
|
// }
|
|
// return column;
|
|
// })
|
|
// );
|
|
|
|
toast.add(
|
|
{
|
|
title: "Tâche créée",
|
|
description: `La tâche "${taskData.title}" a été créée avec succès`,
|
|
type: "success",
|
|
},
|
|
{ timeout: 3000 }
|
|
);
|
|
};
|
|
|
|
const handleTaskMove = (taskId: string, newStatus: TaskStatus) => {
|
|
updateTaskPositions([
|
|
{
|
|
id: taskId,
|
|
position: columns.find((column) => column.status === newStatus)?.position ?? 0,
|
|
status: newStatus,
|
|
},
|
|
]);
|
|
|
|
toast.add(
|
|
{
|
|
title: "Tâche déplacée",
|
|
description: "La tâche a été déplacée avec succès",
|
|
type: "success",
|
|
},
|
|
{ timeout: 2000 }
|
|
);
|
|
};
|
|
|
|
const handleTaskClick = (task: KanbanTask) => {
|
|
setSelectedTask(task);
|
|
setModalStatus(task.status ?? "todo");
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-foreground flex items-center gap-3">
|
|
<ListChecks className="w-8 h-8" />
|
|
Tâches
|
|
</h1>
|
|
<p className="text-muted-foreground mt-1">Gérez vos tâches avec un tableau Kanban</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Warning for orphaned tasks */}
|
|
{orphanedTasks.length > 0 && (
|
|
<div className="bg-yellow-50 dark:bg-yellow-950/20 border border-yellow-200 dark:border-yellow-900/50 rounded-lg p-4">
|
|
<div className="flex items-start gap-3">
|
|
<AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
|
{orphanedTasks.length} {pluralize("tâche", orphanedTasks.length)} sans Étape
|
|
</p>
|
|
<p className="text-sm text-yellow-700 dark:text-yellow-300 mt-1">
|
|
{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."}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Kanban Board */}
|
|
<div className="bg-card rounded-lg border border-border p-6">
|
|
<KanbanBoard
|
|
columns={columns}
|
|
members={members}
|
|
etapes={etapes}
|
|
etapeTitles={etapeTitleMap}
|
|
etapeColors={etapeColorMap}
|
|
onTaskClick={handleTaskClick}
|
|
onAddTask={handleAddTask}
|
|
onAddTaskInline={handleCreateTask}
|
|
onTaskMove={handleTaskMove}
|
|
/>
|
|
</div>
|
|
|
|
{/* Task Create Modal */}
|
|
<TaskModal
|
|
tabloId={tablo.id}
|
|
taskId={selectedTask?.id}
|
|
isOpen={isModalOpen}
|
|
onClose={() => setIsModalOpen(false)}
|
|
members={members}
|
|
initialStatus={modalStatus}
|
|
etapes={etapes}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|