@@ -759,8 +755,7 @@ export default function NewSaleForm() {
{/* Photo Ticket de caisse */}
-
Photo du ticket de caisse (optionnel)
-
La photo du ticket n'est pas obligatoire
+
Photo du ticket de caisse
diff --git a/client/src/index.css b/client/src/index.css
index 69be341..655b219 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -409,21 +409,21 @@ body {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
-/* Fonds de couleur unis harmonisés selon la nouvelle interface */
+/* Fonds de couleur unis harmonisés avec des teintes distinctes */
.section-vendor {
- background: #e0f2fe !important; /* Fond bleu clair comme l'image */
+ background: #f0f9ff !important; /* Fond bleu très clair */
}
.section-product {
- background: #e8f5e8 !important; /* Fond vert clair comme l'image */
+ background: #f0fdf4 !important; /* Fond vert très clair */
}
.section-client {
- background: #f3e8ff !important; /* Fond violet clair comme l'image */
+ background: #fef3c7 !important; /* Fond jaune clair */
}
.section-identity {
- background: #fff3e0 !important; /* Fond orange clair comme l'image */
+ background: #fce7f3 !important; /* Fond rose très clair */
}
/* S'assurer que tous les inputs ont un fond blanc dans toutes les sections */
diff --git a/replit.md b/replit.md
index 7e40e2f..c549cbc 100644
--- a/replit.md
+++ b/replit.md
@@ -315,28 +315,6 @@ Preferred communication style: Simple, everyday language.
- ✅ **Database Schema**: Sales and sale_products tables properly configured for multi-product sales
- ✅ **Export Compatibility**: All export functions (Excel, PDF, CSV) support multiple products per sale
-### SQL Backup System Implementation (January 19, 2025)
-- ✅ **SQL Backup Format**: Migrated from JSON to native PostgreSQL SQL format using pg_dump
-- ✅ **Database Restoration**: Implemented SQL import capability using psql for complete database restoration
-- ✅ **Automatic SQL Backups**: Modified scheduler to create SQL dumps instead of JSON exports
-- ✅ **Manual Export/Import**: Updated administration interface to handle SQL files (.sql)
-- ✅ **Buffer Optimization**: Increased maxBuffer to 50MB for handling large databases with photo data
-- ✅ **Production Compatibility**: Native PostgreSQL backup format ensures full compatibility with production systems
-- ✅ **Data Integrity**: SQL format preserves all database structure, constraints, and relationships
-- ✅ **User Interface**: Updated backup management UI with SQL-specific messaging and file extensions
-
-### Photo Requirements Optimization (January 19, 2025)
-- ✅ **Optional Identity Photos**: Made identity document photos (recto/verso) optional as per user request
-- ✅ **Optional Receipt Photos**: Clarified that receipt/ticket photos are also optional
-- ✅ **UI Updates**: Updated form labels to clearly indicate photo fields are not mandatory
-- ✅ **Validation Adjustment**: Photos no longer required for form submission compliance
-- ✅ **User Experience**: Added explanatory text to reduce confusion about photo requirements
-
-### Form Usability Improvements (January 19, 2025)
-- ✅ **Default Quantity**: Set product quantity field to default value of 1 for better user experience
-- ✅ **Streamlined Input**: Users no longer need to manually enter quantity for single item purchases
-- ✅ **Placeholder Update**: Changed placeholder text to reflect default value
-
### Docker Configuration & Cleanup (January 9, 2025)
- ✅ Complete Docker setup optimized for external PostgreSQL database
- ✅ Docker Compose configuration with RegisFlow application only
diff --git a/server/backup-scheduler.ts b/server/backup-scheduler.ts
index 564f65e..72a9d07 100644
--- a/server/backup-scheduler.ts
+++ b/server/backup-scheduler.ts
@@ -2,22 +2,10 @@ import cron from 'node-cron';
import fs from 'fs';
import path from 'path';
import { storage } from './storage';
-import { exec } from 'child_process';
-import { promisify } from 'util';
-
-const execAsync = promisify(exec);
const BACKUP_DIR = path.join(process.cwd(), 'backups');
const MAX_BACKUPS = 10;
-// Database connection info from environment variables
-const DB_HOST = process.env.PGHOST || 'localhost';
-const DB_PORT = process.env.PGPORT || '5432';
-const DB_NAME = process.env.PGDATABASE || 'postgres';
-const DB_USER = process.env.PGUSER || 'postgres';
-const DB_PASSWORD = process.env.PGPASSWORD || '';
-const DATABASE_URL = process.env.DATABASE_URL;
-
// Ensure backup directory exists
if (!fs.existsSync(BACKUP_DIR)) {
fs.mkdirSync(BACKUP_DIR, { recursive: true });
@@ -25,117 +13,66 @@ if (!fs.existsSync(BACKUP_DIR)) {
export async function createAutomaticBackup() {
try {
- console.log('Creating automatic SQL backup...');
+ console.log('Creating automatic backup...');
- // Create filename with timestamp
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
- const filename = `auto-backup-${timestamp}.sql`;
- const filepath = path.join(BACKUP_DIR, filename);
-
- // Create SQL dump using pg_dump
- await createSQLBackup(filepath);
-
- // Get stats for logging
+ // Get all data from the database
const users = await storage.getAllUsers();
const stores = await storage.getAllStores();
- let totalSales = 0;
+
+ // Get all sales from all stores
+ const allSales = [];
for (const store of stores) {
const storeSales = await storage.getSalesByStore(store.id);
- totalSales += storeSales.length;
+ allSales.push(...storeSales);
}
+
+ const backupData = {
+ timestamp: new Date().toISOString(),
+ version: "1.0",
+ type: "automatic",
+ data: {
+ users: users.map(user => ({
+ ...user,
+ // Don't include the password hash in backup for security
+ password: undefined
+ })),
+ stores,
+ sales: allSales
+ },
+ metadata: {
+ totalUsers: users.length,
+ totalStores: stores.length,
+ totalSales: allSales.length,
+ exportedBy: 'system-auto-backup'
+ }
+ };
+
+ // Create filename with timestamp
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
+ const filename = `auto-backup-${timestamp}.json`;
+ const filepath = path.join(BACKUP_DIR, filename);
+
+ // Write backup to file
+ fs.writeFileSync(filepath, JSON.stringify(backupData, null, 2));
- console.log(`✅ Automatic SQL backup created: ${filename}`);
- console.log(`📊 Backup stats: ${users.length} users, ${stores.length} stores, ${totalSales} sales`);
+ console.log(`✅ Automatic backup created: ${filename}`);
+ console.log(`📊 Backup stats: ${users.length} users, ${stores.length} stores, ${allSales.length} sales`);
// Clean up old backups
await cleanupOldBackups();
- return {
- success: true,
- filename,
- stats: {
- totalUsers: users.length,
- totalStores: stores.length,
- totalSales: totalSales,
- exportedBy: 'system-auto-backup'
- }
- };
+ return { success: true, filename, stats: backupData.metadata };
} catch (error) {
- console.error('❌ Failed to create automatic SQL backup:', error);
+ console.error('❌ Failed to create automatic backup:', error);
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
}
-export async function createSQLBackup(filepath: string): Promise {
- try {
- // Use pg_dump to create SQL backup
- let command: string;
-
- if (DATABASE_URL) {
- // Use DATABASE_URL if available
- command = `pg_dump "${DATABASE_URL}" --no-owner --no-privileges --clean --if-exists`;
- } else {
- // Use individual connection parameters
- command = `PGPASSWORD="${DB_PASSWORD}" pg_dump -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" --no-owner --no-privileges --clean --if-exists`;
- }
-
- const { stdout, stderr } = await execAsync(command, {
- maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large databases
- });
-
- if (stderr && !stderr.includes('NOTICE')) {
- console.warn('pg_dump warnings:', stderr);
- }
-
- // Write SQL dump to file
- fs.writeFileSync(filepath, stdout);
-
- console.log(`SQL backup created: ${path.basename(filepath)}`);
- } catch (error) {
- console.error('Failed to create SQL backup:', error);
- throw new Error(`SQL backup failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
- }
-}
-
-export async function restoreSQLBackup(filepath: string): Promise {
- try {
- console.log(`Restoring SQL backup from: ${path.basename(filepath)}`);
-
- // Read SQL file
- const sqlContent = fs.readFileSync(filepath, 'utf8');
-
- // Use psql to restore the backup
- let command: string;
-
- if (DATABASE_URL) {
- // Use DATABASE_URL if available
- command = `psql "${DATABASE_URL}"`;
- } else {
- // Use individual connection parameters
- command = `PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}"`;
- }
-
- const { stderr } = await execAsync(command, {
- input: sqlContent,
- maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large databases
- });
-
- if (stderr && !stderr.includes('NOTICE')) {
- console.warn('psql warnings:', stderr);
- }
-
- console.log(`✅ SQL backup restored successfully from: ${path.basename(filepath)}`);
- } catch (error) {
- console.error('Failed to restore SQL backup:', error);
- throw new Error(`SQL restore failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
- }
-}
-
async function cleanupOldBackups() {
try {
- // Get all backup files (now SQL files)
+ // Get all backup files
const files = fs.readdirSync(BACKUP_DIR)
- .filter(file => file.startsWith('auto-backup-') && file.endsWith('.sql'))
+ .filter(file => file.startsWith('auto-backup-') && file.endsWith('.json'))
.map(file => ({
name: file,
path: path.join(BACKUP_DIR, file),
@@ -162,7 +99,7 @@ async function cleanupOldBackups() {
export function getBackupStats() {
try {
const files = fs.readdirSync(BACKUP_DIR)
- .filter(file => file.startsWith('auto-backup-') && file.endsWith('.sql'));
+ .filter(file => file.startsWith('auto-backup-') && file.endsWith('.json'));
return {
totalBackups: files.length,
diff --git a/server/routes.ts b/server/routes.ts
index a8fe864..c17e71f 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -5,9 +5,7 @@ import session from "express-session";
import connectPg from "connect-pg-simple";
import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema";
import { z } from "zod";
-import fs from "fs";
-import path from "path";
-import { createAutomaticBackup, getBackupStats, createSQLBackup, restoreSQLBackup } from "./backup-scheduler";
+import { createAutomaticBackup, getBackupStats } from "./backup-scheduler";
import { executePurgeManually, getPurgeStats } from "./data-purge";
declare module 'express-session' {
@@ -466,65 +464,139 @@ export async function registerRoutes(app: Express): Promise {
}
});
- // SQL Backup routes
+ // Backup routes
app.get('/api/admin/backup/export', requireRole(['admin']), async (req, res) => {
try {
- // Create a temporary SQL backup
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
- const filename = `manual-backup-${timestamp}.sql`;
- const filepath = path.join(process.cwd(), 'backups', filename);
-
- await createSQLBackup(filepath);
-
- // Send the SQL file
- res.setHeader('Content-Type', 'application/sql');
- res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
-
- const sqlContent = fs.readFileSync(filepath, 'utf8');
- res.send(sqlContent);
-
- // Optionally delete the temporary file after sending
- // fs.unlinkSync(filepath);
+ // Get all data from the database
+ const users = await storage.getAllUsers();
+ const stores = await storage.getAllStores();
+ // Get all sales from all stores
+ const allSales = [];
+ for (const store of stores) {
+ const storeSales = await storage.getSalesByStore(store.id);
+ allSales.push(...storeSales);
+ }
+
+ const backupData = {
+ timestamp: new Date().toISOString(),
+ version: "1.0",
+ data: {
+ users: users.map(user => ({
+ ...user,
+ // Don't include the password hash in backup for security
+ password: undefined
+ })),
+ stores,
+ sales: allSales
+ },
+ metadata: {
+ totalUsers: users.length,
+ totalStores: stores.length,
+ totalSales: allSales.length,
+ exportedBy: req.user.username
+ }
+ };
+
+ res.json(backupData);
} catch (error) {
- console.error('Export SQL backup error:', error);
- res.status(500).json({ error: "Failed to export SQL backup" });
+ console.error('Export backup error:', error);
+ res.status(500).json({ error: "Failed to export backup" });
}
});
- // SQL Backup import route (receives SQL file content)
app.post('/api/admin/backup/import', requireRole(['admin']), async (req, res) => {
try {
- const { sqlContent } = req.body;
+ const backupData = req.body;
- if (!sqlContent || typeof sqlContent !== 'string') {
- return res.status(400).json({ error: "SQL content is required" });
+ // Validate backup structure
+ if (!backupData.data || !backupData.data.users || !backupData.data.stores || !backupData.data.sales) {
+ return res.status(400).json({ error: "Invalid backup file format" });
}
- // Create a temporary file for the SQL content
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
- const filename = `restore-${timestamp}.sql`;
- const filepath = path.join(process.cwd(), 'backups', filename);
-
- // Write SQL content to temporary file
- fs.writeFileSync(filepath, sqlContent);
-
- // Restore the backup
- await restoreSQLBackup(filepath);
-
- // Clean up temporary file
- fs.unlinkSync(filepath);
+ // Clear existing data (in reverse order due to foreign keys)
+ // First delete all sales
+ const existingSales = [];
+ const existingStores = await storage.getAllStores();
+ for (const store of existingStores) {
+ const storeSales = await storage.getSalesByStore(store.id);
+ existingSales.push(...storeSales);
+ }
+ for (const sale of existingSales) {
+ await storage.deleteSale(sale.id);
+ }
+
+ // Then delete users (except admin to avoid locking out)
+ const existingUsers = await storage.getAllUsers();
+ for (const user of existingUsers) {
+ if (user.role !== 'administrator' || user.username !== 'admin') {
+ await storage.deleteUser(user.id);
+ }
+ }
+
+ // Then delete stores
+ for (const store of existingStores) {
+ await storage.deleteStore(store.id);
+ }
+
+ // Import new data
+ // First import stores
+ const storeIdMapping = new Map();
+ for (const storeData of backupData.data.stores) {
+ const { id, ...storeWithoutId } = storeData;
+ const newStore = await storage.createStore(storeWithoutId);
+ storeIdMapping.set(id, newStore.id);
+ }
+
+ // Then import users (excluding admin and users with passwords)
+ const userIdMapping = new Map();
+ for (const userData of backupData.data.users) {
+ if (userData.username === 'admin') continue; // Skip admin user
+
+ const { id, password, ...userWithoutIdAndPassword } = userData;
+
+ // Map store ID if it exists
+ if (userData.storeId && storeIdMapping.has(userData.storeId)) {
+ userWithoutIdAndPassword.storeId = storeIdMapping.get(userData.storeId);
+ }
+
+ // Set a default password since we can't restore the original
+ const userWithPassword = {
+ ...userWithoutIdAndPassword,
+ password: 'restored123' // Users will need to change this
+ };
+
+ const newUser = await storage.createUser(userWithPassword);
+ userIdMapping.set(id, newUser.id);
+ }
+
+ // Finally import sales
+ for (const saleData of backupData.data.sales) {
+ const { id, ...saleWithoutId } = saleData;
+
+ // Map store and user IDs
+ if (saleData.storeId && storeIdMapping.has(saleData.storeId)) {
+ saleWithoutId.storeId = storeIdMapping.get(saleData.storeId);
+ }
+ if (saleData.userId && userIdMapping.has(saleData.userId)) {
+ saleWithoutId.userId = userIdMapping.get(saleData.userId);
+ }
+
+ await storage.createSale(saleWithoutId);
+ }
+
res.json({
- message: "SQL backup restored successfully",
- timestamp: new Date().toISOString()
+ message: "Backup imported successfully",
+ imported: {
+ stores: backupData.data.stores.length,
+ users: backupData.data.users.filter((u: any) => u.username !== 'admin').length,
+ sales: backupData.data.sales.length
+ }
});
} catch (error) {
- console.error('Import SQL backup error:', error);
- res.status(500).json({
- error: "Failed to restore SQL backup",
- details: error instanceof Error ? error.message : 'Unknown error'
- });
+ console.error('Import backup error:', error);
+ res.status(500).json({ error: "Failed to import backup", details: error instanceof Error ? error.message : 'Unknown error' });
}
});