diff --git a/attached_assets/image_1755421756383.png b/attached_assets/image_1755421756383.png new file mode 100644 index 0000000..cfdb340 Binary files /dev/null and b/attached_assets/image_1755421756383.png differ diff --git a/client/src/components/AnnouncementCard.tsx b/client/src/components/AnnouncementCard.tsx index e982708..d04e6b1 100644 --- a/client/src/components/AnnouncementCard.tsx +++ b/client/src/components/AnnouncementCard.tsx @@ -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 React from "react"; -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 { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { insertAnnouncementSchema } from "@shared/schema"; -import type { AnnouncementWithRelations } from "@shared/schema"; -import { z } from "zod"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +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 { + insertAnnouncementSchema, + type InsertAnnouncement, + type DashboardMessage +} from "@shared/schema"; import { useToast } from "@/hooks/use-toast"; -// Schéma adapté pour DASHBOARD_MESSAGES -const formSchema = z.object({ - title: z.string().min(1, "Le titre est obligatoire"), - content: z.string().min(1, "Le contenu est obligatoire"), - type: z.enum(["info", "warning", "error", "success"]).default("info"), -}); +// Type pour les annonces avec relations +type AnnouncementWithRelations = DashboardMessage & { + author: { + id: string | number; + 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() { const { user } = useAuthUnified(); const { selectedStoreId } = useStore(); const queryClient = useQueryClient(); + const { toast } = useToast(); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [editingAnnouncement, setEditingAnnouncement] = useState(null); - const { toast } = useToast(); - const form = useForm>({ - resolver: zodResolver(formSchema), + // Configuration du formulaire + const form = useForm({ + resolver: zodResolver(insertAnnouncementSchema), defaultValues: { - title: "", - content: "", - type: "info", + title: '', + content: '', + type: 'info', + storeId: selectedStoreId ? parseInt(selectedStoreId.toString()) : null, + createdBy: user?.username || '', }, }); - // Reset form when editing announcement changes - React.useEffect(() => { - if (editingAnnouncement) { - form.reset({ - title: editingAnnouncement.title, - content: editingAnnouncement.content, - type: editingAnnouncement.type as "info" | "warning" | "error" | "success", - }); - } else { - form.reset({ - title: "", - content: "", - type: "info", - }); + // Construire l'URL avec les paramètres appropriés + const buildApiUrl = () => { + const params = new URLSearchParams(); + + // Pour les admins, permettre le filtrage par magasin sélectionné + if (user?.role === 'admin' && selectedStoreId) { + params.append('storeId', selectedStoreId.toString()); } - }, [editingAnnouncement, form]); + + return `/api/announcements${params.toString() ? `?${params.toString()}` : ''}`; + }; - // Fetch announcements + // Récupérer les annonces const { data: announcements = [], isLoading } = useQuery({ - queryKey: ['/api/announcements', selectedStoreId], + queryKey: ['/api/announcements', selectedStoreId, user?.role], queryFn: async () => { - const params = new URLSearchParams(); - if (selectedStoreId && user?.role === 'admin') { - params.append('storeId', selectedStoreId.toString()); - } + const url = buildApiUrl(); + console.log('🔍 [ANNOUNCEMENTS] Fetching from:', url); - const response = await fetch(`/api/announcements?${params}`, { + const response = await fetch(url, { credentials: 'include' }); 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, }); - - - // Create/Update announcement mutation + // Mutation pour créer/modifier une annonce const createMutation = useMutation({ - mutationFn: async (data: z.infer) => { - const isEditing = !!editingAnnouncement; - console.log('📢 Submitting announcement:', { data, isEditing }); - - const url = isEditing + mutationFn: async (announcementData: InsertAnnouncement) => { + const url = editingAnnouncement ? `/api/announcements/${editingAnnouncement.id}` : '/api/announcements'; - const method = isEditing ? 'PUT' : 'POST'; + const method = editingAnnouncement ? 'PUT' : 'POST'; + + console.log(`🔍 [ANNOUNCEMENTS] ${method} ${url}`, announcementData); const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, 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) { const errorText = await response.text(); - console.error('📢 Announcement failed:', errorText); - throw new Error(`Failed to ${isEditing ? 'update' : 'create'} announcement: ${response.status} - ${errorText}`); + console.error(`❌ [ANNOUNCEMENTS] ${method} failed:`, response.status, errorText); + throw new Error(`Failed to ${editingAnnouncement ? 'update' : 'create'} announcement: ${response.status}`); } - - const result = await response.json(); - console.log('📢 Announcement success:', result); - return result; + + return response.json(); }, - onSuccess: (data) => { - const isEditing = !!editingAnnouncement; - console.log('📢 Announcement success callback:', data); + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/announcements'] }); setIsCreateDialogOpen(false); setEditingAnnouncement(null); form.reset(); toast({ - title: "Succès", - description: isEditing ? "Information modifiée avec succès" : "Information créée avec succès", + title: editingAnnouncement ? "Information modifiée" : "Information créée", + description: editingAnnouncement ? "L'information a été modifiée avec succès." : "L'information a été créée avec succès.", }); }, onError: (error) => { - const isEditing = !!editingAnnouncement; - console.error('📢 Announcement error:', error); + console.error('❌ [ANNOUNCEMENTS] Mutation error:', error); toast({ title: "Erreur", - description: error.message || `Impossible de ${isEditing ? 'modifier' : 'créer'} l'information`, + description: `Impossible de ${editingAnnouncement ? 'modifier' : 'créer'} l'information.`, variant: "destructive", }); }, }); - // Delete announcement mutation + // Mutation pour supprimer une annonce const deleteMutation = useMutation({ - mutationFn: async (id: number) => { - const response = await fetch(`/api/announcements/${id}`, { + mutationFn: async (announcementId: number) => { + const response = await fetch(`/api/announcements/${announcementId}`, { method: 'DELETE', credentials: 'include', }); - + if (!response.ok) { - throw new Error('Failed to delete announcement'); + throw new Error(`Failed to delete announcement: ${response.status}`); } - + return response.json(); }, onSuccess: () => { 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) => { - console.log('🔥 [FORM] onSubmit called with data:', data); - console.log('🔥 [FORM] Form errors:', form.formState.errors); - console.log('🔥 [FORM] Form is valid:', form.formState.isValid); - console.log('🔥 [FORM] Form is submitting:', form.formState.isSubmitting); + // Soumission du formulaire + const onSubmit = (data: InsertAnnouncement) => { + console.log('🔍 [ANNOUNCEMENTS] Form submitted:', data); - if (!data.title || !data.content) { - console.error('🔥 [FORM] Missing required fields:', { title: data.title, content: data.content }); - toast({ - title: "Erreur", - description: "Le titre et le contenu sont obligatoires", - variant: "destructive", - }); - return; - } + const announcementData = { + ...data, + createdBy: user?.username || '', + storeId: data.storeId || null + }; - console.log('🔥 [FORM] Calling createMutation.mutate'); - createMutation.mutate(data); + createMutation.mutate(announcementData); }; - const getPriorityIcon = (priority: string) => { - switch (priority) { - case 'urgent': - return ; - case 'important': - return ; - default: - return ; - } - }; - - 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 ( - - - Informations - - - -
-
-
-
-
- ); - } - - 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; - } + // Ouvrir le dialogue pour éditer + const handleEdit = (announcement: AnnouncementWithRelations) => { + setEditingAnnouncement(announcement); + form.reset({ + title: announcement.title, + content: announcement.content, + type: announcement.type as any, + storeId: announcement.storeId, + createdBy: announcement.createdBy, + }); + setIsCreateDialogOpen(true); }; return ( - - - Informations -
- {user?.role === 'admin' && ( - { - setIsCreateDialogOpen(open); - if (!open) { - setEditingAnnouncement(null); - form.reset(); - } - }}> - - - - - - - {editingAnnouncement ? 'Modifier l\'information' : 'Nouvelle information'} - - -
- - ( - - Titre - - - - - - )} - /> - ( - - Contenu - -