Files
LogiFlow/client/src/components/AnnouncementCard.tsx
T
michaelschal 26d1a7aada Update announcement cards to match task card design and add type icons
Refactors the `AnnouncementCard` component to replicate the styling of the "To Do" task card, including icon placement next to titles, grey message text, square message corners, and right-aligned priority information with corresponding icons.

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/ZIhswcI
2025-08-17 08:22:15 +00:00

453 lines
16 KiB
TypeScript

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 { 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 { safeFormat, safeDate } from "@/lib/dateUtils";
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"),
});
export default function AnnouncementCard() {
const { user } = useAuthUnified();
const { selectedStoreId } = useStore();
const queryClient = useQueryClient();
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),
defaultValues: {
title: "",
content: "",
type: "info",
},
});
// 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",
});
}
}, [editingAnnouncement, form]);
// Fetch announcements
const { data: announcements = [], isLoading } = useQuery<AnnouncementWithRelations[]>({
queryKey: ['/api/announcements', selectedStoreId],
queryFn: async () => {
const params = new URLSearchParams();
if (selectedStoreId && user?.role === 'admin') {
params.append('storeId', selectedStoreId.toString());
}
const response = await fetch(`/api/announcements?${params}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch announcements');
}
return response.json();
},
enabled: !!user,
});
// Create/Update announcement mutation
const createMutation = useMutation({
mutationFn: async (data: z.infer<typeof formSchema>) => {
const isEditing = !!editingAnnouncement;
console.log('📢 Submitting announcement:', { data, isEditing });
const url = isEditing
? `/api/announcements/${editingAnnouncement.id}`
: '/api/announcements';
const method = isEditing ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data),
});
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}`);
}
const result = await response.json();
console.log('📢 Announcement success:', result);
return result;
},
onSuccess: (data) => {
const isEditing = !!editingAnnouncement;
console.log('📢 Announcement success callback:', data);
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",
});
},
onError: (error) => {
const isEditing = !!editingAnnouncement;
console.error('📢 Announcement error:', error);
toast({
title: "Erreur",
description: error.message || `Impossible de ${isEditing ? 'modifier' : 'créer'} l'information`,
variant: "destructive",
});
},
});
// Delete announcement mutation
const deleteMutation = useMutation({
mutationFn: async (id: number) => {
const response = await fetch(`/api/announcements/${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to delete announcement');
}
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/announcements'] });
},
});
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);
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;
}
console.log('🔥 [FORM] Calling createMutation.mutate');
createMutation.mutate(data);
};
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;
}
};
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="flex items-center gap-2">
<Megaphone className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium">Informations</CardTitle>
</div>
<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>
)}
</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 className="space-y-3">
{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 bg-muted/30 transition-colors">
<div className="flex items-start gap-3 flex-1 min-w-0">
<TypeIcon className="h-4 w-4 text-muted-foreground mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between 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 flex-shrink-0">
{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}
{announcement.group && ` • ${announcement.group.name}`}
</span>
</div>
</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>
)}
</div>
);
})}
</div>
)}
</CardContent>
</Card>
);
}