mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-15 03:09:32 +02:00
Initial commit
This commit is contained in:
commit
e3fb29ab1d
163 files changed
+8140
No files matched your search
@@ -0,0 +1,192 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
ListTodo,
|
||||
Circle,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
ArrowRight
|
||||
} from "lucide-react";
|
||||
import { format, addDays, isAfter, isBefore } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { useLocation } from "wouter";
|
||||
import { useStore } from "@/components/Layout";
|
||||
import { Task } from "@shared/schema";
|
||||
|
||||
type TaskWithRelations = Task & {
|
||||
assignedUser: { id: string; username: string; firstName?: string; lastName?: string; };
|
||||
creator: { id: string; username: string; firstName?: string; lastName?: string; };
|
||||
group: { id: number; name: string; color: string; };
|
||||
};
|
||||
|
||||
export default function RecentTasksCard() {
|
||||
const { user } = useAuthUnified();
|
||||
const { selectedStoreId } = useStore();
|
||||
const [, navigate] = useLocation();
|
||||
|
||||
// Fetch tasks
|
||||
const { data: tasks = [], isLoading } = useQuery({
|
||||
queryKey: ["/api/tasks", selectedStoreId],
|
||||
queryFn: () => {
|
||||
const params = new URLSearchParams();
|
||||
if (selectedStoreId) {
|
||||
params.append('storeId', selectedStoreId);
|
||||
}
|
||||
return fetch(`/api/tasks?${params.toString()}`).then(res => res.json());
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Filtrer les tâches des 7 prochains jours qui ne sont pas terminées
|
||||
const upcomingTasks = tasks
|
||||
.filter((task: TaskWithRelations) => {
|
||||
// Seulement les tâches incomplètes
|
||||
if (task.status === 'completed') return false;
|
||||
|
||||
// Tâches avec date d'échéance dans les 7 prochains jours
|
||||
if (task.dueDate) {
|
||||
const dueDate = new Date(task.dueDate);
|
||||
const now = new Date();
|
||||
const sevenDaysFromNow = addDays(now, 7);
|
||||
return isAfter(dueDate, now) && isBefore(dueDate, sevenDaysFromNow);
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.sort((a: TaskWithRelations, b: TaskWithRelations) => {
|
||||
// Trier par priorité (high -> medium -> low) puis par date
|
||||
const priorityOrder = { high: 3, medium: 2, low: 1 };
|
||||
const priorityDiff = (priorityOrder[b.priority as keyof typeof priorityOrder] || 2) -
|
||||
(priorityOrder[a.priority as keyof typeof priorityOrder] || 2);
|
||||
|
||||
if (priorityDiff !== 0) return priorityDiff;
|
||||
|
||||
// Si même priorité, trier par date d'échéance
|
||||
if (a.dueDate && b.dueDate) {
|
||||
return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
.slice(0, 5); // Limite à 5 tâches
|
||||
|
||||
const getPriorityConfig = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return {
|
||||
color: 'destructive' as const,
|
||||
icon: AlertTriangle,
|
||||
label: 'Élevée'
|
||||
};
|
||||
case 'medium':
|
||||
return {
|
||||
color: 'default' as const,
|
||||
icon: Clock,
|
||||
label: 'Moyenne'
|
||||
};
|
||||
case 'low':
|
||||
return {
|
||||
color: 'secondary' as const,
|
||||
icon: Circle,
|
||||
label: 'Faible'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: 'secondary' as const,
|
||||
icon: Circle,
|
||||
label: 'Moyenne'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewAllTasks = () => {
|
||||
navigate("/tasks");
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Tâches à venir</CardTitle>
|
||||
<ListTodo className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Tâches à venir</CardTitle>
|
||||
<ListTodo className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{upcomingTasks.length === 0 ? (
|
||||
<div className="text-center py-6">
|
||||
<ListTodo className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Aucune tâche à venir dans les 7 prochains jours
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{upcomingTasks.map((task: TaskWithRelations) => {
|
||||
const priorityConfig = getPriorityConfig(task.priority);
|
||||
const PriorityIcon = priorityConfig.icon;
|
||||
|
||||
return (
|
||||
<div key={task.id} className="flex items-start justify-between p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-medium text-sm truncate">
|
||||
{task.title}
|
||||
</h4>
|
||||
<Badge variant={priorityConfig.color} className="flex items-center gap-1 text-xs">
|
||||
<PriorityIcon className="w-2.5 h-2.5" />
|
||||
{priorityConfig.label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{task.description && (
|
||||
<p className="text-xs text-muted-foreground mb-1 line-clamp-1">
|
||||
{task.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
{task.dueDate && (
|
||||
<span>
|
||||
{format(new Date(task.dueDate), 'dd/MM à HH:mm', { locale: fr })}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{task.assignedTo}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-center mt-4"
|
||||
onClick={handleViewAllTasks}
|
||||
>
|
||||
Voir toutes les tâches
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
|
||||
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||
import { useStore } from "@/components/Layout";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { insertTaskSchema, Task, Group } from "@shared/schema";
|
||||
import { z } from "zod";
|
||||
|
||||
type TaskWithRelations = Task & {
|
||||
assignedUser: { id: string; username: string; firstName?: string; lastName?: string; };
|
||||
creator: { id: string; username: string; firstName?: string; lastName?: string; };
|
||||
group: { id: number; name: string; color: string; };
|
||||
};
|
||||
|
||||
// Schéma simplifié sans magasin et sans date d'échéance
|
||||
const taskFormSchema = insertTaskSchema.omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
completedAt: true,
|
||||
createdBy: true,
|
||||
groupId: true,
|
||||
dueDate: true, // Suppression du champ échéance
|
||||
});
|
||||
|
||||
type TaskFormData = z.infer<typeof taskFormSchema>;
|
||||
|
||||
interface TaskFormProps {
|
||||
task?: TaskWithRelations;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function TaskForm({ task, onClose }: TaskFormProps) {
|
||||
const { user } = useAuthUnified();
|
||||
const { selectedStoreId } = useStore();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// État local pour gérer le magasin sélectionné (auto-sélection intelligente)
|
||||
const [localSelectedStoreId, setLocalSelectedStoreId] = useState<number | null>(null);
|
||||
|
||||
// Récupération des magasins pour l'auto-sélection
|
||||
const { data: groupsData = [] } = useQuery<Group[]>({
|
||||
queryKey: ['/api/groups'],
|
||||
});
|
||||
|
||||
// Filtrer les groupes selon le magasin sélectionné pour les admins
|
||||
const groups = Array.isArray(groupsData) ? (
|
||||
user?.role === 'admin' && selectedStoreId
|
||||
? groupsData.filter(g => g.id === selectedStoreId)
|
||||
: groupsData
|
||||
) : [];
|
||||
|
||||
// Auto-sélectionner le magasin selon les règles (même logique que CreateOrderModal)
|
||||
useEffect(() => {
|
||||
console.log('🏪 TaskForm - Store selection effect:', {
|
||||
groupsLength: groups.length,
|
||||
selectedStoreId,
|
||||
localSelectedStoreId,
|
||||
userRole: user?.role,
|
||||
allGroups: groups.map(g => ({ id: g.id, name: g.name })),
|
||||
});
|
||||
|
||||
if (groups.length > 0 && !localSelectedStoreId) {
|
||||
let defaultStoreId: number | null = null;
|
||||
|
||||
if (user?.role === 'admin') {
|
||||
// Pour l'admin : utiliser le magasin sélectionné dans le header (filtré), sinon le premier de la liste
|
||||
if (selectedStoreId && groups.find(g => g.id === selectedStoreId)) {
|
||||
defaultStoreId = selectedStoreId;
|
||||
} else {
|
||||
defaultStoreId = groups[0].id;
|
||||
}
|
||||
console.log('🏪 Admin task store selection:', {
|
||||
selectedStoreId,
|
||||
defaultStoreId,
|
||||
firstGroupId: groups[0].id,
|
||||
groupsAvailable: groups.map(g => g.name)
|
||||
});
|
||||
} else {
|
||||
// Pour les autres rôles : prendre le premier magasin attribué
|
||||
defaultStoreId = groups[0].id;
|
||||
console.log('🏪 Non-admin task store selection:', { defaultStoreId, firstGroupId: groups[0].id });
|
||||
}
|
||||
|
||||
if (defaultStoreId) {
|
||||
console.log('🏪 Setting default task store ID:', defaultStoreId, 'for group:', groups.find(g => g.id === defaultStoreId)?.name);
|
||||
setLocalSelectedStoreId(defaultStoreId);
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour localSelectedStoreId si selectedStoreId change (pour les admins)
|
||||
if (user?.role === 'admin' && selectedStoreId && selectedStoreId !== localSelectedStoreId) {
|
||||
console.log('🔄 Updating task form store because header store changed:', {
|
||||
oldStoreId: localSelectedStoreId,
|
||||
newStoreId: selectedStoreId
|
||||
});
|
||||
setLocalSelectedStoreId(selectedStoreId);
|
||||
}
|
||||
}, [groups, selectedStoreId, user?.role, localSelectedStoreId]);
|
||||
|
||||
const form = useForm<TaskFormData>({
|
||||
resolver: zodResolver(taskFormSchema),
|
||||
defaultValues: {
|
||||
title: task?.title || "",
|
||||
description: task?.description || "",
|
||||
priority: task?.priority || "medium",
|
||||
status: task?.status || "pending",
|
||||
assignedTo: task?.assignedTo || "",
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation pour créer/modifier une tâche
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => {
|
||||
// Ajouter le groupId basé sur le magasin auto-sélectionné
|
||||
const taskData = {
|
||||
...data,
|
||||
groupId: localSelectedStoreId,
|
||||
createdBy: user?.id,
|
||||
dueDate: data.dueDate || null
|
||||
};
|
||||
console.log('🚀 Creating task with data:', taskData);
|
||||
return apiRequest("/api/tasks", "POST", taskData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Tâche créée avec succès",
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("Error creating task:", error);
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de créer la tâche",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: any) => apiRequest(`/api/tasks/${task?.id}`, "PUT", data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Tâche modifiée avec succès",
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("Error updating task:", error);
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de modifier la tâche",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: TaskFormData) => {
|
||||
// Vérifier qu'un magasin est auto-sélectionné
|
||||
if (!localSelectedStoreId) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Aucun magasin disponible pour créer une tâche",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (task) {
|
||||
// Modification de tâche existante
|
||||
const submitData = {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
priority: data.priority,
|
||||
status: data.status,
|
||||
assignedTo: data.assignedTo,
|
||||
};
|
||||
updateMutation.mutate(submitData);
|
||||
} else {
|
||||
// Création d'une nouvelle tâche - le groupId sera ajouté dans createMutation
|
||||
createMutation.mutate(data);
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityLabel = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'Élevée';
|
||||
case 'medium': return 'Moyenne';
|
||||
case 'low': return 'Faible';
|
||||
default: return 'Moyenne';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending': return 'En cours';
|
||||
case 'completed': return 'Terminée';
|
||||
default: return 'En cours';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* Affichage du magasin auto-sélectionné */}
|
||||
{localSelectedStoreId && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-3">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-blue-800">
|
||||
<span className="font-medium">Magasin sélectionné :</span>{" "}
|
||||
{(() => {
|
||||
const selectedGroup = groups.find(g => g.id === localSelectedStoreId);
|
||||
return selectedGroup ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: selectedGroup.color }}
|
||||
/>
|
||||
{selectedGroup.name}
|
||||
</span>
|
||||
) : `Magasin ID: ${localSelectedStoreId}`;
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Avertissement si aucun magasin n'est disponible */}
|
||||
{!localSelectedStoreId && groups.length === 0 && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-3">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-yellow-800">
|
||||
Aucun magasin disponible. Veuillez contacter un administrateur.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Titre */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Titre *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Titre de la tâche"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Description */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Description détaillée de la tâche (optionnel)"
|
||||
rows={3}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Priorité */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="priority"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Priorité *</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Sélectionner une priorité" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Faible</SelectItem>
|
||||
<SelectItem value="medium">Moyenne</SelectItem>
|
||||
<SelectItem value="high">Élevée</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Statut */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Statut</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Sélectionner un statut" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="pending">En cours</SelectItem>
|
||||
<SelectItem value="completed">Terminée</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Assigné à */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="assignedTo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Assigné à *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Nom de la personne assignée"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
{/* Boutons d'action */}
|
||||
<div className="flex justify-end space-x-3 pt-6">
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending || updateMutation.isPending}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{createMutation.isPending || updateMutation.isPending ? (
|
||||
<div className="flex items-center">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
{task ? "Modification..." : "Création..."}
|
||||
</div>
|
||||
) : (
|
||||
task ? "Modifier" : "Créer"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user