espace-paie-odentas/app/(app)/staff/facturation/create/page.tsx
2025-10-12 17:05:46 +02:00

364 lines
No EOL
12 KiB
TypeScript

"use client";
import { useState, useRef } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useQuery, useMutation } from "@tanstack/react-query";
import { api } from "@/lib/fetcher";
import { ArrowLeft, Save, Plus, Upload, FileX } from "lucide-react";
// ---------------- Types ----------------
type Organization = {
id: string;
structure_api: string;
};
type CreateInvoiceForm = {
org_id: string;
numero: string;
periode: string;
date: string;
montant_ht: string;
montant_ttc: string;
statut: string;
notes: string;
};
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="rounded-2xl border bg-white">
<div className="px-4 py-3 border-b font-medium text-slate-700 bg-slate-50/60">
{title}
</div>
<div className="p-4">{children}</div>
</section>
);
}
// -------------- Page --------------
export default function CreateInvoicePage() {
const router = useRouter();
const fileInputRef = useRef<HTMLInputElement>(null);
const [uploadedPdfKey, setUploadedPdfKey] = useState<string | null>(null);
const [isUploadingPdf, setIsUploadingPdf] = useState(false);
const [form, setForm] = useState<CreateInvoiceForm>({
org_id: "",
numero: "",
periode: "",
date: new Date().toISOString().split('T')[0],
montant_ht: "",
montant_ttc: "",
statut: "emise",
notes: "",
});
// Récupérer la liste des organisations
const { data: organizationsResponse } = useQuery<{ organizations: Organization[] }>({
queryKey: ["organizations"],
queryFn: () => api("/staff/organizations"),
});
const organizations = organizationsResponse?.organizations || [];
// Mutation pour créer la facture
const createMutation = useMutation({
mutationFn: (data: CreateInvoiceForm & { pdf_s3_key?: string }) =>
api("/staff/facturation", {
method: "POST",
body: JSON.stringify({
...data,
montant_ht: parseFloat(data.montant_ht) || 0,
montant_ttc: parseFloat(data.montant_ttc) || 0,
}),
headers: { "Content-Type": "application/json" }
}),
onSuccess: (newInvoice: any) => {
router.push(`/staff/facturation/${newInvoice.id}`);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!form.org_id || !form.montant_ttc) {
alert("Veuillez remplir au minimum l'organisation et le montant TTC.");
return;
}
createMutation.mutate({
...form,
pdf_s3_key: uploadedPdfKey || undefined
});
};
const updateForm = (field: keyof CreateInvoiceForm, value: string) => {
setForm(prev => ({ ...prev, [field]: value }));
};
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
if (file.type !== 'application/pdf') {
alert('Seuls les fichiers PDF sont acceptés.');
return;
}
if (!form.org_id || !form.numero) {
alert('Veuillez remplir l\'organisation et le numéro de facture avant d\'uploader le PDF.');
return;
}
setIsUploadingPdf(true);
try {
const formData = new FormData();
formData.append('pdf', file);
formData.append('org_id', form.org_id);
formData.append('invoice_number', form.numero);
const response = await fetch('/api/staff/facturation/upload-pdf', {
method: 'POST',
body: formData,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Upload failed');
}
const result = await response.json();
setUploadedPdfKey(result.s3_key);
} catch (error) {
console.error('Upload error:', error);
alert(`Erreur d'upload : ${error instanceof Error ? error.message : 'erreur inconnue'}`);
} finally {
setIsUploadingPdf(false);
}
};
const handleRemovePdf = () => {
setUploadedPdfKey(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<main className="space-y-5">
{/* Navigation */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Link href="/staff/facturation" className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800">
<ArrowLeft className="w-4 h-4" />
Retour aux factures
</Link>
<div className="text-slate-400">/</div>
<h1 className="text-xl font-bold text-slate-900">
Créer une nouvelle facture
</h1>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{/* Informations de base */}
<Section title="Informations de base">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Organisation cliente *
</label>
<select
value={form.org_id}
onChange={(e) => updateForm("org_id", e.target.value)}
className="w-full px-3 py-2 border rounded-lg bg-white"
required
>
<option value="">Sélectionner une organisation</option>
{organizations?.map((org) => (
<option key={org.id} value={org.id}>
{org.structure_api}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Numéro de facture
</label>
<input
type="text"
value={form.numero}
onChange={(e) => updateForm("numero", e.target.value)}
className="w-full px-3 py-2 border rounded-lg bg-white"
placeholder="ex: FAC-2025-001"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Période concernée
</label>
<input
type="text"
value={form.periode}
onChange={(e) => updateForm("periode", e.target.value)}
className="w-full px-3 py-2 border rounded-lg bg-white"
placeholder="ex: Janvier 2025"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Date d'émission
</label>
<input
type="date"
value={form.date}
onChange={(e) => updateForm("date", e.target.value)}
className="w-full px-3 py-2 border rounded-lg bg-white"
/>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Montant HT (€)
</label>
<input
type="number"
step="0.01"
value={form.montant_ht}
onChange={(e) => updateForm("montant_ht", e.target.value)}
className="w-full px-3 py-2 border rounded-lg bg-white"
placeholder="0.00"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Montant TTC (€) *
</label>
<input
type="number"
step="0.01"
value={form.montant_ttc}
onChange={(e) => updateForm("montant_ttc", e.target.value)}
className="w-full px-3 py-2 border rounded-lg bg-white"
placeholder="0.00"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Statut
</label>
<select
value={form.statut}
onChange={(e) => updateForm("statut", e.target.value)}
className="w-full px-3 py-2 border rounded-lg bg-white"
>
<option value="emise">Émise</option>
<option value="en_cours">En cours</option>
<option value="payee">Payée</option>
<option value="annulee">Annulée</option>
<option value="prete">Prête</option>
</select>
</div>
</div>
</div>
</Section>
{/* Notes */}
<Section title="Notes et PDF">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Notes sur la facture
</label>
<textarea
value={form.notes}
onChange={(e) => updateForm("notes", e.target.value)}
className="w-full px-3 py-2 border rounded-lg bg-white min-h-[100px]"
placeholder="Notes additionnelles..."
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
PDF de la facture
</label>
<div className="space-y-2">
<input
ref={fileInputRef}
type="file"
accept=".pdf"
onChange={handleFileUpload}
className="hidden"
/>
{uploadedPdfKey ? (
<div className="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-lg">
<span className="text-sm text-green-700">PDF uploadé avec succès</span>
<button
type="button"
onClick={handleRemovePdf}
className="inline-flex items-center gap-1 text-xs px-2 py-1 bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors"
>
<FileX className="w-3 h-3" />
Supprimer
</button>
</div>
) : (
<button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={isUploadingPdf || !form.org_id || !form.numero}
className="inline-flex items-center gap-2 px-4 py-2 border border-dashed border-slate-300 rounded-lg hover:border-slate-400 transition-colors disabled:opacity-50"
>
<Upload className="w-4 h-4" />
{isUploadingPdf ? "Upload en cours..." : "Choisir un fichier PDF"}
</button>
)}
{!form.org_id || !form.numero ? (
<p className="text-xs text-slate-500">
Veuillez d'abord remplir l'organisation et le numéro de facture
</p>
) : null}
</div>
</div>
</div>
</Section>
{/* Actions */}
<div className="flex items-center justify-end gap-3">
<Link
href="/staff/facturation"
className="px-4 py-2 text-slate-600 hover:text-slate-800 transition-colors"
>
Annuler
</Link>
<button
type="submit"
disabled={createMutation.isPending}
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
>
<Save className="w-4 h-4" />
{createMutation.isPending ? "Création..." : "Créer la facture"}
</button>
</div>
{createMutation.isError && (
<div className="p-3 bg-rose-100 border border-rose-200 rounded-lg text-rose-700">
Erreur lors de la création : {(createMutation.error as any)?.message || "erreur inconnue"}
</div>
)}
</form>
</main>
);
}