mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
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:
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 |
@@ -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<AnnouncementWithRelations | null>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
// Configuration du formulaire
|
||||
const form = useForm<InsertAnnouncement>({
|
||||
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<AnnouncementWithRelations[]>({
|
||||
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<typeof formSchema>) => {
|
||||
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<typeof formSchema>) => {
|
||||
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 <AlertTriangle className="h-4 w-4" />;
|
||||
case 'important':
|
||||
return <Bell className="h-4 w-4" />;
|
||||
default:
|
||||
return <Info className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
// 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 (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Informations</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{user?.role === 'admin' && (
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={(open) => {
|
||||
setIsCreateDialogOpen(open);
|
||||
if (!open) {
|
||||
setEditingAnnouncement(null);
|
||||
form.reset();
|
||||
}
|
||||
}}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" variant="ghost" className="h-8 w-8 p-0">
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<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}
|
||||
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>
|
||||
<>
|
||||
<Card className="bg-white border border-gray-200 shadow-lg hover:shadow-xl transition-shadow">
|
||||
<CardHeader className="pb-4 border-b border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg font-semibold text-gray-800 flex items-center">
|
||||
<Megaphone className="h-5 w-5 mr-3 text-blue-600" />
|
||||
Informations
|
||||
</CardTitle>
|
||||
{user?.role === 'admin' && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditingAnnouncement(null);
|
||||
form.reset({
|
||||
title: '',
|
||||
content: '',
|
||||
type: 'info',
|
||||
storeId: selectedStoreId ? parseInt(selectedStoreId.toString()) : null,
|
||||
createdBy: user?.username || '',
|
||||
});
|
||||
setIsCreateDialogOpen(true);
|
||||
}}
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nouvelle
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{announcements.map((announcement) => {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 p-6">
|
||||
{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 TypeIcon = getTypeIcon(announcement.type);
|
||||
|
||||
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 className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-medium text-sm truncate">
|
||||
{announcement.title}
|
||||
</h4>
|
||||
<Badge variant={priorityConfig.color} className="flex items-center gap-1 text-xs">
|
||||
<TypeIcon className="w-2.5 h-2.5" />
|
||||
{priorityConfig.label}
|
||||
</Badge>
|
||||
</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}
|
||||
<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 items-center space-x-3">
|
||||
<div className="h-2 w-2 bg-blue-500"></div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{announcement.title}</p>
|
||||
{announcement.content && (
|
||||
<p className="text-sm text-gray-600 truncate max-w-60">{announcement.content}</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{announcement.author?.firstName} {announcement.author?.lastName}
|
||||
{announcement.group && ` • ${announcement.group.name}`}
|
||||
</span>
|
||||
</p>
|
||||
</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={() => {
|
||||
setEditingAnnouncement(announcement);
|
||||
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 className="text-right flex items-center space-x-2">
|
||||
<div>
|
||||
<Badge className={`text-xs ${priorityConfig.color}`}>
|
||||
{priorityConfig.label}
|
||||
</Badge>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{safeFormat(safeDate(announcement.createdAt), 'dd/MM à HH:mm')}
|
||||
</p>
|
||||
</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>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
})
|
||||
)}
|
||||
</CardContent>
|
||||
</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: () => {
|
||||
const params = new URLSearchParams();
|
||||
if (selectedStoreId) {
|
||||
params.append('storeId', selectedStoreId);
|
||||
params.append('storeId', selectedStoreId.toString());
|
||||
}
|
||||
return fetch(`/api/tasks?${params.toString()}`).then(res => res.json());
|
||||
},
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# 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
|
||||
@@ -46,7 +46,7 @@ class AnnouncementDatabaseStorage implements IAnnouncementStorage {
|
||||
|
||||
const results = await query;
|
||||
|
||||
return results.map(result => ({
|
||||
return results.map((result: any) => ({
|
||||
id: result.id,
|
||||
title: result.title,
|
||||
content: result.content,
|
||||
@@ -249,15 +249,12 @@ class AnnouncementMemoryStorage implements IAnnouncementStorage {
|
||||
return announcementWithRelations;
|
||||
}
|
||||
|
||||
async deleteAnnouncement(id: number): Promise<boolean> {
|
||||
const existed = this.announcements.has(id);
|
||||
async deleteAnnouncement(id: number): Promise<void> {
|
||||
const deleted = this.announcements.delete(id);
|
||||
|
||||
if (deleted) {
|
||||
console.log(`🗑️ Annonce supprimée: ID ${id} (${this.announcements.size}/${this.MAX_ANNOUNCEMENTS} restantes)`);
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -3043,18 +3043,19 @@ RÉSUMÉ DU SCAN
|
||||
const announcements = await Promise.all(
|
||||
messages.map(async (message) => {
|
||||
// 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 {
|
||||
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) {
|
||||
author = {
|
||||
id: parseInt(message.createdBy),
|
||||
id: userResult.id,
|
||||
firstName: userResult.firstName || 'Utilisateur',
|
||||
lastName: userResult.lastName || 'Inconnu'
|
||||
};
|
||||
}
|
||||
} 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
|
||||
@@ -3152,7 +3153,7 @@ RÉSUMÉ DU SCAN
|
||||
content: announcementData.content,
|
||||
type: announcementData.type,
|
||||
storeId: announcementData.storeId,
|
||||
createdBy: user.id.toString(),
|
||||
createdBy: user.username, // Utiliser username plutôt que ID pour PostgreSQL
|
||||
}).returning();
|
||||
|
||||
const announcement = {
|
||||
|
||||
Reference in new issue
Block a user