Initial commit

This commit is contained in:
LogiFlow committed 2025-08-11 14:38:44 +00:00
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>
);
}
+397
View File
@@ -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>
);
}