From 72a6b157cabcd9ac5e7d4bb4450c3de3f2434758 Mon Sep 17 00:00:00 2001 From: odentas Date: Tue, 14 Oct 2025 16:39:19 +0200 Subject: [PATCH] Modales staff/virements-salaires --- components/staff/SalaryTransfersGrid.tsx | 136 +++++++++-- .../salary-transfers/NotifyClientModal.tsx | 225 ++++++++++++++++++ 2 files changed, 342 insertions(+), 19 deletions(-) create mode 100644 components/staff/salary-transfers/NotifyClientModal.tsx diff --git a/components/staff/SalaryTransfersGrid.tsx b/components/staff/SalaryTransfersGrid.tsx index 231d3e4..c15b7ea 100644 --- a/components/staff/SalaryTransfersGrid.tsx +++ b/components/staff/SalaryTransfersGrid.tsx @@ -3,6 +3,9 @@ import { useEffect, useMemo, useState, useRef } from "react"; import { supabase } from "@/lib/supabaseClient"; import { Plus, Edit, Trash2, FileText, Save, X, CheckCircle2, XCircle } from "lucide-react"; +import { ConfirmationModal } from "@/components/ui/confirmation-modal"; +import { toast } from "sonner"; +import NotifyClientModal from "./salary-transfers/NotifyClientModal"; // Utility function to format dates as DD/MM/YYYY function formatDate(dateString: string | null | undefined): string { @@ -119,6 +122,16 @@ export default function SalaryTransfersGrid({ // Notification client const [sendingNotification, setSendingNotification] = useState(false); + const [showNotifyClientModal, setShowNotifyClientModal] = useState(false); + const [organizationDetails, setOrganizationDetails] = useState<{ + email_notifs?: string | null; + email_notifs_cc?: string | null; + } | null>(null); + + // Confirmation modals + const [showGeneratePdfConfirm, setShowGeneratePdfConfirm] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [pendingPdfTransferId, setPendingPdfTransferId] = useState(null); // optimistic update helper const selectedRow = useMemo(() => rows.find((r) => r.id === selectedId) ?? null, [rows, selectedId]); @@ -243,7 +256,7 @@ export default function SalaryTransfersGrid({ // Function to create a new salary transfer async function handleCreateTransfer() { if (!createForm.org_id || !createForm.period_month || !createForm.deadline || !createForm.mode || !createForm.num_appel) { - alert("Veuillez remplir tous les champs obligatoires"); + toast.error("Veuillez remplir tous les champs obligatoires"); return; } @@ -293,10 +306,10 @@ export default function SalaryTransfersGrid({ }); setShowCreateModal(false); - alert("Virement créé avec succès"); + toast.success("Virement créé avec succès"); } catch (err: any) { console.error("Create error:", err); - alert(err.message || "Erreur lors de la création"); + toast.error(err.message || "Erreur lors de la création"); } finally { setCreating(false); } @@ -304,9 +317,17 @@ export default function SalaryTransfersGrid({ // Function to generate PDF for a transfer async function handleGeneratePdf(transferId: string) { - if (!confirm("Générer la feuille d'appel pour ce virement ?")) return; + setPendingPdfTransferId(transferId); + setShowGeneratePdfConfirm(true); + } + + async function confirmGeneratePdf() { + if (!pendingPdfTransferId) return; + const transferId = pendingPdfTransferId; + setShowGeneratePdfConfirm(false); setGeneratingPdfForId(transferId); + try { const res = await fetch("/api/staff/virements-salaires/generate-pdf", { method: "POST", @@ -333,12 +354,13 @@ export default function SalaryTransfersGrid({ ) ); - alert(`PDF généré avec succès (${result.contracts_count} contrats)`); + toast.success(`PDF généré avec succès (${result.contracts_count} contrats)`); } catch (err: any) { console.error("Generate PDF error:", err); - alert(err.message || "Erreur lors de la génération du PDF"); + toast.error(err.message || "Erreur lors de la génération du PDF"); } finally { setGeneratingPdfForId(null); + setPendingPdfTransferId(null); } } @@ -432,10 +454,10 @@ export default function SalaryTransfersGrid({ setEditForm(result.data); setIsEditing(false); - alert("Virement modifié avec succès"); + toast.success("Virement modifié avec succès"); } catch (err: any) { console.error("Update error:", err); - alert(err.message || "Erreur lors de la modification"); + toast.error(err.message || "Erreur lors de la modification"); } finally { setUpdating(false); } @@ -443,13 +465,15 @@ export default function SalaryTransfersGrid({ // Function to delete a transfer async function handleDeleteTransfer() { + setShowDeleteConfirm(true); + } + + async function confirmDeleteTransfer() { if (!selectedTransfer || !selectedTransfer.id) return; - if (!confirm("Êtes-vous sûr de vouloir supprimer ce virement ? Cette action est irréversible.")) { - return; - } - + setShowDeleteConfirm(false); setDeleting(true); + try { const res = await fetch(`/api/staff/virements-salaires/${selectedTransfer.id}`, { method: "DELETE", @@ -469,10 +493,10 @@ export default function SalaryTransfersGrid({ setSelectedTransfer(null); setEditForm(null); - alert("Virement supprimé avec succès"); + toast.success("Virement supprimé avec succès"); } catch (err: any) { console.error("Delete error:", err); - alert(err.message || "Erreur lors de la suppression"); + toast.error(err.message || "Erreur lors de la suppression"); } finally { setDeleting(false); } @@ -480,13 +504,35 @@ export default function SalaryTransfersGrid({ // Function to send client notification async function handleNotifyClient() { - if (!selectedTransfer || !selectedTransfer.id) return; + if (!selectedTransfer || !selectedTransfer.id || !selectedTransfer.org_id) return; - if (!confirm("Envoyer la notification au client pour ce virement ?")) { - return; + // Charger les détails de l'organisation pour afficher les emails dans le modal + try { + const { data, error } = await supabase + .from("organization_details") + .select("email_notifs, email_notifs_cc") + .eq("org_id", selectedTransfer.org_id) + .single(); + + if (!error && data) { + setOrganizationDetails(data); + } else { + setOrganizationDetails(null); + } + } catch (err) { + console.error("Error loading organization details:", err); + setOrganizationDetails(null); } + setShowNotifyClientModal(true); + } + + async function confirmNotifyClient() { + if (!selectedTransfer || !selectedTransfer.id) return; + + setShowNotifyClientModal(false); setSendingNotification(true); + try { const res = await fetch(`/api/staff/virements-salaires/${selectedTransfer.id}/notify-client`, { method: "POST", @@ -515,10 +561,10 @@ export default function SalaryTransfersGrid({ prev ? { ...prev, notification_sent: true, notification_ok: true } : null ); - alert(`Notification envoyée avec succès à ${result.emailSentTo}${result.emailCc ? ` (CC: ${result.emailCc})` : ''}`); + toast.success(`Notification envoyée avec succès à ${result.emailSentTo}${result.emailCc ? ` (CC: ${result.emailCc})` : ''}`); } catch (err: any) { console.error("Notification error:", err); - alert(err.message || "Erreur lors de l'envoi de la notification"); + toast.error(err.message || "Erreur lors de l'envoi de la notification"); } finally { setSendingNotification(false); } @@ -1396,6 +1442,58 @@ export default function SalaryTransfersGrid({ )} + + {/* Confirmation Modals */} + { + setShowGeneratePdfConfirm(false); + setPendingPdfTransferId(null); + }} + confirmButtonVariant="gradient" + isLoading={generatingPdfForId !== null} + /> + + setShowDeleteConfirm(false)} + confirmButtonVariant="destructive" + isLoading={deleting} + /> + + { + setShowNotifyClientModal(false); + setOrganizationDetails(null); + }} + onConfirm={confirmNotifyClient} + transfer={selectedTransfer || { + id: "", + num_appel: null, + period_month: null, + period_label: null, + total_net: null, + deadline: null, + }} + organizationName={ + selectedTransfer?.org_id + ? organizations.find(org => org.id === selectedTransfer.org_id)?.name + : undefined + } + clientEmail={organizationDetails?.email_notifs || undefined} + ccEmails={organizationDetails?.email_notifs_cc ? [organizationDetails.email_notifs_cc] : []} + /> ); } \ No newline at end of file diff --git a/components/staff/salary-transfers/NotifyClientModal.tsx b/components/staff/salary-transfers/NotifyClientModal.tsx new file mode 100644 index 0000000..67bf180 --- /dev/null +++ b/components/staff/salary-transfers/NotifyClientModal.tsx @@ -0,0 +1,225 @@ +// components/staff/salary-transfers/NotifyClientModal.tsx +"use client"; + +import React from "react"; +import { X, Mail, AlertTriangle, Building2, Calendar, Euro } from "lucide-react"; + +type NotifyClientModalProps = { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + transfer: { + id: string; + num_appel?: string | null; + period_month?: string | null; + period_label?: string | null; + total_net?: string | null; + deadline?: string | null; + }; + organizationName?: string; + clientEmail?: string; + ccEmails?: string[]; +}; + +function formatDate(dateString: string | null | undefined): string { + if (!dateString) return "—"; + try { + const date = new Date(dateString); + return date.toLocaleDateString('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); + } catch { + return "—"; + } +} + +function formatAmount(amount: string | number | null | undefined): string { + if (!amount) return "—"; + try { + const num = typeof amount === 'string' ? parseFloat(amount) : amount; + return new Intl.NumberFormat('fr-FR', { + style: 'currency', + currency: 'EUR' + }).format(num); + } catch { + return String(amount) || "—"; + } +} + +export default function NotifyClientModal({ + isOpen, + onClose, + onConfirm, + transfer, + organizationName, + clientEmail, + ccEmails = [] +}: NotifyClientModalProps) { + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

Notification client

+

Appel de fonds n°{transfer.num_appel || transfer.id}

+
+
+ +
+ +
+ {/* Warning */} +
+ +
+

Confirmation requise

+

+ Vous êtes sur le point d'envoyer un email de notification pour l'appel à virement. + Vérifiez les informations ci-dessous avant de confirmer. +

+
+
+ + {/* Informations du virement */} +
+

+ Détails du virement +

+ +
+
+ +
+
Client
+
+ {organizationName || "—"} +
+
+
+ +
+ +
+
Période
+
+ {transfer.period_label || formatDate(transfer.period_month)} +
+
+
+ +
+ +
+
Montant total
+
+ {formatAmount(transfer.total_net)} +
+
+
+ +
+ +
+
Date d'échéance
+
+ {formatDate(transfer.deadline)} +
+
+
+
+
+ + {/* Destinataires */} +
+

+ Destinataires de l'email +

+ +
+ {clientEmail && ( +
+
À :
+
+ + {clientEmail} +
+
+ )} + + {ccEmails && ccEmails.length > 0 && ( +
+
CC :
+
+ {ccEmails.map((email, idx) => ( +
+ + {email} +
+ ))} +
+
+ )} + + {!clientEmail && (!ccEmails || ccEmails.length === 0) && ( +
+ Aucune adresse email configurée pour ce client +
+ )} +
+
+ + {/* Contenu de l'email */} +
+

+ Contenu de l'email +

+ +
+

+ ✉️ L'email contiendra : +

+
    +
  • Le numéro de la feuille d'appel
  • +
  • La période concernée
  • +
  • Le montant total à virer
  • +
  • La date d'échéance
  • +
  • Le lien vers la feuille d'appel PDF (si disponible)
  • +
+
+
+
+ + {/* Actions */} +
+ + +
+
+
+ ); +}