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; completed: number;
inProgress: number; inProgress: number;
todo: number; todo: number;
cancelled: number;
freeze: number;
completionRate: number; completionRate: number;
}; };
count: number; count: number;

View File

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

View File

@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/Input';
import { TagInput } from '@/components/ui/TagInput'; import { TagInput } from '@/components/ui/TagInput';
import { TaskPriority, TaskStatus } from '@/lib/types'; import { TaskPriority, TaskStatus } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client'; import { CreateTaskData } from '@/clients/tasks-client';
import { getAllStatuses } from '@/lib/status-config';
interface CreateTaskFormProps { interface CreateTaskFormProps {
isOpen: boolean; isOpen: boolean;
@@ -136,10 +137,11 @@ export function CreateTaskForm({ isOpen, onClose, onSubmit, loading = false }: C
disabled={loading} 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" 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> {getAllStatuses().map(statusConfig => (
<option value="in_progress">En cours</option> <option key={statusConfig.key} value={statusConfig.key}>
<option value="done">Terminé</option> {statusConfig.label}
<option value="cancelled">Annulé</option> </option>
))}
</select> </select>
</div> </div>
</div> </div>

View File

@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/Input';
import { TagInput } from '@/components/ui/TagInput'; import { TagInput } from '@/components/ui/TagInput';
import { Task, TaskPriority, TaskStatus } from '@/lib/types'; import { Task, TaskPriority, TaskStatus } from '@/lib/types';
import { UpdateTaskData } from '@/clients/tasks-client'; import { UpdateTaskData } from '@/clients/tasks-client';
import { getAllStatuses } from '@/lib/status-config';
interface EditTaskFormProps { interface EditTaskFormProps {
isOpen: boolean; isOpen: boolean;
@@ -148,10 +149,11 @@ export function EditTaskForm({ isOpen, onClose, onSubmit, task, loading = false
disabled={loading} 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" 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> {getAllStatuses().map(statusConfig => (
<option value="in_progress">En cours</option> <option key={statusConfig.key} value={statusConfig.key}>
<option value="done">Terminé</option> {statusConfig.label}
<option value="cancelled">Annulé</option> </option>
))}
</select> </select>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@@ -1,37 +1,38 @@
'use client'; 'use client';
import { TaskStatus } from '@/lib/types'; import { TaskStatus } from '@/lib/types';
import { getAllStatuses } from '@/lib/status-config';
interface ColumnVisibilityToggleProps { interface ColumnVisibilityToggleProps {
statuses: { id: TaskStatus; title: string; color: string }[];
hiddenStatuses: Set<TaskStatus>; hiddenStatuses: Set<TaskStatus>;
onToggleStatus: (status: TaskStatus) => void; onToggleStatus: (status: TaskStatus) => void;
className?: string; className?: string;
} }
export function ColumnVisibilityToggle({ export function ColumnVisibilityToggle({
statuses,
hiddenStatuses, hiddenStatuses,
onToggleStatus, onToggleStatus,
className = "" className = ""
}: ColumnVisibilityToggleProps) { }: ColumnVisibilityToggleProps) {
const statuses = getAllStatuses();
return ( return (
<div className={`flex items-center gap-2 ${className}`}> <div className={`flex items-center gap-2 ${className}`}>
<span className="text-sm font-mono font-medium text-slate-400"> <span className="text-sm font-mono font-medium text-slate-400">
Colonnes : Colonnes :
</span> </span>
{statuses.map(status => ( {statuses.map(statusConfig => (
<button <button
key={status.id} key={statusConfig.key}
onClick={() => onToggleStatus(status.id)} onClick={() => onToggleStatus(statusConfig.key)}
className={`px-3 py-1 rounded-lg text-xs font-mono font-medium transition-colors ${ 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-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' : '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> </button>
))} ))}
</div> </div>

View File

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

View File

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

View File

@@ -11,6 +11,8 @@ interface UseTasksState {
completed: number; completed: number;
inProgress: number; inProgress: number;
todo: number; todo: number;
cancelled: number;
freeze: number;
completionRate: number; completionRate: number;
}; };
loading: boolean; loading: boolean;
@@ -41,6 +43,8 @@ export function useTasks(
completed: 0, completed: 0,
inProgress: 0, inProgress: 0,
todo: 0, todo: 0,
cancelled: 0,
freeze: 0,
completionRate: 0 completionRate: 0
}, },
loading: false, loading: false,
@@ -147,6 +151,8 @@ export function useTasks(
completed: updatedTasks.filter(t => t.status === 'done').length, completed: updatedTasks.filter(t => t.status === 'done').length,
inProgress: updatedTasks.filter(t => t.status === 'in_progress').length, inProgress: updatedTasks.filter(t => t.status === 'in_progress').length,
todo: updatedTasks.filter(t => t.status === 'todo').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 completionRate: updatedTasks.length > 0
? Math.round((updatedTasks.filter(t => t.status === 'done').length / updatedTasks.length) * 100) ? Math.round((updatedTasks.filter(t => t.status === 'done').length / updatedTasks.length) * 100)
: 0 : 0
@@ -188,6 +194,8 @@ export function useTasks(
completed: currentTasks.filter(t => t.status === 'done').length, completed: currentTasks.filter(t => t.status === 'done').length,
inProgress: currentTasks.filter(t => t.status === 'in_progress').length, inProgress: currentTasks.filter(t => t.status === 'in_progress').length,
todo: currentTasks.filter(t => t.status === 'todo').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 completionRate: currentTasks.length > 0
? Math.round((currentTasks.filter(t => t.status === 'done').length / currentTasks.length) * 100) ? Math.round((currentTasks.filter(t => t.status === 'done').length / currentTasks.length) * 100)
: 0 : 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 // 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 TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
export type TaskSource = 'reminders' | 'jira' | 'manual'; export type TaskSource = 'reminders' | 'jira' | 'manual';

View File

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

View File

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