652 lines
30 KiB
JavaScript
652 lines
30 KiB
JavaScript
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Helmet } from 'react-helmet';
|
|
import { useForm, Controller } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import * as z from 'zod';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import {
|
|
ChevronDown, ChevronUp, Info, CheckCircle2, AlertCircle,
|
|
Save, X, Building2, User, MapPin, Wallet, Tag,
|
|
FileText, Zap, Shield, ArrowLeft, UploadCloud
|
|
} from 'lucide-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { toast } from '@/components/ui/use-toast';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Progress } from '@/components/ui/progress';
|
|
import { cn } from '@/lib/utils';
|
|
import { mockUsers } from '@/data/mockData';
|
|
|
|
// --- Schema Validation ---
|
|
const prospectSchema = z.object({
|
|
// 1. General
|
|
type: z.enum(['entreprise', 'particulier']),
|
|
name: z.string().min(2, "Le nom est requis"),
|
|
companyName: z.string().optional(),
|
|
siret: z.string().optional(),
|
|
category: z.string().optional(),
|
|
origin: z.string().optional(),
|
|
status: z.string().optional(),
|
|
assignedTo: z.string().optional(),
|
|
|
|
// 2. Coordinates
|
|
address1: z.string().min(5, "L'adresse est requise"),
|
|
address2: z.string().optional(),
|
|
zipCode: z.string().min(4, "Code postal requis"),
|
|
city: z.string().min(2, "Ville requise"),
|
|
country: z.string().default('France'),
|
|
|
|
// Contact
|
|
civility: z.string().optional(),
|
|
contactName: z.string().min(2, "Nom requis"),
|
|
contactFirstName: z.string().min(2, "Prénom requis"),
|
|
position: z.string().optional(),
|
|
email: z.string().email("Email invalide"),
|
|
phone: z.string().min(10, "Numéro invalide"),
|
|
phoneSecondary: z.string().optional(),
|
|
contactPref: z.enum(['email', 'phone', 'sms', 'whatsapp']).default('email'),
|
|
|
|
// 3. Company Info (Conditional in UI logic, permissive in schema)
|
|
legalForm: z.string().optional(),
|
|
employees: z.string().optional(),
|
|
revenue: z.string().optional(),
|
|
sector: z.string().optional(),
|
|
website: z.string().url("URL invalide").optional().or(z.literal('')),
|
|
linkedin: z.string().url("URL invalide").optional().or(z.literal('')),
|
|
|
|
// 4. Commercial
|
|
priceFamily: z.string().optional(),
|
|
accountingCategory: z.string().optional(),
|
|
accountingAccount: z.string().optional(),
|
|
paymentMethod: z.string().optional(),
|
|
paymentTerm: z.string().optional(),
|
|
billingConditions: z.string().optional(),
|
|
discountCommercial: z.string().optional(),
|
|
discountFinancial: z.string().optional(),
|
|
|
|
// 5. Technical
|
|
needsDescription: z.string().optional(),
|
|
products: z.array(z.string()).optional(),
|
|
interestLevel: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
urgency: z.string().optional(),
|
|
budget: z.string().optional(),
|
|
followUpDate: z.string().optional(),
|
|
tags: z.string().optional(), // Simplification for tags as comma string
|
|
|
|
// 6. Notes
|
|
notes: z.string().optional(),
|
|
|
|
// 7. Automation
|
|
autoOpportunity: z.boolean().default(false),
|
|
autoTask: z.boolean().default(false),
|
|
sendWelcome: z.boolean().default(false),
|
|
autoAssign: z.boolean().default(false),
|
|
|
|
// 8. Permissions
|
|
visibility: z.enum(['me', 'team', 'all']).default('team'),
|
|
role: z.string().default('commercial'),
|
|
}).refine((data) => {
|
|
if (data.type === 'entreprise' && !data.companyName) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}, {
|
|
message: "La raison sociale est requise pour une entreprise",
|
|
path: ["companyName"],
|
|
});
|
|
|
|
|
|
// --- Components ---
|
|
|
|
const Section = ({ title, icon: Icon, children, defaultOpen = true, progress = 100 }) => {
|
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
|
|
return (
|
|
<div className="bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-xl overflow-hidden shadow-sm transition-all duration-200 hover:shadow-md">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="w-full flex items-center justify-between p-4 bg-gray-50/50 dark:bg-gray-900/50 hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className={cn("p-2 rounded-lg bg-red-50 dark:bg-red-900/20 text-[#007E45]")}>
|
|
<Icon className="w-5 h-5" />
|
|
</div>
|
|
<div className="text-left">
|
|
<h3 className="font-semibold text-gray-900 dark:text-white">{title}</h3>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{progress < 100 && (
|
|
<span className="text-xs font-medium text-amber-600 bg-amber-50 px-2 py-1 rounded-full">
|
|
Incomplet
|
|
</span>
|
|
)}
|
|
{isOpen ? <ChevronUp className="w-5 h-5 text-gray-400" /> : <ChevronDown className="w-5 h-5 text-gray-400" />}
|
|
</div>
|
|
</button>
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: 'auto', opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div className="p-6 border-t border-gray-100 dark:border-gray-800">
|
|
{children}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const InputGroup = ({ label, error, required, tooltip, children, className }) => (
|
|
<div className={cn("space-y-1.5", className)}>
|
|
<div className="flex items-center justify-between">
|
|
<label className="flex items-center gap-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{label}
|
|
{required && <span className="text-[#007E45]">*</span>}
|
|
{tooltip && (
|
|
<div className="group relative ml-1 cursor-help">
|
|
<Info className="w-3.5 h-3.5 text-gray-400" />
|
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-48 p-2 bg-gray-900 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-10">
|
|
{tooltip}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</label>
|
|
</div>
|
|
<div className="relative">
|
|
{children}
|
|
{error ? (
|
|
<AlertCircle className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-red-500 pointer-events-none" />
|
|
) : !error && required && children?.props?.value ? (
|
|
<CheckCircle2 className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-green-500 pointer-events-none" />
|
|
) : null}
|
|
</div>
|
|
{error && <p className="text-xs text-red-500 font-medium animate-in slide-in-from-top-1">{error.message}</p>}
|
|
</div>
|
|
);
|
|
|
|
|
|
const CreateProspectPage = () => {
|
|
const navigate = useNavigate();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [completion, setCompletion] = useState(0);
|
|
|
|
const { register, handleSubmit, watch, control, formState: { errors, isValid, touchedFields }, reset } = useForm({
|
|
resolver: zodResolver(prospectSchema),
|
|
mode: "onChange",
|
|
defaultValues: {
|
|
type: 'entreprise',
|
|
contactPref: 'email',
|
|
interestLevel: 'medium',
|
|
country: 'France',
|
|
autoOpportunity: false,
|
|
visibility: 'team'
|
|
}
|
|
});
|
|
|
|
const watchAllFields = watch();
|
|
const type = watch('type');
|
|
|
|
// Calculate completion percentage
|
|
useEffect(() => {
|
|
const requiredFields = ['name', 'address1', 'zipCode', 'city', 'contactName', 'contactFirstName', 'email', 'phone'];
|
|
if (type === 'entreprise') requiredFields.push('companyName');
|
|
|
|
const filled = requiredFields.filter(field => !!watchAllFields[field]);
|
|
setCompletion(Math.round((filled.length / requiredFields.length) * 100));
|
|
}, [watchAllFields, type]);
|
|
|
|
const onSubmit = async (data) => {
|
|
setIsLoading(true);
|
|
// Simulate API call
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
console.log(data);
|
|
toast({
|
|
title: "Prospect créé avec succès",
|
|
description: `${data.name} a été ajouté à votre base CRM.`,
|
|
variant: "success"
|
|
});
|
|
setIsLoading(false);
|
|
navigate('/prospects');
|
|
};
|
|
|
|
const inputClass = (error, success) => cn(
|
|
"w-full px-3 py-2 bg-white dark:bg-gray-950 border rounded-xl text-sm shadow-sm transition-all focus:outline-none focus:ring-2 pr-10",
|
|
error
|
|
? "border-red-300 focus:border-red-500 focus:ring-red-200"
|
|
: success
|
|
? "border-green-300 focus:border-green-500 focus:ring-green-200"
|
|
: "border-gray-200 dark:border-gray-800 focus:border-[#007E45] focus:ring-red-100/50"
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<Helmet>
|
|
<title>Créer un prospect - Bijou ERP</title>
|
|
</Helmet>
|
|
|
|
<div className="max-w-5xl mx-auto pb-24 space-y-6">
|
|
{/* Header */}
|
|
<div className="space-y-4">
|
|
<nav className="flex items-center gap-2 text-sm text-gray-500">
|
|
<button onClick={() => navigate('/home')} className="hover:text-gray-900 dark:hover:text-white">Accueil</button>
|
|
<span>/</span>
|
|
<button onClick={() => navigate('/home/prospects')} className="hover:text-gray-900 dark:hover:text-white">CRM</button>
|
|
<span>/</span>
|
|
<span className="font-medium text-gray-900 dark:text-white">Créer un prospect</span>
|
|
</nav>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<button
|
|
onClick={() => navigate('/home/prospects')}
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors"
|
|
>
|
|
<ArrowLeft className="w-6 h-6 text-gray-500" />
|
|
</button>
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Nouveau Prospect</h1>
|
|
<p className="text-sm text-gray-500">Remplissez les informations pour créer une fiche prospect complète.</p>
|
|
</div>
|
|
</div>
|
|
<div className="hidden md:block w-64">
|
|
<div className="flex justify-between text-xs mb-1">
|
|
<span className="text-gray-500">Complétion de la fiche</span>
|
|
<span className="font-medium text-[#007E45]">{completion}%</span>
|
|
</div>
|
|
<Progress value={completion} className="h-2" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
|
|
{/* SECTION 1: INFORMATIONS GÉNÉRALES */}
|
|
<Section title="Informations Générales" icon={Building2}>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<InputGroup label="Type de prospect" className="col-span-1 md:col-span-2">
|
|
<div className="flex gap-4">
|
|
{['entreprise', 'particulier'].map((val) => (
|
|
<label key={val} className={cn(
|
|
"flex-1 flex items-center justify-center gap-2 p-3 rounded-xl border cursor-pointer transition-all",
|
|
type === val
|
|
? "bg-red-50 border-[#007E45] text-[#007E45]"
|
|
: "bg-white border-gray-200 hover:bg-gray-50 text-gray-600"
|
|
)}>
|
|
<input
|
|
type="radio"
|
|
value={val}
|
|
{...register('type')}
|
|
className="hidden"
|
|
/>
|
|
<span className="capitalize font-medium">{val}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</InputGroup>
|
|
|
|
<InputGroup label="Nom du prospect" error={errors.name} required>
|
|
<input
|
|
{...register('name')}
|
|
placeholder="Nom d'affichage (ex: ACME Corp)"
|
|
className={inputClass(errors.name, touchedFields.name && !errors.name)}
|
|
/>
|
|
</InputGroup>
|
|
|
|
{type === 'entreprise' && (
|
|
<InputGroup label="Raison sociale" error={errors.companyName} required>
|
|
<input
|
|
{...register('companyName')}
|
|
placeholder="Raison sociale officielle"
|
|
className={inputClass(errors.companyName, touchedFields.companyName && !errors.companyName)}
|
|
/>
|
|
</InputGroup>
|
|
)}
|
|
|
|
<InputGroup label="SIRET / TVA">
|
|
<input
|
|
{...register('siret')}
|
|
placeholder="123 456 789 00012"
|
|
className={inputClass(errors.siret)}
|
|
/>
|
|
</InputGroup>
|
|
|
|
<InputGroup label="Catégorie">
|
|
<select {...register('category')} className={inputClass()}>
|
|
<option value="">Sélectionner...</option>
|
|
<option value="standard">Standard</option>
|
|
<option value="premium">Premium</option>
|
|
<option value="revendeur">Revendeur</option>
|
|
<option value="vip">VIP</option>
|
|
</select>
|
|
</InputGroup>
|
|
|
|
<InputGroup label="Origine">
|
|
<select {...register('origin')} className={inputClass()}>
|
|
<option value="">Sélectionner...</option>
|
|
<option value="web">Site Web</option>
|
|
<option value="phone">Téléphone</option>
|
|
<option value="email">Email</option>
|
|
<option value="social">Réseaux Sociaux</option>
|
|
<option value="partner">Partenaire</option>
|
|
</select>
|
|
</InputGroup>
|
|
|
|
<InputGroup label="Commercial attribué">
|
|
<select {...register('assignedTo')} className={inputClass()}>
|
|
<option value="">Sélectionner...</option>
|
|
{mockUsers.map(user => (
|
|
<option key={user.id} value={user.id}>{user.name}</option>
|
|
))}
|
|
</select>
|
|
</InputGroup>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* SECTION 2: COORDONNÉES */}
|
|
<Section title="Coordonnées & Contact" icon={MapPin}>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
<div className="space-y-4">
|
|
<h4 className="font-medium text-gray-900 border-b pb-2">Adresse</h4>
|
|
<InputGroup label="Adresse" error={errors.address1} required>
|
|
<input {...register('address1')} placeholder="123 Rue de la Paix" className={inputClass(errors.address1)} />
|
|
</InputGroup>
|
|
<InputGroup label="Complément">
|
|
<input {...register('address2')} placeholder="Bâtiment B" className={inputClass()} />
|
|
</InputGroup>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<InputGroup label="Code Postal" error={errors.zipCode} required>
|
|
<input {...register('zipCode')} placeholder="75001" className={inputClass(errors.zipCode)} />
|
|
</InputGroup>
|
|
<InputGroup label="Ville" error={errors.city} required>
|
|
<input {...register('city')} placeholder="Paris" className={inputClass(errors.city)} />
|
|
</InputGroup>
|
|
</div>
|
|
<InputGroup label="Pays">
|
|
<select {...register('country')} className={inputClass()}>
|
|
<option value="France">France</option>
|
|
<option value="Belgium">Belgique</option>
|
|
<option value="Switzerland">Suisse</option>
|
|
<option value="Germany">Allemagne</option>
|
|
<option value="Spain">Espagne</option>
|
|
</select>
|
|
</InputGroup>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<h4 className="font-medium text-gray-900 border-b pb-2">Contact Principal</h4>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<InputGroup label="Civilité">
|
|
<select {...register('civility')} className={inputClass()}>
|
|
<option value="M.">M.</option>
|
|
<option value="Mme">Mme</option>
|
|
</select>
|
|
</InputGroup>
|
|
<InputGroup label="Prénom" className="col-span-2" error={errors.contactFirstName} required>
|
|
<input {...register('contactFirstName')} className={inputClass(errors.contactFirstName)} />
|
|
</InputGroup>
|
|
</div>
|
|
<InputGroup label="Nom" error={errors.contactName} required>
|
|
<input {...register('contactName')} className={inputClass(errors.contactName)} />
|
|
</InputGroup>
|
|
<InputGroup label="Email" error={errors.email} required>
|
|
<input type="email" {...register('email')} className={inputClass(errors.email)} />
|
|
</InputGroup>
|
|
<InputGroup label="Téléphone" error={errors.phone} required>
|
|
<input type="tel" {...register('phone')} className={inputClass(errors.phone)} />
|
|
</InputGroup>
|
|
<InputGroup label="Préférence de contact">
|
|
<div className="flex gap-2 mt-2">
|
|
{['email', 'phone', 'sms', 'whatsapp'].map(pref => (
|
|
<label key={pref} className={cn(
|
|
"px-3 py-1.5 rounded-lg text-xs font-medium border cursor-pointer transition-colors",
|
|
watchAllFields.contactPref === pref
|
|
? "bg-gray-900 text-white border-gray-900"
|
|
: "bg-white text-gray-600 border-gray-200 hover:bg-gray-50"
|
|
)}>
|
|
<input type="radio" value={pref} {...register('contactPref')} className="hidden" />
|
|
<span className="capitalize">{pref}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</InputGroup>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* SECTION 3: COMPANY INFO (Conditional) */}
|
|
{type === 'entreprise' && (
|
|
<Section title="Informations Entreprise" icon={Building2}>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<InputGroup label="Forme Juridique">
|
|
<select {...register('legalForm')} className={inputClass()}>
|
|
<option value="">Sélectionner...</option>
|
|
<option value="SARL">SARL</option>
|
|
<option value="SAS">SAS</option>
|
|
<option value="SA">SA</option>
|
|
<option value="EURL">EURL</option>
|
|
<option value="Auto">Auto-entrepreneur</option>
|
|
</select>
|
|
</InputGroup>
|
|
<InputGroup label="Effectif">
|
|
<select {...register('employees')} className={inputClass()}>
|
|
<option value="1-10">1-10</option>
|
|
<option value="11-50">11-50</option>
|
|
<option value="51-200">51-200</option>
|
|
<option value="200+">200+</option>
|
|
</select>
|
|
</InputGroup>
|
|
<InputGroup label="CA Annuel (€)">
|
|
<input {...register('revenue')} placeholder="ex: 150 000" className={inputClass()} />
|
|
</InputGroup>
|
|
<InputGroup label="Secteur d'activité" className="md:col-span-2">
|
|
<select {...register('sector')} className={inputClass()}>
|
|
<option value="">Sélectionner...</option>
|
|
<option value="IT">Informatique & Télécoms</option>
|
|
<option value="Retail">Commerce & Distribution</option>
|
|
<option value="Industry">Industrie</option>
|
|
<option value="Services">Services</option>
|
|
<option value="Construction">BTP</option>
|
|
</select>
|
|
</InputGroup>
|
|
<InputGroup label="Site Web" error={errors.website}>
|
|
<input {...register('website')} placeholder="https://" className={inputClass(errors.website)} />
|
|
</InputGroup>
|
|
<InputGroup label="LinkedIn" className="md:col-span-3" error={errors.linkedin}>
|
|
<input {...register('linkedin')} placeholder="https://linkedin.com/company/..." className={inputClass(errors.linkedin)} />
|
|
</InputGroup>
|
|
</div>
|
|
</Section>
|
|
)}
|
|
|
|
{/* SECTION 4: COMMERCIAL & COMPTA */}
|
|
<Section title="Commercial & Comptabilité" icon={Wallet} defaultOpen={false}>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<InputGroup label="Mode de règlement">
|
|
<select {...register('paymentMethod')} className={inputClass()}>
|
|
<option value="virement">Virement</option>
|
|
<option value="cheque">Chèque</option>
|
|
<option value="cb">Carte Bancaire</option>
|
|
<option value="prelevement">Prélèvement</option>
|
|
</select>
|
|
</InputGroup>
|
|
<InputGroup label="Délai de paiement">
|
|
<select {...register('paymentTerm')} className={inputClass()}>
|
|
<option value="30">30 jours</option>
|
|
<option value="45">45 jours</option>
|
|
<option value="60">60 jours</option>
|
|
<option value="immediate">Réception de facture</option>
|
|
</select>
|
|
</InputGroup>
|
|
<InputGroup label="Compte comptable">
|
|
<input {...register('accountingAccount')} placeholder="411..." className={inputClass()} />
|
|
</InputGroup>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<InputGroup label="Remise Com. (%)">
|
|
<input type="number" {...register('discountCommercial')} placeholder="0" className={inputClass()} />
|
|
</InputGroup>
|
|
<InputGroup label="Remise Fin. (%)">
|
|
<input type="number" {...register('discountFinancial')} placeholder="0" className={inputClass()} />
|
|
</InputGroup>
|
|
</div>
|
|
<InputGroup label="Conditions de facturation" className="md:col-span-2">
|
|
<textarea {...register('billingConditions')} rows={3} className={inputClass()} placeholder="Conditions particulières..." />
|
|
</InputGroup>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* SECTION 5: BESOINS TECHNIQUES */}
|
|
<Section title="Données Techniques & Besoins" icon={Zap}>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<InputGroup label="Niveau d'intérêt">
|
|
<div className="flex gap-4">
|
|
{[
|
|
{ val: 'low', label: 'Faible', color: 'bg-gray-100 text-gray-600' },
|
|
{ val: 'medium', label: 'Moyen', color: 'bg-orange-100 text-orange-600' },
|
|
{ val: 'high', label: 'Fort', color: 'bg-green-100 text-green-600' }
|
|
].map(opt => (
|
|
<label key={opt.val} className={cn(
|
|
"flex-1 p-3 text-center rounded-xl border cursor-pointer transition-all font-medium",
|
|
watchAllFields.interestLevel === opt.val ? `border-current ${opt.color}` : "border-gray-200 hover:bg-gray-50"
|
|
)}>
|
|
<input type="radio" value={opt.val} {...register('interestLevel')} className="hidden" />
|
|
{opt.label}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</InputGroup>
|
|
<InputGroup label="Urgence">
|
|
<select {...register('urgency')} className={inputClass()}>
|
|
<option value="now">Immédiat</option>
|
|
<option value="1month">Dans le mois</option>
|
|
<option value="3months">3 mois</option>
|
|
<option value="later">Plus tard</option>
|
|
</select>
|
|
</InputGroup>
|
|
<InputGroup label="Description du besoin" className="md:col-span-2">
|
|
<textarea
|
|
{...register('needsDescription')}
|
|
rows={4}
|
|
className={inputClass()}
|
|
placeholder="Détaillez le besoin du client..."
|
|
/>
|
|
<p className="text-xs text-gray-500 text-right mt-1">
|
|
{watchAllFields.needsDescription?.length || 0} caractères
|
|
</p>
|
|
</InputGroup>
|
|
<InputGroup label="Budget Estimé (€)">
|
|
<input {...register('budget')} type="number" className={inputClass()} />
|
|
</InputGroup>
|
|
<InputGroup label="Tags">
|
|
<input {...register('tags')} placeholder="vip, chaud, tech..." className={inputClass()} />
|
|
</InputGroup>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* SECTION 6: NOTES & HISTORIQUE */}
|
|
<Section title="Notes & Documents" icon={FileText} defaultOpen={false}>
|
|
<div className="space-y-6">
|
|
<InputGroup label="Commentaire interne">
|
|
<textarea {...register('notes')} rows={4} className={inputClass()} placeholder="Notes réservées à l'équipe..." />
|
|
</InputGroup>
|
|
<div className="border-2 border-dashed border-gray-200 dark:border-gray-800 rounded-xl p-8 text-center hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors cursor-pointer">
|
|
<UploadCloud className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">Glissez-déposez des fichiers ici ou cliquez pour uploader</p>
|
|
<p className="text-xs text-gray-400 mt-1">(PDF, DOCX, JPG - Max 10Mo)</p>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* SECTION 7: ACTIONS AUTOMATIQUES */}
|
|
<Section title="Actions Automatisées" icon={Zap} defaultOpen={false}>
|
|
<div className="space-y-4">
|
|
{[
|
|
{ id: 'autoOpportunity', label: 'Créer automatiquement une opportunité', desc: 'Ajoute une opportunité dans le pipeline par défaut' },
|
|
{ id: 'autoTask', label: 'Créer une tâche de suivi', desc: 'Rappel automatique dans 3 jours' },
|
|
{ id: 'sendWelcome', label: 'Envoyer un email de bienvenue', desc: 'Template "Nouveau prospect standard"' },
|
|
{ id: 'autoAssign', label: 'Assignation intelligente', desc: 'Attribuer au commercial le plus disponible' }
|
|
].map((action) => (
|
|
<div key={action.id} className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900 rounded-xl">
|
|
<div>
|
|
<p className="font-medium text-gray-900 dark:text-white">{action.label}</p>
|
|
<p className="text-xs text-gray-500">{action.desc}</p>
|
|
</div>
|
|
<Controller
|
|
name={action.id}
|
|
control={control}
|
|
render={({ field }) => (
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
)}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Section>
|
|
|
|
{/* SECTION 8: PERMISSIONS */}
|
|
<Section title="Permissions & Visibilité" icon={Shield} defaultOpen={false}>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<InputGroup label="Visible par">
|
|
<div className="space-y-2">
|
|
{[
|
|
{ val: 'me', label: 'Moi uniquement' },
|
|
{ val: 'team', label: 'Mon équipe commerciale' },
|
|
{ val: 'all', label: 'Toute l\'entreprise' }
|
|
].map(opt => (
|
|
<label key={opt.val} className="flex items-center gap-3 p-3 border rounded-xl cursor-pointer hover:bg-gray-50">
|
|
<input type="radio" value={opt.val} {...register('visibility')} className="w-4 h-4 text-[#007E45]" />
|
|
<span className="text-sm font-medium">{opt.label}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</InputGroup>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* STICKY ACTIONS */}
|
|
<div className="sticky bottom-4 z-10 bg-white/90 dark:bg-gray-950/90 backdrop-blur-md border border-gray-200 dark:border-gray-800 p-4 rounded-2xl shadow-2xl flex items-center justify-between">
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate('/prospects')}
|
|
className="px-6 py-2.5 text-sm font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800 rounded-xl transition-colors"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
className="px-6 py-2.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 rounded-xl transition-colors"
|
|
>
|
|
Enregistrer comme brouillon
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading || !isValid}
|
|
className="inline-flex items-center gap-2 px-8 py-2.5 bg-[#007E45] text-white text-sm font-medium rounded-xl hover:bg-[#7a1002] disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg shadow-red-900/20"
|
|
>
|
|
{isLoading ? (
|
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
) : (
|
|
<Save className="w-4 h-4" />
|
|
)}
|
|
Créer le prospect
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default CreateProspectPage;
|