Improve announcement card display and fix creation bug

Update the announcement card component to match the dashboard's visual style and resolve an issue where new announcements were not displaying correctly due to incorrect data handling and display logic.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e437ffed-c31b-44eb-9fd5-b6f9faa8e7ce
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/e437ffed-c31b-44eb-9fd5-b6f9faa8e7ce/dnQ3tT8
This commit is contained in:
michaelschal committed 2025-08-17 09:17:43 +00:00
1 parent 9f694d0251
commit 243240701c
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 = {