espace-paie-odentas/app/(app)/staff/naa/page.tsx

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>
);
}