mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-12 01:37:40 +02:00
Merge pull request #192 from R0m1k3/1.1
Improve announcement card display and fix creation bug
This commit is contained in:
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