diff --git a/apps/main/src/components/gantt/GanttChart.tsx b/apps/main/src/components/gantt/GanttChart.tsx index dee1d11..ea45c4d 100644 --- a/apps/main/src/components/gantt/GanttChart.tsx +++ b/apps/main/src/components/gantt/GanttChart.tsx @@ -1,5 +1,5 @@ import { LoadingSpinner } from "@ui/components/LoadingSpinner"; -import type { KanbanTask } from "@xtablo/shared-types"; +import type { KanbanTask, TaskStatus } from "@xtablo/shared-types"; import { Button } from "@xtablo/ui/components/button"; import { DropdownMenu, @@ -36,11 +36,16 @@ type GanttTask = KanbanTask & { interface GanttChartProps { tasks: GanttTask[]; isLoading: boolean; + onDateClick?: (date: Date) => void; + onTaskStatusChange?: (taskId: string, status: TaskStatus) => void; } // ─── Helpers ───────────────────────────────────────────────────────────────── -const STATUS_STYLES: Record = { +const STATUS_STYLES: Record< + string, + { bg: string; border: string; dot: string; label: string } +> = { todo: { bg: "bg-[#EFF8FF]", border: "border-l-[#3B82F6]", @@ -74,6 +79,13 @@ const STATUS_TEXT_COLORS: Record = { done: "text-[#16B364]", }; +const ROADMAP_TASK_STATUSES: TaskStatus[] = [ + "todo", + "in_progress", + "in_review", + "done", +]; + function getTabloIcon(color: string | null | undefined) { switch (color) { case "bg-blue-500": @@ -125,7 +137,9 @@ function isSameDay(a: Date, b: Date): boolean { } function formatShortDay(date: Date): string { - return date.toLocaleDateString("fr-FR", { weekday: "short" }).replace(".", ""); + return date + .toLocaleDateString("fr-FR", { weekday: "short" }) + .replace(".", ""); } function formatDateRange(start: Date, end: Date): string { @@ -147,7 +161,12 @@ const CARD_TOP_OFFSET = 20; // ─── Component ─────────────────────────────────────────────────────────────── -export function GanttChart({ tasks, isLoading }: GanttChartProps) { +export function GanttChart({ + tasks, + isLoading, + onDateClick, + onTaskStatusChange, +}: GanttChartProps) { const [weekOffset, setWeekOffset] = useState(0); const [viewMode, setViewMode] = useState("weekly"); const containerRef = useRef(null); @@ -179,12 +198,18 @@ export function GanttChart({ tasks, isLoading }: GanttChartProps) { return d; }, []); - const periodStart = useMemo(() => addDays(getMonday(today), weekOffset * 7), [today, weekOffset]); - const periodEnd = useMemo(() => addDays(periodStart, numDays - 1), [periodStart, numDays]); + const periodStart = useMemo( + () => addDays(getMonday(today), weekOffset * 7), + [today, weekOffset], + ); + const periodEnd = useMemo( + () => addDays(periodStart, numDays - 1), + [periodStart, numDays], + ); const days = useMemo( () => Array.from({ length: numDays }, (_, i) => addDays(periodStart, i)), - [periodStart, numDays] + [periodStart, numDays], ); // Filter tasks with due_date in this period @@ -235,7 +260,10 @@ export function GanttChart({ tasks, isLoading }: GanttChartProps) { // Compute chart height const maxRow = positionedTasks.reduce((max, pt) => Math.max(max, pt.row), 0); - const chartHeight = Math.max(400, (maxRow + 1) * (cardHeight + CARD_GAP) + CARD_TOP_OFFSET + 20); + const chartHeight = Math.max( + 400, + (maxRow + 1) * (cardHeight + CARD_GAP) + CARD_TOP_OFFSET + 20, + ); // Today indicator position const todayIndex = days.findIndex((d) => isSameDay(d, today)); @@ -316,32 +344,50 @@ export function GanttChart({ tasks, isLoading }: GanttChartProps) { {days.map((day, i) => { const isToday = isSameDay(day, today); return ( -
onDateClick?.(new Date(day))} + className="flex flex-col items-center py-3 relative transition-colors hover:bg-accent/40" style={{ width: colWidth }} > {formatShortDay(day)} {day.getDate()} -
+ ); })} {/* Grid body */}
+ {/* Clickable day columns */} +
+ {days.map((day, i) => ( +
+ {/* Vertical grid lines */} -
+
{days.map((_, i) => (
))} @@ -350,8 +396,12 @@ export function GanttChart({ tasks, isLoading }: GanttChartProps) { {/* Today indicator */} {todayInRange && (
@@ -362,34 +412,27 @@ export function GanttChart({ tasks, isLoading }: GanttChartProps) { {/* Task cards */} {positionedTasks.map((pt) => { - const status = STATUS_STYLES[pt.task.status ?? "todo"] ?? STATUS_STYLES.todo; + const status = + STATUS_STYLES[pt.task.status ?? "todo"] ?? STATUS_STYLES.todo; const textColor = - STATUS_TEXT_COLORS[pt.task.status ?? "todo"] ?? STATUS_TEXT_COLORS.todo; - const TabloIcon = pt.task.tablos ? getTabloIcon(pt.task.tablos.color) : null; + STATUS_TEXT_COLORS[pt.task.status ?? "todo"] ?? + STATUS_TEXT_COLORS.todo; + const TabloIcon = pt.task.tablos + ? getTabloIcon(pt.task.tablos.color) + : null; const isCompact = viewMode === "biweekly"; - - return ( -
+ const taskCardContent = ( + <> {/* Status badge */}
- + {!isCompact && ( - + {status.label} )} @@ -399,7 +442,7 @@ export function GanttChart({ tasks, isLoading }: GanttChartProps) {

{pt.task.title} @@ -420,7 +463,7 @@ export function GanttChart({ tasks, isLoading }: GanttChartProps) {
@@ -430,13 +473,87 @@ export function GanttChart({ tasks, isLoading }: GanttChartProps) {
)} -

+ + ); + + if (!onTaskStatusChange) { + return ( +
+ {taskCardContent} +
+ ); + } + + return ( + + + + + + {ROADMAP_TASK_STATUSES.map((nextStatus) => { + const isCurrent = nextStatus === pt.task.status; + return ( + + onTaskStatusChange(pt.task.id, nextStatus) + } + className="gap-2" + > + + + {STATUS_STYLES[nextStatus]?.label ?? nextStatus} + {isCurrent ? " (actuel)" : ""} + + + ); + })} + + ); })} {/* Empty state within chart */} {positionedTasks.length === 0 && ( -
+

Aucune tâche avec échéance sur cette période diff --git a/apps/main/src/components/kanban/TaskModal.tsx b/apps/main/src/components/kanban/TaskModal.tsx index 319ec4b..344b827 100644 --- a/apps/main/src/components/kanban/TaskModal.tsx +++ b/apps/main/src/components/kanban/TaskModal.tsx @@ -16,7 +16,12 @@ import { TypographyH2 } from "@xtablo/ui/components/typography"; import { X } from "lucide-react"; import { useEffect, useState } from "react"; import { useTabloMembers } from "../../hooks/tablos"; -import { useCreateTask, useTabloEtapes, useTask, useUpdateTask } from "../../hooks/tasks"; +import { + useCreateTask, + useTabloEtapes, + useTask, + useUpdateTask, +} from "../../hooks/tasks"; import type { TabloMember } from "./types"; interface TaskModalProps { @@ -29,6 +34,7 @@ interface TaskModalProps { etapes?: Etape[]; // Optional - will be fetched if tabloId is provided tablos?: UserTablo[]; // Optional - for tablo selection when creating allowTabloSelection?: boolean; // Whether to show tablo selector + initialDueDate?: Date; } export const TaskModal = ({ @@ -41,6 +47,7 @@ export const TaskModal = ({ etapes: providedEtapes, tablos, allowTabloSelection = false, + initialDueDate, }: TaskModalProps) => { const { data: task = null } = useTask(taskId); const [title, setTitle] = useState(""); @@ -49,20 +56,26 @@ export const TaskModal = ({ const [etapeId, setEtapeId] = useState("none"); const [dueDate, setDueDate] = useState(undefined); const [selectedTabloId, setSelectedTabloId] = useState( - initialTabloId || tablos?.[0]?.id || "" + initialTabloId || tablos?.[0]?.id || "", ); // Determine which tablo to use for fetching data - const tabloIdForFetch = allowTabloSelection ? selectedTabloId : initialTabloId || ""; + const tabloIdForFetch = allowTabloSelection + ? selectedTabloId + : initialTabloId || ""; // Fetch members and etapes for selected tablo if not provided const { data: fetchedMembers = [] } = useTabloMembers(tabloIdForFetch || ""); - const { data: fetchedEtapes = [] } = useTabloEtapes(tabloIdForFetch || undefined); + const { data: fetchedEtapes = [] } = useTabloEtapes( + tabloIdForFetch || undefined, + ); // Use provided or fetched data const members = providedMembers || fetchedMembers; const etapes = providedEtapes || fetchedEtapes; - const currentTabloId = allowTabloSelection ? selectedTabloId : initialTabloId || ""; + const currentTabloId = allowTabloSelection + ? selectedTabloId + : initialTabloId || ""; useEffect(() => { if (task) { @@ -80,12 +93,12 @@ export const TaskModal = ({ setDescription(""); setAssigneeId("unassigned"); setEtapeId("none"); - setDueDate(undefined); + setDueDate(initialDueDate ? new Date(initialDueDate) : undefined); if (allowTabloSelection && tablos && tablos.length > 0) { setSelectedTabloId(tablos[0].id); } } - }, [task, initialTabloId, allowTabloSelection, tablos]); + }, [task, initialTabloId, allowTabloSelection, tablos, initialDueDate]); const { mutate: createTask } = useCreateTask(); const { mutate: updateTask } = useUpdateTask(); @@ -161,7 +174,10 @@ export const TaskModal = ({ {allowTabloSelection && !taskId && tablos && tablos.length > 0 && (

- @@ -209,7 +225,11 @@ export const TaskModal = ({ {/* Due Date */}
- +
{/* Assignee */} diff --git a/apps/main/src/pages/tablo-details.tsx b/apps/main/src/pages/tablo-details.tsx index 4f3997f..3b22ceb 100644 --- a/apps/main/src/pages/tablo-details.tsx +++ b/apps/main/src/pages/tablo-details.tsx @@ -2,7 +2,11 @@ import { LoadingSpinner } from "@ui/components/LoadingSpinner"; import { cn, toast } from "@xtablo/shared"; import type { UserTablo } from "@xtablo/shared/types/tablos.types"; import type { Etape, KanbanTask } from "@xtablo/shared-types"; -import { Avatar, AvatarFallback, AvatarImage } from "@xtablo/ui/components/avatar"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@xtablo/ui/components/avatar"; import { Button } from "@xtablo/ui/components/button"; import { Dialog, @@ -39,7 +43,12 @@ import { Zap, } from "lucide-react"; import { useEffect, useState } from "react"; -import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { + Link, + useNavigate, + useParams, + useSearchParams, +} from "react-router-dom"; import { GanttChart } from "../components/gantt/GanttChart"; import { TaskModal } from "../components/kanban/TaskModal"; import { TabloDiscussionSection } from "../components/TabloDiscussionSection"; @@ -50,7 +59,12 @@ import { useInviteUser } from "../hooks/invite"; import { useTabloFileNames } from "../hooks/tablo_data"; import { usePendingTabloInvitesByTablo } from "../hooks/tablo_invites"; import { useTabloMembers, useTablosList } from "../hooks/tablos"; -import { useAllTasks, useCreateTask, useTabloEtapes } from "../hooks/tasks"; +import { + useAllTasks, + useCreateTask, + useTabloEtapes, + useUpdateTask, +} from "../hooks/tasks"; import { useUser } from "../providers/UserStoreProvider"; // ─── Icon helpers ───────────────────────────────────────────────────────────── @@ -158,6 +172,9 @@ export const TabloDetailsPage = () => { const { data: tablos, isLoading } = useTablosList(); const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); + const [taskModalInitialDueDate, setTaskModalInitialDueDate] = useState< + Date | undefined + >(undefined); const [isShareDialogOpen, setIsShareDialogOpen] = useState(false); const [inviteEmail, setInviteEmail] = useState(""); @@ -179,9 +196,20 @@ export const TabloDetailsPage = () => { }; const filteredMembers = members?.filter( - (member) => !pendingInvites?.some((invite) => invite.invited_email === member.email) + (member) => + !pendingInvites?.some((invite) => invite.invited_email === member.email), ); + const openTaskModal = (dueDate?: Date) => { + setTaskModalInitialDueDate(dueDate ? new Date(dueDate) : undefined); + setIsTaskModalOpen(true); + }; + + const closeTaskModal = () => { + setIsTaskModalOpen(false); + setTaskModalInitialDueDate(undefined); + }; + const sectionParam = searchParams.get("section") as TabSection | null; const activeSection: TabSection = sectionParam && TABS.some((t) => t.id === sectionParam && !t.disabled) @@ -199,10 +227,11 @@ export const TabloDetailsPage = () => { toast.add( { title: "Projet introuvable", - description: "Le projet demandé n'existe pas ou vous n'y avez pas accès", + description: + "Le projet demandé n'existe pas ou vous n'y avez pas accès", type: "error", }, - { timeout: 5000 } + { timeout: 5000 }, ); navigate("/tablos"); } @@ -211,14 +240,18 @@ export const TabloDetailsPage = () => { // Tasks for this tablo (used in overview) const { data: allTasks = [] } = useAllTasks(); - const tabloTasks = (allTasks as KanbanTask[]).filter((t) => t.tablo_id === tabloId); + const tabloTasks = (allTasks as KanbanTask[]).filter( + (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((f) => !f.startsWith(".")); + const fileNames = (filesData?.fileNames ?? []).filter( + (f) => !f.startsWith("."), + ); if (isLoading) { return ( @@ -230,7 +263,11 @@ export const TabloDetailsPage = () => { if (!tablo) return null; - const { label: statusLabel, badgeClass, progress } = getStatusConfig(tablo.status); + const { + label: statusLabel, + badgeClass, + progress, + } = getStatusConfig(tablo.status); const isAdmin = tablo.is_admin; const TabloIcon = getTabloIcon(tablo.color); const iconColor = getTabloIconColor(tablo.color); @@ -244,16 +281,22 @@ export const TabloDetailsPage = () => {
{tablo.image ? ( - {tablo.name} + {tablo.name} ) : ( )}
-

{tablo.name}

+

+ {tablo.name} +

@@ -281,7 +324,9 @@ export const TabloDetailsPage = () => {
Rôle : - {isAdmin ? "Admin" : "Invité"} + + {isAdmin ? "Admin" : "Invité"} +
Créé le : @@ -295,14 +340,22 @@ export const TabloDetailsPage = () => {
Statut : - + {statusLabel}
Progression :
-
+
{progress}%
@@ -320,13 +373,15 @@ export const TabloDetailsPage = () => { key={tab.id} type="button" disabled={tab.disabled} - onClick={() => !tab.disabled && setSearchParams({ section: tab.id })} + onClick={() => + !tab.disabled && setSearchParams({ section: tab.id }) + } className={cn( "flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2", isActive ? "text-[#804EEC] border-[#804EEC]" : "text-[#667085] border-transparent hover:text-gray-900 dark:hover:text-gray-100", - tab.disabled && "opacity-40 cursor-not-allowed" + tab.disabled && "opacity-40 cursor-not-allowed", )} > @@ -355,8 +410,9 @@ export const TabloDetailsPage = () => { Description du projet

- Ce projet regroupe les tâches, fichiers et événements associés. Utilisez les - onglets ci-dessus pour naviguer entre les différentes sections. + Ce projet regroupe les tâches, fichiers et événements + associés. Utilisez les onglets ci-dessus pour naviguer entre + les différentes sections.

@@ -368,7 +424,7 @@ export const TabloDetailsPage = () => {
{fileNames.length === 0 ? ( -

Aucun fichier

+

+ Aucun fichier +

) : ( fileNames.slice(0, 5).map((fileName) => (
{
-

{fileName}

+

+ {fileName} +

)} - {activeSection === "tasks" && } - {activeSection === "files" && } + {activeSection === "tasks" && ( + + )} + {activeSection === "files" && ( + + )} {activeSection === "discussion" && ( )} - {activeSection === "events" && } - - {activeSection === "etapes" && ( - + {activeSection === "events" && ( + )} - {activeSection === "roadmap" && } + {activeSection === "etapes" && ( + + )} + + {activeSection === "roadmap" && ( + + )}
{/* Task Create Modal */} @@ -506,8 +597,9 @@ export const TabloDetailsPage = () => { setIsTaskModalOpen(false)} + onClose={closeTaskModal} initialStatus="todo" + initialDueDate={taskModalInitialDueDate} /> )} @@ -516,7 +608,9 @@ export const TabloDetailsPage = () => { Partager le projet - Invitez des personnes à collaborer sur ce projet + + Invitez des personnes à collaborer sur ce projet +
@@ -591,14 +685,18 @@ export const TabloDetailsPage = () => {
{filteredMembers.map((member) => { const isCurrentUser = member.id === currentUser.id; - const avatarUrl = isCurrentUser ? currentUser.avatar_url : null; + const avatarUrl = isCurrentUser + ? currentUser.avatar_url + : null; return (
- {avatarUrl && } + {avatarUrl && ( + + )} {member.name.charAt(0).toUpperCase()} @@ -636,9 +734,11 @@ function EtapesSection({ tabloId: string; }) { const [expandedEtapes, setExpandedEtapes] = useState>( - new Set(etapes.map((e) => e.id)) + new Set(etapes.map((e) => e.id)), + ); + const [addingTaskToEtape, setAddingTaskToEtape] = useState( + null, ); - const [addingTaskToEtape, setAddingTaskToEtape] = useState(null); const [newTaskTitle, setNewTaskTitle] = useState(""); const { mutate: createTask } = useCreateTask(); @@ -673,15 +773,18 @@ function EtapesSection({ }, in_progress: { label: "En cours", - color: "bg-yellow-100 text-yellow-700 dark:bg-yellow-950/30 dark:text-yellow-400", + 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", + 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", + color: + "bg-green-100 text-green-700 dark:bg-green-950/30 dark:text-green-400", }, }; @@ -689,7 +792,9 @@ function EtapesSection({ return (
-

Aucune étape

+

+ Aucune étape +

Les étapes permettent de structurer votre projet en grandes phases

@@ -700,10 +805,13 @@ function EtapesSection({ return (
{etapes.map((etape, index) => { - const childTasks = tabloTasks.filter((t) => t.parent_task_id === etape.id); + 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 progressPct = + totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0; const isExpanded = expandedEtapes.has(etape.id); // Derive status from child tasks instead of etape.status @@ -756,16 +864,18 @@ function EtapesSection({ className={cn( "flex items-center gap-1 text-xs shrink-0", derivedStatus !== "done" && - new Date(etape.due_date) < new Date(new Date().toDateString()) + new Date(etape.due_date) < + new Date(new Date().toDateString()) ? "text-red-500" - : "text-muted-foreground" + : "text-muted-foreground", )} > - {new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "short" }).format( - new Date(etape.due_date) - )} + {new Intl.DateTimeFormat("fr-FR", { + day: "2-digit", + month: "short", + }).format(new Date(etape.due_date))}
)} @@ -773,7 +883,7 @@ function EtapesSection({ {status.label} @@ -814,7 +924,7 @@ function EtapesSection({ "text-sm flex-1 truncate", task.status === "done" ? "line-through text-gray-400" - : "text-gray-900 dark:text-gray-100" + : "text-gray-900 dark:text-gray-100", )} > {task.title} @@ -824,9 +934,10 @@ function EtapesSection({ className={cn( "flex items-center gap-1 text-xs shrink-0", task.status !== "done" && - new Date(task.due_date) < new Date(new Date().toDateString()) + new Date(task.due_date) < + new Date(new Date().toDateString()) ? "text-red-500" - : "text-muted-foreground" + : "text-muted-foreground", )} > @@ -842,10 +953,14 @@ function EtapesSection({ - {(statusConfig[task.status] ?? statusConfig.todo).label} + { + (statusConfig[task.status] ?? statusConfig.todo) + .label + } )}
@@ -922,6 +1037,24 @@ function EtapesSection({ // ─── Roadmap Section ───────────────────────────────────────────────────────── -function RoadmapSection({ tabloTasks }: { etapes: Etape[]; tabloTasks: KanbanTask[] }) { - return ; +function RoadmapSection({ + tabloTasks, + onDateClick, +}: { + etapes: Etape[]; + tabloTasks: KanbanTask[]; + onDateClick: (date: Date) => void; +}) { + const { mutate: updateTask } = useUpdateTask(); + + return ( + + updateTask({ id: taskId, status }) + } + /> + ); } diff --git a/apps/main/src/pages/tasks.tsx b/apps/main/src/pages/tasks.tsx index 26bbb28..e3eaf4c 100644 --- a/apps/main/src/pages/tasks.tsx +++ b/apps/main/src/pages/tasks.tsx @@ -114,10 +114,15 @@ export function TasksPage() { const [statusFilter, setStatusFilter] = useState("all"); const [assigneeFilter, setAssigneeFilter] = useState("all"); const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); + const [taskModalInitialDueDate, setTaskModalInitialDueDate] = useState< + Date | undefined + >(undefined); const searchQuery = searchParams.get("q") ?? ""; // Get view mode from URL params, default to "kanban" - const viewMode = (searchParams.get("view") as "kanban" | "aggregated" | "roadmap") || "kanban"; + const viewMode = + (searchParams.get("view") as "kanban" | "aggregated" | "roadmap") || + "kanban"; // Function to update view mode in URL const setViewMode = (mode: "kanban" | "aggregated" | "roadmap") => { @@ -133,6 +138,16 @@ export function TasksPage() { // Mutation for updating task status const updateTaskMutation = useUpdateTask(); + const openTaskModal = (dueDate?: Date) => { + setTaskModalInitialDueDate(dueDate ? new Date(dueDate) : undefined); + setIsTaskModalOpen(true); + }; + + const closeTaskModal = () => { + setIsTaskModalOpen(false); + setTaskModalInitialDueDate(undefined); + }; + // Filter and search tasks const filteredTasks = useMemo(() => { let filtered = allTasks as TaskWithTablo[]; @@ -154,7 +169,9 @@ export function TasksPage() { } else if (assigneeFilter === "unassigned") { filtered = filtered.filter((task) => !task.assignee_id); } else { - filtered = filtered.filter((task) => task.assignee_id === assigneeFilter); + filtered = filtered.filter( + (task) => task.assignee_id === assigneeFilter, + ); } } @@ -163,12 +180,20 @@ export function TasksPage() { const q = searchQuery.toLowerCase(); filtered = filtered.filter( (task) => - task.title?.toLowerCase().includes(q) || task.description?.toLowerCase().includes(q) + task.title?.toLowerCase().includes(q) || + task.description?.toLowerCase().includes(q), ); } return filtered; - }, [allTasks, selectedTabloId, statusFilter, assigneeFilter, user.id, searchQuery]); + }, [ + allTasks, + selectedTabloId, + statusFilter, + assigneeFilter, + user.id, + searchQuery, + ]); // Initialize Kanban columns from filtered tasks const columns = useMemo((): KanbanColumn[] => { @@ -239,7 +264,7 @@ export function TasksPage() { const handleDrop = ( e: React.DragEvent, - targetStatus: "todo" | "in_progress" | "in_review" | "done" + targetStatus: "todo" | "in_progress" | "in_review" | "done", ) => { e.preventDefault(); const taskId = e.dataTransfer.getData("taskId"); @@ -280,7 +305,7 @@ export function TasksPage() { {t("pages:tasks.title")} @@ -348,7 +381,7 @@ export function TasksPage() {
{tablo.name} @@ -405,7 +438,14 @@ export function TasksPage() { {/* Main Content */}
{viewMode === "roadmap" ? ( - + + updateTaskMutation.mutate({ id: taskId, status }) + } + /> ) : viewMode === "kanban" ? ( /* Kanban Board */ <> @@ -444,7 +484,9 @@ export function TasksPage() { {/* Column header */}
- +

{column.title}

@@ -454,7 +496,7 @@ export function TasksPage() {
- + { e.stopPropagation(); @@ -518,15 +564,27 @@ export function TasksPage() { Ouvrir la tâche - Déplacer vers - {(["todo", "in_progress", "in_review", "done"] as const) + + Déplacer vers + + {( + [ + "todo", + "in_progress", + "in_review", + "done", + ] as const + ) .filter((s) => s !== task.status) .map((s) => ( { e.stopPropagation(); - updateTaskMutation.mutate({ id: task.id, status: s }); + updateTaskMutation.mutate({ + id: task.id, + status: s, + }); }} > {columnTitles[s]} @@ -542,7 +600,7 @@ export function TasksPage() { "flex items-center text-xs mb-3", isOverdue ? "text-red-500" - : "text-gray-500 dark:text-gray-400" + : "text-gray-500 dark:text-gray-400", )} > @@ -553,17 +611,27 @@ export function TasksPage() { {/* Tablo row */} {taskWithTablo.tablos && (() => { - const TabloIcon = getTabloIcon(taskWithTablo.tablos.color); - const iconColor = getTabloIconColor(taskWithTablo.tablos.color); + const TabloIcon = getTabloIcon( + taskWithTablo.tablos.color, + ); + const iconColor = getTabloIconColor( + taskWithTablo.tablos.color, + ); return (
- +
{taskWithTablo.tablos.name} @@ -576,10 +644,12 @@ export function TasksPage() {
- 0 + + 0
- 0 + + 0
@@ -594,7 +664,9 @@ export function TasksPage() { /> ) : (
- {task.assignee_name?.charAt(0).toUpperCase() || ( + {task.assignee_name + ?.charAt(0) + .toUpperCase() || ( )}
@@ -656,7 +728,9 @@ export function TasksPage() { {/* Column header */}
- +

{column.title}

@@ -666,7 +740,7 @@ export function TasksPage() {
@@ -827,7 +916,10 @@ export function TasksPage() { - + { e.stopPropagation(); @@ -837,8 +929,17 @@ export function TasksPage() { Ouvrir la tâche - Déplacer vers - {(["todo", "in_progress", "in_review", "done"] as const) + + Déplacer vers + + {( + [ + "todo", + "in_progress", + "in_review", + "done", + ] as const + ) .filter((s) => s !== task.status) .map((s) => ( setIsTaskModalOpen(false)} + onClose={closeTaskModal} tablos={tablos} allowTabloSelection={true} initialStatus="todo" + initialDueDate={taskModalInitialDueDate} />
); @@ -889,7 +991,25 @@ export function TasksPage() { type RoadmapTaskWithTablo = KanbanTask & { tablos: { id: string; name: string; color: string | null } | null; }; +type RoadmapTaskStatus = "todo" | "in_progress" | "in_review" | "done"; -function RoadmapView({ tasks, isLoading }: { tasks: RoadmapTaskWithTablo[]; isLoading: boolean }) { - return ; +function RoadmapView({ + tasks, + isLoading, + onDateClick, + onTaskStatusChange, +}: { + tasks: RoadmapTaskWithTablo[]; + isLoading: boolean; + onDateClick: (date: Date) => void; + onTaskStatusChange: (taskId: string, status: RoadmapTaskStatus) => void; +}) { + return ( + + ); }