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:
michaelschal committed 2025-10-31 09:03:53 +00:00
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
@@ -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",
+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>
);
}
+82
View File
@@ -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>
);
}
+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>
+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(
+16
View File
@@ -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;