From ee4fca64d051e3d1b03823622f4e8242fd41baf2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 11:47:49 +0000 Subject: [PATCH 1/3] feat(rapprochement): add supplier mail button to request invoice PDF or BL Excel Each reconciliation row now shows a mail icon that opens the default mail client (Outlook) with the supplier address, subject and body pre-filled, asking for the invoice as PDF or the delivery note as Excel. - new client/src/lib/supplierMail.ts helper building the subject, body and mailto URL from the delivery (supplier, store, delivery date, BL number, BL amount, invoice reference) with the current user as signature - body line breaks encoded as CRLF per RFC 6068 so Outlook keeps the layout - supplier email read from the delivery join, with fallback to the suppliers list; when no email is set the button explains where to add it - button added to both the manual and validated tabs Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FHdE9nEh8zHhQNGpCgrtYe --- client/src/lib/supplierMail.ts | 144 ++++++++++++++++++++++++++ client/src/pages/BLReconciliation.tsx | 74 ++++++++++++- 2 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 client/src/lib/supplierMail.ts diff --git a/client/src/lib/supplierMail.ts b/client/src/lib/supplierMail.ts new file mode 100644 index 0000000..9c247e3 --- /dev/null +++ b/client/src/lib/supplierMail.ts @@ -0,0 +1,144 @@ +import { safeFormat } from "@/lib/dateUtils"; + +/** + * Génération des emails de relance fournisseur (rapprochement BL/Factures). + * Le lien mailto ouvre le client de messagerie par défaut du poste (Outlook) + * avec le destinataire, l'objet et le corps du message déjà préremplis. + */ + +export interface SupplierMailDelivery { + blNumber?: string | null; + blAmount?: string | number | null; + invoiceReference?: string | null; + scheduledDate?: string | Date | null; + deliveredDate?: string | Date | null; + supplier?: { + name?: string | null; + email?: string | null; + contact?: string | null; + } | null; + group?: { name?: string | null } | null; +} + +export interface SupplierMailSender { + firstName?: string | null; + lastName?: string | null; + username?: string | null; +} + +/** + * Nom lisible de l'expéditeur pour la signature du mail. + */ +export function getSenderName(user?: SupplierMailSender | null): string { + if (!user) return ""; + const fullName = [user.firstName, user.lastName].filter(Boolean).join(" ").trim(); + return fullName || user.username || ""; +} + +/** + * Objet du mail : identifie la livraison concernée. + */ +export function buildSupplierMailSubject(delivery: SupplierMailDelivery): string { + const deliveryDate = delivery.deliveredDate || delivery.scheduledDate; + const formattedDate = deliveryDate + ? safeFormat(deliveryDate, "dd/MM/yyyy", { defaultValue: "" }) + : ""; + + const reference = delivery.blNumber + ? `BL n° ${delivery.blNumber}` + : formattedDate + ? `Livraison du ${formattedDate}` + : "Livraison"; + + const parts = [ + "Demande de facture (PDF) ou BL (Excel)", + delivery.blNumber && formattedDate ? `${reference} du ${formattedDate}` : reference, + ]; + + if (delivery.group?.name) { + parts.push(delivery.group.name); + } + + return parts.join(" - "); +} + +/** + * Corps du mail : rappel des documents attendus + récapitulatif de la livraison. + * Seules les informations réellement renseignées sont listées. + */ +export function buildSupplierMailBody( + delivery: SupplierMailDelivery, + user?: SupplierMailSender | null +): string { + const deliveryDate = delivery.deliveredDate || delivery.scheduledDate; + const formattedDate = deliveryDate + ? safeFormat(deliveryDate, "dd/MM/yyyy", { defaultValue: "" }) + : ""; + + const blAmount = + delivery.blAmount !== null && delivery.blAmount !== undefined && delivery.blAmount !== "" + ? parseFloat(String(delivery.blAmount)) + : null; + + const details: string[] = []; + if (delivery.supplier?.name) details.push(`- Fournisseur : ${delivery.supplier.name}`); + if (delivery.group?.name) details.push(`- Magasin : ${delivery.group.name}`); + if (formattedDate) details.push(`- Date de livraison : ${formattedDate}`); + if (delivery.blNumber) details.push(`- N° de BL : ${delivery.blNumber}`); + if (blAmount !== null && !isNaN(blAmount)) { + details.push(`- Montant du BL : ${blAmount.toFixed(2)} €`); + } + if (delivery.invoiceReference) { + details.push(`- Référence facture : ${delivery.invoiceReference}`); + } + + const senderName = getSenderName(user); + const signature = [senderName, delivery.group?.name].filter(Boolean).join("\n"); + + const lines = [ + "Bonjour,", + "", + "Dans le cadre du rapprochement de nos bons de livraison et de vos factures, nous vous remercions de bien vouloir nous transmettre :", + "", + "- la facture au format PDF, ou", + "- le bon de livraison au format Excel", + "", + "Livraison concernée :", + ...(details.length > 0 ? details : ["- (informations de livraison à préciser)"]), + "", + "Vous en remerciant par avance.", + "", + "Cordialement,", + ...(signature ? [signature] : []), + ]; + + return lines.join("\n"); +} + +/** + * Construit le lien mailto complet (destinataire + objet + corps préremplis). + */ +export function buildSupplierMailtoUrl( + delivery: SupplierMailDelivery, + supplierEmail: string, + user?: SupplierMailSender | null +): string { + const subject = buildSupplierMailSubject(delivery); + // RFC 6068 : les sauts de ligne du corps doivent être encodés en CRLF (%0D%0A), + // sinon Outlook peut coller les lignes les unes aux autres. + const body = buildSupplierMailBody(delivery, user).replace(/\n/g, "\r\n"); + + return `mailto:${supplierEmail.trim()}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; +} + +/** + * Ouvre le client de messagerie par défaut (Outlook sur les postes du magasin). + */ +export function openMailClient(mailtoUrl: string): void { + const link = document.createElement("a"); + link.href = mailtoUrl; + link.style.display = "none"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} diff --git a/client/src/pages/BLReconciliation.tsx b/client/src/pages/BLReconciliation.tsx index 4926446..32dc655 100644 --- a/client/src/pages/BLReconciliation.tsx +++ b/client/src/pages/BLReconciliation.tsx @@ -12,13 +12,14 @@ import { useStore } from "@/contexts/StoreContext"; import { useAuthUnified } from "@/hooks/useAuthUnified"; import { usePermissions } from "@shared/permissions"; import { Pagination, usePagination } from "@/components/ui/pagination"; -import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter, Upload, CheckCircle, XCircle, Clock, MessageSquare } from "lucide-react"; +import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter, Upload, CheckCircle, XCircle, Clock, MessageSquare, Mail } 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 { Textarea } from "@/components/ui/textarea"; import ReconciliationComments from "@/components/ReconciliationComments"; import ReconciliationModal from "@/components/modals/ReconciliationModal"; +import { buildSupplierMailtoUrl, openMailClient } from "@/lib/supplierMail"; export default function BLReconciliation() { const { user } = useAuthUnified(); @@ -607,6 +608,35 @@ export default function BLReconciliation() { return (isNotValidated || hasNoInvoiceReference) && hasValidGroup; }; + // Email du fournisseur : donnée jointe à la livraison, avec repli sur la liste des fournisseurs + const getSupplierEmail = (delivery: any): string => { + const fromDelivery = delivery?.supplier?.email; + if (fromDelivery && String(fromDelivery).trim()) { + return String(fromDelivery).trim(); + } + + const supplier = suppliers.find((s: any) => s.id === delivery?.supplierId); + return supplier?.email ? String(supplier.email).trim() : ''; + }; + + // Ouvre Outlook (client mail par défaut) avec un message prérempli + // demandant la facture au format PDF ou le BL au format Excel + const handleRequestDocumentsByEmail = (delivery: any) => { + const supplierEmail = getSupplierEmail(delivery); + + if (!supplierEmail) { + toast({ + title: "Email fournisseur manquant", + description: `Aucune adresse email renseignée pour ${delivery.supplier?.name || 'ce fournisseur'}. Ajoutez-la depuis la page Contacts ou Fournisseurs.`, + variant: "destructive", + }); + return; + } + + const mailtoUrl = buildSupplierMailtoUrl(delivery, supplierEmail, user); + openMailClient(mailtoUrl); + }; + const handleQuickValidate = async (delivery: any) => { try { await apiRequest(`/api/deliveries/${delivery.id}`, "PUT", { @@ -1043,6 +1073,28 @@ export default function BLReconciliation() {
+ {(() => { + const supplierEmail = getSupplierEmail(delivery); + return ( + + ); + })()} {shouldShowInvoiceButton(delivery) && ( + ); + })()}