espace-paie-odentas/app/auto-declaration/page.tsx

1025 lines
No EOL
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect } from "react";
import { useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Upload, File, X, Save, Loader2, CheckCircle, AlertCircle, Check, FileText, Eye } from "lucide-react";
import { toast } from "sonner";
import { DocumentPreviewModal } from "@/components/DocumentPreviewModal";
import { SaveConfirmationModal } from "@/components/SaveConfirmationModal";
// Helper components matching the design of salaries/nouveau
function LabelComponent({ children, required = false }: { children: React.ReactNode; required?: boolean }) {
return (
<label className="block text-sm font-medium">
{children} {required && <span className="text-red-500">*</span>}
</label>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="rounded-2xl border bg-white p-5">
<h2 className="text-base font-semibold mb-4">{title}</h2>
{children}
</section>
);
}
function FieldRow({ children }: { children: React.ReactNode }) {
return <div className="grid grid-cols-1 md:grid-cols-2 gap-5">{children}</div>;
}
interface SalarieData {
id?: string;
code_salarie?: string;
nom?: string;
nom_de_naissance?: string;
prenom?: string;
civilite?: string;
pseudonyme?: string;
adresse_mail?: string;
tel?: string;
adresse?: string;
date_naissance?: string;
lieu_de_naissance?: string;
nir?: string;
iban?: string;
bic?: string;
derniere_profession?: string;
employer_id?: string;
conges_spectacles?: string;
organizations?: {
name?: string;
};
}
// Helper functions
function capitalizeFirst(s: string) {
if (!s) return s;
return s.charAt(0).toUpperCase() + s.slice(1);
}
function upper(s: string) {
return s.toUpperCase();
}
function normalizeIban(s: string) {
return s.replace(/\s+/g, "").toUpperCase();
}
function ibanToDigits(s: string) {
return s.replace(/[A-Z]/g, (ch) => String(ch.charCodeAt(0) - 55));
}
function validateIBAN(input: string): boolean {
if (!input) return false;
const iban = normalizeIban(input);
if (iban.length < 15 || iban.length > 34) return false;
if (!/^[A-Z0-9]+$/.test(iban)) return false;
const rearranged = iban.slice(4) + iban.slice(0, 4);
const digits = ibanToDigits(rearranged);
let rem = 0;
for (let i = 0; i < digits.length; i++) {
const code = digits.charCodeAt(i) - 48;
if (code < 0 || code > 9) return false;
rem = (rem * 10 + code) % 97;
}
return rem === 1;
}
interface SalarieData {
id?: string;
code_salarie?: string;
nom?: string;
nom_de_naissance?: string;
prenom?: string;
civilite?: string;
pseudonyme?: string;
adresse_mail?: string;
tel?: string;
adresse?: string;
date_naissance?: string;
lieu_de_naissance?: string;
nir?: string;
iban?: string;
bic?: string;
derniere_profession?: string;
employer_id?: string;
conges_spectacles?: string;
organizations?: {
name?: string;
};
}
interface FormData {
// Identité
civilite: "Monsieur" | "Madame" | "";
nom: string;
prenom: string;
nom_naissance: string;
// Contact
email: string;
telephone: string;
adresse: string;
employeur: string;
// État civil
date_naissance: string;
lieu_naissance: string;
numero_secu: string;
// Bancaire
iban: string;
bic: string;
// Justificatifs
piece_identite: File | null;
attestation_secu: File | null;
rib: File | null;
medecine_travail: File | null;
autre: File | null;
// Divers
notes: string;
protection_donnees: boolean;
}
interface S3Document {
key: string;
name: string;
type: string;
size: number;
lastModified: string;
downloadUrl: string;
}
export default function AutoDeclarationPage() {
const searchParams = useSearchParams();
const token = searchParams.get('token'); // Changé de 'matricule' à 'token'
const [salarieData, setSalarieData] = useState<SalarieData | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [uploading, setUploading] = useState<string | null>(null);
const [existingDocuments, setExistingDocuments] = useState<S3Document[]>([]);
const [loadingDocuments, setLoadingDocuments] = useState(true);
const [previewDocument, setPreviewDocument] = useState<S3Document | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [isConfirmationOpen, setIsConfirmationOpen] = useState(false);
const [missingItems, setMissingItems] = useState<string[]>([]);
const [formData, setFormData] = useState<FormData>({
// Identité
civilite: '',
nom: '',
prenom: '',
nom_naissance: '',
// Contact
email: '',
telephone: '',
adresse: '',
employeur: '',
// État civil
date_naissance: '',
lieu_naissance: '',
numero_secu: '',
// Bancaire
iban: '',
bic: '',
// Justificatifs
piece_identite: null,
attestation_secu: null,
rib: null,
medecine_travail: null,
autre: null,
// Divers
notes: '',
protection_donnees: false
});
// Récupérer les données du salarié
useEffect(() => {
const fetchSalarieData = async () => {
if (!token) {
toast.error("Token d'accès manquant dans l'URL");
setLoading(false);
return;
}
try {
const response = await fetch(`/api/auto-declaration?token=${encodeURIComponent(token)}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Accès non autorisé');
}
const data = await response.json();
setSalarieData(data);
// Pré-remplir le formulaire
setFormData(prev => ({
...prev,
civilite: data.civilite || '',
nom: data.nom || '',
prenom: data.prenom || '',
nom_naissance: data.nom_de_naissance || '',
email: data.adresse_mail || '',
telephone: data.tel || '',
adresse: data.adresse || '',
date_naissance: data.date_naissance || '',
lieu_naissance: data.lieu_de_naissance || '',
numero_secu: data.nir || '',
iban: data.iban || '',
bic: data.bic || '',
employeur: data.organizations?.name || ''
}));
} catch (error) {
console.error('Erreur:', error);
toast.error("Accès non autorisé. Vérifiez votre lien d'accès.");
} finally {
setLoading(false);
}
};
fetchSalarieData();
}, [token]); // Changé de 'matricule' à 'token'
// Récupérer les documents existants depuis S3
useEffect(() => {
const fetchDocuments = async () => {
if (!token) {
setLoadingDocuments(false);
return;
}
try {
const response = await fetch(`/api/auto-declaration/documents?token=${encodeURIComponent(token)}`);
if (response.ok) {
const data = await response.json();
setExistingDocuments(data.documents || []);
}
} catch (error) {
console.error('Erreur lors de la récupération des documents:', error);
} finally {
setLoadingDocuments(false);
}
};
fetchDocuments();
}, [token]);
const handleFileUpload = async (type: keyof FormData, file: File) => {
if (!file || !token) return; // Changé de 'matricule' à 'token'
setUploading(type);
try {
const formData = new FormData();
formData.append('file', file);
formData.append('token', token); // Changé de 'matricule' à 'token'
formData.append('type', type);
const response = await fetch('/api/auto-declaration/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Erreur lors du téléchargement');
}
const result = await response.json();
setFormData(prev => ({
...prev,
[type]: file
}));
toast.success(`${getFileTypeLabel(type)} téléchargé avec succès`);
// Rafraîchir la liste des documents
const documentsResponse = await fetch(`/api/auto-declaration/documents?token=${encodeURIComponent(token)}`);
if (documentsResponse.ok) {
const data = await documentsResponse.json();
setExistingDocuments(data.documents || []);
}
} catch (error) {
console.error('Erreur upload:', error);
toast.error(`Erreur lors du téléchargement : ${error instanceof Error ? error.message : 'erreur inconnue'}`);
} finally {
setUploading(null);
}
};
const handleRemoveFile = (type: keyof FormData) => {
setFormData(prev => ({
...prev,
[type]: null
}));
};
const handleDeleteDocument = async (fileKey: string) => {
if (!token) return;
try {
const response = await fetch('/api/auto-declaration/documents/delete', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token,
fileKey
})
});
if (!response.ok) {
throw new Error('Erreur lors de la suppression');
}
// Rafraîchir la liste des documents
const documentsResponse = await fetch(`/api/auto-declaration/documents?token=${encodeURIComponent(token)}`);
if (documentsResponse.ok) {
const data = await documentsResponse.json();
setExistingDocuments(data.documents || []);
}
} catch (error) {
console.error('Erreur suppression:', error);
throw error;
}
};
const openPreview = (doc: S3Document) => {
setPreviewDocument(doc);
setIsPreviewOpen(true);
};
const closePreview = () => {
setIsPreviewOpen(false);
setPreviewDocument(null);
};
const checkMissingItems = () => {
const missing: string[] = [];
// Vérifier les champs obligatoires
if (!formData.civilite) missing.push("Civilité");
if (!formData.nom) missing.push("Nom de famille");
if (!formData.prenom) missing.push("Prénom");
if (!formData.email) missing.push("Adresse e-mail");
// Vérifier les documents (nouveaux + existants)
const hasPieceIdentite = formData.piece_identite || existingDocuments.some(doc => doc.name.toLowerCase().includes('piece-identite'));
const hasAttestationSecu = formData.attestation_secu || existingDocuments.some(doc => doc.name.toLowerCase().includes('attestation-secu'));
const hasRib = formData.rib || existingDocuments.some(doc => doc.name.toLowerCase().includes('rib'));
if (!hasPieceIdentite) missing.push("Pièce d'identité");
if (!hasAttestationSecu) missing.push("Attestation de Sécurité Sociale");
if (!hasRib) missing.push("RIB");
// Informations complémentaires
if (!formData.telephone) missing.push("Numéro de téléphone");
if (!formData.adresse) missing.push("Adresse postale");
if (!formData.date_naissance) missing.push("Date de naissance");
if (!formData.lieu_naissance) missing.push("Lieu de naissance");
if (!formData.numero_secu) missing.push("Numéro de Sécurité Sociale");
if (!formData.iban) missing.push("IBAN");
if (!formData.bic) missing.push("BIC/SWIFT");
return missing;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.protection_donnees) {
toast.error("Vous devez accepter les conditions de protection des données personnelles");
return;
}
setSaving(true);
try {
const updateData = {
civilite: formData.civilite,
nom: formData.nom,
prenom: formData.prenom,
nom_de_naissance: formData.nom_naissance || null,
adresse_mail: formData.email,
tel: formData.telephone || null,
adresse: formData.adresse || null,
date_naissance: formData.date_naissance || null,
lieu_de_naissance: formData.lieu_naissance || null,
nir: formData.numero_secu || null,
iban: formData.iban || null,
bic: formData.bic || null,
notes: formData.notes
};
const response = await fetch('/api/auto-declaration', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token, // Changé de 'matricule' à 'token'
...updateData
})
});
if (!response.ok) {
throw new Error('Erreur lors de la sauvegarde');
}
// Vérifier les éléments manquants
const missing = checkMissingItems();
setMissingItems(missing);
// Ouvrir le modal de confirmation
setIsConfirmationOpen(true);
} catch (error) {
console.error('Erreur sauvegarde:', error);
toast.error("Erreur lors de la sauvegarde de vos informations");
} finally {
setSaving(false);
}
};
const getFileTypeLabel = (type: string) => {
const labels = {
piece_identite: "Pièce d'identité",
attestation_secu: "Attestation Sécurité Sociale",
rib: "RIB",
medecine_travail: "Attestation médecine du travail",
autre: "Autre document"
};
return labels[type as keyof typeof labels] || type;
};
const FileUploadField = ({
type,
label,
required = false
}: {
type: keyof FormData;
label: string;
required?: boolean
}) => {
const file = formData[type] as File | null;
const isUploading = uploading === type;
const [isDragging, setIsDragging] = useState(false);
// Trouver les documents existants de ce type
const typeKeywords: Record<string, string> = {
piece_identite: "piece-identite",
attestation_secu: "attestation-secu",
rib: "rib",
medecine_travail: "medecine-travail",
autre: "autre"
};
const existingDocs = existingDocuments.filter(doc =>
doc.name.toLowerCase().includes(typeKeywords[type as keyof typeof typeKeywords] || '')
);
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
};
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isUploading) {
setIsDragging(true);
}
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
if (isUploading) return;
const files = e.dataTransfer.files;
if (files && files.length > 0) {
const droppedFile = files[0];
// Vérifier le type de fichier
const validTypes = ['.pdf', '.jpg', '.jpeg', '.png'];
const fileExtension = '.' + droppedFile.name.split('.').pop()?.toLowerCase();
if (validTypes.includes(fileExtension)) {
handleFileUpload(type, droppedFile);
} else {
toast.error('Type de fichier non autorisé. Utilisez PDF, JPG ou PNG.');
}
}
};
return (
<div className="space-y-2">
<Label className="text-sm font-medium">
{label} {required && <span className="text-red-500">*</span>}
</Label>
{/* Documents existants */}
{existingDocs.length > 0 && (
<div className="space-y-2 mb-3">
{existingDocs.map((doc) => (
<button
key={doc.key}
type="button"
onClick={() => openPreview(doc)}
className="w-full flex items-center justify-between p-3 bg-blue-50 border border-blue-200 rounded-md hover:bg-blue-100 transition-colors group"
>
<div className="flex items-center gap-2 min-w-0">
<FileText className="h-4 w-4 text-blue-600 flex-shrink-0" />
<div className="text-left min-w-0">
<div className="text-sm text-blue-700 truncate">{doc.name}</div>
<div className="text-xs text-blue-600">{formatFileSize(doc.size)}</div>
</div>
</div>
<Eye className="h-4 w-4 text-blue-600 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" />
</button>
))}
</div>
)}
{file ? (
<div className="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-md">
<div className="flex items-center gap-2">
<CheckCircle className="h-4 w-4 text-green-600" />
<span className="text-sm text-green-700">{file.name}</span>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleRemoveFile(type)}
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<div
className={`border-2 border-dashed rounded-md p-6 text-center transition-colors ${
isDragging
? 'border-blue-400 bg-blue-50'
: 'border-gray-300 bg-white'
}`}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<input
type="file"
id={`file-${type}`}
className="hidden"
accept=".pdf,.jpg,.jpeg,.png"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
handleFileUpload(type, file);
}
}}
disabled={isUploading}
/>
<label htmlFor={`file-${type}`} className="cursor-pointer">
<div className="flex flex-col items-center">
{isUploading ? (
<Loader2 className="h-8 w-8 text-gray-400 animate-spin mb-2" />
) : (
<Upload className="h-8 w-8 text-gray-400 mb-2" />
)}
<span className="text-sm text-gray-600">
{isUploading ? 'Téléchargement...' : isDragging ? 'Déposez le fichier ici' : 'Glissez un fichier ou cliquez pour choisir'}
</span>
<span className="text-xs text-gray-400 mt-1">
PDF, JPG, PNG (max 10MB)
</span>
</div>
</label>
</div>
)}
</div>
);
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
<p>Chargement de vos informations...</p>
</div>
</div>
);
}
if (!token) {
return (
<div className="min-h-screen flex items-center justify-center">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-red-500" />
Accès non autorisé
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600">
Cette page nécessite un lien d'accès sécurisé. Veuillez utiliser le lien reçu par email.
</p>
</CardContent>
</Card>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8 px-4">
<div className="max-w-2xl mx-auto space-y-5">
<h1 className="text-xl font-semibold">Vos justificatifs pour votre embauche</h1>
<Section title="Votre projet d'embauche">
<p className="text-sm text-slate-600"></p>
<p>
Bonjour,
</p>
<p>
Dans le cadre de votre projet d'embauche géré par les services d'Odentas pour le
compte de votre futur employeur, nous vous invitons à nous transmettre les informations
et justificatifs demandés ci-dessous.
</p>
<p>
Ce formulaire est sécurisé et les fichiers sont chiffrés pendant l'envoi, pour garantir la
sécurité de vos données.
</p>
<p>
N'hésitez pas à nous contacter à{" "}
<a href="mailto:paie@odentas.fr" className="text-blue-600 underline">
paie@odentas.fr
</a>{" "}
pour toute question.
</p>
<p>
Merci par avance,<br />
L'équipe Odentas.
</p>
</Section>
<form onSubmit={handleSubmit} className="space-y-5">
{/* Section Identité */}
<Section title="Identité">
<div className="space-y-3">
<LabelComponent required>Votre civilité</LabelComponent>
<div className="flex items-center gap-6">
<label className="inline-flex items-center gap-2 text-sm">
<input
type="radio"
name="civilite"
checked={formData.civilite === "Monsieur"}
onChange={() => setFormData(prev => ({ ...prev, civilite: "Monsieur" }))}
/>
Monsieur
</label>
<label className="inline-flex items-center gap-2 text-sm">
<input
type="radio"
name="civilite"
checked={formData.civilite === "Madame"}
onChange={() => setFormData(prev => ({ ...prev, civilite: "Madame" }))}
/>
Madame
</label>
</div>
<FieldRow>
<div>
<LabelComponent required>Votre nom de famille (ou nom d'usage)</LabelComponent>
<input
value={formData.nom}
onChange={(e) => setFormData(prev => ({ ...prev, nom: upper(e.target.value) }))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
required
/>
<p className="text-[11px] text-slate-500 mt-1">La saisie se fait automatiquement en majuscules.</p>
</div>
<div>
<LabelComponent required>Votre prénom</LabelComponent>
<input
value={formData.prenom}
onChange={(e) => setFormData(prev => ({ ...prev, prenom: capitalizeFirst(e.target.value) }))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
required
/>
<p className="text-[11px] text-slate-500 mt-1">Une majuscule est automatiquement ajoutée au début du prénom.</p>
</div>
</FieldRow>
<FieldRow>
<div>
<LabelComponent>Votre nom de naissance</LabelComponent>
<input
value={formData.nom_naissance}
onChange={(e) => setFormData(prev => ({ ...prev, nom_naissance: upper(e.target.value) }))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
<p className="text-[11px] text-slate-500 mt-1">Remplir uniquement s'il diffère du nom d'usage (automatiquement en majuscules).</p>
</div>
<div>
<LabelComponent required>Votre employeur</LabelComponent>
<input
value={formData.employeur}
onChange={(e) => setFormData(prev => ({ ...prev, employeur: e.target.value }))}
className="w-full px-3 py-2 rounded-lg border bg-blue-50 text-sm"
required
readOnly
/>
</div>
</FieldRow>
</div>
</Section>
{/* Section Coordonnées */}
<Section title="Coordonnées">
<FieldRow>
<div>
<LabelComponent required>Votre adresse email</LabelComponent>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
required
/>
</div>
<div>
<LabelComponent>Votre numéro de téléphone</LabelComponent>
<input
value={formData.telephone}
onChange={(e) => setFormData(prev => ({ ...prev, telephone: e.target.value }))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
placeholder="06 12 34 56 78"
/>
</div>
</FieldRow>
<div>
<LabelComponent>Votre adresse postale complète</LabelComponent>
<input
value={formData.adresse}
onChange={(e) => setFormData(prev => ({ ...prev, adresse: e.target.value }))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
placeholder="Numéro, rue, code postal, ville"
/>
</div>
</Section>
{/* Section État civil */}
<Section title="État civil">
<FieldRow>
<div>
<LabelComponent>Votre date de naissance</LabelComponent>
<input
type="date"
value={formData.date_naissance}
onChange={(e) => setFormData(prev => ({ ...prev, date_naissance: e.target.value }))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
<div>
<LabelComponent>Votre lieu de naissance</LabelComponent>
<input
value={formData.lieu_naissance}
onChange={(e) => setFormData(prev => ({ ...prev, lieu_naissance: e.target.value }))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
placeholder="Ville, département, pays le cas échéant"
/>
</div>
</FieldRow>
<div>
<LabelComponent>Votre numéro de Sécurité Sociale</LabelComponent>
<input
value={formData.numero_secu}
onChange={(e) => setFormData(prev => ({ ...prev, numero_secu: e.target.value }))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
placeholder="15 chiffres ou numéro provisoire"
/>
<p className="text-[11px] text-slate-500 mt-1">Indiquez le NIR complet ou provisoire si pas encore définitif.</p>
</div>
</Section>
{/* Section Coordonnées bancaires */}
<Section title="Coordonnées bancaires">
<FieldRow>
<div>
<LabelComponent>Votre IBAN</LabelComponent>
<input
value={formData.iban}
onChange={(e) => setFormData(prev => ({ ...prev, iban: e.target.value }))}
className={`w-full px-3 py-2 rounded-lg border bg-white text-sm ${
formData.iban && !validateIBAN(formData.iban) ? 'border-red-400' : 'border-slate-300'
}`}
placeholder="FR.. .. .. .. .. .. .. .. .. .."
/>
{formData.iban && !validateIBAN(formData.iban) ? (
<p className="text-[11px] text-red-600 mt-1">IBAN invalide (vérifiez la clé et le format).</p>
) : formData.iban && validateIBAN(formData.iban) ? (
<p className="text-[11px] text-emerald-600 mt-1 inline-flex items-center gap-1">
<Check className="w-3.5 h-3.5" /> IBAN valide
</p>
) : (
<p className="text-[11px] text-slate-500 mt-1">Doit provenir d'une banque de la zone SEPA.</p>
)}
</div>
<div>
<LabelComponent>Votre BIC/SWIFT</LabelComponent>
<input
value={formData.bic}
onChange={(e) => setFormData(prev => ({ ...prev, bic: e.target.value.toUpperCase() }))}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
placeholder="BNPAFRPP"
/>
</div>
</FieldRow>
</Section>
{/* Section Justificatifs */}
<Section title="Justificatifs">
<div className="space-y-4">
<FileUploadField
type="piece_identite"
label="Pièce d'identité en cours de validité"
/>
<p className="text-[11px] text-slate-500">
Pièces acceptées : Carte Nationale d'Identité recto-verso, Passeport, Permis nouveau format recto-verso,
Carte de séjour recto-verso, Carte vitale, Carte étudiant.
</p>
<FileUploadField
type="attestation_secu"
label="Attestation de Sécurité Sociale"
/>
<p className="text-[11px] text-slate-500">
Ce document est téléchargeable sur votre espace Ameli.
</p>
<FileUploadField
type="rib"
label="Votre RIB"
/>
<p className="text-[11px] text-slate-500">
Pour le versement de vos salaires.
</p>
<FileUploadField
type="medecine_travail"
label="Attestation médecine du travail"
/>
<p className="text-[11px] text-slate-500">
Facultatif. Pour les intermittents du spectacle.
</p>
<FileUploadField
type="autre"
label="Tout autre document que vous jugez utile de nous transmettre"
/>
<p className="text-[11px] text-slate-500">
Facultatif.
</p>
</div>
</Section>
{/* Section Notes */}
<Section title="Notes">
<div>
<LabelComponent>Informations complémentaires</LabelComponent>
<textarea
value={formData.notes}
onChange={(e) => setFormData(prev => ({ ...prev, notes: e.target.value }))}
placeholder="Utilisez ce champ libre si vous devez mentionner toutes les informations complémentaires."
rows={4}
className="w-full px-3 py-2 rounded-lg border bg-white text-sm"
/>
</div>
</Section>
{/* Protection des données */}
<Section title="Protection des données personnelles">
<div className="space-y-3">
<div className="p-4 bg-gray-50 border rounded-lg text-xs text-gray-700 space-y-2">
<p>
Aux fins de gestion du personnel et de traitement des rémunérations, nous sommes amenés à solliciter des données personnelles vous concernant à l'occasion de la conclusion, l'exécution et le cas échéant, la rupture de votre contrat de travail.
</p>
<p>
La transmission de vos documents et l'acceptation des présentes valent autorisation pour votre employeur et nous-même (Odentas Media SAS) de collecter, d'enregistrer et de stocker les données nécessaires. Outre les services internes de votre employeur, les destinataires de ces données sont, à ce jour, les organismes de sécurité sociale, les caisses de retraite et de prévoyance, la mutuelle, France Travail, les services des impôts, le service de médecine du travail et nous-même (Odentas Media SAS).
</p>
<p>
Ces informations sont réservées à l'usage des services concernés et ne peuvent être communiquées qu'à ces destinataires.
</p>
<p>
Vous bénéficiez notamment d'un droit d'accès, de rectification et d'effacement des informations vous concernant, que vous pouvez exercer en adressant directement une demande au responsable de ces traitements : Nicolas ROL, paie@odentas.fr.
</p>
<p>
Si votre embauche ne se réalise pas, vos justificatifs seront immédiatement supprimés de nos serveurs.
</p>
</div>
<label htmlFor="protection_donnees" className="flex items-start gap-3 cursor-pointer group">
<input
type="checkbox"
id="protection_donnees"
checked={formData.protection_donnees}
onChange={(e) => setFormData(prev => ({ ...prev, protection_donnees: e.target.checked }))}
className="mt-1 cursor-pointer"
required
/>
<span className="text-sm font-medium group-hover:text-gray-700">
J'ai lu et compris les informations concernant la protection des données personnelles. <span className="text-red-500">*</span>
</span>
</label>
</div>
</Section>
{/* Section Enregistrement */}
<Section title="Enregistrer vos modifications">
<div className="space-y-4">
<p className="text-sm text-gray-600">
Vous pouvez enregistrer vos modifications et revenir plus tard pour compléter les informations manquantes.
Vos données sont sauvegardées de manière sécurisée.
</p>
<div className="flex justify-center">
<button
type="submit"
disabled={saving || !formData.protection_donnees}
className="inline-flex items-center gap-2 px-6 py-3 bg-teal-600 hover:bg-teal-700 disabled:bg-gray-400 text-white rounded-lg text-sm font-medium transition-colors"
>
{saving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Enregistrement...
</>
) : (
<>
<Save className="h-4 w-4" />
Enregistrer les modifications
</>
)}
</button>
</div>
</div>
</Section>
</form>
{/* Modal de prévisualisation */}
<DocumentPreviewModal
isOpen={isPreviewOpen}
onClose={closePreview}
document={previewDocument}
onDelete={handleDeleteDocument}
/>
{/* Modal de confirmation */}
<SaveConfirmationModal
isOpen={isConfirmationOpen}
onClose={() => setIsConfirmationOpen(false)}
missingItems={missingItems}
/>
</div>
</div>
);
}