mirror of
https://github.com/R0m1k3/Regisflow.git
synced 2026-10-11 17:29:48 +02:00
Add feature to automatically delete old sales data for legal compliance
Implements a data purge system, scheduler, and API endpoints for stats and manual execution. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 91318273-c764-4fd4-be04-bdc12c38af32 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/c3a86207-4ee0-47f9-8062-2dbbae696290.jpg
This commit is contained in:
1 parent
686512516d
commit
7404dc6443
5 files changed
+315
No files matched your search
@@ -81,6 +81,15 @@ export default function Administration() {
|
|||||||
refetchInterval: 30000, // Refresh every 30 seconds
|
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
|
// User mutations
|
||||||
const createUserMutation = useMutation({
|
const createUserMutation = useMutation({
|
||||||
mutationFn: async (userData: CreateUserData) => {
|
mutationFn: async (userData: CreateUserData) => {
|
||||||
@@ -386,6 +395,34 @@ export default function Administration() {
|
|||||||
input.click();
|
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 getRoleBadge = (role: string) => {
|
||||||
const variants = {
|
const variants = {
|
||||||
administrator: "destructive" as const,
|
administrator: "destructive" as const,
|
||||||
@@ -418,6 +455,7 @@ export default function Administration() {
|
|||||||
<TabsTrigger value="users">Utilisateurs</TabsTrigger>
|
<TabsTrigger value="users">Utilisateurs</TabsTrigger>
|
||||||
<TabsTrigger value="stores">Magasins</TabsTrigger>
|
<TabsTrigger value="stores">Magasins</TabsTrigger>
|
||||||
<TabsTrigger value="backup">Sauvegarde</TabsTrigger>
|
<TabsTrigger value="backup">Sauvegarde</TabsTrigger>
|
||||||
|
<TabsTrigger value="purge">Purge</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* Users Tab */}
|
{/* Users Tab */}
|
||||||
@@ -1238,6 +1276,134 @@ export default function Administration() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Data Purge Tab */}
|
||||||
|
<TabsContent value="purge" className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
||||||
|
<Clock className="h-5 w-5" />
|
||||||
|
Purge des Données
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<Calendar className="h-4 w-4" />
|
||||||
|
Statistiques de Purge
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Total des ventes :</span>
|
||||||
|
<span className="font-medium">{purgeStats?.totalSales || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Ventes anciennes (>19 mois) :</span>
|
||||||
|
<span className="font-medium text-orange-600">{purgeStats?.oldSales || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Date limite :</span>
|
||||||
|
<span className="font-medium text-xs">
|
||||||
|
{purgeStats?.cutoffDate ? new Date(purgeStats.cutoffDate).toLocaleDateString('fr-FR') : '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2 border-t">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`w-3 h-3 rounded-full ${purgeStats?.purgeEligible ? 'bg-orange-500' : 'bg-green-500'}`}></div>
|
||||||
|
<span className="text-sm">
|
||||||
|
{purgeStats?.purgeEligible ? 'Purge nécessaire' : 'Aucune purge nécessaire'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
Configuration
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium text-sm">Purge Automatique</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Exécutée automatiquement le 1er de chaque mois à 02:00
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium text-sm">Rétention des Données</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
19 mois maximum (conforme à la réglementation)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2 border-t">
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
disabled={!purgeStats?.purgeEligible || executePurgeMutation.isPending}
|
||||||
|
>
|
||||||
|
{executePurgeMutation.isPending ? 'Purge en cours...' : 'Exécuter Purge Manuelle'}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Confirmer la Purge</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Cette action va supprimer définitivement {purgeStats?.oldSales || 0} vente(s)
|
||||||
|
datant de plus de 19 mois. Cette opération est irréversible.
|
||||||
|
<br/><br/>
|
||||||
|
<strong>Ventes concernées :</strong> antérieures au {purgeStats?.cutoffDate ? new Date(purgeStats.cutoffDate).toLocaleDateString('fr-FR') : '-'}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleExecutePurge} className="bg-destructive hover:bg-destructive/90">
|
||||||
|
Confirmer la Purge
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Information sur la Purge</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="bg-blue-50 p-3 rounded-lg border border-blue-200">
|
||||||
|
<h4 className="font-medium text-sm text-blue-900 mb-2">📋 Réglementation</h4>
|
||||||
|
<p className="text-xs text-blue-700">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-50 p-3 rounded-lg border border-green-200">
|
||||||
|
<h4 className="font-medium text-sm text-green-900 mb-2">🔄 Automatisation</h4>
|
||||||
|
<p className="text-xs text-green-700">
|
||||||
|
La purge s'exécute automatiquement chaque mois pour maintenir la conformité.
|
||||||
|
Vous pouvez également l'exécuter manuellement si nécessaire.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-orange-50 p-3 rounded-lg border border-orange-200">
|
||||||
|
<h4 className="font-medium text-sm text-orange-900 mb-2">⚠️ Attention</h4>
|
||||||
|
<p className="text-xs text-orange-700">
|
||||||
|
La purge supprime définitivement les données. Assurez-vous d'avoir effectué
|
||||||
|
une sauvegarde récente avant d'exécuter une purge manuelle.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ Preferred communication style: Simple, everyday language.
|
|||||||
5. **Sales History**: Complete transaction history with date filtering and search
|
5. **Sales History**: Complete transaction history with date filtering and search
|
||||||
6. **Data Export**: CSV export functionality for regulatory reporting
|
6. **Data Export**: CSV export functionality for regulatory reporting
|
||||||
7. **Role-Based Permissions**: Different access levels for Admin, Manager, and Employee roles
|
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
|
### Authentication & Authorization
|
||||||
- **Administrators**: Full access to all features including user/store management
|
- **Administrators**: Full access to all features including user/store management
|
||||||
@@ -181,3 +183,12 @@ Preferred communication style: Simple, everyday language.
|
|||||||
- ✅ Added dynamic credential visibility based on whether default password is still in use
|
- ✅ Added dynamic credential visibility based on whether default password is still in use
|
||||||
- ✅ Security warning displayed when default credentials are shown
|
- ✅ Security warning displayed when default credentials are shown
|
||||||
- ✅ API endpoint to check default credential status with password hash comparison
|
- ✅ 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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import express, { type Request, Response, NextFunction } from "express";
|
|||||||
import { registerRoutes } from "./routes";
|
import { registerRoutes } from "./routes";
|
||||||
import { setupVite, serveStatic, log } from "./vite";
|
import { setupVite, serveStatic, log } from "./vite";
|
||||||
import { startBackupScheduler } from "./backup-scheduler";
|
import { startBackupScheduler } from "./backup-scheduler";
|
||||||
|
import { startPurgeScheduler } from "./data-purge";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -70,5 +71,8 @@ app.use((req, res, next) => {
|
|||||||
|
|
||||||
// Start the automatic backup scheduler
|
// Start the automatic backup scheduler
|
||||||
startBackupScheduler();
|
startBackupScheduler();
|
||||||
|
|
||||||
|
// Start the data purge scheduler
|
||||||
|
startPurgeScheduler();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
@@ -6,6 +6,7 @@ import connectPg from "connect-pg-simple";
|
|||||||
import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema";
|
import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createAutomaticBackup, getBackupStats } from "./backup-scheduler";
|
import { createAutomaticBackup, getBackupStats } from "./backup-scheduler";
|
||||||
|
import { executePurgeManually, getPurgeStats } from "./data-purge";
|
||||||
|
|
||||||
declare module 'express-session' {
|
declare module 'express-session' {
|
||||||
interface SessionData {
|
interface SessionData {
|
||||||
@@ -535,6 +536,27 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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);
|
const httpServer = createServer(app);
|
||||||
return httpServer;
|
return httpServer;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user