Merge pull request #158 from R0m1k3/1.1

1.1
This commit is contained in:
LogiFlow authored and GitHub committed 2025-08-15 15:33:44 +02:00
commit 02f60d7c99
4 files changed
+289 -170

No files matched your search

+12 -4
View File
@@ -31,7 +31,7 @@ const taskFormSchema = z.object({
title: z.string().min(1, "Le titre est requis"),
description: z.string().optional(),
priority: z.enum(["low", "medium", "high"]).default("medium"),
status: z.enum(["pending", "completed"]).default("pending"),
status: z.enum(["backlog", "todo", "in_progress", "review", "testing", "blocked", "ready_to_deploy", "deployed", "done", "archived"]).default("todo"),
assignedTo: z.string().min(1, "L'assignation est requise"),
dueDate: z.date().optional(),
});
@@ -118,7 +118,7 @@ export default function TaskForm({ task, onClose }: TaskFormProps) {
title: task?.title || "",
description: task?.description || "",
priority: (task?.priority as "low" | "medium" | "high") || "medium",
status: (task?.status as "pending" | "completed") || "pending",
status: (task?.status as "backlog" | "todo" | "in_progress" | "review" | "testing" | "blocked" | "ready_to_deploy" | "deployed" | "done" | "archived") || "todo",
assignedTo: task?.assignedTo || "",
dueDate: task?.dueDate ? new Date(task.dueDate) : undefined,
},
@@ -346,8 +346,16 @@ export default function TaskForm({ task, onClose }: TaskFormProps) {
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="pending">En cours</SelectItem>
<SelectItem value="completed">Terminée</SelectItem>
<SelectItem value="backlog">Backlog</SelectItem>
<SelectItem value="todo">À faire</SelectItem>
<SelectItem value="in_progress">En cours</SelectItem>
<SelectItem value="review">Révision</SelectItem>
<SelectItem value="testing">Test</SelectItem>
<SelectItem value="blocked">Bloqué</SelectItem>
<SelectItem value="ready_to_deploy">Prêt Déploiement</SelectItem>
<SelectItem value="deployed">Déployé</SelectItem>
<SelectItem value="done">Terminé</SelectItem>
<SelectItem value="archived">Archivé</SelectItem>
</SelectContent>
</Select>
<FormMessage />
+275 -164
View File
@@ -48,7 +48,7 @@ export default function Tasks() {
const [statusFilter, setStatusFilter] = useState<string>("all");
const [priorityFilter, setPriorityFilter] = useState<string>("all");
const [dueDateFilter, setDueDateFilter] = useState<string>("all");
const [viewMode, setViewMode] = useState<string>("list");
const [viewMode, setViewMode] = useState<string>("kanban");
// Modals
const [showCreateModal, setShowCreateModal] = useState(false);
@@ -194,7 +194,7 @@ export default function Tasks() {
if (!isThisWeek(dueDate)) return false;
break;
case "overdue":
if (!isPast(dueDate) || task.status === 'completed') return false;
if (!isPast(dueDate) || task.status === 'done' || task.status === 'archived') return false;
break;
case "no_due_date":
if (task.dueDate) return false;
@@ -207,9 +207,22 @@ export default function Tasks() {
return true;
})
.sort((a: TaskWithRelations, b: TaskWithRelations) => {
// Faire remonter les tâches non validées (pending) en premier
if (a.status === 'pending' && b.status === 'completed') return -1;
if (a.status === 'completed' && b.status === 'pending') return 1;
// Ordre des statuts (backlog > todo > in_progress > review > testing > done > archived)
const statusOrder = {
backlog: 7,
todo: 6,
in_progress: 5,
review: 4,
testing: 3,
done: 2,
archived: 1
};
const aStatusOrder = statusOrder[a.status as keyof typeof statusOrder] || 0;
const bStatusOrder = statusOrder[b.status as keyof typeof statusOrder] || 0;
if (aStatusOrder !== bStatusOrder) {
return bStatusOrder - aStatusOrder; // Ordre décroissant
}
// Pour les tâches de même statut, trier par priorité (high > medium > low)
const priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 };
@@ -266,8 +279,8 @@ export default function Tasks() {
}
};
const getDueDateStatus = (dueDate: string | null, status: string) => {
if (!dueDate || status === 'completed') return null;
const getDueDateStatus = (dueDate: string | Date | null, status: string) => {
if (!dueDate || status === 'done' || status === 'archived') return null;
const due = new Date(dueDate);
const now = new Date();
@@ -308,6 +321,86 @@ export default function Tasks() {
};
};
// Fonction pour rendre une carte Kanban
const renderKanbanCard = (task: TaskWithRelations, canEditTasks: boolean, isCompleted = false) => {
const priorityConfig = getPriorityConfig(task.priority);
const PriorityIcon = priorityConfig.icon;
const dueDateStatus = getDueDateStatus(task.dueDate, task.status);
const DueDateIcon = dueDateStatus?.icon;
return (
<Card key={task.id} className={`hover:shadow-md transition-shadow cursor-pointer ${isCompleted ? 'opacity-75' : ''}`}>
<CardContent className="p-2">
<div className="flex items-start justify-between mb-1">
<h5 className={`font-medium text-gray-900 text-xs ${isCompleted ? 'line-through' : ''}`}>{task.title}</h5>
<Badge variant={priorityConfig.color} className="flex items-center gap-1 text-xs">
<PriorityIcon className="w-2 h-2" />
{priorityConfig.label.charAt(0)}
</Badge>
</div>
{task.description && (
<p className="text-xs text-gray-600 mb-1 line-clamp-1">
{task.description}
</p>
)}
<div className="text-xs text-gray-500 mb-1">
{task.assignedTo}
</div>
{dueDateStatus && DueDateIcon && (
<div className="flex items-center gap-1 mb-1">
<Badge variant={dueDateStatus.color} className="flex items-center gap-1 text-xs">
<DueDateIcon className="w-2 h-2" />
{dueDateStatus.text}
</Badge>
</div>
)}
{isCompleted && (
<div className="text-xs text-gray-500 mb-1">
{task.completedAt ? format(new Date(task.completedAt), 'dd/MM', { locale: fr }) : ''}
</div>
)}
<div className="flex items-center gap-1 mt-1">
{canEditTasks && (
<>
{!isCompleted && (
<Button
size="sm"
variant="outline"
onClick={() => handleCompleteTask(task.id)}
className="text-green-600 hover:text-green-700 text-xs h-5 w-5 p-0"
>
<CheckCircle className="w-2 h-2" />
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => handleEditTask(task)}
className="text-xs h-5 w-5 p-0"
>
<Edit className="w-2 h-2" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteClick(task)}
className="text-red-600 hover:text-red-700 text-xs h-5 w-5 p-0"
>
<Trash2 className="w-2 h-2" />
</Button>
</>
)}
</div>
</CardContent>
</Card>
);
};
const canCreateTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur';
const canEditTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur';
@@ -337,14 +430,14 @@ export default function Tasks() {
{/* Sélecteur de vue */}
<Tabs value={viewMode} onValueChange={setViewMode} className="w-auto">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="list" className="flex items-center gap-2">
<ListTodo className="w-4 h-4" />
Liste
</TabsTrigger>
<TabsTrigger value="kanban" className="flex items-center gap-2">
<Kanban className="w-4 h-4" />
Kanban
</TabsTrigger>
<TabsTrigger value="list" className="flex items-center gap-2">
<ListTodo className="w-4 h-4" />
Liste
</TabsTrigger>
</TabsList>
</Tabs>
@@ -401,8 +494,16 @@ export default function Tasks() {
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes</SelectItem>
<SelectItem value="pending">En cours</SelectItem>
<SelectItem value="completed">Terminées</SelectItem>
<SelectItem value="backlog">Backlog</SelectItem>
<SelectItem value="todo">À faire</SelectItem>
<SelectItem value="in_progress">En cours</SelectItem>
<SelectItem value="review">Révision</SelectItem>
<SelectItem value="testing">Test</SelectItem>
<SelectItem value="blocked">Bloqué</SelectItem>
<SelectItem value="ready_to_deploy">Prêt Déploiement</SelectItem>
<SelectItem value="deployed">Déployé</SelectItem>
<SelectItem value="done">Terminé</SelectItem>
<SelectItem value="archived">Archivé</SelectItem>
</SelectContent>
</Select>
</div>
@@ -451,6 +552,143 @@ export default function Tasks() {
<div className="flex-1">
{/* Contenu selon la vue sélectionnée */}
<Tabs value={viewMode} onValueChange={setViewMode} className="h-full">
<TabsContent value="kanban" className="mt-0">
{/* Vue Kanban avec 10 colonnes - Workflow complet */}
<div className="p-6">
<div className="grid grid-cols-10 gap-1 h-full text-xs overflow-x-auto">
{/* Colonne Backlog */}
<div className="bg-gray-50 rounded-lg p-3">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<Circle className="w-4 h-4 text-gray-400" />
Backlog ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'backlog').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'backlog').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne À faire */}
<div className="bg-blue-50 rounded-lg p-3">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<Circle className="w-4 h-4 text-blue-500" />
À faire ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'todo').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'todo').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne En cours */}
<div className="bg-yellow-50 rounded-lg p-3">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<Clock className="w-4 h-4 text-yellow-500" />
En cours ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'in_progress').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'in_progress').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne Révision */}
<div className="bg-purple-50 rounded-lg p-3">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<Edit className="w-4 h-4 text-purple-500" />
Révision ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'review').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'review').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne Test */}
<div className="bg-orange-50 rounded-lg p-2">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<AlertTriangle className="w-4 h-4 text-orange-500" />
Test ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'testing').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'testing').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne Bloqué */}
<div className="bg-red-50 rounded-lg p-2">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<Circle className="w-4 h-4 text-red-500" />
Bloqué ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'blocked').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'blocked').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne Prêt Déploiement */}
<div className="bg-indigo-50 rounded-lg p-2">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<CheckCircle className="w-4 h-4 text-indigo-500" />
Prêt Déploiement ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'ready_to_deploy').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'ready_to_deploy').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne Déployé */}
<div className="bg-cyan-50 rounded-lg p-2">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<CheckCircle className="w-4 h-4 text-cyan-500" />
Déployé ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'deployed').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'deployed').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks);
})}
</div>
</div>
{/* Colonne Terminé */}
<div className="bg-green-50 rounded-lg p-2">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<CheckCircle className="w-4 h-4 text-green-500" />
Terminé ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'done').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'done').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks, true);
})}
</div>
</div>
{/* Colonne Archivé */}
<div className="bg-gray-100 rounded-lg p-2">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2 text-xs">
<Circle className="w-4 h-4 text-gray-400" />
Archivé ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'archived').length})
</h3>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'archived').map((task: TaskWithRelations) => {
return renderKanbanCard(task, canEditTasks, true);
})}
</div>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="list" className="mt-0">
{/* Liste des tâches */}
<div className="p-6">
@@ -466,15 +704,15 @@ export default function Tasks() {
</div>
) : (
<div className="space-y-4">
{/* Tâches en cours */}
{paginatedTasks.filter(task => task.status === 'pending').length > 0 && (
{/* Tâches actives (non terminées/archivées) */}
{paginatedTasks.filter(task => !['done', 'archived'].includes(task.status)).length > 0 && (
<div>
<h4 className="text-lg font-semibold text-gray-900 mb-3">
Tâches en cours ({paginatedTasks.filter(task => task.status === 'pending').length})
Tâches actives ({paginatedTasks.filter(task => !['done', 'archived'].includes(task.status)).length})
</h4>
<div className="space-y-3">
{paginatedTasks
.filter(task => task.status === 'pending')
.filter(task => !['done', 'archived'].includes(task.status))
.map((task) => {
const priorityConfig = getPriorityConfig(task.priority);
const PriorityIcon = priorityConfig.icon;
@@ -492,6 +730,16 @@ export default function Tasks() {
<PriorityIcon className="w-3 h-3" />
{priorityConfig.label}
</Badge>
<Badge variant="outline" className="text-xs">
{task.status === 'backlog' ? 'Backlog' :
task.status === 'todo' ? 'À faire' :
task.status === 'in_progress' ? 'En cours' :
task.status === 'review' ? 'Révision' :
task.status === 'testing' ? 'Test' :
task.status === 'blocked' ? 'Bloqué' :
task.status === 'ready_to_deploy' ? 'Prêt Déploiement' :
task.status === 'deployed' ? 'Déployé' : task.status}
</Badge>
</div>
{task.description && (
@@ -577,15 +825,15 @@ export default function Tasks() {
</div>
)}
{/* Tâches terminées */}
{paginatedTasks.filter(task => task.status === 'completed').length > 0 && (
{/* Tâches terminées et archivées */}
{paginatedTasks.filter(task => ['done', 'archived'].includes(task.status)).length > 0 && (
<div>
<h4 className="text-lg font-semibold text-gray-900 mb-3 mt-8">
Tâches terminées ({paginatedTasks.filter(task => task.status === 'completed').length})
Tâches terminées ({paginatedTasks.filter(task => ['done', 'archived'].includes(task.status)).length})
</h4>
<div className="space-y-3">
{paginatedTasks
.filter(task => task.status === 'completed')
.filter(task => ['done', 'archived'].includes(task.status))
.map((task) => {
const priorityConfig = getPriorityConfig(task.priority);
const PriorityIcon = priorityConfig.icon;
@@ -599,9 +847,12 @@ export default function Tasks() {
<h5 className="font-medium text-gray-500 truncate line-through">
{task.title}
</h5>
<Badge variant="secondary" className="flex items-center gap-1">
<CheckCircle className="w-3 h-3 border border-gray-300 rounded p-0.5" />
Terminée
<Badge
variant={task.status === 'done' ? 'secondary' : 'outline'}
className="flex items-center gap-1"
>
<CheckCircle className="w-3 h-3" />
{task.status === 'done' ? 'Terminée' : 'Archivée'}
</Badge>
</div>
@@ -662,147 +913,7 @@ export default function Tasks() {
</div>
</TabsContent>
<TabsContent value="kanban" className="mt-0">
{/* Vue Kanban */}
<div className="p-6">
<div className="grid grid-cols-2 gap-6 h-full">
{/* Colonne En cours */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Circle className="w-5 h-5 text-yellow-500" />
En cours ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'pending').length})
</h3>
<div className="space-y-3 max-h-[600px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'pending').map((task: TaskWithRelations) => {
const priorityConfig = getPriorityConfig(task.priority);
const PriorityIcon = priorityConfig.icon;
const dueDateStatus = getDueDateStatus(task.dueDate, task.status);
const DueDateIcon = dueDateStatus?.icon;
return (
<Card key={task.id} className="hover:shadow-md transition-shadow cursor-pointer">
<CardContent className="p-3">
<div className="flex items-start justify-between mb-2">
<h5 className="font-medium text-gray-900 text-sm">{task.title}</h5>
<Badge variant={priorityConfig.color} className="flex items-center gap-1 text-xs">
<PriorityIcon className="w-3 h-3" />
{priorityConfig.label}
</Badge>
</div>
{task.description && (
<p className="text-xs text-gray-600 mb-2 line-clamp-2">
{task.description}
</p>
)}
<div className="text-xs text-gray-500 mb-2">
Assigné à: {task.assignedTo}
</div>
{dueDateStatus && DueDateIcon && (
<div className="flex items-center gap-2 mb-2">
<Badge variant={dueDateStatus.color} className="flex items-center gap-1 text-xs">
<DueDateIcon className="w-3 h-3" />
{dueDateStatus.text}
</Badge>
</div>
)}
<div className="flex items-center gap-1 mt-2">
{canEditTasks && (
<>
<Button
size="sm"
variant="outline"
onClick={() => handleCompleteTask(task.id)}
className="text-green-600 hover:text-green-700 text-xs h-7"
>
<CheckCircle className="w-3 h-3" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleEditTask(task)}
className="text-xs h-7"
>
<Edit className="w-3 h-3" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteClick(task)}
className="text-red-600 hover:text-red-700 text-xs h-7"
>
<Trash2 className="w-3 h-3" />
</Button>
</>
)}
</div>
</CardContent>
</Card>
);
})}
</div>
</div>
{/* Colonne Terminées */}
<div className="bg-green-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-green-500" />
Terminées ({filteredTasks.filter((task: TaskWithRelations) => task.status === 'completed').length})
</h3>
<div className="space-y-3 max-h-[600px] overflow-y-auto">
{filteredTasks.filter((task: TaskWithRelations) => task.status === 'completed').map((task: TaskWithRelations) => {
const priorityConfig = getPriorityConfig(task.priority);
const PriorityIcon = priorityConfig.icon;
return (
<Card key={task.id} className="hover:shadow-md transition-shadow cursor-pointer opacity-75">
<CardContent className="p-3">
<div className="flex items-start justify-between mb-2">
<h5 className="font-medium text-gray-900 text-sm line-through">{task.title}</h5>
<Badge variant={priorityConfig.color} className="flex items-center gap-1 text-xs">
<PriorityIcon className="w-3 h-3" />
{priorityConfig.label}
</Badge>
</div>
<div className="text-xs text-gray-500">
Terminée le: {task.completedAt ? format(new Date(task.completedAt), 'dd/MM/yyyy', { locale: fr }) : 'Date inconnue'}
</div>
<div className="flex items-center gap-1 mt-2">
{canEditTasks && (
<>
<Button
size="sm"
variant="outline"
onClick={() => handleEditTask(task)}
className="text-xs h-7"
>
<Edit className="w-3 h-3" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDeleteClick(task)}
className="text-red-600 hover:text-red-700 text-xs h-7"
>
<Trash2 className="w-3 h-3" />
</Button>
</>
)}
</div>
</CardContent>
</Card>
);
})}
</div>
</div>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
+1 -1
View File
@@ -34,7 +34,7 @@ The platform is built with a clear separation of concerns, utilizing a modern fu
- **Reconciliation:** Functionality for reconciling Bills of Lading (BL) and invoices with immediate UI updates.
- **Invoice Verification Cache:** Robust caching system for invoice verification results with automatic expiration and conflict resolution. Enhanced with unique constraint handling and graceful error management for production stability (August 2025). Fixed production SSL certificate errors and fetch timeout loops with enhanced error handling, network timeout protection, and error result caching to prevent infinite retry loops (August 2025).
- **UI Scrollbar Optimization:** Eliminated redundant vertical scrollbar on the right side of the screen while preserving necessary internal page scrolling. Implemented global CSS rules with `overflow: hidden` on html/body/root and Layout height management to prevent vertical overflow issues in production (August 2025).
- **Task Management:** Système complet de gestion des tâches avec fonctionnalités temporelles avancées - tri intelligent (pending > priorité > date), dates d'échéance avec sélecteur calendrier, indicateurs visuels pour tâches en retard/urgentes, filtres par échéance (aujourd'hui, cette semaine, en retard, sans échéance), vue Kanban et liste, badges colorés pour statut temporel. Interface optimisée pour suivi prioritaire des tâches non validées (Août 2025).
- **Task Management:** Système complet de gestion des tâches avec workflow Kanban 7 colonnes - Backlog, À faire, En cours, Révision, Test, Terminé, Archivé. Fonctionnalités temporelles avancées avec tri intelligent par statut/priorité/date, dates d'échéance avec sélecteur calendrier, indicateurs visuels pour tâches en retard/urgentes, filtres par échéance et statut complets. Interface optimisée avec cartes compactes pour vue Kanban étendue et vue liste détaillée (Août 2025).
- **User and Permission Management:** Granular access controls ensuring that users (e.g., employees) have appropriate permissions for tasks like creating customer orders.
- **Reporting:** Statistical reporting, including monthly summaries of deliveries based on `deliveredDate`.
+1 -1
View File
@@ -245,7 +245,7 @@ export const tasks = pgTable("tasks", {
description: text("description"), // Description de la tâche (optionnel)
dueDate: timestamp("due_date"), // Date d'échéance (optionnel)
priority: varchar("priority").notNull().default("medium"), // low, medium, high
status: varchar("status").notNull().default("pending"), // pending, completed
status: varchar("status").notNull().default("todo"), // backlog, todo, in_progress, review, testing, blocked, ready_to_deploy, deployed, done, archived
assignedTo: text("assigned_to").notNull(), // Utilisateur responsable (champ libre)
createdBy: varchar("created_by").notNull(), // Utilisateur créateur
groupId: integer("group_id").notNull(), // Magasin/groupe associé