diff --git a/.replit b/.replit
index baa3edd..2e1a3ae 100644
--- a/.replit
+++ b/.replit
@@ -37,4 +37,4 @@ args = "npm run dev"
waitForPort = 5000
[agent]
-integrations = ["javascript_object_storage==1.0.0", "javascript_log_in_with_replit==1.0.0", "javascript_database==1.0.0"]
+integrations = ["javascript_object_storage==1.0.0", "javascript_database==1.0.0", "javascript_log_in_with_replit==1.0.0"]
diff --git a/attached_assets/{E841CD9A-C6E4-4C7A-A17D-CA1CF9DBA375}_1756904852244.png b/attached_assets/{E841CD9A-C6E4-4C7A-A17D-CA1CF9DBA375}_1756904852244.png
new file mode 100644
index 0000000..501edd4
Binary files /dev/null and b/attached_assets/{E841CD9A-C6E4-4C7A-A17D-CA1CF9DBA375}_1756904852244.png differ
diff --git a/client/src/pages/Utilities.tsx b/client/src/pages/Utilities.tsx
index f46a5c6..d2c0a62 100644
--- a/client/src/pages/Utilities.tsx
+++ b/client/src/pages/Utilities.tsx
@@ -6,7 +6,8 @@ import {
Database,
Server,
Bug,
- Cloud
+ Cloud,
+ Send
} from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -17,6 +18,7 @@ import BackupManager from "./BackupManager";
import NocoDBConfig from "./NocoDBConfig";
import DatabaseDebug from "./DatabaseDebug";
import WeatherSettings from "./WeatherSettings";
+import WebhookBAPConfig from "./WebhookBAPConfig";
export default function Utilities() {
const { user } = useAuthUnified();
@@ -35,9 +37,10 @@ export default function Utilities() {
const canManageNocoDB = user?.role === 'admin';
const canDebugDatabase = user?.role === 'admin';
const canManageWeather = user?.role === 'admin';
+ const canManageWebhookBAP = user?.role === 'admin';
// Si l'utilisateur n'a aucune permission, afficher le message d'accès refusé
- if (!canManageBackups && !canManageNocoDB && !canDebugDatabase && !canManageWeather) {
+ if (!canManageBackups && !canManageNocoDB && !canDebugDatabase && !canManageWeather && !canManageWebhookBAP) {
return (
@@ -73,7 +76,7 @@ export default function Utilities() {
{/* Content avec onglets */}
-
+
{canManageBackups && (
@@ -98,6 +101,12 @@ export default function Utilities() {
Météo
)}
+ {canManageWebhookBAP && (
+
+
+ Configuration BAP
+
+ )}
{/* Contenu des onglets */}
@@ -132,6 +141,14 @@ export default function Utilities() {
)}
+
+ {canManageWebhookBAP && (
+
+
+
+
+
+ )}
diff --git a/client/src/pages/WebhookBAPConfig.tsx b/client/src/pages/WebhookBAPConfig.tsx
new file mode 100644
index 0000000..e58ca96
--- /dev/null
+++ b/client/src/pages/WebhookBAPConfig.tsx
@@ -0,0 +1,289 @@
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import { Textarea } from "@/components/ui/textarea";
+import { useToast } from "@/hooks/use-toast";
+import { Send, Globe, AlertCircle, CheckCircle2, Copy } from "lucide-react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { apiRequest } from "@/lib/queryClient";
+
+interface WebhookBapConfig {
+ id: number;
+ name: string;
+ webhookUrl: string;
+ description: string | null;
+ isActive: boolean;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export default function WebhookBAPConfig() {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+ const [formData, setFormData] = useState({
+ name: "Configuration BAP",
+ webhookUrl: "",
+ description: "",
+ isActive: true
+ });
+
+ const { data: config, isLoading } = useQuery({
+ queryKey: ['/api/webhook-bap-config'],
+ });
+
+ const testMutation = useMutation({
+ mutationFn: async () => {
+ if (!formData.webhookUrl) {
+ throw new Error("URL webhook requise pour le test");
+ }
+
+ const response = await fetch('/api/webhook-bap-config/test', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ webhookUrl: formData.webhookUrl }),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || 'Erreur lors du test');
+ }
+
+ return response.json();
+ },
+ onSuccess: () => {
+ toast({
+ title: "Test réussi",
+ description: "Le webhook BAP répond correctement",
+ });
+ },
+ onError: (error: any) => {
+ toast({
+ title: "Échec du test",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const saveMutation = useMutation({
+ mutationFn: async (data: typeof formData) => {
+ return apiRequest('/api/webhook-bap-config', 'POST', data);
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['/api/webhook-bap-config'] });
+ toast({
+ title: "Configuration sauvegardée",
+ description: "La configuration du webhook BAP a été mise à jour",
+ });
+ },
+ onError: (error: any) => {
+ toast({
+ title: "Erreur de sauvegarde",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ useEffect(() => {
+ if (config) {
+ // Gérer à la fois les cas où l'API retourne un objet ou un array
+ const configData = Array.isArray(config) && config.length > 0 ? config[0] : config;
+ if (configData && typeof configData === 'object') {
+ setFormData({
+ name: configData.name || "Configuration BAP",
+ webhookUrl: configData.webhookUrl || "",
+ description: configData.description || "",
+ isActive: configData.isActive ?? true
+ });
+ }
+ }
+ }, [config]);
+
+ const handleSave = () => {
+ if (!formData.webhookUrl.trim()) {
+ toast({
+ title: "Erreur",
+ description: "L'URL du webhook est requise",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ if (!formData.webhookUrl.startsWith('http')) {
+ toast({
+ title: "Erreur",
+ description: "L'URL doit commencer par http:// ou https://",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ saveMutation.mutate(formData);
+ };
+
+ const handleTest = () => {
+ if (!formData.webhookUrl.trim()) {
+ toast({
+ title: "Erreur",
+ description: "L'URL du webhook est requise pour effectuer le test",
+ variant: "destructive",
+ });
+ return;
+ }
+ testMutation.mutate();
+ };
+
+ const copyUrl = () => {
+ navigator.clipboard.writeText(formData.webhookUrl);
+ toast({
+ title: "URL copiée",
+ description: "L'URL a été copiée dans le presse-papiers",
+ });
+ };
+
+ if (isLoading) {
+ return (
+
+
Chargement de la configuration...
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Configuration Webhook BAP
+
+
+ Configurez l'URL du webhook n8n pour l'envoi des fichiers BAP vers Prissela et Célia
+
+
+
+
+
+ setFormData({ ...formData, name: e.target.value })}
+ placeholder="Configuration BAP"
+ />
+
+
+
+
+
+ setFormData({ ...formData, webhookUrl: e.target.value })}
+ placeholder="https://workflow.ffnancy.fr/webhook/..."
+ className="flex-1"
+ />
+
+
+
+ URL complète du webhook n8n qui recevra les fichiers PDF BAP
+
+
+
+
+
+
+
+
+ setFormData({ ...formData, isActive: checked })}
+ />
+
+
+
+
+
+
+
+
+
+ Test de Connexion
+
+
+ Testez la connectivité avec le webhook n8n configuré
+
+
+
+
+
+
+ {testMutation.isSuccess && (
+
+
+ Connexion réussie
+
+ )}
+
+ {testMutation.isError && (
+
+
+
Échec de la connexion
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/init.sql b/init.sql
index 141a1ea..e28692b 100644
--- a/init.sql
+++ b/init.sql
@@ -380,10 +380,31 @@ CREATE INDEX "idx_sav_ticket_history_ticket_id" ON "sav_ticket_history" USING bt
CREATE INDEX "idx_weather_data_date_year" ON "weather_data" USING btree ("date" text_ops, "is_current_year" bool_ops);
CREATE INDEX "idx_invoice_verification_expires" ON "invoice_verification_cache" USING btree ("expires_at" timestamp_ops);
+-- Table pour configuration webhook BAP
+CREATE TABLE IF NOT EXISTS "webhook_bap_config" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" varchar(100) DEFAULT 'Configuration BAP' NOT NULL,
+ "webhook_url" text NOT NULL,
+ "description" text,
+ "is_active" boolean DEFAULT true NOT NULL,
+ "created_at" timestamp DEFAULT now(),
+ "updated_at" timestamp DEFAULT now()
+);
+
+-- Configuration par défaut webhook BAP
+INSERT INTO "webhook_bap_config" ("name", "webhook_url", "description", "is_active")
+VALUES (
+ 'Configuration BAP',
+ 'https://workflow.ffnancy.fr/webhook/a3d03176-b72f-412d-8fb9-f920b9fbab4d',
+ 'Configuration par défaut pour l''envoi des fichiers BAP vers n8n',
+ true
+) ON CONFLICT DO NOTHING;
+
-- Commentaires sur les tables
COMMENT ON TABLE "announcements" IS 'Admin-managed announcements/information system';
COMMENT ON COLUMN "announcements"."priority" IS 'Priority level: normal, important, urgent';
COMMENT ON COLUMN "announcements"."group_id" IS 'NULL for global announcements, specific group_id for store-specific announcements';
COMMENT ON TABLE "sav_tickets" IS 'Service après-vente tickets management';
COMMENT ON TABLE "weather_data" IS 'Cache for weather API data';
-COMMENT ON TABLE "invoice_verification_cache" IS 'Cache for NocoDB invoice verification results';
\ No newline at end of file
+COMMENT ON TABLE "invoice_verification_cache" IS 'Cache for NocoDB invoice verification results';
+COMMENT ON TABLE "webhook_bap_config" IS 'Configuration pour webhook BAP n8n';
\ No newline at end of file
diff --git a/scripts/create-webhook-bap-table.sql b/scripts/create-webhook-bap-table.sql
new file mode 100644
index 0000000..b15cbb6
--- /dev/null
+++ b/scripts/create-webhook-bap-table.sql
@@ -0,0 +1,24 @@
+-- Script pour créer la table webhook_bap_config en production
+-- À exécuter sur le serveur de production PostgreSQL
+
+CREATE TABLE IF NOT EXISTS webhook_bap_config (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(100) NOT NULL DEFAULT 'Configuration BAP',
+ webhook_url TEXT NOT NULL,
+ description TEXT,
+ is_active BOOLEAN NOT NULL DEFAULT true,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Insérer une configuration par défaut
+INSERT INTO webhook_bap_config (name, webhook_url, description, is_active)
+VALUES (
+ 'Configuration BAP',
+ 'https://workflow.ffnancy.fr/webhook/a3d03176-b72f-412d-8fb9-f920b9fbab4d',
+ 'Configuration par défaut pour l''envoi des fichiers BAP',
+ true
+) ON CONFLICT DO NOTHING;
+
+-- Commande pour exécuter ce script :
+-- psql -U username -d database_name -f scripts/create-webhook-bap-table.sql
\ No newline at end of file
diff --git a/server/routes.ts b/server/routes.ts
index 57a1321..d595668 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -68,6 +68,139 @@ export async function registerRoutes(app: Express): Promise {
// Auth middleware
await setupAuth(app);
+ // Routes pour configuration webhook BAP
+ app.get('/api/webhook-bap-config', isAuthenticated, async (req: any, res) => {
+ try {
+ // Vérifier que l'utilisateur est admin
+ 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') {
+ return res.status(403).json({ error: 'Accès refusé - Admin uniquement' });
+ }
+
+ const config = await storage.getWebhookBapConfig();
+ res.json(config || null);
+
+ } catch (error: any) {
+ console.error('❌ Erreur récupération config webhook BAP:', error);
+ res.status(500).json({ error: 'Erreur serveur', details: error.message });
+ }
+ });
+
+ app.post('/api/webhook-bap-config', isAuthenticated, async (req: any, res) => {
+ try {
+ // Vérifier que l'utilisateur est admin
+ 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') {
+ return res.status(403).json({ error: 'Accès refusé - Admin uniquement' });
+ }
+
+ // Valider les données avec Zod
+ const validatedData = insertWebhookBapConfigSchema.parse(req.body);
+
+ // Vérifier si une configuration existe déjà
+ const existingConfig = await storage.getWebhookBapConfig();
+
+ let config: any;
+ if (existingConfig) {
+ // Mettre à jour la configuration existante
+ config = await storage.updateWebhookBapConfig(existingConfig.id, validatedData);
+ } else {
+ // Créer une nouvelle configuration
+ config = await storage.createWebhookBapConfig(validatedData);
+ }
+
+ console.log('✅ Configuration webhook BAP sauvegardée:', {
+ id: config.id,
+ name: config.name,
+ isActive: config.isActive
+ });
+
+ res.json(config);
+
+ } catch (error: any) {
+ console.error('❌ Erreur sauvegarde config webhook BAP:', error);
+ res.status(500).json({ error: 'Erreur serveur', details: error.message });
+ }
+ });
+
+ app.post('/api/webhook-bap-config/test', isAuthenticated, async (req: any, res) => {
+ try {
+ // Vérifier que l'utilisateur est admin
+ 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') {
+ return res.status(403).json({ error: 'Accès refusé - Admin uniquement' });
+ }
+
+ const { webhookUrl } = req.body;
+ if (!webhookUrl) {
+ return res.status(400).json({ error: 'URL webhook requise' });
+ }
+
+ console.log('🔍 Test webhook BAP:', { url: webhookUrl });
+
+ // Tester la connectivité avec le webhook
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 secondes
+
+ const testResponse = await fetch(webhookUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ test: true,
+ timestamp: new Date().toISOString(),
+ message: 'Test de connectivité depuis LogiFlow'
+ }),
+ signal: controller.signal
+ });
+
+ clearTimeout(timeoutId);
+
+ console.log('🌐 Réponse test webhook:', { status: testResponse.status, ok: testResponse.ok });
+
+ if (!testResponse.ok) {
+ throw new Error(`HTTP ${testResponse.status}: ${testResponse.statusText}`);
+ }
+
+ const result = await testResponse.text();
+
+ res.json({
+ success: true,
+ status: testResponse.status,
+ message: 'Webhook accessible',
+ response: result.substring(0, 200) // Limiter la réponse
+ });
+
+ } catch (error: any) {
+ console.error('❌ Erreur test webhook BAP:', error);
+
+ let errorMessage = 'Échec du test de connectivité';
+ if (error.name === 'AbortError') {
+ errorMessage = 'Timeout - Le webhook ne répond pas';
+ } else if (error.message) {
+ errorMessage = error.message;
+ }
+
+ res.status(500).json({ error: errorMessage, details: error.message });
+ }
+ });
+
// Route BAP pour envoi webhook n8n
app.post('/api/bap/send-webhook', isAuthenticated, async (req: any, res) => {
try {
@@ -143,8 +276,14 @@ export async function registerRoutes(app: Express): Promise {
fileSize: fileBuffer.length
});
- // Envoyer vers le webhook n8n
- const webhookUrl = 'https://workflow.ffnancy.fr/webhook/a3d03176-b72f-412d-8fb9-f920b9fbab4d';
+ // Récupérer l'URL du webhook depuis la configuration
+ const webhookConfig = await storage.getWebhookBapConfig();
+ if (!webhookConfig || !webhookConfig.isActive) {
+ console.error('❌ BAP: Configuration webhook non trouvée ou inactive');
+ return res.status(500).json({ error: 'Configuration webhook BAP non disponible' });
+ }
+
+ const webhookUrl = webhookConfig.webhookUrl;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 secondes
diff --git a/server/storage.ts b/server/storage.ts
index d3625ee..53b6ef6 100644
--- a/server/storage.ts
+++ b/server/storage.ts
@@ -66,6 +66,9 @@ import {
type InsertWeatherData,
type WeatherSettings,
type InsertWeatherSettings,
+ webhookBapConfig,
+ type WebhookBapConfig,
+ type InsertWebhookBapConfig,
} from "@shared/schema";
import { db } from "./db";
import { eq, and, inArray, desc, sql, gte, lte, lt, gt, or, isNull, isNotNull, asc, ne } from "drizzle-orm";
@@ -219,6 +222,11 @@ export interface IStorage {
updateWeatherData(id: number, data: Partial): Promise;
deleteOldWeatherData(daysToKeep: number): Promise;
clearWeatherCache(): Promise;
+
+ // Webhook BAP Configuration
+ getWebhookBapConfig(): Promise;
+ createWebhookBapConfig(config: InsertWebhookBapConfig): Promise;
+ updateWebhookBapConfig(id: number, config: Partial): Promise;
}
export class DatabaseStorage implements IStorage {
@@ -2211,6 +2219,29 @@ export class DatabaseStorage implements IStorage {
await db.delete(weatherData);
console.log('🧹 Weather cache cleared due to location change');
}
+
+ // Webhook BAP Configuration
+ async getWebhookBapConfig(): Promise {
+ const [config] = await db.select().from(webhookBapConfig).limit(1);
+ return config;
+ }
+
+ async createWebhookBapConfig(configData: InsertWebhookBapConfig): Promise {
+ const [config] = await db
+ .insert(webhookBapConfig)
+ .values(configData)
+ .returning();
+ return config;
+ }
+
+ async updateWebhookBapConfig(id: number, configData: Partial): Promise {
+ const [config] = await db
+ .update(webhookBapConfig)
+ .set({ ...configData, updatedAt: new Date() })
+ .where(eq(webhookBapConfig.id, id))
+ .returning();
+ return config;
+ }
}
// MemStorage class for development
@@ -3859,6 +3890,38 @@ export class MemStorage implements IStorage {
console.log('🧹 DEV: Weather cache cleared due to location change');
}
+ // Webhook BAP Configuration
+ async getWebhookBapConfig(): Promise {
+ // En développement, retourner une config par défaut
+ return {
+ id: 1,
+ name: "Configuration BAP",
+ webhookUrl: "https://workflow.ffnancy.fr/webhook/a3d03176-b72f-412d-8fb9-f920b9fbab4d",
+ description: "Configuration par défaut pour développement",
+ isActive: true,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+ }
+
+ async createWebhookBapConfig(config: InsertWebhookBapConfig): Promise {
+ return {
+ id: 1,
+ ...config,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+ }
+
+ async updateWebhookBapConfig(id: number, config: Partial): Promise {
+ const existing = await this.getWebhookBapConfig();
+ return {
+ ...existing!,
+ ...config,
+ updatedAt: new Date()
+ };
+ }
+
// Announcement operations
async createAnnouncement(announcementData: InsertAnnouncement): Promise {
const announcementStorage = getAnnouncementStorage(
diff --git a/shared/schema.ts b/shared/schema.ts
index 6e64e80..c3c6c2f 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -802,3 +802,25 @@ export type WeatherData = typeof weatherData.$inferSelect;
export type InsertWeatherData = z.infer;
export type WeatherSettings = typeof weatherSettings.$inferSelect;
export type InsertWeatherSettings = z.infer;
+
+// Configuration Webhook BAP
+export const webhookBapConfig = pgTable("webhook_bap_config", {
+ id: serial("id").primaryKey(),
+ name: varchar("name", { length: 100 }).notNull().default("Configuration BAP"),
+ webhookUrl: text("webhook_url").notNull(),
+ description: text("description"),
+ isActive: boolean("is_active").notNull().default(true),
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+});
+
+// Webhook BAP Schemas
+export const insertWebhookBapConfigSchema = createInsertSchema(webhookBapConfig).omit({
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+});
+
+// Webhook BAP Types
+export type WebhookBapConfig = typeof webhookBapConfig.$inferSelect;
+export type InsertWebhookBapConfig = z.infer;