diff --git a/client/src/components/Administration.tsx b/client/src/components/Administration.tsx index aad68a7..a38f900 100644 --- a/client/src/components/Administration.tsx +++ b/client/src/components/Administration.tsx @@ -81,6 +81,15 @@ export default function Administration() { refetchInterval: 30000, // Refresh every 30 seconds }); + const { data: purgeStats } = useQuery({ + queryKey: ['/api/admin/purge/stats'], + queryFn: async () => { + const response = await apiRequest('/api/admin/purge/stats'); + return response.json(); + }, + refetchInterval: 30000, // Refresh every 30 seconds + }); + // User mutations const createUserMutation = useMutation({ mutationFn: async (userData: CreateUserData) => { @@ -386,6 +395,34 @@ export default function Administration() { input.click(); }; + const executePurgeMutation = useMutation({ + mutationFn: async () => { + const response = await apiRequest('/api/admin/purge/execute', { + method: 'POST', + }); + return response.json(); + }, + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['/api/admin/purge/stats'] }); + queryClient.invalidateQueries({ queryKey: ['/api/sales'] }); + toast({ + title: "Purge exécutée avec succès", + description: result.message || `${result.deletedCount} vente(s) supprimée(s)` + }); + }, + onError: (error: any) => { + toast({ + title: "Erreur de purge", + description: error.message || "Impossible d'exécuter la purge", + variant: "destructive", + }); + }, + }); + + const handleExecutePurge = () => { + executePurgeMutation.mutate(); + }; + const getRoleBadge = (role: string) => { const variants = { administrator: "destructive" as const, @@ -418,6 +455,7 @@ export default function Administration() { Utilisateurs Magasins Sauvegarde + Purge {/* Users Tab */} @@ -1238,6 +1276,134 @@ export default function Administration() { + + {/* Data Purge Tab */} + +
+

+ + Purge des Données +

+
+ +
+ + + + + Statistiques de Purge + + + +
+ Total des ventes : + {purgeStats?.totalSales || 0} +
+
+ Ventes anciennes (>19 mois) : + {purgeStats?.oldSales || 0} +
+
+ Date limite : + + {purgeStats?.cutoffDate ? new Date(purgeStats.cutoffDate).toLocaleDateString('fr-FR') : '-'} + +
+
+
+
+ + {purgeStats?.purgeEligible ? 'Purge nécessaire' : 'Aucune purge nécessaire'} + +
+
+
+
+ + + + + + Configuration + + + +
+

Purge Automatique

+

+ Exécutée automatiquement le 1er de chaque mois à 02:00 +

+
+
+

Rétention des Données

+

+ 19 mois maximum (conforme à la réglementation) +

+
+
+ + + + + + + Confirmer la Purge + + Cette action va supprimer définitivement {purgeStats?.oldSales || 0} vente(s) + datant de plus de 19 mois. Cette opération est irréversible. +

+ Ventes concernées : antérieures au {purgeStats?.cutoffDate ? new Date(purgeStats.cutoffDate).toLocaleDateString('fr-FR') : '-'} +
+
+ + Annuler + + Confirmer la Purge + + +
+
+
+
+
+
+ + + + Information sur la Purge + + +
+

📋 Réglementation

+

+ Les données de ventes de feux d'artifice doivent être conservées pendant 19 mois maximum + conformément à la réglementation française sur les articles pyrotechniques. +

+
+
+

🔄 Automatisation

+

+ La purge s'exécute automatiquement chaque mois pour maintenir la conformité. + Vous pouvez également l'exécuter manuellement si nécessaire. +

+
+
+

⚠️ Attention

+

+ La purge supprime définitivement les données. Assurez-vous d'avoir effectué + une sauvegarde récente avant d'exécuter une purge manuelle. +

+
+
+
+
diff --git a/replit.md b/replit.md index c098e03..3389b57 100644 --- a/replit.md +++ b/replit.md @@ -41,6 +41,8 @@ Preferred communication style: Simple, everyday language. 5. **Sales History**: Complete transaction history with date filtering and search 6. **Data Export**: CSV export functionality for regulatory reporting 7. **Role-Based Permissions**: Different access levels for Admin, Manager, and Employee roles +8. **Automated Data Purge**: Regulatory compliance with automatic deletion of sales data older than 19 months +9. **Backup & Recovery**: Complete database backup system with import/export capabilities ### Authentication & Authorization - **Administrators**: Full access to all features including user/store management @@ -180,4 +182,13 @@ Preferred communication style: Simple, everyday language. - ✅ Enhanced login page to hide default admin credentials (admin/admin123) once password is changed - ✅ Added dynamic credential visibility based on whether default password is still in use - ✅ Security warning displayed when default credentials are shown -- ✅ API endpoint to check default credential status with password hash comparison \ No newline at end of file +- ✅ API endpoint to check default credential status with password hash comparison + +### Automatic Data Purge System (January 9, 2025) +- ✅ Implemented automatic purge system for sales data older than 19 months +- ✅ Scheduled purge execution on the 1st of each month at 02:00 (Europe/Paris timezone) +- ✅ Manual purge execution available in administration panel +- ✅ Comprehensive statistics dashboard showing purge-eligible records +- ✅ Regulatory compliance with French fireworks sales data retention requirements +- ✅ Safety confirmation dialogs for manual purge operations +- ✅ Real-time monitoring of data retention status with visual indicators \ No newline at end of file diff --git a/server/data-purge.ts b/server/data-purge.ts new file mode 100644 index 0000000..6763913 --- /dev/null +++ b/server/data-purge.ts @@ -0,0 +1,112 @@ +import { db } from './db'; +import { sales } from '@shared/schema'; +import { lt } from 'drizzle-orm'; +import cron from 'node-cron'; + +/** + * Purge des ventes de plus de 19 mois + * Conformément aux exigences réglementaires de conservation des données + */ +export async function purgeSalesData() { + try { + // Calculer la date limite (19 mois en arrière) + const cutoffDate = new Date(); + cutoffDate.setMonth(cutoffDate.getMonth() - 19); + + console.log(`🗑️ Démarrage purge des ventes antérieures au ${cutoffDate.toLocaleDateString('fr-FR')}`); + + // Supprimer les ventes antérieures à la date limite + const result = await db + .delete(sales) + .where(lt(sales.timestamp, cutoffDate.toISOString())) + .returning({ id: sales.id }); + + const deletedCount = result.length; + + if (deletedCount > 0) { + console.log(`✅ Purge terminée : ${deletedCount} vente(s) supprimée(s)`); + } else { + console.log('✅ Purge terminée : aucune vente à supprimer'); + } + + return { + success: true, + deletedCount, + cutoffDate: cutoffDate.toISOString(), + message: `${deletedCount} vente(s) supprimée(s) (antérieures au ${cutoffDate.toLocaleDateString('fr-FR')})` + }; + + } catch (error) { + console.error('❌ Erreur lors de la purge des ventes:', error); + return { + success: false, + error: error.message || 'Erreur inconnue', + deletedCount: 0 + }; + } +} + +/** + * Obtenir les statistiques de purge + */ +export async function getPurgeStats() { + try { + const cutoffDate = new Date(); + cutoffDate.setMonth(cutoffDate.getMonth() - 19); + + // Compter les ventes qui seraient supprimées + const oldSales = await db + .select({ id: sales.id }) + .from(sales) + .where(lt(sales.timestamp, cutoffDate.toISOString())); + + // Compter le total des ventes + const totalSales = await db + .select({ id: sales.id }) + .from(sales); + + return { + totalSales: totalSales.length, + oldSales: oldSales.length, + cutoffDate: cutoffDate.toISOString(), + purgeEligible: oldSales.length > 0 + }; + + } catch (error) { + console.error('❌ Erreur lors du calcul des statistiques de purge:', error); + return { + totalSales: 0, + oldSales: 0, + cutoffDate: new Date().toISOString(), + purgeEligible: false, + error: error.message + }; + } +} + +/** + * Démarrer le planificateur de purge automatique + * Exécute la purge tous les 1er du mois à 02:00 + */ +export function startPurgeScheduler() { + // Tâche planifiée : tous les 1er du mois à 02:00 + cron.schedule('0 2 1 * *', async () => { + console.log('🕐 Exécution programmée de la purge des ventes'); + await purgeSalesData(); + }, { + scheduled: true, + timezone: 'Europe/Paris' + }); + + console.log('🕐 Planificateur de purge des ventes démarré'); + console.log('⏰ Purge automatique : 1er de chaque mois à 02:00'); + console.log('📅 Rétention des données : 19 mois maximum'); +} + +/** + * Exécuter une purge manuelle (pour l'interface d'administration) + */ +export async function executePurgeManually() { + console.log('🔧 Purge manuelle demandée via l\'interface d\'administration'); + return await purgeSalesData(); +} \ No newline at end of file diff --git a/server/index.ts b/server/index.ts index d1fc280..8243bdb 100644 --- a/server/index.ts +++ b/server/index.ts @@ -2,6 +2,7 @@ import express, { type Request, Response, NextFunction } from "express"; import { registerRoutes } from "./routes"; import { setupVite, serveStatic, log } from "./vite"; import { startBackupScheduler } from "./backup-scheduler"; +import { startPurgeScheduler } from "./data-purge"; const app = express(); app.use(express.json()); @@ -70,5 +71,8 @@ app.use((req, res, next) => { // Start the automatic backup scheduler startBackupScheduler(); + + // Start the data purge scheduler + startPurgeScheduler(); }); })(); diff --git a/server/routes.ts b/server/routes.ts index f8e4a74..df46ef0 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -6,6 +6,7 @@ import connectPg from "connect-pg-simple"; import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema"; import { z } from "zod"; import { createAutomaticBackup, getBackupStats } from "./backup-scheduler"; +import { executePurgeManually, getPurgeStats } from "./data-purge"; declare module 'express-session' { interface SessionData { @@ -535,6 +536,27 @@ export async function registerRoutes(app: Express): Promise { } }); + // Admin routes - Data Purge + app.get('/api/admin/purge/stats', requireRole(['admin']), async (req, res) => { + try { + const stats = await getPurgeStats(); + res.json(stats); + } catch (error) { + console.error('Get purge stats error:', error); + res.status(500).json({ error: "Failed to get purge stats" }); + } + }); + + app.post('/api/admin/purge/execute', requireRole(['admin']), async (req, res) => { + try { + const result = await executePurgeManually(); + res.json(result); + } catch (error) { + console.error('Execute purge error:', error); + res.status(500).json({ error: "Failed to execute purge" }); + } + }); + const httpServer = createServer(app); return httpServer; }