From 250b5914d5bb56a1455502e14ac384440e9a6a36 Mon Sep 17 00:00:00 2001 From: Edwar Amaya Date: Mon, 29 Jun 2026 01:43:45 -0500 Subject: [PATCH] feat: mejoras UI, fusion Ventas en Facturacion y metodos de pago dinamicos --- .gitignore | 5 + app/(protected)/bienes/bienes-client.tsx | 63 +-- .../categorias/categorias-client.tsx | 17 +- app/(protected)/clientes/clientes-client.tsx | 136 +++---- .../configuracion/configuracion-client.tsx | 225 +++++++---- .../consumibles/consumibles-client.tsx | 12 +- app/(protected)/dashboard/loading.tsx | 2 +- app/(protected)/facturas/facturas-client.tsx | 377 ++++++++++++------ .../facturas/invoice-form-modal.tsx | 96 ++++- .../facturas/invoice-pdf-button.tsx | 9 +- app/(protected)/facturas/loading.tsx | 35 +- app/(protected)/layout.tsx | 3 +- app/(protected)/rentabilidad/loading.tsx | 2 +- .../rentabilidad/rentabilidad-client.tsx | 227 ++++++----- app/(protected)/ventas/page.tsx | 70 +--- app/(protected)/ventas/ventas-client.tsx | 2 +- app/api/payment-methods/route.ts | 12 + app/globals.css | 268 ------------- lib/actions/companies.ts | 22 +- lib/actions/invoices.ts | 111 +++++- lib/actions/payment-methods.ts | 55 +++ prisma/schema.prisma | 19 +- 22 files changed, 964 insertions(+), 804 deletions(-) create mode 100644 app/api/payment-methods/route.ts create mode 100644 lib/actions/payment-methods.ts diff --git a/.gitignore b/.gitignore index 45254b6..9ce845a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,11 @@ .DS_Store *.pem +# IA +.claude/ +.playwright-cli/ +.playwright/ + # debug npm-debug.log* yarn-debug.log* diff --git a/app/(protected)/bienes/bienes-client.tsx b/app/(protected)/bienes/bienes-client.tsx index 373a274..350d279 100644 --- a/app/(protected)/bienes/bienes-client.tsx +++ b/app/(protected)/bienes/bienes-client.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Button } from '@/components/ui/button' import { formatCurrency } from '@/lib/utils' import { Building2, Plus, Edit2, Trash2 } from 'lucide-react' @@ -163,61 +164,61 @@ export function BienesClient({ properties: initialProperties }: BienesClientProp Lista de Bienes - - - - - - - - - - - - - + +
NombreTipoDirecciónValor CompraValor ActualAcciones
+ + + Nombre + Tipo + Dirección + Valor Compra + Valor Actual + Acciones + + + {safeProperties.map((property) => ( - - - - - - - - + + ))} - -
+ +

{property.name}

{property.description && (

{property.description}

)} -
+ + {typeLabel(property.type)} - {property.address || '-'}{formatCurrency(property.purchaseValue)} + + {property.address || '-'} + {formatCurrency(property.purchaseValue)} + {formatCurrency(property.currentValue)} - + +
-
+ +
)} diff --git a/app/(protected)/categorias/categorias-client.tsx b/app/(protected)/categorias/categorias-client.tsx index 61de84f..71748b9 100644 --- a/app/(protected)/categorias/categorias-client.tsx +++ b/app/(protected)/categorias/categorias-client.tsx @@ -5,8 +5,9 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { Plus, Tag, Trash2, Edit2, X } from 'lucide-react' +import { Plus, Tag, Trash2, Edit2, Package, ShoppingBag } from 'lucide-react' import { Modal } from '@/components/ui/modal' +import { EmptyState } from '@/components/ui/empty-state' import { createCategory, deleteCategory, updateCategory } from '@/lib/actions/categories' import { ConfirmationModal } from '@/components/ui/confirmation-modal' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' @@ -105,7 +106,12 @@ export function CategoriasClient({ categories: initialCategories }: CategoriasCl {supplyCategories.length === 0 ? ( -
No hay categorías de este tipo
+ ) : (
{supplyCategories.map(category => ( @@ -134,7 +140,12 @@ export function CategoriasClient({ categories: initialCategories }: CategoriasCl {productCategories.length === 0 ? ( -
No hay categorías de este tipo
+ ) : (
{productCategories.map(category => ( diff --git a/app/(protected)/clientes/clientes-client.tsx b/app/(protected)/clientes/clientes-client.tsx index 49ed075..ab8dca0 100644 --- a/app/(protected)/clientes/clientes-client.tsx +++ b/app/(protected)/clientes/clientes-client.tsx @@ -1,7 +1,7 @@ 'use client' import { useState } from 'react' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Card, CardContent } from '@/components/ui/card' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Button } from '@/components/ui/button' import { Users, Plus, Phone, Mail, MapPin, Edit2, Trash2 } from 'lucide-react' @@ -9,7 +9,6 @@ import { ConfirmationModal } from '@/components/ui/confirmation-modal' import { Modal } from '@/components/ui/modal' import { CustomerFormModal } from '@/components/customer-form-modal' import { EmptyState } from '@/components/ui/empty-state' -import { ExportButton } from '@/components/ui/export-button' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { deleteCustomer } from '@/lib/actions/customers' @@ -136,76 +135,69 @@ export function ClientesClient({ customers: initialCustomers }: ClientesClientPr onAction={handleOpenNew} /> ) : ( - - - Base de Clientes - - - - - - Cliente - Contacto - Ventas - Acciones - - - - {(customers ?? []).map((customer: Customer) => ( - - -
- {customer.name} -
- - {customer.address || 'Sin dirección'} -
-
-
- -
-
- - {customer.email || '-'} -
-
- - {customer.phone || '-'} -
-
-
- -
- {customer.saleCount} - Compras -
-
- -
- - -
-
-
- ))} -
-
-
-
+ + + + Cliente + Contacto + Ventas + Acciones + + + + {(customers ?? []).map((customer: Customer) => ( + + +
+ {customer.name} +
+ + {customer.address || 'Sin dirección'} +
+
+
+ +
+
+ + {customer.email || '-'} +
+
+ + {customer.phone || '-'} +
+
+
+ +
+ {customer.saleCount} + Compras +
+
+ +
+ + +
+
+
+ ))} +
+
)} {/* Modal */} diff --git a/app/(protected)/configuracion/configuracion-client.tsx b/app/(protected)/configuracion/configuracion-client.tsx index 378e169..7600371 100644 --- a/app/(protected)/configuracion/configuracion-client.tsx +++ b/app/(protected)/configuracion/configuracion-client.tsx @@ -4,9 +4,12 @@ import { useState } from 'react' import { useRouter } from 'next/navigation' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { updateMyCompany } from '@/lib/actions/companies' -import { Building2, Globe, Phone, Mail, MapPin, ImagePlus } from 'lucide-react' +import { createPaymentMethod, deletePaymentMethod } from '@/lib/actions/payment-methods' +import { Building2, Globe, Phone, Mail, MapPin, ImagePlus, Wallet, Plus, Trash2 } from 'lucide-react' import { sileo as toast } from 'sileo' +import { useQuery, useQueryClient } from '@tanstack/react-query' interface Company { id: string @@ -22,8 +25,47 @@ interface Company { export function ConfiguracionClient({ company }: { company: Company | null }) { const router = useRouter() + const queryClient = useQueryClient() const [saving, setSaving] = useState(false) const [logo, setLogo] = useState(company?.logo ?? '') + const [newMethodName, setNewMethodName] = useState('') + const [addingMethod, setAddingMethod] = useState(false) + + const { data: paymentMethods = [] } = useQuery({ + queryKey: ['payment-methods'], + queryFn: async () => { + const res = await fetch('/api/payment-methods') + if (!res.ok) return [] + return res.json() + }, + }) + + const handleAddMethod = async () => { + if (!newMethodName.trim()) return + setAddingMethod(true) + try { + const fd = new FormData() + fd.set('name', newMethodName.trim()) + await createPaymentMethod(fd) + setNewMethodName('') + queryClient.invalidateQueries({ queryKey: ['payment-methods'] }) + toast.success({ title: 'Método de pago agregado' }) + } catch (err: any) { + toast.error({ title: err.message || 'Error al agregar' }) + } finally { + setAddingMethod(false) + } + } + + const handleDeleteMethod = async (id: string) => { + try { + await deletePaymentMethod(id) + queryClient.invalidateQueries({ queryKey: ['payment-methods'] }) + toast.success({ title: 'Método de pago eliminado' }) + } catch { + toast.error({ title: 'Error al eliminar' }) + } + } const handleLogoUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] @@ -59,85 +101,134 @@ export function ConfiguracionClient({ company }: { company: Company | null }) { const storeUrl = company ? `${window.location.origin}/tienda/${company.slug}` : null return ( -
+
-

Configuración

-

Información de tu compañía y tienda pública

+

Configuración

+

Información de tu compañía y tienda pública

-
-
-
- {logo ? ( - - ) : ( - - )} -
-
- -

PNG, JPG. Recomendado: 512x512px

+
+ {/* Company Info */} + +
+
+ {logo ? ( + + ) : ( + + )} +
+
+ +

PNG, JPG. Recomendado: 512x512px

+
-
- - {storeUrl && ( - - - Tu tienda pública: {storeUrl} - - )} - -
-
- - + + {storeUrl && ( + + + Tu tienda pública: {storeUrl} + + )} + +
+
+ + +
+
+ + +

URL de tu tienda: /tienda/{company?.slug}

+
+
- - -

URL de tu tienda: /tienda/{company?.slug}

+ +
-
- -
- - -
-
-
- - -

Recibirás notificaciones de pedidos aquí

+
+
+ + +

Recibirás notificaciones de pedidos aquí

+
+
+ + +
+
- +
-
- -
- - -
- - - + + + + + {/* Payment Methods */} + + + + + Métodos de Pago + + + +
+ {paymentMethods.map((pm: any) => ( +
+ {pm.name} + +
+ ))} +
+
+
+ setNewMethodName(e.target.value)} + placeholder="Nuevo método de pago..." + className="h-10" + onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleAddMethod())} + /> + +
+
+
+
+
) } diff --git a/app/(protected)/consumibles/consumibles-client.tsx b/app/(protected)/consumibles/consumibles-client.tsx index 5ce9ec4..ed5f521 100644 --- a/app/(protected)/consumibles/consumibles-client.tsx +++ b/app/(protected)/consumibles/consumibles-client.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { NumericInput } from '@/components/ui/numeric-input' import { formatCurrency, formatDate } from '@/lib/utils' -import { Package, Plus, Trash2, AlertCircle } from 'lucide-react' +import { Package, Plus, AlertCircle } from 'lucide-react' import { registerConsumableUsage } from '@/lib/actions/consumables' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { EmptyState } from '@/components/ui/empty-state' @@ -222,10 +222,12 @@ export function ConsumiblesClient({ supplies: initialSupplies, history: initialH {history.length === 0 ? ( -
- -

No hay registros de consumo

-
+ ) : (
{history.map((item) => ( diff --git a/app/(protected)/dashboard/loading.tsx b/app/(protected)/dashboard/loading.tsx index ea51f51..4b067aa 100644 --- a/app/(protected)/dashboard/loading.tsx +++ b/app/(protected)/dashboard/loading.tsx @@ -2,7 +2,7 @@ import { Skeleton } from '@/components/ui/skeleton' export default function Loading() { return ( -
+
diff --git a/app/(protected)/facturas/facturas-client.tsx b/app/(protected)/facturas/facturas-client.tsx index 8e5eca5..f6028c4 100644 --- a/app/(protected)/facturas/facturas-client.tsx +++ b/app/(protected)/facturas/facturas-client.tsx @@ -5,12 +5,15 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { formatCurrency, formatDate } from '@/lib/utils' import { Button } from '@/components/ui/button' -import { Plus, FileText, Settings } from 'lucide-react' +import { Plus, FileText, Settings, Receipt, DollarSign, Clock, FileSignature, Edit2, Trash2, XCircle } from 'lucide-react' import { InvoiceFormModal } from './invoice-form-modal' import { EmptyState } from '@/components/ui/empty-state' import { InvoicePDFButton } from './invoice-pdf-button' +import { ExportButton } from '@/components/ui/export-button' +import { ConfirmationModal } from '@/components/ui/confirmation-modal' import { BusinessSettings, BusinessSettingsModal } from './business-settings-modal' -import { useQuery, useQueryClient } from '@tanstack/react-query' +import { deleteInvoice, cancelInvoice, updateInvoiceStatus } from '@/lib/actions/invoices' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' interface Product { id: string @@ -79,7 +82,9 @@ const paymentStatusColors: Record = { export function FacturasClient({ invoices: initialInvoices, customers, products, businessSettings }: FacturasClientProps) { const queryClient = useQueryClient() const [modalOpen, setModalOpen] = useState(false) + const [editingInvoice, setEditingInvoice] = useState(null) const [settingsOpen, setSettingsOpen] = useState(false) + const [deletingId, setDeletingId] = useState(null) const [localSettings, setLocalSettings] = useState(businessSettings) const { data: invoices } = useQuery({ @@ -99,9 +104,43 @@ export function FacturasClient({ invoices: initialInvoices, customers, products, .reduce((sum: number, inv: Invoice) => sum + Number(inv.total), 0) const pendingAmount = totalAmount - paidAmount + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + await deleteInvoice(id) + }, + onMutate: async (id) => { + await queryClient.cancelQueries({ queryKey: ['invoices'] }) + const previous = queryClient.getQueryData(['invoices']) + queryClient.setQueryData(['invoices'], (old) => + old?.filter((inv) => inv.id !== id) ?? [] + ) + setDeletingId(null) + return { previous } + }, + onError: (_err, _id, context) => { + if (context?.previous) { + queryClient.setQueryData(['invoices'], context.previous) + } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['invoices'] }) + }, + }) + const handleSuccess = () => { queryClient.invalidateQueries({ queryKey: ['invoices'] }) setModalOpen(false) + setEditingInvoice(null) + } + + const handleOpenNew = () => { + setEditingInvoice(null) + setModalOpen(true) + } + + const handleOpenEdit = (invoice: Invoice) => { + setEditingInvoice(invoice) + setModalOpen(true) } const handleSettingsSuccess = (updatedSettings?: any) => { @@ -120,161 +159,247 @@ export function FacturasClient({ invoices: initialInvoices, customers, products, } } + const exportData = safeInvoices.map((inv: Invoice) => ({ + Folio: inv.folio, + 'Fecha Emisión': formatDate(inv.issueDate), + Cliente: inv.customerName, + NIT: inv.customerTin || '-', + Subtotal: inv.subtotal, + 'IVA': inv.taxAmount, + Total: inv.total, + Estado: inv.status === 'draft' ? 'Borrador' : inv.status === 'issued' ? 'Emitida' : inv.status === 'paid' ? 'Pagada' : 'Cancelada', + 'Estado Pago': inv.paymentStatus === 'pending' ? 'Pendiente' : inv.paymentStatus === 'paid' ? 'Pagado' : 'Parcial', + 'Método Pago': inv.paymentMethod || '-', + })) + return ( -
-
+
+ {/* Header */} +
-

Facturación

-

Gestión de facturas y folios

+

Facturación

+

+ Gestión de facturas y folios. +

-
- -
-
- - - Total Facturado - - -
{formatCurrency(Number(totalAmount))}
+ {/* Stats */} +
+ +
+ +
+
+ +
+
+

Total Facturado

+

{formatCurrency(Number(totalAmount))}

+
+
- - - Cobrado - - -
{formatCurrency(Number(paidAmount))}
+ + +
+ +
+
+ +
+
+

Cobrado

+

{formatCurrency(Number(paidAmount))}

+
+
- - - Pendiente - - -
{formatCurrency(Number(pendingAmount))}
+ + +
+ +
+
+ +
+
+

Pendiente

+

{formatCurrency(Number(pendingAmount))}

+
+
- - - Total Facturas - - -
{safeInvoices.length}
+ + +
+ +
+
+ +
+
+

Total Facturas

+

{safeInvoices.length}

+
+
- {businessSettings && ( - - + {localSettings && ( + +
- {businessSettings.businessName} + {localSettings.businessName}
- {businessSettings.tin && NIT: {businessSettings.tin}} - IVA: {Number(businessSettings.taxRate)}% + {localSettings.tin && NIT: {localSettings.tin}} + IVA: {Number(localSettings.taxRate)}%
)} - - - Facturas - - - {safeInvoices.length === 0 ? ( - setModalOpen(true)} - /> - ) : ( -
- - - - Folio - Fecha - Cliente - Total - Estado - Pago - Acciones - - - - {safeInvoices.map((invoice) => ( - - {invoice.folio} - {formatDate(invoice.issueDate)} - -
{invoice.customerName}
- {invoice.customerTin && ( -
NIT: {invoice.customerTin}
- )} -
- {formatCurrency(Number(invoice.total))} - - - {invoice.status === 'draft' ? 'Borrador' : - invoice.status === 'issued' ? 'Emitida' : - invoice.status === 'paid' ? 'Pagada' : 'Cancelada'} - - - - - {invoice.paymentStatus === 'pending' ? 'Pendiente' : - invoice.paymentStatus === 'paid' ? 'Pagado' : 'Parcial'} - - - -
- -
-
-
- ))} -
-
-
- )} -
-
- - {modalOpen && ( - setModalOpen(false)} - onSuccess={handleSuccess} - customers={customers} - products={products} - businessSettings={localSettings} + {safeInvoices.length === 0 ? ( + Nueva Factura} + onAction={() => setModalOpen(true)} /> + ) : ( + + + Facturas + + + + + + Folio + Fecha + Cliente + Total + Estado + Pago + Acciones + + + + {safeInvoices.map((invoice) => ( + + {invoice.folio} + {formatDate(invoice.issueDate)} + +
{invoice.customerName}
+ {invoice.customerTin && ( +
NIT: {invoice.customerTin}
+ )} +
+ {formatCurrency(Number(invoice.total))} + + + {invoice.status === 'draft' ? 'Borrador' : + invoice.status === 'issued' ? 'Emitida' : + invoice.status === 'paid' ? 'Pagada' : 'Cancelada'} + + + + + {invoice.paymentStatus === 'pending' ? 'Pendiente' : + invoice.paymentStatus === 'paid' ? 'Pagado' : 'Parcial'} + + + +
+ + + {invoice.status !== 'cancelled' && ( + + )} + +
+
+
+ ))} +
+
+
+
)} - {settingsOpen && ( -
- setSettingsOpen(false)} - onSuccess={handleSettingsSuccess} - settings={businessSettings || { id: '', businessName: '', tin: null, address: null, phone: null, email: null, taxRate: 19, currency: 'COP' }} - /> -
- )} + { setModalOpen(false); setEditingInvoice(null) }} + onSuccess={handleSuccess} + customers={customers} + products={products} + businessSettings={localSettings} + invoice={editingInvoice} + /> + + setSettingsOpen(false)} + onSuccess={handleSettingsSuccess} + settings={localSettings || { id: '', businessName: '', tin: null, address: null, phone: null, email: null, taxRate: 19, currency: 'COP' }} + /> + + setDeletingId(null)} + onConfirm={() => deletingId && deleteMutation.mutate(deletingId)} + loading={deleteMutation.isPending} + title="¿Eliminar factura?" + description="Se devolverá el stock de los productos y la factura se eliminará permanentemente." + confirmText="Eliminar Factura" + variant="danger" + />
) } diff --git a/app/(protected)/facturas/invoice-form-modal.tsx b/app/(protected)/facturas/invoice-form-modal.tsx index a5fd5ff..d9ff576 100644 --- a/app/(protected)/facturas/invoice-form-modal.tsx +++ b/app/(protected)/facturas/invoice-form-modal.tsx @@ -10,7 +10,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Combobox } from '@/components/ui/combobox' import { formatCurrency } from '@/lib/utils' import { Trash2, Search } from 'lucide-react' -import { createInvoice } from '@/lib/actions/invoices' +import { createInvoice, updateInvoice } from '@/lib/actions/invoices' +import { useQuery } from '@tanstack/react-query' interface Product { id: string @@ -41,6 +42,7 @@ interface InvoiceFormModalProps { customers: Customer[] products: Product[] businessSettings: BusinessSettings | null + invoice?: any } interface InvoiceItem { @@ -51,16 +53,27 @@ interface InvoiceItem { unitPrice: number } -export function InvoiceFormModal({ isOpen, onClose, onSuccess, customers, products, businessSettings }: InvoiceFormModalProps) { +export function InvoiceFormModal({ isOpen, onClose, onSuccess, customers, products, businessSettings, invoice }: InvoiceFormModalProps) { const [loading, setLoading] = useState(false) const [selectedCustomerId, setSelectedCustomerId] = useState('') const [items, setItems] = useState([]) const [showProductDropdown, setShowProductDropdown] = useState(false) const [productSearch, setProductSearch] = useState('') const [itemToRemove, setItemToRemove] = useState(null) + const [draftQuantities, setDraftQuantities] = useState>({}) + const isEditing = !!invoice const taxRate = businessSettings?.taxRate || 16 + const { data: paymentMethods = [] } = useQuery({ + queryKey: ['payment-methods'], + queryFn: async () => { + const res = await fetch('/api/payment-methods') + if (!res.ok) return [] + return res.json() + }, + }) + const customerOptions = [ { value: 'publico', label: 'Público General' }, ...customers.map(c => ({ value: c.id, label: c.name })) @@ -68,6 +81,30 @@ export function InvoiceFormModal({ isOpen, onClose, onSuccess, customers, produc const selectedCustomer = customers.find(c => c.id === selectedCustomerId) + useEffect(() => { + if (isOpen && invoice) { + setSelectedCustomerId(invoice.customerId || 'publico') + setDraftQuantities({}) + if (invoice.invoiceItems?.length > 0) { + const mapped = invoice.invoiceItems.map((item: any) => ({ + id: item.id || crypto.randomUUID(), + productId: item.productId || '', + productName: item.productName, + quantity: Number(item.quantity), + unitPrice: Number(item.unitPrice) + })) + setItems(mapped) + const drafts: Record = {} + mapped.forEach((i: InvoiceItem) => { drafts[i.id] = i.quantity.toString() }) + setDraftQuantities(drafts) + } + } else if (isOpen && !invoice) { + setSelectedCustomerId('') + setItems([]) + setDraftQuantities({}) + } + }, [isOpen, invoice]) + const filteredProducts = products.filter(p => p.name.toLowerCase().includes(productSearch.toLowerCase()) && !items.find(i => i.productId === p.id) @@ -78,13 +115,15 @@ export function InvoiceFormModal({ isOpen, onClose, onSuccess, customers, produc const total = subtotal + taxAmount const addItem = (product: Product) => { + const id = crypto.randomUUID() setItems([...items, { - id: crypto.randomUUID(), + id, productId: product.id, productName: product.name, quantity: 1, unitPrice: product.salePrice }]) + setDraftQuantities(prev => ({ ...prev, [id]: '1' })) setProductSearch('') setShowProductDropdown(false) } @@ -136,18 +175,22 @@ export function InvoiceFormModal({ isOpen, onClose, onSuccess, customers, produc setLoading(true) try { - await createInvoice(formData) + if (isEditing && invoice) { + await updateInvoice(invoice.id, formData) + } else { + await createInvoice(formData) + } onSuccess() } catch (error) { console.error(error) - alert('Error al crear factura') + alert('Error al guardar factura') } finally { setLoading(false) } } return ( - +
@@ -169,10 +212,9 @@ export function InvoiceFormModal({ isOpen, onClose, onSuccess, customers, produc - Efectivo - Transferencia - Tarjeta - Otro + {paymentMethods.map((pm: any) => ( + {pm.name} + ))}
@@ -229,24 +271,34 @@ export function InvoiceFormModal({ isOpen, onClose, onSuccess, customers, produc
{items.map(item => (
-
{item.productName}
+
{item.productName}
updateItem(item.id, 'quantity', parseInt(e.target.value) || 1)} + value={draftQuantities[item.id] ?? item.quantity} + onChange={(e) => { + const raw = e.target.value + setDraftQuantities(prev => ({ ...prev, [item.id]: raw })) + if (raw !== '') { + const parsed = parseInt(raw) + if (!isNaN(parsed) && parsed >= 1) { + updateItem(item.id, 'quantity', parsed) + } + } + }} + onBlur={() => { + const draft = draftQuantities[item.id] + if (!draft || draft === '') { + setDraftQuantities(prev => ({ ...prev, [item.id]: '1' })) + updateItem(item.id, 'quantity', 1) + } + }} className="w-full px-2 py-1 border rounded text-center" />
-
- updateItem(item.id, 'unitPrice', parseFloat(e.target.value) || 0)} - className="w-full px-2 py-1 border rounded" - /> +
+ {formatCurrency(item.unitPrice)}
{formatCurrency(item.quantity * item.unitPrice)} @@ -296,7 +348,7 @@ export function InvoiceFormModal({ isOpen, onClose, onSuccess, customers, produc
diff --git a/app/(protected)/facturas/invoice-pdf-button.tsx b/app/(protected)/facturas/invoice-pdf-button.tsx index 195cc9e..2696833 100644 --- a/app/(protected)/facturas/invoice-pdf-button.tsx +++ b/app/(protected)/facturas/invoice-pdf-button.tsx @@ -60,13 +60,6 @@ export function InvoicePDFButton({ invoice, businessSettings }: Props) { setTimeout(() => { const bs = businessSettings || { businessName: 'Mi Negocio', tin: '', address: '', phone: '', email: '', taxRate: 19 } - const paymentMethodLabels: Record = { - efectivo: 'Efectivo', - transferencia: 'Transferencia', - tarjeta: 'Tarjeta de Débito/Crédito', - otro: 'Otro' - } - const html = ` @@ -143,7 +136,7 @@ export function InvoicePDFButton({ invoice, businessSettings }: Props) {
Total: ${formatCurrency(invoice.total)}
- ${invoice.paymentMethod ? `
Método de Pago: ${paymentMethodLabels[invoice.paymentMethod] || invoice.paymentMethod}
` : ''} + ${invoice.paymentMethod ? `
Método de Pago: ${invoice.paymentMethod}
` : ''} ${invoice.notes ? `
Notas: ${invoice.notes}
` : ''}