1025 lines
No EOL
36 KiB
TypeScript
1025 lines
No EOL
36 KiB
TypeScript
"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 e‑mail</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>
|
||
);
|
||
} |