import React, { useEffect, useMemo, useState, useCallback, useRef } from 'react'; import { Helmet } from 'react-helmet'; import { useForm, Controller, type SubmitHandler } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; import { ArrowLeft, Save, Building2, User, Phone, Mail, Globe, CreditCard, Loader2, Search, X, CheckCircle, AlertCircle } from 'lucide-react'; import { useNavigate, useParams } from 'react-router-dom'; import { toast } from '@/components/ui/use-toast'; import { cn } from '@/lib/utils'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { Client, ClientCreateResponse, ClientRequest } from '@/types/clientType'; import { addClient, getClient, updateClient } from '@/store/features/client/thunk'; import { clientStatus, getAllClients, getClientSelected } from '@/store/features/client/selectors'; import { selectClient } from '@/store/features/client/slice'; import { Progress } from "@/components/ui/Progress"; import { CompanyInfo } from '@/data/mockData'; import { Alert } from '@mui/material'; import { InputField } from '@/components/ui/InputValidator'; import { searchEntreprise } from '@/store/features/entreprise/thunk'; import { Entreprise, EntrepriseResponse } from '@/types/entreprise'; import { selectentreprise } from '@/store/features/entreprise/slice'; import { getentrepriseSelected } from '@/store/features/entreprise/selectors'; import { motion, AnimatePresence } from 'framer-motion'; import { InternationalPhoneInput } from '@/components/ui/InternationalPhoneInput'; // ============================================ // SCHÉMA DE VALIDATION ZOD // ============================================ const clientSchema = z.object({ intitule: z.string() .min(1, "L'intitulé est requis") .max(69, "L'intitulé ne peut pas dépasser 69 caractères"), numero: z.string() .min(1, "Le code client est requis") .max(17, "Le code client ne peut pas dépasser 17 caractères") .regex(/^[A-Za-z0-9]+$/, "Le code client ne peut contenir que des lettres et des chiffres"), type_tiers: z.number().min(0).max(3).optional().nullable(), qualite: z.string().optional().or(z.literal("")), classement: z.string().optional().or(z.literal("")), raccourci: z.string().optional().or(z.literal("")), siret: z.string() .regex(/^\d*$/, "Le SIRET doit contenir uniquement des chiffres") .optional() .or(z.literal("")), tva_intra: z.string().optional().or(z.literal("")), code_naf: z.string().optional().or(z.literal("")), contact: z.string().optional().or(z.literal("")), adresse: z.string() .max(35, "L'adresse ne peut pas dépasser 35 caractères") .optional() .or(z.literal("")), complement: z.string().optional().or(z.literal("")), code_postal: z.string().optional().or(z.literal("")), ville: z.string().optional().or(z.literal("")), region: z.string().optional().or(z.literal("")), pays: z.string().default("France"), telephone: z.string().optional().or(z.literal("")), telecopie: z.string().optional().or(z.literal("")), email: z.string() .refine( (val) => !val || val.trim() === "" || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), { message: "Format email invalide" } ) .optional() .or(z.literal("")), site_web: z.string().optional().or(z.literal("")), facebook: z.string().optional().or(z.literal("")), linkedin: z.string().optional().or(z.literal("")), compte_general: z.string().optional().or(z.literal("")), categorie_tarifaire: z.string().optional().or(z.literal("")), categorie_comptable: z.string().optional().or(z.literal("")), taux01: z.number().optional(), taux02: z.number().optional(), taux03: z.number().optional(), taux04: z.number().optional(), encours_autorise: z.number().optional(), assurance_credit: z.number().optional(), langue: z.number().optional(), commercial_code: z.number().optional(), lettrage_auto: z.boolean().optional(), est_actif: z.boolean().default(true), type_facture: z.number().optional(), est_prospect: z.boolean().default(false), forme_juridique: z.string().optional().or(z.literal("")), effectif: z.number().optional(), ca_annuel: z.number().optional(), commentaire: z.string().optional().or(z.literal("")), }); type ClientFormData = z.infer; // ============================================ // COMPOSANT RECHERCHE ENTREPRISE // ============================================ interface EntrepriseSearchProps { onSelect: (entreprise: Entreprise) => void; } const EntrepriseSearch: React.FC = ({ onSelect }) => { const dispatch = useAppDispatch(); const [query, setQuery] = useState(''); const [isSearching, setIsSearching] = useState(false); const [results, setResults] = useState([]); const [showResults, setShowResults] = useState(false); const [error, setError] = useState(null); const [selectedEntreprise, setSelectedEntreprise] = useState(null); const searchRef = useRef(null); const debounceRef = useRef(null); // Fermer les résultats au clic extérieur useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (searchRef.current && !searchRef.current.contains(event.target as Node)) { setShowResults(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // Recherche avec debounce const handleSearch = useCallback(async (searchQuery: string) => { if (!searchQuery || searchQuery.length < 2) { setResults([]); setShowResults(false); return; } setIsSearching(true); setError(null); try { const response = await dispatch(searchEntreprise(searchQuery)).unwrap() as EntrepriseResponse; setResults(response.results || []); setShowResults(true); } catch (e: any) { setError("Erreur lors de la recherche"); setResults([]); } finally { setIsSearching(false); } }, [dispatch]); // Debounce de la recherche const handleInputChange = (value: string) => { setQuery(value); setSelectedEntreprise(null); if (debounceRef.current) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(() => { handleSearch(value); }, 400); }; // Sélectionner une entreprise const handleSelectEntreprise = (entreprise: Entreprise) => { setSelectedEntreprise(entreprise); setQuery(entreprise.company_name); setShowResults(false); dispatch(selectentreprise(entreprise)); onSelect(entreprise); toast({ title: "Entreprise sélectionnée", description: `Les informations de "${entreprise.company_name}" ont été pré-remplies.`, className: "bg-green-500 text-white border-green-600" }); }; // Réinitialiser la recherche const handleClear = () => { setQuery(''); setResults([]); setShowResults(false); setSelectedEntreprise(null); setError(null); }; return (
Recherchez une entreprise par son nom, SIREN ou SIRET pour pré-remplir automatiquement les informations
{isSearching ? ( ) : ( )}
handleInputChange(e.target.value)} onFocus={() => results.length > 0 && setShowResults(true)} placeholder="Nom de l'entreprise, n° SIREN ou n° SIRET..." className={cn( "w-full pl-10 pr-10 py-3 bg-white dark:bg-gray-900 border rounded-xl text-sm transition-all", "focus:outline-none focus:ring-2 focus:ring-[#007E45]/20 focus:border-[#007E45]", selectedEntreprise ? "border-green-500 bg-green-50 dark:bg-green-900/20" : "border-gray-200 dark:border-gray-700" )} /> {/* Indicateur de sélection ou bouton clear */}
{selectedEntreprise ? ( ) : query && ( )}
{/* Message entreprise sélectionnée */} {selectedEntreprise && (

{selectedEntreprise.company_name}

SIRET: {selectedEntreprise.siret_siege} • {selectedEntreprise.ville}

)}
{/* Liste des résultats */} {showResults && results.length > 0 && (

{results.length} résultat{results.length > 1 ? 's' : ''} trouvé{results.length > 1 ? 's' : ''}

{results.map((entreprise, index) => ( ))}
)} {/* Pas de résultats */} {showResults && query.length >= 2 && results.length === 0 && !isSearching && (

Aucune entreprise trouvée

Essayez avec un autre nom ou numéro

)} {/* Erreur */} {error && (

{error}

)}
); }; // ============================================ // COMPOSANTS UI // ============================================ const SectionHeader = ({ title, icon: Icon }: { title: string; icon: React.ElementType }) => (

{title}

); // ============================================ // COMPOSANT PRINCIPAL // ============================================ const CreateClientPage = () => { const navigate = useNavigate(); const dispatch = useAppDispatch(); const { id } = useParams<{ id?: string }>(); const status = useAppSelector(clientStatus); const clients = useAppSelector(getAllClients) as Client[]; const clientSelected = useAppSelector(getClientSelected) as Client | null; const entrepriseSelected = useAppSelector(getentrepriseSelected) as Entreprise | null; const [completion, setCompletion] = useState(0); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const isEditing = !!id; const editing = isEditing ? clientSelected : null; const { control, handleSubmit, watch, reset, setValue, formState: { errors } } = useForm({ resolver: zodResolver(clientSchema), mode: "onChange", defaultValues: { intitule: "", numero: "", type_tiers: undefined, qualite: "", classement: "", raccourci: "", siret: "", tva_intra: "", code_naf: "", contact: "", adresse: "", complement: "", code_postal: "", ville: "", region: "", pays: "France", telephone: "", telecopie: "", email: "", site_web: "", facebook: "", linkedin: "", compte_general: "", categorie_tarifaire: "", categorie_comptable: "", est_actif: true, est_prospect: false, } }); const formValues = watch(); const intitule = watch("intitule"); const numero = watch("numero"); const adresse = watch("adresse"); // ✅ Pré-remplir les champs quand une entreprise est sélectionnée const handleEntrepriseSelect = useCallback((entreprise: Entreprise) => { let cleanedAddress = entreprise.address || ''; if (cleanedAddress && entreprise.code_postal) { const pattern = new RegExp(`\\s*${entreprise.code_postal}.*$`, 'i'); cleanedAddress = cleanedAddress.replace(pattern, '').trim(); } if (cleanedAddress && entreprise.ville) { const villePattern = new RegExp(`\\s*${entreprise.ville}.*$`, 'i'); cleanedAddress = cleanedAddress.replace(villePattern, '').trim(); } if (cleanedAddress.length > 35) { cleanedAddress = cleanedAddress.substring(0, 35).trim(); } setValue('intitule', entreprise.company_name || '', { shouldValidate: true }); setValue('siret', entreprise.siret_siege || '', { shouldValidate: true }); setValue('tva_intra', entreprise.vat_number || '', { shouldValidate: true }); setValue('code_naf', entreprise.naf_code || '', { shouldValidate: true }); setValue('adresse', cleanedAddress, { shouldValidate: true }); setValue('code_postal', entreprise.code_postal || '', { shouldValidate: true }); setValue('ville', entreprise.ville || '', { shouldValidate: true }); setValue('est_actif', entreprise.is_active ?? true, { shouldValidate: true }); }, [setValue]); useEffect(() => { if (isEditing && editing) { reset({ intitule: editing.intitule || "", numero: editing.numero || "", type_tiers: editing.type_tiers ?? undefined, qualite: editing.qualite || "", classement: editing.classement || "", raccourci: editing.raccourci || "", siret: editing.siret || "", tva_intra: editing.tva_intra || "", code_naf: editing.code_naf || "", contact: editing.contact || "", adresse: editing.adresse || "", complement: editing.complement || "", code_postal: editing.code_postal || "", ville: editing.ville || "", region: editing.region || "", pays: editing.pays || "France", telephone: editing.telephone || "", telecopie: editing.telecopie || "", email: editing.email || "", site_web: editing.site_web || "", facebook: editing.facebook || "", linkedin: editing.linkedin || "", compte_general: editing.compte_general || "", categorie_tarifaire: editing.categorie_tarif?.toString() || "", categorie_comptable: editing.categorie_compta?.toString() || "", taux01: editing.taux01 || undefined, taux02: editing.taux02 || undefined, taux03: editing.taux03 || undefined, taux04: editing.taux04 || undefined, encours_autorise: editing.encours_autorise || undefined, assurance_credit: editing.assurance_credit || undefined, langue: editing.langue || undefined, commercial_code: editing.commercial_code || undefined, lettrage_auto: editing.lettrage_auto || undefined, est_actif: editing.est_actif ?? true, type_facture: editing.type_facture || undefined, est_prospect: editing.est_prospect ?? false, forme_juridique: editing.forme_juridique || "", effectif: editing.effectif || undefined, ca_annuel: undefined, commentaire: editing.commentaire || "", }); } }, [editing, isEditing, reset]); // Calcul completion useEffect(() => { const requiredFilled = (intitule ? 1 : 0) + (numero ? 1 : 0); const optionalFields = ['email', 'telephone', 'adresse', 'ville', 'siret']; const optionalFilled = optionalFields.filter(f => !!formValues[f as keyof ClientFormData]).length; const requiredPercent = (requiredFilled / 2) * 40; const optionalPercent = (optionalFilled / optionalFields.length) * 60; setCompletion(Math.round(requiredPercent + optionalPercent)); }, [formValues, intitule, numero]); const canSave = useMemo(() => { const hasIntitule = !!intitule && intitule.length <= 69; const hasNumero = !!numero && numero.length > 0 && numero.length <= 17; const validAdresse = !adresse || adresse.length <= 35; const notLoading = !isLoading && status !== "loading"; const noErrors = Object.keys(errors).length === 0; return hasIntitule && hasNumero && validAdresse && notLoading && noErrors; }, [intitule, numero, adresse, isLoading, status, errors]); // Submit handler const onSave: SubmitHandler = async (data) => { setIsLoading(true); setError(null); try { if (!isEditing && data.numero) { const existingClient = clients.find(client => client.numero === data.numero); if (existingClient) { setError(`Le code client "${data.numero}" est déjà utilisé.`); setIsLoading(false); return; } } const payload: ClientRequest = { intitule: data.intitule, numero: data.numero || undefined, type_tiers: data.type_tiers ?? undefined, qualite: data.qualite || null, classement: data.classement || null, raccourci: data.raccourci || null, siret: data.siret?.replace(/\D/g, "") || null, tva_intra: data.tva_intra || null, code_naf: data.code_naf || null, contact: data.contact || null, adresse: data.adresse || null, complement: data.complement || null, code_postal: data.code_postal || null, ville: data.ville || null, region: data.region || null, pays: data.pays || "France", telephone: data.telephone || null, telecopie: data.telecopie || null, email: data.email || null, site_web: data.site_web || null, facebook: data.facebook || null, linkedin: data.linkedin || null, compte_general: data.compte_general || null, categorie_tarifaire: data.categorie_tarifaire || null, categorie_comptable: data.categorie_comptable || null, taux01: data.taux01 || null, taux02: data.taux02 || null, taux03: data.taux03 || null, taux04: data.taux04 || null, encours_autorise: data.encours_autorise || null, assurance_credit: data.assurance_credit || null, langue: data.langue || null, commercial_code: data.commercial_code || null, lettrage_auto: data.lettrage_auto || null, est_actif: data.est_actif, type_facture: data.type_facture || null, est_prospect: data.est_prospect, forme_juridique: data.forme_juridique || null, effectif: data.effectif || null, ca_annuel: data.ca_annuel || null, commentaire: data.commentaire || null, }; if (isEditing && editing) { const rep = await dispatch(updateClient({ numero: editing.numero, data: payload })).unwrap() as any; const dataResponse = rep.client as Client; toast({ title: "Client mis à jour !", description: `Le client ${dataResponse.numero} a été mis à jour avec succès.`, className: "bg-green-500 text-white border-green-600" }); const clientCreated = await dispatch(getClient(dataResponse.numero)).unwrap() as Client; dispatch(selectClient(clientCreated)); navigate(`/home/clients/${dataResponse.numero}`); } else { const rep = await dispatch(addClient(payload)).unwrap() as ClientCreateResponse; if (!rep.success) { setError("La création du client a échoué."); } else { toast({ title: "Client créé !", description: `Le client ${rep.data.numero} a été créé avec succès.`, className: "bg-green-500 text-white border-green-600" }); const clientCreated = await dispatch(getClient(rep.data.numero)).unwrap() as Client; dispatch(selectClient(clientCreated)); navigate(`/home/clients/${clientCreated.numero}`); } } } catch (e: any) { setError("Une erreur est survenue lors de la sauvegarde du client."); console.error("Erreur:", e); toast({ title: "Erreur", description: e.message || "Une erreur est survenue.", variant: "destructive" }); } finally { setIsLoading(false); } }; const inputClass = (error?: any) => cn( "w-full px-3 py-2.5 bg-white dark:bg-gray-900 border rounded-xl text-xs transition-all focus:outline-none focus:ring-2 focus:ring-[#007E45]/20 focus:border-[#007E45]", error ? "border-red-300 focus:border-red-500 focus:ring-red-200" : "border-gray-200 dark:border-gray-700" ); return ( <> {isEditing ? `Modifier ${editing?.intitule || 'Client'}` : 'Nouveau Client'} - {CompanyInfo.name}
{/* Header */}

{isEditing ? `Modifier: ${editing?.intitule}` : 'Nouveau Client'}

{isEditing ? 'Modifiez les informations du client' : 'Créez une nouvelle fiche client'}

{completion}%

complété

{/* Section Recherche Entreprise */}
{/* Formulaire principal */}
(
{errors.intitule &&

{errors.intitule.message}

}

{field.value?.length || 0}/69

)} /> (
{errors.numero &&

{errors.numero.message}

}

{field.value?.length || 0}/17

)} /> (
)} />
( )} /> ( )} />
( } className='text-xs' /> )} /> ( )} /> (
{errors.adresse &&

{errors.adresse.message}

}

{field.value?.length || 0}/35

)} /> ( )} /> ( )} /> ( )} />
( )} /> ( )} /> ( )} />
(
field.onChange(parseFloat(e.target.value) || undefined)} placeholder="0.00" className={inputClass()} />
)} /> (
field.onChange(parseFloat(e.target.value) || undefined)} placeholder="0.00" className={inputClass()} />
)} />
{error && ( setError(null)}> {error} )}
); }; export default CreateClientPage;