338 lines
12 KiB
TypeScript
338 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { Plus, FileText, Download, Eye, Calendar, RefreshCw, Trash2 } from "lucide-react";
|
|
import CreateNAAModal from "@/components/staff/CreateNAAModal";
|
|
|
|
type NAADocument = {
|
|
id: string;
|
|
naa_number: string;
|
|
referrer_code: string;
|
|
referrer_name?: string;
|
|
periode: string;
|
|
callsheet_date: string;
|
|
total_commission: number;
|
|
total_facture: number;
|
|
nbre_clients: number;
|
|
nbre_prestations: number;
|
|
status: string;
|
|
pdf_url?: string;
|
|
created_at: string;
|
|
};
|
|
|
|
function formatDate(dateString: string) {
|
|
return new Intl.DateTimeFormat("fr-FR", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric"
|
|
}).format(new Date(dateString));
|
|
}
|
|
|
|
function formatCurrency(amount: number) {
|
|
return new Intl.NumberFormat("fr-FR", {
|
|
style: "currency",
|
|
currency: "EUR"
|
|
}).format(amount);
|
|
}
|
|
|
|
export default function NAAPage() {
|
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
const [loadingPdf, setLoadingPdf] = useState<string | null>(null);
|
|
const [regenerating, setRegenerating] = useState<string | null>(null);
|
|
const queryClient = useQueryClient();
|
|
|
|
// Fonction pour régénérer le PDF
|
|
const handleRegeneratePdf = async (naaId: string) => {
|
|
if (!confirm("Voulez-vous régénérer le PDF de cette NAA ?")) {
|
|
return;
|
|
}
|
|
|
|
setRegenerating(naaId);
|
|
try {
|
|
const res = await fetch(`/api/staff/naa/${naaId}/regenerate`, {
|
|
method: "POST",
|
|
credentials: "include"
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error("Erreur lors de la régénération du PDF");
|
|
}
|
|
const data = await res.json();
|
|
|
|
// Rafraîchir la liste
|
|
queryClient.invalidateQueries({ queryKey: ["staff-naa-list"] });
|
|
|
|
// Ouvrir le PDF régénéré
|
|
if (data.presigned_url) {
|
|
window.open(data.presigned_url, "_blank");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error regenerating PDF:", error);
|
|
alert("Impossible de régénérer le PDF");
|
|
} finally {
|
|
setRegenerating(null);
|
|
}
|
|
};
|
|
|
|
// Fonction pour supprimer une NAA
|
|
const handleDeleteNaa = async (naaId: string, naaNumber: string) => {
|
|
if (!confirm(`Voulez-vous vraiment supprimer la NAA ${naaNumber} ?\n\nCette action est irréversible.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`/api/staff/naa/${naaId}`, {
|
|
method: "DELETE",
|
|
credentials: "include"
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error("Erreur lors de la suppression de la NAA");
|
|
}
|
|
|
|
// Rafraîchir la liste
|
|
queryClient.invalidateQueries({ queryKey: ["staff-naa-list"] });
|
|
|
|
} catch (error) {
|
|
console.error("Error deleting NAA:", error);
|
|
alert("Impossible de supprimer la NAA");
|
|
}
|
|
};
|
|
|
|
// Fonction pour ouvrir le PDF avec URL présignée
|
|
const handleViewPdf = async (naaId: string) => {
|
|
setLoadingPdf(naaId);
|
|
try {
|
|
const res = await fetch(`/api/staff/naa/${naaId}/presigned-url`, {
|
|
credentials: "include"
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error("Erreur lors de la récupération du PDF");
|
|
}
|
|
const data = await res.json();
|
|
window.open(data.presigned_url, "_blank");
|
|
} catch (error) {
|
|
console.error("Error opening PDF:", error);
|
|
alert("Impossible d'ouvrir le PDF");
|
|
} finally {
|
|
setLoadingPdf(null);
|
|
}
|
|
};
|
|
|
|
// Fonction pour télécharger le PDF
|
|
const handleDownloadPdf = async (naaId: string, naaNumber: string) => {
|
|
setLoadingPdf(naaId);
|
|
try {
|
|
const res = await fetch(`/api/staff/naa/${naaId}/presigned-url`, {
|
|
credentials: "include"
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error("Erreur lors de la récupération du PDF");
|
|
}
|
|
const data = await res.json();
|
|
|
|
// Télécharger le fichier
|
|
const pdfRes = await fetch(data.presigned_url);
|
|
const blob = await pdfRes.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `${naaNumber}.pdf`;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
} catch (error) {
|
|
console.error("Error downloading PDF:", error);
|
|
alert("Impossible de télécharger le PDF");
|
|
} finally {
|
|
setLoadingPdf(null);
|
|
}
|
|
};
|
|
|
|
// Récupération de la liste des NAA
|
|
const { data: naaList = [], isLoading } = useQuery<NAADocument[]>({
|
|
queryKey: ["staff-naa-list"],
|
|
queryFn: async () => {
|
|
const res = await fetch("/api/staff/naa", {
|
|
credentials: "include"
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error("Erreur lors de la récupération des NAA");
|
|
}
|
|
return res.json();
|
|
},
|
|
});
|
|
|
|
const handleCreateSuccess = () => {
|
|
queryClient.invalidateQueries({ queryKey: ["staff-naa-list"] });
|
|
setIsCreateModalOpen(false);
|
|
};
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
const badges = {
|
|
draft: "bg-slate-100 text-slate-700",
|
|
sent: "bg-blue-100 text-blue-700",
|
|
paid: "bg-green-100 text-green-700"
|
|
};
|
|
const labels = {
|
|
draft: "Brouillon",
|
|
sent: "Envoyée",
|
|
paid: "Payée"
|
|
};
|
|
return (
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${badges[status as keyof typeof badges] || badges.draft}`}>
|
|
{labels[status as keyof typeof labels] || status}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<main className="p-4 md:p-6">
|
|
<div className="mb-6 flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-800">
|
|
Notes Apporteurs d'Affaires
|
|
</h1>
|
|
<p className="text-slate-500 text-sm mt-1">
|
|
Gestion des commissions pour les apporteurs d'affaires
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsCreateModalOpen(true)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 transition"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Créer une NAA
|
|
</button>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="text-center py-12 text-slate-500">
|
|
Chargement...
|
|
</div>
|
|
) : naaList.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<FileText className="w-12 h-12 text-slate-300 mx-auto mb-3" />
|
|
<p className="text-slate-500">Aucune NAA créée pour le moment</p>
|
|
<button
|
|
onClick={() => setIsCreateModalOpen(true)}
|
|
className="mt-4 text-indigo-600 hover:text-indigo-700 font-medium"
|
|
>
|
|
Créer votre première NAA
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="bg-white rounded-2xl border overflow-hidden">
|
|
<table className="w-full">
|
|
<thead className="bg-slate-50 border-b">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
N° NAA
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
Apporteur
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
Période
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
Date
|
|
</th>
|
|
<th className="px-4 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
Clients
|
|
</th>
|
|
<th className="px-4 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
Commission
|
|
</th>
|
|
<th className="px-4 py-3 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
Total
|
|
</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
Statut
|
|
</th>
|
|
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase tracking-wider">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{naaList.map((naa) => (
|
|
<tr key={naa.id} className="hover:bg-slate-50 transition">
|
|
<td className="px-4 py-3 text-sm font-medium text-slate-800">
|
|
{naa.naa_number}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-slate-600">
|
|
{naa.referrer_name || naa.referrer_code}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-slate-600">
|
|
{naa.periode}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-slate-600">
|
|
{formatDate(naa.callsheet_date)}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-slate-600 text-right">
|
|
{naa.nbre_clients}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-slate-600 text-right font-medium">
|
|
{formatCurrency(naa.total_commission)}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-slate-800 text-right font-semibold">
|
|
{formatCurrency(naa.total_facture)}
|
|
</td>
|
|
<td className="px-4 py-3 text-center">
|
|
{getStatusBadge(naa.status)}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center justify-center gap-2">
|
|
{naa.pdf_url && (
|
|
<>
|
|
<button
|
|
onClick={() => handleViewPdf(naa.id)}
|
|
disabled={loadingPdf === naa.id || regenerating === naa.id}
|
|
className="p-1.5 text-slate-600 hover:text-indigo-600 hover:bg-indigo-50 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Voir le PDF"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDownloadPdf(naa.id, naa.naa_number)}
|
|
disabled={loadingPdf === naa.id || regenerating === naa.id}
|
|
className="p-1.5 text-slate-600 hover:text-green-600 hover:bg-green-50 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Télécharger le PDF"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleRegeneratePdf(naa.id)}
|
|
disabled={loadingPdf === naa.id || regenerating === naa.id}
|
|
className="p-1.5 text-slate-600 hover:text-orange-600 hover:bg-orange-50 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Régénérer le PDF"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 ${regenerating === naa.id ? 'animate-spin' : ''}`} />
|
|
</button>
|
|
</>
|
|
)}
|
|
<button
|
|
onClick={() => handleDeleteNaa(naa.id, naa.naa_number)}
|
|
disabled={loadingPdf === naa.id || regenerating === naa.id}
|
|
className="p-1.5 text-slate-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
|
title="Supprimer la NAA"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{isCreateModalOpen && (
|
|
<CreateNAAModal
|
|
onClose={() => setIsCreateModalOpen(false)}
|
|
onSuccess={handleCreateSuccess}
|
|
/>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|