mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-12 01:37:40 +02:00
Created a checkpoint
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 1e4433b5-47ab-464c-b663-fea2e53367dd Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 0a1b1aa1-ca7d-4f74-8ecb-8057b6feb3f6 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/1e4433b5-47ab-464c-b663-fea2e53367dd/7RN8LqE
This commit is contained in:
1 parent
345a2d968f
commit
0cf817e03f
18 files changed
+658
-33310
No files matched your search
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
+292
-226
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",
|
||||
|
||||
@@ -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,82 @@
|
||||
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="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="bg-white shadow-sm border-b border-gray-200 p-6">
|
||||
<div className="flex items-center">
|
||||
<TrendingUp className="w-8 h-8 text-blue-600 mr-3" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
Analyse des Ventes
|
||||
</h2>
|
||||
<p className="text-gray-600 text-sm">
|
||||
{salesAnalysisUrl}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Iframe Content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<iframe
|
||||
src={salesAnalysisUrl}
|
||||
className="w-full h-full border-0"
|
||||
title="Analyse des Ventes"
|
||||
data-testid="iframe-sales-analysis"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -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,26 @@ 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
|
||||
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