feat: extend task management with new statuses and centralized configuration

- Added `cancelled` and `freeze` statuses to `TasksResponse`, `HomePageClientProps`, and `useTasks` for comprehensive task tracking.
- Updated task forms to dynamically load statuses using `getAllStatuses`, enhancing maintainability and reducing hardcoded values.
- Refactored Kanban components to utilize centralized status configuration, improving consistency across the application.
- Adjusted visibility toggle and swimlanes to reflect new status options, ensuring a seamless user experience.
This commit is contained in:
Julien Froidefond
2025-09-14 23:06:50 +02:00
parent 2df64262ab
commit e6d24f2693
14 changed files with 189 additions and 105 deletions

View File

@@ -17,6 +17,8 @@ export interface TasksResponse {
completed: number;
inProgress: number;
todo: number;
cancelled: number;
freeze: number;
completionRate: number;
};
count: number;

View File

@@ -12,6 +12,8 @@ interface HomePageClientProps {
completed: number;
inProgress: number;
todo: number;
cancelled: number;
freeze: number;
completionRate: number;
};
initialTags: (Tag & { usage: number })[];

View File

@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/Input';
import { TagInput } from '@/components/ui/TagInput';
import { TaskPriority, TaskStatus } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client';
import { getAllStatuses } from '@/lib/status-config';
interface CreateTaskFormProps {
isOpen: boolean;
@@ -136,10 +137,11 @@ export function CreateTaskForm({ isOpen, onClose, onSubmit, loading = false }: C
disabled={loading}
className="w-full px-3 py-2 bg-slate-800/50 border border-slate-700/50 rounded-lg text-slate-100 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 hover:border-slate-600/50 transition-all duration-200 backdrop-blur-sm"
>
<option value="todo">À faire</option>
<option value="in_progress">En cours</option>
<option value="done">Terminé</option>
<option value="cancelled">Annulé</option>
{getAllStatuses().map(statusConfig => (
<option key={statusConfig.key} value={statusConfig.key}>
{statusConfig.label}
</option>
))}
</select>
</div>
</div>

View File

@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/Input';
import { TagInput } from '@/components/ui/TagInput';
import { Task, TaskPriority, TaskStatus } from '@/lib/types';
import { UpdateTaskData } from '@/clients/tasks-client';
import { getAllStatuses } from '@/lib/status-config';
interface EditTaskFormProps {
isOpen: boolean;
@@ -148,10 +149,11 @@ export function EditTaskForm({ isOpen, onClose, onSubmit, task, loading = false
disabled={loading}
className="w-full px-3 py-2 bg-slate-800/50 border border-slate-700/50 rounded-lg text-slate-100 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 hover:border-slate-600/50 transition-all duration-200 backdrop-blur-sm"
>
<option value="todo">À faire</option>
<option value="in_progress">En cours</option>
<option value="done">Terminé</option>
<option value="cancelled">Annulé</option>
{getAllStatuses().map(statusConfig => (
<option key={statusConfig.key} value={statusConfig.key}>
{statusConfig.label}
</option>
))}
</select>
</div>
</div>

View File

@@ -8,6 +8,7 @@ import { CreateTaskData } from '@/clients/tasks-client';
import { useMemo, useState } from 'react';
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
import { ColumnVisibilityToggle } from './ColumnVisibilityToggle';
import { getAllStatuses } from '@/lib/status-config';
import {
DndContext,
DragEndEvent,
@@ -58,38 +59,13 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
return grouped;
}, [tasks]);
// Configuration des colonnes
const allColumns: Array<{
id: TaskStatus;
title: string;
color: string;
tasks: Task[];
}> = [
{
id: 'todo',
title: 'À faire',
color: 'gray',
tasks: tasksByStatus.todo || []
},
{
id: 'in_progress',
title: 'En cours',
color: 'blue',
tasks: tasksByStatus.in_progress || []
},
{
id: 'done',
title: 'Terminé',
color: 'green',
tasks: tasksByStatus.done || []
},
{
id: 'cancelled',
title: 'Annulé',
color: 'red',
tasks: tasksByStatus.cancelled || []
}
];
// Configuration des colonnes basée sur la config centralisée
const allColumns = useMemo(() => {
return getAllStatuses().map(statusConfig => ({
id: statusConfig.key,
tasks: tasksByStatus[statusConfig.key] || []
}));
}, [tasksByStatus]);
// Filtrer les colonnes visibles
const visibleColumns = getVisibleStatuses(allColumns);
@@ -156,7 +132,6 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
{/* Toggle de visibilité des colonnes */}
<div className="px-6 pb-4">
<ColumnVisibilityToggle
statuses={allColumns}
hiddenStatuses={hiddenStatuses}
onToggleStatus={toggleStatusVisibility}
/>
@@ -168,8 +143,6 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
<KanbanColumn
key={column.id}
id={column.id}
title={column.title}
color={column.color}
tasks={column.tasks}
onCreateTask={onCreateTask ? handleCreateTask : undefined}
onDeleteTask={onDeleteTask}

View File

@@ -6,11 +6,10 @@ import { Badge } from '@/components/ui/Badge';
import { CreateTaskData } from '@/clients/tasks-client';
import { useState } from 'react';
import { useDroppable } from '@dnd-kit/core';
import { getStatusConfig, getTechStyle, getBadgeVariant } from '@/lib/status-config';
interface KanbanColumnProps {
id: TaskStatus;
title: string;
color: string;
tasks: Task[];
onCreateTask?: (data: CreateTaskData) => Promise<void>;
onDeleteTask?: (taskId: string) => Promise<void>;
@@ -19,52 +18,18 @@ interface KanbanColumnProps {
compactView?: boolean;
}
export function KanbanColumn({ id, title, color, tasks, onCreateTask, onDeleteTask, onEditTask, onUpdateTitle, compactView = false }: KanbanColumnProps) {
export function KanbanColumn({ id, tasks, onCreateTask, onDeleteTask, onEditTask, onUpdateTitle, compactView = false }: KanbanColumnProps) {
const [showQuickAdd, setShowQuickAdd] = useState(false);
// Configuration de la zone droppable
const { setNodeRef, isOver } = useDroppable({
id: id,
});
// Couleurs tech/cyberpunk
const techStyles = {
gray: {
border: 'border-slate-700',
glow: 'shadow-slate-500/20',
accent: 'text-slate-400',
badge: 'bg-slate-800 text-slate-300 border border-slate-600'
},
blue: {
border: 'border-cyan-500/30',
glow: 'shadow-cyan-500/20',
accent: 'text-cyan-400',
badge: 'bg-cyan-950 text-cyan-300 border border-cyan-500/30'
},
green: {
border: 'border-emerald-500/30',
glow: 'shadow-emerald-500/20',
accent: 'text-emerald-400',
badge: 'bg-emerald-950 text-emerald-300 border border-emerald-500/30'
},
red: {
border: 'border-red-500/30',
glow: 'shadow-red-500/20',
accent: 'text-red-400',
badge: 'bg-red-950 text-red-300 border border-red-500/30'
}
};
const style = techStyles[color as keyof typeof techStyles];
// Icônes tech
const techIcons = {
todo: '⚡',
in_progress: '🔄',
done: '✓',
cancelled: '✕'
};
const badgeVariant = color === 'green' ? 'success' : color === 'blue' ? 'primary' : color === 'red' ? 'danger' : 'default';
// Récupération de la config du statut
const statusConfig = getStatusConfig(id);
const style = getTechStyle(statusConfig.color);
const badgeVariant = getBadgeVariant(statusConfig.color);
return (
<div className="flex-shrink-0 w-80 md:w-1/4 md:flex-1 h-full">
@@ -80,7 +45,7 @@ export function KanbanColumn({ id, title, color, tasks, onCreateTask, onDeleteTa
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${style.accent.replace('text-', 'bg-')} animate-pulse`}></div>
<h3 className={`font-mono text-sm font-bold ${style.accent} uppercase tracking-wider`}>
{title}
{statusConfig.label} {statusConfig.icon}
</h3>
</div>
<div className="flex items-center gap-2">
@@ -117,7 +82,7 @@ export function KanbanColumn({ id, title, color, tasks, onCreateTask, onDeleteTa
{tasks.length === 0 && !showQuickAdd ? (
<div className="text-center py-20">
<div className={`w-16 h-16 mx-auto mb-4 rounded-full bg-slate-800 border-2 border-dashed ${style.border} flex items-center justify-center`}>
<span className={`text-2xl ${style.accent} opacity-50`}>{techIcons[id]}</span>
<span className={`text-2xl ${style.accent} opacity-50`}>{statusConfig.icon}</span>
</div>
<p className="text-xs font-mono text-slate-500 uppercase tracking-wide">NO DATA</p>
<div className="mt-2 flex justify-center">

View File

@@ -1,37 +1,38 @@
'use client';
import { TaskStatus } from '@/lib/types';
import { getAllStatuses } from '@/lib/status-config';
interface ColumnVisibilityToggleProps {
statuses: { id: TaskStatus; title: string; color: string }[];
hiddenStatuses: Set<TaskStatus>;
onToggleStatus: (status: TaskStatus) => void;
className?: string;
}
export function ColumnVisibilityToggle({
statuses,
hiddenStatuses,
onToggleStatus,
className = ""
}: ColumnVisibilityToggleProps) {
const statuses = getAllStatuses();
return (
<div className={`flex items-center gap-2 ${className}`}>
<span className="text-sm font-mono font-medium text-slate-400">
Colonnes :
</span>
{statuses.map(status => (
{statuses.map(statusConfig => (
<button
key={status.id}
onClick={() => onToggleStatus(status.id)}
key={statusConfig.key}
onClick={() => onToggleStatus(statusConfig.key)}
className={`px-3 py-1 rounded-lg text-xs font-mono font-medium transition-colors ${
hiddenStatuses.has(status.id)
hiddenStatuses.has(statusConfig.key)
? 'bg-slate-700/50 text-slate-500 hover:bg-slate-700'
: 'bg-blue-600/20 text-blue-300 border border-blue-500/30 hover:bg-blue-600/30'
}`}
title={hiddenStatuses.has(status.id) ? `Afficher ${status.title}` : `Masquer ${status.title}`}
title={hiddenStatuses.has(statusConfig.key) ? `Afficher ${statusConfig.label}` : `Masquer ${statusConfig.label}`}
>
{hiddenStatuses.has(status.id) ? '👁️‍🗨️' : '👁️'} {status.title}
{hiddenStatuses.has(statusConfig.key) ? '👁️‍🗨️' : '👁️'} {statusConfig.label}
</button>
))}
</div>

View File

@@ -6,6 +6,7 @@ import { useMemo, useState } from 'react';
import { useTasksContext } from '@/contexts/TasksContext';
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
import { ColumnVisibilityToggle } from './ColumnVisibilityToggle';
import { getAllStatuses } from '@/lib/status-config';
import {
DndContext,
DragEndEvent,
@@ -101,12 +102,14 @@ export function SwimlanesBoard({
})
);
const statuses: { id: TaskStatus; title: string; color: string }[] = [
{ id: 'todo', title: 'À faire', color: 'gray' },
{ id: 'in_progress', title: 'En cours', color: 'blue' },
{ id: 'done', title: 'Terminé', color: 'green' },
{ id: 'cancelled', title: 'Annulé', color: 'red' }
];
// Configuration des statuts basée sur la config centralisée
const statuses = useMemo(() => {
return getAllStatuses().map(statusConfig => ({
id: statusConfig.key,
title: statusConfig.label,
color: statusConfig.color
}));
}, []);
// Handlers pour le drag & drop
const handleDragStart = (event: DragStartEvent) => {
@@ -195,7 +198,6 @@ export function SwimlanesBoard({
{/* Headers des colonnes avec boutons toggle */}
<div className="flex items-center justify-between px-6 pb-4">
<ColumnVisibilityToggle
statuses={statuses}
hiddenStatuses={hiddenStatuses}
onToggleStatus={toggleStatusVisibility}
/>

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { TaskStatus } from '@/lib/types';
import { userPreferencesService } from '@/services/user-preferences';
import { getAllStatuses } from '@/lib/status-config';
export function useColumnVisibility() {
const [hiddenStatuses, setHiddenStatuses] = useState<Set<TaskStatus>>(new Set());
@@ -37,10 +38,15 @@ export function useColumnVisibility() {
return !hiddenStatuses.has(status);
};
const getAllAvailableStatuses = () => {
return getAllStatuses();
};
return {
hiddenStatuses,
toggleStatusVisibility,
getVisibleStatuses,
isStatusVisible
isStatusVisible,
getAllAvailableStatuses
};
}

View File

@@ -11,6 +11,8 @@ interface UseTasksState {
completed: number;
inProgress: number;
todo: number;
cancelled: number;
freeze: number;
completionRate: number;
};
loading: boolean;
@@ -41,6 +43,8 @@ export function useTasks(
completed: 0,
inProgress: 0,
todo: 0,
cancelled: 0,
freeze: 0,
completionRate: 0
},
loading: false,
@@ -147,6 +151,8 @@ export function useTasks(
completed: updatedTasks.filter(t => t.status === 'done').length,
inProgress: updatedTasks.filter(t => t.status === 'in_progress').length,
todo: updatedTasks.filter(t => t.status === 'todo').length,
cancelled: updatedTasks.filter(t => t.status === 'cancelled').length,
freeze: updatedTasks.filter(t => t.status === 'freeze').length,
completionRate: updatedTasks.length > 0
? Math.round((updatedTasks.filter(t => t.status === 'done').length / updatedTasks.length) * 100)
: 0
@@ -188,6 +194,8 @@ export function useTasks(
completed: currentTasks.filter(t => t.status === 'done').length,
inProgress: currentTasks.filter(t => t.status === 'in_progress').length,
todo: currentTasks.filter(t => t.status === 'todo').length,
cancelled: currentTasks.filter(t => t.status === 'cancelled').length,
freeze: currentTasks.filter(t => t.status === 'freeze').length,
completionRate: currentTasks.length > 0
? Math.round((currentTasks.filter(t => t.status === 'done').length / currentTasks.length) * 100)
: 0

116
lib/status-config.ts Normal file
View File

@@ -0,0 +1,116 @@
import { TaskStatus } from './types';
export interface StatusConfig {
key: TaskStatus;
label: string;
icon: string;
color: 'gray' | 'blue' | 'green' | 'red' | 'purple';
order: number;
}
export const STATUS_CONFIG: Record<TaskStatus, StatusConfig> = {
todo: {
key: 'todo',
label: 'À faire',
icon: '⚡',
color: 'gray',
order: 1
},
in_progress: {
key: 'in_progress',
label: 'En cours',
icon: '🔄',
color: 'blue',
order: 2
},
freeze: {
key: 'freeze',
label: 'Gelé',
icon: '🧊',
color: 'purple',
order: 3
},
done: {
key: 'done',
label: 'Terminé',
icon: '✓',
color: 'green',
order: 4
},
cancelled: {
key: 'cancelled',
label: 'Annulé',
icon: '✕',
color: 'red',
order: 5
}
} as const;
// Utilitaires pour récupérer facilement les infos
export const getStatusConfig = (status: TaskStatus): StatusConfig => {
return STATUS_CONFIG[status];
};
export const getAllStatuses = (): StatusConfig[] => {
return Object.values(STATUS_CONFIG).sort((a, b) => a.order - b.order);
};
export const getStatusLabel = (status: TaskStatus): string => {
return STATUS_CONFIG[status].label;
};
export const getStatusIcon = (status: TaskStatus): string => {
return STATUS_CONFIG[status].icon;
};
export const getStatusColor = (status: TaskStatus): StatusConfig['color'] => {
return STATUS_CONFIG[status].color;
};
// Configuration des couleurs tech/cyberpunk
export const TECH_STYLES = {
gray: {
border: 'border-slate-700',
glow: 'shadow-slate-500/20',
accent: 'text-slate-400',
badge: 'bg-slate-800 text-slate-300 border border-slate-600'
},
blue: {
border: 'border-cyan-500/30',
glow: 'shadow-cyan-500/20',
accent: 'text-cyan-400',
badge: 'bg-cyan-950 text-cyan-300 border border-cyan-500/30'
},
green: {
border: 'border-emerald-500/30',
glow: 'shadow-emerald-500/20',
accent: 'text-emerald-400',
badge: 'bg-emerald-950 text-emerald-300 border border-emerald-500/30'
},
red: {
border: 'border-red-500/30',
glow: 'shadow-red-500/20',
accent: 'text-red-400',
badge: 'bg-red-950 text-red-300 border border-red-500/30'
},
purple: {
border: 'border-purple-500/30',
glow: 'shadow-purple-500/20',
accent: 'text-purple-400',
badge: 'bg-purple-950 text-purple-300 border border-purple-500/30'
}
} as const;
export const getTechStyle = (color: StatusConfig['color']) => {
return TECH_STYLES[color];
};
export const getBadgeVariant = (color: StatusConfig['color']): 'success' | 'primary' | 'danger' | 'default' => {
switch (color) {
case 'green': return 'success';
case 'blue':
case 'purple': return 'primary';
case 'red': return 'danger';
default: return 'default';
}
};

View File

@@ -1,5 +1,6 @@
// Types de base pour les tâches
export type TaskStatus = 'todo' | 'in_progress' | 'done' | 'cancelled';
// Note: TaskStatus est maintenant géré par la configuration centralisée dans lib/status-config.ts
export type TaskStatus = 'todo' | 'in_progress' | 'done' | 'cancelled' | 'freeze';
export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
export type TaskSource = 'reminders' | 'jira' | 'manual';

View File

@@ -189,12 +189,13 @@ export class TasksService {
* Récupère les statistiques des tâches
*/
async getTaskStats() {
const [total, completed, inProgress, todo, cancelled] = await Promise.all([
const [total, completed, inProgress, todo, cancelled, freeze] = await Promise.all([
prisma.task.count(),
prisma.task.count({ where: { status: 'done' } }),
prisma.task.count({ where: { status: 'in_progress' } }),
prisma.task.count({ where: { status: 'todo' } }),
prisma.task.count({ where: { status: 'cancelled' } })
prisma.task.count({ where: { status: 'cancelled' } }),
prisma.task.count({ where: { status: 'freeze' } })
]);
return {
@@ -203,6 +204,7 @@ export class TasksService {
inProgress,
todo,
cancelled,
freeze,
completionRate: total > 0 ? Math.round((completed / total) * 100) : 0
};
}

View File

@@ -15,6 +15,8 @@ interface TasksContextType {
completed: number;
inProgress: number;
todo: number;
cancelled: number;
freeze: number;
completionRate: number;
};
loading: boolean;