mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-22 06:28:19 +02:00
2 files changed
+255
-7
No files matched your search
+177
-7
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Plus, Search, FileText, CheckCircle, AlertCircle, Clock, Edit, Trash2, UserCheck, Send, Upload } from "lucide-react";
|
import { Plus, Search, FileText, CheckCircle, AlertCircle, Clock, Edit, Trash2, UserCheck, Send, Upload, XCircle } from "lucide-react";
|
||||||
import { useStore } from "@/components/Layout";
|
import { useStore } from "@/components/Layout";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -47,6 +47,8 @@ interface Avoir {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
nocodbTableName?: string;
|
||||||
|
nocodbConfigId?: number;
|
||||||
};
|
};
|
||||||
creator: {
|
creator: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -65,6 +67,8 @@ interface Group {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
nocodbTableName?: string;
|
||||||
|
nocodbConfigId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schema for form validation
|
// Schema for form validation
|
||||||
@@ -111,6 +115,10 @@ export default function Avoirs() {
|
|||||||
const [showWaitingModal, setShowWaitingModal] = useState(false);
|
const [showWaitingModal, setShowWaitingModal] = useState(false);
|
||||||
const [processingSeconds, setProcessingSeconds] = useState(0);
|
const [processingSeconds, setProcessingSeconds] = useState(0);
|
||||||
const [processingTimeout, setProcessingTimeout] = useState<NodeJS.Timeout | null>(null);
|
const [processingTimeout, setProcessingTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// États pour le système de vérification de facture
|
||||||
|
const [avoirVerificationResults, setAvoirVerificationResults] = useState<Record<number, any>>({});
|
||||||
|
const [verifyingAvoirs, setVerifyingAvoirs] = useState<Set<number>>(new Set());
|
||||||
// Utiliser le contexte global du magasin
|
// Utiliser le contexte global du magasin
|
||||||
const { selectedStoreId } = useStore();
|
const { selectedStoreId } = useStore();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -328,6 +336,123 @@ export default function Avoirs() {
|
|||||||
setIsDeleteDialogOpen(true);
|
setIsDeleteDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🔍 FONCTION DE VÉRIFICATION DE FACTURE (comme rapprochement)
|
||||||
|
const verifyAvoirInvoiceMutation = useMutation({
|
||||||
|
mutationFn: async ({ avoirId, invoiceReference, forceRefresh }: { avoirId: number; invoiceReference?: string; forceRefresh?: boolean }) => {
|
||||||
|
try {
|
||||||
|
const result = await apiRequest(`/api/avoirs/${avoirId}/verify-invoice`, 'POST', {
|
||||||
|
invoiceReference,
|
||||||
|
forceRefresh: forceRefresh || false
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Erreur API vérification avoir:', error);
|
||||||
|
throw new Error(error.message || 'Erreur de vérification');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: (result, variables) => {
|
||||||
|
setAvoirVerificationResults(prev => ({
|
||||||
|
...prev,
|
||||||
|
[variables.avoirId]: result
|
||||||
|
}));
|
||||||
|
|
||||||
|
setVerifyingAvoirs(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(variables.avoirId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error, variables) => {
|
||||||
|
console.error('Erreur vérification facture avoir:', error);
|
||||||
|
setAvoirVerificationResults(prev => ({
|
||||||
|
...prev,
|
||||||
|
[variables.avoirId]: {
|
||||||
|
exists: false,
|
||||||
|
matchType: 'none',
|
||||||
|
errorMessage: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
setVerifyingAvoirs(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(variables.avoirId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Erreur de vérification",
|
||||||
|
description: error instanceof Error ? error.message : 'Erreur inconnue',
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fonction pour déclencher la vérification d'un avoir
|
||||||
|
const handleVerifyAvoirInvoice = (avoir: Avoir, forceRefresh: boolean = false) => {
|
||||||
|
const hasInvoiceRef = avoir.invoiceReference?.trim();
|
||||||
|
|
||||||
|
if (!hasInvoiceRef) {
|
||||||
|
toast({
|
||||||
|
title: "Référence manquante",
|
||||||
|
description: "Veuillez saisir une référence de facture avant la vérification",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!avoir.group?.nocodbTableName && !avoir.group?.nocodbConfigId) {
|
||||||
|
toast({
|
||||||
|
title: "Vérification non disponible",
|
||||||
|
description: "Ce magasin n'a pas de configuration NocoDB",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 Déclenchement vérification avoir:', {
|
||||||
|
avoirId: avoir.id,
|
||||||
|
invoiceReference: avoir.invoiceReference,
|
||||||
|
supplier: avoir.supplier?.name,
|
||||||
|
group: avoir.group?.name
|
||||||
|
});
|
||||||
|
|
||||||
|
setVerifyingAvoirs(prev => new Set(prev).add(avoir.id));
|
||||||
|
|
||||||
|
verifyAvoirInvoiceMutation.mutate({
|
||||||
|
avoirId: avoir.id,
|
||||||
|
invoiceReference: avoir.invoiceReference,
|
||||||
|
forceRefresh
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fonction pour vérifier tous les avoirs avec une référence facture
|
||||||
|
const handleVerifyAllAvoirInvoices = () => {
|
||||||
|
const avoirsToVerify = avoirs.filter(avoir =>
|
||||||
|
avoir.invoiceReference?.trim() &&
|
||||||
|
(avoir.group?.nocodbTableName || avoir.group?.nocodbConfigId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (avoirsToVerify.length === 0) {
|
||||||
|
toast({
|
||||||
|
title: "Aucun avoir à vérifier",
|
||||||
|
description: "Aucun avoir avec référence de facture trouvé",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
avoirsToVerify.forEach((avoir, index) => {
|
||||||
|
// Délai échelonné pour éviter la surcharge
|
||||||
|
setTimeout(() => {
|
||||||
|
handleVerifyAvoirInvoice(avoir, true); // Force refresh pour tous
|
||||||
|
}, index * 200); // 200ms entre chaque vérification
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Vérification lancée",
|
||||||
|
description: `Vérification de ${avoirsToVerify.length} avoir(s) en cours...`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 🔥 FONCTIONS WEBHOOK MODAL (comme rapprochement)
|
// 🔥 FONCTIONS WEBHOOK MODAL (comme rapprochement)
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
@@ -525,13 +650,30 @@ export default function Avoirs() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
<div className="flex items-center space-x-2">
|
||||||
<DialogTrigger asChild>
|
{/* Bouton Vérifier tous si des avoirs avec référence existent */}
|
||||||
<Button>
|
{avoirs.some(avoir => avoir.invoiceReference?.trim()) && (
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Button
|
||||||
Nouvel avoir
|
variant="outline"
|
||||||
|
onClick={handleVerifyAllAvoirInvoices}
|
||||||
|
disabled={verifyingAvoirs.size > 0}
|
||||||
|
>
|
||||||
|
<Search className="h-4 w-4 mr-2" />
|
||||||
|
{verifyingAvoirs.size > 0 ? "Vérification..." : "Vérifier tous"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
)}
|
||||||
|
|
||||||
|
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Nouvel avoir
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Créer un nouvel avoir</DialogTitle>
|
<DialogTitle>Créer un nouvel avoir</DialogTitle>
|
||||||
@@ -766,6 +908,9 @@ export default function Avoirs() {
|
|||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Créé le
|
Créé le
|
||||||
</th>
|
</th>
|
||||||
|
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Vérification
|
||||||
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Actions
|
Actions
|
||||||
</th>
|
</th>
|
||||||
@@ -826,6 +971,31 @@ export default function Avoirs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-center text-sm">
|
||||||
|
{avoir.invoiceReference?.trim() ? (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
{verifyingAvoirs.has(avoir.id) ? (
|
||||||
|
<Clock className="h-4 w-4 text-blue-500 animate-spin" />
|
||||||
|
) : avoirVerificationResults[avoir.id] ? (
|
||||||
|
avoirVerificationResults[avoir.id].exists ? (
|
||||||
|
<CheckCircle className="h-4 w-4 text-green-500 cursor-help" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-4 w-4 text-red-500 cursor-help" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => handleVerifyAvoirInvoice(avoir)}
|
||||||
|
className="h-4 w-4 text-gray-400 hover:text-blue-500 transition-colors"
|
||||||
|
title="Cliquer pour vérifier la facture"
|
||||||
|
>
|
||||||
|
<Search className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400 text-xs">Sans réf.</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{/* 📋 Icône Upload pour avoirs "Reçu" */}
|
{/* 📋 Icône Upload pour avoirs "Reçu" */}
|
||||||
|
|||||||
@@ -2600,6 +2600,84 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route de vérification de facture NocoDB pour les avoirs
|
||||||
|
app.post('/api/avoirs/:id/verify-invoice', isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ message: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const avoirId = parseInt(req.params.id);
|
||||||
|
const avoir = await storage.getAvoir(avoirId);
|
||||||
|
|
||||||
|
if (!avoir) {
|
||||||
|
return res.status(404).json({ message: "Avoir not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions (using 'deliveries' module for similar logic)
|
||||||
|
if (!hasPermission(user.role, 'deliveries', 'view')) {
|
||||||
|
return res.status(403).json({ message: "Insufficient permissions" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only admin have access to all avoirs, others must be in the same group
|
||||||
|
if (user.role !== 'admin') {
|
||||||
|
const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || [];
|
||||||
|
if (!userGroupIds.includes(avoir.groupId)) {
|
||||||
|
console.log('🚫 Access denied - User groups check:', {
|
||||||
|
userId: user.id,
|
||||||
|
userRole: user.role,
|
||||||
|
userGroupIds,
|
||||||
|
avoirGroupId: avoir.groupId,
|
||||||
|
avoirSupplier: avoir.supplier?.name
|
||||||
|
});
|
||||||
|
return res.status(403).json({ message: "Access denied to this group" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { invoiceReference, forceRefresh } = req.body;
|
||||||
|
|
||||||
|
if (!avoir.supplier || !avoir.group) {
|
||||||
|
console.log('❌ Avoir manque informations:', {
|
||||||
|
avoirId,
|
||||||
|
hasSupplier: !!avoir.supplier,
|
||||||
|
hasGroup: !!avoir.group
|
||||||
|
});
|
||||||
|
return res.status(400).json({ message: "Avoir missing supplier or group information" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que la référence facture est présente
|
||||||
|
if (!invoiceReference || !invoiceReference.trim()) {
|
||||||
|
return res.status(400).json({ message: "Référence de facture requise" });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 Vérification facture avoir:', {
|
||||||
|
avoirId,
|
||||||
|
invoiceReference,
|
||||||
|
supplier: avoir.supplier?.name,
|
||||||
|
group: avoir.group?.name,
|
||||||
|
groupId: avoir.groupId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Vérifier par référence de facture uniquement
|
||||||
|
const result = await invoiceVerificationService.verifyInvoice(
|
||||||
|
invoiceReference,
|
||||||
|
avoir.groupId,
|
||||||
|
forceRefresh || false,
|
||||||
|
false // Les avoirs ne sont pas "réconciliés" comme les livraisons
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('✅ Résultat vérification avoir:', result);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error verifying avoir invoice:", error);
|
||||||
|
res.status(500).json({
|
||||||
|
message: "Failed to verify avoir invoice",
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Avoir status update routes
|
// Avoir status update routes
|
||||||
app.put('/api/avoirs/:id/webhook-status', isAuthenticated, async (req: any, res) => {
|
app.put('/api/avoirs/:id/webhook-status', isAuthenticated, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user