chore: remove committed session cookies, debug artifacts and dead code
Repo hygiene pass: - cookie.txt / cookies.txt held real session cookies and must never be committed; .gitignore now blocks them along with .env files - half a megabyte of debug screenshots, one-shot production hotfix scripts (all superseded by the automatic startup migrations), scratch files and the unused attached_assets folder (with its dangling @assets vite alias) - dead code: server/storage-old.ts (unreferenced, 167 of the project's 430 TypeScript errors) and five client pages no route ever imported (BLReconciliationNative, Avoirs_backup, TasksSimplified, TasksListSimple, TasksProductionSimple) TypeScript error count drops from 430 to 261 with no behavior change; the client build is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHdE9nEh8zHhQNGpCgrtYe
No files matched your search
@@ -4,3 +4,12 @@ dist
|
||||
server/public
|
||||
vite.config.ts.*
|
||||
*.tar.gz
|
||||
|
||||
# Secrets et fichiers de session : ne jamais committer
|
||||
.env
|
||||
.env.local
|
||||
cookie*.txt
|
||||
|
||||
# Artefacts de debug locaux
|
||||
*.log
|
||||
scratch/
|
||||
@@ -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');
|
||||
|
Before Width: | Height: | Size: 274 KiB |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 108 KiB |
@@ -1 +0,0 @@
|
||||
// Backup temporaire pour restaurer si nécessaire
|
||||
@@ -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 (
|
||||
<div className="p-6">
|
||||
<div className="bg-orange-50 border-l-4 border-orange-400 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<FileText className="h-5 w-5 text-orange-400" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-orange-700">
|
||||
<strong>Accès restreint</strong><br />
|
||||
Seuls les directeurs et administrateurs peuvent accéder au module de rapprochement BL/Factures.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<Supplier[]>({
|
||||
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 (
|
||||
<div className="flex items-center justify-between px-4 py-3 bg-white border-t border-gray-200">
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<span>
|
||||
Affichage de {startItem} à {endItem} sur {totalItems} éléments
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1 text-sm border border-gray-300 rounded bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
<span>Précédent</span>
|
||||
</button>
|
||||
|
||||
<span className="px-3 py-1 text-sm bg-gray-100 rounded">
|
||||
Page {currentPage} sur {totalPages}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1 text-sm border border-gray-300 rounded bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
|
||||
>
|
||||
<span>Suivant</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<span className={`${baseClasses} ${variantClasses[variant as keyof typeof variantClasses] || variantClasses.default} ${className}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
className={`${baseClasses} ${variantClasses[variant as keyof typeof variantClasses] || variantClasses.default} ${sizeClasses[size as keyof typeof sizeClasses] || sizeClasses.default} ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading || suppliersLoading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/4"></div>
|
||||
<div className="h-64 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header avec onglets */}
|
||||
<div className="bg-white border-b border-gray-200 p-6 shadow-sm -m-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 flex items-center">
|
||||
<FileText className="w-6 h-6 mr-3 text-blue-600" />
|
||||
Rapprochement BL/Factures
|
||||
</h2>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Gestion des rapprochements manuels et automatiques
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<NativeBadge variant="outline" className="text-sm border border-gray-300">
|
||||
{manualReconciliationDeliveries.length} rapprochements manuels
|
||||
</NativeBadge>
|
||||
<NativeBadge variant="outline" className="text-sm border border-gray-300 bg-blue-50">
|
||||
{automaticReconciliationDeliveries.length} rapprochements automatiques
|
||||
</NativeBadge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Onglets natifs */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab("manual")}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 ${
|
||||
activeTab === "manual"
|
||||
? "border-blue-500 text-blue-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
<span>Rapprochement Manuel</span>
|
||||
<NativeBadge variant="secondary" className="ml-2">
|
||||
{manualReconciliationDeliveries.length}
|
||||
</NativeBadge>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("automatic")}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 ${
|
||||
activeTab === "automatic"
|
||||
? "border-blue-500 text-blue-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
<span>Rapprochement Automatique</span>
|
||||
<NativeBadge variant="secondary" className="ml-2">
|
||||
{automaticReconciliationDeliveries.length}
|
||||
</NativeBadge>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtre de recherche */}
|
||||
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
|
||||
<div className="relative max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher par fournisseur, BL ou facture..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenu des onglets */}
|
||||
{activeTab === "manual" && (
|
||||
<div className="space-y-6">
|
||||
{filteredManualDeliveries.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
Aucun rapprochement manuel trouvé
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Les livraisons de fournisseurs en mode manuel apparaîtront ici.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg">
|
||||
{/* Pagination du haut */}
|
||||
<NativePagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalManualPages}
|
||||
onPageChange={setCurrentPage}
|
||||
totalItems={filteredManualDeliveries.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
/>
|
||||
|
||||
<div className="table-container">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fournisseur</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">N° BL</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Livr.</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Montant BL</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ref. Facture</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Montant Fact.</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Statut</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{currentManualData.map((delivery: any) => (
|
||||
<tr
|
||||
key={delivery.id}
|
||||
className={`hover:bg-gray-50 ${
|
||||
delivery.reconciled ? 'bg-green-50' :
|
||||
isDevalidated(delivery) ? 'bg-red-50' : ''
|
||||
}`}
|
||||
>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="font-medium text-gray-900 truncate max-w-32">
|
||||
{delivery.supplier?.name}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{delivery.blNumber || (
|
||||
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="font-medium text-gray-900">
|
||||
{delivery.blAmount ?
|
||||
`${parseFloat(delivery.blAmount).toFixed(2)}€` :
|
||||
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{delivery.invoiceReference || (
|
||||
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{delivery.invoiceAmount ?
|
||||
`${parseFloat(delivery.invoiceAmount).toFixed(2)}€` :
|
||||
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
{delivery.reconciled ? (
|
||||
<NativeBadge variant="default" className="bg-green-600 text-white">
|
||||
Validé
|
||||
</NativeBadge>
|
||||
) : isDevalidated(delivery) ? (
|
||||
<NativeBadge variant="destructive">
|
||||
Dévalidé
|
||||
</NativeBadge>
|
||||
) : (
|
||||
<NativeBadge variant="outline">
|
||||
En attente
|
||||
</NativeBadge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
{permissions.canEdit('reconciliation') && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-gray-600 hover:text-blue-600 p-1"
|
||||
title="Modifier"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
|
||||
{!delivery.reconciled && permissions.canValidate('reconciliation') && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleValidateManual(delivery.id)}
|
||||
disabled={validateManualMutation.isPending}
|
||||
className="text-green-600 hover:text-green-700 p-1"
|
||||
title="Valider"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
|
||||
{delivery.reconciled && permissions.canEdit('reconciliation') && user?.role === 'admin' && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDevalidate(delivery.id)}
|
||||
disabled={devalidateMutation.isPending}
|
||||
className="text-orange-600 hover:text-orange-700 p-1"
|
||||
title="Dévalider"
|
||||
>
|
||||
<Ban className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
|
||||
{permissions.canDelete('reconciliation') && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(delivery.id)}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="text-red-600 hover:text-red-700 p-1"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination du bas */}
|
||||
<NativePagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalManualPages}
|
||||
onPageChange={setCurrentPage}
|
||||
totalItems={filteredManualDeliveries.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "automatic" && (
|
||||
<div className="space-y-6">
|
||||
{/* Message d'information */}
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertTriangle className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-blue-900">Mode rapprochement automatique</h4>
|
||||
<p className="text-sm text-blue-700 mt-1">
|
||||
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."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredAutomaticDeliveries.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Settings className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
Aucun rapprochement automatique trouvé
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Les livraisons de fournisseurs en mode automatique apparaîtront ici.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg">
|
||||
{/* Pagination du haut */}
|
||||
<NativePagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalAutomaticPages}
|
||||
onPageChange={setCurrentPage}
|
||||
totalItems={filteredAutomaticDeliveries.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
/>
|
||||
|
||||
<div className="table-container">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Fournisseur
|
||||
<NativeBadge variant="secondary" className="ml-2 text-xs">AUTO</NativeBadge>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">N° BL</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Livr.</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Valid.</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Montant BL</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ref. Facture</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Statut</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{currentAutomaticData.map((delivery: any) => (
|
||||
<tr
|
||||
key={delivery.id}
|
||||
className={`hover:bg-gray-50 ${
|
||||
isDevalidated(delivery) ? 'bg-red-50' : 'bg-green-50'
|
||||
}`}
|
||||
>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="font-medium text-gray-900 truncate max-w-32">
|
||||
{delivery.supplier?.name}
|
||||
</div>
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-full" title="Mode automatique" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900 font-medium">
|
||||
{delivery.blNumber || '-'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{delivery.reconciled && delivery.validatedAt ?
|
||||
safeFormat(delivery.validatedAt, 'dd/MM/yy') :
|
||||
'-'
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="font-medium text-gray-900">
|
||||
{delivery.blAmount ?
|
||||
`${parseFloat(delivery.blAmount).toFixed(2)}€` :
|
||||
'-'
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="text-gray-900">
|
||||
{delivery.invoiceReference || '-'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
{isDevalidated(delivery) ? (
|
||||
<NativeBadge variant="destructive">
|
||||
Dévalidé
|
||||
</NativeBadge>
|
||||
) : (
|
||||
<NativeBadge variant="default" className="bg-green-600 text-white">
|
||||
Auto-validé
|
||||
</NativeBadge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
{isDevalidated(delivery) && permissions.canEdit('reconciliation') && (
|
||||
<>
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-gray-600 hover:text-blue-600 p-1"
|
||||
title="Modifier"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleValidateManual(delivery.id)}
|
||||
disabled={validateManualMutation.isPending}
|
||||
className="text-green-600 hover:text-green-700 p-1"
|
||||
title="Valider"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isDevalidated(delivery) && permissions.canEdit('reconciliation') && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDevalidate(delivery.id)}
|
||||
disabled={devalidateMutation.isPending}
|
||||
className="text-orange-600 hover:text-orange-700 p-1"
|
||||
title="Dévalider"
|
||||
>
|
||||
<Ban className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
|
||||
{permissions.canDelete('reconciliation') && (
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(delivery.id)}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="text-red-600 hover:text-red-700 p-1"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</NativeButton>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination du bas */}
|
||||
<NativePagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalAutomaticPages}
|
||||
onPageChange={setCurrentPage}
|
||||
totalItems={filteredAutomaticDeliveries.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,568 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
// Interface pour les tâches
|
||||
interface Task {
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
status: 'pending' | 'completed';
|
||||
assignedTo: string;
|
||||
startDate?: string | null;
|
||||
dueDate?: string | null;
|
||||
createdAt?: string;
|
||||
groupId?: number;
|
||||
createdBy?: string;
|
||||
}
|
||||
|
||||
export default function TasksListSimple() {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
|
||||
// Charger les tâches
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/tasks');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTasks(data);
|
||||
} else {
|
||||
console.error('Erreur lors du chargement des tâches:', response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des tâches:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, []);
|
||||
|
||||
// Créer une tâche
|
||||
const createTask = async (formData: FormData) => {
|
||||
const title = formData.get('title') as string;
|
||||
const description = formData.get('description') as string;
|
||||
const priority = formData.get('priority') as string;
|
||||
const status = formData.get('status') as string;
|
||||
const assignedTo = formData.get('assignedTo') as string;
|
||||
const startDate = formData.get('startDate') as string;
|
||||
const dueDate = formData.get('dueDate') as string;
|
||||
|
||||
if (!title?.trim()) {
|
||||
alert("Le titre est requis");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!assignedTo?.trim()) {
|
||||
alert("L'assignation est requise");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const taskData = {
|
||||
title: title.trim(),
|
||||
description: description?.trim() || "",
|
||||
priority: priority || "medium",
|
||||
status: status || "pending",
|
||||
assignedTo: assignedTo.trim(),
|
||||
startDate: startDate || null,
|
||||
dueDate: dueDate || null,
|
||||
groupId: 1,
|
||||
createdBy: 'admin',
|
||||
};
|
||||
|
||||
console.log('📤 Creating task:', taskData);
|
||||
|
||||
const response = await fetch('/api/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(taskData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('❌ Create Error:', { status: response.status, error: errorText });
|
||||
throw new Error(`Erreur ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
alert("Tâche créée avec succès");
|
||||
loadTasks();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création:', error);
|
||||
alert("Erreur lors de la création");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Modifier une tâche
|
||||
const updateTask = async (id: number, formData: FormData) => {
|
||||
const title = formData.get('title') as string;
|
||||
const description = formData.get('description') as string;
|
||||
const priority = formData.get('priority') as string;
|
||||
const status = formData.get('status') as string;
|
||||
const assignedTo = formData.get('assignedTo') as string;
|
||||
const startDate = formData.get('startDate') as string;
|
||||
const dueDate = formData.get('dueDate') as string;
|
||||
|
||||
if (!title?.trim()) {
|
||||
alert("Le titre est requis");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!assignedTo?.trim()) {
|
||||
alert("L'assignation est requise");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Préparer les données exactement comme le serveur les attend
|
||||
const taskData: Partial<Task> = {
|
||||
title: title.trim(),
|
||||
description: description?.trim() || "",
|
||||
priority: (priority as Task['priority']) || "medium",
|
||||
status: (status as Task['status']) || "pending",
|
||||
assignedTo: assignedTo.trim(),
|
||||
};
|
||||
|
||||
// Gestion spéciale des dates pour éviter les erreurs de format
|
||||
if (startDate && startDate.trim() !== '') {
|
||||
taskData.startDate = startDate;
|
||||
} else {
|
||||
taskData.startDate = null;
|
||||
}
|
||||
|
||||
if (dueDate && dueDate.trim() !== '') {
|
||||
taskData.dueDate = dueDate;
|
||||
} else {
|
||||
taskData.dueDate = null;
|
||||
}
|
||||
|
||||
console.log('📤 Updating task:', { id, taskData });
|
||||
|
||||
const response = await fetch(`/api/tasks/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(taskData),
|
||||
});
|
||||
|
||||
console.log('📥 Update Response:', { status: response.status, ok: response.ok });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('❌ Update Error:', { status: response.status, error: errorText });
|
||||
throw new Error(`Erreur ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
alert("Tâche modifiée avec succès");
|
||||
setEditingId(null);
|
||||
loadTasks();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la modification:', error);
|
||||
alert("Erreur lors de la modification");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Filtrer les tâches
|
||||
const filteredTasks = tasks.filter(task => {
|
||||
const matchesSearch = !searchTerm || task.title?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = statusFilter === "all" || task.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
// Formulaire inline
|
||||
const TaskForm = ({ task, onSubmit, onCancel }: {
|
||||
task?: Task;
|
||||
onSubmit: (formData: FormData) => Promise<boolean>;
|
||||
onCancel: () => void;
|
||||
}) => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const success = await onSubmit(formData);
|
||||
if (success && !task) {
|
||||
(e.target as HTMLFormElement).reset();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<tr style={{ backgroundColor: '#f9fafb' }}>
|
||||
<td colSpan={6} style={{ padding: '16px' }}>
|
||||
<form onSubmit={handleSubmit} style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', alignItems: 'end' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
Titre *
|
||||
</label>
|
||||
<input
|
||||
name="title"
|
||||
type="text"
|
||||
defaultValue={task?.title || ""}
|
||||
placeholder="Titre de la tâche"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
name="description"
|
||||
type="text"
|
||||
defaultValue={task?.description || ""}
|
||||
placeholder="Description (optionnel)"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
Priorité
|
||||
</label>
|
||||
<select
|
||||
name="priority"
|
||||
defaultValue={task?.priority || "medium"}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
<option value="low">🟢 Faible</option>
|
||||
<option value="medium">🟡 Moyenne</option>
|
||||
<option value="high">🔴 Élevée</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
Statut
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
defaultValue={task?.status || "pending"}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
<option value="pending">⏳ En cours</option>
|
||||
<option value="completed">✅ Terminée</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
📅 Date de début
|
||||
</label>
|
||||
<input
|
||||
name="startDate"
|
||||
type="date"
|
||||
defaultValue={task?.startDate ? new Date(task.startDate).toISOString().split('T')[0] : ""}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
⏰ Date d'échéance
|
||||
</label>
|
||||
<input
|
||||
name="dueDate"
|
||||
type="date"
|
||||
defaultValue={task?.dueDate ? new Date(task.dueDate).toISOString().split('T')[0] : ""}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '12px', fontWeight: '500', marginBottom: '4px' }}>
|
||||
Assigné à *
|
||||
</label>
|
||||
<input
|
||||
name="assignedTo"
|
||||
type="text"
|
||||
defaultValue={task?.assignedTo || "admin"}
|
||||
placeholder="Nom de la personne"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
{task ? "Modifier" : "Créer"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '24px', textAlign: 'center' }}>
|
||||
<p>Chargement des tâches...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', maxWidth: '1400px', margin: '0 auto' }}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h1 style={{ fontSize: '24px', fontWeight: '600', margin: '0', color: '#1f2937' }}>
|
||||
📋 Gestion des Tâches
|
||||
</h1>
|
||||
<button
|
||||
onClick={() => setShowCreateForm(!showCreateForm)}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: showCreateForm ? '#dc2626' : '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{showCreateForm ? '❌ Annuler' : '➕ Nouvelle Tâche'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filtres */}
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="🔍 Rechercher..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
minWidth: '150px'
|
||||
}}
|
||||
>
|
||||
<option value="all">Tous les statuts</option>
|
||||
<option value="pending">En cours</option>
|
||||
<option value="completed">Terminées</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tableau des tâches */}
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead style={{ backgroundColor: '#f9fafb' }}>
|
||||
<tr>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Titre</th>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Priorité</th>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Statut</th>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Assigné à</th>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Dates</th>
|
||||
<th style={{ padding: '12px 16px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#6b7280', borderBottom: '1px solid #e5e7eb' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{showCreateForm && (
|
||||
<TaskForm
|
||||
onSubmit={createTask}
|
||||
onCancel={() => setShowCreateForm(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{filteredTasks.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ padding: '48px', textAlign: 'center', color: '#6b7280' }}>
|
||||
Aucune tâche trouvée
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredTasks.map((task) => (
|
||||
editingId === task.id ? (
|
||||
<TaskForm
|
||||
key={task.id}
|
||||
task={task}
|
||||
onSubmit={(formData) => updateTask(task.id, formData)}
|
||||
onCancel={() => setEditingId(null)}
|
||||
/>
|
||||
) : (
|
||||
<tr key={task.id} style={{ borderBottom: '1px solid #e5e7eb' }}>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<div>
|
||||
<div style={{
|
||||
fontWeight: '500',
|
||||
color: task.status === 'completed' ? '#6b7280' : '#1f2937',
|
||||
textDecoration: task.status === 'completed' ? 'line-through' : 'none'
|
||||
}}>
|
||||
{task.title}
|
||||
</div>
|
||||
{task.description && (
|
||||
<div style={{ fontSize: '12px', color: '#6b7280', marginTop: '2px' }}>
|
||||
{task.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<span style={{
|
||||
padding: '2px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '500',
|
||||
backgroundColor: task.priority === 'high' ? '#fee2e2' : task.priority === 'medium' ? '#fef3c7' : '#d1fae5',
|
||||
color: task.priority === 'high' ? '#dc2626' : task.priority === 'medium' ? '#d97706' : '#059669'
|
||||
}}>
|
||||
{task.priority === 'high' ? '🔴 Élevée' : task.priority === 'medium' ? '🟡 Moyenne' : '🟢 Faible'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<span style={{
|
||||
padding: '2px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '11px',
|
||||
fontWeight: '500',
|
||||
backgroundColor: task.status === 'completed' ? '#d1fae5' : '#fef3c7',
|
||||
color: task.status === 'completed' ? '#059669' : '#d97706'
|
||||
}}>
|
||||
{task.status === 'completed' ? '✅ Terminée' : '⏳ En cours'}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: '14px', color: '#6b7280' }}>
|
||||
{task.assignedTo}
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px', fontSize: '12px', color: '#6b7280' }}>
|
||||
{task.startDate && (
|
||||
<div>📅 {new Date(task.startDate).toLocaleDateString('fr-FR')}</div>
|
||||
)}
|
||||
{task.dueDate && (
|
||||
<div>⏰ {new Date(task.dueDate).toLocaleDateString('fr-FR')}</div>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '12px 16px' }}>
|
||||
<button
|
||||
onClick={() => setEditingId(task.id)}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
✏️ Modifier
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,546 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
// Version 100% autonome sans aucune dépendance externe
|
||||
export default function TasksProductionSimple() {
|
||||
const [tasks, setTasks] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [selectedTask, setSelectedTask] = useState<any>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
|
||||
// Charger les tâches
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/tasks');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setTasks(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des tâches:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, []);
|
||||
|
||||
// Filtrer les tâches
|
||||
const filteredTasks = tasks.filter(task => {
|
||||
const matchesSearch = task.title?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesStatus = statusFilter === "all" || task.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
// Composant formulaire inline
|
||||
const TaskForm = ({ task, onClose }: any) => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const title = formData.get('title') as string;
|
||||
const description = formData.get('description') as string;
|
||||
const priority = formData.get('priority') as string;
|
||||
const status = formData.get('status') as string;
|
||||
const assignedTo = formData.get('assignedTo') as string;
|
||||
const startDate = formData.get('startDate') as string;
|
||||
const dueDate = formData.get('dueDate') as string;
|
||||
|
||||
if (!title?.trim()) {
|
||||
alert("Le titre est requis");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!assignedTo?.trim()) {
|
||||
alert("L'assignation est requise");
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = e.currentTarget.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = "...";
|
||||
|
||||
try {
|
||||
const taskData = {
|
||||
title: title.trim(),
|
||||
description: description?.trim() || "",
|
||||
priority: priority || "medium",
|
||||
status: status || "pending",
|
||||
assignedTo: assignedTo.trim(),
|
||||
startDate: startDate || null,
|
||||
dueDate: dueDate || null,
|
||||
};
|
||||
|
||||
const url = task ? `/api/tasks/${task.id}` : '/api/tasks';
|
||||
const method = task ? 'PUT' : 'POST';
|
||||
|
||||
if (!task) {
|
||||
(taskData as any).groupId = 1;
|
||||
(taskData as any).createdBy = 'admin';
|
||||
}
|
||||
|
||||
console.log('📤 Sending request:', { url, method, taskData });
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(taskData),
|
||||
});
|
||||
|
||||
console.log('📥 Response:', { status: response.status, ok: response.ok });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('❌ API Error:', { status: response.status, error: errorText });
|
||||
throw new Error(`Erreur ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
alert(task ? "Tâche modifiée avec succès" : "Tâche créée avec succès");
|
||||
onClose();
|
||||
loadTasks(); // Recharger les tâches
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
alert("Erreur lors de l'opération");
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = task ? "Modifier" : "Créer";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', backgroundColor: 'white' }}>
|
||||
<div style={{ borderBottom: '1px solid #e5e7eb', paddingBottom: '16px', marginBottom: '24px' }}>
|
||||
<p style={{ fontSize: '14px', color: '#6b7280', margin: '0' }}>
|
||||
📅 Date de début = Quand la tâche devient visible<br/>
|
||||
⏰ Date d'échéance = Quand la tâche doit être terminée
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
float: 'right',
|
||||
padding: '8px 16px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||
Titre *
|
||||
</label>
|
||||
<input
|
||||
name="title"
|
||||
type="text"
|
||||
defaultValue={task?.title || ""}
|
||||
placeholder="Titre de la tâche"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
defaultValue={task?.description || ""}
|
||||
placeholder="Description de la tâche (optionnel)"
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||
Priorité *
|
||||
</label>
|
||||
<select
|
||||
name="priority"
|
||||
defaultValue={task?.priority || "medium"}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
<option value="low">🟢 Faible</option>
|
||||
<option value="medium">🟡 Moyenne</option>
|
||||
<option value="high">🔴 Élevée</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||
Statut
|
||||
</label>
|
||||
<select
|
||||
name="status"
|
||||
defaultValue={task?.status || "pending"}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
<option value="pending">⏳ En cours</option>
|
||||
<option value="completed">✅ Terminée</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||
📅 Date de début <span style={{ fontSize: '12px', color: '#6b7280' }}>(optionnel)</span>
|
||||
</label>
|
||||
<input
|
||||
name="startDate"
|
||||
type="date"
|
||||
defaultValue={task?.startDate ? new Date(task.startDate).toISOString().split('T')[0] : ""}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||
⏰ Date d'échéance <span style={{ fontSize: '12px', color: '#6b7280' }}>(optionnel)</span>
|
||||
</label>
|
||||
<input
|
||||
name="dueDate"
|
||||
type="date"
|
||||
defaultValue={task?.dueDate ? new Date(task.dueDate).toISOString().split('T')[0] : ""}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||
Assigné à *
|
||||
</label>
|
||||
<input
|
||||
name="assignedTo"
|
||||
type="text"
|
||||
defaultValue={task?.assignedTo || "admin"}
|
||||
placeholder="Nom de la personne assignée"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{task ? "Modifier" : "Créer"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '24px', textAlign: 'center' }}>
|
||||
<p>Chargement des tâches...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
marginBottom: '24px',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h1 style={{ fontSize: '24px', fontWeight: '600', margin: '0', color: '#1f2937' }}>
|
||||
📋 Gestion des Tâches
|
||||
</h1>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
➕ Nouvelle Tâche
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filtres */}
|
||||
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="🔍 Rechercher..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
minWidth: '150px'
|
||||
}}
|
||||
>
|
||||
<option value="all">Tous les statuts</option>
|
||||
<option value="pending">En cours</option>
|
||||
<option value="completed">Terminées</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Liste des tâches */}
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
{filteredTasks.length === 0 ? (
|
||||
<div style={{ padding: '48px', textAlign: 'center', color: '#6b7280' }}>
|
||||
<p>Aucune tâche trouvée</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredTasks.map((task, index) => (
|
||||
<div
|
||||
key={task.id}
|
||||
style={{
|
||||
padding: '16px 24px',
|
||||
borderBottom: index < filteredTasks.length - 1 ? '1px solid #e5e7eb' : 'none',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
<h3 style={{
|
||||
margin: '0 0 8px 0',
|
||||
fontSize: '16px',
|
||||
fontWeight: '500',
|
||||
color: task.status === 'completed' ? '#6b7280' : '#1f2937',
|
||||
textDecoration: task.status === 'completed' ? 'line-through' : 'none'
|
||||
}}>
|
||||
{task.title}
|
||||
</h3>
|
||||
<div style={{ display: 'flex', gap: '16px', fontSize: '14px', color: '#6b7280' }}>
|
||||
<span>
|
||||
Priorité: {task.priority === 'high' ? '🔴 Élevée' : task.priority === 'medium' ? '🟡 Moyenne' : '🟢 Faible'}
|
||||
</span>
|
||||
<span>
|
||||
Statut: {task.status === 'completed' ? '✅ Terminée' : '⏳ En cours'}
|
||||
</span>
|
||||
<span>Assigné à: {task.assignedTo}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTask(task);
|
||||
setShowEditModal(true);
|
||||
}}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
border: '1px solid #d1d5db',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
✏️ Modifier
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal de création */}
|
||||
{showCreateModal && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '600px',
|
||||
width: '90%',
|
||||
maxHeight: '90vh',
|
||||
overflow: 'auto',
|
||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '24px 24px 16px',
|
||||
borderBottom: '1px solid #e5e7eb'
|
||||
}}>
|
||||
<h2 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
margin: '0'
|
||||
}}>
|
||||
Créer une nouvelle tâche
|
||||
</h2>
|
||||
</div>
|
||||
<TaskForm onClose={() => setShowCreateModal(false)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal d'édition */}
|
||||
{showEditModal && selectedTask && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '600px',
|
||||
width: '90%',
|
||||
maxHeight: '90vh',
|
||||
overflow: 'auto',
|
||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '24px 24px 16px',
|
||||
borderBottom: '1px solid #e5e7eb'
|
||||
}}>
|
||||
<h2 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
margin: '0'
|
||||
}}>
|
||||
Modifier la tâche
|
||||
</h2>
|
||||
</div>
|
||||
<TaskForm
|
||||
task={selectedTask}
|
||||
onClose={() => {
|
||||
setShowEditModal(false);
|
||||
setSelectedTask(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,504 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||
import { useStore } from "@/contexts/StoreContext";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import {
|
||||
ListTodo,
|
||||
Plus,
|
||||
Search,
|
||||
Circle,
|
||||
Clock,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Edit,
|
||||
Trash2
|
||||
} from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { Task } from "@shared/schema";
|
||||
|
||||
type TaskWithRelations = Task & {
|
||||
creator: { id: string; username: string; name: string; };
|
||||
group: { id: number; name: string; color: string; };
|
||||
};
|
||||
|
||||
export default function TasksSimplified() {
|
||||
const { user } = useAuthUnified();
|
||||
const { selectedStoreId } = useStore();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// États locaux
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||
const [priorityFilter, setPriorityFilter] = useState<string>("all");
|
||||
|
||||
// Modals et tâche sélectionnée
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [selectedTask, setSelectedTask] = useState<TaskWithRelations | null>(null);
|
||||
|
||||
// Récupération des tâches
|
||||
const { data: tasks = [], isLoading } = useQuery({
|
||||
queryKey: ["/api/tasks", selectedStoreId],
|
||||
queryFn: () => {
|
||||
const params = new URLSearchParams();
|
||||
if (selectedStoreId) {
|
||||
params.append('storeId', selectedStoreId.toString());
|
||||
}
|
||||
return fetch(`/api/tasks?${params.toString()}`).then(res => res.json());
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Récupération des magasins
|
||||
const { data: stores = [] } = useQuery<any[]>({
|
||||
queryKey: ["/api/groups"],
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
// Filtrer les tâches
|
||||
const filteredTasks = tasks.filter((task: TaskWithRelations) => {
|
||||
const matchesSearch = task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
task.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
task.assignedTo?.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesStatus = statusFilter === "all" || task.status === statusFilter;
|
||||
const matchesPriority = priorityFilter === "all" || task.priority === priorityFilter;
|
||||
|
||||
return matchesSearch && matchesStatus && matchesPriority;
|
||||
});
|
||||
|
||||
// Création d'une tâche
|
||||
const handleCreateTask = async (taskData: any) => {
|
||||
try {
|
||||
const finalData = {
|
||||
...taskData,
|
||||
groupId: selectedStoreId ? parseInt(selectedStoreId) : stores[0]?.id
|
||||
};
|
||||
|
||||
console.log('🔍 TasksSimplified - Creating task with data:', {
|
||||
selectedStoreId,
|
||||
storesLength: stores.length,
|
||||
finalGroupId: finalData.groupId,
|
||||
taskData: finalData
|
||||
});
|
||||
|
||||
await apiRequest('/api/tasks', 'POST', finalData);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
setShowCreateModal(false);
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Tâche créée avec succès",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ TasksSimplified - Error creating task:', error);
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de créer la tâche",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Modification d'une tâche
|
||||
const handleUpdateTask = async (taskData: any) => {
|
||||
if (!selectedTask) return;
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/tasks/${selectedTask.id}`, 'PUT', taskData);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
setShowEditModal(false);
|
||||
setSelectedTask(null);
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Tâche modifiée avec succès",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de modifier la tâche",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Complétion d'une tâche
|
||||
const handleCompleteTask = async (taskId: number) => {
|
||||
try {
|
||||
await apiRequest(`/api/tasks/${taskId}/complete`, 'POST');
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Tâche marquée comme terminée",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de compléter la tâche",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Suppression d'une tâche
|
||||
const handleDeleteTask = async (taskId: number) => {
|
||||
try {
|
||||
await apiRequest(`/api/tasks/${taskId}`, 'DELETE');
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Tâche supprimée avec succès",
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de supprimer la tâche",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditTask = (task: TaskWithRelations) => {
|
||||
setSelectedTask(task);
|
||||
setShowEditModal(true);
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed": return "bg-green-100 text-green-800";
|
||||
case "pending": return "bg-yellow-100 text-yellow-800";
|
||||
default: return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case "high": return "bg-red-100 text-red-800";
|
||||
case "medium": return "bg-blue-100 text-blue-800";
|
||||
case "low": return "bg-green-100 text-green-800";
|
||||
default: return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-lg">Chargement des tâches...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-lg">Vous devez être connecté pour accéder aux tâches.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<ListTodo className="h-6 w-6 text-blue-600" />
|
||||
<h1 className="text-2xl font-bold">Gestion des Tâches</h1>
|
||||
</div>
|
||||
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Nouvelle Tâche
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Créer une nouvelle tâche</DialogTitle>
|
||||
</DialogHeader>
|
||||
<TaskForm onSubmit={handleCreateTask} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Filtres */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Filtres</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="Rechercher une tâche..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-[150px]">
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Statut" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Tous les statuts</SelectItem>
|
||||
<SelectItem value="pending">En attente</SelectItem>
|
||||
<SelectItem value="completed">Terminée</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="min-w-[150px]">
|
||||
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Priorité" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toutes priorités</SelectItem>
|
||||
<SelectItem value="high">Haute</SelectItem>
|
||||
<SelectItem value="medium">Moyenne</SelectItem>
|
||||
<SelectItem value="low">Basse</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Liste des tâches */}
|
||||
<div className="space-y-4">
|
||||
{filteredTasks.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center h-32">
|
||||
<div className="text-center">
|
||||
<ListTodo className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500">Aucune tâche trouvée</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
filteredTasks.map((task: TaskWithRelations) => (
|
||||
<Card key={task.id} className={`hover:shadow-md transition-shadow ${task.status === 'completed' ? 'opacity-60 bg-gray-50' : ''}`}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<h3 className={`font-semibold text-lg ${task.status === 'completed' ? 'line-through text-gray-500' : ''}`}>{task.title}</h3>
|
||||
<Badge className={getStatusColor(task.status)}>
|
||||
{task.status === "completed" ? "Terminée" : "En attente"}
|
||||
</Badge>
|
||||
<Badge className={getPriorityColor(task.priority)}>
|
||||
{task.priority === "high" ? "Haute" : task.priority === "medium" ? "Moyenne" : "Basse"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{task.description && (
|
||||
<p className={`mb-3 ${task.status === 'completed' ? 'text-gray-400 line-through' : 'text-gray-600'}`}>{task.description}</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-4 text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="font-medium">Assigné à:</span>
|
||||
<span>{task.assignedTo || "Non assigné"}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="font-medium">Magasin:</span>
|
||||
<span className="px-2 py-1 rounded text-xs" style={{ backgroundColor: task.group.color + '20', color: task.group.color }}>
|
||||
{task.group.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="font-medium">Créé par:</span>
|
||||
<span>{task.creator.name || task.creator.username}</span>
|
||||
</div>
|
||||
|
||||
{task.dueDate && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>
|
||||
Échéance: {format(new Date(task.dueDate), 'dd/MM/yyyy HH:mm', { locale: fr })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
{task.status !== "completed" && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleCompleteTask(task.id)}
|
||||
className="text-green-600 hover:text-green-700"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleEditTask(task)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDeleteTask(task.id)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal de modification */}
|
||||
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Modifier la tâche</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedTask && (
|
||||
<TaskForm
|
||||
initialData={selectedTask}
|
||||
onSubmit={handleUpdateTask}
|
||||
isEditing={true}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Composant de formulaire simplifié
|
||||
function TaskForm({ onSubmit, initialData, isEditing = false }: {
|
||||
onSubmit: (data: any) => void;
|
||||
initialData?: TaskWithRelations;
|
||||
isEditing?: boolean;
|
||||
}) {
|
||||
const [title, setTitle] = useState(initialData?.title || "");
|
||||
const [description, setDescription] = useState(initialData?.description || "");
|
||||
const [assignedTo, setAssignedTo] = useState(initialData?.assignedTo || "");
|
||||
const [priority, setPriority] = useState(initialData?.priority || "medium");
|
||||
const [dueDate, setDueDate] = useState(
|
||||
initialData?.dueDate ? format(new Date(initialData.dueDate), 'yyyy-MM-dd') : ""
|
||||
);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!title.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskData = {
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
assignedTo: assignedTo?.trim() || "Non assigné",
|
||||
priority,
|
||||
startDate: null, // Toujours inclure startDate même si null
|
||||
dueDate: dueDate ? new Date(dueDate).toISOString() : null,
|
||||
};
|
||||
|
||||
onSubmit(taskData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="title" className="block text-sm font-medium mb-1">
|
||||
Titre de la tâche *
|
||||
</label>
|
||||
<Input
|
||||
id="title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Titre de la tâche"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="description" className="block text-sm font-medium mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Description de la tâche"
|
||||
className="w-full p-2 border rounded-md"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="assignedTo" className="block text-sm font-medium mb-1">
|
||||
Assigné à <span className="text-xs text-gray-500">(optionnel)</span>
|
||||
</label>
|
||||
<Input
|
||||
id="assignedTo"
|
||||
value={assignedTo}
|
||||
onChange={(e) => setAssignedTo(e.target.value)}
|
||||
placeholder="Nom de la personne responsable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="priority" className="block text-sm font-medium mb-1">
|
||||
Priorité
|
||||
</label>
|
||||
<Select value={priority} onValueChange={setPriority}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Basse</SelectItem>
|
||||
<SelectItem value="medium">Moyenne</SelectItem>
|
||||
<SelectItem value="high">Haute</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="dueDate" className="block text-sm font-medium mb-1">
|
||||
Date d'échéance
|
||||
</label>
|
||||
<Input
|
||||
id="dueDate"
|
||||
type="date"
|
||||
value={dueDate}
|
||||
onChange={(e) => setDueDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="submit">
|
||||
{isEditing ? "Modifier" : "Créer"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_localhost FALSE / FALSE 1758639574 connect.sid s%3A8_9iOHdXBiypBHfh9CFiGGT7-mgFDQ-9.MPmF2NrBG7ywTs2h9HX4yW1JVNQ5dbq%2FHoZpz8jRTpo
|
||||
@@ -1,5 +0,0 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_localhost FALSE / FALSE 1755861696 connect.sid s%3AI3ww2VSYnJCTbvhTLFfsKWucSCgLEXnH.Q%2FZB%2FqjpNIGDgcSjAp4O%2BHP03Qrmj71qIb8njdhnrK4
|
||||
@@ -1,43 +0,0 @@
|
||||
-- Script DEFINITIF pour créer la table webhook_bap_config en production
|
||||
-- À exécuter directement sur votre PostgreSQL de production
|
||||
-- ATTENTION : Remplacez YOUR_DATABASE par le nom de votre base
|
||||
|
||||
\c YOUR_DATABASE;
|
||||
|
||||
-- Créer la table webhook_bap_config
|
||||
DROP TABLE IF EXISTS webhook_bap_config CASCADE;
|
||||
|
||||
CREATE TABLE 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 la configuration par défaut avec VOTRE URL
|
||||
INSERT INTO webhook_bap_config (name, webhook_url, description, is_active) VALUES (
|
||||
'Configuration BAP',
|
||||
'https://workflow.ffnancy.fr/webhook-test/a3d03176-b72f-412d-8fb9-f920b9fbab4d',
|
||||
'Configuration pour envoi des fichiers BAP vers n8n',
|
||||
true
|
||||
);
|
||||
|
||||
-- Créer la table migrations si elle n'existe pas
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename VARCHAR(255) UNIQUE NOT NULL,
|
||||
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Marquer la migration comme appliquée
|
||||
INSERT INTO migrations (filename) VALUES ('20250903141000_create_webhook_bap_config.sql')
|
||||
ON CONFLICT (filename) DO NOTHING;
|
||||
|
||||
-- Vérifier que tout est créé
|
||||
SELECT 'Table webhook_bap_config créée avec succès' as message;
|
||||
SELECT * FROM webhook_bap_config;
|
||||
SELECT 'Table migrations mise à jour' as message;
|
||||
SELECT * FROM migrations WHERE filename LIKE '%webhook_bap%';
|
||||
@@ -1,26 +0,0 @@
|
||||
-- Script SQL simple pour créer UNIQUEMENT la table announcements en production
|
||||
-- À exécuter directement dans le conteneur Docker
|
||||
|
||||
CREATE TABLE IF NOT EXISTS announcements (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
priority VARCHAR(20) NOT NULL DEFAULT 'normal',
|
||||
author_id VARCHAR(255) NOT NULL,
|
||||
group_id INTEGER,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
CONSTRAINT announcements_priority_check CHECK (priority IN ('normal', 'important', 'urgent')),
|
||||
CONSTRAINT announcements_author_id_fkey FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT announcements_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Créer les index pour les performances
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_priority ON announcements(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_author_id ON announcements(author_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_group_id ON announcements(group_id);
|
||||
|
||||
-- Vérification
|
||||
SELECT 'Table announcements créée avec succès' as resultat;
|
||||
SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'announcements' ORDER BY ordinal_position;
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Script de débogage pour tester les endpoints de production
|
||||
import https from 'https';
|
||||
|
||||
const BASE_URL = 'https://logiflow.ffnancy.fr';
|
||||
|
||||
async function makeRequest(path, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(path, BASE_URL);
|
||||
const requestOptions = {
|
||||
hostname: url.hostname,
|
||||
path: url.pathname + url.search,
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Debug-Script/1.0',
|
||||
...options.headers
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(requestOptions, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
status: res.statusCode,
|
||||
headers: res.headers,
|
||||
data: data,
|
||||
length: data.length
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (options.body) {
|
||||
req.write(options.body);
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function debugProduction() {
|
||||
console.log('🔍 Testing production endpoints...\n');
|
||||
|
||||
try {
|
||||
// Test 1: Health check
|
||||
console.log('1. Health check:');
|
||||
const health = await makeRequest('/api/health');
|
||||
console.log(` Status: ${health.status}`);
|
||||
console.log(` Response: ${health.data.substring(0, 100)}...\n`);
|
||||
|
||||
// Test 2: Default credentials check
|
||||
console.log('2. Default credentials check:');
|
||||
const defaultCreds = await makeRequest('/api/default-credentials-check');
|
||||
console.log(` Status: ${defaultCreds.status}`);
|
||||
console.log(` Response: ${defaultCreds.data}\n`);
|
||||
|
||||
// Test 3: Login attempts with different credentials
|
||||
const credentialsList = [
|
||||
{ username: 'admin', password: 'admin' },
|
||||
{ username: 'administrator', password: 'administrator' },
|
||||
{ username: 'lff', password: 'lff2024' },
|
||||
{ username: 'logiflow', password: 'logiflow' },
|
||||
{ username: 'root', password: 'root' }
|
||||
];
|
||||
|
||||
let login = null;
|
||||
let cookies = '';
|
||||
|
||||
for (const creds of credentialsList) {
|
||||
console.log(`3. Login attempt with ${creds.username}:${creds.password}:`);
|
||||
login = await makeRequest('/api/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(creds)
|
||||
});
|
||||
console.log(` Status: ${login.status}`);
|
||||
console.log(` Response: ${login.data.substring(0, 200)}...\n`);
|
||||
|
||||
if (login.status === 200 && login.headers['set-cookie']) {
|
||||
cookies = login.headers['set-cookie'].map(c => c.split(';')[0]).join('; ');
|
||||
console.log(`✅ Successful login with ${creds.username}\n`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cookies already extracted above if login was successful
|
||||
|
||||
// Test 4: User info (with cookies)
|
||||
if (cookies) {
|
||||
console.log('4. User info (authenticated):');
|
||||
const user = await makeRequest('/api/user', {
|
||||
headers: { 'Cookie': cookies }
|
||||
});
|
||||
console.log(` Status: ${user.status}`);
|
||||
console.log(` Response: ${user.data.substring(0, 200)}...\n`);
|
||||
|
||||
// Test 5: Tasks (with cookies)
|
||||
console.log('5. Tasks list (authenticated):');
|
||||
const tasks = await makeRequest('/api/tasks', {
|
||||
headers: { 'Cookie': cookies }
|
||||
});
|
||||
console.log(` Status: ${tasks.status}`);
|
||||
console.log(` Response: ${tasks.data.substring(0, 300)}...\n`);
|
||||
|
||||
// Test 6: Test task update (with cookies) - testing the problematic endpoint
|
||||
console.log('6. Task update test (problematic endpoint):');
|
||||
const taskUpdate = await makeRequest('/api/tasks/99', {
|
||||
method: 'PUT',
|
||||
headers: { 'Cookie': cookies },
|
||||
body: JSON.stringify({
|
||||
title: 'Test Debug',
|
||||
description: 'Test de debug',
|
||||
priority: 'medium',
|
||||
status: 'pending',
|
||||
assignedTo: 'admin',
|
||||
startDate: '2025-08-20',
|
||||
dueDate: null
|
||||
})
|
||||
});
|
||||
console.log(` Status: ${taskUpdate.status}`);
|
||||
console.log(` Response: ${taskUpdate.data}\n`);
|
||||
} else {
|
||||
console.log('❌ No cookies received from login - cannot test authenticated endpoints\n');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error during debug:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
debugProduction();
|
||||
@@ -1,77 +0,0 @@
|
||||
-- 🔍 DIAGNOSTIC PRODUCTION COMPLET - Module Avoirs
|
||||
-- Copiez-collez ce script dans votre utilitaire SQL pour diagnostic précis
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- SECTION 1: STRUCTURE DES TABLES
|
||||
SELECT 'VÉRIFICATION STRUCTURE TABLES' AS section_title;
|
||||
|
||||
-- Table avoirs - structure exacte
|
||||
SELECT 'STRUCTURE_AVOIRS' AS check_type, column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'avoirs' AND table_schema = 'public'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- Table suppliers - structure exacte
|
||||
SELECT 'STRUCTURE_SUPPLIERS' AS check_type, column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'suppliers' AND table_schema = 'public'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- Table groups - structure exacte
|
||||
SELECT 'STRUCTURE_GROUPS' AS check_type, column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'groups' AND table_schema = 'public'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- Table users - structure exacte
|
||||
SELECT 'STRUCTURE_USERS' AS check_type, column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND table_schema = 'public'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- SECTION 2: CONTRAINTES
|
||||
SELECT 'CONTRAINTES_AVOIRS' AS check_type, conname AS constraint_name, contype AS type,
|
||||
confrelid::regclass AS referenced_table
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'avoirs'::regclass;
|
||||
|
||||
-- SECTION 3: DONNÉES EXISTANTES
|
||||
SELECT 'COMPTES_TABLES' AS check_type, 'suppliers' AS table_name, COUNT(*) AS count FROM suppliers
|
||||
UNION ALL
|
||||
SELECT 'COMPTES_TABLES', 'groups', COUNT(*) FROM groups
|
||||
UNION ALL
|
||||
SELECT 'COMPTES_TABLES', 'users', COUNT(*) FROM users
|
||||
UNION ALL
|
||||
SELECT 'COMPTES_TABLES', 'avoirs', COUNT(*) FROM avoirs;
|
||||
|
||||
-- SECTION 4: EXEMPLES DE DONNÉES (si elles existent)
|
||||
-- Suppliers
|
||||
SELECT 'EXEMPLES_SUPPLIERS' AS check_type, id, name FROM suppliers LIMIT 3;
|
||||
|
||||
-- Groups
|
||||
SELECT 'EXEMPLES_GROUPS' AS check_type, id, name FROM groups LIMIT 3;
|
||||
|
||||
-- Users
|
||||
SELECT 'EXEMPLES_USERS' AS check_type, id,
|
||||
COALESCE(username, 'no_username') AS username,
|
||||
role
|
||||
FROM users LIMIT 3;
|
||||
|
||||
-- SECTION 5: TEST DE COMPATIBILITÉ
|
||||
-- Test si on peut insérer avec des champs NULL
|
||||
SELECT 'TEST_COMPATIBILITÉ' AS check_type,
|
||||
CASE
|
||||
WHEN (SELECT is_nullable FROM information_schema.columns
|
||||
WHERE table_name = 'avoirs' AND column_name = 'invoice_reference') = 'YES'
|
||||
THEN 'invoice_reference_NULLABLE'
|
||||
ELSE 'invoice_reference_NOT_NULL'
|
||||
END AS invoice_ref_status,
|
||||
CASE
|
||||
WHEN (SELECT is_nullable FROM information_schema.columns
|
||||
WHERE table_name = 'avoirs' AND column_name = 'amount') = 'YES'
|
||||
THEN 'amount_NULLABLE'
|
||||
ELSE 'amount_NOT_NULL'
|
||||
END AS amount_status;
|
||||
|
||||
ROLLBACK; -- Ne pas sauvegarder, c'est juste du diagnostic
|
||||
@@ -1,42 +0,0 @@
|
||||
-- Commandes à exécuter DIRECTEMENT dans votre conteneur Docker
|
||||
-- Copiez et collez ces commandes une par une dans votre terminal
|
||||
|
||||
-- 1. Entrer dans le conteneur
|
||||
-- docker exec -it logiflow-logiflow-1 /bin/bash
|
||||
|
||||
-- 2. Une fois dans le conteneur, exécuter cette commande SQL :
|
||||
-- psql $DATABASE_URL
|
||||
|
||||
-- 3. Coller ce SQL dans psql :
|
||||
|
||||
CREATE TABLE IF NOT EXISTS announcements (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
priority VARCHAR(20) NOT NULL DEFAULT 'normal',
|
||||
author_id VARCHAR(255) NOT NULL,
|
||||
group_id INTEGER,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE announcements ADD CONSTRAINT announcements_priority_check
|
||||
CHECK (priority IN ('normal', 'important', 'urgent'));
|
||||
|
||||
ALTER TABLE announcements ADD CONSTRAINT announcements_author_id_fkey
|
||||
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE announcements ADD CONSTRAINT announcements_group_id_fkey
|
||||
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_priority ON announcements(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_author_id ON announcements(author_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_group_id ON announcements(group_id);
|
||||
|
||||
-- Vérification
|
||||
SELECT 'SUCCESS: Table announcements créée avec succès!' as result;
|
||||
SELECT COUNT(*) as nb_colonnes FROM information_schema.columns WHERE table_name = 'announcements';
|
||||
|
||||
-- 4. Taper \q pour quitter psql
|
||||
-- 5. Taper exit pour quitter le conteneur
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script de correction pour Docker entrypoint
|
||||
# Applique la migration directement via docker-compose exec
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 [FIX] Correction du problème docker-entrypoint.sh"
|
||||
|
||||
# Vérifier que le conteneur fonctionne
|
||||
if ! docker-compose ps | grep -q logiflow; then
|
||||
echo "❌ [FIX] Conteneur logiflow non trouvé, démarrage..."
|
||||
docker-compose up -d logiflow-db
|
||||
sleep 10
|
||||
fi
|
||||
|
||||
echo "🔄 [FIX] Application de la migration DLC directement..."
|
||||
|
||||
# Appliquer la migration via exec
|
||||
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' as status;
|
||||
EOF
|
||||
|
||||
echo "✅ [FIX] Migration DLC appliquée avec succès"
|
||||
|
||||
# Redémarrer le conteneur application
|
||||
echo "🔄 [FIX] Redémarrage du conteneur application..."
|
||||
docker-compose restart logiflow
|
||||
|
||||
echo "✅ [FIX] Correction terminée"
|
||||
@@ -1,34 +0,0 @@
|
||||
-- Script pour créer la table webhook_bap_config en production
|
||||
-- À exécuter directement sur votre base PostgreSQL
|
||||
|
||||
-- Créer la table
|
||||
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 la configuration par défaut
|
||||
INSERT INTO webhook_bap_config (name, webhook_url, description, is_active)
|
||||
VALUES (
|
||||
'Configuration BAP',
|
||||
'https://workflow.ffnancy.fr/webhook-test/a3d03176-b72f-412d-8fb9-f920b9fbab4d',
|
||||
'Configuration par défaut pour l''envoi des fichiers BAP vers n8n',
|
||||
true
|
||||
) ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Créer la table de suivi des migrations
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename VARCHAR(255) UNIQUE NOT NULL,
|
||||
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Marquer la migration comme appliquée
|
||||
INSERT INTO migrations (filename)
|
||||
VALUES ('20250903141000_create_webhook_bap_config.sql')
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -1,38 +0,0 @@
|
||||
-- Script de correction production pour créer la table announcements
|
||||
-- À exécuter dans votre conteneur Docker
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Création de la table announcements
|
||||
CREATE TABLE IF NOT EXISTS announcements (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
priority VARCHAR(20) NOT NULL DEFAULT 'normal' CHECK (priority IN ('normal', 'important', 'urgent')),
|
||||
author_id VARCHAR(255) NOT NULL,
|
||||
group_id INTEGER,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
CONSTRAINT announcements_author_id_fkey FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT announcements_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Création des index
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_priority ON announcements(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_author_id ON announcements(author_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_group_id ON announcements(group_id);
|
||||
|
||||
-- Commentaires
|
||||
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';
|
||||
|
||||
-- Vérification
|
||||
SELECT
|
||||
'Table announcements créée avec succès!' as message,
|
||||
COUNT(*) as nb_colonnes
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'announcements';
|
||||
|
||||
COMMIT;
|
||||
@@ -1,25 +0,0 @@
|
||||
-- Script d'initialisation PostgreSQL pour les annonces
|
||||
CREATE TABLE IF NOT EXISTS announcements (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
priority VARCHAR(20) DEFAULT 'normal' CHECK (priority IN ('normal', 'important', 'urgent')),
|
||||
author_id VARCHAR(255) NOT NULL,
|
||||
group_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Index pour améliorer les performances
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_priority ON announcements(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_announcements_group_id ON announcements(group_id);
|
||||
|
||||
-- Insertion de données de test
|
||||
INSERT INTO announcements (title, content, priority, author_id, group_id) VALUES
|
||||
('Bienvenue dans le système d''informations', 'Ce nouveau système remplace l''ancienne carte des commandes clients. Vous pouvez maintenant créer et gérer des annonces importantes.', 'important', 'admin', 1),
|
||||
('Maintenance programmée', 'Une maintenance système aura lieu dimanche prochain de 2h à 4h du matin.', 'normal', 'admin', NULL)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Afficher le résultat
|
||||
SELECT COUNT(*) as total_announcements FROM announcements;
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Production deployment script
|
||||
* Runs database migrations and starts the application
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function deployToProduction() {
|
||||
console.log('🚀 Starting production deployment...');
|
||||
|
||||
try {
|
||||
// Check if we're in production environment
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('⚠️ Warning: NODE_ENV is not set to production');
|
||||
}
|
||||
|
||||
// Check if DATABASE_URL is available
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error('❌ DATABASE_URL environment variable is required for production');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ Environment variables verified');
|
||||
|
||||
// Run database migrations
|
||||
console.log('🔄 Running database migrations...');
|
||||
|
||||
const migrateScriptPath = path.join(__dirname, 'migrations', 'migrate.js');
|
||||
if (fs.existsSync(migrateScriptPath)) {
|
||||
execSync(`node ${migrateScriptPath}`, { stdio: 'inherit' });
|
||||
console.log('✅ Database migrations completed');
|
||||
} else {
|
||||
console.log('⚠️ No migration script found, skipping migrations');
|
||||
}
|
||||
|
||||
// Start the application
|
||||
console.log('🚀 Starting application...');
|
||||
execSync('npm start', { stdio: 'inherit' });
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Deployment failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute if this script is run directly
|
||||
if (require.main === module) {
|
||||
deployToProduction();
|
||||
}
|
||||
|
||||
module.exports = { deployToProduction };
|
||||
|
Before Width: | Height: | Size: 275 KiB |
@@ -1,33 +0,0 @@
|
||||
import fetch from 'node-fetch';
|
||||
import FormData from 'form-data';
|
||||
|
||||
const webhookUrl = 'https://workflow.ffnancy.fr/webhook-test/acf9cbf7-040a-4cf5-a43d-80210420d30a';
|
||||
|
||||
async function testWebhook() {
|
||||
console.log('🚀 Testing webhook:', webhookUrl);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', Buffer.from('test content'), {
|
||||
filename: 'test.txt',
|
||||
contentType: 'text/plain'
|
||||
});
|
||||
formData.append('supplier', 'Test Supplier');
|
||||
formData.append('blNumber', '12345');
|
||||
formData.append('type', 'Facture');
|
||||
|
||||
try {
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: formData.getHeaders()
|
||||
});
|
||||
|
||||
console.log('📡 Response status:', response.status);
|
||||
const text = await response.text();
|
||||
console.log('📄 Response body:', text);
|
||||
} catch (error) {
|
||||
console.error('❌ Error calling webhook:', error);
|
||||
}
|
||||
}
|
||||
|
||||
testWebhook();
|
||||
@@ -1,27 +0,0 @@
|
||||
const webhookUrl = 'https://workflow.ffnancy.fr/webhook-test/acf9cbf7-040a-4cf5-a43d-80210420d30a';
|
||||
|
||||
async function testWebhook() {
|
||||
console.log('🚀 Testing webhook (Native Fetch/FormData):', webhookUrl);
|
||||
|
||||
const formData = new FormData();
|
||||
const fileBlob = new Blob(['test content'], { type: 'text/plain' });
|
||||
formData.append('file', fileBlob, 'test.txt');
|
||||
formData.append('supplier', 'Test Supplier');
|
||||
formData.append('blNumber', '12345');
|
||||
formData.append('type', 'Facture');
|
||||
|
||||
try {
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
console.log('📡 Response status:', response.status);
|
||||
const text = await response.text();
|
||||
console.log('📄 Response body:', text);
|
||||
} catch (error) {
|
||||
console.error('❌ Error calling webhook:', error);
|
||||
}
|
||||
}
|
||||
|
||||
testWebhook();
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script de test d'accès à la base de données
|
||||
|
||||
echo "🔍 Test d'accès à la base de données..."
|
||||
|
||||
# Test de connexion
|
||||
echo "1. Test de connexion à la base..."
|
||||
docker exec logiflow-logiflow-1 psql "$DATABASE_URL" -c "SELECT version();" > /dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Connexion à la base réussie"
|
||||
else
|
||||
echo "❌ Erreur de connexion à la base"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Liste des tables existantes
|
||||
echo "2. Tables existantes dans la base:"
|
||||
docker exec logiflow-logiflow-1 psql "$DATABASE_URL" -c "
|
||||
SELECT tablename
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename;
|
||||
"
|
||||
|
||||
# Vérification spécifique de la table announcements
|
||||
echo "3. Vérification de la table announcements:"
|
||||
docker exec logiflow-logiflow-1 psql "$DATABASE_URL" -c "
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'announcements'
|
||||
) as announcements_exists;
|
||||
"
|
||||
|
||||
echo "✅ Test terminé!"
|
||||
@@ -1,124 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Task Update - Production Debug</title>
|
||||
<style>
|
||||
body { font-family: monospace; padding: 20px; }
|
||||
.log { margin: 10px 0; padding: 10px; background: #f5f5f5; }
|
||||
.error { background: #ffe6e6; }
|
||||
.success { background: #e6ffe6; }
|
||||
button { padding: 10px; margin: 5px; }
|
||||
textarea, input { width: 100%; margin: 5px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Task Update - Production Debug</h1>
|
||||
|
||||
<div>
|
||||
<h3>1. D'abord, connectez-vous manuellement sur LogiFlow</h3>
|
||||
<p>Ouvrez <a href="https://logiflow.ffnancy.fr" target="_blank">LogiFlow</a> dans un autre onglet et connectez-vous avec vos vrais identifiants.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>2. Test de l'endpoint problématique</h3>
|
||||
<label>Task ID à tester:</label>
|
||||
<input type="number" id="taskId" value="99" />
|
||||
|
||||
<label>JSON des données à envoyer:</label>
|
||||
<textarea id="taskData" rows="10">{
|
||||
"title": "Test Debug",
|
||||
"description": "Test de debug depuis le script",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"assignedTo": "admin",
|
||||
"startDate": "2025-08-20",
|
||||
"dueDate": null
|
||||
}</textarea>
|
||||
|
||||
<button onclick="testTaskUpdate()">Test Task Update (PUT)</button>
|
||||
<button onclick="clearLogs()">Clear Logs</button>
|
||||
</div>
|
||||
|
||||
<div id="logs"></div>
|
||||
|
||||
<script>
|
||||
function log(message, type = 'info') {
|
||||
const logs = document.getElementById('logs');
|
||||
const div = document.createElement('div');
|
||||
div.className = `log ${type}`;
|
||||
div.innerHTML = `[${new Date().toLocaleTimeString()}] ${message}`;
|
||||
logs.insertBefore(div, logs.firstChild);
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
document.getElementById('logs').innerHTML = '';
|
||||
}
|
||||
|
||||
async function testTaskUpdate() {
|
||||
const taskId = document.getElementById('taskId').value;
|
||||
const taskData = document.getElementById('taskData').value;
|
||||
|
||||
log(`Testing task update for ID: ${taskId}`);
|
||||
|
||||
try {
|
||||
// Parse JSON data
|
||||
const data = JSON.parse(taskData);
|
||||
log(`Parsed data: ${JSON.stringify(data, null, 2)}`);
|
||||
|
||||
// Test the problematic endpoint
|
||||
const response = await fetch(`/api/tasks/${taskId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin', // Important pour envoyer les cookies de session
|
||||
body: taskData
|
||||
});
|
||||
|
||||
log(`Response status: ${response.status} ${response.statusText}`);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
log(`Success! Response: ${JSON.stringify(result, null, 2)}`, 'success');
|
||||
} else {
|
||||
const error = await response.text();
|
||||
log(`Error response: ${error}`, 'error');
|
||||
|
||||
// Additional debug info
|
||||
log(`Response headers: ${JSON.stringify([...response.headers.entries()])}`, 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`JavaScript error: ${error.message}`, 'error');
|
||||
log(`Stack trace: ${error.stack}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test connectivity on load
|
||||
window.onload = function() {
|
||||
log('Script loaded. Please login to LogiFlow first!');
|
||||
|
||||
// Test if we can reach the API
|
||||
fetch('/api/user')
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
log('Not authenticated - please login first', 'error');
|
||||
} else if (response.ok) {
|
||||
log('Authenticated successfully', 'success');
|
||||
return response.json();
|
||||
} else {
|
||||
log(`Unexpected status: ${response.status}`, 'error');
|
||||
}
|
||||
})
|
||||
.then(user => {
|
||||
if (user) {
|
||||
log(`Logged in as: ${user.username} (${user.role})`, 'success');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
log(`Connection test failed: ${error.message}`, 'error');
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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"
|
||||
@@ -20,7 +20,6 @@ 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"),
|
||||
},
|
||||
},
|
||||
root: path.resolve(import.meta.dirname, "client"),
|
||||
|
||||