feature/tabs for invoice

This commit is contained in:
hreyes
2026-02-04 09:17:09 -06:00
parent 0dd9f96f5b
commit 2c5d94722b
7 changed files with 797 additions and 727 deletions

View File

@@ -18,6 +18,10 @@ export const GLOBAL_NAV = {
'r': '/dashboard/reference_data/pedimento_regimens',
'e': '/dashboard/reference_data/customs_sections#',
'd': '/dashboard/reference_data/customs_sections',
// Invoices
'i': '/dashboard/invoices',
// Common Actions
'b': 'SEARCH_FOCUS', // Special case for generic focus
'h': '/', // Home

View File

@@ -6,12 +6,9 @@ export const obtenerAtajosFormularioMercancia = (acciones: {
manejarRegresar: () => void;
}): ShortcutDef[] => [
// Pestañas
{ key: 'Alt+G', description: 'Tab General', action: () => acciones.cambiarPestana('general') },
{ key: 'Alt+I', description: 'Tab Imágenes', action: () => acciones.cambiarPestana('cont1') },
{ key: 'Alt+E', description: 'Tab Exportación', action: () => acciones.cambiarPestana('cont2') },
{ key: 'Alt+O', description: 'Tab Opciones', action: () => acciones.cambiarPestana('opciones') },
{ key: 'Alt+S', description: 'Tab Sanitarias', action: () => acciones.cambiarPestana('opcionales2') },
{ key: 'Alt+X', description: 'Tab Otros', action: () => acciones.cambiarPestana('otros') },
{ key: 'Alt+Digit1', description: 'Tab General', action: () => acciones.cambiarPestana('general') },
{ key: 'Alt+Digit2', description: 'Tab Imágenes', action: () => acciones.cambiarPestana('cont1') },
{ key: 'Alt+Digit3', description: 'Tab Exportación', action: () => acciones.cambiarPestana('cont2') },
// Acciones Globales
{ key: 'Ctrl+S', description: 'Guardar cambios', action: acciones.manejarGuardar },

View File

@@ -1,73 +0,0 @@
import type { ShortcutDef } from '../../stores/shortcut-store';
export const obtenerAtajosListaPartes = (acciones: {
manejarNuevo: () => void;
manejarActualizar: () => void;
manejarBorrar: () => void;
}): ShortcutDef[] => [
{
key: 'Alt+Shift+N',
description: 'Nueva Parte',
action: acciones.manejarNuevo
},
{
key: 'Alt+Shift+R',
description: 'Actualizar Lista',
action: acciones.manejarActualizar
},
{
key: 'Alt+Shift+D',
description: 'Borrar Seleccionado',
action: acciones.manejarBorrar
}
];
export const obtenerAtajosListaClasesActivoFijo = (acciones: {
manejarNuevo: () => void;
manejarActualizar: () => void;
manejarBorrar: () => void;
}): ShortcutDef[] => [
{
key: 'Alt+Shift+N',
description: 'Nueva Clase',
action: acciones.manejarNuevo
},
{
key: 'Alt+Shift+R',
description: 'Actualizar Lista',
action: acciones.manejarActualizar
},
{
key: 'Alt+Shift+D',
description: 'Borrar Seleccionado',
action: acciones.manejarBorrar
}
];
export const obtenerAtajosFormularioParte = (acciones: {
cambiarPestana: (pestana: string) => void;
manejarGuardar: () => void;
manejarCancelar: () => void;
esActivoFijo: boolean;
}): ShortcutDef[] => {
const common = [
{ key: 'Alt+Digit1', description: 'Pest. General', action: () => acciones.cambiarPestana('general') },
{ key: 'Ctrl+S', description: 'Guardar cambios', action: acciones.manejarGuardar },
{ key: 'Escape', description: 'Cancelar / Regresar', action: acciones.manejarCancelar }
];
if (acciones.esActivoFijo) {
return [
...common,
{ key: 'Alt+Digit2', description: 'Pest. Continuación 1', action: () => acciones.cambiarPestana('cont1') },
{ key: 'Alt+Digit3', description: 'Pest. Continuación 2', action: () => acciones.cambiarPestana('cont2') }
];
}
return [
...common,
{ key: 'Alt+Digit2', description: 'Pest. Opciones', action: () => acciones.cambiarPestana('opciones') },
{ key: 'Alt+Digit3', description: 'Pest. Opcionales 2', action: () => acciones.cambiarPestana('opcionales2') },
{ key: 'Alt+Digit4', description: 'Pest. Otros', action: () => acciones.cambiarPestana('otros') }
];
};

View File

@@ -0,0 +1,15 @@
import type { ShortcutDef } from '$lib/stores/shortcut-store';
export const obtenerAtajosEdicionFactura = (acciones: {
cambiarPestana: (pestana: string) => void;
manejarGuardar: () => void;
manejarRegresar: () => void;
}): ShortcutDef[] => [
{ key: 'Alt+Q', description: 'Tab General', action: () => acciones.cambiarPestana('general') },
{ key: 'Alt+O', description: 'Tab Observ.', action: () => acciones.cambiarPestana('observations') },
{ key: 'Alt+A', description: 'Tab Partidas (Artículos)', action: () => acciones.cambiarPestana('items') },
{ key: 'Alt+X', description: 'Tab Otros', action: () => acciones.cambiarPestana('others') },
{ key: 'Alt+K', description: 'Tab Cont.', action: () => acciones.cambiarPestana('continuation') },
{ key: 'Ctrl+S', description: 'Guardar cambios', action: acciones.manejarGuardar },
{ key: 'Escape', description: 'Regresar / Cancelar', action: acciones.manejarRegresar }
];

View File

@@ -0,0 +1,48 @@
import type { ShortcutDef } from '$lib/stores/shortcut-store';
export const obtenerAtajosListaFacturas = (acciones: {
manejarCrear: () => void;
manejarActualizar: () => void;
manejarEditar: () => void;
manejarDescargarPdf: () => void;
manejarDescargarConsolidado: () => void;
// Filtros de navegación
irTemporal: () => void;
irDefinitiva: () => void;
irNacional: () => void;
irCambioRegimen: () => void;
irExportacion: () => void;
irReparacion: () => void;
}): ShortcutDef[] => [
{ key: 'Alt+V', description: 'Ver Temporales', action: acciones.irTemporal },
{ key: 'Alt+F', description: 'Ver Definitivas', action: acciones.irDefinitiva },
{ key: 'Alt+N', description: 'Ver Nacionales', action: acciones.irNacional },
{ key: 'Alt+Z', description: 'Ver Cambio Régimen', action: acciones.irCambioRegimen },
{ key: 'Alt+L', description: 'Ver Exportaciones', action: acciones.irExportacion },
{ key: 'Alt+Y', description: 'Ver Reparaciones', action: acciones.irReparacion },
{
key: 'Alt+Shift+N',
description: 'Nueva Factura',
action: acciones.manejarCrear
},
{
key: 'Alt+Shift+R',
description: 'Actualizar Lista',
action: acciones.manejarActualizar
},
{
key: 'Alt+Shift+E',
description: 'Editar Seleccionada',
action: acciones.manejarEditar
},
{
key: 'Alt+Shift+P',
description: 'Descargar PDF',
action: acciones.manejarDescargarPdf
},
{
key: 'Alt+Shift+C',
description: 'Descargar Consolidado',
action: acciones.manejarDescargarConsolidado
}
];

File diff suppressed because it is too large Load Diff

View File

@@ -2,29 +2,29 @@
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/stores';
import * as Tabs from '$lib/components/ui/tabs';
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
import { Separator } from '$lib/components/ui/separator';
import { toast } from 'svelte-sonner';
import {
ArrowLeft,
FileText,
DollarSign,
Truck,
Package,
import {
ArrowLeft,
FileText,
DollarSign,
Truck,
Package,
Eye,
LoaderCircle,
Save
} from 'lucide-svelte';
LoaderCircle,
Save
} from 'lucide-svelte';
// Importar los componentes de cada pestaña
import GeneralTabForm from '$lib/components/dashboard/invoices/edit/general-tab-form.svelte';
import ObservationsTabForm from '$lib/components/dashboard/invoices/edit/observations-tab-form.svelte';
import ItemsTabForm from '$lib/components/dashboard/invoices/edit/items/items-tab-form.svelte';
import OthersTabForm from '$lib/components/dashboard/invoices/edit/others-tab-form.svelte';
import InvoiceTopFields from '$lib/components/dashboard/invoices/edit/invoice-top-fields.svelte';
import InvoiceTopFields from '$lib/components/dashboard/invoices/edit/invoice-top-fields.svelte';
import ContinuationTabForm from '$lib/components/dashboard/invoices/edit/continuation-tab-form.svelte';
// Importar la API de facturas
@@ -35,11 +35,13 @@
import { getExchangeRateByDate } from '$lib/api/dashboard/a76/exchange-rate';
import { getExchangeRates } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
import ExchangeRateDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosEdicionFactura } from '$lib/config/shortcuts/invoice-edit-shortcuts';
// Cargar companyStore solo en el cliente - no usamos sidebar en esta página
let companyStore: any = $state(undefined);
let mounted = $state(false);
onMount(async () => {
try {
const companyStoreModule = await import('$lib/stores/company.svelte');
@@ -85,7 +87,7 @@
let activeTab = $state('general');
let saving = $state(false);
// ID de la factura
let invoiceId = $state<number | null>(data.invoiceId ?? null);
@@ -103,10 +105,12 @@
let othersExists = $state(false);
let continuationExists = $state(false);
let calculatedExchangeRate = $state<number | null>(data.invoice?.financials?.exchange_rate ?? null);
let calculatedExchangeRate = $state<number | null>(
data.invoice?.financials?.exchange_rate ?? null
);
let showExchangeRateDialog = $state(false);
let missingExchangeRateDate = $state("");
let missingExchangeRateDate = $state('');
async function checkExchangeRate(date: string): Promise<boolean> {
if (!date || !companyStore?.activeCompany?.id) return true;
@@ -120,10 +124,10 @@
date: date,
page_size: 1
});
const actualResponse = response as any;
const items = actualResponse.data?.items || [];
if (items.length === 0) {
console.log('No exchange rate found for', date);
missingExchangeRateDate = date;
@@ -139,7 +143,7 @@
// Por seguridad, abrimos modal.
missingExchangeRateDate = date;
showExchangeRateDialog = true;
return false;
return false;
}
}
@@ -147,14 +151,14 @@
$effect(() => {
if (mounted && companyStore?.activeCompany?.id && InvoiceTopFieldsFormData?.invoice_date) {
getExchangeRateByDate(InvoiceTopFieldsFormData.invoice_date, companyStore.activeCompany.id)
.then(rate => {
.then((rate) => {
if (rate) {
calculatedExchangeRate = rate.value;
calculatedExchangeRate = rate.value;
} else {
calculatedExchangeRate = 0;
}
})
.catch(err => console.error('Error auto-updating exchange rate:', err));
.catch((err) => console.error('Error auto-updating exchange rate:', err));
}
});
@@ -173,7 +177,7 @@
try {
// Validar ID si es edición
if (!data.isCreate && !invoiceId) {
toast.error("Error interrrno: No se encuentra el ID de la factura para actualizar.");
toast.error('Error interrrno: No se encuentra el ID de la factura para actualizar.');
saving = false;
return;
}
@@ -199,7 +203,7 @@
othersFormData,
continuationFormData
}
});
});
if (!result.success) {
// Crear un error con validationErrors si existen
@@ -212,8 +216,8 @@
toast.success('Todos los cambios se guardaron correctamente');
} catch (e) {
console.error('Error saving all:', e);
console.error('Error saving all:', e);
if (e instanceof Error && e.message.includes('401')) {
toast.error('Sesión expirada. Recargando página...');
setTimeout(() => {
@@ -221,26 +225,34 @@
}, 1500);
} else {
const errorMessage = e instanceof Error ? e.message : 'Error al guardar los cambios';
// Intercept exchange rate error
if (errorMessage.includes('No existe un Tipo de Cambio registrado') || errorMessage.includes('financials.exchange_rate')) {
console.log("Interceptor: Exchange rate missing error caught (Invoice Page).");
if (
errorMessage.includes('No existe un Tipo de Cambio registrado') ||
errorMessage.includes('financials.exchange_rate')
) {
console.log('Interceptor: Exchange rate missing error caught (Invoice Page).');
const dateMatch = errorMessage.match(/(\d{4}-\d{2}-\d{2})/);
const missingDate = dateMatch ? dateMatch[0] : (InvoiceTopFieldsFormData?.invoice_date || "");
const missingDate = dateMatch
? dateMatch[0]
: InvoiceTopFieldsFormData?.invoice_date || '';
missingExchangeRateDate = missingDate;
showExchangeRateDialog = true;
return;
}
// Si hay errores de validación, mostrarlos en detalle
if (e && typeof e === 'object' && 'validationErrors' in e && Array.isArray((e as any).validationErrors)) {
const validationErrors = (e as any).validationErrors;
const errorList = validationErrors.map((err: any) =>
`• ${err.message}`
).join('\n');
if (
e &&
typeof e === 'object' &&
'validationErrors' in e &&
Array.isArray((e as any).validationErrors)
) {
const validationErrors = (e as any).validationErrors;
const errorList = validationErrors.map((err: any) => `• ${err.message}`).join('\n');
toast.error(errorMessage, {
description: errorList
});
@@ -254,6 +266,15 @@
saving = false;
}
}
useShortcuts(
'Invoice Edit',
obtenerAtajosEdicionFactura({
cambiarPestana: (pestana) => (activeTab = pestana),
manejarGuardar: handleSaveAll,
manejarRegresar: handleBack
})
);
</script>
<div class="space-y-3">
@@ -267,7 +288,9 @@
<h1 class="text-3xl font-bold tracking-tight">
{#if data.isCreate}
{@const invoiceType = generalFormData?.invoice_type || data.filters?.invoice_type}
{@const invoiceTypeInfo = invoiceType ? data.invoiceTypes?.find(t => t.key === invoiceType) : null}
{@const invoiceTypeInfo = invoiceType
? data.invoiceTypes?.find((t) => t.key === invoiceType)
: null}
Nueva Factura
{#if invoiceTypeInfo}
<span class="text-muted-foreground font-normal text-2xl">
@@ -301,18 +324,18 @@
<!-- Contenido de las tabs con padding inferior para el footer flotante -->
<div class="pb-48">
<Tabs.Root bind:value={activeTab}>
<InvoiceTopFields
invoice={data.invoice}
bind:formData={InvoiceTopFieldsFormData}
invoiceTypes={data.invoiceTypes || []}
pedimentos={data.pedimentos || []}
defaultOperationType={data.filters?.operation_type ?? undefined}
defaultInvoiceType={data.filters?.invoice_type ?? undefined}
<InvoiceTopFields
invoice={data.invoice}
bind:formData={InvoiceTopFieldsFormData}
invoiceTypes={data.invoiceTypes || []}
pedimentos={data.pedimentos || []}
defaultOperationType={data.filters?.operation_type ?? undefined}
defaultInvoiceType={data.filters?.invoice_type ?? undefined}
/>
<Tabs.Content value="general">
<GeneralTabForm
invoice={data.invoice}
<Tabs.Content value="general">
<GeneralTabForm
invoice={data.invoice}
bind:formData={generalFormData}
invoiceTypes={data.invoiceTypes || []}
customsBrokers={data.customsBrokers || []}
@@ -323,45 +346,46 @@
transporters={data.transporters || []}
vehicles={data.vehicles || []}
drivers={data.drivers || []}
trailers={data.trailers || []}
trailers={data.trailers || []}
customsSections={data.customsSections || []}
codePedimentoRegimens={data.codePedimentoRegimens || []}
codePedimentoRegimens={data.codePedimentoRegimens || []}
defaultOperationType={data.filters?.operation_type ?? undefined}
defaultInvoiceType={data.filters?.invoice_type ?? undefined}
operationType={
InvoiceTopFieldsFormData?.operation_type === 'exp' ? 1
: InvoiceTopFieldsFormData?.operation_type === 'imp' ? 2
: data.invoice?.operation_type === 'exp' ? 1
: data.invoice?.operation_type === 'imp' ? 2
: undefined
}
operationType={InvoiceTopFieldsFormData?.operation_type === 'exp'
? 1
: InvoiceTopFieldsFormData?.operation_type === 'imp'
? 2
: data.invoice?.operation_type === 'exp'
? 1
: data.invoice?.operation_type === 'imp'
? 2
: undefined}
exchangeRate={calculatedExchangeRate}
/>
</Tabs.Content>
<Tabs.Content value="observations">
<ObservationsTabForm
invoice={data.invoice}
<ObservationsTabForm
invoice={data.invoice}
bind:formData={observationFormData}
bind:exists={observationExists}
seals={data.seals || []}
incoterms={data.incoterms || []}
enclosure={data.enclosure || []}
/>
</Tabs.Content>
<Tabs.Content value="items">
<ItemsTabForm
invoice={data.invoice}
<ItemsTabForm
invoice={data.invoice}
bind:formData={itemsFormData}
bind:exists={itemsExists}
/>
</Tabs.Content>
<Tabs.Content value="others">
<OthersTabForm
invoice={data.invoice}
<OthersTabForm
invoice={data.invoice}
bind:formData={othersFormData}
bind:exists={othersExists}
transportModes={data.transportModes || []}
@@ -369,31 +393,33 @@
</Tabs.Content>
<Tabs.Content value="continuation">
<ContinuationTabForm
invoice={data.invoice}
bind:formData={continuationFormData}
bind:exists={continuationExists}
/>
<ContinuationTabForm
invoice={data.invoice}
bind:formData={continuationFormData}
bind:exists={continuationExists}
/>
</Tabs.Content>
</Tabs.Root>
</div>
</div>
<!-- Footer fijo en la parte inferior -->
<ExchangeRateDialog
bind:open={showExchangeRateDialog}
<ExchangeRateDialog
bind:open={showExchangeRateDialog}
initialDate={missingExchangeRateDate}
onSuccess={() => {
// Actualizar el tipo de cambio mostrado
if (InvoiceTopFieldsFormData.invoice_date && companyStore?.activeCompany?.id) {
getExchangeRateByDate(InvoiceTopFieldsFormData.invoice_date, companyStore.activeCompany.id)
.then(rate => {
getExchangeRateByDate(
InvoiceTopFieldsFormData.invoice_date,
companyStore.activeCompany.id
).then((rate) => {
if (rate) calculatedExchangeRate = rate.value;
});
}
}}
/>
<div
<div
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]"
>
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
@@ -413,7 +439,7 @@
<DollarSign size={16} class="mr-2" />
Partidas
</Tabs.Trigger>
<Tabs.Trigger value="others" disabled={false} class="whitespace-nowrap">
<Tabs.Trigger value="others" disabled={false} class="whitespace-nowrap">
<Truck size={16} class="mr-2" />
Otros
</Tabs.Trigger>