feature/tipo-operacion-invoice

This commit is contained in:
2026-04-24 07:09:28 -06:00
parent 6329e8e82f
commit 43caaeffd3
8 changed files with 121 additions and 87 deletions

View File

@@ -193,6 +193,18 @@
"title": "Invoices", "title": "Invoices",
"description": "Manage system invoices" "description": "Manage system invoices"
}, },
"titles": {
"base": "INVOICE CATALOG",
"import": "IMPORT",
"export": "EXPORT",
"import_temporal": "TEMPORARY IMPORT",
"import_definitive": "DEFINITIVE IMPORT",
"import_mexican": "MEXICAN PURCHASES",
"import_regime_change": "REGIME CHANGE AND REGULARIZATION",
"import_repair": "IMPORT REPAIR",
"export_definitive": "DEFINITIVE EXIT",
"export_repair": "REPAIR"
},
"filters": { "filters": {
"operation_label": "Operation Type", "operation_label": "Operation Type",
"operation_all_option": "Operation: All", "operation_all_option": "Operation: All",

View File

@@ -192,6 +192,18 @@
"title": "Facturas", "title": "Facturas",
"description": "Gestiona las facturas del sistema" "description": "Gestiona las facturas del sistema"
}, },
"titles": {
"base": "CATALOGO DE FACTURAS",
"import": "DE IMPORTACION",
"export": "DE EXPORTACION",
"import_temporal": "DE IMPORTACION TEMPORAL",
"import_definitive": "DE IMPORTACION DEFINITIVA",
"import_mexican": "DE COMPRAS MEXICANAS",
"import_regime_change": "DE CAMBIO DE REGIMEN Y REGULARIZACION",
"import_repair": "DE IMPO. DE REPARACION",
"export_definitive": "DE SALIDA DEFINITIVA",
"export_repair": "DE REPARACION"
},
"filters": { "filters": {
"operation_label": "Tipo de Operacion", "operation_label": "Tipo de Operacion",
"operation_all_option": "Operacion: Todas", "operation_all_option": "Operacion: Todas",

View File

@@ -82,7 +82,11 @@
let loadingTrigger = $state<HTMLDivElement>(); let loadingTrigger = $state<HTMLDivElement>();
// Intersection Observer para detectar cuando el usuario llega al final // Intersection Observer para detectar cuando el usuario llega al final
onMount(() => { $effect(() => {
const target = loadingTrigger;
const root = scrollContainer;
if (!target) return;
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
const [entry] = entries; const [entry] = entries;
@@ -91,14 +95,12 @@
} }
}, },
{ {
root: scrollContainer, root: root,
threshold: 0.1 threshold: 0.1
} }
); );
if (loadingTrigger) { observer.observe(target);
observer.observe(loadingTrigger);
}
return () => { return () => {
observer.disconnect(); observer.disconnect();

View File

@@ -152,56 +152,26 @@
<!-- Datos Principales en una fila compacta (reusable across tabs) --> <!-- Datos Principales en una fila compacta (reusable across tabs) -->
<div class="flex flex-wrap items-end gap-3 pb-3"> <div class="flex flex-wrap items-end gap-3 pb-3">
{#if !isSettings} {#if !isSettings}
<!-- Campos no manipulables pero visibles -->
<div class="min-w-[100px] flex-1 space-y-1"> <div class="min-w-[100px] flex-1 space-y-1">
<Label for="operation_type" class="text-xs" <Label class="text-xs text-muted-foreground"
>{m.invoice_edit_form_operation_type_label()}<span class="text-red-500">*</span></Label >{m.invoice_edit_form_operation_type_label()}</Label
> >
<Select.Root <p class="h-8 text-sm font-medium flex items-center">
type="single" {formData.operation_type
value={formData.operation_type || ''} ? (formData.operation_type === 'exp'
onValueChange={(v) => { ? m.invoice_edit_form_operation_type_export()
formData.operation_type = v; : m.invoice_edit_form_operation_type_import())
}} : '...'}
> </p>
<Select.Trigger id="operation_type" class="h-8 text-sm">
<span class="truncate">
{formData.operation_type
? (formData.operation_type === 'exp'
? m.invoice_edit_form_operation_type_export()
: m.invoice_edit_form_operation_type_import())
: '...'}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="exp">{m.invoice_edit_form_operation_type_export()}</Select.Item>
<Select.Item value="imp">{m.invoice_edit_form_operation_type_import()}</Select.Item>
</Select.Content>
</Select.Root>
</div> </div>
<div class="min-w-[100px] flex-1 space-y-1"> <div class="min-w-[100px] flex-1 space-y-1">
<Label for="operation_type" class="text-xs" <Label class="text-xs text-muted-foreground"
>{m.invoice_edit_form_invoice_type_label()} <span class="text-red-500">*</span></Label >{m.invoice_edit_form_invoice_type_label()}</Label
> >
<Select.Root <p class="h-8 text-sm font-medium flex items-center">
type="single" {formData.invoice_type ? `${formData.invoice_type}` : '...'}
value={formData.invoice_type || ''} </p>
onValueChange={(v) => {
formData.invoice_type = v ?? '';
}}
>
<Select.Trigger id="invoice_type" class="h-7 text-xs">
<span class="truncate">
{formData.invoice_type ? `${formData.invoice_type}` : '...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each filteredInvoiceTypes as type}
<Select.Item value={type.key}>
{type.key} - {type.description}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div> </div>
{/if} {/if}

View File

@@ -443,7 +443,7 @@ export function getSidebarData(): SidebarData {
items: [ items: [
{ {
title: m["sidebar.export_invoices.exportation"](), title: m["sidebar.export_invoices.exportation"](),
url: "/dashboard/invoices?operation_type=exp", url: "/dashboard/invoices?operation_type=exp&invoice_type=EXDEF",
}, },
{ {
title: m["sidebar.export_invoices.repair"](), title: m["sidebar.export_invoices.repair"](),

View File

@@ -6,14 +6,22 @@ import {
} from '$lib/server/api'; } from '$lib/server/api';
export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => { export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
const invoiceType = url.searchParams.get('invoice_type');
const operationType = url.searchParams.get('operation_type');
if (!operationType) {
throw redirect(302, '/dashboard');
}
// Esperar a que el layout padre valide/refresque el token // Esperar a que el layout padre valide/refresque el token
const parentData = await parent(); const parentData = await parent();
// Verificar autenticación // Verificar autenticación
const { accessToken } = getAuthTokens(cookies); const tokens = getAuthTokens(cookies);
const accessToken = tokens.accessToken;
if (!accessToken) { if (!accessToken) {
throw redirect(302, '/login'); throw redirect(302, '/auth/login');
} }
try { try {
@@ -43,7 +51,6 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
} }
// Construir parámetros de consulta // Construir parámetros de consulta
// Los query parameters invoice_type y operation_type se pasan al endpoint
const params = new URLSearchParams({ const params = new URLSearchParams({
company_id: companyId.toString(), company_id: companyId.toString(),
page: '1', page: '1',
@@ -51,8 +58,6 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
}); });
// Agregar filtros opcionales desde query parameters // Agregar filtros opcionales desde query parameters
const invoiceType = url.searchParams.get('invoice_type');
const operationType = url.searchParams.get('operation_type');
const invoiceNumber = url.searchParams.get('invoice_number'); const invoiceNumber = url.searchParams.get('invoice_number');
const projectNumber = url.searchParams.get('project_number'); const projectNumber = url.searchParams.get('project_number');
const year = url.searchParams.get('year'); const year = url.searchParams.get('year');
@@ -70,14 +75,14 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
{}, {},
cookies, cookies,
fetch, fetch,
'/login' '/auth/login'
), ),
authenticatedFetch( authenticatedFetch(
'v1/public/reference_data/invoice-types?page=1&page_size=100', 'v1/public/reference_data/invoice-types?page=1&page_size=100',
{}, {},
cookies, cookies,
fetch, fetch,
'/login' '/auth/login'
) )
]); ]);
@@ -90,9 +95,9 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
error: 'Error al cargar facturas', error: 'Error al cargar facturas',
companies: parentData.companies || [], companies: parentData.companies || [],
currentCompanyId: companyId, currentCompanyId: companyId,
// Preservar los filtros aplicados
filters: { filters: {
invoice_type: invoiceType, invoice_type: invoiceType,
operation_type: operationType
} }
}; };
} }
@@ -108,7 +113,6 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
companies: parentData.companies || [], companies: parentData.companies || [],
currentCompanyId: companyId, currentCompanyId: companyId,
invoiceTypes: invoiceTypesData.items || [], invoiceTypes: invoiceTypesData.items || [],
// Preservar los filtros aplicados
filters: { filters: {
invoice_type: invoiceType, invoice_type: invoiceType,
operation_type: operationType, operation_type: operationType,
@@ -119,6 +123,9 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
}; };
} catch (error) { } catch (error) {
console.error('Error loading invoices:', error); console.error('Error loading invoices:', error);
if (error instanceof Response && error.status >= 300 && error.status < 400) {
throw error; // Propagate redirects
}
return { return {
items: [], items: [],
total: 0, total: 0,

View File

@@ -274,6 +274,29 @@
} }
}); });
// Título dinámico según el tipo de operación y factura
const viewTitle = $derived.by(() => {
const op = filters.operation_type;
const type = filters.invoice_type;
const base = m.invoice_list_titles_base();
if (op === 'imp') {
if (type === 'TEM') return `${base} ${m.invoice_list_titles_import_temporal()}`;
if (type === 'DEF') return `${base} ${m.invoice_list_titles_import_definitive()}`;
if (type === 'MEX') return `${base} ${m.invoice_list_titles_import_mexican()}`;
if (type === 'CR') return `${base} ${m.invoice_list_titles_import_regime_change()}`;
if (type === 'REP') return `${base} ${m.invoice_list_titles_import_repair()}`;
return `${base} ${m.invoice_list_titles_import()}`;
} else if (op === 'exp') {
if (type === 'EXDEF') return `${base} ${m.invoice_list_titles_export_definitive()}`;
if (type === 'REPAR') return `${base} ${m.invoice_list_titles_export_repair()}`;
return `${base} ${m.invoice_list_titles_export()}`;
}
return m.invoice_list_header_title();
});
async function loadMore() { async function loadMore() {
if (loading || !hasMore) return; if (loading || !hasMore) return;
@@ -1360,35 +1383,38 @@
</a> </a>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h1 class="text-2xl font-bold tracking-tight">{m.invoice_list_header_title()}</h1> <h1 class="text-2xl font-bold tracking-tight">{viewTitle}</h1>
<p class="text-muted-foreground">{m.invoice_list_header_description()}</p> <p class="text-muted-foreground">{m.invoice_list_header_description()}</p>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<select <!-- Filtros ocultos por requerimiento -->
id="filter-operation-type" <div class="hidden">
bind:value={filters.operation_type} <select
class="flex h-9 w-[180px] rounded-md border border-input bg-card px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" id="filter-operation-type"
title={m.invoice_list_filters_operation_label()} bind:value={filters.operation_type}
> class="flex h-9 w-[180px] rounded-md border border-input bg-card px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
{#each operationTypeOptions as option} title={m.invoice_list_filters_operation_label()}
<option value={option.value}> >
{option.value === '' ? m.invoice_list_filters_operation_all_option() : option.label} {#each operationTypeOptions as option}
</option> <option value={option.value}>
{/each} {option.value === '' ? m.invoice_list_filters_operation_all_option() : option.label}
</select> </option>
{/each}
<select </select>
id="filter-invoice-type"
bind:value={filters.invoice_type} <select
class="flex h-9 w-[220px] rounded-md border border-input bg-card px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" id="filter-invoice-type"
title={m.invoice_list_filters_invoice_type_label()} bind:value={filters.invoice_type}
> class="flex h-9 w-[220px] rounded-md border border-input bg-card px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
{#each invoiceTypeOptions() as option} title={m.invoice_list_filters_invoice_type_label()}
<option value={option.value}> >
{option.value === '' ? m.invoice_list_filters_invoice_type_all_option() : option.label} {#each invoiceTypeOptions() as option}
</option> <option value={option.value}>
{/each} {option.value === '' ? m.invoice_list_filters_invoice_type_all_option() : option.label}
</select> </option>
{/each}
</select>
</div>
<Button variant="outline" class="h-9" onclick={() => goto('/dashboard/invoices/settings')}> <Button variant="outline" class="h-9" onclick={() => goto('/dashboard/invoices/settings')}>
<Settings class="mr-2" size={16} /> <Settings class="mr-2" size={16} />

View File

@@ -26,6 +26,11 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
parsedOperationType = operationTypeParam; parsedOperationType = operationTypeParam;
} }
// Para creación ('new'), es obligatorio tener operation_type e invoice_type
if (params.id === 'new' && (!parsedOperationType || !invoiceTypeParam)) {
throw redirect(302, '/dashboard');
}
try { try {
let invoiceData: any = null; let invoiceData: any = null;
let catalogsData: any = {}; let catalogsData: any = {};