mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-20 13:38:15 +02:00
Add functionality to upload and send invoices via webhook integration
Implement a modal for uploading PDF invoices, selecting invoice types, and sending them to configured webhook URLs for deliveries lacking validation or invoice numbers. Replit-Commit-Author: Agent Replit-Commit-Session-Id: c52a701b-4841-4d00-b8cf-5a41c12abdf3 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/c52a701b-4841-4d00-b8cf-5a41c12abdf3/MvDLhQ6
This commit is contained in:
1 parent
8de35b9709
commit
f75a7ea25c
1 file changed
+197
-1
@@ -12,7 +12,10 @@ import { useStore } from "@/components/Layout";
|
|||||||
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
import { usePermissions } from "@shared/permissions";
|
import { usePermissions } from "@shared/permissions";
|
||||||
import { Pagination, usePagination } from "@/components/ui/pagination";
|
import { Pagination, usePagination } from "@/components/ui/pagination";
|
||||||
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter } from "lucide-react";
|
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter, Upload } from "lucide-react";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import ReconciliationModal from "@/components/modals/ReconciliationModal";
|
import ReconciliationModal from "@/components/modals/ReconciliationModal";
|
||||||
|
|
||||||
export default function BLReconciliation() {
|
export default function BLReconciliation() {
|
||||||
@@ -47,6 +50,13 @@ export default function BLReconciliation() {
|
|||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [selectedDelivery, setSelectedDelivery] = useState<any>(null);
|
const [selectedDelivery, setSelectedDelivery] = useState<any>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// État pour le modal d'envoi de facture
|
||||||
|
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
|
||||||
|
const [selectedDeliveryForInvoice, setSelectedDeliveryForInvoice] = useState<any>(null);
|
||||||
|
const [invoiceType, setInvoiceType] = useState<string>("");
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
// Récupérer les fournisseurs pour la logique automatique
|
// Récupérer les fournisseurs pour la logique automatique
|
||||||
const { data: suppliers = [] } = useQuery<any[]>({
|
const { data: suppliers = [] } = useQuery<any[]>({
|
||||||
@@ -120,6 +130,101 @@ export default function BLReconciliation() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Nouvelles fonctions pour le modal d'envoi de facture
|
||||||
|
const handleOpenInvoiceModal = (delivery: any) => {
|
||||||
|
setSelectedDeliveryForInvoice(delivery);
|
||||||
|
setInvoiceType("");
|
||||||
|
setSelectedFile(null);
|
||||||
|
setShowInvoiceModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseInvoiceModal = () => {
|
||||||
|
setShowInvoiceModal(false);
|
||||||
|
setSelectedDeliveryForInvoice(null);
|
||||||
|
setInvoiceType("");
|
||||||
|
setSelectedFile(null);
|
||||||
|
setIsUploading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file && file.type === 'application/pdf') {
|
||||||
|
setSelectedFile(file);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Veuillez sélectionner un fichier PDF",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendInvoice = async () => {
|
||||||
|
if (!selectedDeliveryForInvoice || !invoiceType || !selectedFile) {
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Veuillez remplir tous les champs",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedDeliveryForInvoice.group?.webhookUrl) {
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Aucun webhook configuré pour ce magasin",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUploading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', selectedFile);
|
||||||
|
formData.append('supplier', selectedDeliveryForInvoice.supplier?.name || '');
|
||||||
|
formData.append('blNumber', selectedDeliveryForInvoice.blNumber || '');
|
||||||
|
formData.append('type', invoiceType);
|
||||||
|
|
||||||
|
const response = await fetch(selectedDeliveryForInvoice.group.webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erreur ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Succès",
|
||||||
|
description: `${invoiceType} envoyée avec succès via le webhook`,
|
||||||
|
});
|
||||||
|
|
||||||
|
handleCloseInvoiceModal();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: `Impossible d'envoyer la ${invoiceType.toLowerCase()}: ${error.message}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fonction pour déterminer si le bouton "Envoyer Facture" doit être affiché
|
||||||
|
const shouldShowInvoiceButton = (delivery: any) => {
|
||||||
|
// Conditions : ligne non validée OU pas de RefFacture renseignée
|
||||||
|
const isNotValidated = !delivery.reconciled;
|
||||||
|
const hasNoInvoiceReference = !delivery.invoiceReference;
|
||||||
|
|
||||||
|
// Et il faut qu'il y ait un magasin assigné avec un webhook
|
||||||
|
const hasValidGroup = delivery.group && delivery.group.webhookUrl;
|
||||||
|
|
||||||
|
return (isNotValidated || hasNoInvoiceReference) && hasValidGroup;
|
||||||
|
};
|
||||||
|
|
||||||
const handleQuickValidate = async (delivery: any) => {
|
const handleQuickValidate = async (delivery: any) => {
|
||||||
try {
|
try {
|
||||||
await apiRequest(`/api/deliveries/${delivery.id}`, "PUT", {
|
await apiRequest(`/api/deliveries/${delivery.id}`, "PUT", {
|
||||||
@@ -459,6 +564,17 @@ export default function BLReconciliation() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right">
|
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||||
<div className="flex items-center justify-end space-x-2">
|
<div className="flex items-center justify-end space-x-2">
|
||||||
|
{shouldShowInvoiceButton(delivery) && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleOpenInvoiceModal(delivery)}
|
||||||
|
className="h-8 w-8 p-0 text-green-600 hover:text-green-700"
|
||||||
|
title="Envoyer Facture/Avoir"
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{!delivery.reconciled ? (
|
{!delivery.reconciled ? (
|
||||||
<>
|
<>
|
||||||
{permissions.canValidate('reconciliation') && (
|
{permissions.canValidate('reconciliation') && (
|
||||||
@@ -647,6 +763,15 @@ export default function BLReconciliation() {
|
|||||||
</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">
|
||||||
|
{shouldShowInvoiceButton(delivery) && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleOpenInvoiceModal(delivery)}
|
||||||
|
className="text-green-600 hover:text-green-700 transition-colors duration-200 p-1 hover:bg-green-50 rounded"
|
||||||
|
title="Envoyer Facture/Avoir"
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{user?.role === 'admin' && (
|
{user?.role === 'admin' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDevalidateReconciliation(delivery.id)}
|
onClick={() => handleDevalidateReconciliation(delivery.id)}
|
||||||
@@ -706,6 +831,77 @@ export default function BLReconciliation() {
|
|||||||
onClose={handleCloseModal}
|
onClose={handleCloseModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Modal d'envoi de facture */}
|
||||||
|
<Dialog open={showInvoiceModal} onOpenChange={setShowInvoiceModal}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Envoyer Facture/Avoir</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
{selectedDeliveryForInvoice && (
|
||||||
|
<div className="bg-gray-50 p-3 rounded-md">
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="font-medium">{selectedDeliveryForInvoice.supplier?.name}</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
BL: {selectedDeliveryForInvoice.blNumber || 'Non renseigné'}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-600">
|
||||||
|
Magasin: {selectedDeliveryForInvoice.group?.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="invoice-type">Type de document</Label>
|
||||||
|
<Select value={invoiceType} onValueChange={setInvoiceType}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Sélectionnez le type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Facture">Facture</SelectItem>
|
||||||
|
<SelectItem value="Avoir">Avoir</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="pdf-file">Fichier PDF</Label>
|
||||||
|
<input
|
||||||
|
id="pdf-file"
|
||||||
|
type="file"
|
||||||
|
accept=".pdf"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
{selectedFile && (
|
||||||
|
<div className="text-sm text-green-600">
|
||||||
|
Fichier sélectionné: {selectedFile.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedDeliveryForInvoice?.group?.webhookUrl && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Envoi via: {selectedDeliveryForInvoice.group.webhookUrl}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={handleCloseInvoiceModal}>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSendInvoice}
|
||||||
|
disabled={!invoiceType || !selectedFile || isUploading}
|
||||||
|
>
|
||||||
|
{isUploading ? "Envoi..." : "Envoyer"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user