Enhance reconciliation module with edit, validate, and delete functionalities

Implement edit, validate, and delete permissions for reconciliation, update validation logic, and address UI color inconsistencies for director and admin roles.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: b163d4c0-de5e-4f4e-a9c0-aed4c7049718
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/b163d4c0-de5e-4f4e-a9c0-aed4c7049718/7EbZYsH
This commit is contained in:
michaelschal committed 2025-08-12 10:33:09 +00:00
1 parent 831e1bb9b0
commit e9551aeb63
4 files changed
+539 -998

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

+245 -67
View File
@@ -9,16 +9,19 @@ import { useToast } from "@/hooks/use-toast";
import { apiRequest } from "@/lib/queryClient"; import { apiRequest } from "@/lib/queryClient";
import { useStore } from "@/components/Layout"; import { useStore } from "@/components/Layout";
import { useAuthUnified } from "@/hooks/useAuthUnified"; import { useAuthUnified } from "@/hooks/useAuthUnified";
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X } from "lucide-react"; import { usePermissions } from "@shared/permissions";
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban } from "lucide-react";
import type { Supplier } from "@shared/schema";
export default function BLReconciliation() { export default function BLReconciliation() {
const { user } = useAuthUnified(); const { user } = useAuthUnified();
const { selectedStoreId } = useStore(); const { selectedStoreId } = useStore();
const { toast } = useToast(); const { toast } = useToast();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const permissions = usePermissions(user?.role);
// Redirection pour les employés // Vérification des permissions d'accès au module
if (user?.role === 'employee') { if (!permissions.canView('reconciliation')) {
return ( return (
<div className="p-6"> <div className="p-6">
<div className="bg-orange-50 border-l-4 border-orange-400 p-4"> <div className="bg-orange-50 border-l-4 border-orange-400 p-4">
@@ -29,7 +32,7 @@ export default function BLReconciliation() {
<div className="ml-3"> <div className="ml-3">
<p className="text-sm text-orange-700"> <p className="text-sm text-orange-700">
<strong>Accès restreint</strong><br /> <strong>Accès restreint</strong><br />
Seuls les managers et administrateurs peuvent accéder au module de rapprochement BL/Factures. Seuls les directeurs et administrateurs peuvent accéder au module de rapprochement BL/Factures.
</p> </p>
</div> </div>
</div> </div>
@@ -42,7 +45,7 @@ export default function BLReconciliation() {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
// Récupérer les fournisseurs pour la logique automatique // Récupérer les fournisseurs pour la logique automatique
const { data: suppliers = [] } = useQuery({ const { data: suppliers = [], isLoading: suppliersLoading } = useQuery<Supplier[]>({
queryKey: ['/api/suppliers'], queryKey: ['/api/suppliers'],
}); });
@@ -81,36 +84,114 @@ export default function BLReconciliation() {
return supplier?.automaticReconciliation; return supplier?.automaticReconciliation;
}); });
// Fonction pour dévalider un rapprochement automatique (directeurs et admins uniquement) // Mutation pour valider un rapprochement manuel
const handleDevalidateAutoReconciliation = async (deliveryId: number) => { const validateManualMutation = useMutation({
if (user?.role !== 'directeur' && user?.role !== 'admin') { mutationFn: async (deliveryId: number) => {
return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", {
reconciled: true,
validatedAt: new Date().toISOString()
});
},
onSuccess: () => {
toast({ toast({
title: "Accès refusé", title: "Succès",
description: "Seuls les directeurs et administrateurs peuvent dévalider les rapprochements automatiques", description: "Rapprochement validé avec succès",
});
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
},
onError: () => {
toast({
title: "Erreur",
description: "Impossible de valider le rapprochement",
variant: "destructive", variant: "destructive",
}); });
return;
} }
});
try { // Mutation pour dévalider un rapprochement (admins uniquement)
await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", { const devalidateMutation = useMutation({
mutationFn: async (deliveryId: number) => {
return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", {
reconciled: false, reconciled: false,
validatedAt: null validatedAt: null
}); });
},
onSuccess: () => {
toast({ toast({
title: "Succès", title: "Succès",
description: "Rapprochement automatique dévalidé avec succès", description: "Rapprochement dévalidé avec succès",
}); });
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
} catch (error) { },
onError: () => {
toast({ toast({
title: "Erreur", title: "Erreur",
description: "Impossible de dévalider le rapprochement", description: "Impossible de dévalider le rapprochement",
variant: "destructive", variant: "destructive",
}); });
} }
});
// Mutation pour supprimer une ligne (admins uniquement)
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 // Filtrage des livraisons par recherche
@@ -128,7 +209,21 @@ export default function BLReconciliation() {
const filteredManualDeliveries = filterDeliveries(manualReconciliationDeliveries); const filteredManualDeliveries = filterDeliveries(manualReconciliationDeliveries);
const filteredAutomaticDeliveries = filterDeliveries(automaticReconciliationDeliveries); const filteredAutomaticDeliveries = filterDeliveries(automaticReconciliationDeliveries);
const canModify = user?.role === 'directeur' || user?.role === 'admin'; // Vérifier si une ligne est dévalidée (pour la coloration)
const isDevalidated = (delivery: any) => {
return delivery.reconciled === false && delivery.validatedAt === null;
};
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 ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
@@ -226,7 +321,7 @@ export default function BLReconciliation() {
Montant Fact. Montant Fact.
</th> </th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Magasin Statut
</th> </th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions Actions
@@ -235,7 +330,13 @@ export default function BLReconciliation() {
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{filteredManualDeliveries.map((delivery: any) => ( {filteredManualDeliveries.map((delivery: any) => (
<tr key={delivery.id} className="hover:bg-gray-50"> <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"> <td className="px-3 py-2 text-sm">
<div className="font-medium text-gray-900 truncate max-w-32"> <div className="font-medium text-gray-900 truncate max-w-32">
{delivery.supplier?.name} {delivery.supplier?.name}
@@ -277,18 +378,71 @@ export default function BLReconciliation() {
</div> </div>
</td> </td>
<td className="px-3 py-2 text-sm"> <td className="px-3 py-2 text-sm">
<div className="text-gray-900"> {delivery.reconciled ? (
{delivery.group?.name} <Badge variant="default" className="bg-green-600 text-white">
</div> Validé
</Badge>
) : isDevalidated(delivery) ? (
<Badge variant="destructive">
Dévalidé
</Badge>
) : (
<Badge variant="outline">
En attente
</Badge>
)}
</td> </td>
<td className="px-3 py-2 text-sm"> <td className="px-3 py-2 text-sm">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button {permissions.canEdit('reconciliation') && (
className="text-gray-600 hover:text-blue-600 transition-colors duration-200 p-1 hover:bg-blue-50" <Button
title="Modifier" variant="ghost"
> size="sm"
<Edit className="w-4 h-4" /> className="text-gray-600 hover:text-blue-600 p-1"
</button> title="Modifier"
>
<Edit className="w-4 h-4" />
</Button>
)}
{!delivery.reconciled && permissions.canValidate('reconciliation') && (
<Button
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" />
</Button>
)}
{delivery.reconciled && permissions.canEdit('reconciliation') && user?.role === 'admin' && (
<Button
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" />
</Button>
)}
{permissions.canDelete('reconciliation') && (
<Button
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" />
</Button>
)}
</div> </div>
</td> </td>
</tr> </tr>
@@ -309,7 +463,7 @@ export default function BLReconciliation() {
<h4 className="text-sm font-medium text-blue-900">Mode rapprochement automatique</h4> <h4 className="text-sm font-medium text-blue-900">Mode rapprochement automatique</h4>
<p className="text-sm text-blue-700 mt-1"> <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. Les livraisons de fournisseurs en mode automatique sont validées automatiquement lorsqu'elles ont le statut "delivered" et un numéro de BL.
{canModify ? ( {permissions.canEdit('reconciliation') ? (
" Vous pouvez dévalider ces rapprochements si nécessaire." " Vous pouvez dévalider ces rapprochements si nécessaire."
) : ( ) : (
" Seuls les directeurs et administrateurs peuvent modifier ces rapprochements." " Seuls les directeurs et administrateurs peuvent modifier ces rapprochements."
@@ -355,10 +509,7 @@ export default function BLReconciliation() {
Ref. Facture Ref. Facture
</th> </th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Montant Fact. Statut
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Magasin
</th> </th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions Actions
@@ -367,13 +518,18 @@ export default function BLReconciliation() {
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{filteredAutomaticDeliveries.map((delivery: any) => ( {filteredAutomaticDeliveries.map((delivery: any) => (
<tr key={delivery.id} className="hover:bg-gray-50 bg-green-50"> <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"> <td className="px-3 py-2 text-sm">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="font-medium text-gray-900 truncate max-w-32"> <div className="font-medium text-gray-900 truncate max-w-32">
{delivery.supplier?.name} {delivery.supplier?.name}
</div> </div>
<Settings className="w-3 h-3 text-blue-500" title="Mode automatique" /> <div className="w-3 h-3 bg-blue-500 rounded-full" title="Mode automatique" />
</div> </div>
</td> </td>
<td className="px-3 py-2 text-sm"> <td className="px-3 py-2 text-sm">
@@ -388,8 +544,8 @@ export default function BLReconciliation() {
</td> </td>
<td className="px-3 py-2 text-sm"> <td className="px-3 py-2 text-sm">
<div className="text-gray-900"> <div className="text-gray-900">
{delivery.reconciled && delivery.updatedAt ? {delivery.reconciled && delivery.validatedAt ?
safeFormat(delivery.updatedAt, 'dd/MM/yy') : safeFormat(delivery.validatedAt, 'dd/MM/yy') :
'-' '-'
} }
</div> </div>
@@ -408,43 +564,65 @@ export default function BLReconciliation() {
</div> </div>
</td> </td>
<td className="px-3 py-2 text-sm"> <td className="px-3 py-2 text-sm">
<div className="text-gray-900"> {isDevalidated(delivery) ? (
{delivery.invoiceAmount ? <Badge variant="destructive">
`${parseFloat(delivery.invoiceAmount).toFixed(2)}€` : Dévalidé
'-' </Badge>
} ) : (
</div> <Badge variant="default" className="bg-green-600 text-white">
</td> Auto-validé
<td className="px-3 py-2 text-sm"> </Badge>
<div className="text-gray-900"> )}
{delivery.group?.name}
</div>
</td> </td>
<td className="px-3 py-2 text-sm"> <td className="px-3 py-2 text-sm">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{canModify ? ( {isDevalidated(delivery) && permissions.canEdit('reconciliation') && (
<> <>
<button <Button
className="text-gray-600 hover:text-blue-600 transition-colors duration-200 p-1 hover:bg-blue-50" variant="ghost"
title="Voir les détails" size="sm"
className="text-gray-600 hover:text-blue-600 p-1"
title="Modifier"
> >
<Eye className="w-4 h-4" /> <Edit className="w-4 h-4" />
</button> </Button>
<button <Button
onClick={() => handleDevalidateAutoReconciliation(delivery.id)} variant="ghost"
className="text-gray-600 hover:text-orange-600 transition-colors duration-200 p-1 hover:bg-orange-50" size="sm"
title="Dévalider le rapprochement automatique" onClick={() => handleValidateManual(delivery.id)}
disabled={validateManualMutation.isPending}
className="text-green-600 hover:text-green-700 p-1"
title="Valider"
> >
<X className="w-4 h-4" /> <Check className="w-4 h-4" />
</button> </Button>
</> </>
) : ( )}
<button
className="text-gray-600 hover:text-blue-600 transition-colors duration-200 p-1 hover:bg-blue-50" {!isDevalidated(delivery) && permissions.canEdit('reconciliation') && (
title="Voir les détails" <Button
variant="ghost"
size="sm"
onClick={() => handleDevalidate(delivery.id)}
disabled={devalidateMutation.isPending}
className="text-orange-600 hover:text-orange-700 p-1"
title="Dévalider"
> >
<Eye className="w-4 h-4" /> <Ban className="w-4 h-4" />
</button> </Button>
)}
{permissions.canDelete('reconciliation') && (
<Button
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" />
</Button>
)} )}
</div> </div>
</td> </td>
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -48,10 +48,10 @@ const PERMISSIONS: Record<Module, Record<Role, Permission[]>> = {
employee: [] employee: []
}, },
// Rapprochement - Admin tout, Directeur tout sauf delete, Manager/Employé rien // Rapprochement - Admin tout y compris validate, Directeur tout sauf delete, Manager/Employé rien
reconciliation: { reconciliation: {
admin: ['view', 'create', 'edit', 'delete'], admin: ['view', 'create', 'edit', 'delete', 'validate'],
directeur: ['view', 'create', 'edit'], directeur: ['view', 'create', 'edit', 'validate'],
manager: [], manager: [],
employee: [] employee: []
}, },