diff --git a/.env.example b/.env.example index 82a1cea..2a01028 100644 --- a/.env.example +++ b/.env.example @@ -21,4 +21,12 @@ POSTGRES_PORT=5434 # Note de sécurité: # Les identifiants sont préconfigurés pour simplifier le déploiement. -# En production, vous pouvez les modifier si nécessaire. \ No newline at end of file +# En production, vous pouvez les modifier si nécessaire. +# Chiffrement des secrets en base (mots de passe SMTP, jetons NocoDB). +# Repli sur SESSION_SECRET si absent. ATTENTION : changer cette clé rend +# les secrets déjà chiffrés illisibles. +ENCRYPTION_KEY= + +# Route de secours POST /api/emergency-admin-reset : désactivée si vide. +# Ne définir qu'en cas de besoin, puis retirer. +EMERGENCY_SECRET= diff --git a/.gitignore b/.gitignore index f9ba7f8..3a35e07 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,13 @@ dist .DS_Store server/public vite.config.ts.* -*.tar.gz \ No newline at end of file +*.tar.gz + +# Secrets et fichiers de session : ne jamais committer +.env +.env.local +cookie*.txt + +# Artefacts de debug locaux +*.log +scratch/ diff --git a/Dockerfile b/Dockerfile index 3939f79..aa61863 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ RUN ls -la server/ # Build the application # Build frontend first -RUN npx vite build +RUN NODE_ENV=production npx vite build # Vérifier que les fichiers sont construits RUN echo "=== BUILD VERIFICATION ===" && \ @@ -61,7 +61,8 @@ RUN npx esbuild server/index.production.ts \ --external:express-rate-limit \ --external:memoizee \ --external:nanoid \ - --external:date-fns + --external:date-fns \ + --external:nodemailer # Production stage FROM node:20-alpine AS production diff --git a/MIGRATION_PRODUCTION_URGENTE.sql b/MIGRATION_PRODUCTION_URGENTE.sql deleted file mode 100644 index ceea0a6..0000000 --- a/MIGRATION_PRODUCTION_URGENTE.sql +++ /dev/null @@ -1,24 +0,0 @@ --- MIGRATION URGENTE PRODUCTION DLC --- Exécutez cette commande directement sur votre serveur - --- Ajout des colonnes DLC stock épuisé -ALTER TABLE dlc_products -ADD COLUMN IF NOT EXISTS stock_epuise boolean DEFAULT false NOT NULL, -ADD COLUMN IF NOT EXISTS stock_epuise_by varchar(255), -ADD COLUMN IF NOT EXISTS stock_epuise_at timestamp; - --- Index pour performance -CREATE INDEX IF NOT EXISTS idx_dlc_products_stock_epuise ON dlc_products(stock_epuise); - --- Initialisation des valeurs -UPDATE dlc_products -SET stock_epuise = false -WHERE stock_epuise IS NULL; - --- Vérification -SELECT - 'Migration DLC terminée' as status, - COUNT(*) as colonnes_ajoutees -FROM information_schema.columns -WHERE table_name = 'dlc_products' -AND column_name IN ('stock_epuise', 'stock_epuise_by', 'stock_epuise_at'); \ No newline at end of file diff --git a/after_load.png b/after_load.png deleted file mode 100644 index 9e3f72f..0000000 Binary files a/after_load.png and /dev/null differ diff --git a/attached_assets/Pasted--Images-Networks-Volumes-Events-Host-Administration-User-related-Environment-relat-1762270133118_1762270133118.txt b/attached_assets/Pasted--Images-Networks-Volumes-Events-Host-Administration-User-related-Environment-relat-1762270133118_1762270133118.txt deleted file mode 100644 index 80b9c7c..0000000 --- a/attached_assets/Pasted--Images-Networks-Volumes-Events-Host-Administration-User-related-Environment-relat-1762270133118_1762270133118.txt +++ /dev/null @@ -1,245 +0,0 @@ - -Images -Networks -Volumes -Events - -Host - -Administration - -User-related - - -Environment-related - -Registries - -Logs - -Notifications - -Settings - -New version available 2.33.3 -DismissSee what's new -Community Edition -2.27.6 LTS -Containers>logiflow-logiflow-1>Logs -Container logs - - -admin -Log viewer settings -Auto-refresh logs -Wrap lines -Display timestamps -Fetch -All logs -Search -Filter... -Lines -100 -Actions - - - - - storeId: 2, - - createdBy: 'admin', - - createdAt: 2025-09-24T11:26:13.521Z - - }, - - { - - id: 17, - - title: 'Module Avoir', - - content: "Le module d'avoir est en place", - - type: 'error', - - storeId: 1, - - createdBy: 'admin', - - createdAt: 2025-09-09T07:47:47.650Z - - }, - - { - - id: 16, - - title: 'Mise à jour Prix', - - content: 'Merci de mettre vos prix a jour tous les matins, certain magasin ne le font pas.', - - type: 'success', - - storeId: null, - - createdBy: 'admin', - - createdAt: 2025-08-18T10:45:08.585Z - - }, - - { - - id: 8, - - title: 'Bon de Livraison', - - content: 'Lorsque vous rentrez un numéro de bon de livraison, faite le a l identique, même les majuscules. Pour My candy Shop, prendre le numéro de commande.', - - type: 'info', - - storeId: null, - - createdBy: 'admin_local', - - createdAt: 2025-08-07T11:04:38.123Z - - } - -] - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin_local - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Found - -❌ [PRODUCTION] User not found with username: admin_local - -🔍 [PRODUCTION] User search result: Not found - -🎯 [PRODUCTION] Found announcements: 4 - -GET /api/announcements 304 in 18ms - -📢 [SERVER] Fetching announcements for user: _1754936203292 environment: production - -📢 [SERVER] User found: { username: 'ff292', role: 'employee' } - -🎯 [PRODUCTION] Using PostgreSQL DASHBOARD_MESSAGES table - -GET /api/groups 304 in 4ms - -🔍 [PRODUCTION] Raw messages from DB: 4 items: [ - - { - - id: 20, - - title: 'DLC', - - content: 'Attention des DLC sont en expiré bientôt ou expiré, il faut s en occuper', - - type: 'success', - - storeId: 2, - - createdBy: 'admin', - - createdAt: 2025-09-24T11:26:13.521Z - - }, - - { - - id: 17, - - title: 'Module Avoir', - - content: "Le module d'avoir est en place", - - type: 'error', - - storeId: 1, - - createdBy: 'admin', - - createdAt: 2025-09-09T07:47:47.650Z - - }, - - { - - id: 16, - - title: 'Mise à jour Prix', - - content: 'Merci de mettre vos prix a jour tous les matins, certain magasin ne le font pas.', - - type: 'success', - - storeId: null, - - createdBy: 'admin', - - createdAt: 2025-08-18T10:45:08.585Z - - }, - - { - - id: 8, - - title: 'Bon de Livraison', - - content: 'Lorsque vous rentrez un numéro de bon de livraison, faite le a l identique, même les majuscules. Pour My candy Shop, prendre le numéro de commande.', - - type: 'info', - - storeId: null, - - createdBy: 'admin_local', - - createdAt: 2025-08-07T11:04:38.123Z - - } - -] - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin_local - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Not found - -❌ [PRODUCTION] User not found with username: admin_local - -🎯 [PRODUCTION] Found announcements: 4 - -GET /api/announcements 304 in 8ms - -GET /api/user 304 in 13ms - -GET /api/suppliers 304 in 4ms - -GET /api/dlc-products 304 in 58ms - diff --git a/attached_assets/Pasted--Images-Networks-Volumes-Events-Host-Administration-User-related-Environment-relat-1762270274693_1762270274693.txt b/attached_assets/Pasted--Images-Networks-Volumes-Events-Host-Administration-User-related-Environment-relat-1762270274693_1762270274693.txt deleted file mode 100644 index 80b9c7c..0000000 --- a/attached_assets/Pasted--Images-Networks-Volumes-Events-Host-Administration-User-related-Environment-relat-1762270274693_1762270274693.txt +++ /dev/null @@ -1,245 +0,0 @@ - -Images -Networks -Volumes -Events - -Host - -Administration - -User-related - - -Environment-related - -Registries - -Logs - -Notifications - -Settings - -New version available 2.33.3 -DismissSee what's new -Community Edition -2.27.6 LTS -Containers>logiflow-logiflow-1>Logs -Container logs - - -admin -Log viewer settings -Auto-refresh logs -Wrap lines -Display timestamps -Fetch -All logs -Search -Filter... -Lines -100 -Actions - - - - - storeId: 2, - - createdBy: 'admin', - - createdAt: 2025-09-24T11:26:13.521Z - - }, - - { - - id: 17, - - title: 'Module Avoir', - - content: "Le module d'avoir est en place", - - type: 'error', - - storeId: 1, - - createdBy: 'admin', - - createdAt: 2025-09-09T07:47:47.650Z - - }, - - { - - id: 16, - - title: 'Mise à jour Prix', - - content: 'Merci de mettre vos prix a jour tous les matins, certain magasin ne le font pas.', - - type: 'success', - - storeId: null, - - createdBy: 'admin', - - createdAt: 2025-08-18T10:45:08.585Z - - }, - - { - - id: 8, - - title: 'Bon de Livraison', - - content: 'Lorsque vous rentrez un numéro de bon de livraison, faite le a l identique, même les majuscules. Pour My candy Shop, prendre le numéro de commande.', - - type: 'info', - - storeId: null, - - createdBy: 'admin_local', - - createdAt: 2025-08-07T11:04:38.123Z - - } - -] - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin_local - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Found - -❌ [PRODUCTION] User not found with username: admin_local - -🔍 [PRODUCTION] User search result: Not found - -🎯 [PRODUCTION] Found announcements: 4 - -GET /api/announcements 304 in 18ms - -📢 [SERVER] Fetching announcements for user: _1754936203292 environment: production - -📢 [SERVER] User found: { username: 'ff292', role: 'employee' } - -🎯 [PRODUCTION] Using PostgreSQL DASHBOARD_MESSAGES table - -GET /api/groups 304 in 4ms - -🔍 [PRODUCTION] Raw messages from DB: 4 items: [ - - { - - id: 20, - - title: 'DLC', - - content: 'Attention des DLC sont en expiré bientôt ou expiré, il faut s en occuper', - - type: 'success', - - storeId: 2, - - createdBy: 'admin', - - createdAt: 2025-09-24T11:26:13.521Z - - }, - - { - - id: 17, - - title: 'Module Avoir', - - content: "Le module d'avoir est en place", - - type: 'error', - - storeId: 1, - - createdBy: 'admin', - - createdAt: 2025-09-09T07:47:47.650Z - - }, - - { - - id: 16, - - title: 'Mise à jour Prix', - - content: 'Merci de mettre vos prix a jour tous les matins, certain magasin ne le font pas.', - - type: 'success', - - storeId: null, - - createdBy: 'admin', - - createdAt: 2025-08-18T10:45:08.585Z - - }, - - { - - id: 8, - - title: 'Bon de Livraison', - - content: 'Lorsque vous rentrez un numéro de bon de livraison, faite le a l identique, même les majuscules. Pour My candy Shop, prendre le numéro de commande.', - - type: 'info', - - storeId: null, - - createdBy: 'admin_local', - - createdAt: 2025-08-07T11:04:38.123Z - - } - -] - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin_local - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Not found - -❌ [PRODUCTION] User not found with username: admin_local - -🎯 [PRODUCTION] Found announcements: 4 - -GET /api/announcements 304 in 8ms - -GET /api/user 304 in 13ms - -GET /api/suppliers 304 in 4ms - -GET /api/dlc-products 304 in 58ms - diff --git a/attached_assets/Pasted--storeId-2-createdBy-admin-createdAt-2025-09-24T11-26-13-521Z--1762269307429_1762269307429.txt b/attached_assets/Pasted--storeId-2-createdBy-admin-createdAt-2025-09-24T11-26-13-521Z--1762269307429_1762269307429.txt deleted file mode 100644 index 26f1410..0000000 --- a/attached_assets/Pasted--storeId-2-createdBy-admin-createdAt-2025-09-24T11-26-13-521Z--1762269307429_1762269307429.txt +++ /dev/null @@ -1,202 +0,0 @@ - - - storeId: 2, - - createdBy: 'admin', - - createdAt: 2025-09-24T11:26:13.521Z - - }, - - { - - id: 17, - - title: 'Module Avoir', - - content: "Le module d'avoir est en place", - - type: 'error', - - storeId: 1, - - createdBy: 'admin', - - createdAt: 2025-09-09T07:47:47.650Z - - }, - - { - - id: 16, - - title: 'Mise à jour Prix', - - content: 'Merci de mettre vos prix a jour tous les matins, certain magasin ne le font pas.', - - type: 'success', - - storeId: null, - - createdBy: 'admin', - - createdAt: 2025-08-18T10:45:08.585Z - - }, - - { - - id: 8, - - title: 'Bon de Livraison', - - content: 'Lorsque vous rentrez un numéro de bon de livraison, faite le a l identique, même les majuscules. Pour My candy Shop, prendre le numéro de commande.', - - type: 'info', - - storeId: null, - - createdBy: 'admin_local', - - createdAt: 2025-08-07T11:04:38.123Z - - } - -] - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin_local - -❌ [PRODUCTION] User not found with username: admin_local - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Not found - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Found - -🎯 [PRODUCTION] Found announcements: 4 - -GET /api/announcements 304 in 8ms - -📢 [SERVER] Fetching announcements for user: _1754936203292 environment: production - -📢 [SERVER] User found: { username: 'ff292', role: 'employee' } - -🎯 [PRODUCTION] Using PostgreSQL DASHBOARD_MESSAGES table - -🔍 [PRODUCTION] Raw messages from DB: 4 items: [ - - { - - id: 20, - - title: 'DLC', - - content: 'Attention des DLC sont en expiré bientôt ou expiré, il faut s en occuper', - - type: 'success', - - storeId: 2, - - createdBy: 'admin', - - createdAt: 2025-09-24T11:26:13.521Z - - }, - - { - - id: 17, - - title: 'Module Avoir', - - content: "Le module d'avoir est en place", - - type: 'error', - - storeId: 1, - - createdBy: 'admin', - - createdAt: 2025-09-09T07:47:47.650Z - - }, - - { - - id: 16, - - title: 'Mise à jour Prix', - - content: 'Merci de mettre vos prix a jour tous les matins, certain magasin ne le font pas.', - - type: 'success', - - storeId: null, - - createdBy: 'admin', - - createdAt: 2025-08-18T10:45:08.585Z - - }, - - { - - id: 8, - - title: 'Bon de Livraison', - - content: 'Lorsque vous rentrez un numéro de bon de livraison, faite le a l identique, même les majuscules. Pour My candy Shop, prendre le numéro de commande.', - - type: 'info', - - storeId: null, - - createdBy: 'admin_local', - - createdAt: 2025-08-07T11:04:38.123Z - - } - -] - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin - -🔍 [PRODUCTION] Looking for user with username: admin_local - -🔍 [PRODUCTION] User search result: Found - -❌ [PRODUCTION] User not found with username: admin_local - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Found - -🔍 [PRODUCTION] User search result: Not found - -🎯 [PRODUCTION] Found announcements: 4 - -GET /api/announcements 304 in 9ms - -GET /api/groups 304 in 3ms - -GET /api/user 304 in 5ms - -GET /api/suppliers 304 in 4ms - -GET /api/dlc-products 304 in 45ms - -HEAD /api/health 200 in 0ms - diff --git a/attached_assets/image_1762267338308.png b/attached_assets/image_1762267338308.png deleted file mode 100644 index 038493e..0000000 Binary files a/attached_assets/image_1762267338308.png and /dev/null differ diff --git a/attached_assets/{053E4C93-A4BA-4106-8530-38501E1EADEE}_1762269064399.png b/attached_assets/{053E4C93-A4BA-4106-8530-38501E1EADEE}_1762269064399.png deleted file mode 100644 index cc88b9a..0000000 Binary files a/attached_assets/{053E4C93-A4BA-4106-8530-38501E1EADEE}_1762269064399.png and /dev/null differ diff --git a/attached_assets/{1665ED95-20B6-4344-8493-11A55C558DA8}_1762270285012.png b/attached_assets/{1665ED95-20B6-4344-8493-11A55C558DA8}_1762270285012.png deleted file mode 100644 index 46caf83..0000000 Binary files a/attached_assets/{1665ED95-20B6-4344-8493-11A55C558DA8}_1762270285012.png and /dev/null differ diff --git a/attached_assets/{20F16595-1D55-42B5-B63C-FBB1B8C893E8}_1762269074780.png b/attached_assets/{20F16595-1D55-42B5-B63C-FBB1B8C893E8}_1762269074780.png deleted file mode 100644 index 41c2842..0000000 Binary files a/attached_assets/{20F16595-1D55-42B5-B63C-FBB1B8C893E8}_1762269074780.png and /dev/null differ diff --git a/attached_assets/{3604C248-7F41-46CC-8972-47C38222F2B0}_1762265360405.png b/attached_assets/{3604C248-7F41-46CC-8972-47C38222F2B0}_1762265360405.png deleted file mode 100644 index 707933a..0000000 Binary files a/attached_assets/{3604C248-7F41-46CC-8972-47C38222F2B0}_1762265360405.png and /dev/null differ diff --git a/attached_assets/{5DD75BD0-365E-402A-99DA-82F24637B27B}_1762270384666.png b/attached_assets/{5DD75BD0-365E-402A-99DA-82F24637B27B}_1762270384666.png deleted file mode 100644 index 2d67c53..0000000 Binary files a/attached_assets/{5DD75BD0-365E-402A-99DA-82F24637B27B}_1762270384666.png and /dev/null differ diff --git a/attached_assets/{76762EAD-952D-467A-A003-D3F763C1B334}_1762267435658.png b/attached_assets/{76762EAD-952D-467A-A003-D3F763C1B334}_1762267435658.png deleted file mode 100644 index 1a81e4c..0000000 Binary files a/attached_assets/{76762EAD-952D-467A-A003-D3F763C1B334}_1762267435658.png and /dev/null differ diff --git a/attached_assets/{9A963AF5-8CA1-4143-8A50-BF6B1320ABA2}_1762263946868.png b/attached_assets/{9A963AF5-8CA1-4143-8A50-BF6B1320ABA2}_1762263946868.png deleted file mode 100644 index 062d432..0000000 Binary files a/attached_assets/{9A963AF5-8CA1-4143-8A50-BF6B1320ABA2}_1762263946868.png and /dev/null differ diff --git a/attached_assets/{B4E9E8F0-2C31-4B0E-AC00-C5EBBDB91FCC}_1762269830356.png b/attached_assets/{B4E9E8F0-2C31-4B0E-AC00-C5EBBDB91FCC}_1762269830356.png deleted file mode 100644 index e3b81f9..0000000 Binary files a/attached_assets/{B4E9E8F0-2C31-4B0E-AC00-C5EBBDB91FCC}_1762269830356.png and /dev/null differ diff --git a/attached_assets/{CA0A7F2F-9BAD-4A42-8B00-2EF6EFED443A}_1762267992011.png b/attached_assets/{CA0A7F2F-9BAD-4A42-8B00-2EF6EFED443A}_1762267992011.png deleted file mode 100644 index 7fea852..0000000 Binary files a/attached_assets/{CA0A7F2F-9BAD-4A42-8B00-2EF6EFED443A}_1762267992011.png and /dev/null differ diff --git a/attached_assets/{CA3DEE31-2D2B-4434-875F-336714E13B38}_1762269295473.png b/attached_assets/{CA3DEE31-2D2B-4434-875F-336714E13B38}_1762269295473.png deleted file mode 100644 index f52b7dd..0000000 Binary files a/attached_assets/{CA3DEE31-2D2B-4434-875F-336714E13B38}_1762269295473.png and /dev/null differ diff --git a/attached_assets/{CACBFFDA-B7C7-4B0D-892C-80A666E41814}_1762266527968.png b/attached_assets/{CACBFFDA-B7C7-4B0D-892C-80A666E41814}_1762266527968.png deleted file mode 100644 index d417411..0000000 Binary files a/attached_assets/{CACBFFDA-B7C7-4B0D-892C-80A666E41814}_1762266527968.png and /dev/null differ diff --git a/attached_assets/{CB80C8CC-D314-4E12-A25E-1169225B0395}_1762268004420.png b/attached_assets/{CB80C8CC-D314-4E12-A25E-1169225B0395}_1762268004420.png deleted file mode 100644 index b6593f5..0000000 Binary files a/attached_assets/{CB80C8CC-D314-4E12-A25E-1169225B0395}_1762268004420.png and /dev/null differ diff --git a/attached_assets/{E2F8586B-F34F-4395-838D-6340E1F407DD}_1762260398088.png b/attached_assets/{E2F8586B-F34F-4395-838D-6340E1F407DD}_1762260398088.png deleted file mode 100644 index 8cdb39e..0000000 Binary files a/attached_assets/{E2F8586B-F34F-4395-838D-6340E1F407DD}_1762260398088.png and /dev/null differ diff --git a/attached_assets/{F1228CA9-8789-481D-9840-D8D872BAF1ED}_1762270116892.png b/attached_assets/{F1228CA9-8789-481D-9840-D8D872BAF1ED}_1762270116892.png deleted file mode 100644 index e609d8a..0000000 Binary files a/attached_assets/{F1228CA9-8789-481D-9840-D8D872BAF1ED}_1762270116892.png and /dev/null differ diff --git a/attached_assets/{F5E72738-3287-4989-AD4F-8A12528BD627}_1762265876680.png b/attached_assets/{F5E72738-3287-4989-AD4F-8A12528BD627}_1762265876680.png deleted file mode 100644 index f61803a..0000000 Binary files a/attached_assets/{F5E72738-3287-4989-AD4F-8A12528BD627}_1762265876680.png and /dev/null differ diff --git a/client/src/components/CustomerOrderDetails.tsx b/client/src/components/CustomerOrderDetails.tsx index 288397d..83715b4 100644 --- a/client/src/components/CustomerOrderDetails.tsx +++ b/client/src/components/CustomerOrderDetails.tsx @@ -210,7 +210,7 @@ export function CustomerOrderDetails({ order }: CustomerOrderDetailsProps) { diff --git a/client/src/components/CustomerOrderForm.tsx b/client/src/components/CustomerOrderForm.tsx index 310c738..04cf76e 100644 --- a/client/src/components/CustomerOrderForm.tsx +++ b/client/src/components/CustomerOrderForm.tsx @@ -42,13 +42,14 @@ const customerOrderFormSchema = z.object({ customerNotified: z.boolean().default(false), notes: z.string().optional(), customerEmail: z.string().optional(), + status: z.string().optional(), }); type CustomerOrderFormData = z.infer; interface CustomerOrderFormProps { order?: CustomerOrderWithRelations; - onSubmit: (data: CustomerOrderFormData) => void; + onSubmit: (data: Record) => void; onCancel: () => void; isLoading?: boolean; } @@ -113,7 +114,7 @@ export function CustomerOrderForm({ quantity: order?.quantity || 1, supplierId: order?.supplierId || 1, status: "En attente de Commande", // Statut fixe - deposit: order?.deposit || 0, + deposit: order?.deposit ? Number(order.deposit) : 0, isPromotionalPrice: order?.isPromotionalPrice || false, customerNotified: order?.customerNotified || false, groupId: order?.groupId || getUserAssignedGroupId() || 1, diff --git a/client/src/components/modals/CreateDeliveryModal.tsx b/client/src/components/modals/CreateDeliveryModal.tsx index 3c8f89b..c87e030 100644 --- a/client/src/components/modals/CreateDeliveryModal.tsx +++ b/client/src/components/modals/CreateDeliveryModal.tsx @@ -273,7 +273,7 @@ export default function CreateDeliveryModal({ <>
{selectedGroup.name} diff --git a/client/src/components/modals/CreateOrderModal.tsx b/client/src/components/modals/CreateOrderModal.tsx index 94d4b8b..7dda54d 100644 --- a/client/src/components/modals/CreateOrderModal.tsx +++ b/client/src/components/modals/CreateOrderModal.tsx @@ -210,7 +210,7 @@ export default function CreateOrderModal({ <>
{selectedGroup.name} diff --git a/client/src/components/modals/EditDeliveryModal.tsx b/client/src/components/modals/EditDeliveryModal.tsx index 0ba0c33..b44fb55 100644 --- a/client/src/components/modals/EditDeliveryModal.tsx +++ b/client/src/components/modals/EditDeliveryModal.tsx @@ -178,7 +178,7 @@ export default function EditDeliveryModal({
{group.name}
diff --git a/client/src/components/modals/EditOrderModal.tsx b/client/src/components/modals/EditOrderModal.tsx index d718e2f..a490da8 100644 --- a/client/src/components/modals/EditOrderModal.tsx +++ b/client/src/components/modals/EditOrderModal.tsx @@ -155,7 +155,7 @@ export default function EditOrderModal({
{group.name}
diff --git a/client/src/hooks/useAuthUnified.ts b/client/src/hooks/useAuthUnified.ts index f282332..4a0e6b6 100644 --- a/client/src/hooks/useAuthUnified.ts +++ b/client/src/hooks/useAuthUnified.ts @@ -137,7 +137,7 @@ export function useAuthUnified() { console.log('🔄 Development refetch result:', { success: result.isSuccess, hasData: !!result.data, - userId: result.data?.id + userId: (result.data as any)?.id }); } return result.data; diff --git a/client/src/lib/dateUtils.ts b/client/src/lib/dateUtils.ts index 4baa836..d449ebb 100644 --- a/client/src/lib/dateUtils.ts +++ b/client/src/lib/dateUtils.ts @@ -1,4 +1,4 @@ -import { format } from "date-fns"; +import { format, type Locale } from "date-fns"; import { fr } from "date-fns/locale"; /** diff --git a/client/src/lib/supplierMail.ts b/client/src/lib/supplierMail.ts deleted file mode 100644 index 7df5d97..0000000 --- a/client/src/lib/supplierMail.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { safeFormat } from "@/lib/dateUtils"; - -/** - * Génération des emails de relance fournisseur (rapprochement BL/Factures). - * Le lien mailto ouvre le client de messagerie par défaut du poste (Outlook) - * avec le destinataire, l'objet et le corps du message déjà préremplis. - */ - -export interface SupplierMailDelivery { - blNumber?: string | null; - blAmount?: string | number | null; - invoiceReference?: string | null; - scheduledDate?: string | Date | null; - deliveredDate?: string | Date | null; - supplier?: { - name?: string | null; - email?: string | null; - contact?: string | null; - } | null; - group?: { name?: string | null } | null; -} - -/** - * Objet du mail : identifie la livraison concernée. - */ -export function buildSupplierMailSubject(delivery: SupplierMailDelivery): string { - const deliveryDate = delivery.deliveredDate || delivery.scheduledDate; - const formattedDate = deliveryDate - ? safeFormat(deliveryDate, "dd/MM/yyyy", { defaultValue: "" }) - : ""; - - const reference = delivery.blNumber - ? `BL n° ${delivery.blNumber}` - : formattedDate - ? `Livraison du ${formattedDate}` - : "Livraison"; - - const parts = [ - "Demande de facture (PDF) ou BL (Excel)", - delivery.blNumber && formattedDate ? `${reference} du ${formattedDate}` : reference, - ]; - - if (delivery.group?.name) { - parts.push(delivery.group.name); - } - - return parts.join(" - "); -} - -/** - * Corps du mail : rappel des documents attendus + récapitulatif de la livraison. - * Seules les informations réellement renseignées sont listées. - */ -export function buildSupplierMailBody(delivery: SupplierMailDelivery): string { - const deliveryDate = delivery.deliveredDate || delivery.scheduledDate; - const formattedDate = deliveryDate - ? safeFormat(deliveryDate, "dd/MM/yyyy", { defaultValue: "" }) - : ""; - - const blAmount = - delivery.blAmount !== null && delivery.blAmount !== undefined && delivery.blAmount !== "" - ? parseFloat(String(delivery.blAmount)) - : null; - - const details: string[] = []; - if (delivery.supplier?.name) details.push(`- Fournisseur : ${delivery.supplier.name}`); - if (delivery.group?.name) details.push(`- Magasin : ${delivery.group.name}`); - if (formattedDate) details.push(`- Date de livraison : ${formattedDate}`); - if (delivery.blNumber) details.push(`- N° de BL : ${delivery.blNumber}`); - if (blAmount !== null && !isNaN(blAmount)) { - details.push(`- Montant du BL : ${blAmount.toFixed(2)} €`); - } - if (delivery.invoiceReference) { - details.push(`- Référence facture : ${delivery.invoiceReference}`); - } - - const lines = [ - "Bonjour,", - "", - "Dans le cadre du rapprochement de nos bons de livraison et de vos factures, nous vous remercions de bien vouloir nous transmettre :", - "", - "- la facture au format PDF, ou", - "- le bon de livraison au format Excel", - "", - "Livraison concernée :", - ...(details.length > 0 ? details : ["- (informations de livraison à préciser)"]), - "", - "Vous en remerciant par avance.", - "", - // Le corps s'arrête ici : l'enseigne, le magasin et le logo sont apportés - // par la signature configurée dans Outlook sur le poste. - "Cordialement,", - ]; - - return lines.join("\n"); -} - -/** - * Construit le lien mailto complet (destinataire + objet + corps préremplis). - */ -export function buildSupplierMailtoUrl( - delivery: SupplierMailDelivery, - supplierEmail: string -): string { - const subject = buildSupplierMailSubject(delivery); - // RFC 6068 : les sauts de ligne du corps doivent être encodés en CRLF (%0D%0A), - // sinon Outlook peut coller les lignes les unes aux autres. - const body = buildSupplierMailBody(delivery).replace(/\n/g, "\r\n"); - - return `mailto:${supplierEmail.trim()}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; -} - -/** - * Ouvre le client de messagerie par défaut (Outlook sur les postes du magasin). - */ -export function openMailClient(mailtoUrl: string): void { - const link = document.createElement("a"); - link.href = mailtoUrl; - link.style.display = "none"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -} diff --git a/client/src/pages/Avoirs_backup.tsx b/client/src/pages/Avoirs_backup.tsx deleted file mode 100644 index abc78c8..0000000 --- a/client/src/pages/Avoirs_backup.tsx +++ /dev/null @@ -1 +0,0 @@ -// Backup temporaire pour restaurer si nécessaire \ No newline at end of file diff --git a/client/src/pages/BLReconciliation.tsx b/client/src/pages/BLReconciliation.tsx index 995da70..83c0c62 100644 --- a/client/src/pages/BLReconciliation.tsx +++ b/client/src/pages/BLReconciliation.tsx @@ -19,7 +19,6 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import ReconciliationComments from "@/components/ReconciliationComments"; import ReconciliationModal from "@/components/modals/ReconciliationModal"; -import { buildSupplierMailtoUrl, openMailClient } from "@/lib/supplierMail"; export default function BLReconciliation() { const { user } = useAuthUnified(); @@ -68,6 +67,8 @@ export default function BLReconciliation() { // État pour le système de vérification de facture const [verificationResults, setVerificationResults] = useState>({}); const [verifyingDeliveries, setVerifyingDeliveries] = useState>(new Set()); + // Livraisons dont le mail fournisseur est en cours d'envoi + const [sendingMailDeliveries, setSendingMailDeliveries] = useState>(new Set()); // État pour le modal de commentaire const [showCommentModal, setShowCommentModal] = useState(false); @@ -78,6 +79,34 @@ export default function BLReconciliation() { queryKey: ['/api/suppliers'], }); + // Historique des relances fournisseurs : dernier envoi affiché sur l'icône mail + const { data: supplierMailLogs = [] } = useQuery({ + queryKey: ['/api/supplier-mail-logs', selectedStoreId], + queryFn: async () => { + const params = new URLSearchParams(); + if (selectedStoreId && (user?.role === 'admin' || user?.role === 'directeur')) { + params.append('storeId', selectedStoreId.toString()); + } + const response = await fetch(`/api/supplier-mail-logs?${params.toString()}`, { + credentials: 'include' + }); + if (!response.ok) throw new Error('Failed to fetch supplier mail logs'); + return response.json(); + }, + enabled: !!user, + }); + + // Dernier envoi réussi par livraison (les logs arrivent triés du plus récent au plus ancien) + const lastMailByDelivery = React.useMemo(() => { + const map = new Map(); + for (const log of supplierMailLogs) { + if (log.status === 'sent' && !map.has(log.deliveryId)) { + map.set(log.deliveryId, log); + } + } + return map; + }, [supplierMailLogs]); + // --------------------------------------------------------------------------- // File d'attente des vérifications de facture // @@ -121,52 +150,31 @@ export default function BLReconciliation() { // Auto-remplissage si facture trouvée (référence facture OU numéro BL) if (result.exists) { - console.log('🔍 DEBUG - Résultat complet de vérification:', { - deliveryId: variables.deliveryId, - exists: result.exists, - matchType: result.matchType, - invoiceReference: result.invoiceReference, - invoiceAmount: result.invoiceAmount, - dueDate: result.dueDate, - hasInvoiceRef: !!result.invoiceReference, - hasAmount: result.invoiceAmount !== undefined && result.invoiceAmount !== null, - hasDueDate: !!result.dueDate - }); - - // Auto-remplir les champs dans la livraison via API const updateData: any = {}; - + // Ajouter la référence de facture SEULEMENT si trouvée via BL (pas déjà renseignée) if (result.invoiceReference && result.matchType === 'bl_number') { updateData.invoiceReference = result.invoiceReference; - console.log('✅ Ajout invoiceReference:', result.invoiceReference); - } else { - console.log('⚠️ Pas d\'ajout invoiceReference:', { hasRef: !!result.invoiceReference, matchType: result.matchType }); } - + // TOUJOURS mettre à jour le montant si disponible (peu importe le matchType) if (result.invoiceAmount !== undefined && result.invoiceAmount !== null) { updateData.invoiceAmount = result.invoiceAmount; - console.log('✅ Ajout invoiceAmount:', result.invoiceAmount); - } else { - console.log('⚠️ Pas d\'ajout invoiceAmount:', { amount: result.invoiceAmount }); } - + // TOUJOURS mettre à jour la date d'échéance si disponible (peu importe le matchType) if (result.dueDate) { updateData.dueDate = result.dueDate; - console.log('✅ Ajout dueDate:', result.dueDate); - } else { - console.log('⚠️ Pas d\'ajout dueDate:', { dueDate: result.dueDate }); } - - console.log('📝 Données finales à sauvegarder:', { deliveryId: variables.deliveryId, updateData, matchType: result.matchType }); - + + if (import.meta.env.DEV) { + console.log('📝 Auto-remplissage:', { deliveryId: variables.deliveryId, updateData, matchType: result.matchType }); + } + // Ne faire l'appel que si on a des données à mettre à jour if (Object.keys(updateData).length > 0) { apiRequest(`/api/deliveries/${variables.deliveryId}`, "PUT", updateData) .then(() => { - console.log('✅ Données sauvegardées avec succès'); // Invalidation différée : sinon chaque auto-remplissage relance un // refetch complet de la liste, qui relance l'effet, qui relance des // vérifications... jusqu'à saturer le navigateur. @@ -175,8 +183,6 @@ export default function BLReconciliation() { .catch((error) => { console.error('❌ Erreur auto-remplissage:', error); }); - } else { - console.log('⚠️ Aucune donnée à sauvegarder'); } } @@ -676,8 +682,36 @@ export default function BLReconciliation() { return supplier?.email ? String(supplier.email).trim() : ''; }; - // Ouvre Outlook (client mail par défaut) avec un message prérempli - // demandant la facture au format PDF ou le BL au format Excel + // Envoi de la demande de facture (PDF) / BL (Excel) au fournisseur. + // Le mail part du serveur SMTP configuré sur la fiche du magasin. + const sendSupplierMailMutation = useMutation({ + mutationFn: async (delivery: any) => { + return await apiRequest(`/api/deliveries/${delivery.id}/send-supplier-mail`, 'POST'); + }, + onSuccess: (result: any) => { + toast({ + title: "Mail envoyé", + description: `Demande envoyée à ${result?.supplierName || 'le fournisseur'} (${result?.sentTo})`, + }); + // Rafraîchir l'historique pour marquer la ligne comme relancée + queryClient.invalidateQueries({ queryKey: ['/api/supplier-mail-logs'] }); + }, + onError: (error: any) => { + toast({ + title: "Envoi impossible", + description: error?.message || "Le mail n'a pas pu être envoyé", + variant: "destructive", + }); + }, + onSettled: (_result, _error, delivery: any) => { + setSendingMailDeliveries(prev => { + const next = new Set(prev); + next.delete(delivery.id); + return next; + }); + }, + }); + const handleRequestDocumentsByEmail = (delivery: any) => { const supplierEmail = getSupplierEmail(delivery); @@ -690,8 +724,12 @@ export default function BLReconciliation() { return; } - const mailtoUrl = buildSupplierMailtoUrl(delivery, supplierEmail); - openMailClient(mailtoUrl); + // Envoi direct : on bloque la ligne le temps de la requête pour éviter + // qu'un double clic ne déclenche deux mails au même fournisseur. + if (sendingMailDeliveries.has(delivery.id)) return; + + setSendingMailDeliveries(prev => new Set(prev).add(delivery.id)); + sendSupplierMailMutation.mutate(delivery); }; const handleQuickValidate = async (delivery: any) => { @@ -1132,23 +1170,36 @@ export default function BLReconciliation() {
{(() => { const supplierEmail = getSupplierEmail(delivery); + const isSending = sendingMailDeliveries.has(delivery.id); + const lastMail = lastMailByDelivery.get(delivery.id); return ( ); })()} @@ -1462,21 +1513,34 @@ export default function BLReconciliation() {
{(() => { const supplierEmail = getSupplierEmail(delivery); + const isSending = sendingMailDeliveries.has(delivery.id); + const lastMail = lastMailByDelivery.get(delivery.id); return ( ); })()} diff --git a/client/src/pages/BLReconciliationNative.tsx b/client/src/pages/BLReconciliationNative.tsx deleted file mode 100644 index 9c7bf4e..0000000 --- a/client/src/pages/BLReconciliationNative.tsx +++ /dev/null @@ -1,778 +0,0 @@ -import { useState, useEffect } from "react"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { safeFormat } from "@/lib/dateUtils"; -import { useToast } from "@/hooks/use-toast"; -import { apiRequest } from "@/lib/queryClient"; -import { useStore } from "@/contexts/StoreContext"; -import { useAuthUnified } from "@/hooks/useAuthUnified"; -import { usePermissions } from "@shared/permissions"; -import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, ChevronLeft, ChevronRight } from "lucide-react"; -import type { Supplier } from "@shared/schema"; - -export default function BLReconciliation() { - const { user } = useAuthUnified(); - const { selectedStoreId } = useStore(); - const { toast } = useToast(); - const queryClient = useQueryClient(); - const permissions = usePermissions(user?.role); - - // Vérification des permissions d'accès au module - if (!permissions.canView('reconciliation')) { - return ( -
-
-
-
- -
-
-

- Accès restreint
- Seuls les directeurs et administrateurs peuvent accéder au module de rapprochement BL/Factures. -

-
-
-
-
- ); - } - - const [activeTab, setActiveTab] = useState("manual"); - const [searchTerm, setSearchTerm] = useState(""); - const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 20; - - // Récupérer les fournisseurs pour la logique automatique - const { data: suppliers = [], isLoading: suppliersLoading } = useQuery({ - queryKey: ['/api/suppliers'], - }); - - // Récupérer les livraisons validées avec BL - const { data: deliveriesWithBL = [], isLoading } = useQuery({ - queryKey: ['/api/deliveries/bl', selectedStoreId], - queryFn: async () => { - const params = new URLSearchParams({}); - if (selectedStoreId && user?.role === 'admin') { - params.append('storeId', selectedStoreId.toString()); - } - - const response = await fetch(`/api/deliveries?${params.toString()}`, { - credentials: 'include' - }); - - if (!response.ok) { - throw new Error('Failed to fetch deliveries'); - } - - const deliveries = await response.json(); - const filtered = Array.isArray(deliveries) ? deliveries.filter((d: any) => d.status === 'delivered') : []; - - return filtered.sort((a: any, b: any) => new Date(b.deliveredDate || b.updatedAt).getTime() - new Date(a.deliveredDate || a.updatedAt).getTime()); - }, - }); - - // Séparer les livraisons selon le mode de rapprochement du fournisseur - const manualReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => { - const supplier = suppliers.find((s: any) => s.id === delivery.supplierId); - return !supplier?.automaticReconciliation; - }); - - const automaticReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => { - const supplier = suppliers.find((s: any) => s.id === delivery.supplierId); - return supplier?.automaticReconciliation; - }); - - // Mutations pour les actions - const validateManualMutation = useMutation({ - mutationFn: async (deliveryId: number) => { - return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", { - reconciled: true, - validatedAt: new Date().toISOString() - }); - }, - onSuccess: () => { - toast({ - title: "Succès", - description: "Rapprochement validé avec succès", - }); - queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); - }, - onError: () => { - toast({ - title: "Erreur", - description: "Impossible de valider le rapprochement", - variant: "destructive", - }); - } - }); - - const devalidateMutation = useMutation({ - mutationFn: async (deliveryId: number) => { - return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", { - reconciled: false, - validatedAt: null - }); - }, - onSuccess: () => { - toast({ - title: "Succès", - description: "Rapprochement dévalidé avec succès", - }); - queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); - }, - onError: () => { - toast({ - title: "Erreur", - description: "Impossible de dévalider le rapprochement", - variant: "destructive", - }); - } - }); - - const deleteMutation = useMutation({ - mutationFn: async (deliveryId: number) => { - return await apiRequest(`/api/deliveries/${deliveryId}`, "DELETE"); - }, - onSuccess: () => { - toast({ - title: "Succès", - description: "Ligne supprimée avec succès", - }); - queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); - }, - onError: () => { - toast({ - title: "Erreur", - description: "Impossible de supprimer la ligne", - variant: "destructive", - }); - } - }); - - // Fonctions de gestion des actions - const handleValidateManual = (deliveryId: number) => { - if (!permissions.canValidate('reconciliation')) { - toast({ - title: "Accès refusé", - description: "Vous n'avez pas les permissions pour valider les rapprochements", - variant: "destructive", - }); - return; - } - validateManualMutation.mutate(deliveryId); - }; - - const handleDevalidate = (deliveryId: number) => { - if (!permissions.canEdit('reconciliation')) { - toast({ - title: "Accès refusé", - description: "Seuls les administrateurs peuvent dévalider les rapprochements", - variant: "destructive", - }); - return; - } - if (confirm("Êtes-vous sûr de vouloir dévalider ce rapprochement ?")) { - devalidateMutation.mutate(deliveryId); - } - }; - - const handleDelete = (deliveryId: number) => { - if (!permissions.canDelete('reconciliation')) { - toast({ - title: "Accès refusé", - description: "Seuls les administrateurs peuvent supprimer les lignes", - variant: "destructive", - }); - return; - } - if (confirm("Êtes-vous sûr de vouloir supprimer cette ligne ? Cette action est irréversible.")) { - deleteMutation.mutate(deliveryId); - } - }; - - // Filtrage des livraisons par recherche - const filterDeliveries = (deliveries: any[]) => { - return deliveries.filter((delivery: any) => { - const searchLower = searchTerm.toLowerCase(); - return ( - delivery.supplier?.name?.toLowerCase().includes(searchLower) || - delivery.blNumber?.toLowerCase().includes(searchLower) || - delivery.invoiceReference?.toLowerCase().includes(searchLower) - ); - }); - }; - - const filteredManualDeliveries = filterDeliveries(manualReconciliationDeliveries); - const filteredAutomaticDeliveries = filterDeliveries(automaticReconciliationDeliveries); - - // Pagination - const getCurrentPageData = (data: any[]) => { - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - return data.slice(startIndex, endIndex); - }; - - const getTotalPages = (data: any[]) => { - return Math.ceil(data.length / itemsPerPage); - }; - - const currentManualData = getCurrentPageData(filteredManualDeliveries); - const currentAutomaticData = getCurrentPageData(filteredAutomaticDeliveries); - - const totalManualPages = getTotalPages(filteredManualDeliveries); - const totalAutomaticPages = getTotalPages(filteredAutomaticDeliveries); - - // Reset page when changing tabs or searching - useEffect(() => { - setCurrentPage(1); - }, [activeTab, searchTerm]); - - // Vérifier si une ligne est dévalidée (pour la coloration) - const isDevalidated = (delivery: any) => { - return delivery.reconciled === false && delivery.validatedAt === null; - }; - - // Composant de pagination simplifiée - const NativePagination = ({ currentPage, totalPages, onPageChange, totalItems, itemsPerPage }: { - currentPage: number; - totalPages: number; - onPageChange: (page: number) => void; - totalItems: number; - itemsPerPage: number; - }) => { - if (totalPages <= 1) return null; - - const startItem = (currentPage - 1) * itemsPerPage + 1; - const endItem = Math.min(currentPage * itemsPerPage, totalItems); - - return ( -
-
- - Affichage de {startItem} à {endItem} sur {totalItems} éléments - -
-
- - - - Page {currentPage} sur {totalPages} - - - -
-
- ); - }; - - // Badge native - const NativeBadge = ({ children, variant = "default", className = "" }: { children: React.ReactNode, variant?: string, className?: string }) => { - const baseClasses = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"; - const variantClasses = { - default: "bg-blue-100 text-blue-800", - secondary: "bg-gray-100 text-gray-800", - destructive: "bg-red-100 text-red-800", - outline: "border border-gray-300 text-gray-700 bg-white" - }; - - return ( - - {children} - - ); - }; - - // Button native - const NativeButton = ({ children, onClick, disabled = false, variant = "default", size = "default", className = "", title = "" }: { - children: React.ReactNode, - onClick?: () => void, - disabled?: boolean, - variant?: string, - size?: string, - className?: string, - title?: string - }) => { - const baseClasses = "inline-flex items-center justify-center font-medium rounded transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2"; - const variantClasses = { - default: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", - ghost: "text-gray-600 hover:text-gray-900 hover:bg-gray-100", - outline: "border border-gray-300 bg-white text-gray-700 hover:bg-gray-50" - }; - const sizeClasses = { - default: "px-4 py-2 text-sm", - sm: "px-2 py-1 text-xs" - }; - - return ( - - ); - }; - - if (isLoading || suppliersLoading) { - return ( -
-
-
-
-
-
- ); - } - - return ( -
- {/* Header avec onglets */} -
-
-
-

- - Rapprochement BL/Factures -

-

- Gestion des rapprochements manuels et automatiques -

-
-
- - {manualReconciliationDeliveries.length} rapprochements manuels - - - {automaticReconciliationDeliveries.length} rapprochements automatiques - -
-
- - {/* Onglets natifs */} -
- -
-
- - {/* Filtre de recherche */} -
-
- - setSearchTerm(e.target.value)} - className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" - /> -
-
- - {/* Contenu des onglets */} - {activeTab === "manual" && ( -
- {filteredManualDeliveries.length === 0 ? ( -
- -

- Aucun rapprochement manuel trouvé -

-

- Les livraisons de fournisseurs en mode manuel apparaîtront ici. -

-
- ) : ( -
- {/* Pagination du haut */} - - -
- - - - - - - - - - - - - - - {currentManualData.map((delivery: any) => ( - - - - - - - - - - - ))} - -
FournisseurN° BLDate Livr.Montant BLRef. FactureMontant Fact.StatutActions
-
- {delivery.supplier?.name} -
-
-
- {delivery.blNumber || ( - Non renseigné - )} -
-
-
- {safeFormat(delivery.scheduledDate, 'dd/MM/yy')} -
-
-
- {delivery.blAmount ? - `${parseFloat(delivery.blAmount).toFixed(2)}€` : - Non renseigné - } -
-
-
- {delivery.invoiceReference || ( - Non renseigné - )} -
-
-
- {delivery.invoiceAmount ? - `${parseFloat(delivery.invoiceAmount).toFixed(2)}€` : - Non renseigné - } -
-
- {delivery.reconciled ? ( - - Validé - - ) : isDevalidated(delivery) ? ( - - Dévalidé - - ) : ( - - En attente - - )} - -
- {permissions.canEdit('reconciliation') && ( - - - - )} - - {!delivery.reconciled && permissions.canValidate('reconciliation') && ( - handleValidateManual(delivery.id)} - disabled={validateManualMutation.isPending} - className="text-green-600 hover:text-green-700 p-1" - title="Valider" - > - - - )} - - {delivery.reconciled && permissions.canEdit('reconciliation') && user?.role === 'admin' && ( - handleDevalidate(delivery.id)} - disabled={devalidateMutation.isPending} - className="text-orange-600 hover:text-orange-700 p-1" - title="Dévalider" - > - - - )} - - {permissions.canDelete('reconciliation') && ( - handleDelete(delivery.id)} - disabled={deleteMutation.isPending} - className="text-red-600 hover:text-red-700 p-1" - title="Supprimer" - > - - - )} -
-
-
- - {/* Pagination du bas */} - -
- )} -
- )} - - {activeTab === "automatic" && ( -
- {/* Message d'information */} -
-
- -
-

Mode rapprochement automatique

-

- Les livraisons de fournisseurs en mode automatique sont validées automatiquement lorsqu'elles ont le statut "delivered" et un numéro de BL. - {permissions.canEdit('reconciliation') ? ( - " Vous pouvez dévalider ces rapprochements si nécessaire." - ) : ( - " Seuls les directeurs et administrateurs peuvent modifier ces rapprochements." - )} -

-
-
-
- - {filteredAutomaticDeliveries.length === 0 ? ( -
- -

- Aucun rapprochement automatique trouvé -

-

- Les livraisons de fournisseurs en mode automatique apparaîtront ici. -

-
- ) : ( -
- {/* Pagination du haut */} - - -
- - - - - - - - - - - - - - - {currentAutomaticData.map((delivery: any) => ( - - - - - - - - - - - ))} - -
- Fournisseur - AUTO - N° BLDate Livr.Date Valid.Montant BLRef. FactureStatutActions
-
-
- {delivery.supplier?.name} -
-
-
-
-
- {delivery.blNumber || '-'} -
-
-
- {safeFormat(delivery.scheduledDate, 'dd/MM/yy')} -
-
-
- {delivery.reconciled && delivery.validatedAt ? - safeFormat(delivery.validatedAt, 'dd/MM/yy') : - '-' - } -
-
-
- {delivery.blAmount ? - `${parseFloat(delivery.blAmount).toFixed(2)}€` : - '-' - } -
-
-
- {delivery.invoiceReference || '-'} -
-
- {isDevalidated(delivery) ? ( - - Dévalidé - - ) : ( - - Auto-validé - - )} - -
- {isDevalidated(delivery) && permissions.canEdit('reconciliation') && ( - <> - - - - handleValidateManual(delivery.id)} - disabled={validateManualMutation.isPending} - className="text-green-600 hover:text-green-700 p-1" - title="Valider" - > - - - - )} - - {!isDevalidated(delivery) && permissions.canEdit('reconciliation') && ( - handleDevalidate(delivery.id)} - disabled={devalidateMutation.isPending} - className="text-orange-600 hover:text-orange-700 p-1" - title="Dévalider" - > - - - )} - - {permissions.canDelete('reconciliation') && ( - handleDelete(delivery.id)} - disabled={deleteMutation.isPending} - className="text-red-600 hover:text-red-700 p-1" - title="Supprimer" - > - - - )} -
-
-
- - {/* Pagination du bas */} - -
- )} -
- )} -
- ); -} \ No newline at end of file diff --git a/client/src/pages/DlcPage.tsx b/client/src/pages/DlcPage.tsx index 07beb77..ce2810f 100644 --- a/client/src/pages/DlcPage.tsx +++ b/client/src/pages/DlcPage.tsx @@ -61,7 +61,7 @@ export default function DlcPage() { }, [searchTerm]); // Fetch stores/groups - optimized cache - const { data: stores = [] } = useQuery({ + const { data: stores = [] } = useQuery({ queryKey: ["/api/groups"], enabled: !authLoading, staleTime: 5 * 60 * 1000, // 5 minutes cache @@ -348,18 +348,18 @@ export default function DlcPage() { console.log("🏪 DLC GroupId Selection DEBUG:", { userRole: user?.role, selectedStoreId, - userGroups: user?.userGroups?.map(ug => ({groupId: ug.groupId, groupName: ug.group?.name})), - availableStores: stores.map(s => ({id: s.id, name: s.name})), + userGroups: user?.userGroups?.map((ug: any) => ({groupId: ug.groupId, groupName: ug.group?.name})), + availableStores: stores.map((s: any) => ({id: s.id, name: s.name})), userGroupsRaw: user?.userGroups, firstUserGroup: user?.userGroups?.[0], finalGroupId: groupId, logicPath: !groupId ? 'need-fallback' : 'already-set' }); - const dlcData: InsertDlcProduct = { + const dlcData: any = { ...data, name: data.productName, // Copier productName vers name (requis dans la DB) - expiryDate: dlcDate, + expiryDate: dlcDate as any, quantity: 1, // Valeur par défaut unit: "unité", // Valeur par défaut location: "Magasin", // Valeur par défaut @@ -377,7 +377,7 @@ export default function DlcPage() { const handleEdit = (product: DlcProductWithRelations) => { setEditingProduct(product); form.reset({ - productName: product.productName, + productName: product.productName as string, gencode: product.gencode || "", dlcDate: product.expiryDate ? format(new Date(product.expiryDate), "yyyy-MM-dd") : "", dateType: product.dateType as "dlc" | "ddm" | "dluo", @@ -526,7 +526,7 @@ export default function DlcPage() { - ${expiringSoon.map(product => { + ${expiringSoon.map((product: any) => { const today = new Date(); const expiry = new Date(product.dlcDate || new Date()); const diffDays = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); @@ -585,7 +585,7 @@ export default function DlcPage() { - ${expired.map(product => { + ${expired.map((product: any) => { const today = new Date(); const expiry = new Date(product.dlcDate || new Date()); const diffDays = Math.ceil((today.getTime() - expiry.getTime()) / (1000 * 60 * 60 * 24)); @@ -620,14 +620,14 @@ export default function DlcPage() { // Memoized calculations for print functions const { expiringSoonProducts, expiredProducts } = useMemo(() => { const today = new Date(); - const expiringSoon = filteredProducts.filter(product => { + const expiringSoon = filteredProducts.filter((product: any) => { if (product.status === 'valides') return false; const expiryDate = new Date(product.dlcDate || new Date()); const diffDays = Math.ceil((expiryDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); return diffDays <= 15 && diffDays > 0; }); - const expired = filteredProducts.filter(product => { + const expired = filteredProducts.filter((product: any) => { if (product.status === 'valides') return false; const expiryDate = new Date(product.dlcDate || new Date()); return expiryDate <= today; diff --git a/client/src/pages/Groups.tsx b/client/src/pages/Groups.tsx index 8b80e95..63bc373 100644 --- a/client/src/pages/Groups.tsx +++ b/client/src/pages/Groups.tsx @@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { useAuthUnified } from "@/hooks/useAuthUnified"; import { useToast } from "@/hooks/use-toast"; @@ -33,6 +34,38 @@ const colorOptions = [ { value: '#00796B', label: 'Sarcelle' }, ]; +// Taille maximale du logo magasin : il est encodé en base64 dans la fiche +// et joint à chaque mail, un fichier lourd alourdirait tous les envois. +const MAX_LOGO_KB = 200; + +// Valeurs par défaut du formulaire magasin (création et réinitialisations) +const EMPTY_GROUP_FORM = { + name: "", + color: "#1976D2", + nocodbConfigId: "", + nocodbTableName: "", + invoiceColumnName: "", + nocodbBlColumnName: "", + nocodbAmountColumnName: "", + nocodbInvoiceAmountTTCColumnName: "", + nocodbSupplierColumnName: "", + nocodbDueDateColumnName: "", + webhookUrl: "", + // Coordonnées reprises dans la signature des mails fournisseurs + address: "", + phone: "", + logo: "", + // Configuration SMTP propre au magasin + smtpEnabled: false, + smtpHost: "", + smtpPort: "", + smtpSecure: false, + smtpUser: "", + smtpPassword: "", + smtpSenderEmail: "", + smtpSenderName: "", +}; + export default function Groups() { const { user } = useAuthUnified(); const { toast } = useToast(); @@ -42,19 +75,7 @@ export default function Groups() { const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [selectedGroup, setSelectedGroup] = useState(null); - const [formData, setFormData] = useState({ - name: "", - color: "#1976D2", - nocodbConfigId: "", - nocodbTableName: "", - invoiceColumnName: "", - nocodbBlColumnName: "", - nocodbAmountColumnName: "", - nocodbInvoiceAmountTTCColumnName: "", - nocodbSupplierColumnName: "", - nocodbDueDateColumnName: "", - webhookUrl: "", - }); + const [formData, setFormData] = useState({ ...EMPTY_GROUP_FORM }); const { data: groups = [], isLoading } = useQuery({ queryKey: ['/api/groups'], @@ -66,16 +87,6 @@ export default function Groups() { // Protection renforcée contre les erreurs TypeError const nocodbConfigs = Array.isArray(rawNocodbConfigs) ? rawNocodbConfigs : []; - - console.log('🔍 Groups NocoDB Debug:', { - rawNocodbConfigs, - rawType: typeof rawNocodbConfigs, - nocodbConfigs, - configsType: typeof nocodbConfigs, - isArray: Array.isArray(rawNocodbConfigs), - length: nocodbConfigs.length, - environment: window.location.hostname - }); // Comptages agrégés en base : la page n'affiche que des totaux, inutile de // rapatrier l'historique complet des commandes et des livraisons. @@ -97,19 +108,7 @@ export default function Groups() { }); queryClient.invalidateQueries({ queryKey: ['/api/groups'] }); setShowCreateModal(false); - setFormData({ - name: "", - color: "#1976D2", - nocodbConfigId: "", - nocodbTableName: "", - invoiceColumnName: "", - nocodbBlColumnName: "", - nocodbAmountColumnName: "", - nocodbInvoiceAmountTTCColumnName: "", - nocodbSupplierColumnName: "", - nocodbDueDateColumnName: "", - webhookUrl: "", - }); + setFormData({ ...EMPTY_GROUP_FORM }); }, onError: (error) => { if (isUnauthorizedError(error)) { @@ -143,19 +142,7 @@ export default function Groups() { queryClient.invalidateQueries({ queryKey: ['/api/groups'] }); setShowEditModal(false); setSelectedGroup(null); - setFormData({ - name: "", - color: "#1976D2", - nocodbConfigId: "", - nocodbTableName: "", - invoiceColumnName: "", - nocodbBlColumnName: "", - nocodbAmountColumnName: "", - nocodbInvoiceAmountTTCColumnName: "", - nocodbSupplierColumnName: "", - nocodbDueDateColumnName: "", - webhookUrl: "", - }); + setFormData({ ...EMPTY_GROUP_FORM }); }, onError: (error) => { if (isUnauthorizedError(error)) { @@ -177,6 +164,53 @@ export default function Groups() { }, }); + // Test de la connexion SMTP du magasin (aucun message envoyé) + const testSmtpMutation = useMutation({ + mutationFn: async (groupId: number) => { + return await apiRequest(`/api/groups/${groupId}/test-smtp`, 'POST'); + }, + onSuccess: (result: any) => { + toast({ + title: "Connexion réussie", + description: result?.message || "Le serveur SMTP répond correctement", + }); + }, + onError: (error: any) => { + toast({ + title: "Échec de la connexion", + description: error?.message || "Impossible de joindre le serveur SMTP", + variant: "destructive", + }); + }, + }); + + // Lecture du logo en data URI : stocké tel quel sur la fiche magasin + const handleLogoChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + if (file.size > MAX_LOGO_KB * 1024) { + toast({ + title: "Logo trop volumineux", + description: `Le fichier fait ${Math.round(file.size / 1024)} Ko. Maximum autorisé : ${MAX_LOGO_KB} Ko.`, + variant: "destructive", + }); + e.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = () => handleChange('logo', String(reader.result || '')); + reader.onerror = () => { + toast({ + title: "Lecture impossible", + description: "Le fichier n'a pas pu être lu", + variant: "destructive", + }); + }; + reader.readAsDataURL(file); + }; + const deleteMutation = useMutation({ mutationFn: async (id: number) => { await apiRequest(`/api/groups/${id}`, "DELETE"); @@ -225,19 +259,7 @@ export default function Groups() { }; const handleCreate = () => { - setFormData({ - name: "", - color: "#1976D2", - nocodbConfigId: "", - nocodbTableName: "", - invoiceColumnName: "", - nocodbBlColumnName: "", - nocodbAmountColumnName: "", - nocodbInvoiceAmountTTCColumnName: "", - nocodbSupplierColumnName: "", - nocodbDueDateColumnName: "", - webhookUrl: "", - }); + setFormData({ ...EMPTY_GROUP_FORM }); setShowCreateModal(true); }; @@ -255,6 +277,18 @@ export default function Groups() { nocodbSupplierColumnName: group.nocodbSupplierColumnName || "", nocodbDueDateColumnName: group.nocodbDueDateColumnName || "", webhookUrl: group.webhookUrl || "", + address: (group as any).address || "", + phone: (group as any).phone || "", + logo: (group as any).logo || "", + smtpEnabled: Boolean((group as any).smtpEnabled), + smtpHost: (group as any).smtpHost || "", + smtpPort: (group as any).smtpPort?.toString() || "", + smtpSecure: Boolean((group as any).smtpSecure), + smtpUser: (group as any).smtpUser || "", + // Jamais renvoyé par l'API : vide = mot de passe inchangé + smtpPassword: "", + smtpSenderEmail: (group as any).smtpSenderEmail || "", + smtpSenderName: (group as any).smtpSenderName || "", }); setShowEditModal(true); }; @@ -289,11 +323,17 @@ export default function Groups() { } // Prepare data with proper type conversion - const submitData = { + const submitData: any = { ...formData, nocodbConfigId: formData.nocodbConfigId ? parseInt(formData.nocodbConfigId) : null, + smtpPort: formData.smtpPort ? parseInt(formData.smtpPort) : null, }; + // Mot de passe laissé vide = on conserve celui déjà enregistré + if (!formData.smtpPassword) { + delete submitData.smtpPassword; + } + if (selectedGroup) { updateMutation.mutate(submitData); } else { @@ -301,7 +341,7 @@ export default function Groups() { } }; - const handleChange = (field: string, value: string) => { + const handleChange = (field: string, value: string | boolean) => { setFormData(prev => ({ ...prev, [field]: value })); }; @@ -384,7 +424,7 @@ export default function Groups() {
@@ -419,7 +459,7 @@ export default function Groups() {
{group.color}
@@ -463,14 +503,7 @@ export default function Groups() { setShowCreateModal(false); setShowEditModal(false); setSelectedGroup(null); - setFormData({ - name: "", - color: "#1976D2", - nocodbConfigId: "", - nocodbTableId: "", - nocodbTableName: "", - invoiceColumnName: "Ref Facture", - }); + setFormData({ ...EMPTY_GROUP_FORM, invoiceColumnName: "Ref Facture" }); }}> @@ -683,27 +716,215 @@ export default function Groups() { )}
+ {/* Section Coordonnées du magasin (signature des mails) */} +
+
+
+

Coordonnées du magasin

+
+

+ Reprises dans la signature des mails envoyés aux fournisseurs. +

+ +
+ + - - - -
- -
- - - - \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 097d1d1..3537c42 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,9 +34,7 @@ "vite/client", "express", "react", - "react-dom", - "node-fetch", - "date-fns" + "react-dom" ], "paths": { "@/*": [ diff --git a/urgent_production_fix.sh b/urgent_production_fix.sh deleted file mode 100755 index 8796a6e..0000000 --- a/urgent_production_fix.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash - -# Script de correction urgente pour la production -# Applique la migration DLC immédiatement - -set -e - -echo "🚨 CORRECTION URGENTE - Migration DLC Production" -echo "================================================" - -# Vérifier que Docker Compose est disponible -if ! command -v docker-compose &> /dev/null; then - echo "❌ docker-compose non trouvé" - exit 1 -fi - -# Vérifier que le conteneur PostgreSQL est en cours d'exécution -if ! docker-compose ps | grep -q logiflow-db; then - echo "❌ Conteneur logiflow-db non trouvé" - echo "Containers disponibles:" - docker-compose ps - exit 1 -fi - -echo "✅ Conteneur PostgreSQL trouvé" - -# Appliquer la migration DLC -echo "🔄 Application de la migration DLC..." - -docker-compose exec -T logiflow-db psql -U logiflow_admin -d logiflow_db << 'EOF' --- Migration DLC Stock Épuisé -ALTER TABLE dlc_products -ADD COLUMN IF NOT EXISTS stock_epuise boolean DEFAULT false NOT NULL, -ADD COLUMN IF NOT EXISTS stock_epuise_by varchar(255), -ADD COLUMN IF NOT EXISTS stock_epuise_at timestamp; - --- Index pour performance -CREATE INDEX IF NOT EXISTS idx_dlc_products_stock_epuise ON dlc_products(stock_epuise); - --- Vérification -SELECT 'Migration DLC appliquée avec succès' as status; - --- Afficher les colonnes ajoutées -SELECT column_name, data_type -FROM information_schema.columns -WHERE table_name = 'dlc_products' -AND column_name LIKE '%stock_epuise%' -ORDER BY column_name; -EOF - -if [ $? -eq 0 ]; then - echo "✅ Migration DLC appliquée avec succès" -else - echo "❌ Erreur lors de la migration" - exit 1 -fi - -# Redémarrer l'application -echo "🔄 Redémarrage de l'application..." -docker-compose restart logiflow - -if [ $? -eq 0 ]; then - echo "✅ Application redémarrée" -else - echo "❌ Erreur lors du redémarrage" - exit 1 -fi - -echo "" -echo "🎉 CORRECTION TERMINÉE" -echo "L'erreur 'column does not exist' devrait maintenant être résolue" -echo "" -echo "Vérifiez dans les logs que l'API /api/dlc-products fonctionne maintenant" \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index ce3e3d1..e82e051 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,13 +20,32 @@ export default defineConfig({ alias: { "@": path.resolve(import.meta.dirname, "client", "src"), "@shared": path.resolve(import.meta.dirname, "shared"), - "@assets": path.resolve(import.meta.dirname, "attached_assets"), }, }, + // En production, les console.log/debug de debug sont retirés du bundle par + // le minifieur (console.warn et console.error sont conservés). En dev, tout + // reste visible. + esbuild: { + pure: process.env.NODE_ENV === "production" ? ["console.log", "console.debug"] : [], + }, root: path.resolve(import.meta.dirname, "client"), build: { outDir: path.resolve(import.meta.dirname, "dist/public"), emptyOutDir: true, + rollupOptions: { + output: { + // Sépare les grosses dépendances stables du code applicatif : + // elles ne changent qu'aux montées de version et restent donc en + // cache navigateur entre deux déploiements, au lieu de faire + // retélécharger 1,7 Mo à chaque mise en production. + manualChunks: { + "vendor-react": ["react", "react-dom", "wouter"], + "vendor-query": ["@tanstack/react-query"], + "vendor-charts": ["recharts"], + "vendor-icons": ["lucide-react"], + }, + }, + }, }, server: { fs: {