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:
@@ -17,6 +17,8 @@ export interface TasksResponse {
|
||||
completed: number;
|
||||
inProgress: number;
|
||||
todo: number;
|
||||
cancelled: number;
|
||||
freeze: number;
|
||||
completionRate: number;
|
||||
};
|
||||
count: number;
|
||||
|
||||
@@ -12,6 +12,8 @@ interface HomePageClientProps {
|
||||
completed: number;
|
||||
inProgress: number;
|
||||
todo: number;
|
||||
cancelled: number;
|
||||
freeze: number;
|
||||
completionRate: number;
|
||||
};
|
||||
initialTags: (Tag & { usage: number })[];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
116
lib/status-config.ts
Normal 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';
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ interface TasksContextType {
|
||||
completed: number;
|
||||
inProgress: number;
|
||||
todo: number;
|
||||
cancelled: number;
|
||||
freeze: number;
|
||||
completionRate: number;
|
||||
};
|
||||
loading: boolean;
|
||||
|
||||
Reference in New Issue
Block a user