mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
22 files changed
+464
-36745
No files matched your search
@@ -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} />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
{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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user