mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-17 12:16:56 +02:00
Merge pull request #251 from R0m1k3/1.1
Improve task management by handling date errors and ensuring data int…
This commit is contained in:
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