mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-12 01:37:40 +02:00
Refactor announcement system to use in-memory storage
Move announcement data management from SQL to an in-memory solution with a limit of 5 entries. 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/aVFsaJG
This commit is contained in:
1 parent
48b784d0f1
commit
f2d056ee5d
3 files changed
+181
-78
No files matched your search
@@ -35,7 +35,7 @@ The platform is built with a clear separation of concerns, utilizing a modern fu
|
||||
- **Invoice Verification Cache:** Robust caching system for invoice verification results with automatic expiration and conflict resolution. Enhanced with unique constraint handling and graceful error management for production stability (August 2025). Fixed production SSL certificate errors and fetch timeout loops with enhanced error handling, network timeout protection, and error result caching to prevent infinite retry loops (August 2025).
|
||||
- **UI Scrollbar Optimization:** Eliminated redundant vertical scrollbar on the right side of the screen while preserving necessary internal page scrolling. Implemented global CSS rules with `overflow: hidden` on html/body/root and Layout height management to prevent vertical overflow issues in production (August 2025).
|
||||
- **Task Management:** Système complet de gestion des tâches avec fonctionnalités temporelles avancées - tri intelligent (pending > priorité > date), dates d'échéance avec sélecteur calendrier, indicateurs visuels pour tâches en retard/urgentes, filtres par échéance (aujourd'hui, cette semaine, en retard, sans échéance), vue Kanban et liste, badges colorés pour statut temporel. Interface optimisée pour suivi prioritaire des tâches non validées. Correction critique de l'isolation des données : chaque magasin ne voit maintenant que ses propres tâches (Août 2025). **PRODUCTION FIX (Août 2025)** : Résolution du problème de rechargements multiples nécessaires pour l'administrateur - le cache des tâches s'invalide maintenant automatiquement lors du changement de magasin sélectionné.
|
||||
- **Information System (Announcements):** Système d'informations complet remplaçant la carte commandes clients dans le dashboard - gestion d'annonces avec niveaux de priorité (normal, important, urgent), contrôle d'accès basé sur les rôles (admin : CRUD complet, autres rôles : lecture seule), tri intelligent par priorité et date, filtrage par magasin pour administrateurs, interface moderne avec badges colorés et indicateurs visuels. Intégration complète avec migrations automatiques et fichier init.sql complet pour nouvelles installations. **MIGRATION AUTOMATIQUE (Août 2025)** : Système de migration automatique intégré au Dockerfile avec scripts de déploiement pour créer automatiquement toutes les tables lors des mises à jour Docker.
|
||||
- **Information System (Announcements):** Système d'informations complet remplaçant la carte commandes clients dans le dashboard - gestion d'annonces avec niveaux de priorité (normal, important, urgent), contrôle d'accès basé sur les rôles (admin : CRUD complet, autres rôles : lecture seule), tri intelligent par priorité et date, filtrage par magasin pour administrateurs, interface moderne avec badges colorés et indicateurs visuels. **STOCKAGE MÉMOIRE (Août 2025)** : Migration du stockage SQL vers stockage en mémoire partagé avec limite de 5 annonces maximum pour éviter tout problème de base de données et simplifier l'architecture.
|
||||
- **DLC Module Permissions:** Correction des permissions de validation DLC pour permettre aux admin, directeur et manager de valider les produits DLC. Ajout de logs de debug pour traçabilité des validations (Août 2025).
|
||||
- **User and Permission Management:** Granular access controls ensuring that users (e.g., employees) have appropriate permissions for tasks like creating customer orders.
|
||||
- **Reporting:** Statistical reporting, including monthly summaries of deliveries based on `deliveredDate`.
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { Announcement, InsertAnnouncement, AnnouncementWithRelations, User, Group } from "@shared/schema";
|
||||
|
||||
// Stockage en mémoire partagé pour les annonces (maximum 5)
|
||||
class AnnouncementMemoryStorage {
|
||||
private announcements = new Map<number, Announcement>();
|
||||
private idCounter = 1;
|
||||
private readonly MAX_ANNOUNCEMENTS = 5;
|
||||
|
||||
// Référence vers les utilisateurs et groupes pour les relations
|
||||
private usersGetter: () => Promise<User[]>;
|
||||
private groupsGetter: () => Promise<Group[]>;
|
||||
|
||||
constructor(usersGetter: () => Promise<User[]>, groupsGetter: () => Promise<Group[]>) {
|
||||
this.usersGetter = usersGetter;
|
||||
this.groupsGetter = groupsGetter;
|
||||
this.initializeTestData();
|
||||
}
|
||||
|
||||
private initializeTestData() {
|
||||
// Créer quelques annonces de test
|
||||
const testAnnouncement: Announcement = {
|
||||
id: this.idCounter++,
|
||||
title: "Bienvenue dans le système d'informations",
|
||||
content: "Ce nouveau système remplace l'ancienne carte des commandes clients. Vous pouvez maintenant créer et gérer des annonces importantes.",
|
||||
priority: 'important',
|
||||
authorId: 'admin',
|
||||
groupId: 1,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.announcements.set(testAnnouncement.id, testAnnouncement);
|
||||
|
||||
const testAnnouncement2: Announcement = {
|
||||
id: this.idCounter++,
|
||||
title: "Maintenance programmée",
|
||||
content: "Une maintenance système aura lieu dimanche prochain de 2h à 4h du matin.",
|
||||
priority: 'normal',
|
||||
authorId: 'admin',
|
||||
groupId: null, // Annonce globale
|
||||
createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000), // Hier
|
||||
updatedAt: new Date(Date.now() - 24 * 60 * 60 * 1000),
|
||||
};
|
||||
this.announcements.set(testAnnouncement2.id, testAnnouncement2);
|
||||
}
|
||||
|
||||
async getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]> {
|
||||
const announcements = Array.from(this.announcements.values());
|
||||
|
||||
let filteredAnnouncements = announcements;
|
||||
|
||||
// Filtrer par groupIds si fourni (pour le filtrage admin par magasin)
|
||||
if (groupIds && groupIds.length > 0) {
|
||||
filteredAnnouncements = announcements.filter(announcement =>
|
||||
!announcement.groupId || groupIds.includes(announcement.groupId)
|
||||
);
|
||||
}
|
||||
|
||||
// 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]));
|
||||
|
||||
// Ajouter les relations
|
||||
const announcementsWithRelations = filteredAnnouncements.map(announcement => {
|
||||
const author = usersMap.get(announcement.authorId);
|
||||
const group = announcement.groupId ? groupsMap.get(announcement.groupId) : undefined;
|
||||
|
||||
return {
|
||||
...announcement,
|
||||
author: author!,
|
||||
group,
|
||||
};
|
||||
}).filter(a => a.author); // Filtrer celles sans auteur valide
|
||||
|
||||
// Trier par priorité (urgent > important > normal) puis par date (plus récent d'abord)
|
||||
return announcementsWithRelations.sort((a, b) => {
|
||||
const priorityOrder = { urgent: 3, important: 2, normal: 1 };
|
||||
const priorityDiff = (priorityOrder[b.priority as keyof typeof priorityOrder] || 1) -
|
||||
(priorityOrder[a.priority as keyof typeof priorityOrder] || 1);
|
||||
if (priorityDiff !== 0) return priorityDiff;
|
||||
return new Date(b.createdAt!).getTime() - new Date(a.createdAt!).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
async createAnnouncement(announcementData: InsertAnnouncement): Promise<Announcement> {
|
||||
// Vérifier la limite de 5 annonces maximum
|
||||
if (this.announcements.size >= this.MAX_ANNOUNCEMENTS) {
|
||||
// Supprimer l'annonce la plus ancienne (par ordre de création)
|
||||
const oldestAnnouncement = Array.from(this.announcements.values())
|
||||
.sort((a, b) => new Date(a.createdAt!).getTime() - new Date(b.createdAt!).getTime())[0];
|
||||
|
||||
if (oldestAnnouncement) {
|
||||
this.announcements.delete(oldestAnnouncement.id);
|
||||
console.log(`🗑️ Annonce supprimée automatiquement (limite de ${this.MAX_ANNOUNCEMENTS}): ${oldestAnnouncement.title}`);
|
||||
}
|
||||
}
|
||||
|
||||
const id = this.idCounter++;
|
||||
const announcement: Announcement = {
|
||||
id,
|
||||
...announcementData,
|
||||
groupId: announcementData.groupId || null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
this.announcements.set(id, announcement);
|
||||
console.log(`✅ Nouvelle annonce créée: ${announcement.title} (${this.announcements.size}/${this.MAX_ANNOUNCEMENTS})`);
|
||||
|
||||
return announcement;
|
||||
}
|
||||
|
||||
async deleteAnnouncement(id: number): Promise<boolean> {
|
||||
const existed = this.announcements.has(id);
|
||||
const deleted = this.announcements.delete(id);
|
||||
|
||||
if (deleted) {
|
||||
console.log(`🗑️ Annonce supprimée: ID ${id} (${this.announcements.size}/${this.MAX_ANNOUNCEMENTS} restantes)`);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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)');
|
||||
}
|
||||
return announcementStorage;
|
||||
}
|
||||
+30
-77
@@ -68,6 +68,7 @@ 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";
|
||||
|
||||
export interface IStorage {
|
||||
// User operations
|
||||
@@ -1591,51 +1592,27 @@ export class DatabaseStorage implements IStorage {
|
||||
|
||||
// Announcement operations
|
||||
async createAnnouncement(announcementData: InsertAnnouncement): Promise<Announcement> {
|
||||
const [announcement] = await db.insert(announcements).values(announcementData).returning();
|
||||
return announcement;
|
||||
const announcementStorage = getAnnouncementStorage(
|
||||
() => this.getUsers(),
|
||||
() => this.getGroups()
|
||||
);
|
||||
return await announcementStorage.createAnnouncement(announcementData);
|
||||
}
|
||||
|
||||
async getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]> {
|
||||
let query = db
|
||||
.select({
|
||||
announcement: announcements,
|
||||
author: users,
|
||||
group: groups,
|
||||
})
|
||||
.from(announcements)
|
||||
.leftJoin(users, eq(announcements.authorId, users.id))
|
||||
.leftJoin(groups, eq(announcements.groupId, groups.id));
|
||||
|
||||
// Filter by groupIds if provided (for admin filtering by store)
|
||||
if (groupIds && groupIds.length > 0) {
|
||||
query = query.where(
|
||||
or(
|
||||
isNull(announcements.groupId), // Global announcements (no group)
|
||||
inArray(announcements.groupId, groupIds)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const results = await query.orderBy(
|
||||
// Sort by priority (urgent > important > normal) then by date (newest first)
|
||||
sql`CASE
|
||||
WHEN ${announcements.priority} = 'urgent' THEN 3
|
||||
WHEN ${announcements.priority} = 'important' THEN 2
|
||||
ELSE 1
|
||||
END DESC`,
|
||||
desc(announcements.createdAt)
|
||||
const announcementStorage = getAnnouncementStorage(
|
||||
() => this.getUsers(),
|
||||
() => this.getGroups()
|
||||
);
|
||||
|
||||
return results.map(result => ({
|
||||
...result.announcement,
|
||||
author: result.author!,
|
||||
group: result.group,
|
||||
}));
|
||||
return await announcementStorage.getAnnouncements(groupIds);
|
||||
}
|
||||
|
||||
async deleteAnnouncement(id: number): Promise<boolean> {
|
||||
const result = await db.delete(announcements).where(eq(announcements.id, id));
|
||||
return result.rowCount ? result.rowCount > 0 : false;
|
||||
const announcementStorage = getAnnouncementStorage(
|
||||
() => this.getUsers(),
|
||||
() => this.getGroups()
|
||||
);
|
||||
return await announcementStorage.deleteAnnouncement(id);
|
||||
}
|
||||
|
||||
// SAV operations
|
||||
@@ -3335,51 +3312,27 @@ export class MemStorage implements IStorage {
|
||||
|
||||
// Announcement operations
|
||||
async createAnnouncement(announcementData: InsertAnnouncement): Promise<Announcement> {
|
||||
const id = this.idCounters.announcement++;
|
||||
const announcement: Announcement = {
|
||||
id,
|
||||
...announcementData,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.announcements.set(id, announcement);
|
||||
return announcement;
|
||||
const announcementStorage = getAnnouncementStorage(
|
||||
async () => Array.from(this.users.values()),
|
||||
async () => Array.from(this.groups.values())
|
||||
);
|
||||
return await announcementStorage.createAnnouncement(announcementData);
|
||||
}
|
||||
|
||||
async getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]> {
|
||||
const announcements = Array.from(this.announcements.values());
|
||||
|
||||
let filteredAnnouncements = announcements;
|
||||
|
||||
// Filter by groupIds if provided (for admin filtering by store)
|
||||
if (groupIds && groupIds.length > 0) {
|
||||
filteredAnnouncements = announcements.filter(announcement =>
|
||||
!announcement.groupId || groupIds.includes(announcement.groupId)
|
||||
);
|
||||
}
|
||||
|
||||
// Add relations
|
||||
return filteredAnnouncements.map(announcement => {
|
||||
const author = this.users.get(announcement.authorId);
|
||||
const group = announcement.groupId ? this.groups.get(announcement.groupId) : undefined;
|
||||
|
||||
return {
|
||||
...announcement,
|
||||
author: author!,
|
||||
group,
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
// Sort by priority (urgent > important > normal) then by date (newest first)
|
||||
const priorityOrder = { urgent: 3, important: 2, normal: 1 };
|
||||
const priorityDiff = (priorityOrder[b.priority as keyof typeof priorityOrder] || 1) -
|
||||
(priorityOrder[a.priority as keyof typeof priorityOrder] || 1);
|
||||
if (priorityDiff !== 0) return priorityDiff;
|
||||
return new Date(b.createdAt!).getTime() - new Date(a.createdAt!).getTime();
|
||||
});
|
||||
const announcementStorage = getAnnouncementStorage(
|
||||
async () => Array.from(this.users.values()),
|
||||
async () => Array.from(this.groups.values())
|
||||
);
|
||||
return await announcementStorage.getAnnouncements(groupIds);
|
||||
}
|
||||
|
||||
async deleteAnnouncement(id: number): Promise<boolean> {
|
||||
return this.announcements.delete(id);
|
||||
const announcementStorage = getAnnouncementStorage(
|
||||
async () => Array.from(this.users.values()),
|
||||
async () => Array.from(this.groups.values())
|
||||
);
|
||||
return await announcementStorage.deleteAnnouncement(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user