mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
Improve task management by handling date errors and ensuring data integrity
Implement robust error handling and data validation for task dates and properties in both frontend and backend to resolve production issues. Replit-Commit-Author: Agent Replit-Commit-Session-Id: cc2d271b-82f0-4d6a-9302-a0b067ffb429 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/cc2d271b-82f0-4d6a-9302-a0b067ffb429/0taewQN
This commit is contained in:
1 parent
aa1906a9bb
commit
0d863da97e
7 files changed
+765
-56
No files matched your search
@@ -0,0 +1,48 @@
|
||||
# CORRECTION PRODUCTION - MODULE TÂCHES (19 Août 2025)
|
||||
|
||||
## PROBLÈME IDENTIFIÉ
|
||||
L'implémentation des dates de départ a causé des erreurs JavaScript en production qui empêchent le module tâches de se charger.
|
||||
|
||||
## CORRECTION IMMÉDIATE
|
||||
|
||||
### 1. Backend - Robustesse des données
|
||||
|
||||
Le backend doit retourner des données sécurisées :
|
||||
- Filtrage des tâches nulles
|
||||
- Gestion sécurisée des dates
|
||||
- Validation des propriétés
|
||||
|
||||
### 2. Frontend - Gestion d'erreur
|
||||
|
||||
Le frontend doit gérer les cas d'erreur :
|
||||
- Validation des données reçues
|
||||
- Protection contre les propriétés undefined
|
||||
- Fallback pour les erreurs de parsing de dates
|
||||
|
||||
### 3. Fichiers à copier en production
|
||||
|
||||
**Priorité CRITIQUE :** Copier ces fichiers dans l'ordre :
|
||||
|
||||
1. `server/storage.ts` - Méthode getTasks corrigée
|
||||
2. `client/src/pages/Tasks.tsx` - Gestion d'erreur renforcée
|
||||
3. `client/src/components/tasks/NewTaskForm.tsx` - Validation des données
|
||||
4. `shared/schema.ts` - Types mis à jour
|
||||
|
||||
## TEST DE VALIDATION
|
||||
|
||||
Après copie des fichiers :
|
||||
1. Redémarrer le serveur Docker
|
||||
2. Tester l'accès au module tâches
|
||||
3. Vérifier la création d'une nouvelle tâche
|
||||
4. Confirmer l'affichage correct des dates
|
||||
|
||||
## ROLLBACK SI PROBLÈME
|
||||
|
||||
Si les erreurs persistent, remplacer uniquement `client/src/components/tasks/TaskForm.tsx` par :
|
||||
|
||||
```typescript
|
||||
// Version simplifiée sans dates de départ
|
||||
export { default } from "./SimpleTaskForm";
|
||||
```
|
||||
|
||||
Et créer un SimpleTaskForm.tsx basique sans les nouvelles fonctionnalités.
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
Administration
|
||||
|
||||
User-related
|
||||
|
||||
|
||||
Environment-related
|
||||
|
||||
Registries
|
||||
|
||||
Logs
|
||||
|
||||
Notifications
|
||||
|
||||
Settings
|
||||
|
||||
New version available 2.27.9
|
||||
DismissSee what's new
|
||||
Community Edition
|
||||
2.27.6 LTS
|
||||
Containers>logiflow-logiflow-1>Logs
|
||||
Container logs
|
||||
|
||||
|
||||
admin
|
||||
Log viewer settings
|
||||
Auto-refresh logs
|
||||
Wrap lines
|
||||
Display timestamps
|
||||
Fetch
|
||||
All logs
|
||||
Search
|
||||
Filter...
|
||||
Lines
|
||||
100
|
||||
Actions
|
||||
|
||||
|
||||
|
||||
|
||||
taskGroups: [
|
||||
|
||||
{ id: 99, title: 'test', groupId: 1 },
|
||||
|
||||
{ id: 98, title: 'test', groupId: 1 },
|
||||
|
||||
{ id: 91, title: 'refaire podium lot assiettes', groupId: 2 }
|
||||
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
GET /api/tasks 200 in 461ms
|
||||
|
||||
GET /api/user 304 in 37ms
|
||||
|
||||
📢 [SERVER] User found: { username: 'admin', role: 'admin' }
|
||||
|
||||
🎯 [PRODUCTION] Using PostgreSQL DASHBOARD_MESSAGES table
|
||||
|
||||
🔍 [PRODUCTION] Raw messages from DB: 2 items: [
|
||||
|
||||
{
|
||||
|
||||
id: 16,
|
||||
|
||||
title: 'Mise à jour Prix',
|
||||
|
||||
content: 'Merci de mettre vos prix a jour tous les matins, certain magasin ne le font pas.',
|
||||
|
||||
type: 'success',
|
||||
|
||||
storeId: null,
|
||||
|
||||
createdBy: 'admin',
|
||||
|
||||
createdAt: 2025-08-18T10:45:08.585Z
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
id: 8,
|
||||
|
||||
title: 'Bon de Livraison',
|
||||
|
||||
content: 'Lorsque vous rentrez un numéro de bon de livraison, faite le a l identique, même les majuscules. Pour My candy Shop, prendre le numéro de commande.',
|
||||
|
||||
type: 'info',
|
||||
|
||||
storeId: null,
|
||||
|
||||
createdBy: 'admin_local',
|
||||
|
||||
createdAt: 2025-08-07T11:04:38.123Z
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
🔍 [PRODUCTION] Looking for user with username: admin
|
||||
|
||||
🔍 [PRODUCTION] Looking for user with username: admin_local
|
||||
|
||||
🔍 [PRODUCTION] User search result: Found
|
||||
|
||||
🔍 [PRODUCTION] User search result: Not found
|
||||
|
||||
🎯 [PRODUCTION] Found announcements: 2
|
||||
|
||||
❌ [PRODUCTION] User not found with username: admin_local
|
||||
|
||||
GET /api/announcements 200 in 36ms
|
||||
|
||||
GET /api/publicities 304 in 28ms
|
||||
|
||||
GET /api/user 304 in 6ms
|
||||
|
||||
Orders API called with: {
|
||||
|
||||
startDate: undefined,
|
||||
|
||||
endDate: undefined,
|
||||
|
||||
storeId: '1',
|
||||
|
||||
userRole: 'admin'
|
||||
|
||||
}
|
||||
|
||||
Admin filtering with groupIds: [ 1 ]
|
||||
|
||||
Fetching all orders
|
||||
|
||||
📢 [SERVER] Fetching announcements for user: admin_local environment: production
|
||||
|
||||
Deliveries API called with: {
|
||||
|
||||
startDate: undefined,
|
||||
|
||||
endDate: undefined,
|
||||
|
||||
storeId: '1',
|
||||
|
||||
withBL: undefined,
|
||||
|
||||
userRole: 'admin'
|
||||
|
||||
}
|
||||
|
||||
Admin filtering deliveries with groupIds: [ 1 ]
|
||||
|
||||
Fetching all deliveries
|
||||
|
||||
🔗 PRODUCTION: getOrders() récupéré 33 commandes avec relations
|
||||
|
||||
Orders returned: 33 items
|
||||
|
||||
GET /api/orders 200 in 94ms
|
||||
|
||||
📢 [SERVER] User found: { username: 'admin', role: 'admin' }
|
||||
|
||||
🎯 [PRODUCTION] Using PostgreSQL DASHBOARD_MESSAGES table
|
||||
|
||||
Customer Orders API called with: { groupIds: [ 1 ], userRole: 'admin' }
|
||||
|
||||
🔗 PRODUCTION: getDeliveries() récupéré 49 livraisons avec relations
|
||||
|
||||
Deliveries returned: 49 items
|
||||
|
||||
GET /api/deliveries 304 in 150ms
|
||||
|
||||
GET /api/groups 304 in 94ms
|
||||
|
||||
🔍 [PRODUCTION] Raw messages from DB: 2 items: [
|
||||
|
||||
{
|
||||
|
||||
id: 16,
|
||||
|
||||
title: 'Mise à jour Prix',
|
||||
|
||||
content: 'Merci de mettre vos prix a jour tous les matins, certain magasin ne le font pas.',
|
||||
|
||||
type: 'success',
|
||||
|
||||
storeId: null,
|
||||
|
||||
createdBy: 'admin',
|
||||
|
||||
createdAt: 2025-08-18T10:45:08.585Z
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
id: 8,
|
||||
|
||||
title: 'Bon de Livraison',
|
||||
|
||||
content: 'Lorsque vous rentrez un numéro de bon de livraison, faite le a l identique, même les majuscules. Pour My candy Shop, prendre le numéro de commande.',
|
||||
|
||||
type: 'info',
|
||||
|
||||
storeId: null,
|
||||
|
||||
createdBy: 'admin_local',
|
||||
|
||||
createdAt: 2025-08-07T11:04:38.123Z
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
🔍 [PRODUCTION] Looking for user with username: admin
|
||||
|
||||
🔍 [PRODUCTION] Looking for user with username: admin_local
|
||||
|
||||
Customer Orders returned: 10 items
|
||||
|
||||
GET /api/customer-orders 304 in 156ms
|
||||
|
||||
🔍 [PRODUCTION] User search result: Found
|
||||
|
||||
🔍 [PRODUCTION] User search result: Not found
|
||||
|
||||
❌ [PRODUCTION] User not found with username: admin_local
|
||||
|
||||
🎯 [PRODUCTION] Found announcements: 2
|
||||
|
||||
GET /api/announcements 200 in 157ms
|
||||
|
||||
GET /api/user 304 in 7ms
|
||||
|
||||
GET /api/publicities 304 in 12ms
|
||||
|
||||
GET /api/publicities 200 in 10ms
|
||||
|
||||
HEAD /api/health 200 in 0ms
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
@@ -66,13 +66,14 @@ export default function NewTaskForm({ task, onClose }: NewTaskFormProps) {
|
||||
// Récupération des magasins pour l'auto-sélection
|
||||
const { data: groupsData = [] } = useQuery<Group[]>({
|
||||
queryKey: ['/api/groups'],
|
||||
enabled: !!user, // Seulement si l'utilisateur est connecté
|
||||
});
|
||||
|
||||
// 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
|
||||
? groupsData.filter(g => g && g.id === selectedStoreId)
|
||||
: groupsData.filter(g => g)
|
||||
) : [];
|
||||
|
||||
// Auto-sélectionner le magasin selon les règles
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
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 { Calendar } from "@/components/ui/calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
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";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Version simplifiée du schéma sans startDate pour éviter les erreurs de production
|
||||
const simpleTaskFormSchema = 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"),
|
||||
assignedTo: z.string().min(1, "L'assignation est requise"),
|
||||
dueDate: z.date().optional(), // Seulement la date d'échéance
|
||||
});
|
||||
|
||||
type SimpleTaskFormData = z.infer<typeof simpleTaskFormSchema>;
|
||||
|
||||
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; };
|
||||
};
|
||||
|
||||
interface SimpleTaskFormProps {
|
||||
task?: TaskWithRelations;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function SimpleTaskForm({ task, onClose }: SimpleTaskFormProps) {
|
||||
const { user } = useAuthUnified();
|
||||
const { selectedStoreId } = useStore();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// État local pour gérer le magasin sélectionné
|
||||
const [localSelectedStoreId, setLocalSelectedStoreId] = useState<number | null>(null);
|
||||
|
||||
// Récupération sécurisée des magasins
|
||||
const { data: groupsData = [] } = useQuery<Group[]>({
|
||||
queryKey: ['/api/groups'],
|
||||
enabled: !!user,
|
||||
onError: () => console.warn('Erreur lors du chargement des magasins')
|
||||
});
|
||||
|
||||
const groups = Array.isArray(groupsData) ? groupsData.filter(g => g && g.id) : [];
|
||||
|
||||
// Auto-sélection du magasin
|
||||
useEffect(() => {
|
||||
if (groups.length > 0 && !localSelectedStoreId) {
|
||||
let defaultStoreId: number | null = null;
|
||||
|
||||
if (user?.role === 'admin') {
|
||||
if (selectedStoreId && groups.find(g => g.id === selectedStoreId)) {
|
||||
defaultStoreId = selectedStoreId;
|
||||
} else {
|
||||
defaultStoreId = groups[0].id;
|
||||
}
|
||||
} else {
|
||||
defaultStoreId = groups[0].id;
|
||||
}
|
||||
|
||||
if (defaultStoreId) {
|
||||
setLocalSelectedStoreId(defaultStoreId);
|
||||
}
|
||||
}
|
||||
}, [groups, selectedStoreId, user?.role, localSelectedStoreId]);
|
||||
|
||||
const form = useForm<SimpleTaskFormData>({
|
||||
resolver: zodResolver(simpleTaskFormSchema),
|
||||
defaultValues: {
|
||||
title: task?.title || "",
|
||||
description: task?.description || "",
|
||||
priority: (task?.priority as "low" | "medium" | "high") || "medium",
|
||||
status: (task?.status as "pending" | "completed") || "pending",
|
||||
assignedTo: task?.assignedTo || "",
|
||||
dueDate: task?.dueDate ? new Date(task.dueDate) : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// Mutations sécurisées
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => {
|
||||
const taskData = {
|
||||
...data,
|
||||
groupId: localSelectedStoreId,
|
||||
createdBy: user?.id,
|
||||
dueDate: data.dueDate || null
|
||||
};
|
||||
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) => {
|
||||
return 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: SimpleTaskFormData) => {
|
||||
if (!localSelectedStoreId) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Aucun magasin disponible",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (task) {
|
||||
updateMutation.mutate(data);
|
||||
} else {
|
||||
createMutation.mutate(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between border-b pb-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">
|
||||
{task ? "Modifier la tâche" : "Nouvelle tâche"}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* 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 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>
|
||||
|
||||
{/* Date d'échéance */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dueDate"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Date d'échéance</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-full pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP", { locale: fr })
|
||||
) : (
|
||||
<span>Sélectionner une date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
initialFocus
|
||||
locale={fr}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 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 border-t">
|
||||
<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 la tâche" : "Créer la tâche"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+89
-48
@@ -59,27 +59,55 @@ export default function Tasks() {
|
||||
const [taskToDelete, setTaskToDelete] = useState<TaskWithRelations | null>(null);
|
||||
|
||||
// Fetch tasks - attendre que l'initialisation des stores soit terminée pour les admins
|
||||
const { data: tasks = [], isLoading } = useQuery({
|
||||
const { data: tasks = [], isLoading, error } = useQuery({
|
||||
queryKey: ["/api/tasks", selectedStoreId],
|
||||
queryFn: () => {
|
||||
const params = new URLSearchParams();
|
||||
if (selectedStoreId && user?.role === 'admin') {
|
||||
params.append('storeId', selectedStoreId.toString());
|
||||
}
|
||||
console.log('📋 TASKS QUERY - Fetching with params:', {
|
||||
selectedStoreId,
|
||||
userRole: user?.role,
|
||||
storeInitialized,
|
||||
url: `/api/tasks?${params.toString()}`
|
||||
});
|
||||
return fetch(`/api/tasks?${params.toString()}`, {
|
||||
credentials: 'include'
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! status: ${res.status}`);
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (selectedStoreId && user?.role === 'admin') {
|
||||
params.append('storeId', selectedStoreId.toString());
|
||||
}
|
||||
return res.json();
|
||||
});
|
||||
console.log('📋 TASKS QUERY - Fetching with params:', {
|
||||
selectedStoreId,
|
||||
userRole: user?.role,
|
||||
storeInitialized,
|
||||
url: `/api/tasks?${params.toString()}`
|
||||
});
|
||||
|
||||
const response = await fetch(`/api/tasks?${params.toString()}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log('📋 TASKS QUERY - Response received:', {
|
||||
dataType: typeof data,
|
||||
isArray: Array.isArray(data),
|
||||
length: data?.length,
|
||||
firstTask: data?.[0] ? {
|
||||
id: data[0].id,
|
||||
title: data[0].title,
|
||||
hasStartDate: !!data[0].startDate,
|
||||
hasGroup: !!data[0].group
|
||||
} : null
|
||||
});
|
||||
|
||||
// Valider et nettoyer les données reçues
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('Tasks API returned non-array data:', data);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.filter(task => task && typeof task === 'object' && task.id);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching tasks:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
enabled: !!user && (user.role !== 'admin' || storeInitialized),
|
||||
});
|
||||
@@ -318,40 +346,53 @@ export default function Tasks() {
|
||||
const getStartDateStatus = (startDate: Date | string | null) => {
|
||||
if (!startDate) return null;
|
||||
|
||||
const start = new Date(startDate);
|
||||
const now = new Date();
|
||||
const daysDiff = differenceInDays(start, now);
|
||||
|
||||
if (isPast(start)) {
|
||||
return {
|
||||
type: 'started',
|
||||
color: 'default' as const,
|
||||
icon: CheckCircle,
|
||||
label: 'Démarrée',
|
||||
text: 'Active'
|
||||
};
|
||||
} else if (isToday(start)) {
|
||||
return {
|
||||
type: 'today',
|
||||
color: 'secondary' as const,
|
||||
icon: CalendarClock,
|
||||
label: "Démarre aujourd'hui",
|
||||
text: "Aujourd'hui"
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'future',
|
||||
color: 'outline' as const,
|
||||
icon: Clock,
|
||||
label: `Démarre dans ${daysDiff} jour${daysDiff > 1 ? 's' : ''}`,
|
||||
text: `+${daysDiff}j`
|
||||
};
|
||||
try {
|
||||
const start = new Date(startDate);
|
||||
if (isNaN(start.getTime())) return null;
|
||||
|
||||
const now = new Date();
|
||||
const daysDiff = differenceInDays(start, now);
|
||||
|
||||
if (isPast(start)) {
|
||||
return {
|
||||
type: 'started',
|
||||
color: 'default' as const,
|
||||
icon: CheckCircle,
|
||||
label: 'Démarrée',
|
||||
text: 'Active'
|
||||
};
|
||||
} else if (isToday(start)) {
|
||||
return {
|
||||
type: 'today',
|
||||
color: 'secondary' as const,
|
||||
icon: CalendarClock,
|
||||
label: "Démarre aujourd'hui",
|
||||
text: "Aujourd'hui"
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'future',
|
||||
color: 'outline' as const,
|
||||
icon: Clock,
|
||||
label: `Démarre dans ${daysDiff} jour${daysDiff > 1 ? 's' : ''}`,
|
||||
text: `+${daysDiff}j`
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error parsing start date:', error, startDate);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour déterminer si une tâche est future (pas encore commencée)
|
||||
const isTaskFuture = (task: TaskWithRelations) => {
|
||||
return task.isFutureTask || (task.startDate && !isPast(new Date(task.startDate)));
|
||||
if (!task) return false;
|
||||
try {
|
||||
return task.isFutureTask || (task.startDate && !isPast(new Date(task.startDate)));
|
||||
} catch (error) {
|
||||
console.warn('Error checking if task is future:', error, task);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const canCreateTasks = user?.role === 'admin' || user?.role === 'manager' || user?.role === 'directeur';
|
||||
|
||||
+44
-6
@@ -1619,12 +1619,50 @@ export class DatabaseStorage implements IStorage {
|
||||
}
|
||||
|
||||
const results = await query.orderBy(desc(tasks.createdAt));
|
||||
return results.map((row: any) => ({
|
||||
...row.task,
|
||||
group: row.group,
|
||||
// Ajouter le statut "future" pour les tâches futures (admin/directeur)
|
||||
isFutureTask: row.task.startDate && new Date(row.task.startDate) > today
|
||||
}));
|
||||
|
||||
console.log('📋 DatabaseStorage.getTasks - Raw results:', {
|
||||
resultCount: results.length,
|
||||
userRole,
|
||||
sampleTasks: results.slice(0, 2).map(r => ({
|
||||
taskExists: !!r.task,
|
||||
taskId: r.task?.id,
|
||||
title: r.task?.title,
|
||||
startDate: r.task?.startDate,
|
||||
groupExists: !!r.group
|
||||
}))
|
||||
});
|
||||
|
||||
return results
|
||||
.filter((row: any) => row.task) // Filtrer les tâches nulles
|
||||
.map((row: any) => {
|
||||
try {
|
||||
const task = row.task;
|
||||
const group = row.group || null;
|
||||
|
||||
// Calculer si la tâche est future de manière sécurisée
|
||||
let isFutureTask = false;
|
||||
if (task.startDate) {
|
||||
try {
|
||||
const startDate = new Date(task.startDate);
|
||||
if (!isNaN(startDate.getTime())) {
|
||||
isFutureTask = startDate > today;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error parsing startDate:', error, task.startDate);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...task,
|
||||
group,
|
||||
isFutureTask
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error processing task row:', error, row);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
async getTask(id: number): Promise<TaskWithRelations | undefined> {
|
||||
|
||||
Reference in new issue
Block a user