323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { z } from 'zod';
|
|
import { Button } from '@components/atoms/Button';
|
|
import { FormField } from '@components/molecules/FormField';
|
|
import { Select } from '@components/organisms/Select';
|
|
import type { Partner, CreatePartnerDto, UpdatePartnerDto, PartnerType } from '../types';
|
|
|
|
const partnerSchema = z.object({
|
|
name: z.string().min(2, 'Mínimo 2 caracteres'),
|
|
legalName: z.string().optional(),
|
|
partnerType: z.enum(['person', 'company'] as const),
|
|
isCustomer: z.boolean(),
|
|
isSupplier: z.boolean(),
|
|
isEmployee: z.boolean(),
|
|
email: z.string().email('Email inválido').optional().or(z.literal('')),
|
|
phone: z.string().optional(),
|
|
mobile: z.string().optional(),
|
|
website: z.string().optional(),
|
|
taxId: z.string().optional(),
|
|
language: z.string().optional(),
|
|
notes: z.string().optional(),
|
|
internalNotes: z.string().optional(),
|
|
});
|
|
|
|
type FormData = z.infer<typeof partnerSchema>;
|
|
|
|
interface PartnerFormProps {
|
|
partner?: Partner;
|
|
onSubmit: (data: CreatePartnerDto | UpdatePartnerDto) => Promise<void>;
|
|
onCancel: () => void;
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
const partnerTypeOptions = [
|
|
{ value: 'person', label: 'Persona física' },
|
|
{ value: 'company', label: 'Empresa' },
|
|
];
|
|
|
|
const languageOptions = [
|
|
{ value: 'es', label: 'Español' },
|
|
{ value: 'en', label: 'English' },
|
|
];
|
|
|
|
export function PartnerForm({ partner, onSubmit, onCancel, isLoading }: PartnerFormProps) {
|
|
const isEditing = !!partner;
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
setValue,
|
|
watch,
|
|
formState: { errors },
|
|
} = useForm<FormData>({
|
|
resolver: zodResolver(partnerSchema),
|
|
defaultValues: partner
|
|
? {
|
|
name: partner.name,
|
|
legalName: partner.legalName || '',
|
|
partnerType: partner.partnerType,
|
|
isCustomer: partner.isCustomer,
|
|
isSupplier: partner.isSupplier,
|
|
isEmployee: partner.isEmployee,
|
|
email: partner.email || '',
|
|
phone: partner.phone || '',
|
|
mobile: partner.mobile || '',
|
|
website: partner.website || '',
|
|
taxId: partner.taxId || '',
|
|
language: partner.language || 'es',
|
|
notes: partner.notes || '',
|
|
internalNotes: partner.internalNotes || '',
|
|
}
|
|
: {
|
|
name: '',
|
|
legalName: '',
|
|
partnerType: 'company' as PartnerType,
|
|
isCustomer: true,
|
|
isSupplier: false,
|
|
isEmployee: false,
|
|
email: '',
|
|
phone: '',
|
|
mobile: '',
|
|
website: '',
|
|
taxId: '',
|
|
language: 'es',
|
|
notes: '',
|
|
internalNotes: '',
|
|
},
|
|
});
|
|
|
|
const selectedType = watch('partnerType');
|
|
const selectedLanguage = watch('language');
|
|
|
|
const handleFormSubmit = async (data: FormData) => {
|
|
const cleanData: CreatePartnerDto | UpdatePartnerDto = {
|
|
name: data.name,
|
|
partnerType: data.partnerType,
|
|
isCustomer: data.isCustomer,
|
|
isSupplier: data.isSupplier,
|
|
isEmployee: data.isEmployee,
|
|
isCompany: data.partnerType === 'company',
|
|
...(data.legalName && { legalName: data.legalName }),
|
|
...(data.email && { email: data.email }),
|
|
...(data.phone && { phone: data.phone }),
|
|
...(data.mobile && { mobile: data.mobile }),
|
|
...(data.website && { website: data.website }),
|
|
...(data.taxId && { taxId: data.taxId }),
|
|
...(data.language && { language: data.language }),
|
|
...(data.notes && { notes: data.notes }),
|
|
...(data.internalNotes && { internalNotes: data.internalNotes }),
|
|
};
|
|
await onSubmit(cleanData);
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
|
|
{/* Basic Info */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium text-gray-900">Información básica</h3>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<FormField
|
|
label="Nombre"
|
|
error={errors.name?.message}
|
|
required
|
|
>
|
|
<input
|
|
{...register('name')}
|
|
type="text"
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
placeholder="Nombre del partner"
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField
|
|
label="Tipo"
|
|
error={errors.partnerType?.message}
|
|
required
|
|
>
|
|
<Select
|
|
options={partnerTypeOptions}
|
|
value={selectedType}
|
|
onChange={(value) => setValue('partnerType', value as PartnerType)}
|
|
placeholder="Seleccionar tipo..."
|
|
/>
|
|
</FormField>
|
|
</div>
|
|
|
|
<FormField
|
|
label="Razón social"
|
|
error={errors.legalName?.message}
|
|
>
|
|
<input
|
|
{...register('legalName')}
|
|
type="text"
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
placeholder="Razón social (empresas)"
|
|
/>
|
|
</FormField>
|
|
|
|
{/* Partner Roles */}
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-700">
|
|
Tipo de relación
|
|
</label>
|
|
<div className="flex flex-wrap gap-4">
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
{...register('isCustomer')}
|
|
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
|
/>
|
|
<span className="text-sm text-gray-700">Cliente</span>
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
{...register('isSupplier')}
|
|
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
|
/>
|
|
<span className="text-sm text-gray-700">Proveedor</span>
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
{...register('isEmployee')}
|
|
className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
|
/>
|
|
<span className="text-sm text-gray-700">Empleado</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Contact Info */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium text-gray-900">Información de contacto</h3>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<FormField
|
|
label="Email"
|
|
error={errors.email?.message}
|
|
>
|
|
<input
|
|
{...register('email')}
|
|
type="email"
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
placeholder="email@ejemplo.com"
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField
|
|
label="Teléfono"
|
|
error={errors.phone?.message}
|
|
>
|
|
<input
|
|
{...register('phone')}
|
|
type="tel"
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
placeholder="+52 55 1234 5678"
|
|
/>
|
|
</FormField>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<FormField
|
|
label="Móvil"
|
|
error={errors.mobile?.message}
|
|
>
|
|
<input
|
|
{...register('mobile')}
|
|
type="tel"
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
placeholder="+52 55 8765 4321"
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField
|
|
label="Sitio web"
|
|
error={errors.website?.message}
|
|
>
|
|
<input
|
|
{...register('website')}
|
|
type="url"
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
placeholder="https://www.ejemplo.com"
|
|
/>
|
|
</FormField>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Fiscal Info */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium text-gray-900">Información fiscal</h3>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<FormField
|
|
label="RFC / Tax ID"
|
|
error={errors.taxId?.message}
|
|
>
|
|
<input
|
|
{...register('taxId')}
|
|
type="text"
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
placeholder="ABC123456XYZ"
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField
|
|
label="Idioma"
|
|
error={errors.language?.message}
|
|
>
|
|
<Select
|
|
options={languageOptions}
|
|
value={selectedLanguage || 'es'}
|
|
onChange={(value) => setValue('language', value as string)}
|
|
placeholder="Seleccionar idioma..."
|
|
/>
|
|
</FormField>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notes */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium text-gray-900">Notas</h3>
|
|
|
|
<FormField
|
|
label="Notas públicas"
|
|
error={errors.notes?.message}
|
|
>
|
|
<textarea
|
|
{...register('notes')}
|
|
rows={3}
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
placeholder="Notas visibles para el partner..."
|
|
/>
|
|
</FormField>
|
|
|
|
<FormField
|
|
label="Notas internas"
|
|
error={errors.internalNotes?.message}
|
|
>
|
|
<textarea
|
|
{...register('internalNotes')}
|
|
rows={3}
|
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
placeholder="Notas internas (solo visibles para empleados)..."
|
|
/>
|
|
</FormField>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
|
<Button type="button" variant="outline" onClick={onCancel}>
|
|
Cancelar
|
|
</Button>
|
|
<Button type="submit" isLoading={isLoading}>
|
|
{isEditing ? 'Guardar cambios' : 'Crear partner'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|