Modales staff/virements-salaires
This commit is contained in:
parent
add33bdb8f
commit
72a6b157ca
2 changed files with 342 additions and 19 deletions
|
|
@ -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<string | null>(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({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirmation Modals */}
|
||||
<ConfirmationModal
|
||||
isOpen={showGeneratePdfConfirm}
|
||||
title="Générer la feuille d'appel"
|
||||
description="Voulez-vous générer la feuille d'appel PDF pour ce virement ? Cette action compilera tous les contrats associés à la période."
|
||||
confirmText="Générer"
|
||||
cancelText="Annuler"
|
||||
onConfirm={confirmGeneratePdf}
|
||||
onCancel={() => {
|
||||
setShowGeneratePdfConfirm(false);
|
||||
setPendingPdfTransferId(null);
|
||||
}}
|
||||
confirmButtonVariant="gradient"
|
||||
isLoading={generatingPdfForId !== null}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
isOpen={showDeleteConfirm}
|
||||
title="Supprimer ce virement"
|
||||
description="Êtes-vous sûr de vouloir supprimer ce virement ? Cette action est irréversible et supprimera toutes les données associées."
|
||||
confirmText="Supprimer"
|
||||
cancelText="Annuler"
|
||||
onConfirm={confirmDeleteTransfer}
|
||||
onCancel={() => setShowDeleteConfirm(false)}
|
||||
confirmButtonVariant="destructive"
|
||||
isLoading={deleting}
|
||||
/>
|
||||
|
||||
<NotifyClientModal
|
||||
isOpen={showNotifyClientModal}
|
||||
onClose={() => {
|
||||
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] : []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
225
components/staff/salary-transfers/NotifyClientModal.tsx
Normal file
225
components/staff/salary-transfers/NotifyClientModal.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 bg-white border-b px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<Mail className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900">Notification client</h2>
|
||||
<p className="text-sm text-slate-500">Appel de fonds n°{transfer.num_appel || transfer.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-6 space-y-6">
|
||||
{/* Warning */}
|
||||
<div className="p-4 bg-amber-50 border border-amber-200 rounded-xl flex gap-3">
|
||||
<AlertTriangle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-amber-900">
|
||||
<p className="font-semibold mb-1">Confirmation requise</p>
|
||||
<p>
|
||||
Vous êtes sur le point d'envoyer un email de notification pour l'appel à virement.
|
||||
Vérifiez les informations ci-dessous avant de confirmer.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Informations du virement */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide">
|
||||
Détails du virement
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-3 p-3 bg-slate-50 rounded-lg">
|
||||
<Building2 className="w-5 h-5 text-slate-500 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-slate-500 mb-1">Client</div>
|
||||
<div className="text-sm font-medium text-slate-900 truncate">
|
||||
{organizationName || "—"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-3 bg-slate-50 rounded-lg">
|
||||
<Calendar className="w-5 h-5 text-slate-500 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-slate-500 mb-1">Période</div>
|
||||
<div className="text-sm font-medium text-slate-900">
|
||||
{transfer.period_label || formatDate(transfer.period_month)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-3 bg-slate-50 rounded-lg">
|
||||
<Euro className="w-5 h-5 text-slate-500 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-slate-500 mb-1">Montant total</div>
|
||||
<div className="text-sm font-medium text-slate-900">
|
||||
{formatAmount(transfer.total_net)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-3 bg-slate-50 rounded-lg">
|
||||
<Calendar className="w-5 h-5 text-slate-500 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-slate-500 mb-1">Date d'échéance</div>
|
||||
<div className="text-sm font-medium text-slate-900">
|
||||
{formatDate(transfer.deadline)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Destinataires */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide">
|
||||
Destinataires de l'email
|
||||
</h3>
|
||||
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-xl space-y-3">
|
||||
{clientEmail && (
|
||||
<div>
|
||||
<div className="text-xs text-blue-700 font-medium mb-1">À :</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-sm text-blue-900 font-medium">{clientEmail}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ccEmails && ccEmails.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-blue-700 font-medium mb-1">CC :</div>
|
||||
<div className="space-y-1">
|
||||
{ccEmails.map((email, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-sm text-blue-900">{email}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!clientEmail && (!ccEmails || ccEmails.length === 0) && (
|
||||
<div className="text-sm text-slate-600 italic">
|
||||
Aucune adresse email configurée pour ce client
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenu de l'email */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide">
|
||||
Contenu de l'email
|
||||
</h3>
|
||||
|
||||
<div className="p-4 bg-slate-50 border border-slate-200 rounded-xl text-sm text-slate-700 space-y-2">
|
||||
<p>
|
||||
✉️ L'email contiendra :
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1 ml-2 text-slate-600">
|
||||
<li>Le numéro de la feuille d'appel</li>
|
||||
<li>La période concernée</li>
|
||||
<li>Le montant total à virer</li>
|
||||
<li>La date d'échéance</li>
|
||||
<li>Le lien vers la feuille d'appel PDF (si disponible)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="sticky bottom-0 bg-white border-t px-6 py-4 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="px-5 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Mail className="w-4 h-4" />
|
||||
Envoyer la notification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue