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
This commit is contained in:
Claude committed 2026-08-14 16:14:45 +00:00
1 parent 9de717387f
commit af3800aa09
47 files changed
+9 -6522

No files matched your search

+9
View File
@@ -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/
-24
View File
@@ -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');
BIN
View File
Binary file not shown.

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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

-1
View File
@@ -1 +0,0 @@
// Backup temporaire pour restaurer si nécessaire
-778
View File
@@ -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>
);
}
-568
View File
@@ -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>
);
}
-546
View File
@@ -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>
);
}
-504
View File
@@ -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>
);
}
-5
View File
@@ -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
-5
View File
@@ -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
-43
View File
@@ -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%';
-26
View File
@@ -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;
-136
View File
@@ -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();
-77
View File
@@ -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
-42
View File
@@ -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
-40
View File
@@ -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"
-34
View File
@@ -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;
-38
View File
@@ -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;
-25
View File
@@ -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;
-55
View File
@@ -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 };
Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

-33
View File
@@ -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();
-27
View File
@@ -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();
File diff suppressed because it is too large. Load diff
-35
View File
@@ -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é!"
-124
View File
@@ -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>
-73
View File
@@ -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"
-1
View File
@@ -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"),