186 lines
8.1 KiB
TypeScript
186 lines
8.1 KiB
TypeScript
import { useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { X, Save, User, MapPin, Mail, Phone, CreditCard, FileText } from 'lucide-react';
|
|
import CuitInput from '../Shared/CuitInput';
|
|
import { clientService, type CreateClientRequest } from '../../services/clientService';
|
|
import { useToast } from '../../context/use-toast';
|
|
import clsx from 'clsx';
|
|
|
|
interface Props {
|
|
onClose: () => void;
|
|
onSuccess: (client: { id: number; name: string; dniOrCuit: string }) => void;
|
|
initialCuit?: string; // Por si el cajero buscó un CUIT y no existía, precargarlo
|
|
}
|
|
|
|
export default function ClientCreateModal({ onClose, onSuccess, initialCuit = '' }: Props) {
|
|
const { showToast } = useToast();
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// Estado del formulario
|
|
const [formData, setFormData] = useState<CreateClientRequest>({
|
|
name: '',
|
|
dniOrCuit: initialCuit,
|
|
email: '',
|
|
phone: '',
|
|
address: '',
|
|
taxType: 'Consumidor Final'
|
|
});
|
|
|
|
const [isCuitValid, setIsCuitValid] = useState(!!initialCuit);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!isCuitValid) return showToast("El CUIT ingresado no es válido", "error");
|
|
|
|
setLoading(true);
|
|
try {
|
|
const newClient = await clientService.create(formData);
|
|
showToast("Cliente creado exitosamente", "success");
|
|
onSuccess(newClient);
|
|
onClose();
|
|
} catch (error: any) {
|
|
console.error(error);
|
|
const msg = error.response?.data || "Error al crear cliente";
|
|
showToast(msg, "error");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[400] flex items-center justify-center p-4">
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }}
|
|
className="bg-white w-full max-w-2xl rounded-[2rem] shadow-2xl overflow-hidden border border-slate-200 flex flex-col"
|
|
>
|
|
{/* Header */}
|
|
<div className="p-6 bg-slate-50 border-b border-slate-100 flex justify-between items-center">
|
|
<div>
|
|
<h3 className="text-xl font-black text-slate-800 uppercase tracking-tight flex items-center gap-2">
|
|
<User className="text-blue-600" /> Alta Rápida de Cliente
|
|
</h3>
|
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest ml-8">Registro Express para Facturación</p>
|
|
</div>
|
|
<button onClick={onClose} className="p-2 hover:bg-white rounded-xl transition-colors text-slate-400"><X /></button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-8 space-y-6">
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* CUIT - Componente Reutilizable */}
|
|
<div className="md:col-span-1">
|
|
<CuitInput
|
|
label="DNI / CUIT"
|
|
value={formData.dniOrCuit}
|
|
required
|
|
onChange={(val, valid) => {
|
|
setFormData({ ...formData, dniOrCuit: val });
|
|
setIsCuitValid(valid);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Condición Fiscal */}
|
|
<div className="md:col-span-1 space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1">
|
|
<FileText size={12} /> Condición Fiscal
|
|
</label>
|
|
<select
|
|
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-bold text-sm text-slate-700 appearance-none"
|
|
value={formData.taxType}
|
|
onChange={e => setFormData({ ...formData, taxType: e.target.value })}
|
|
>
|
|
<option value="Consumidor Final">Consumidor Final</option>
|
|
<option value="IVA Responsable Inscripto">Responsable Inscripto</option>
|
|
<option value="Monotributista">Monotributista</option>
|
|
<option value="IVA Exento">IVA Exento</option>
|
|
<option value="No Alcanzado">No Alcanzado</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Razón Social */}
|
|
<div className="md:col-span-2 space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1">
|
|
<CreditCard size={12} /> Razón Social / Nombre Completo <span className="text-rose-500">*</span>
|
|
</label>
|
|
<input
|
|
required
|
|
type="text"
|
|
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-black text-slate-800 text-lg uppercase"
|
|
placeholder="Ej: EMPRESA S.A. o JUAN PEREZ"
|
|
value={formData.name}
|
|
onChange={e => setFormData({ ...formData, name: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
{/* Dirección */}
|
|
<div className="md:col-span-2 space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1">
|
|
<MapPin size={12} /> Dirección de Facturación
|
|
</label>
|
|
<input
|
|
type="text"
|
|
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-medium text-sm text-slate-700"
|
|
placeholder="Calle, Altura, Localidad"
|
|
value={formData.address || ''}
|
|
onChange={e => setFormData({ ...formData, address: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
{/* Email */}
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1">
|
|
<Mail size={12} /> Correo Electrónico
|
|
</label>
|
|
<input
|
|
type="email"
|
|
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-medium text-sm text-slate-700"
|
|
placeholder="contacto@cliente.com"
|
|
value={formData.email || ''}
|
|
onChange={e => setFormData({ ...formData, email: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
{/* Teléfono */}
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 flex items-center gap-1">
|
|
<Phone size={12} /> Teléfono
|
|
</label>
|
|
<input
|
|
type="text"
|
|
className="w-full p-3 bg-slate-50 border-2 border-slate-100 rounded-xl outline-none focus:border-blue-500 font-medium text-sm text-slate-700"
|
|
placeholder="Cod. Area + Número"
|
|
value={formData.phone || ''}
|
|
onChange={e => setFormData({ ...formData, phone: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer Actions */}
|
|
<div className="pt-4 flex gap-4 border-t border-slate-100">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="flex-1 py-4 bg-white border-2 border-slate-200 text-slate-500 font-black uppercase text-xs tracking-widest rounded-xl hover:bg-slate-50 transition-all"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={loading || !isCuitValid || !formData.name}
|
|
className={clsx(
|
|
"flex-[2] py-4 rounded-xl font-black uppercase text-xs tracking-widest shadow-lg transition-all flex items-center justify-center gap-2",
|
|
loading || !isCuitValid || !formData.name
|
|
? "bg-slate-300 text-white cursor-not-allowed shadow-none"
|
|
: "bg-blue-600 text-white shadow-blue-200 hover:bg-blue-700"
|
|
)}
|
|
>
|
|
<Save size={16} /> {loading ? 'Registrando...' : 'Crear Cliente'}
|
|
</button>
|
|
</div>
|
|
|
|
</form>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
} |