- Conserver data URI complète (data:image/png;base64,...) lors de l'upload - Ajout script migration SQL pour logos existants - Compatible avec affichage et génération PDF PDFMonkey
1154 lines
No EOL
47 KiB
TypeScript
1154 lines
No EOL
47 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { useParams, useRouter } from "next/navigation";
|
||
import Link from "next/link";
|
||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||
import { Send, Loader2 } from "lucide-react";
|
||
|
||
type Organization = {
|
||
id: string;
|
||
name: string;
|
||
subscription_plan?: string;
|
||
subscription_status?: string;
|
||
monthly_fee?: number;
|
||
setup_fee?: number;
|
||
created_at?: string;
|
||
updated_at?: string;
|
||
};
|
||
|
||
type StructureInfos = {
|
||
raison_sociale?: string;
|
||
code_employeur?: string; // Remplace structure_api
|
||
siret?: string;
|
||
siren?: string;
|
||
forme_juridique?: string;
|
||
declaration?: string;
|
||
convention_collective?: string;
|
||
ccn_raccourci?: string;
|
||
code_ape?: string;
|
||
tva?: string;
|
||
rna?: string;
|
||
adresse_siege?: string;
|
||
adresse?: string;
|
||
cp?: string;
|
||
ville?: string;
|
||
presidente?: string;
|
||
tresoriere?: string;
|
||
structure_a_spectacles?: boolean;
|
||
entree_en_relation?: string;
|
||
logo_base64?: string;
|
||
|
||
// Nouveaux champs abonnement
|
||
statut?: string;
|
||
ouverture_compte?: string;
|
||
offre_speciale?: string;
|
||
notes?: string;
|
||
|
||
// Gestion paie
|
||
virements_salaires?: string;
|
||
agrement_aem?: string;
|
||
|
||
contact_principal?: string;
|
||
email?: string;
|
||
email_cc?: string; // Nouveau champ
|
||
email_signature?: string; // Nouveau champ
|
||
telephone?: string;
|
||
signataire_contrats?: string;
|
||
signataire_delegation?: boolean; // Boolean au lieu de string
|
||
// Responsable de traitement (RGPD)
|
||
nom_responsable_traitement?: string;
|
||
qualite_responsable_traitement?: string;
|
||
email_responsable_traitement?: string;
|
||
|
||
licence_spectacles?: string;
|
||
urssaf?: string;
|
||
audiens?: string;
|
||
conges_spectacles?: string;
|
||
pole_emploi_spectacle?: string;
|
||
recouvrement_pe_spectacle?: string;
|
||
afdas?: string;
|
||
fnas?: string;
|
||
fcap?: string;
|
||
};
|
||
|
||
type ClientData = {
|
||
organization: Organization;
|
||
structureInfos: StructureInfos;
|
||
details: any;
|
||
};
|
||
|
||
type Referrer = {
|
||
id: string;
|
||
code: string;
|
||
name: string;
|
||
contact_name: string;
|
||
};
|
||
|
||
function Line({ label, value }: { label: string; value?: string | number | null }) {
|
||
return (
|
||
<div className="grid grid-cols-3 gap-2 border-b last:border-b-0 py-2">
|
||
<div className="text-slate-500">{label}</div>
|
||
<div className="col-span-2">{value ?? "—"}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function LogoLine({ label, value }: { label: string; value?: string | null }) {
|
||
return (
|
||
<div className="grid grid-cols-3 gap-2 border-b last:border-b-0 py-2">
|
||
<div className="text-slate-500">{label}</div>
|
||
<div className="col-span-2">
|
||
{value ? (
|
||
<img
|
||
src={value.startsWith('data:') ? value : `data:image/png;base64,${value}`}
|
||
alt="Logo"
|
||
className="max-w-32 max-h-32 object-contain border rounded"
|
||
/>
|
||
) : (
|
||
"—"
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function EditableLine({
|
||
label,
|
||
value,
|
||
onChange,
|
||
type = "text",
|
||
options
|
||
}: {
|
||
label: string;
|
||
value?: string | number | null;
|
||
onChange: (value: string) => void;
|
||
type?: "text" | "email" | "number" | "date" | "select";
|
||
options?: { value: string; label: string }[];
|
||
}) {
|
||
return (
|
||
<div className="grid grid-cols-3 gap-2 border-b last:border-b-0 py-2">
|
||
<div className="text-slate-500">{label}</div>
|
||
<div className="col-span-2">
|
||
{type === "select" && options ? (
|
||
<select
|
||
value={value || ""}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
className="w-full px-2 py-1 text-sm border rounded"
|
||
>
|
||
<option value="">—</option>
|
||
{options.map((opt) => (
|
||
<option key={opt.value} value={opt.value}>
|
||
{opt.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
) : (
|
||
<input
|
||
type={type}
|
||
value={value || ""}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
className="w-full px-2 py-1 text-sm border rounded"
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ImageUpload({
|
||
label,
|
||
value,
|
||
onChange
|
||
}: {
|
||
label: string;
|
||
value?: string | null;
|
||
onChange: (base64: string) => void;
|
||
}) {
|
||
const [preview, setPreview] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
if (value) {
|
||
// Si la valeur commence par "data:", c'est déjà un data URL complet
|
||
// Sinon, on ajoute le préfixe pour les images
|
||
const imageUrl = value.startsWith('data:') ? value : `data:image/png;base64,${value}`;
|
||
setPreview(imageUrl);
|
||
} else {
|
||
setPreview(null);
|
||
}
|
||
}, [value]);
|
||
|
||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = event.target.files?.[0];
|
||
if (!file) return;
|
||
|
||
// Vérifier que c'est bien une image
|
||
if (!file.type.startsWith('image/')) {
|
||
alert('Veuillez sélectionner un fichier image');
|
||
return;
|
||
}
|
||
|
||
// Vérifier la taille (max 5MB)
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
alert('Le fichier est trop volumineux (max 5MB)');
|
||
return;
|
||
}
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
const result = e.target?.result as string;
|
||
if (result) {
|
||
// Conserver la data URI complète (avec le préfixe data:image/...;base64,)
|
||
// pour compatibilité avec PDFMonkey et l'affichage
|
||
setPreview(result);
|
||
onChange(result);
|
||
}
|
||
};
|
||
reader.readAsDataURL(file);
|
||
};
|
||
|
||
const handleRemove = () => {
|
||
setPreview(null);
|
||
onChange('');
|
||
};
|
||
|
||
return (
|
||
<div className="grid grid-cols-3 gap-2 border-b last:border-b-0 py-2">
|
||
<div className="text-slate-500">{label}</div>
|
||
<div className="col-span-2 space-y-2">
|
||
{preview && (
|
||
<div className="relative inline-block">
|
||
<img
|
||
src={preview}
|
||
alt="Logo"
|
||
className="max-w-32 max-h-32 object-contain border rounded"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={handleRemove}
|
||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs hover:bg-red-600"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
)}
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={handleFileChange}
|
||
className="w-full px-2 py-1 text-sm border rounded file:mr-2 file:py-1 file:px-2 file:border-0 file:text-sm file:bg-slate-100 file:text-slate-700 file:rounded"
|
||
/>
|
||
<div className="text-xs text-slate-500">
|
||
Formats acceptés: JPG, PNG, GIF (max 5MB)
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function formatDate(d?: string | null) {
|
||
if (!d) return "—";
|
||
try {
|
||
return new Intl.DateTimeFormat("fr-FR", {
|
||
dateStyle: "medium",
|
||
timeStyle: "short"
|
||
}).format(new Date(d));
|
||
} catch {
|
||
return d;
|
||
}
|
||
}
|
||
|
||
export default function ClientDetailPage() {
|
||
const params = useParams();
|
||
const router = useRouter();
|
||
const queryClient = useQueryClient();
|
||
const clientId = params.id as string;
|
||
|
||
const [isEditing, setIsEditing] = useState(false);
|
||
const [editData, setEditData] = useState<Partial<Organization & StructureInfos & any>>({});
|
||
const [showSepaMandateModal, setShowSepaMandateModal] = useState(false);
|
||
|
||
// Récupération de la liste des apporteurs
|
||
const { data: referrers = [] } = useQuery<Referrer[]>({
|
||
queryKey: ["referrers"],
|
||
queryFn: async () => {
|
||
const res = await fetch("/api/staff/referrers", {
|
||
cache: "no-store",
|
||
credentials: "include"
|
||
});
|
||
if (!res.ok) return [];
|
||
return res.json();
|
||
},
|
||
});
|
||
|
||
// Récupération des données du client
|
||
const {
|
||
data: clientData,
|
||
isLoading,
|
||
error
|
||
} = useQuery<ClientData>({
|
||
queryKey: ["staff-client", clientId],
|
||
queryFn: async () => {
|
||
const res = await fetch(`/api/staff/clients/${clientId}`, {
|
||
cache: "no-store",
|
||
credentials: "include"
|
||
});
|
||
if (!res.ok) {
|
||
throw new Error("Erreur lors de la récupération du client");
|
||
}
|
||
return res.json();
|
||
},
|
||
enabled: !!clientId,
|
||
});
|
||
|
||
// Mutation pour la mise à jour
|
||
const updateMutation = useMutation({
|
||
mutationFn: async (data: any) => {
|
||
console.log("📤 [CLIENT UPDATE] Données envoyées:", data);
|
||
const res = await fetch(`/api/staff/clients/${clientId}`, {
|
||
method: "PUT",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
credentials: "include",
|
||
body: JSON.stringify(data),
|
||
});
|
||
if (!res.ok) {
|
||
const errorData = await res.json().catch(() => ({}));
|
||
console.error("❌ [CLIENT UPDATE] Erreur API:", {
|
||
status: res.status,
|
||
statusText: res.statusText,
|
||
errorData
|
||
});
|
||
throw new Error(`Erreur lors de la mise à jour: ${errorData.error || res.statusText}`);
|
||
}
|
||
return res.json();
|
||
},
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ["staff-client", clientId] });
|
||
setIsEditing(false);
|
||
setEditData({});
|
||
},
|
||
});
|
||
|
||
// Mutation pour envoyer la demande de mandat SEPA
|
||
const sendSepaMandateMutation = useMutation({
|
||
mutationFn: async () => {
|
||
const res = await fetch(`/api/staff/clients/${clientId}/request-sepa-mandate`, {
|
||
method: "POST",
|
||
credentials: "include",
|
||
});
|
||
if (!res.ok) {
|
||
const errorData = await res.json().catch(() => ({}));
|
||
throw new Error(errorData.message || "Erreur lors de l'envoi de la demande");
|
||
}
|
||
return res.json();
|
||
},
|
||
onSuccess: () => {
|
||
setShowSepaMandateModal(false);
|
||
alert("Demande de mandat SEPA envoyée avec succès !");
|
||
},
|
||
onError: (error: any) => {
|
||
alert(`Erreur: ${error.message}`);
|
||
},
|
||
});
|
||
|
||
// Initialiser les données d'édition
|
||
useEffect(() => {
|
||
if (clientData && isEditing) {
|
||
const { organization, structureInfos, details } = clientData;
|
||
setEditData({
|
||
// Organisation
|
||
name: organization.name,
|
||
statut: structureInfos.statut,
|
||
ouverture_compte: structureInfos.ouverture_compte,
|
||
offre_speciale: structureInfos.offre_speciale,
|
||
notes: structureInfos.notes,
|
||
|
||
// Gestion paie
|
||
virements_salaires: structureInfos.virements_salaires,
|
||
agrement_aem: structureInfos.agrement_aem,
|
||
|
||
// Apporteur d'affaires
|
||
is_referred: details.is_referred,
|
||
referrer_code: details.referrer_code,
|
||
commission_rate: details.commission_rate,
|
||
|
||
// Structure infos
|
||
code_employeur: structureInfos.code_employeur, // Remplace structure_api
|
||
siret: structureInfos.siret,
|
||
siren: structureInfos.siren,
|
||
forme_juridique: structureInfos.forme_juridique,
|
||
declaration: structureInfos.declaration,
|
||
ccn: structureInfos.convention_collective,
|
||
ccn_raccourci: structureInfos.ccn_raccourci,
|
||
ape: structureInfos.code_ape,
|
||
tva: structureInfos.tva,
|
||
rna: structureInfos.rna,
|
||
adresse_siege: structureInfos.adresse_siege,
|
||
adresse: structureInfos.adresse,
|
||
cp: structureInfos.cp,
|
||
ville: structureInfos.ville,
|
||
president: structureInfos.presidente,
|
||
tresorier: structureInfos.tresoriere,
|
||
structure_a_spectacles: structureInfos.structure_a_spectacles,
|
||
entree_en_relation: structureInfos.entree_en_relation,
|
||
logo_base64: structureInfos.logo_base64,
|
||
|
||
// Contact
|
||
nom_contact: details.nom_contact,
|
||
prenom_contact: details.prenom_contact,
|
||
email_notifs: structureInfos.email, // Modifié pour utiliser email_notifs
|
||
email_notifs_cc: structureInfos.email_cc, // Nouveau champ
|
||
email_signature: structureInfos.email_signature, // Nouveau champ
|
||
tel_contact: structureInfos.telephone,
|
||
prenom_signataire: details.prenom_signataire,
|
||
nom_signataire: details.nom_signataire,
|
||
qualite_signataire: details.qualite_signataire,
|
||
delegation_signature: structureInfos.signataire_delegation, // Boolean
|
||
|
||
// Responsable de traitement (RGPD)
|
||
nom_responsable_traitement: structureInfos.nom_responsable_traitement,
|
||
qualite_responsable_traitement: structureInfos.qualite_responsable_traitement,
|
||
email_responsable_traitement: structureInfos.email_responsable_traitement,
|
||
|
||
// Facturation (SEPA)
|
||
iban: details.iban,
|
||
bic: details.bic,
|
||
id_mandat_sepa: details.id_mandat_sepa,
|
||
|
||
// Caisses
|
||
licence_spectacles: structureInfos.licence_spectacles,
|
||
urssaf: structureInfos.urssaf,
|
||
audiens: structureInfos.audiens,
|
||
conges_spectacles_id: structureInfos.conges_spectacles,
|
||
pole_emploi_id: structureInfos.pole_emploi_spectacle,
|
||
recouvrement_pe_id: structureInfos.recouvrement_pe_spectacle,
|
||
afdas_id: structureInfos.afdas,
|
||
fnas_id: structureInfos.fnas,
|
||
fcap_id: structureInfos.fcap,
|
||
});
|
||
}
|
||
}, [clientData, isEditing]);
|
||
|
||
const handleSave = () => {
|
||
updateMutation.mutate(editData);
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
setIsEditing(false);
|
||
setEditData({});
|
||
};
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<main className="p-4 md:p-6">
|
||
<div className="text-slate-500">Chargement...</div>
|
||
</main>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<main className="p-4 md:p-6">
|
||
<div className="text-red-600">Erreur: {error.message}</div>
|
||
<Link href="/staff/clients" className="text-blue-600 underline">
|
||
Retour à la liste des clients
|
||
</Link>
|
||
</main>
|
||
);
|
||
}
|
||
|
||
if (!clientData) {
|
||
return (
|
||
<main className="p-4 md:p-6">
|
||
<div className="text-slate-500">Client non trouvé</div>
|
||
<Link href="/staff/clients" className="text-blue-600 underline">
|
||
Retour à la liste des clients
|
||
</Link>
|
||
</main>
|
||
);
|
||
}
|
||
|
||
const { organization, structureInfos } = clientData;
|
||
|
||
return (
|
||
<main className="p-4 md:p-6 space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<Link
|
||
href="/staff/clients"
|
||
className="text-blue-600 hover:text-blue-800 text-sm mb-2 inline-block"
|
||
>
|
||
← Retour aux clients
|
||
</Link>
|
||
<h1 className="text-lg font-semibold">Détail client : {organization.name}</h1>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
{isEditing ? (
|
||
<>
|
||
<button
|
||
onClick={handleCancel}
|
||
className="px-3 py-2 text-sm border rounded-lg hover:bg-slate-50"
|
||
disabled={updateMutation.isPending}
|
||
>
|
||
Annuler
|
||
</button>
|
||
<button
|
||
onClick={handleSave}
|
||
className="px-3 py-2 text-sm bg-emerald-600 text-white rounded-lg hover:bg-emerald-700"
|
||
disabled={updateMutation.isPending}
|
||
>
|
||
{updateMutation.isPending ? "Sauvegarde..." : "Sauvegarder"}
|
||
</button>
|
||
</>
|
||
) : (
|
||
<button
|
||
onClick={() => setIsEditing(true)}
|
||
className="px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||
>
|
||
Modifier
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{updateMutation.error && (
|
||
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-red-800">
|
||
Erreur lors de la sauvegarde : {updateMutation.error.message}
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-start">
|
||
<div className="space-y-4">
|
||
{/* Informations principales + Votre structure */}
|
||
<section className="rounded-2xl border bg-white">
|
||
<div className="px-4 py-3 border-b">
|
||
<h2 className="font-medium">Informations principales</h2>
|
||
</div>
|
||
<div className="p-4 text-sm">
|
||
{isEditing ? (
|
||
<div className="space-y-2">
|
||
<EditableLine
|
||
label="Nom"
|
||
value={editData.name}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, name: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Code employeur"
|
||
value={editData.code_employeur}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, code_employeur: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="SIRET"
|
||
value={editData.siret}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, siret: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="SIREN"
|
||
value={editData.siren}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, siren: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Forme juridique"
|
||
value={editData.forme_juridique}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, forme_juridique: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Déclaration"
|
||
value={editData.declaration}
|
||
type="date"
|
||
onChange={(value) => setEditData(prev => ({ ...prev, declaration: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Convention collective"
|
||
value={editData.ccn}
|
||
type="select"
|
||
options={[
|
||
{ value: "Convention Collective Nationale des Entreprises Artistiques & Culturelles", label: "Convention Collective Nationale des Entreprises Artistiques & Culturelles" },
|
||
{ value: "Convention Collective Nationale du Spectacle Vivant Privé", label: "Convention Collective Nationale du Spectacle Vivant Privé" },
|
||
{ value: "Convention Collective Nationale des Prestataires de Services du Secteur Tertiaire", label: "Convention Collective Nationale des Prestataires de Services du Secteur Tertiaire" },
|
||
{ value: "Convention Collective Nationale de la Production Audiovisuelle", label: "Convention Collective Nationale de la Production Audiovisuelle" },
|
||
{ value: "Convention Collective Nationale de l'Édition", label: "Convention Collective Nationale de l'Édition" },
|
||
{ value: "Non concerné", label: "Non concerné" },
|
||
]}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, ccn: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="CCN raccourci"
|
||
value={editData.ccn_raccourci}
|
||
type="select"
|
||
options={[
|
||
{ value: "CCNEAC", label: "CCNEAC" },
|
||
{ value: "CCNSVP", label: "CCNSVP" },
|
||
{ value: "CCNPSST", label: "CCNPSST" },
|
||
{ value: "CCNPA", label: "CCNPA" },
|
||
{ value: "CCNE", label: "CCNE" },
|
||
{ value: "NC", label: "NC" },
|
||
]}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, ccn_raccourci: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Code APE"
|
||
value={editData.ape}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, ape: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="TVA intracommunautaire"
|
||
value={editData.tva}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, tva: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="N° RNA"
|
||
value={editData.rna}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, rna: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Adresse"
|
||
value={editData.adresse}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, adresse: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Code postal"
|
||
value={editData.cp}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, cp: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Ville"
|
||
value={editData.ville}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, ville: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Adresse siège"
|
||
value={editData.adresse_siege}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, adresse_siege: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Président(e)"
|
||
value={editData.president}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, president: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Trésorier(ère)"
|
||
value={editData.tresorier}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, tresorier: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Licence spectacle"
|
||
value={editData.licence_spectacles}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, licence_spectacles: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Structure à spectacles ?"
|
||
value={editData.structure_a_spectacles ? "Oui" : "Non"}
|
||
type="select"
|
||
options={[
|
||
{ value: "true", label: "Oui" },
|
||
{ value: "false", label: "Non" },
|
||
]}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, structure_a_spectacles: value === "true" }))}
|
||
/>
|
||
<ImageUpload
|
||
label="Logo"
|
||
value={editData.logo_base64}
|
||
onChange={(base64) => setEditData(prev => ({ ...prev, logo_base64: base64 }))}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
<Line label="Nom" value={organization.name} />
|
||
<Line label="Code employeur" value={structureInfos.code_employeur} />
|
||
<Line label="ID" value={organization.id} />
|
||
<Line label="SIRET" value={structureInfos.siret} />
|
||
<Line label="SIREN" value={structureInfos.siren} />
|
||
<Line label="Forme juridique" value={structureInfos.forme_juridique} />
|
||
<Line label="Déclaration" value={structureInfos.declaration} />
|
||
<Line label="Convention collective" value={structureInfos.convention_collective} />
|
||
<Line label="CCN raccourci" value={structureInfos.ccn_raccourci} />
|
||
<Line label="Code APE" value={structureInfos.code_ape} />
|
||
<Line label="TVA intracommunautaire" value={structureInfos.tva} />
|
||
<Line label="N° RNA" value={structureInfos.rna} />
|
||
<Line label="Adresse" value={structureInfos.adresse} />
|
||
<Line label="Code postal" value={structureInfos.cp} />
|
||
<Line label="Ville" value={structureInfos.ville} />
|
||
<Line label="Adresse siège" value={structureInfos.adresse_siege} />
|
||
<Line label="Président(e)" value={structureInfos.presidente} />
|
||
<Line label="Trésorier(ère)" value={structureInfos.tresoriere} />
|
||
<Line label="Licence spectacle" value={structureInfos.licence_spectacles} />
|
||
<Line label="Structure à spectacles ?" value={structureInfos.structure_a_spectacles ? "Oui" : "Non"} />
|
||
<LogoLine label="Logo" value={structureInfos.logo_base64} />
|
||
<Line label="Créé le" value={formatDate(organization.created_at)} />
|
||
<Line label="Mis à jour le" value={formatDate(organization.updated_at)} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{/* Abonnement */}
|
||
<section className="rounded-2xl border bg-white">
|
||
<div className="px-4 py-3 border-b">
|
||
<h2 className="font-medium">Abonnement</h2>
|
||
</div>
|
||
<div className="p-4 text-sm">
|
||
{isEditing ? (
|
||
<div className="space-y-2">
|
||
{/* Section Contrat */}
|
||
<div className="pb-2">
|
||
<div className="text-xs font-semibold text-slate-600 uppercase tracking-wide mb-2">Contrat</div>
|
||
<EditableLine
|
||
label="Entrée en relation"
|
||
value={editData.entree_en_relation}
|
||
type="date"
|
||
onChange={(value) => setEditData(prev => ({ ...prev, entree_en_relation: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Statut"
|
||
value={editData.statut}
|
||
type="select"
|
||
options={[
|
||
{ value: "Actif", label: "Actif" },
|
||
{ value: "Ancien client", label: "Ancien client" },
|
||
]}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, statut: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Ouverture de compte"
|
||
value={editData.ouverture_compte}
|
||
type="select"
|
||
options={[
|
||
{ value: "Simple", label: "Simple" },
|
||
{ value: "Complexe", label: "Complexe" },
|
||
]}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, ouverture_compte: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Offre spéciale"
|
||
value={editData.offre_speciale}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, offre_speciale: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Note"
|
||
value={editData.notes}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, notes: value }))}
|
||
/>
|
||
</div>
|
||
|
||
{/* Section Apporteur d'Affaires */}
|
||
<div className="pt-2 border-t">
|
||
<div className="text-xs font-semibold text-slate-600 uppercase tracking-wide mb-2">Apporteur d'affaires</div>
|
||
<EditableLine
|
||
label="Client apporté ?"
|
||
value={editData.is_referred ? "true" : "false"}
|
||
type="select"
|
||
options={[
|
||
{ value: "false", label: "Non" },
|
||
{ value: "true", label: "Oui" },
|
||
]}
|
||
onChange={(value) => setEditData(prev => ({
|
||
...prev,
|
||
is_referred: value === "true",
|
||
referrer_code: value === "false" ? undefined : prev.referrer_code,
|
||
commission_rate: value === "false" ? undefined : prev.commission_rate
|
||
}))}
|
||
/>
|
||
|
||
{editData.is_referred && (
|
||
<>
|
||
<EditableLine
|
||
label="Apporteur"
|
||
value={editData.referrer_code}
|
||
type="select"
|
||
options={referrers.map(r => ({ value: r.code, label: r.name }))}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, referrer_code: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Taux de commission"
|
||
value={editData.commission_rate}
|
||
type="number"
|
||
onChange={(value) => setEditData(prev => ({ ...prev, commission_rate: value }))}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* Section Facturation */}
|
||
<div className="pt-2 border-t">
|
||
<div className="text-xs font-semibold text-slate-600 uppercase tracking-wide mb-2">Facturation</div>
|
||
<EditableLine
|
||
label="IBAN"
|
||
value={editData.iban}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, iban: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="BIC"
|
||
value={editData.bic}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, bic: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="ID mandat SEPA"
|
||
value={editData.id_mandat_sepa}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, id_mandat_sepa: value }))}
|
||
/>
|
||
</div>
|
||
|
||
{/* Section Gestion paie */}
|
||
<div className="pt-2 border-t">
|
||
<div className="text-xs font-semibold text-slate-600 uppercase tracking-wide mb-2">Gestion paie</div>
|
||
<EditableLine
|
||
label="Virements salaires"
|
||
value={editData.virements_salaires}
|
||
type="select"
|
||
options={[
|
||
{ value: "Odentas", label: "Odentas" },
|
||
{ value: "Client", label: "Client (structure)" },
|
||
]}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, virements_salaires: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="AEM"
|
||
value={editData.agrement_aem}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, agrement_aem: value }))}
|
||
/>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{/* Section Contrat */}
|
||
<div className="pb-2">
|
||
<div className="text-xs font-semibold text-slate-600 uppercase tracking-wide mb-2">Contrat</div>
|
||
<Line label="Entrée en relation" value={structureInfos.entree_en_relation} />
|
||
<Line label="Statut" value={structureInfos.statut} />
|
||
<Line label="Ouverture de compte" value={structureInfos.ouverture_compte} />
|
||
<Line label="Offre spéciale" value={structureInfos.offre_speciale} />
|
||
<Line label="Note" value={structureInfos.notes} />
|
||
</div>
|
||
|
||
{/* Section Apporteur d'Affaires */}
|
||
<div className="pt-2 border-t">
|
||
<div className="text-xs font-semibold text-slate-600 uppercase tracking-wide mb-2">Apporteur d'affaires</div>
|
||
<Line
|
||
label="Client apporté ?"
|
||
value={clientData.details.is_referred ? "Oui" : "Non"}
|
||
/>
|
||
{clientData.details.is_referred && (
|
||
<>
|
||
<Line
|
||
label="Apporteur"
|
||
value={referrers.find(r => r.code === clientData.details.referrer_code)?.name || clientData.details.referrer_code}
|
||
/>
|
||
<Line
|
||
label="Taux de commission"
|
||
value={clientData.details.commission_rate ? `${(clientData.details.commission_rate * 100).toFixed(2)}%` : "—"}
|
||
/>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* Section Facturation */}
|
||
<div className="pt-2 border-t">
|
||
<div className="text-xs font-semibold text-slate-600 uppercase tracking-wide mb-2">Facturation</div>
|
||
|
||
{/* Carte Statut Mandat SEPA */}
|
||
<div className="mb-3">
|
||
{clientData.details.id_mandat_sepa ? (
|
||
<div className="flex items-center gap-3 px-4 py-3 bg-gradient-to-r from-emerald-50 to-emerald-100 border border-emerald-200 rounded-xl">
|
||
<div className="flex-shrink-0 w-10 h-10 bg-emerald-500 rounded-full flex items-center justify-center">
|
||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
</div>
|
||
<div className="flex-1">
|
||
<div className="font-semibold text-emerald-900">Mandat SEPA actif</div>
|
||
<div className="text-sm text-emerald-700">Les prélèvements automatiques sont activés</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center gap-3 px-4 py-3 bg-gradient-to-r from-amber-50 to-amber-100 border border-amber-200 rounded-xl">
|
||
<div className="flex-shrink-0 w-10 h-10 bg-amber-500 rounded-full flex items-center justify-center">
|
||
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||
</svg>
|
||
</div>
|
||
<div className="flex-1">
|
||
<div className="font-semibold text-amber-900">Aucun mandat SEPA</div>
|
||
<div className="text-sm text-amber-700">Paiement par virement uniquement</div>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowSepaMandateModal(true)}
|
||
className="flex-shrink-0 inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"
|
||
>
|
||
<Send className="w-4 h-4" />
|
||
Demande de mandat
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<Line label="IBAN" value={clientData.details.iban} />
|
||
<Line label="BIC" value={clientData.details.bic} />
|
||
<Line label="ID mandat SEPA" value={clientData.details.id_mandat_sepa} />
|
||
</div>
|
||
|
||
{/* Section Gestion paie */}
|
||
<div className="pt-2 border-t">
|
||
<div className="text-xs font-semibold text-slate-600 uppercase tracking-wide mb-2">Gestion paie</div>
|
||
<Line
|
||
label="Virements salaires"
|
||
value={structureInfos.virements_salaires || "—"}
|
||
/>
|
||
<Line
|
||
label="AEM"
|
||
value={structureInfos.agrement_aem || "—"}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Informations de contact */}
|
||
<section className="rounded-2xl border bg-white">
|
||
<div className="px-4 py-3 border-b">
|
||
<h2 className="font-medium">Informations de contact</h2>
|
||
</div>
|
||
<div className="p-4 text-sm">
|
||
{isEditing ? (
|
||
<div className="space-y-2">
|
||
<EditableLine
|
||
label="Prénom contact"
|
||
value={editData.prenom_contact}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, prenom_contact: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Nom contact"
|
||
value={editData.nom_contact}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, nom_contact: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Email"
|
||
value={editData.email_notifs}
|
||
type="email"
|
||
onChange={(value) => setEditData(prev => ({ ...prev, email_notifs: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Email CC"
|
||
value={editData.email_notifs_cc}
|
||
type="email"
|
||
onChange={(value) => setEditData(prev => ({ ...prev, email_notifs_cc: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Email signature"
|
||
value={editData.email_signature}
|
||
type="email"
|
||
onChange={(value) => setEditData(prev => ({ ...prev, email_signature: value }))}
|
||
/>
|
||
{/* Responsable de traitement (RGPD) */}
|
||
<EditableLine
|
||
label="Nom responsable de traitement"
|
||
value={editData.nom_responsable_traitement}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, nom_responsable_traitement: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Qualité responsable de traitement"
|
||
value={editData.qualite_responsable_traitement}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, qualite_responsable_traitement: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Email responsable de traitement"
|
||
value={editData.email_responsable_traitement}
|
||
type="email"
|
||
onChange={(value) => setEditData(prev => ({ ...prev, email_responsable_traitement: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Téléphone"
|
||
value={editData.tel_contact}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, tel_contact: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Prénom signataire"
|
||
value={editData.prenom_signataire}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, prenom_signataire: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Nom signataire"
|
||
value={editData.nom_signataire}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, nom_signataire: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Qualité signataire"
|
||
value={editData.qualite_signataire}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, qualite_signataire: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Signataire agissant par délégation ?"
|
||
value={editData.delegation_signature ? "Oui" : "Non"}
|
||
type="select"
|
||
options={[
|
||
{ value: "true", label: "Oui" },
|
||
{ value: "false", label: "Non" },
|
||
]}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, delegation_signature: value === "true" }))}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
<Line label="Contact principal" value={structureInfos.contact_principal} />
|
||
<Line label="Email" value={structureInfos.email} />
|
||
<Line label="Email CC" value={structureInfos.email_cc} />
|
||
<Line label="Email signature" value={structureInfos.email_signature} />
|
||
{/* Responsable de traitement (RGPD) */}
|
||
<Line label="Nom responsable de traitement" value={structureInfos.nom_responsable_traitement} />
|
||
<Line label="Qualité responsable de traitement" value={structureInfos.qualite_responsable_traitement} />
|
||
<Line label="Email responsable de traitement" value={structureInfos.email_responsable_traitement} />
|
||
<Line label="Téléphone" value={structureInfos.telephone} />
|
||
<Line label="Signataire des contrats" value={structureInfos.signataire_contrats} />
|
||
<Line label="Qualité signataire" value={clientData.details.qualite_signataire} />
|
||
<Line label="Signataire agissant par délégation ?" value={structureInfos.signataire_delegation ? "Oui" : "Non"} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Caisses & organismes */}
|
||
<section className="rounded-2xl border bg-white">
|
||
<div className="px-4 py-3 border-b">
|
||
<h2 className="font-medium">Caisses & organismes</h2>
|
||
</div>
|
||
<div className="p-4 text-sm">
|
||
{isEditing ? (
|
||
<div className="space-y-2">
|
||
<EditableLine
|
||
label="URSSAF"
|
||
value={editData.urssaf}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, urssaf: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="AUDIENS"
|
||
value={editData.audiens}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, audiens: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Congés Spectacles"
|
||
value={editData.conges_spectacles_id}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, conges_spectacles_id: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Pôle Emploi Spectacle"
|
||
value={editData.pole_emploi_id}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, pole_emploi_id: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="Recouvrement PE Spectacle"
|
||
value={editData.recouvrement_pe_id}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, recouvrement_pe_id: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="AFDAS"
|
||
value={editData.afdas_id}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, afdas_id: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="FNAS"
|
||
value={editData.fnas_id}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, fnas_id: value }))}
|
||
/>
|
||
<EditableLine
|
||
label="FCAP"
|
||
value={editData.fcap_id}
|
||
onChange={(value) => setEditData(prev => ({ ...prev, fcap_id: value }))}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
<Line label="URSSAF" value={structureInfos.urssaf} />
|
||
<Line label="AUDIENS" value={structureInfos.audiens} />
|
||
<Line label="Congés Spectacles" value={structureInfos.conges_spectacles} />
|
||
<Line label="Pôle Emploi Spectacle" value={structureInfos.pole_emploi_spectacle} />
|
||
<Line label="Recouvrement PE Spectacle" value={structureInfos.recouvrement_pe_spectacle} />
|
||
<Line label="AFDAS" value={structureInfos.afdas} />
|
||
<Line label="FNAS" value={structureInfos.fnas} />
|
||
<Line label="FCAP" value={structureInfos.fcap} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Modale de confirmation pour la demande de mandat SEPA */}
|
||
{showSepaMandateModal && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-2xl shadow-2xl max-w-md w-full">
|
||
<div className="px-6 py-4 border-b">
|
||
<h3 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
|
||
<Send className="w-5 h-5 text-blue-600" />
|
||
Demande de mandat SEPA
|
||
</h3>
|
||
</div>
|
||
|
||
<div className="p-6 space-y-4">
|
||
<p className="text-slate-700">
|
||
Êtes-vous sûr de vouloir envoyer une demande de signature de mandat SEPA au client ?
|
||
</p>
|
||
|
||
<div className="bg-slate-50 rounded-lg p-4 space-y-2">
|
||
<div className="font-medium text-slate-900">
|
||
Client : {organization.name}
|
||
</div>
|
||
{structureInfos.email && (
|
||
<div className="text-sm text-slate-600">
|
||
Email : {structureInfos.email}
|
||
</div>
|
||
)}
|
||
{structureInfos.email_cc && (
|
||
<div className="text-sm text-slate-600">
|
||
Email CC : {structureInfos.email_cc}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||
<div className="text-blue-800 text-sm font-medium mb-2">
|
||
Cette action va :
|
||
</div>
|
||
<ul className="text-blue-700 text-sm space-y-1 list-disc list-inside">
|
||
<li>Envoyer un email au client</li>
|
||
<li>Inclure un lien vers GoCardless pour la signature</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="px-6 py-4 border-t flex gap-3 justify-end">
|
||
<button
|
||
onClick={() => setShowSepaMandateModal(false)}
|
||
disabled={sendSepaMandateMutation.isPending}
|
||
className="px-4 py-2 text-sm border rounded-lg hover:bg-slate-50 transition-colors"
|
||
>
|
||
Annuler
|
||
</button>
|
||
<button
|
||
onClick={() => sendSepaMandateMutation.mutate()}
|
||
disabled={sendSepaMandateMutation.isPending}
|
||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2 disabled:opacity-50"
|
||
>
|
||
{sendSepaMandateMutation.isPending ? (
|
||
<>
|
||
<Loader2 className="w-4 h-4 animate-spin" />
|
||
Envoi en cours...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Send className="w-4 h-4" />
|
||
Envoyer la demande
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</main>
|
||
);
|
||
} |