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"
args = "npm run dev"
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 { Badge } from "@/components/ui/badge";
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 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";
@@ -32,6 +33,7 @@ export default function AnnouncementCard() {
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>>({
@@ -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
const { data: announcements = [], isLoading } = useQuery<AnnouncementWithRelations[]>({
queryKey: ['/api/announcements', selectedStoreId],
@@ -72,19 +93,26 @@ export default function AnnouncementCard() {
enabled: user?.role === 'admin',
});
// Create announcement mutation
// Create/Update announcement mutation
const createMutation = useMutation({
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', {
method: 'POST',
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('📢 Create announcement response:', {
console.log('📢 Announcement response:', {
status: response.status,
statusText: response.statusText,
ok: response.ok
@@ -92,31 +120,32 @@ export default function AnnouncementCard() {
if (!response.ok) {
const errorText = await response.text();
console.error('📢 Create announcement failed:', errorText);
throw new Error(`Failed to create announcement: ${response.status} - ${errorText}`);
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 created successfully:', result);
console.log('📢 Announcement success:', result);
return result;
},
onSuccess: (data) => {
console.log('📢 Announcement creation success callback:', data);
const isEditing = !!editingAnnouncement;
console.log('📢 Announcement success callback:', data);
queryClient.invalidateQueries({ queryKey: ['/api/announcements'] });
setIsCreateDialogOpen(false);
setEditingAnnouncement(null);
form.reset();
// Add success toast
toast({
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) => {
console.error('📢 Announcement creation error:', error);
// Add error toast
const isEditing = !!editingAnnouncement;
console.error('📢 Announcement error:', error);
toast({
title: "Erreur",
description: error.message || "Impossible de créer l'annonce",
description: error.message || `Impossible de ${isEditing ? 'modifier' : 'créer'} l'information`,
variant: "destructive",
});
},
@@ -229,7 +258,13 @@ export default function AnnouncementCard() {
<CardTitle className="text-sm font-medium">Informations</CardTitle>
<div className="flex items-center gap-2">
{user?.role === 'admin' && (
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<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" />
@@ -237,7 +272,9 @@ export default function AnnouncementCard() {
</DialogTrigger>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Nouvelle information</DialogTitle>
<DialogTitle>
{editingAnnouncement ? 'Modifier l\'information' : 'Nouvelle information'}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
@@ -331,7 +368,10 @@ export default function AnnouncementCard() {
disabled={createMutation.isPending}
onClick={() => console.log('🔥 [BUTTON] Submit button clicked')}
>
{createMutation.isPending ? 'Création...' : 'Créer'}
{createMutation.isPending
? (editingAnnouncement ? 'Modification...' : 'Création...')
: (editingAnnouncement ? 'Modifier' : 'Créer')
}
</Button>
</div>
</form>
@@ -386,15 +426,28 @@ export default function AnnouncementCard() {
</div>
</div>
{user?.role === 'admin' && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 ml-2 flex-shrink-0"
onClick={() => deleteMutation.mutate(announcement.id)}
disabled={deleteMutation.isPending}
>
<X className="h-3 w-3" />
</Button>
<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>
);
+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 = {
id,
...announcementData,
priority: announcementData.priority || 'normal',
groupId: announcementData.groupId || null,
createdAt: new Date(),
updatedAt: new Date(),
@@ -114,6 +115,60 @@ class AnnouncementMemoryStorage {
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> {
const existed = this.announcements.has(id);
const deleted = this.announcements.delete(id);
@@ -124,27 +179,18 @@ class AnnouncementMemoryStorage {
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
let announcementStorage: AnnouncementMemoryStorage | null = null;
// Instance globale partagée
let globalAnnouncementStorage: AnnouncementMemoryStorage | null = null;
export function getAnnouncementStorage(usersGetter: () => Promise<User[]>, groupsGetter: () => Promise<Group[]>): AnnouncementMemoryStorage {
if (!announcementStorage) {
announcementStorage = new AnnouncementMemoryStorage(usersGetter, groupsGetter);
console.log('📢 Système d\'annonces en mémoire initialisé (max 5 annonces)');
export function getAnnouncementStorage(
usersGetter: () => Promise<User[]>,
groupsGetter: () => Promise<Group[]>
): 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 pool: any;
if (isProduction && dbUrl && (dbUrl.includes('logiflow-db') || dbUrl.includes('localhost') || dbUrl.includes('127.0.0.1'))) {
// Standard PostgreSQL for production Docker
console.log('🐳 PRODUCTION: Using standard PostgreSQL');
if (dbUrl && (isProduction || process.env.FORCE_POSTGRES === 'true')) {
// Use PostgreSQL for production or when forced
console.log('🐘 Using PostgreSQL database');
const { Pool } = await import('pg');
const { drizzle } = await import('drizzle-orm/node-postgres');
pool = new Pool({
connectionString: dbUrl,
ssl: false,
ssl: dbUrl.includes('localhost') ? false : { rejectUnauthorized: false },
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
@@ -38,25 +38,15 @@ if (isProduction && dbUrl && (dbUrl.includes('logiflow-db') || dbUrl.includes('l
});
} catch (error) {
console.error('❌ PostgreSQL connection test failed:', error);
}
} 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
// Continue anyway, will use MemStorage as fallback
pool = 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 };
+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) => {
try {
const userId = req.user.claims ? req.user.claims.sub : req.user.id;
+20 -1
View File
@@ -68,7 +68,8 @@ import {
} from "@shared/schema";
import { db } from "./db";
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 {
// User operations
@@ -179,6 +180,8 @@ export interface IStorage {
// Announcement operations
createAnnouncement(announcement: InsertAnnouncement): Promise<Announcement>;
getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]>;
getAnnouncement(id: number): Promise<AnnouncementWithRelations | undefined>;
updateAnnouncement(id: number, announcement: Partial<InsertAnnouncement>): Promise<AnnouncementWithRelations>;
deleteAnnouncement(id: number): Promise<boolean>;
// SAV operations
@@ -1607,6 +1610,22 @@ export class DatabaseStorage implements IStorage {
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> {
const announcementStorage = getAnnouncementStorage(
() => this.getUsers(),