Merge pull request #184 from R0m1k3/1.1

1.1
This commit is contained in:
LogiFlow authored and GitHub committed 2025-08-16 15:38:01 +02:00
commit 4d96c1cad2
7 files changed
+255 -67

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>
); );
+25
View File
@@ -0,0 +1,25 @@
-- Script d'initialisation PostgreSQL pour les annonces
CREATE TABLE IF NOT EXISTS announcements (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
priority VARCHAR(20) DEFAULT 'normal' CHECK (priority IN ('normal', 'important', 'urgent')),
author_id VARCHAR(255) NOT NULL,
group_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Index pour améliorer les performances
CREATE INDEX IF NOT EXISTS idx_announcements_priority ON announcements(priority);
CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at);
CREATE INDEX IF NOT EXISTS idx_announcements_group_id ON announcements(group_id);
-- Insertion de données de test
INSERT INTO announcements (title, content, priority, author_id, group_id) VALUES
('Bienvenue dans le système d''informations', 'Ce nouveau système remplace l''ancienne carte des commandes clients. Vous pouvez maintenant créer et gérer des annonces importantes.', 'important', 'admin', 1),
('Maintenance programmée', 'Une maintenance système aura lieu dimanche prochain de 2h à 4h du matin.', 'normal', 'admin', NULL)
ON CONFLICT DO NOTHING;
-- Afficher le résultat
SELECT COUNT(*) as total_announcements FROM announcements;
+65 -19
View File
@@ -103,6 +103,7 @@ class AnnouncementMemoryStorage {
const announcement: Announcement = { const announcement: Announcement = {
id, id,
...announcementData, ...announcementData,
priority: announcementData.priority || 'normal',
groupId: announcementData.groupId || null, groupId: announcementData.groupId || null,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
@@ -114,6 +115,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);
@@ -124,27 +179,18 @@ class AnnouncementMemoryStorage {
return deleted; return deleted;
} }
// Méthode utilitaire pour obtenir le nombre d'annonces
getCount(): number {
return this.announcements.size;
}
// Méthode utilitaire pour nettoyer toutes les annonces (si nécessaire)
clear(): void {
this.announcements.clear();
this.idCounter = 1;
console.log('🧹 Toutes les annonces ont été supprimées');
}
} }
// Instance singleton pour partager entre production et développement // Instance globale partagée
let announcementStorage: AnnouncementMemoryStorage | null = null; let globalAnnouncementStorage: AnnouncementMemoryStorage | null = null;
export function getAnnouncementStorage(usersGetter: () => Promise<User[]>, groupsGetter: () => Promise<Group[]>): AnnouncementMemoryStorage { export function getAnnouncementStorage(
if (!announcementStorage) { usersGetter: () => Promise<User[]>,
announcementStorage = new AnnouncementMemoryStorage(usersGetter, groupsGetter); groupsGetter: () => Promise<Group[]>
console.log('📢 Système d\'annonces en mémoire initialisé (max 5 annonces)'); ): AnnouncementMemoryStorage {
if (!globalAnnouncementStorage) {
globalAnnouncementStorage = new AnnouncementMemoryStorage(usersGetter, groupsGetter);
console.log('📢 Stockage mémoire des annonces initialisé');
} }
return announcementStorage; return globalAnnouncementStorage;
} }
+10 -20
View File
@@ -14,15 +14,15 @@ console.log('🔗 Database initialization:', {
let db: any; let db: any;
let pool: any; let pool: any;
if (isProduction && dbUrl && (dbUrl.includes('logiflow-db') || dbUrl.includes('localhost') || dbUrl.includes('127.0.0.1'))) { if (dbUrl && (isProduction || process.env.FORCE_POSTGRES === 'true')) {
// Standard PostgreSQL for production Docker // Use PostgreSQL for production or when forced
console.log('🐳 PRODUCTION: Using standard PostgreSQL'); console.log('🐘 Using PostgreSQL database');
const { Pool } = await import('pg'); const { Pool } = await import('pg');
const { drizzle } = await import('drizzle-orm/node-postgres'); const { drizzle } = await import('drizzle-orm/node-postgres');
pool = new Pool({ pool = new Pool({
connectionString: dbUrl, connectionString: dbUrl,
ssl: false, ssl: dbUrl.includes('localhost') ? false : { rejectUnauthorized: false },
max: 10, max: 10,
idleTimeoutMillis: 30000, idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000, connectionTimeoutMillis: 5000,
@@ -38,25 +38,15 @@ if (isProduction && dbUrl && (dbUrl.includes('logiflow-db') || dbUrl.includes('l
}); });
} catch (error) { } catch (error) {
console.error('❌ PostgreSQL connection test failed:', error); console.error('❌ PostgreSQL connection test failed:', error);
} // Continue anyway, will use MemStorage as fallback
} else {
// Fallback: use Neon for development (but will be overridden by MemStorage)
console.log('🔧 DEV: Using Neon (fallback, will use MemStorage)');
const { Pool, neonConfig } = await import('@neondatabase/serverless');
const { drizzle } = await import('drizzle-orm/neon-serverless');
const ws = await import('ws');
neonConfig.webSocketConstructor = ws.default;
if (dbUrl) {
pool = new Pool({ connectionString: dbUrl });
db = drizzle({ client: pool, schema });
} else {
// Create dummy instances for development
pool = null; pool = null;
db = null; db = null;
console.log('🔧 DEV: No DATABASE_URL, using MemStorage');
} }
} else {
// Development fallback: use MemStorage
console.log('🔧 DEV: Using MemStorage (development fallback)');
pool = null;
db = null;
} }
export { db, pool }; export { db, pool };
+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;
+20 -1
View File
@@ -68,7 +68,8 @@ 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"; import { getAnnouncementStorage } from "./announcementStorage";
export interface IStorage { export interface IStorage {
// User operations // User operations
@@ -179,6 +180,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 +1610,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(),