Merge pull request #484 from R0m1k3/1.1

1.1
This commit is contained in:
LogiFlow authored and GitHub committed 2025-10-31 10:30:55 +01:00
commit 39b8044568
22 files changed
+465 -36746

No files matched your search

+4
View File
@@ -27,6 +27,10 @@ externalPort = 4200
localPort = 33873
externalPort = 3003
[[ports]]
localPort = 34197
externalPort = 8099
[[ports]]
localPort = 34203
externalPort = 8080
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -24,6 +24,7 @@ import SavTickets from "@/pages/SavTickets";
import Avoirs from "@/pages/Avoirs";
import WeatherSettings from "@/pages/WeatherSettings";
import Analytics from "@/pages/Analytics";
import SalesAnalysisPage from "@/pages/SalesAnalysisPage";
import Layout from "@/components/Layout";
function RouterProduction() {
@@ -104,6 +105,7 @@ function RouterProduction() {
<Route path="/sav" component={SavTickets} />
<Route path="/avoirs" component={Avoirs} />
<Route path="/analytics" component={Analytics} />
<Route path="/sales-analysis" component={SalesAnalysisPage} />
{/* Routes de compatibilité - redirection vers utilities */}
<Route path="/backup" component={Utilities} />
+8 -1
View File
@@ -34,7 +34,8 @@ import {
Wrench,
FileUp,
Send,
Receipt
Receipt,
TrendingUp
} from "lucide-react";
export default function Sidebar() {
@@ -263,6 +264,12 @@ export default function Sidebar() {
icon: BarChart3,
roles: ["admin", "directeur", "manager"]
},
{
path: "/sales-analysis",
label: "Analyse Vente",
icon: TrendingUp,
roles: ["admin", "directeur"]
},
{
path: "/avoirs",
label: "Avoirs",
+85 -4
View File
@@ -3,6 +3,8 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { useToast } from "@/hooks/use-toast";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { apiRequest } from "@/lib/queryClient";
@@ -16,7 +18,8 @@ import {
FileText,
AlertTriangle,
CheckCircle,
Loader2
Loader2,
Settings
} from "lucide-react";
import { format } from "date-fns";
import { fr } from "date-fns/locale";
@@ -33,6 +36,14 @@ interface BackupFile {
backupType: string;
}
interface UtilitiesConfig {
id?: number;
salesAnalysisUrl?: string | null;
automaticBackupsEnabled?: boolean;
createdAt?: Date;
updatedAt?: Date;
}
export default function BackupManager() {
const { user } = useAuthUnified();
const { toast } = useToast();
@@ -50,6 +61,34 @@ export default function BackupManager() {
refetchInterval: 30000, // Refresh every 30 seconds
});
// Fetch utilities configuration
const { data: config } = useQuery<UtilitiesConfig>({
queryKey: ['/api/utilities'],
enabled: canManageBackups,
});
// Toggle automatic backups
const toggleAutomaticBackupsMutation = useMutation({
mutationFn: (enabled: boolean) =>
apiRequest('/api/utilities', 'POST', { automaticBackupsEnabled: enabled }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/utilities'] });
toast({
title: "Configuration mise à jour",
description: config?.automaticBackupsEnabled
? "Sauvegardes automatiques désactivées"
: "Sauvegardes automatiques activées",
});
},
onError: (error: any) => {
toast({
title: "Erreur",
description: error.message || "Impossible de mettre à jour la configuration",
variant: "destructive",
});
},
});
// Create manual backup
const createBackupMutation = useMutation({
mutationFn: () => apiRequest('/api/backups', 'POST'),
@@ -185,6 +224,40 @@ export default function BackupManager() {
{/* Content */}
<div className="flex-1 p-6 overflow-auto">
{/* Configuration des backups automatiques */}
<Card className="mb-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="w-5 h-5" />
Configuration des Sauvegardes Automatiques
</CardTitle>
<CardDescription>
Activez ou désactivez les sauvegardes automatiques quotidiennes à 2h00
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="automatic-backups" className="text-base font-medium">
Sauvegardes automatiques
</Label>
<p className="text-sm text-gray-600">
{config?.automaticBackupsEnabled === false
? "Les sauvegardes automatiques sont actuellement désactivées"
: "Les sauvegardes automatiques sont actives"}
</p>
</div>
<Switch
id="automatic-backups"
checked={config?.automaticBackupsEnabled !== false}
onCheckedChange={(checked) => toggleAutomaticBackupsMutation.mutate(checked)}
disabled={toggleAutomaticBackupsMutation.isPending}
data-testid="switch-automatic-backups"
/>
</div>
</CardContent>
</Card>
{/* Status Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<Card>
@@ -228,12 +301,20 @@ export default function BackupManager() {
<CardTitle className="text-sm font-medium">
Statut Système
</CardTitle>
<CheckCircle className="h-4 w-4 text-green-500" />
{config?.automaticBackupsEnabled !== false ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<AlertTriangle className="h-4 w-4 text-orange-500" />
)}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">Actif</div>
<div className={`text-2xl font-bold ${config?.automaticBackupsEnabled !== false ? 'text-green-600' : 'text-orange-600'}`}>
{config?.automaticBackupsEnabled !== false ? 'Actif' : 'Désactivé'}
</div>
<p className="text-xs text-muted-foreground">
Sauvegarde automatique 2h00
{config?.automaticBackupsEnabled !== false
? 'Sauvegarde automatique 2h00'
: 'Backups automatiques désactivés'}
</p>
</CardContent>
</Card>
+125
View File
@@ -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<UtilitiesConfig>({
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 (
<div className="flex items-center justify-center h-full">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
);
}
return (
<Card>
<CardHeader>
<CardTitle>Configuration Analyse des Ventes</CardTitle>
<CardDescription>
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".
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="salesAnalysisUrl">URL ou Adresse IP</Label>
<Input
id="salesAnalysisUrl"
type="text"
placeholder="https://exemple.com/analyse ou http://192.168.1.100:8080"
value={salesAnalysisUrl}
onChange={(e) => setSalesAnalysisUrl(e.target.value)}
data-testid="input-sales-analysis-url"
/>
<p className="text-sm text-gray-500">
Exemple : https://votre-domaine.com/analyse ou http://192.168.1.100:8080
</p>
</div>
<Button
onClick={handleSave}
disabled={saveMutation.isPending}
data-testid="button-save-sales-analysis"
>
{saveMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sauvegarde...
</>
) : (
<>
<Save className="w-4 h-4 mr-2" />
Sauvegarder
</>
)}
</Button>
{config?.salesAnalysisUrl && (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-800">
<strong>URL configurée :</strong> {config.salesAnalysisUrl}
</p>
<p className="text-xs text-blue-600 mt-1">
Cette URL sera affichée dans le menu "Analyse Vente"
</p>
</div>
)}
</CardContent>
</Card>
);
}
+65
View File
@@ -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<UtilitiesConfig>({
queryKey: ['/api/utilities']
});
if (isLoading) {
return (
<div className="flex-1 flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
);
}
const salesAnalysisUrl = config?.salesAnalysisUrl;
if (!salesAnalysisUrl) {
return (
<div className="flex-1 flex items-center justify-center">
<Card className="w-96">
<CardHeader className="text-center">
<AlertCircle className="w-12 h-12 text-orange-500 mx-auto mb-4" />
<CardTitle>URL non configurée</CardTitle>
<CardDescription>
L'URL d'analyse des ventes n'a pas encore été configurée.
Veuillez la définir dans les utilitaires.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-center">
<Link href="/utilities">
<Button variant="default" data-testid="button-configure-url">
Configurer l'URL
</Button>
</Link>
</CardContent>
</Card>
</div>
);
}
return (
<div className="w-full h-screen">
<iframe
src={salesAnalysisUrl}
className="w-full h-full border-0"
title="Analyse des Ventes"
data-testid="iframe-sales-analysis"
allow="fullscreen"
/>
</div>
);
}
+20 -3
View File
@@ -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 (
<div className="flex-1 flex items-center justify-center">
<Card className="w-96">
@@ -79,7 +82,7 @@ export default function Utilities() {
{/* Content avec onglets */}
<div className="flex-1 p-6 overflow-hidden">
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
<TabsList className="grid w-full grid-cols-6 mb-6">
<TabsList className="grid w-full grid-cols-7 mb-6">
{canManageBackups && (
<TabsTrigger value="backups" className="flex items-center gap-2">
<Database className="w-4 h-4" />
@@ -116,6 +119,12 @@ export default function Utilities() {
Exécution SQL
</TabsTrigger>
)}
{canManageSalesAnalysis && (
<TabsTrigger value="salesanalysis" className="flex items-center gap-2">
<TrendingUp className="w-4 h-4" />
Analyse Vente
</TabsTrigger>
)}
</TabsList>
{/* Contenu des onglets */}
@@ -166,6 +175,14 @@ export default function Utilities() {
</div>
</TabsContent>
)}
{canManageSalesAnalysis && (
<TabsContent value="salesanalysis" className="flex-1 overflow-hidden">
<div className="h-full bg-gray-50 -m-6 p-6">
<SalesAnalysisConfig />
</div>
</TabsContent>
)}
</Tabs>
</div>
</div>
+1
View File
@@ -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
+25 -1
View File
@@ -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
+59
View File
@@ -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<Server> {
}
});
// 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 {
+54
View File
@@ -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<WebhookBapConfig>;
updateWebhookBapConfig(id: number, config: Partial<InsertWebhookBapConfig>): Promise<WebhookBapConfig>;
// Utilities operations
getUtilities(): Promise<Utilities | undefined>;
createUtilities(utilities: InsertUtilities): Promise<Utilities>;
updateUtilities(id: number, utilities: Partial<InsertUtilities>): Promise<Utilities>;
// Reconciliation Comments operations
getReconciliationComments(deliveryId: number): Promise<ReconciliationCommentWithRelations[]>;
getReconciliationCommentById(id: number): Promise<ReconciliationCommentWithRelations | undefined>;
@@ -2812,6 +2820,26 @@ export class DatabaseStorage implements IStorage {
return config;
}
// Utilities operations
async getUtilities(): Promise<Utilities | undefined> {
const [result] = await db.select().from(utilities).limit(1);
return result;
}
async createUtilities(utilitiesData: InsertUtilities): Promise<Utilities> {
const [result] = await db.insert(utilities).values(utilitiesData).returning();
return result;
}
async updateUtilities(id: number, utilitiesData: Partial<InsertUtilities>): Promise<Utilities> {
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<ReconciliationCommentWithRelations[]> {
const comments = await db
@@ -5041,6 +5069,32 @@ export class MemStorage implements IStorage {
};
}
// Utilities operations
async getUtilities(): Promise<Utilities | undefined> {
// MemStorage ne stocke pas les utilities, retourne undefined
return undefined;
}
async createUtilities(utilitiesData: InsertUtilities): Promise<Utilities> {
// 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<InsertUtilities>): Promise<Utilities> {
// 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<Announcement> {
const announcementStorage = getAnnouncementStorage(
+17
View File
@@ -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<typeof insertNocodbConfigSchema>;
export type InsertInvoiceVerificationCache = z.infer<typeof insertInvoiceVerificationCacheSchema>;
export type DatabaseBackup = typeof databaseBackups.$inferSelect;
export type InsertDatabaseBackup = z.infer<typeof insertDatabaseBackupSchema>;
export type Utilities = typeof utilities.$inferSelect;
export type InsertUtilities = z.infer<typeof insertUtilitiesSchema>;
// Types
export type User = typeof users.$inferSelect;