Merge branch 'development' of https://git.aduanasoft.com/ADUANASOFT/anexo76 into fix/modulos-ingles

This commit is contained in:
2026-06-01 16:17:09 -05:00
13 changed files with 517 additions and 111 deletions

View File

@@ -598,8 +598,9 @@ export const invoicesApi = {
);
},
copyInvoice: (invoiceId: number, companyId: number) => {
copyInvoice: (invoiceId: number, companyId: number, newInvoiceNumber?: string) => {
const params = new URLSearchParams({ company_id: companyId.toString() });
if (newInvoiceNumber) params.set('new_invoice_number', newInvoiceNumber);
return api.post<Invoice>(`/v1/a76/invoices/${invoiceId}/copy?${params.toString()}`, {});
},
copyInvoiceHeader: (invoiceId: number, companyId: number) => {

View File

@@ -0,0 +1,148 @@
import type { InvoiceType } from '$lib/api/dashboard/reference_data/invoice_types';
export type InvoiceListFilters = {
operation_type?: string | null;
invoice_type?: string | null;
};
/** Claves de importación expuestas en el menú lateral. */
export const IMPORT_INVOICE_TYPE_KEYS = ['TEM', 'DEF', 'MEX', 'CR', 'REP'] as const;
/** Claves de exportación relevantes para configuración (Exportación y Reparación). */
export const EXPORT_INVOICE_TYPE_KEYS = ['EXDEF', 'REPAR'] as const;
/**
* Filtra tipos de factura por operación (imp/exp), incluyendo tipos marcados como `both`.
*/
export function filterInvoiceTypesByOperation(
invoiceTypes: InvoiceType[],
operationType: string
): InvoiceType[] {
if (!operationType) return [];
return invoiceTypes.filter(
(type) => !type.operation || type.operation === 'both' || type.operation === operationType
);
}
/**
* Tipos visibles en parámetros.
*
* - Para importación: se limitan a las claves expuestas en el menú lateral (TEM, DEF, MEX, CR, REP).
* - Para exportación: se permiten todos los tipos cuyo `operation` sea `exp` (catálogo completo).
*/
export function getInvoiceTypesForSettings(
invoiceTypes: InvoiceType[],
operationType: string
): InvoiceType[] {
const filtered = filterInvoiceTypesByOperation(invoiceTypes, operationType);
if (!operationType) return filtered;
// Importación: solo los tipos configurables desde el menú lateral
if (operationType === 'imp') {
const byKey = new Map(filtered.map((type) => [type.key, type]));
return IMPORT_INVOICE_TYPE_KEYS.map((key) => byKey.get(key)).filter(
(type): type is InvoiceType => !!type
);
}
if (operationType === 'exp') {
const byKey = new Map(filtered.map((type) => [type.key, type]));
const fromBackend = EXPORT_INVOICE_TYPE_KEYS.map((key) => byKey.get(key)).filter(
(type): type is InvoiceType => !!type
);
// Si el backend devolvió ambos tipos, usar sus descripciones reales.
if (fromBackend.length > 0) {
return fromBackend;
}
// Fallback mínimo para no dejar el combo vacío
const fallback: InvoiceType[] = [
{ key: 'EXDEF', description: 'EXPORTACIÓN DEFINITIVA', operation: 'exp' },
{ key: 'REPAR', description: 'REPARACIÓN', operation: 'exp' }
];
return fallback;
}
// Otra operación válida: devolvemos lo filtrado
return filtered;
}
/**
* Tipo de factura por defecto cuando la URL no lo incluye.
*/
export function getDefaultInvoiceTypeForOperation(
operationType: string,
invoiceTypes: InvoiceType[]
): string {
const options = getInvoiceTypesForSettings(invoiceTypes, operationType);
if (options.length === 0) return '';
if (operationType === 'imp') {
return options.find((type) => type.key === 'TEM')?.key ?? options[0].key;
}
if (operationType === 'exp') {
return (
options.find((type) => type.key === 'EXDEF')?.key ??
options.find((type) => type.key === 'REPAR')?.key ??
options[0].key
);
}
return options[0].key;
}
/**
* Construye la ruta del listado de facturas preservando query params y evitando
* navegar sin `operation_type` (el SSR del listado redirige al dashboard).
*/
export function buildInvoicesListPath(
searchParams: URLSearchParams,
fallbacks?: InvoiceListFilters
): string {
const params = new URLSearchParams(searchParams);
if (!params.has('operation_type') && fallbacks?.operation_type) {
params.set('operation_type', fallbacks.operation_type);
}
if (!params.has('invoice_type') && fallbacks?.invoice_type) {
params.set('invoice_type', fallbacks.invoice_type);
}
const operationType = params.get('operation_type');
if (!operationType) {
return '/dashboard';
}
const qs = params.toString();
return qs ? `/dashboard/invoices?${qs}` : `/dashboard/invoices?operation_type=${operationType}`;
}
/**
* Construye la ruta de parámetros de factura conservando el contexto del listado.
*/
export function buildInvoicesSettingsPath(searchParams: URLSearchParams): string {
const params = new URLSearchParams(searchParams);
const qs = params.toString();
return qs ? `/dashboard/invoices/settings?${qs}` : '/dashboard/invoices/settings';
}
/**
* Resuelve operación y tipo inicial para la pantalla de parámetros.
*/
export function resolveInvoiceSettingsContext(
searchParams: URLSearchParams,
invoiceTypes: InvoiceType[]
): { operationType: string; invoiceType: string } {
const operationType = searchParams.get('operation_type') ?? '';
let invoiceType = searchParams.get('invoice_type') ?? '';
if (operationType && !invoiceType) {
invoiceType = getDefaultInvoiceTypeForOperation(operationType, invoiceTypes);
}
return { operationType, invoiceType };
}

View File

@@ -38,14 +38,14 @@
{#snippet child({ props })}
<button
{...props}
class="inline-flex size-8 shrink-0 items-center justify-center rounded-md
class="inline-flex size-10 shrink-0 select-none items-center justify-center rounded-md
text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground
transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring
data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
title="Aplicaciones"
aria-label="Abrir selector de aplicaciones"
>
<LayoutGridIcon class="size-4" />
<LayoutGridIcon class="size-5" />
</button>
{/snippet}
</DropdownMenu.Trigger>

View File

@@ -84,6 +84,21 @@
inventory: BarChart3Icon,
};
const SYSTEM_COLORS: Record<SystemType, { border: string; bg: string; iconBg: string; iconText: string }> = {
fixed_asset: {
border: 'border-amber-500/40',
bg: 'bg-amber-500/5 dark:bg-amber-500/10',
iconBg: 'bg-amber-500/10',
iconText: 'text-amber-600 dark:text-amber-400',
},
inventory: {
border: 'border-emerald-500/40',
bg: 'bg-emerald-500/5 dark:bg-emerald-500/10',
iconBg: 'bg-emerald-500/10',
iconText: 'text-emerald-600 dark:text-emerald-400',
},
};
onMount(() => {
const expandForKeyboardNav = () => {
sidebar.setOpen(true);
@@ -104,15 +119,15 @@
{@const activeLabel = systemStore.activeLabel}
{@const activeSystem = systemStore.activeSystem}
{@const Icon = activeSystem ? ACTIVE_ICONS[activeSystem] : null}
{@const colors = activeSystem ? SYSTEM_COLORS[activeSystem] : null}
<div
class="flex items-center gap-2 rounded-lg border border-primary/40 bg-primary/5 px-3 py-2 text-xs shadow-sm
dark:bg-primary/10"
class="flex items-center gap-2 rounded-lg border {colors?.border} {colors?.bg} px-3 py-2 text-xs shadow-sm"
aria-label={`Aplicación activa: ${activeLabel.name}`}
title={`Aplicación activa: ${activeLabel.name}`}
>
{#if Icon}
<div class="flex size-8 items-center justify-center rounded-md bg-primary/10 text-primary">
<div class="flex size-8 items-center justify-center rounded-md {colors?.iconBg} {colors?.iconText}">
<Icon class="size-4" />
</div>
{/if}