Files
towercontrol/components/kanban/SwimlanesBase.tsx
Julien Froidefond c062fcd592 style: update ObjectivesBoard and SwimlanesBase for improved theming
- Replaced hardcoded colors with CSS variables in ObjectivesBoard for better theme consistency.
- Updated background color in SwimlanesBase to use new CSS variable for card columns.
- Enhanced button hover effects to align with the new theming approach.
- Minor adjustments to border colors for better visual coherence.
2025-09-15 11:59:31 +02:00

357 lines
11 KiB
TypeScript

'use client';
import { Task, TaskStatus } from '@/lib/types';
import { TaskCard } from './TaskCard';
import { QuickAddTask } from './QuickAddTask';
import { CreateTaskForm } from '@/components/forms/CreateTaskForm';
import { Button } from '@/components/ui/Button';
import { CreateTaskData } from '@/clients/tasks-client';
import { useState } from 'react';
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
import { getAllStatuses } from '@/lib/status-config';
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
closestCenter,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { useDroppable } from '@dnd-kit/core';
// Composant pour les colonnes droppables
function DroppableColumn({
status,
tasks,
onDeleteTask,
onEditTask,
onUpdateTitle,
compactView,
onCreateTask,
showQuickAdd,
onToggleQuickAdd,
swimlaneContext
}: {
status: TaskStatus;
tasks: Task[];
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
compactView: boolean;
onCreateTask?: (data: CreateTaskData) => Promise<void>;
showQuickAdd?: boolean;
onToggleQuickAdd?: () => void;
swimlaneContext?: {
type: 'tag' | 'priority';
value: string;
};
}) {
const { setNodeRef } = useDroppable({
id: status,
});
return (
<div ref={setNodeRef} className="min-h-[100px] space-y-2">
<SortableContext items={tasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
<div className="space-y-1">
{tasks.map(task => (
<TaskCard
key={task.id}
task={task}
onDelete={onDeleteTask}
onEdit={onEditTask}
onUpdateTitle={onUpdateTitle}
compactView={compactView}
/>
))}
</div>
</SortableContext>
{/* QuickAdd pour cette colonne */}
{onCreateTask && (
<div className="mt-2">
{showQuickAdd ? (
<QuickAddTask
status={status}
onSubmit={onCreateTask}
onCancel={onToggleQuickAdd || (() => {})}
swimlaneContext={swimlaneContext}
/>
) : (
<button
onClick={onToggleQuickAdd}
className="w-full p-2 flex justify-center transition-colors"
title="Ajouter une tâche"
>
<div className="w-5 h-5 rounded-full bg-[var(--card)] hover:bg-[var(--card-hover)] flex items-center justify-center text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-all text-sm">
+
</div>
</button>
)}
</div>
)}
</div>
);
}
// Interface pour une swimlane
export interface SwimlaneData {
key: string;
label: string;
icon?: string;
color?: string;
tasks: Task[];
context?: {
type: 'tag' | 'priority';
value: string;
};
}
interface SwimlanesBaseProps {
tasks: Task[];
title: string;
swimlanes: SwimlaneData[];
onCreateTask?: (data: CreateTaskData) => Promise<Task | null>;
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
compactView?: boolean;
visibleStatuses?: TaskStatus[];
loading?: boolean;
}
export function SwimlanesBase({
tasks,
title,
swimlanes,
onCreateTask,
onDeleteTask,
onEditTask,
onUpdateTitle,
onUpdateStatus,
compactView = false,
visibleStatuses,
loading = false
}: SwimlanesBaseProps) {
const [activeTask, setActiveTask] = useState<Task | null>(null);
const [collapsedSwimlanes, setCollapsedSwimlanes] = useState<Set<string>>(new Set());
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [showQuickAdd, setShowQuickAdd] = useState<{ [key: string]: boolean }>({});
// Gestion de la visibilité des colonnes
const { getVisibleStatuses } = useColumnVisibility();
const allStatuses = getAllStatuses();
const statusesToShow = visibleStatuses ||
getVisibleStatuses(allStatuses.map(s => ({ id: s.key }))).map(s => s.id);
// Configuration des sensors pour le drag & drop
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);
// Handlers pour le drag & drop
const handleDragStart = (event: DragStartEvent) => {
const task = tasks.find(t => t.id === event.active.id);
setActiveTask(task || null);
};
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
setActiveTask(null);
if (!over || !onUpdateStatus) return;
const taskId = active.id as string;
const newStatus = over.id as TaskStatus;
// Vérifier si le statut a changé
const task = tasks.find(t => t.id === taskId);
if (task && task.status !== newStatus) {
await onUpdateStatus(taskId, newStatus);
}
};
// Basculer l'état d'une swimlane
const toggleSwimlane = (swimlaneKey: string) => {
const newCollapsed = new Set(collapsedSwimlanes);
if (newCollapsed.has(swimlaneKey)) {
newCollapsed.delete(swimlaneKey);
} else {
newCollapsed.add(swimlaneKey);
}
setCollapsedSwimlanes(newCollapsed);
};
// Handlers pour la création de tâches
const handleCreateTask = async (data: CreateTaskData) => {
if (onCreateTask) {
await onCreateTask(data);
setIsCreateModalOpen(false);
}
};
const handleQuickAdd = async (data: CreateTaskData, columnId: string) => {
if (onCreateTask) {
await onCreateTask(data);
setShowQuickAdd(prev => ({ ...prev, [columnId]: false }));
}
};
const toggleQuickAdd = (columnId: string) => {
setShowQuickAdd(prev => ({ ...prev, [columnId]: !prev[columnId] }));
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex flex-col h-full bg-[var(--background)]">
{/* Header */}
<div className="flex-shrink-0 px-6 py-4 border-b border-[var(--border)]/50">
<div className="flex items-center justify-between">
<h2 className="text-lg font-mono font-bold text-[var(--foreground)] uppercase tracking-wider">
{title}
</h2>
{onCreateTask && (
<Button
variant="primary"
onClick={() => setIsCreateModalOpen(true)}
disabled={loading}
>
+ Nouvelle tâche
</Button>
)}
</div>
</div>
{/* Headers des colonnes visibles */}
<div
className={`grid gap-4 px-6 py-4 ml-8`}
style={{ gridTemplateColumns: `repeat(${statusesToShow.length}, minmax(0, 1fr))` }}
>
{statusesToShow.map(status => {
const statusConfig = allStatuses.find(s => s.key === status);
return (
<div key={status} className="text-center">
<h3 className="text-sm font-mono font-bold text-[var(--foreground)] uppercase tracking-wider">
{statusConfig?.icon} {statusConfig?.label}
</h3>
</div>
);
})}
</div>
{/* Swimlanes */}
<div className="flex-1 overflow-y-auto px-6">
<div className="space-y-4">
{swimlanes.map(swimlane => {
const isCollapsed = collapsedSwimlanes.has(swimlane.key);
return (
<div key={swimlane.key} className="border border-[var(--border)]/50 rounded-lg bg-[var(--card-column)]">
{/* Header de la swimlane */}
<div className="flex items-center p-2 border-b border-[var(--border)]/50">
<button
onClick={() => toggleSwimlane(swimlane.key)}
className="flex items-center gap-2 hover:bg-[var(--card-hover)] rounded p-1 -m-1 transition-colors"
>
<svg
className={`w-4 h-4 text-[var(--muted-foreground)] transition-transform ${isCollapsed ? '' : 'rotate-90'}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
{swimlane.color && (
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: swimlane.color }}
/>
)}
<span className="text-[var(--foreground)] font-medium text-sm">
{swimlane.icon && `${swimlane.icon} `}
{swimlane.label} ({swimlane.tasks.length})
</span>
</button>
</div>
{/* Contenu de la swimlane */}
{!isCollapsed && (
<div
className={`grid gap-4 p-4`}
style={{ gridTemplateColumns: `repeat(${statusesToShow.length}, minmax(0, 1fr))` }}
>
{statusesToShow.map(status => {
const statusTasks = swimlane.tasks.filter(task => task.status === status);
const columnId = `${swimlane.key}-${status}`;
// Utiliser le contexte défini dans la swimlane
const swimlaneContext = swimlane.context;
return (
<DroppableColumn
key={columnId}
status={status}
tasks={statusTasks}
onDeleteTask={onDeleteTask}
onEditTask={onEditTask}
onUpdateTitle={onUpdateTitle}
compactView={compactView}
onCreateTask={onCreateTask ? (data) => handleQuickAdd(data, columnId) : undefined}
showQuickAdd={showQuickAdd[columnId] || false}
onToggleQuickAdd={() => toggleQuickAdd(columnId)}
swimlaneContext={swimlaneContext}
/>
);
})}
</div>
)}
</div>
);
})}
</div>
</div>
</div>
{/* Drag overlay */}
<DragOverlay>
{activeTask && (
<TaskCard
task={activeTask}
compactView={compactView}
/>
)}
</DragOverlay>
{/* Modal de création complète */}
{onCreateTask && (
<CreateTaskForm
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSubmit={handleCreateTask}
loading={loading}
/>
)}
</DndContext>
);
}