Add ability for administrators to edit existing announcements

Implement update functionality for announcements via a PUT API endpoint, including necessary backend storage and client-side UI adjustments.

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/HjMubBV
This commit is contained in:
michaelschal committed 2025-08-16 13:21:24 +00:00
1 parent ad396339f0
commit a3889dd088
5 files changed
+208 -28

No files matched your search

+3
View File
@@ -35,3 +35,6 @@ author = "agent"
task = "shell.exec" task = "shell.exec"
args = "npm run dev" args = "npm run dev"
waitForPort = 5000 waitForPort = 5000
[agent]
integrations = ["javascript_database==1.0.0"]
+80 -27
View File
@@ -4,8 +4,9 @@ import { useStore } from "@/components/Layout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Megaphone, Plus, X, AlertTriangle, Clock, Circle } from "lucide-react"; import { Megaphone, Plus, X, AlertTriangle, Clock, Circle, Edit2 } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import React from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -32,6 +33,7 @@ export default function AnnouncementCard() {
const { selectedStoreId } = useStore(); const { selectedStoreId } = useStore();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [editingAnnouncement, setEditingAnnouncement] = useState<AnnouncementWithRelations | null>(null);
const { toast } = useToast(); const { toast } = useToast();
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@@ -44,6 +46,25 @@ export default function AnnouncementCard() {
}, },
}); });
// Reset form when editing announcement changes
React.useEffect(() => {
if (editingAnnouncement) {
form.reset({
title: editingAnnouncement.title,
content: editingAnnouncement.content,
priority: editingAnnouncement.priority as "normal" | "important" | "urgent",
groupId: editingAnnouncement.groupId || undefined,
});
} else {
form.reset({
title: "",
content: "",
priority: "normal",
groupId: undefined,
});
}
}, [editingAnnouncement, form]);
// Fetch announcements // Fetch announcements
const { data: announcements = [], isLoading } = useQuery<AnnouncementWithRelations[]>({ const { data: announcements = [], isLoading } = useQuery<AnnouncementWithRelations[]>({
queryKey: ['/api/announcements', selectedStoreId], queryKey: ['/api/announcements', selectedStoreId],
@@ -72,19 +93,26 @@ export default function AnnouncementCard() {
enabled: user?.role === 'admin', enabled: user?.role === 'admin',
}); });
// Create announcement mutation // Create/Update announcement mutation
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: async (data: z.infer<typeof formSchema>) => { mutationFn: async (data: z.infer<typeof formSchema>) => {
console.log('📢 Creating announcement with data:', data); const isEditing = !!editingAnnouncement;
console.log('📢 Submitting announcement:', { data, isEditing });
const response = await fetch('/api/announcements', { const url = isEditing
method: 'POST', ? `/api/announcements/${editingAnnouncement.id}`
: '/api/announcements';
const method = isEditing ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include', credentials: 'include',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
console.log('📢 Create announcement response:', { console.log('📢 Announcement response:', {
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
ok: response.ok ok: response.ok
@@ -92,31 +120,32 @@ export default function AnnouncementCard() {
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('📢 Create announcement failed:', errorText); console.error('📢 Announcement failed:', errorText);
throw new Error(`Failed to create announcement: ${response.status} - ${errorText}`); throw new Error(`Failed to ${isEditing ? 'update' : 'create'} announcement: ${response.status} - ${errorText}`);
} }
const result = await response.json(); const result = await response.json();
console.log('📢 Announcement created successfully:', result); console.log('📢 Announcement success:', result);
return result; return result;
}, },
onSuccess: (data) => { onSuccess: (data) => {
console.log('📢 Announcement creation success callback:', data); 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);
form.reset(); form.reset();
// Add success toast
toast({ toast({
title: "Succès", title: "Succès",
description: "Annonce créée avec succès", description: isEditing ? "Information modifiée avec succès" : "Information créée avec succès",
}); });
}, },
onError: (error) => { onError: (error) => {
console.error('📢 Announcement creation error:', error); const isEditing = !!editingAnnouncement;
// Add error toast console.error('📢 Announcement error:', error);
toast({ toast({
title: "Erreur", title: "Erreur",
description: error.message || "Impossible de créer l'annonce", description: error.message || `Impossible de ${isEditing ? 'modifier' : 'créer'} l'information`,
variant: "destructive", variant: "destructive",
}); });
}, },
@@ -229,7 +258,13 @@ export default function AnnouncementCard() {
<CardTitle className="text-sm font-medium">Informations</CardTitle> <CardTitle className="text-sm font-medium">Informations</CardTitle>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{user?.role === 'admin' && ( {user?.role === 'admin' && (
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}> <Dialog open={isCreateDialogOpen} onOpenChange={(open) => {
setIsCreateDialogOpen(open);
if (!open) {
setEditingAnnouncement(null);
form.reset();
}
}}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm" variant="ghost" className="h-8 w-8 p-0"> <Button size="sm" variant="ghost" className="h-8 w-8 p-0">
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
@@ -237,7 +272,9 @@ export default function AnnouncementCard() {
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[500px]"> <DialogContent className="sm:max-w-[500px]">
<DialogHeader> <DialogHeader>
<DialogTitle>Nouvelle information</DialogTitle> <DialogTitle>
{editingAnnouncement ? 'Modifier l\'information' : 'Nouvelle information'}
</DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
@@ -331,7 +368,10 @@ export default function AnnouncementCard() {
disabled={createMutation.isPending} disabled={createMutation.isPending}
onClick={() => console.log('🔥 [BUTTON] Submit button clicked')} onClick={() => console.log('🔥 [BUTTON] Submit button clicked')}
> >
{createMutation.isPending ? 'Création...' : 'Créer'} {createMutation.isPending
? (editingAnnouncement ? 'Modification...' : 'Création...')
: (editingAnnouncement ? 'Modifier' : 'Créer')
}
</Button> </Button>
</div> </div>
</form> </form>
@@ -386,15 +426,28 @@ export default function AnnouncementCard() {
</div> </div>
</div> </div>
{user?.role === 'admin' && ( {user?.role === 'admin' && (
<Button <div className="flex items-center gap-1 ml-2 flex-shrink-0">
variant="ghost" <Button
size="sm" variant="ghost"
className="h-6 w-6 p-0 ml-2 flex-shrink-0" size="sm"
onClick={() => deleteMutation.mutate(announcement.id)} className="h-6 w-6 p-0 text-muted-foreground hover:text-blue-600"
disabled={deleteMutation.isPending} onClick={() => {
> setEditingAnnouncement(announcement);
<X className="h-3 w-3" /> setIsCreateDialogOpen(true);
</Button> }}
>
<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>
); );
+54
View File
@@ -114,6 +114,60 @@ class AnnouncementMemoryStorage {
return announcement; return announcement;
} }
async getAnnouncement(id: number): Promise<AnnouncementWithRelations | undefined> {
const announcement = this.announcements.get(id);
if (!announcement) {
return undefined;
}
// Récupérer les utilisateurs et groupes pour les relations
const [users, groups] = await Promise.all([
this.usersGetter(),
this.groupsGetter()
]);
const usersMap = new Map(users.map(u => [u.id, u]));
const groupsMap = new Map(groups.map(g => [g.id, g]));
const author = usersMap.get(announcement.authorId);
const group = announcement.groupId ? groupsMap.get(announcement.groupId) : undefined;
if (!author) {
return undefined;
}
return {
...announcement,
author,
group,
};
}
async updateAnnouncement(id: number, announcementData: Partial<InsertAnnouncement>): Promise<AnnouncementWithRelations> {
const existingAnnouncement = this.announcements.get(id);
if (!existingAnnouncement) {
throw new Error(`Announcement with ID ${id} not found`);
}
const updatedAnnouncement: Announcement = {
...existingAnnouncement,
...announcementData,
id, // Ensure ID doesn't change
updatedAt: new Date(),
};
this.announcements.set(id, updatedAnnouncement);
console.log(`✏️ Annonce mise à jour: ${updatedAnnouncement.title} (ID: ${id})`);
// Return the announcement with relations
const announcementWithRelations = await this.getAnnouncement(id);
if (!announcementWithRelations) {
throw new Error(`Failed to get updated announcement with relations`);
}
return announcementWithRelations;
}
async deleteAnnouncement(id: number): Promise<boolean> { async deleteAnnouncement(id: number): Promise<boolean> {
const existed = this.announcements.has(id); const existed = this.announcements.has(id);
const deleted = this.announcements.delete(id); const deleted = this.announcements.delete(id);
+52
View File
@@ -3078,6 +3078,58 @@ RÉSUMÉ DU SCAN
} }
}); });
// PUT /api/announcements/:id - Update announcement (admin only)
app.put('/api/announcements/:id', isAuthenticated, async (req: any, res) => {
console.log('📝 [SERVER] PUT /api/announcements/:id endpoint hit');
console.log('📝 [SERVER] Request body:', JSON.stringify(req.body, null, 2));
try {
const userId = req.user.claims ? req.user.claims.sub : req.user.id;
console.log('📝 [SERVER] Extracted userId:', userId);
const user = await storage.getUserWithGroups(userId);
if (!user) {
console.error('📝 [SERVER] User not found for ID:', userId);
return res.status(404).json({ message: "User not found" });
}
console.log('📝 [SERVER] User found:', { username: user.username, role: user.role, id: user.id });
// Only admin can edit announcements
if (user.role !== 'admin') {
console.error('📝 [SERVER] Access denied - user role:', user.role);
return res.status(403).json({ message: "Only administrators can edit announcements" });
}
const id = parseInt(req.params.id);
console.log('📝 [SERVER] Announcement ID:', id);
// Verify announcement exists
const existingAnnouncement = await storage.getAnnouncement(id);
if (!existingAnnouncement) {
console.error('📝 [SERVER] Announcement not found:', id);
return res.status(404).json({ message: "Announcement not found" });
}
console.log('📝 [SERVER] User is admin, proceeding with validation');
const announcementData = insertAnnouncementSchema.partial().parse(req.body);
console.log('📝 [SERVER] Announcement data validated:', announcementData);
const updatedAnnouncement = await storage.updateAnnouncement(id, announcementData);
console.log('📝 [SERVER] Announcement updated successfully:', updatedAnnouncement);
res.json(updatedAnnouncement);
} catch (error) {
console.error('📝 [SERVER] Error updating announcement:', error);
if (error instanceof z.ZodError) {
console.error('📝 [SERVER] Validation errors:', error.errors);
return res.status(400).json({ message: "Validation error", errors: error.errors });
}
res.status(500).json({ message: "Failed to update announcement" });
}
});
app.delete('/api/announcements/:id', isAuthenticated, async (req: any, res) => { app.delete('/api/announcements/:id', isAuthenticated, async (req: any, res) => {
try { try {
const userId = req.user.claims ? req.user.claims.sub : req.user.id; const userId = req.user.claims ? req.user.claims.sub : req.user.id;
+19 -1
View File
@@ -68,7 +68,7 @@ import {
} from "@shared/schema"; } from "@shared/schema";
import { db } from "./db"; import { db } from "./db";
import { eq, and, inArray, desc, sql, gte, lte, lt, or, isNull, isNotNull, asc } from "drizzle-orm"; import { eq, and, inArray, desc, sql, gte, lte, lt, or, isNull, isNotNull, asc } from "drizzle-orm";
import { getAnnouncementStorage } from "./announcementStorage.js";
export interface IStorage { export interface IStorage {
// User operations // User operations
@@ -179,6 +179,8 @@ export interface IStorage {
// Announcement operations // Announcement operations
createAnnouncement(announcement: InsertAnnouncement): Promise<Announcement>; createAnnouncement(announcement: InsertAnnouncement): Promise<Announcement>;
getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]>; getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]>;
getAnnouncement(id: number): Promise<AnnouncementWithRelations | undefined>;
updateAnnouncement(id: number, announcement: Partial<InsertAnnouncement>): Promise<AnnouncementWithRelations>;
deleteAnnouncement(id: number): Promise<boolean>; deleteAnnouncement(id: number): Promise<boolean>;
// SAV operations // SAV operations
@@ -1607,6 +1609,22 @@ export class DatabaseStorage implements IStorage {
return await announcementStorage.getAnnouncements(groupIds); return await announcementStorage.getAnnouncements(groupIds);
} }
async getAnnouncement(id: number): Promise<AnnouncementWithRelations | undefined> {
const announcementStorage = getAnnouncementStorage(
() => this.getUsers(),
() => this.getGroups()
);
return await announcementStorage.getAnnouncement(id);
}
async updateAnnouncement(id: number, announcementData: Partial<InsertAnnouncement>): Promise<AnnouncementWithRelations> {
const announcementStorage = getAnnouncementStorage(
() => this.getUsers(),
() => this.getGroups()
);
return await announcementStorage.updateAnnouncement(id, announcementData);
}
async deleteAnnouncement(id: number): Promise<boolean> { async deleteAnnouncement(id: number): Promise<boolean> {
const announcementStorage = getAnnouncementStorage( const announcementStorage = getAnnouncementStorage(
() => this.getUsers(), () => this.getUsers(),