import { useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useForm, useFieldArray, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { ArrowLeft, Plus, Trash2, FileText, Package, Calculator, } from 'lucide-react'; import { Button } from '@components/atoms/Button'; import { Input } from '@components/atoms/Input'; import { Label } from '@components/atoms/Label'; import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@components/molecules/Card'; import { Alert } from '@components/molecules/Alert'; import { Select, type SelectOption } from '@components/organisms/Select'; import { Breadcrumbs } from '@components/organisms/Breadcrumbs'; import { useToast } from '@components/organisms/Toast'; import { useQuotations } from '@features/sales/hooks'; import { useCustomers } from '@features/partners/hooks'; import { useCompanyStore } from '@stores/useCompanyStore'; import { formatNumber } from '@utils/formatters'; // ==================== Types ==================== interface LineItem { id: string; productId: string; productName: string; description: string; quantity: number; uomId: string; uomName: string; priceUnit: number; discount: number; taxRate: number; } // ==================== Validation Schema ==================== const lineItemSchema = z.object({ id: z.string(), productId: z.string().min(1, 'Producto requerido'), productName: z.string(), description: z.string(), quantity: z.number().min(0.01, 'Cantidad debe ser mayor a 0'), uomId: z.string().min(1, 'Unidad requerida'), uomName: z.string(), priceUnit: z.number().min(0, 'Precio no puede ser negativo'), discount: z.number().min(0).max(100, 'Descuento debe estar entre 0 y 100'), taxRate: z.number().min(0).max(100), }); const quotationSchema = z.object({ partnerId: z.string().min(1, 'Cliente es requerido'), quotationDate: z.string().min(1, 'Fecha de cotizacion requerida'), validityDate: z.string().optional(), currencyId: z.string().min(1, 'Moneda requerida'), notes: z.string().optional(), termsConditions: z.string().optional(), lines: z.array(lineItemSchema).min(1, 'Debe agregar al menos un producto'), }); type QuotationFormData = z.infer; // ==================== Helpers ==================== const generateTempId = () => `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const formatCurrency = (value: number): string => { return formatNumber(value, 'es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; // Mock products for demo (should come from products API) const mockProducts: SelectOption[] = [ { value: 'prod-001', label: 'Producto A - SKU001' }, { value: 'prod-002', label: 'Producto B - SKU002' }, { value: 'prod-003', label: 'Producto C - SKU003' }, { value: 'prod-004', label: 'Servicio Premium' }, { value: 'prod-005', label: 'Licencia Software Anual' }, ]; // Mock UOMs (should come from UOM API) const mockUoms: SelectOption[] = [ { value: 'uom-001', label: 'Unidad' }, { value: 'uom-002', label: 'Kg' }, { value: 'uom-003', label: 'Litro' }, { value: 'uom-004', label: 'Hora' }, { value: 'uom-005', label: 'Caja' }, ]; // Mock currencies (should come from currency API) const mockCurrencies: SelectOption[] = [ { value: 'curr-001', label: 'MXN - Peso Mexicano' }, { value: 'curr-002', label: 'USD - Dolar Estadounidense' }, { value: 'curr-003', label: 'EUR - Euro' }, ]; // Default tax rate (16% IVA for Mexico) const DEFAULT_TAX_RATE = 16; // ==================== Component ==================== export function QuotationCreatePage() { const navigate = useNavigate(); const { showToast } = useToast(); const { currentCompany } = useCompanyStore(); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); // API hooks const { createQuotation } = useQuotations({ autoFetch: false }); const { partners: customers, isLoading: loadingCustomers } = useCustomers({ limit: 100 }); // Calculate default validity date (30 days from now) const defaultValidityDate = new Date(); defaultValidityDate.setDate(defaultValidityDate.getDate() + 30); // Form setup const { register, control, handleSubmit, watch, setValue, formState: { errors }, } = useForm({ resolver: zodResolver(quotationSchema), defaultValues: { partnerId: '', quotationDate: new Date().toISOString().split('T')[0], validityDate: defaultValidityDate.toISOString().split('T')[0], currencyId: 'curr-001', notes: '', termsConditions: '', lines: [], }, }); const { fields, append, remove } = useFieldArray({ control, name: 'lines', }); // Watch for calculations const watchLines = watch('lines'); // Calculate totals const calculateLineTotals = useCallback((line: LineItem) => { const subtotal = line.quantity * line.priceUnit; const discountAmt = subtotal * (line.discount / 100); const afterDiscount = subtotal - discountAmt; const taxAmt = afterDiscount * (line.taxRate / 100); const total = afterDiscount + taxAmt; return { subtotal, discountAmt, afterDiscount, taxAmt, total }; }, []); const totals = watchLines.reduce( (acc, line) => { const lineTotals = calculateLineTotals(line); return { subtotal: acc.subtotal + lineTotals.subtotal, lineDiscount: acc.lineDiscount + lineTotals.discountAmt, taxAmount: acc.taxAmount + lineTotals.taxAmt, lineTotal: acc.lineTotal + lineTotals.total, }; }, { subtotal: 0, lineDiscount: 0, taxAmount: 0, lineTotal: 0 } ); // Build customer options const customerOptions: SelectOption[] = customers.map((c) => ({ value: c.id, label: c.name, })); // Add new line item const handleAddLine = () => { append({ id: generateTempId(), productId: '', productName: '', description: '', quantity: 1, uomId: 'uom-001', uomName: 'Unidad', priceUnit: 0, discount: 0, taxRate: DEFAULT_TAX_RATE, }); }; // Handle product selection for a line const handleProductChange = (index: number, productId: string) => { const product = mockProducts.find((p) => p.value === productId); if (product) { setValue(`lines.${index}.productId`, productId); setValue(`lines.${index}.productName`, product.label); setValue(`lines.${index}.description`, product.label); // In real implementation, fetch product price from API setValue(`lines.${index}.priceUnit`, Math.random() * 1000 + 100); } }; // Handle UOM selection for a line const handleUomChange = (index: number, uomId: string) => { const uom = mockUoms.find((u) => u.value === uomId); if (uom) { setValue(`lines.${index}.uomId`, uomId); setValue(`lines.${index}.uomName`, uom.label); } }; // Submit handler const onSubmit = async (data: QuotationFormData) => { if (!currentCompany) { setError('No hay una empresa seleccionada'); return; } setIsSubmitting(true); setError(null); try { const quotationData = { companyId: currentCompany.id, partnerId: data.partnerId, quotationDate: data.quotationDate, validityDate: data.validityDate || undefined, currencyId: data.currencyId, notes: data.notes || undefined, termsConditions: data.termsConditions || undefined, }; const newQuotation = await createQuotation(quotationData); showToast({ type: 'success', title: 'Cotizacion creada', message: `La cotizacion ${newQuotation.quotationNumber} ha sido creada exitosamente.`, }); navigate(`/sales/quotations/${newQuotation.id}`); } catch (err) { const message = err instanceof Error ? err.message : 'Error al crear la cotizacion'; setError(message); showToast({ type: 'error', title: 'Error', message, }); } finally { setIsSubmitting(false); } }; return (

Crear Cotizacion

Complete los datos para crear una nueva cotizacion de venta

{error && ( setError(null)}> {error} )} {/* Section 1: Customer Information */} Informacion del Cliente
( {errors.quotationDate && (

{errors.quotationDate.message}

)}
(