feature/sitar-fractions-upgrade
This commit is contained in:
@@ -39,16 +39,81 @@ export interface SitarALADI {
|
||||
}
|
||||
|
||||
export async function getSitarTLCS(filters: { fraccion: string; nico?: string }): Promise<ApiResponse<SitarTLCS[]>> {
|
||||
const queryParams = new URLSearchParams(filters);
|
||||
return await api.get(`/v1/sitar/tlcs/?${queryParams.toString()}`);
|
||||
const queryParams = new URLSearchParams(
|
||||
Object.entries(filters).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||||
acc[key] = String(value);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
)
|
||||
);
|
||||
return await api.get(`/v1/sitar/tlcs/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function getSitarPROSEC(filters: { fraccion: string; nico?: string }): Promise<ApiResponse<SitarPROSEC[]>> {
|
||||
const queryParams = new URLSearchParams(filters);
|
||||
return await api.get(`/v1/sitar/prosec/?${queryParams.toString()}`);
|
||||
const queryParams = new URLSearchParams(
|
||||
Object.entries(filters).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||||
acc[key] = String(value);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
)
|
||||
);
|
||||
return await api.get(`/v1/sitar/prosec/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function getSitarALADI(filters: { fraccion: string; nico?: string }): Promise<ApiResponse<SitarALADI[]>> {
|
||||
const queryParams = new URLSearchParams(filters);
|
||||
return await api.get(`/v1/sitar/aladi2/?${queryParams.toString()}`);
|
||||
const queryParams = new URLSearchParams(
|
||||
Object.entries(filters).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||||
acc[key] = String(value);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
)
|
||||
);
|
||||
return await api.get(`/v1/sitar/aladi2/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export type SitarGenericRecord = Record<string, string | number | boolean | null>;
|
||||
|
||||
export type SitarDatasetEndpoint =
|
||||
| 'reit'
|
||||
| 'requisito-previo'
|
||||
| 'informacion-general'
|
||||
| 'regulaciones'
|
||||
| 'fundamentos-tlc'
|
||||
| 'cuotas2'
|
||||
| 'cupos'
|
||||
| 'noms'
|
||||
| 'precios-estimados'
|
||||
| 'ieps'
|
||||
| 'rcg2'
|
||||
| 'vehiculos-marcas'
|
||||
| 'vehiculos-modelos';
|
||||
|
||||
export async function getSitarDataset(
|
||||
endpoint: SitarDatasetEndpoint,
|
||||
filters: { fraccion: string; nico?: string; [key: string]: string | undefined }
|
||||
): Promise<ApiResponse<SitarGenericRecord[]>> {
|
||||
const queryParams = new URLSearchParams(
|
||||
Object.entries(filters).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||||
acc[key] = String(value);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
)
|
||||
);
|
||||
return await api.get(`/v1/sitar/${endpoint}/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
@@ -1041,8 +1041,8 @@
|
||||
<table class="w-full">
|
||||
<thead class="border-b bg-white text-gray-900 dark:bg-black dark:text-white">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Código</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Prefijo</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Clave</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Fracción</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Ad valorem</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Costo Fijo</th>
|
||||
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Descripción</th>
|
||||
@@ -1054,8 +1054,8 @@
|
||||
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
onclick={() => selectUSFraction(fraction)}
|
||||
>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.fraction || fraction.code}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.code || '—'}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.fraction || '—'}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.adv_impo ?? '—'}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">{fraction.adv_expo ?? '—'}</td>
|
||||
<td class="px-4 py-2">{fraction.description || ''}</td>
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
<script lang="ts">
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { ClipboardList, FileText, Globe, Layers, ShieldCheck } from 'lucide-svelte';
|
||||
import type { TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
|
||||
import {
|
||||
getSitarALADI,
|
||||
getSitarPROSEC,
|
||||
getSitarTLCS,
|
||||
getSitarDataset,
|
||||
type SitarALADI,
|
||||
type SitarPROSEC,
|
||||
type SitarTLCS,
|
||||
type SitarGenericRecord
|
||||
} from '$lib/api/dashboard/a76/sitar';
|
||||
import {
|
||||
buildMexTariffDigitsFromCatalogRow,
|
||||
formatMexTariffDigitsForDisplay,
|
||||
splitMexTariffDigitsForSitar
|
||||
} from '$lib/utils/mexican-tariff-fraction';
|
||||
|
||||
type TabKey = 'descripcion' | 'tlcs' | 'prosec' | 'aladi' | 'immex' | 'acuerdos';
|
||||
type NonDescTabKey = Exclude<TabKey, 'descripcion'>;
|
||||
|
||||
let { selectedFraction }: { selectedFraction: TariffFraction | null } = $props();
|
||||
|
||||
let activeTab = $state<TabKey>('descripcion');
|
||||
let isResolvingTabs = $state(false);
|
||||
|
||||
let tlcsCache = $state<Record<string, SitarTLCS[]>>({});
|
||||
let prosecCache = $state<Record<string, SitarPROSEC[]>>({});
|
||||
let aladiCache = $state<Record<string, SitarALADI[]>>({});
|
||||
let immexCache = $state<Record<string, SitarGenericRecord[]>>({});
|
||||
let acuerdosCache = $state<Record<string, SitarGenericRecord[]>>({});
|
||||
|
||||
let sitarTLCSData = $state<SitarTLCS[]>([]);
|
||||
let sitarPROSECData = $state<SitarPROSEC[]>([]);
|
||||
let sitarALADIData = $state<SitarALADI[]>([]);
|
||||
let sitarIMMEXData = $state<SitarGenericRecord[]>([]);
|
||||
let sitarAcuerdosData = $state<SitarGenericRecord[]>([]);
|
||||
let activeError = $state('');
|
||||
|
||||
// All non-description tabs start blocked; resolveTabAvailability updates them after selection.
|
||||
let noDataTabs = $state<Record<TabKey, boolean>>({
|
||||
descripcion: false,
|
||||
tlcs: true,
|
||||
prosec: true,
|
||||
aladi: true,
|
||||
immex: true,
|
||||
acuerdos: true
|
||||
});
|
||||
|
||||
// Monotonically incrementing token to discard stale async responses on rapid row changes.
|
||||
let resolveToken = 0;
|
||||
|
||||
const fractionDigits = $derived(
|
||||
selectedFraction ? splitMexTariffDigitsForSitar(buildMexTariffDigitsFromCatalogRow(selectedFraction)) : null
|
||||
);
|
||||
|
||||
const hasSelection = $derived(!!selectedFraction);
|
||||
|
||||
const headerFraction = $derived(
|
||||
selectedFraction && fractionDigits
|
||||
? formatMexTariffDigitsForDisplay(buildMexTariffDigitsFromCatalogRow(selectedFraction))
|
||||
: ''
|
||||
);
|
||||
|
||||
function buildCacheKey(
|
||||
fracDigits: NonNullable<typeof fractionDigits>,
|
||||
frac: TariffFraction,
|
||||
tab: NonDescTabKey
|
||||
): string {
|
||||
return `${fracDigits.fraccion8}:${frac.nico || fracDigits.nico2 || ''}:${tab}`;
|
||||
}
|
||||
|
||||
// Hydrates display state from cache when the user navigates to a tab.
|
||||
// By the time a tab is enabled, its data is already in cache from resolveTabAvailability.
|
||||
function hydrateTabFromCache(tab: NonDescTabKey) {
|
||||
if (!fractionDigits || !selectedFraction) return;
|
||||
const key = buildCacheKey(fractionDigits, selectedFraction, tab);
|
||||
if (tab === 'tlcs') sitarTLCSData = tlcsCache[key] ?? [];
|
||||
else if (tab === 'prosec') sitarPROSECData = prosecCache[key] ?? [];
|
||||
else if (tab === 'aladi') sitarALADIData = aladiCache[key] ?? [];
|
||||
else if (tab === 'immex') sitarIMMEXData = immexCache[key] ?? [];
|
||||
else if (tab === 'acuerdos') sitarAcuerdosData = acuerdosCache[key] ?? [];
|
||||
}
|
||||
|
||||
// Fires all 5 tab queries in parallel after a fraction is selected, updates noDataTabs
|
||||
// and populates caches so tab navigation is instant and never re-queries.
|
||||
async function resolveTabAvailability(
|
||||
token: number,
|
||||
fracDigits: NonNullable<typeof fractionDigits>,
|
||||
frac: TariffFraction
|
||||
) {
|
||||
const filters = { fraccion: fracDigits.fraccion8 };
|
||||
|
||||
const [tlcsRes, prosecRes, aladiRes, immexRes, acuerdosRes] = await Promise.allSettled([
|
||||
getSitarTLCS(filters),
|
||||
getSitarPROSEC(filters),
|
||||
getSitarALADI(filters),
|
||||
getSitarDataset('reit', filters),
|
||||
getSitarDataset('requisito-previo', filters)
|
||||
]);
|
||||
|
||||
// Discard results if the fraction changed while waiting
|
||||
if (resolveToken !== token) return;
|
||||
|
||||
const tlcsRows = tlcsRes.status === 'fulfilled' ? ((tlcsRes.value.data ?? []) as SitarTLCS[]) : [];
|
||||
const prosecRows = prosecRes.status === 'fulfilled' ? ((prosecRes.value.data ?? []) as SitarPROSEC[]) : [];
|
||||
const aladiRows = aladiRes.status === 'fulfilled' ? ((aladiRes.value.data ?? []) as SitarALADI[]) : [];
|
||||
const immexRows = immexRes.status === 'fulfilled' ? (immexRes.value.data ?? []) : [];
|
||||
const acuerdosRows = acuerdosRes.status === 'fulfilled' ? (acuerdosRes.value.data ?? []) : [];
|
||||
|
||||
tlcsCache = { ...tlcsCache, [buildCacheKey(fracDigits, frac, 'tlcs')]: tlcsRows };
|
||||
prosecCache = { ...prosecCache, [buildCacheKey(fracDigits, frac, 'prosec')]: prosecRows };
|
||||
aladiCache = { ...aladiCache, [buildCacheKey(fracDigits, frac, 'aladi')]: aladiRows };
|
||||
immexCache = { ...immexCache, [buildCacheKey(fracDigits, frac, 'immex')]: immexRows };
|
||||
acuerdosCache = { ...acuerdosCache, [buildCacheKey(fracDigits, frac, 'acuerdos')]: acuerdosRows };
|
||||
|
||||
noDataTabs = {
|
||||
descripcion: false,
|
||||
tlcs: tlcsRows.length === 0,
|
||||
prosec: prosecRows.length === 0,
|
||||
aladi: aladiRows.length === 0,
|
||||
immex: immexRows.length === 0,
|
||||
acuerdos: acuerdosRows.length === 0
|
||||
};
|
||||
|
||||
isResolvingTabs = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const frac = selectedFraction;
|
||||
const fracDigits = fractionDigits;
|
||||
|
||||
activeTab = 'descripcion';
|
||||
sitarTLCSData = [];
|
||||
sitarPROSECData = [];
|
||||
sitarALADIData = [];
|
||||
sitarIMMEXData = [];
|
||||
sitarAcuerdosData = [];
|
||||
activeError = '';
|
||||
|
||||
if (!frac || !fracDigits) {
|
||||
isResolvingTabs = false;
|
||||
noDataTabs = { descripcion: false, tlcs: true, prosec: true, aladi: true, immex: true, acuerdos: true };
|
||||
return;
|
||||
}
|
||||
|
||||
// Block all non-description tabs while resolving availability for the new selection
|
||||
noDataTabs = { descripcion: false, tlcs: true, prosec: true, aladi: true, immex: true, acuerdos: true };
|
||||
isResolvingTabs = true;
|
||||
const token = ++resolveToken;
|
||||
void resolveTabAvailability(token, fracDigits, frac);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!selectedFraction || activeTab === 'descripcion') return;
|
||||
hydrateTabFromCache(activeTab);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-4 overflow-hidden">
|
||||
<div class="overflow-hidden rounded-xl border bg-card shadow-sm">
|
||||
<div class="border-b bg-muted/50 p-3">
|
||||
<h3 class="flex items-center gap-2 text-xs font-bold tracking-wider uppercase">
|
||||
<ClipboardList class="h-4 w-4" /> Información Arancelaria (Solo Consulta)
|
||||
</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead class="border-b bg-muted/30">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-bold">Fracción</th>
|
||||
<th class="px-4 py-2 text-center font-bold">UMT</th>
|
||||
<th class="px-4 py-2 text-center font-bold">UM</th>
|
||||
<th class="px-4 py-2 text-center font-bold">Advalorem Impo</th>
|
||||
<th class="px-4 py-2 text-center font-bold">Advalorem Expo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-mono text-sm font-bold">
|
||||
{headerFraction || '-'}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">{selectedFraction?.umt || '-'}</td>
|
||||
<td class="px-4 py-3 text-center">{selectedFraction?.um_code || '-'}</td>
|
||||
<td class="px-4 py-3 text-center font-bold text-blue-600">{selectedFraction?.adv_impo || '-'}</td>
|
||||
<td class="px-4 py-3 text-center font-bold text-orange-600">{selectedFraction?.adv_expo || '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !hasSelection}
|
||||
<div class="flex-1 rounded-xl border border-dashed bg-muted/10 p-4 text-xs text-muted-foreground">
|
||||
Selecciona una fracción de la tabla para ver su detalle SITAR (Descripción, TLCS, PROSEC, ALADI).
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 min-h-0">
|
||||
<Tabs.Root value={activeTab} onValueChange={(v) => (activeTab = v as TabKey)} class="h-full w-full flex flex-col">
|
||||
<Tabs.List class="mb-2 grid w-full grid-cols-6">
|
||||
<Tabs.Trigger value="descripcion" class="text-[10px] font-bold uppercase">Descripción</Tabs.Trigger>
|
||||
<Tabs.Trigger value="tlcs" class="text-[10px] font-bold uppercase" disabled={noDataTabs.tlcs || isResolvingTabs}>TLCS</Tabs.Trigger>
|
||||
<Tabs.Trigger value="prosec" class="text-[10px] font-bold uppercase" disabled={noDataTabs.prosec || isResolvingTabs}>PROSEC</Tabs.Trigger>
|
||||
<Tabs.Trigger value="aladi" class="text-[10px] font-bold uppercase" disabled={noDataTabs.aladi || isResolvingTabs}>ALADI</Tabs.Trigger>
|
||||
<Tabs.Trigger value="immex" class="text-[10px] font-bold uppercase" disabled={noDataTabs.immex || isResolvingTabs}>IMMEX</Tabs.Trigger>
|
||||
<Tabs.Trigger value="acuerdos" class="text-[10px] font-bold uppercase" disabled={noDataTabs.acuerdos || isResolvingTabs}>ACUERDOS</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
{#if activeError}
|
||||
<div class="mb-2 rounded-md border border-destructive p-2 text-xs text-destructive">{activeError}</div>
|
||||
{/if}
|
||||
|
||||
<Tabs.Content value="descripcion" class="min-h-0 flex-1">
|
||||
<div class="min-h-[120px] rounded-xl border bg-slate-50/50 p-4 dark:bg-slate-900/10">
|
||||
<h3 class="flex items-center gap-2 text-xs font-bold tracking-wider text-muted-foreground uppercase">
|
||||
<FileText class="h-4 w-4" /> Descripción de la Fracción
|
||||
</h3>
|
||||
<div class="mt-2 text-sm leading-relaxed font-medium whitespace-pre-wrap text-slate-600 dark:text-slate-400">
|
||||
{selectedFraction?.description || 'No hay descripción disponible para esta fracción.'}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="tlcs" class="min-h-0 flex-1">
|
||||
<div class="min-h-[120px] h-full overflow-hidden rounded-xl border bg-card">
|
||||
<div class="flex items-center justify-between border-b bg-muted/30 p-3">
|
||||
<h3 class="flex items-center gap-2 text-xs font-bold tracking-wider text-muted-foreground uppercase">
|
||||
<ShieldCheck class="h-4 w-4" /> Información TLCS
|
||||
</h3>
|
||||
</div>
|
||||
<div class="h-full overflow-y-auto overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead class="border-b bg-muted/10">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-bold">País</th>
|
||||
<th class="px-4 py-2 text-center font-bold">Tasa</th>
|
||||
<th class="px-4 py-2 text-center font-bold">D.O.F</th>
|
||||
<th class="px-4 py-2 text-left font-bold">Notas</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
{#each sitarTLCSData as item}
|
||||
<tr>
|
||||
<td class="px-4 py-2 font-bold">{item.PAIS}</td>
|
||||
<td class="px-4 py-2 text-center font-mono text-blue-600">{item.TASATXT}</td>
|
||||
<td class="px-4 py-2 text-center font-mono">{item.DOF || '-'}</td>
|
||||
<td class="max-w-xs truncate px-4 py-2 text-left text-[10px]" title={item.NOTA || ''}>{item.NOTA || '-'}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr><td colspan="4" class="px-4 py-8 text-center text-muted-foreground italic">No hay información de TLCS disponible para esta fracción.</td></tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="prosec" class="min-h-0 flex-1">
|
||||
<div class="min-h-[120px] h-full overflow-hidden rounded-xl border bg-card">
|
||||
<div class="flex items-center justify-between border-b bg-muted/30 p-3">
|
||||
<h3 class="flex items-center gap-2 text-xs font-bold tracking-wider text-muted-foreground uppercase">
|
||||
<Layers class="h-4 w-4" /> Programa PROSEC
|
||||
</h3>
|
||||
</div>
|
||||
<div class="h-full overflow-y-auto overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead class="border-b bg-muted/10">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-bold">Artículo</th>
|
||||
<th class="px-4 py-2 text-center font-bold">Sector</th>
|
||||
<th class="px-4 py-2 text-center font-bold">Tasa Txt</th>
|
||||
<th class="px-4 py-2 text-center font-bold">D.O.F</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
{#each sitarPROSECData as item}
|
||||
<tr>
|
||||
<td class="max-w-xs truncate px-4 py-2 text-left font-medium" title={item.PRODUCTO}>{item.PRODUCTO}</td>
|
||||
<td class="px-4 py-2 text-center font-mono">{item.SECTOR}</td>
|
||||
<td class="px-4 py-2 text-center font-mono font-bold text-orange-600">{item.TASA}</td>
|
||||
<td class="px-4 py-2 text-center font-mono">{item.DOF || '-'}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr><td colspan="4" class="px-4 py-8 text-center text-muted-foreground italic">No hay información de PROSEC disponible para esta fracción.</td></tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="aladi" class="min-h-0 flex-1">
|
||||
<div class="min-h-[120px] h-full overflow-hidden rounded-xl border bg-card">
|
||||
<div class="flex items-center justify-between border-b bg-muted/30 p-3">
|
||||
<h3 class="flex items-center gap-2 text-xs font-bold tracking-wider text-muted-foreground uppercase">
|
||||
<Globe class="h-4 w-4" /> Acuerdo ALADI
|
||||
</h3>
|
||||
</div>
|
||||
<div class="h-full overflow-y-auto overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead class="border-b bg-muted/10">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-bold">Acuerdo</th>
|
||||
<th class="px-4 py-2 text-center font-bold">País</th>
|
||||
<th class="px-4 py-2 text-center font-bold">Tasa</th>
|
||||
<th class="px-4 py-2 text-center font-bold">D.O.F</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
{#each sitarALADIData as item}
|
||||
<tr>
|
||||
<td class="px-4 py-2 text-left font-medium">{item.ACUERDO}</td>
|
||||
<td class="px-4 py-2 text-center font-bold">{item.PAIS}</td>
|
||||
<td class="px-4 py-2 text-center font-mono text-green-600">{item.TASATXT}</td>
|
||||
<td class="px-4 py-2 text-center font-mono">{item.DOF || '-'}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr><td colspan="4" class="px-4 py-8 text-center text-muted-foreground italic">No hay información de ALADI disponible para esta fracción.</td></tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<!-- IMMEX / REIT -->
|
||||
<Tabs.Content value="immex" class="min-h-0 flex-1">
|
||||
<div class="min-h-[120px] h-full overflow-hidden rounded-xl border bg-card">
|
||||
<div class="flex items-center justify-between border-b bg-muted/30 p-3">
|
||||
<h3 class="flex items-center gap-2 text-xs font-bold tracking-wider text-muted-foreground uppercase">
|
||||
IMMEX / REIT
|
||||
</h3>
|
||||
</div>
|
||||
<div class="h-full overflow-y-auto overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead class="border-b bg-muted/10">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-bold">Artículo</th>
|
||||
<th class="px-4 py-2 text-left font-bold">Fundamento</th>
|
||||
<th class="px-4 py-2 text-left font-bold">Acuerdo</th>
|
||||
<th class="px-4 py-2 text-center font-bold">Permiso</th>
|
||||
<th class="px-4 py-2 text-center font-bold">D.O.F</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
{#each sitarIMMEXData as item}
|
||||
<tr>
|
||||
<td class="px-4 py-2 text-left">{item.ARTICULO || ''}</td>
|
||||
<td class="px-4 py-2 text-left">{item.FUNDAMENTO || ''}</td>
|
||||
<td class="px-4 py-2 text-left">{item.ACUERDO || ''}</td>
|
||||
<td class="px-4 py-2 text-center font-mono">{item.PERMISO || '-'}</td>
|
||||
<td class="px-4 py-2 text-center font-mono">{item.DOF || '-'}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-8 text-center text-muted-foreground italic">
|
||||
No hay información de IMMEX disponible para esta fracción.
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<!-- ACUERDOS / Requisito previo -->
|
||||
<Tabs.Content value="acuerdos" class="min-h-0 flex-1">
|
||||
<div class="min-h-[120px] h-full overflow-hidden rounded-xl border bg-card">
|
||||
<div class="flex items-center justify-between border-b bg-muted/30 p-3">
|
||||
<h3 class="flex items-center gap-2 text-xs font-bold tracking-wider text-muted-foreground uppercase">
|
||||
Acuerdos / Requisitos previos
|
||||
</h3>
|
||||
</div>
|
||||
<div class="h-full overflow-y-auto overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead class="border-b bg-muted/10">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left font-bold">Descripción</th>
|
||||
<th class="px-4 py-2 text-center font-bold">Permiso</th>
|
||||
<th class="px-4 py-2 text-center font-bold">D.O.F</th>
|
||||
<th class="px-4 py-2 text-center font-bold">Vigencia</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
{#each sitarAcuerdosData as item}
|
||||
<tr>
|
||||
<td class="px-4 py-2 text-left">{item.DESCRIPCION || ''}</td>
|
||||
<td class="px-4 py-2 text-center font-mono">{item.PERMISO || '-'}</td>
|
||||
<td class="px-4 py-2 text-center font-mono">{item.DOF || '-'}</td>
|
||||
<td class="px-4 py-2 text-center font-mono">{item.VIGENCIA || '-'}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-8 text-center text-muted-foreground italic">
|
||||
No hay información de ACUERDOS disponible para esta fracción.
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -20,22 +20,29 @@
|
||||
import { untrack } from 'svelte';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import TariffFractionFormDialog from './TariffFractionFormDialog.svelte';
|
||||
import SitarFractionTabs from './SitarFractionTabs.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
import {
|
||||
getTariffFractionDisplayFraction,
|
||||
getTariffFractionDisplayKey
|
||||
} from '$lib/utils/tariff-fraction-display';
|
||||
|
||||
let {
|
||||
title = 'Fracciones Arancelarias',
|
||||
catalog = 'mex', // 'mex' or 'usa'
|
||||
levelFilter = null, // null or number
|
||||
readOnly = false,
|
||||
basePerm: customBasePerm = null
|
||||
basePerm: customBasePerm = null,
|
||||
showSitarTabsOnSelect = false
|
||||
}: {
|
||||
title?: string;
|
||||
catalog?: string;
|
||||
levelFilter?: number | null;
|
||||
readOnly?: boolean;
|
||||
basePerm?: string | null;
|
||||
showSitarTabsOnSelect?: boolean;
|
||||
} = $props();
|
||||
|
||||
let fractions = $state<TariffFraction[]>([]);
|
||||
@@ -74,6 +81,7 @@
|
||||
|
||||
let isFormDialogOpen = $state(false);
|
||||
let selectedFraction = $state<TariffFraction | null>(null);
|
||||
let selectedDetailFraction = $state<TariffFraction | null>(null);
|
||||
let isManageMode = $state(false); // If true, opens form in edit mode
|
||||
|
||||
// Delete confirmation
|
||||
@@ -110,6 +118,11 @@
|
||||
} else {
|
||||
fractions = [...fractions, ...newItems];
|
||||
}
|
||||
if (selectedDetailFraction) {
|
||||
selectedDetailFraction =
|
||||
[...fractions, ...newItems].find((item) => item.id === selectedDetailFraction?.id) ||
|
||||
selectedDetailFraction;
|
||||
}
|
||||
totalFractions = payload.total || 0;
|
||||
|
||||
// Safer end-of-data detection
|
||||
@@ -171,6 +184,11 @@
|
||||
isFormDialogOpen = true;
|
||||
}
|
||||
|
||||
function selectFractionDetail(fraction: TariffFraction) {
|
||||
if (!showSitarTabsOnSelect || catalog !== 'mex') return;
|
||||
selectedDetailFraction = fraction;
|
||||
}
|
||||
|
||||
function confirmDelete(fraction: TariffFraction) {
|
||||
fractionToDelete = fraction;
|
||||
showDeleteConfirm = true;
|
||||
@@ -244,7 +262,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div class="grid min-h-0 flex-1 grid-cols-1 gap-4 xl:grid-cols-12">
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden xl:col-span-8">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-end gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@@ -289,9 +308,12 @@
|
||||
</TableRow>
|
||||
{:else}
|
||||
{#each fractions as fraction (fraction.id)}
|
||||
<TableRow class="catalog-table-row">
|
||||
<TableCell class="font-mono">{fraction.um_code || fraction.code}</TableCell>
|
||||
<TableCell class="font-medium">{fraction.fraction}</TableCell>
|
||||
<TableRow
|
||||
class="catalog-table-row {selectedDetailFraction?.id === fraction.id ? 'bg-muted/40' : ''}"
|
||||
onclick={() => selectFractionDetail(fraction)}
|
||||
>
|
||||
<TableCell class="font-mono">{getTariffFractionDisplayKey(fraction)}</TableCell>
|
||||
<TableCell class="font-medium">{getTariffFractionDisplayFraction(fraction)}</TableCell>
|
||||
<TableCell class="max-w-md truncate" title={fraction.description}>
|
||||
{fraction.description}
|
||||
</TableCell>
|
||||
@@ -346,6 +368,12 @@
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{#if showSitarTabsOnSelect && catalog === 'mex'}
|
||||
<div class="min-h-0 xl:col-span-4 flex flex-col overflow-hidden">
|
||||
<SitarFractionTabs selectedFraction={selectedDetailFraction} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalFractions} registros</div>
|
||||
{/if}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
buildMexTariffDigitsFromCatalogRow,
|
||||
formatMexTariffDigitsForDisplay
|
||||
} from '$lib/utils/mexican-tariff-fraction';
|
||||
import { getTariffFractionDisplayKey } from '$lib/utils/tariff-fraction-display';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
|
||||
let {
|
||||
@@ -139,7 +140,7 @@
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
<td class="px-4 py-2 font-mono whitespace-nowrap">{fraction.um_code}</td>
|
||||
<td class="px-4 py-2 font-mono whitespace-nowrap">{getTariffFractionDisplayKey(fraction)}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap font-mono"
|
||||
>{formatMexTariffDigitsForDisplay(
|
||||
buildMexTariffDigitsFromCatalogRow(fraction)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
getTariffFractions,
|
||||
type TariffFraction
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/tariff-fractions";
|
||||
import { getTariffFractionDisplayKey } from '$lib/utils/tariff-fraction-display';
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { m } from '$lib/i18n/messages';
|
||||
|
||||
@@ -155,7 +156,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<Hash class="h-3 w-3 text-blue-500" />
|
||||
<span class="font-mono font-bold text-blue-600 dark:text-blue-400">
|
||||
{item.fraction || item.code}
|
||||
{getTariffFractionDisplayKey(item)}
|
||||
</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
|
||||
43
frontend/src/lib/utils/tariff-fraction-display.test.ts
Normal file
43
frontend/src/lib/utils/tariff-fraction-display.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
getTariffFractionDisplayFraction,
|
||||
getTariffFractionDisplayKey
|
||||
} from './tariff-fraction-display';
|
||||
import type { TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
|
||||
|
||||
function buildFraction(partial: Partial<TariffFraction>): TariffFraction {
|
||||
return {
|
||||
id: 1,
|
||||
code: '',
|
||||
fraction: '',
|
||||
description: null,
|
||||
nico: null,
|
||||
umt: null,
|
||||
adv_impo: null,
|
||||
adv_expo: null,
|
||||
updated_at: null,
|
||||
dof: null,
|
||||
aplica_ieps: null,
|
||||
um_code: null,
|
||||
...partial
|
||||
};
|
||||
}
|
||||
|
||||
describe('tariff-fraction-display', () => {
|
||||
it('uses technical code for key column', () => {
|
||||
const row = buildFraction({
|
||||
code: '01012101',
|
||||
fraction: '0101.21.01',
|
||||
um_code: '06'
|
||||
});
|
||||
expect(getTariffFractionDisplayKey(row)).toBe('01012101');
|
||||
});
|
||||
|
||||
it('uses formatted fraction for fraction column', () => {
|
||||
const row = buildFraction({
|
||||
code: '1234567890',
|
||||
fraction: '1234.56.78.90'
|
||||
});
|
||||
expect(getTariffFractionDisplayFraction(row)).toBe('1234.56.78.90');
|
||||
});
|
||||
});
|
||||
9
frontend/src/lib/utils/tariff-fraction-display.ts
Normal file
9
frontend/src/lib/utils/tariff-fraction-display.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
|
||||
|
||||
export function getTariffFractionDisplayKey(fraction: TariffFraction): string {
|
||||
return fraction.code || '-';
|
||||
}
|
||||
|
||||
export function getTariffFractionDisplayFraction(fraction: TariffFraction): string {
|
||||
return fraction.fraction || '-';
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
<TariffFractionList
|
||||
title={m['sidebar.fractions.sitar']()}
|
||||
catalog="mex"
|
||||
levelFilter={-1}
|
||||
levelFilter={5}
|
||||
readOnly={true}
|
||||
showSitarTabsOnSelect={true}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user