+ {/* Configuration des backups automatiques */}
+
+
+
+
+ Configuration des Sauvegardes Automatiques
+
+
+ Activez ou désactivez les sauvegardes automatiques quotidiennes à 2h00
+
+
+
+
+
+
+ Sauvegardes automatiques
+
+
+ {config?.automaticBackupsEnabled === false
+ ? "Les sauvegardes automatiques sont actuellement désactivées"
+ : "Les sauvegardes automatiques sont actives"}
+
+
+
toggleAutomaticBackupsMutation.mutate(checked)}
+ disabled={toggleAutomaticBackupsMutation.isPending}
+ data-testid="switch-automatic-backups"
+ />
+
+
+
+
{/* Status Cards */}
@@ -228,12 +301,20 @@ export default function BackupManager() {
Statut Système
-
+ {config?.automaticBackupsEnabled !== false ? (
+
+ ) : (
+
+ )}
- Actif
+
+ {config?.automaticBackupsEnabled !== false ? 'Actif' : 'Désactivé'}
+
- Sauvegarde automatique 2h00
+ {config?.automaticBackupsEnabled !== false
+ ? 'Sauvegarde automatique 2h00'
+ : 'Backups automatiques désactivés'}
diff --git a/client/src/pages/SalesAnalysisConfig.tsx b/client/src/pages/SalesAnalysisConfig.tsx
new file mode 100644
index 0000000..176e95d
--- /dev/null
+++ b/client/src/pages/SalesAnalysisConfig.tsx
@@ -0,0 +1,125 @@
+import { useState, useEffect } from "react";
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { useToast } from "@/hooks/use-toast";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Save, Loader2 } from "lucide-react";
+import { queryClient, apiRequest } from "@/lib/queryClient";
+
+interface UtilitiesConfig {
+ id?: number;
+ salesAnalysisUrl?: string | null;
+ createdAt?: Date;
+ updatedAt?: Date;
+}
+
+export default function SalesAnalysisConfig() {
+ const { toast } = useToast();
+ const [salesAnalysisUrl, setSalesAnalysisUrl] = useState("");
+
+ // Récupérer la configuration existante
+ const { data: config, isLoading } = useQuery
({
+ queryKey: ['/api/utilities']
+ });
+
+ // Mettre à jour l'état quand les données sont chargées
+ useEffect(() => {
+ if (config?.salesAnalysisUrl) {
+ setSalesAnalysisUrl(config.salesAnalysisUrl);
+ }
+ }, [config]);
+
+ // Mutation pour sauvegarder la configuration
+ const saveMutation = useMutation({
+ mutationFn: async (data: { salesAnalysisUrl: string | null }) => {
+ return await apiRequest('/api/utilities', 'POST', data);
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['/api/utilities'] });
+ toast({
+ title: "Configuration sauvegardée",
+ description: "L'URL d'analyse des ventes a été mise à jour avec succès.",
+ });
+ },
+ onError: (error: any) => {
+ toast({
+ title: "Erreur",
+ description: error.message || "Impossible de sauvegarder la configuration",
+ variant: "destructive",
+ });
+ }
+ });
+
+ const handleSave = () => {
+ saveMutation.mutate({
+ salesAnalysisUrl: salesAnalysisUrl.trim() || null
+ });
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ Configuration Analyse des Ventes
+
+ Configurez l'URL ou l'adresse IP pour accéder à l'interface d'analyse des ventes.
+ Cette page sera accessible via le menu "Analyse Vente".
+
+
+
+
+
URL ou Adresse IP
+
setSalesAnalysisUrl(e.target.value)}
+ data-testid="input-sales-analysis-url"
+ />
+
+ Exemple : https://votre-domaine.com/analyse ou http://192.168.1.100:8080
+
+
+
+
+ {saveMutation.isPending ? (
+ <>
+
+ Sauvegarde...
+ >
+ ) : (
+ <>
+
+ Sauvegarder
+ >
+ )}
+
+
+ {config?.salesAnalysisUrl && (
+
+
+ URL configurée : {config.salesAnalysisUrl}
+
+
+ Cette URL sera affichée dans le menu "Analyse Vente"
+
+
+ )}
+
+
+ );
+}
diff --git a/client/src/pages/SalesAnalysisPage.tsx b/client/src/pages/SalesAnalysisPage.tsx
new file mode 100644
index 0000000..1b64665
--- /dev/null
+++ b/client/src/pages/SalesAnalysisPage.tsx
@@ -0,0 +1,65 @@
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Loader2, TrendingUp, AlertCircle } from "lucide-react";
+import { Link } from "wouter";
+import { Button } from "@/components/ui/button";
+
+interface UtilitiesConfig {
+ id?: number;
+ salesAnalysisUrl?: string | null;
+ createdAt?: Date;
+ updatedAt?: Date;
+}
+
+export default function SalesAnalysisPage() {
+ // Récupérer l'URL configurée
+ const { data: config, isLoading } = useQuery({
+ queryKey: ['/api/utilities']
+ });
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ const salesAnalysisUrl = config?.salesAnalysisUrl;
+
+ if (!salesAnalysisUrl) {
+ return (
+
+
+
+
+ URL non configurée
+
+ L'URL d'analyse des ventes n'a pas encore été configurée.
+ Veuillez la définir dans les utilitaires.
+
+
+
+
+
+ Configurer l'URL
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/client/src/pages/Utilities.tsx b/client/src/pages/Utilities.tsx
index 2720b32..b3edd59 100644
--- a/client/src/pages/Utilities.tsx
+++ b/client/src/pages/Utilities.tsx
@@ -8,7 +8,8 @@ import {
Bug,
Cloud,
Send,
- Code
+ Code,
+ TrendingUp
} from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -21,6 +22,7 @@ import DatabaseDebug from "./DatabaseDebug";
import WeatherSettings from "./WeatherSettings";
import WebhookBAPConfig from "./WebhookBAPConfig";
import SQLExecutor from "./SQLExecutor";
+import SalesAnalysisConfig from "./SalesAnalysisConfig";
export default function Utilities() {
const { user } = useAuthUnified();
@@ -41,9 +43,10 @@ export default function Utilities() {
const canManageWeather = user?.role === 'admin';
const canManageWebhookBAP = user?.role === 'admin';
const canExecuteSQL = user?.role === 'admin';
+ const canManageSalesAnalysis = user?.role === 'admin' || user?.role === 'directeur';
// Si l'utilisateur n'a aucune permission, afficher le message d'accès refusé
- if (!canManageBackups && !canManageNocoDB && !canDebugDatabase && !canManageWeather && !canManageWebhookBAP && !canExecuteSQL) {
+ if (!canManageBackups && !canManageNocoDB && !canDebugDatabase && !canManageWeather && !canManageWebhookBAP && !canExecuteSQL && !canManageSalesAnalysis) {
return (
@@ -79,7 +82,7 @@ export default function Utilities() {
{/* Content avec onglets */}
-
+
{canManageBackups && (
@@ -116,6 +119,12 @@ export default function Utilities() {
Exécution SQL
)}
+ {canManageSalesAnalysis && (
+
+
+ Analyse Vente
+
+ )}
{/* Contenu des onglets */}
@@ -166,6 +175,14 @@ export default function Utilities() {
)}
+
+ {canManageSalesAnalysis && (
+
+
+
+
+
+ )}
diff --git a/replit.md b/replit.md
index 3dd8e79..91acb8c 100644
--- a/replit.md
+++ b/replit.md
@@ -39,6 +39,7 @@ The platform is built with a clear separation of concerns, utilizing a modern fu
- **DLC Module:** Permissions allow admin, director, and manager roles to validate DLC products. Includes explanatory tooltips for UX enhancement.
- **Reporting:** Statistical reporting, including monthly summaries of deliveries.
- **UI Harmonization:** All detail modals across modules (orders, deliveries, calendar) use a unified component for consistent information display.
+- **Analyse Vente (Sales Analysis):** A dedicated module for admins and directors that displays a configurable external URL/IP in an iframe. Configuration is managed via a dedicated tab in the Utilities page, allowing administrators to set or update the sales analysis dashboard URL. The menu is visible only to admin and directeur roles, ensuring proper access control. Includes intelligent error handling with informative messages when the URL is not configured.
## External Dependencies
- **API Météo:** Visual Crossing API
diff --git a/server/backupService.ts b/server/backupService.ts
index 3a02fc6..0fedd35 100644
--- a/server/backupService.ts
+++ b/server/backupService.ts
@@ -5,7 +5,7 @@ import path from 'path';
import { nanoid } from 'nanoid';
import { eq, desc } from "drizzle-orm";
import { db } from "./db";
-import { databaseBackups } from "@shared/schema";
+import { databaseBackups, utilities } from "@shared/schema";
import type { DatabaseBackup, InsertDatabaseBackup } from "@shared/schema";
const execAsync = promisify(exec);
@@ -214,6 +214,20 @@ export class BackupService {
// Nouvelle méthode : Vérifier et effectuer une sauvegarde quotidienne si nécessaire
async checkAndPerformDailyBackup(userId: string = 'system'): Promise<{ backupPerformed: boolean; message: string }> {
try {
+ // Vérifier si les backups automatiques sont activés
+ const [config] = await db.select()
+ .from(utilities)
+ .limit(1);
+
+ // Si les backups automatiques sont désactivés, ne rien faire
+ if (config && config.automaticBackupsEnabled === false) {
+ console.log('ℹ️ Sauvegardes automatiques désactivées - Aucune action effectuée');
+ return {
+ backupPerformed: false,
+ message: 'Sauvegardes automatiques désactivées'
+ };
+ }
+
const today = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD
// Vérifier s'il y a déjà une sauvegarde automatique aujourd'hui
@@ -255,6 +269,16 @@ export class BackupService {
// Check for automatic backup every hour
const checkBackupNeeded = async () => {
try {
+ // Vérifier si les backups automatiques sont activés
+ const [config] = await db.select()
+ .from(utilities)
+ .limit(1);
+
+ // Si les backups automatiques sont désactivés, ne rien faire
+ if (config && config.automaticBackupsEnabled === false) {
+ return;
+ }
+
const now = new Date();
const today = now.toISOString().split('T')[0]; // YYYY-MM-DD format
diff --git a/server/routes.ts b/server/routes.ts
index 6d69470..d68cfcb 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -41,10 +41,12 @@ import {
insertWeatherDataSchema,
insertWeatherSettingsSchema,
insertWebhookBapConfigSchema,
+ insertUtilitiesSchema,
insertAvoirSchema,
insertReconciliationCommentSchema,
users, groups, userGroups, suppliers, orders, deliveries, publicities, publicityParticipations,
customerOrders, nocodbConfig, dlcProducts, tasks, invoiceVerificationCache, dashboardMessages, webhookBapConfig,
+ utilities,
avoirs
} from "@shared/schema";
import { hasPermission } from "@shared/permissions";
@@ -219,6 +221,63 @@ export async function registerRoutes(app: Express): Promise
{
}
});
+ // Routes pour configuration utilities
+ app.get('/api/utilities', isAuthenticated, async (req: any, res) => {
+ try {
+ const userId = req.user?.claims?.sub || req.user?.id;
+ if (!userId) {
+ return res.status(401).json({ error: 'Utilisateur non authentifié' });
+ }
+
+ const user = await storage.getUser(userId);
+ if (!user || (user.role !== 'admin' && user.role !== 'directeur')) {
+ return res.status(403).json({ error: 'Accès refusé - Admin ou Directeur uniquement' });
+ }
+
+ const config = await storage.getUtilities();
+ res.json(config || null);
+
+ } catch (error: any) {
+ console.error('❌ Erreur récupération utilities:', error);
+ res.status(500).json({ error: 'Erreur serveur', details: error.message });
+ }
+ });
+
+ app.post('/api/utilities', isAuthenticated, async (req: any, res) => {
+ try {
+ const userId = req.user?.claims?.sub || req.user?.id;
+ if (!userId) {
+ return res.status(401).json({ error: 'Utilisateur non authentifié' });
+ }
+
+ const user = await storage.getUser(userId);
+ if (!user || (user.role !== 'admin' && user.role !== 'directeur')) {
+ return res.status(403).json({ error: 'Accès refusé - Admin ou Directeur uniquement' });
+ }
+
+ const validatedData = insertUtilitiesSchema.parse(req.body);
+
+ const existingConfig = await storage.getUtilities();
+
+ let config: any;
+ if (existingConfig) {
+ config = await storage.updateUtilities(existingConfig.id, validatedData);
+ } else {
+ config = await storage.createUtilities(validatedData);
+ }
+
+ console.log('✅ Configuration utilities sauvegardée:', {
+ id: config.id,
+ salesAnalysisUrl: config.salesAnalysisUrl
+ });
+ res.json(config);
+
+ } catch (error: any) {
+ console.error('❌ Erreur sauvegarde utilities:', error);
+ res.status(500).json({ error: 'Erreur serveur', details: error.message });
+ }
+ });
+
// Route BAP pour envoi webhook n8n
app.post('/api/bap/send-webhook', isAuthenticated, async (req: any, res) => {
try {
diff --git a/server/storage.ts b/server/storage.ts
index 6e9f05b..d7b2dba 100644
--- a/server/storage.ts
+++ b/server/storage.ts
@@ -73,6 +73,9 @@ import {
webhookBapConfig,
type WebhookBapConfig,
type InsertWebhookBapConfig,
+ utilities,
+ type Utilities,
+ type InsertUtilities,
avoirs,
type Avoir,
type InsertAvoir,
@@ -260,6 +263,11 @@ export interface IStorage {
createWebhookBapConfig(config: InsertWebhookBapConfig): Promise;
updateWebhookBapConfig(id: number, config: Partial): Promise;
+ // Utilities operations
+ getUtilities(): Promise;
+ createUtilities(utilities: InsertUtilities): Promise;
+ updateUtilities(id: number, utilities: Partial): Promise;
+
// Reconciliation Comments operations
getReconciliationComments(deliveryId: number): Promise;
getReconciliationCommentById(id: number): Promise;
@@ -2812,6 +2820,26 @@ export class DatabaseStorage implements IStorage {
return config;
}
+ // Utilities operations
+ async getUtilities(): Promise {
+ const [result] = await db.select().from(utilities).limit(1);
+ return result;
+ }
+
+ async createUtilities(utilitiesData: InsertUtilities): Promise {
+ const [result] = await db.insert(utilities).values(utilitiesData).returning();
+ return result;
+ }
+
+ async updateUtilities(id: number, utilitiesData: Partial): Promise {
+ const [result] = await db
+ .update(utilities)
+ .set({ ...utilitiesData, updatedAt: new Date() })
+ .where(eq(utilities.id, id))
+ .returning();
+ return result;
+ }
+
// Reconciliation Comments operations
async getReconciliationComments(deliveryId: number): Promise {
const comments = await db
@@ -5041,6 +5069,32 @@ export class MemStorage implements IStorage {
};
}
+ // Utilities operations
+ async getUtilities(): Promise {
+ // MemStorage ne stocke pas les utilities, retourne undefined
+ return undefined;
+ }
+
+ async createUtilities(utilitiesData: InsertUtilities): Promise {
+ // MemStorage ne supporte pas la création de utilities
+ return {
+ id: 1,
+ salesAnalysisUrl: utilitiesData.salesAnalysisUrl ?? null,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+ }
+
+ async updateUtilities(id: number, utilitiesData: Partial): Promise {
+ // MemStorage ne supporte pas la mise à jour de utilities
+ return {
+ id,
+ salesAnalysisUrl: utilitiesData.salesAnalysisUrl ?? null,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+ }
+
// Announcement operations
async createAnnouncement(announcementData: InsertAnnouncement): Promise {
const announcementStorage = getAnnouncementStorage(
diff --git a/shared/schema.ts b/shared/schema.ts
index 950836a..9ed9b97 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -669,10 +669,27 @@ export const databaseBackups = pgTable("database_backups", {
export const insertDatabaseBackupSchema = createInsertSchema(databaseBackups);
+// Utilities - Configuration générale de l'application
+export const utilities = pgTable("utilities", {
+ id: serial("id").primaryKey(),
+ salesAnalysisUrl: varchar("sales_analysis_url", { length: 500 }), // URL pour l'analyse des ventes
+ automaticBackupsEnabled: boolean("automatic_backups_enabled").default(true), // Activer/désactiver les backups automatiques
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+});
+
+export const insertUtilitiesSchema = createInsertSchema(utilities).omit({
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+});
+
export type InsertNocodbConfig = z.infer;
export type InsertInvoiceVerificationCache = z.infer;
export type DatabaseBackup = typeof databaseBackups.$inferSelect;
export type InsertDatabaseBackup = z.infer;
+export type Utilities = typeof utilities.$inferSelect;
+export type InsertUtilities = z.infer;
// Types
export type User = typeof users.$inferSelect;