Merge pull request #192 from R0m1k3/1.1

Improve announcement card display and fix creation bug
This commit is contained in:
LogiFlow authored and GitHub committed 2025-08-17 11:18:13 +02:00
commit 0e64ec5499
6 files changed
+358 -367

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+348 -355
View File
@@ -1,449 +1,442 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { useStore } from "@/components/Layout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Megaphone, Plus, X, AlertTriangle, Clock, Circle, Edit2, Bell, Info, CheckCircle } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import React from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { insertAnnouncementSchema } from "@shared/schema"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { AnnouncementWithRelations } from "@shared/schema"; import { Button } from "@/components/ui/button";
import { z } from "zod"; import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Megaphone,
Plus,
Edit2,
X,
Info,
AlertTriangle,
CheckCircle,
XCircle
} from "lucide-react";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { useStore } from "@/components/Layout";
import { safeFormat, safeDate } from "@/lib/dateUtils"; import { safeFormat, safeDate } from "@/lib/dateUtils";
import {
insertAnnouncementSchema,
type InsertAnnouncement,
type DashboardMessage
} from "@shared/schema";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
// Schéma adapté pour DASHBOARD_MESSAGES // Type pour les annonces avec relations
const formSchema = z.object({ type AnnouncementWithRelations = DashboardMessage & {
title: z.string().min(1, "Le titre est obligatoire"), author: {
content: z.string().min(1, "Le contenu est obligatoire"), id: string | number;
type: z.enum(["info", "warning", "error", "success"]).default("info"), firstName?: string;
}); lastName?: string;
username: string;
};
group?: {
id: number;
name: string;
};
};
// Configuration des priorités
const getPriorityConfig = (type: string) => {
switch (type) {
case 'error':
return { label: 'Erreur', color: 'bg-red-100 text-red-800' };
case 'warning':
return { label: 'Attention', color: 'bg-orange-100 text-orange-800' };
case 'success':
return { label: 'Succès', color: 'bg-green-100 text-green-800' };
case 'info':
default:
return { label: 'Information', color: 'bg-blue-100 text-blue-800' };
}
};
export default function AnnouncementCard() { export default function AnnouncementCard() {
const { user } = useAuthUnified(); const { user } = useAuthUnified();
const { selectedStoreId } = useStore(); const { selectedStoreId } = useStore();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { toast } = useToast();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [editingAnnouncement, setEditingAnnouncement] = useState<AnnouncementWithRelations | null>(null); const [editingAnnouncement, setEditingAnnouncement] = useState<AnnouncementWithRelations | null>(null);
const { toast } = useToast();
const form = useForm<z.infer<typeof formSchema>>({ // Configuration du formulaire
resolver: zodResolver(formSchema), const form = useForm<InsertAnnouncement>({
resolver: zodResolver(insertAnnouncementSchema),
defaultValues: { defaultValues: {
title: "", title: '',
content: "", content: '',
type: "info", type: 'info',
storeId: selectedStoreId ? parseInt(selectedStoreId.toString()) : null,
createdBy: user?.username || '',
}, },
}); });
// Reset form when editing announcement changes // Construire l'URL avec les paramètres appropriés
React.useEffect(() => { const buildApiUrl = () => {
if (editingAnnouncement) { const params = new URLSearchParams();
form.reset({
title: editingAnnouncement.title, // Pour les admins, permettre le filtrage par magasin sélectionné
content: editingAnnouncement.content, if (user?.role === 'admin' && selectedStoreId) {
type: editingAnnouncement.type as "info" | "warning" | "error" | "success", params.append('storeId', selectedStoreId.toString());
});
} else {
form.reset({
title: "",
content: "",
type: "info",
});
} }
}, [editingAnnouncement, form]);
return `/api/announcements${params.toString() ? `?${params.toString()}` : ''}`;
};
// Fetch announcements // Récupérer les annonces
const { data: announcements = [], isLoading } = useQuery<AnnouncementWithRelations[]>({ const { data: announcements = [], isLoading } = useQuery<AnnouncementWithRelations[]>({
queryKey: ['/api/announcements', selectedStoreId], queryKey: ['/api/announcements', selectedStoreId, user?.role],
queryFn: async () => { queryFn: async () => {
const params = new URLSearchParams(); const url = buildApiUrl();
if (selectedStoreId && user?.role === 'admin') { console.log('🔍 [ANNOUNCEMENTS] Fetching from:', url);
params.append('storeId', selectedStoreId.toString());
}
const response = await fetch(`/api/announcements?${params}`, { const response = await fetch(url, {
credentials: 'include' credentials: 'include'
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch announcements'); console.error('❌ [ANNOUNCEMENTS] Fetch failed:', response.status, response.statusText);
if (response.status === 401) {
console.log('🔑 [ANNOUNCEMENTS] Authentication required');
return [];
}
throw new Error(`Failed to fetch announcements: ${response.status}`);
} }
return response.json(); const data = await response.json();
console.log('✅ [ANNOUNCEMENTS] Data received:', data);
if (!Array.isArray(data)) {
console.warn('⚠️ [ANNOUNCEMENTS] Data is not an array:', data);
return [];
}
return data;
}, },
enabled: !!user, enabled: !!user,
}); });
// Mutation pour créer/modifier une annonce
// Create/Update announcement mutation
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: async (data: z.infer<typeof formSchema>) => { mutationFn: async (announcementData: InsertAnnouncement) => {
const isEditing = !!editingAnnouncement; const url = editingAnnouncement
console.log('📢 Submitting announcement:', { data, isEditing });
const url = isEditing
? `/api/announcements/${editingAnnouncement.id}` ? `/api/announcements/${editingAnnouncement.id}`
: '/api/announcements'; : '/api/announcements';
const method = isEditing ? 'PUT' : 'POST'; const method = editingAnnouncement ? 'PUT' : 'POST';
console.log(`🔍 [ANNOUNCEMENTS] ${method} ${url}`, announcementData);
const response = await fetch(url, { const response = await fetch(url, {
method, method,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include', credentials: 'include',
body: JSON.stringify(data), body: JSON.stringify(announcementData),
}); });
console.log('📢 Announcement response:', {
status: response.status,
statusText: response.statusText,
ok: response.ok
});
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('📢 Announcement failed:', errorText); console.error(`❌ [ANNOUNCEMENTS] ${method} failed:`, response.status, errorText);
throw new Error(`Failed to ${isEditing ? 'update' : 'create'} announcement: ${response.status} - ${errorText}`); throw new Error(`Failed to ${editingAnnouncement ? 'update' : 'create'} announcement: ${response.status}`);
} }
const result = await response.json(); return response.json();
console.log('📢 Announcement success:', result);
return result;
}, },
onSuccess: (data) => { onSuccess: () => {
const isEditing = !!editingAnnouncement;
console.log('📢 Announcement success callback:', data);
queryClient.invalidateQueries({ queryKey: ['/api/announcements'] }); queryClient.invalidateQueries({ queryKey: ['/api/announcements'] });
setIsCreateDialogOpen(false); setIsCreateDialogOpen(false);
setEditingAnnouncement(null); setEditingAnnouncement(null);
form.reset(); form.reset();
toast({ toast({
title: "Succès", title: editingAnnouncement ? "Information modifiée" : "Information créée",
description: isEditing ? "Information modifiée avec succès" : "Information créée avec succès", description: editingAnnouncement ? "L'information a été modifiée avec succès." : "L'information a été créée avec succès.",
}); });
}, },
onError: (error) => { onError: (error) => {
const isEditing = !!editingAnnouncement; console.error('❌ [ANNOUNCEMENTS] Mutation error:', error);
console.error('📢 Announcement error:', error);
toast({ toast({
title: "Erreur", title: "Erreur",
description: error.message || `Impossible de ${isEditing ? 'modifier' : 'créer'} l'information`, description: `Impossible de ${editingAnnouncement ? 'modifier' : 'créer'} l'information.`,
variant: "destructive", variant: "destructive",
}); });
}, },
}); });
// Delete announcement mutation // Mutation pour supprimer une annonce
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: async (id: number) => { mutationFn: async (announcementId: number) => {
const response = await fetch(`/api/announcements/${id}`, { const response = await fetch(`/api/announcements/${announcementId}`, {
method: 'DELETE', method: 'DELETE',
credentials: 'include', credentials: 'include',
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to delete announcement'); throw new Error(`Failed to delete announcement: ${response.status}`);
} }
return response.json(); return response.json();
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/announcements'] }); queryClient.invalidateQueries({ queryKey: ['/api/announcements'] });
toast({
title: "Information supprimée",
description: "L'information a été supprimée avec succès.",
});
},
onError: (error) => {
console.error('❌ [ANNOUNCEMENTS] Delete error:', error);
toast({
title: "Erreur",
description: "Impossible de supprimer l'information.",
variant: "destructive",
});
}, },
}); });
const onSubmit = (data: z.infer<typeof formSchema>) => { // Soumission du formulaire
console.log('🔥 [FORM] onSubmit called with data:', data); const onSubmit = (data: InsertAnnouncement) => {
console.log('🔥 [FORM] Form errors:', form.formState.errors); console.log('🔍 [ANNOUNCEMENTS] Form submitted:', data);
console.log('🔥 [FORM] Form is valid:', form.formState.isValid);
console.log('🔥 [FORM] Form is submitting:', form.formState.isSubmitting);
if (!data.title || !data.content) { const announcementData = {
console.error('🔥 [FORM] Missing required fields:', { title: data.title, content: data.content }); ...data,
toast({ createdBy: user?.username || '',
title: "Erreur", storeId: data.storeId || null
description: "Le titre et le contenu sont obligatoires", };
variant: "destructive",
});
return;
}
console.log('🔥 [FORM] Calling createMutation.mutate'); createMutation.mutate(announcementData);
createMutation.mutate(data);
}; };
const getPriorityIcon = (priority: string) => { // Ouvrir le dialogue pour éditer
switch (priority) { const handleEdit = (announcement: AnnouncementWithRelations) => {
case 'urgent': setEditingAnnouncement(announcement);
return <AlertTriangle className="h-4 w-4" />; form.reset({
case 'important': title: announcement.title,
return <Bell className="h-4 w-4" />; content: announcement.content,
default: type: announcement.type as any,
return <Info className="h-4 w-4" />; storeId: announcement.storeId,
} createdBy: announcement.createdBy,
}; });
setIsCreateDialogOpen(true);
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'urgent':
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
case 'important':
return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200';
default:
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
}
};
if (isLoading) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Informations</CardTitle>
<Megaphone 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>
);
}
const getPriorityConfig = (type: string) => {
switch (type) {
case 'error':
return {
color: 'destructive' as const,
icon: AlertTriangle,
label: 'Erreur'
};
case 'warning':
return {
color: 'default' as const,
icon: Bell,
label: 'Attention'
};
case 'success':
return {
color: 'default' as const,
icon: CheckCircle,
label: 'Succès'
};
case 'info':
default:
return {
color: 'secondary' as const,
icon: Info,
label: 'Information'
};
}
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'error':
return AlertTriangle;
case 'warning':
return Bell;
case 'success':
return CheckCircle;
case 'info':
default:
return Info;
}
}; };
return ( return (
<Card> <>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <Card className="bg-white border border-gray-200 shadow-lg hover:shadow-xl transition-shadow">
<CardTitle className="text-sm font-medium">Informations</CardTitle> <CardHeader className="pb-4 border-b border-gray-100">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between">
{user?.role === 'admin' && ( <CardTitle className="text-lg font-semibold text-gray-800 flex items-center">
<Dialog open={isCreateDialogOpen} onOpenChange={(open) => { <Megaphone className="h-5 w-5 mr-3 text-blue-600" />
setIsCreateDialogOpen(open); Informations
if (!open) { </CardTitle>
setEditingAnnouncement(null); {user?.role === 'admin' && (
form.reset(); <Button
} onClick={() => {
}}> setEditingAnnouncement(null);
<DialogTrigger asChild> form.reset({
<Button size="sm" variant="ghost" className="h-8 w-8 p-0"> title: '',
<Plus className="h-4 w-4" /> content: '',
</Button> type: 'info',
</DialogTrigger> storeId: selectedStoreId ? parseInt(selectedStoreId.toString()) : null,
<DialogContent className="sm:max-w-[500px]"> createdBy: user?.username || '',
<DialogHeader> });
<DialogTitle> setIsCreateDialogOpen(true);
{editingAnnouncement ? 'Modifier l\'information' : 'Nouvelle information'} }}
</DialogTitle> size="sm"
</DialogHeader> className="bg-blue-600 hover:bg-blue-700 text-white"
<Form {...form}> >
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <Plus className="h-4 w-4 mr-2" />
<FormField Nouvelle
control={form.control} </Button>
name="title" )}
render={({ field }) => (
<FormItem>
<FormLabel>Titre</FormLabel>
<FormControl>
<Input placeholder="Titre de l'information" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>Contenu</FormLabel>
<FormControl>
<Textarea
placeholder="Contenu de l'information"
rows={4}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Type</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Sélectionner un type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="info">Information</SelectItem>
<SelectItem value="warning">Attention</SelectItem>
<SelectItem value="error">Erreur</SelectItem>
<SelectItem value="success">Succès</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-2 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setIsCreateDialogOpen(false)}
>
Annuler
</Button>
<Button
type="submit"
disabled={createMutation.isPending}
onClick={() => console.log('🔥 [BUTTON] Submit button clicked')}
>
{createMutation.isPending
? (editingAnnouncement ? 'Modification...' : 'Création...')
: (editingAnnouncement ? 'Modifier' : 'Créer')
}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
)}
<Megaphone className="h-4 w-4 text-muted-foreground" />
</div>
</CardHeader>
<CardContent>
{announcements.length === 0 ? (
<div className="text-center py-6">
<Megaphone className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
<p className="text-sm text-muted-foreground">
Aucune information disponible
</p>
</div> </div>
) : ( </CardHeader>
<div className="space-y-3"> <CardContent className="space-y-3 p-6">
{announcements.map((announcement) => { {isLoading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : !Array.isArray(announcements) || announcements.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-600">Aucune information disponible</p>
<p className="text-xs text-gray-400 mt-1">
{user?.role === 'admin' ? 'Créez la première annonce' : 'Aucune annonce pour le moment'}
</p>
</div>
) : (
announcements.map((announcement) => {
const priorityConfig = getPriorityConfig(announcement.type); const priorityConfig = getPriorityConfig(announcement.type);
const TypeIcon = getTypeIcon(announcement.type);
return ( return (
<div key={announcement.id} className="flex items-start justify-between p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors"> <div key={announcement.id} className="flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors border-l-3 border-blue-500">
<div className="flex-1 min-w-0"> <div className="flex items-center space-x-3">
<div className="flex items-center gap-2 mb-1"> <div className="h-2 w-2 bg-blue-500"></div>
<h4 className="font-medium text-sm truncate"> <div>
{announcement.title} <p className="font-medium text-gray-900">{announcement.title}</p>
</h4> {announcement.content && (
<Badge variant={priorityConfig.color} className="flex items-center gap-1 text-xs"> <p className="text-sm text-gray-600 truncate max-w-60">{announcement.content}</p>
<TypeIcon className="w-2.5 h-2.5" /> )}
{priorityConfig.label} <p className="text-xs text-gray-500 mt-1">
</Badge> {announcement.author?.firstName} {announcement.author?.lastName}
</div>
{announcement.content && (
<p className="text-xs text-muted-foreground mb-1 line-clamp-1">
{announcement.content}
</p>
)}
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span>
{safeFormat(safeDate(announcement.createdAt), 'dd/MM à HH:mm')}
</span>
<span>
{announcement.author.firstName} {announcement.author.lastName}
{announcement.group && ` • ${announcement.group.name}`} {announcement.group && ` • ${announcement.group.name}`}
</span> </p>
</div> </div>
</div> </div>
{user?.role === 'admin' && ( <div className="text-right flex items-center space-x-2">
<div className="flex items-center gap-1 ml-2 flex-shrink-0"> <div>
<Button <Badge className={`text-xs ${priorityConfig.color}`}>
variant="ghost" {priorityConfig.label}
size="sm" </Badge>
className="h-6 w-6 p-0 text-muted-foreground hover:text-blue-600" <p className="text-xs text-gray-500 mt-1">
onClick={() => { {safeFormat(safeDate(announcement.createdAt), 'dd/MM à HH:mm')}
setEditingAnnouncement(announcement); </p>
setIsCreateDialogOpen(true);
}}
>
<Edit2 className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive"
onClick={() => deleteMutation.mutate(announcement.id)}
disabled={deleteMutation.isPending}
>
<X className="h-3 w-3" />
</Button>
</div> </div>
)} {user?.role === 'admin' && (
<div className="flex items-center gap-1 ml-2 flex-shrink-0">
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground hover:text-blue-600"
onClick={() => handleEdit(announcement)}
>
<Edit2 className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive"
onClick={() => deleteMutation.mutate(announcement.id)}
disabled={deleteMutation.isPending}
>
<X className="h-3 w-3" />
</Button>
</div>
)}
</div>
</div> </div>
); );
})} })
</div> )}
)} </CardContent>
</CardContent> </Card>
</Card>
{/* Dialog pour créer/modifier une annonce */}
<Dialog open={isCreateDialogOpen} onOpenChange={(open) => {
setIsCreateDialogOpen(open);
if (!open) {
setEditingAnnouncement(null);
form.reset();
}
}}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>
{editingAnnouncement ? 'Modifier l\'information' : 'Nouvelle information'}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Titre</FormLabel>
<FormControl>
<Input placeholder="Titre de l'information" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>Contenu</FormLabel>
<FormControl>
<Textarea
placeholder="Contenu de l'information"
rows={4}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Type</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Sélectionner un type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="info">Information</SelectItem>
<SelectItem value="warning">Attention</SelectItem>
<SelectItem value="error">Erreur</SelectItem>
<SelectItem value="success">Succès</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-2 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setIsCreateDialogOpen(false)}
>
Annuler
</Button>
<Button
type="submit"
disabled={createMutation.isPending}
>
{createMutation.isPending
? (editingAnnouncement ? 'Modification...' : 'Création...')
: (editingAnnouncement ? 'Modifier' : 'Créer')
}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
</>
); );
} }
@@ -33,7 +33,7 @@ export default function RecentTasksCard() {
queryFn: () => { queryFn: () => {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (selectedStoreId) { if (selectedStoreId) {
params.append('storeId', selectedStoreId); params.append('storeId', selectedStoreId.toString());
} }
return fetch(`/api/tasks?${params.toString()}`).then(res => res.json()); return fetch(`/api/tasks?${params.toString()}`).then(res => res.json());
}, },
+1 -1
View File
@@ -2,4 +2,4 @@
# https://curl.se/docs/http-cookies.html # https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk. # This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 1755334812 connect.sid s%3AIUsQcAbYjJ2Yu9GRwhXwQt22rGvvMGRo.zTBgX64O5zq1nh16Yc0umuKKQnBAy0xAn4BkA5iErOQ #HttpOnly_localhost FALSE / FALSE 1755508518 connect.sid s%3A2H06I9RjaU2RHqAtgys18vROWYhhcdKw.StlcmCQB97ez7pxB4FWYVSv83LR9nxAxXRWu7do9v7E
+2 -5
View File
@@ -46,7 +46,7 @@ class AnnouncementDatabaseStorage implements IAnnouncementStorage {
const results = await query; const results = await query;
return results.map(result => ({ return results.map((result: any) => ({
id: result.id, id: result.id,
title: result.title, title: result.title,
content: result.content, content: result.content,
@@ -249,15 +249,12 @@ class AnnouncementMemoryStorage implements IAnnouncementStorage {
return announcementWithRelations; return announcementWithRelations;
} }
async deleteAnnouncement(id: number): Promise<boolean> { async deleteAnnouncement(id: number): Promise<void> {
const existed = this.announcements.has(id);
const deleted = this.announcements.delete(id); const deleted = this.announcements.delete(id);
if (deleted) { if (deleted) {
console.log(`🗑️ Annonce supprimée: ID ${id} (${this.announcements.size}/${this.MAX_ANNOUNCEMENTS} restantes)`); console.log(`🗑️ Annonce supprimée: ID ${id} (${this.announcements.size}/${this.MAX_ANNOUNCEMENTS} restantes)`);
} }
return deleted;
} }
} }
+6 -5
View File
@@ -3043,18 +3043,19 @@ RÉSUMÉ DU SCAN
const announcements = await Promise.all( const announcements = await Promise.all(
messages.map(async (message) => { messages.map(async (message) => {
// Récupérer l'auteur // Récupérer l'auteur
let author = { id: parseInt(message.createdBy) || 0, firstName: 'Utilisateur', lastName: 'Inconnu' }; let author = { id: message.createdBy, firstName: 'Utilisateur', lastName: 'Inconnu' };
try { try {
const [userResult] = await db.select().from(users).where(eq(users.id, message.createdBy)); // En production, createdBy est varchar, donc chercher par username
const [userResult] = await db.select().from(users).where(eq(users.username, message.createdBy));
if (userResult) { if (userResult) {
author = { author = {
id: parseInt(message.createdBy), id: userResult.id,
firstName: userResult.firstName || 'Utilisateur', firstName: userResult.firstName || 'Utilisateur',
lastName: userResult.lastName || 'Inconnu' lastName: userResult.lastName || 'Inconnu'
}; };
} }
} catch (e) { } catch (e) {
console.warn('Could not fetch author for message:', message.id); console.warn('❌ [PRODUCTION] Could not fetch author for message:', message.id, e);
} }
// Récupérer le groupe si storeId est défini // Récupérer le groupe si storeId est défini
@@ -3152,7 +3153,7 @@ RÉSUMÉ DU SCAN
content: announcementData.content, content: announcementData.content,
type: announcementData.type, type: announcementData.type,
storeId: announcementData.storeId, storeId: announcementData.storeId,
createdBy: user.id.toString(), createdBy: user.username, // Utiliser username plutôt que ID pour PostgreSQL
}).returning(); }).returning();
const announcement = { const announcement = {