Merge pull request 'development' (#144) from development into feature/item-calculations

Reviewed-on: ADUANASOFT/anexo76#144
This commit is contained in:
2026-02-16 18:10:12 +00:00
15 changed files with 611 additions and 644 deletions

View File

@@ -11,8 +11,10 @@ export interface TariffFraction {
umt: string | null;
adv_impo: string | null;
adv_expo: string | null;
created_at: string | null;
updated_at: string | null;
dof: string | null;
aplica_ieps: string | null;
um_code: string | null;
}
export interface TariffFractionCreate {

View File

@@ -143,16 +143,30 @@
let showUnitDialog = $state(false);
let searchUnit = $state('');
import TariffFractionSelector from '$lib/components/dashboard/goods/modales/TariffFractionSelector.svelte';
let showFractionDialog = $state(false);
let tariffFractions = $state<TariffFraction[]>([]);
let searchFraction = $state('');
let currentPage = $state(1);
let totalFractions = $state(0);
let hasMoreFractions = $state(true);
let isLoadingFractions = $state(false);
let searchTimeout: ReturnType<typeof setTimeout>;
// Removing inline tariff fraction state
// let tariffFractions = ...
function openFractionSearch() {
showFractionDialog = true;
}
let showUSFractionDialog = $state(false);
// ...
// loadFractions removed
function selectFraction(fraction: TariffFraction) {
formData.fraction = fraction.fraction;
formData.fraction_umt = (fraction.umt ?? '') as string;
formData.fraction_uma_key = (fraction.nico ?? '') as string;
// Actualizar tarifa de importación
formData.import_tariff_code = fraction.fraction || '';
formData.import_tariff_type = (fraction.umt ?? '') as string;
showFractionDialog = false;
}
let usTariffFractions = $state<USTariffFraction[]>([]);
let searchUSFraction = $state('');
let currentUSPage = $state(1);
@@ -262,56 +276,6 @@
searchUnit = '';
}
async function openFractionSearch() {
showFractionDialog = true;
searchFraction = '';
tariffFractions = [];
currentPage = 1;
totalFractions = 0;
hasMoreFractions = true;
await loadFractions('', 1);
}
async function loadFractions(search: string, page: number = currentPage) {
const companyId = companyStore.activeCompany?.id;
if (!companyId || isLoadingFractions) return;
isLoadingFractions = true;
try {
const filters = search ? { search } : {};
const pageSize = 100;
const response = await getTariffFractions(page, pageSize, companyId, filters);
if (response.data) {
if (page === 1) {
tariffFractions = [...response.data.items];
} else {
tariffFractions = [...tariffFractions, ...response.data.items];
}
totalFractions = response.data.total;
currentPage = page;
hasMoreFractions = tariffFractions.length < response.data.total;
}
} catch (error) {
console.error('Error cargando fracciones arancelarias:', error);
} finally {
isLoadingFractions = false;
}
}
function selectFraction(fraction: TariffFraction) {
formData.fraction = fraction.fraction;
formData.fraction_umt = (fraction.umt ?? '') as string;
formData.fraction_uma_key = (fraction.nico ?? '') as string;
// Actualizar tarifa de importación
formData.import_tariff_code = fraction.fraction || '';
formData.import_tariff_type = (fraction.umt ?? '') as string;
showFractionDialog = false;
searchFraction = '';
}
async function openUSFractionSearch() {
showUSFractionDialog = true;
searchUSFraction = '';
@@ -565,7 +529,7 @@
<div class="space-y-3">
<!-- Clase y Requiere Revisión Física -->
<div class="flex gap-4 items-end">
<div class="flex items-end gap-4">
<div class="flex-1 space-y-2">
<Label for="class_code" class="font-bold">
Clase: <span class="text-red-500">*</span>
@@ -588,7 +552,7 @@
onblur={() => validateField('class_code')}
/>
{#if validationErrors.class_code}
<p class="text-sm text-red-500 mt-1">{validationErrors.class_code}</p>
<p class="mt-1 text-sm text-red-500">{validationErrors.class_code}</p>
{/if}
</div>
@@ -596,7 +560,7 @@
<Label for="material_key" class="font-bold">
Tipo de Activo Fijo: <span class="text-red-500">*</span>
</Label>
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<Input
id="material_key"
bind:value={formData.material_key}
@@ -610,12 +574,12 @@
<Button type="button" variant="outline" size="icon" onclick={openMaterialSearch}>
<Folder class="h-4 w-4" />
</Button>
<span class="text-blue-600 hover:underline cursor-pointer text-sm">
<span class="cursor-pointer text-sm text-blue-600 hover:underline">
{formData.material_description || ''}
</span>
</div>
{#if validationErrors.material_key}
<p class="text-sm text-red-500 mt-1">{validationErrors.material_key}</p>
<p class="mt-1 text-sm text-red-500">{validationErrors.material_key}</p>
{/if}
</div>
</div>
@@ -635,7 +599,7 @@
onblur={() => validateField('description_es')}
/>
{#if validationErrors.description_es}
<p class="text-sm text-red-500 mt-1">{validationErrors.description_es}</p>
<p class="mt-1 text-sm text-red-500">{validationErrors.description_es}</p>
{/if}
</div>
@@ -651,12 +615,12 @@
</div>
<!-- U.M. Comercial y Fracción Arancelaria -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label for="unit_of_measure" class="font-bold">
U.M. Comercial: <span class="text-red-500">*</span>
</Label>
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<Input
id="unit_of_measure"
bind:value={formData.unit_of_measure}
@@ -670,12 +634,12 @@
<Button type="button" variant="outline" size="icon" onclick={openUnitOfMeasureSearch}>
<Folder class="h-4 w-4" />
</Button>
<span class="text-blue-600 hover:underline cursor-pointer text-sm">
<span class="cursor-pointer text-sm text-blue-600 hover:underline">
{formData.unit_of_measure_description || ''}
</span>
</div>
{#if validationErrors.unit_of_measure}
<p class="text-sm text-red-500 mt-1">{validationErrors.unit_of_measure}</p>
<p class="mt-1 text-sm text-red-500">{validationErrors.unit_of_measure}</p>
{/if}
</div>
@@ -683,7 +647,7 @@
<Label for="fraction" class="font-bold">
Fracción Arancelaria: <span class="text-red-500">*</span>
</Label>
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<Input
id="fraction"
bind:value={formData.fraction}
@@ -699,16 +663,16 @@
</Button>
</div>
{#if validationErrors.fraction}
<p class="text-sm text-red-500 mt-1">{validationErrors.fraction}</p>
<p class="mt-1 text-sm text-red-500">{validationErrors.fraction}</p>
{/if}
</div>
</div>
<!-- Fracción Americana y Tasa Depreciación -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label for="us_fraction">Fracción Americana:</Label>
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<Input
id="us_fraction"
bind:value={formData.us_fraction}
@@ -724,7 +688,7 @@
<div class="space-y-2">
<Label for="annual_depreciation_rate">Tasa Anual de Depreciación:</Label>
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<Input
id="annual_depreciation_rate"
type="number"
@@ -742,10 +706,10 @@
</div>
<!-- FDA, Carta Porte y ECCN -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="space-y-2">
<Label for="fda_key">Clave FDA:</Label>
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<Input id="fda_key" bind:value={formData.fda_key} placeholder="Clave FDA" class="flex-1" />
<Button type="button" variant="outline" size="icon" onclick={openFDASearch}>
<Folder class="h-4 w-4" />
@@ -755,7 +719,7 @@
<div class="space-y-2">
<Label for="carta_porte_code">Carta Porte:</Label>
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
<Input
id="carta_porte_code"
bind:value={formData.carta_porte_code}
@@ -800,9 +764,9 @@
class="mt-1"
/>
</div>
<div class="border rounded-md max-h-96 overflow-auto">
<div class="max-h-96 overflow-auto rounded-md border">
<table class="w-full">
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
<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">Clave</th>
<th class="px-4 py-2 text-left font-semibold">Descripción</th>
@@ -811,7 +775,7 @@
<tbody>
{#each filteredMaterialTypes as material (material.key)}
<tr
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
onclick={() => selectMaterial(material)}
>
<td class="px-4 py-2">{material.key}</td>
@@ -844,9 +808,9 @@
class="mt-1"
/>
</div>
<div class="border rounded-md max-h-96 overflow-auto">
<div class="max-h-96 overflow-auto rounded-md border">
<table class="w-full text-sm">
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
<thead class="border-b bg-white text-gray-900 dark:bg-black dark:text-white">
<tr>
<th class="px-3 py-2 text-left font-semibold">Código</th>
<th class="px-3 py-2 text-left font-semibold">Descripción</th>
@@ -857,7 +821,7 @@
<tbody>
{#each filteredUnits as unit (unit.code)}
<tr
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
onclick={() => selectUnit(unit)}
>
<td class="px-3 py-2">{unit.code}</td>
@@ -877,66 +841,11 @@
</Dialog.Root>
<!-- Dialog: Buscar fracción arancelaria -->
<Dialog.Root bind:open={showFractionDialog}>
<Dialog.Content class="max-w-4xl w-full">
<Dialog.Header>
<Dialog.Title>CATALOGO DE FRACCIONES SITAR - SCAII</Dialog.Title>
</Dialog.Header>
<div class="space-y-4">
<div>
<Label for="search_fraction">Buscando:</Label>
<Input
id="search_fraction"
bind:value={searchFraction}
placeholder="Buscar por fracción, descripción, NICO..."
class="mt-1"
/>
</div>
<div class="border rounded-md max-h-[600px] overflow-y-auto overflow-x-auto">
<table class="w-full">
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
<tr>
<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">NICO</th>
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Descripción</th>
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">U.M.T</th>
</tr>
</thead>
<tbody>
{#each tariffFractions as fraction (fraction.code)}
<tr
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
onclick={() => selectFraction(fraction)}
>
<td class="px-4 py-2 whitespace-nowrap">{fraction.fraction}</td>
<td class="px-4 py-2 whitespace-nowrap">{fraction.nico}</td>
<td class="px-4 py-2">{fraction.description}</td>
<td class="px-4 py-2 whitespace-nowrap">{fraction.umt}</td>
</tr>
{:else}
<tr>
<td colspan="4" class="px-4 py-8 text-center text-muted-foreground">
{#if isLoadingFractions}
Cargando fracciones...
{:else}
No hay fracciones disponibles
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => (showFractionDialog = false)}>Cerrar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
<TariffFractionSelector bind:open={showFractionDialog} onSelect={selectFraction} />
<!-- Dialog: Buscar fracción americana -->
<Dialog.Root bind:open={showUSFractionDialog}>
<Dialog.Content class="max-w-4xl w-full">
<Dialog.Content class="w-full max-w-4xl">
<Dialog.Header>
<Dialog.Title>CATALOGO DE FRACCIONES AMERICANAS</Dialog.Title>
</Dialog.Header>
@@ -950,9 +859,9 @@
class="mt-1"
/>
</div>
<div class="border rounded-md max-h-[600px] overflow-y-auto overflow-x-auto">
<div class="max-h-[600px] overflow-x-auto overflow-y-auto rounded-md border">
<table class="w-full">
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
<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>
@@ -964,7 +873,7 @@
<tbody>
{#each usTariffFractions as fraction (fraction.id)}
<tr
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
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.code}</td>
@@ -996,7 +905,7 @@
<!-- Dialog: Buscar depreciación -->
<Dialog.Root bind:open={showDepreciationDialog}>
<Dialog.Content class="max-w-4xl w-full">
<Dialog.Content class="w-full max-w-4xl">
<Dialog.Header>
<Dialog.Title>CATALOGO DE DEPRECIACION</Dialog.Title>
</Dialog.Header>
@@ -1010,9 +919,9 @@
class="mt-1"
/>
</div>
<div class="border rounded-md max-h-[600px] overflow-y-auto overflow-x-auto">
<div class="max-h-[600px] overflow-x-auto overflow-y-auto rounded-md border">
<table class="w-full">
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
<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">Fracción</th>
<th class="px-4 py-2 text-left font-semibold">Descripción</th>
@@ -1022,7 +931,7 @@
<tbody>
{#each depreciationCatalog as item (item.id)}
<tr
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
onclick={() => selectDepreciation(item)}
>
<td class="px-4 py-2">{item.fraction}</td>
@@ -1052,7 +961,7 @@
<!-- Dialog: Buscar FDA -->
<Dialog.Root bind:open={showFDADialog}>
<Dialog.Content class="max-w-4xl w-full">
<Dialog.Content class="w-full max-w-4xl">
<Dialog.Header>
<Dialog.Title>CATALOGO FDA</Dialog.Title>
</Dialog.Header>
@@ -1066,9 +975,9 @@
class="mt-1"
/>
</div>
<div class="border rounded-md max-h-[600px] overflow-y-auto overflow-x-auto">
<div class="max-h-[600px] overflow-x-auto overflow-y-auto rounded-md border">
<table class="w-full">
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
<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">Clave FDA</th>
<th class="px-4 py-2 text-left font-semibold">Descripción</th>
@@ -1077,7 +986,7 @@
<tbody>
{#each fdaCatalog as item (item.id)}
<tr
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
onclick={() => selectFDA(item)}
>
<td class="px-4 py-2">{item.fda_key}</td>
@@ -1106,7 +1015,7 @@
<!-- Dialog: Buscar Carta Porte -->
<Dialog.Root bind:open={showCartaPorteDialog}>
<Dialog.Content class="max-w-4xl w-full">
<Dialog.Content class="w-full max-w-4xl">
<Dialog.Header>
<Dialog.Title>CATALOGO DE CARTA PORTE</Dialog.Title>
</Dialog.Header>
@@ -1120,9 +1029,9 @@
class="mt-1"
/>
</div>
<div class="border rounded-md max-h-[600px] overflow-y-auto overflow-x-auto">
<div class="max-h-[600px] overflow-x-auto overflow-y-auto rounded-md border">
<table class="w-full">
<thead class="bg-white dark:bg-black text-gray-900 dark:text-white border-b">
<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">Código</th>
<th class="px-4 py-2 text-left font-semibold">Descripción</th>
@@ -1131,7 +1040,7 @@
<tbody>
{#each cartaPorteCatalog as item (item.id)}
<tr
class="border-b hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
onclick={() => {
formData.carta_porte_code = item.code;
showCartaPorteDialog = false;

View File

@@ -0,0 +1,171 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button';
import { Loader2, Search } from 'lucide-svelte';
import {
getTariffFractions,
type TariffFraction
} from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
onSelect
}: { open: boolean; onSelect: (fraction: TariffFraction) => void } = $props();
let tariffFractions = $state<TariffFraction[]>([]);
let searchFraction = $state('');
let currentPage = $state(1);
let totalFractions = $state(0);
let hasMoreFractions = $state(true);
let isLoadingFractions = $state(false);
let searchTimeout: ReturnType<typeof setTimeout>;
// Cargar datos al abrir
$effect(() => {
if (open) {
// Reiniciar estado al abrir
if (tariffFractions.length === 0) {
searchFraction = '';
currentPage = 1;
totalFractions = 0;
hasMoreFractions = true;
loadFractions('', 1);
}
}
});
async function loadFractions(search: string, page: number = currentPage) {
const companyId = companyStore.activeCompany?.id;
if (!companyId || isLoadingFractions) return;
isLoadingFractions = true;
try {
const filters = search ? { search } : {};
const pageSize = 100;
const response = await getTariffFractions(page, pageSize, companyId, filters);
if (response.data) {
if (page === 1) {
tariffFractions = [...response.data.items];
} else {
tariffFractions = [...tariffFractions, ...response.data.items];
}
totalFractions = response.data.total;
currentPage = page;
hasMoreFractions = tariffFractions.length < response.data.total;
}
} catch (error) {
console.error('Error cargando fracciones arancelarias:', error);
} finally {
isLoadingFractions = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="w-full max-w-[95vw] sm:max-w-7xl">
<Dialog.Header>
<Dialog.Title>CATALOGO DE FRACCIONES SITAR - SCAII</Dialog.Title>
</Dialog.Header>
<div class="space-y-4">
<div>
<Label for="search_fraction">Buscando:</Label>
<div class="relative mt-1">
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
<Input
id="search_fraction"
bind:value={searchFraction}
placeholder="Buscar por fracción, descripción, NICO..."
class="pl-8"
oninput={() => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
tariffFractions = [];
currentPage = 1;
totalFractions = 0;
hasMoreFractions = true;
loadFractions(searchFraction, 1);
}, 500);
}}
/>
</div>
</div>
<div
class="max-h-[600px] overflow-x-auto overflow-y-auto rounded-md border"
onscroll={(e) => {
const target = e.currentTarget;
if (
target.scrollHeight - target.scrollTop <= target.clientHeight + 50 &&
hasMoreFractions &&
!isLoadingFractions
) {
loadFractions(searchFraction, currentPage + 1);
}
}}
>
<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">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">NICO</th>
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Descripción</th>
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">U.M.T</th>
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Adv. Impo</th>
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Adv. Expo</th>
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">DOF</th>
<th class="px-4 py-2 text-left font-semibold whitespace-nowrap">Aplica IEPS</th>
</tr>
</thead>
<tbody>
{#each tariffFractions as fraction (fraction.id)}
<tr
class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
onclick={() => {
onSelect(fraction);
open = false;
}}
>
<td class="px-4 py-2 font-mono whitespace-nowrap">{fraction.um_code}</td>
<td class="px-4 py-2 whitespace-nowrap">{fraction.fraction}</td>
<td class="px-4 py-2 whitespace-nowrap">{fraction.nico}</td>
<td class="px-4 py-2">{fraction.description}</td>
<td class="px-4 py-2 whitespace-nowrap">{fraction.umt}</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 whitespace-nowrap">{fraction.dof}</td>
<td class="px-4 py-2 whitespace-nowrap">{fraction.aplica_ieps}</td>
</tr>
{:else}
<tr>
<td colspan="9" class="px-4 py-8 text-center text-muted-foreground">
{#if isLoadingFractions}
<div class="flex items-center justify-center gap-2">
<Loader2 class="h-4 w-4 animate-spin" />
Cargando fracciones...
</div>
{:else}
No hay fracciones disponibles
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#if isLoadingFractions && tariffFractions.length > 0}
<div class="flex justify-center py-2">
<Loader2 class="h-4 w-4 animate-spin text-muted-foreground" />
</div>
{/if}
</div>
<div class="flex justify-end">
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -1,156 +1,14 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import * as Dialog from "$lib/components/ui/dialog";
import * as Table from "$lib/components/ui/table";
import { toast } from "svelte-sonner";
import { Search, Loader2, Hash } from "lucide-svelte";
import { getTariffFractions, type TariffFraction } from "$lib/api/dashboard/a76/general_catalogs/tariff-fractions";
import { companyStore } from "$lib/stores/company.svelte";
import TariffFractionSelector from '$lib/components/dashboard/goods/modales/TariffFractionSelector.svelte';
import { type TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
// --- PROPS ---
let {
open = $bindable(false),
onSelect
}: {
open: boolean,
onSelect: (item: TariffFraction) => void
} = $props();
// --- ESTADO ---
let items = $state<TariffFraction[]>([]);
let loading = $state(false);
let searchTerm = $state("");
let loaded = $state(false);
// Filtro local
let filteredItems = $derived(
items.filter(i =>
(i.fraction || "").includes(searchTerm) ||
(i.description || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
(i.nico || "").includes(searchTerm) ||
(i.code || "").toLowerCase().includes(searchTerm.toLowerCase())
)
);
// Cargar datos al abrir
$effect(() => {
if (open && !loaded && companyStore.activeCompany?.id) {
loadFractions();
}
});
async function loadFractions() {
if (!companyStore.activeCompany?.id) {
toast.error("No hay empresa seleccionada");
return;
}
loading = true;
try {
const response = await getTariffFractions(1, 1000, companyStore.activeCompany.id);
if (response.error) {
console.error("Error al cargar fracciones:", response.error);
toast.error(`Error: ${response.error}`);
return;
}
if (response.data?.items) {
items = response.data.items;
loaded = true;
} else {
console.warn("No se encontraron fracciones:", response);
toast.info("No se encontraron fracciones registradas");
}
} catch (e: any) {
console.error("Excepción cargando fracciones:", e);
toast.error(`Error de conexión: ${e.message || e}`);
} finally {
loading = false;
}
}
function handleSelect(item: TariffFraction) {
if (onSelect) onSelect(item);
open = false;
}
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (item: TariffFraction) => void;
} = $props();
</script>
<Dialog.Root bind:open={open}>
<Dialog.Content class="sm:max-w-[900px] max-h-[80vh] flex flex-col">
<Dialog.Header>
<Dialog.Title>Seleccionar Fracción Arancelaria</Dialog.Title>
<Dialog.Description>
Seleccione la fracción arancelaria del catálogo.
</Dialog.Description>
</Dialog.Header>
<div class="relative w-full my-2">
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por fracción, NICO o descripción..."
class="pl-9"
bind:value={searchTerm}
/>
</div>
<div class="flex-1 overflow-y-auto border rounded-md min-h-[300px]">
{#if loading}
<div class="flex flex-col items-center justify-center h-48 gap-2 text-muted-foreground">
<Loader2 class="h-8 w-8 animate-spin text-primary" />
<p>Cargando catálogo...</p>
</div>
{:else if filteredItems.length === 0}
<div class="flex flex-col items-center justify-center h-48 text-muted-foreground">
<p>No se encontraron fracciones.</p>
</div>
{:else}
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head class="w-[100px]">Código</Table.Head>
<Table.Head class="w-[120px]">Fracción</Table.Head>
<Table.Head class="w-[80px]">NICO</Table.Head>
<Table.Head>Descripción</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each filteredItems as item}
<Table.Row
class="cursor-pointer hover:bg-accent/50 transition-colors"
onclick={() => handleSelect(item)}
>
<Table.Cell class="font-mono text-xs text-muted-foreground">
{item.code}
</Table.Cell>
<Table.Cell>
<div class="flex items-center gap-2">
<Hash class="h-3 w-3 text-orange-500" />
<span class="font-mono font-bold text-orange-600 dark:text-orange-400">
{item.fraction}
</span>
</div>
</Table.Cell>
<Table.Cell class="font-mono text-sm">
{item.nico || '-'}
</Table.Cell>
<Table.Cell class="font-medium text-sm">
{item.description || '-'}
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
</div>
<Dialog.Footer>
<div class="text-xs text-muted-foreground self-center mr-auto">
{filteredItems.length} registros encontrados
</div>
<Button variant="outline" onclick={() => open = false}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
<TariffFractionSelector bind:open {onSelect} />

View File

@@ -1,216 +1,14 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Loader2, Search } from 'lucide-svelte';
import TariffFractionSelector from '$lib/components/dashboard/goods/modales/TariffFractionSelector.svelte';
import { type TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions';
let {
open = $bindable(),
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (fraction: any) => void;
onSelect: (item: TariffFraction) => void;
} = $props();
let fractions: any[] = $state([]);
let loading = $state(false);
let loadingMore = $state(false);
let searchTerm = $state('');
let error = $state('');
// Pagination state
let currentPage = $state(1);
let totalPages = $state(1);
let hasMore = $state(true);
const pageSize = 100;
// Scroll container reference
let scrollContainer: HTMLDivElement | null = $state(null);
async function loadFractions(page: number = 1, append: boolean = false) {
if (page === 1) {
loading = true;
} else {
loadingMore = true;
}
error = '';
try {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...(searchTerm.trim() && { search: searchTerm.trim() })
});
const response = await fetch(`/api-sveltekit/tariff-fractions?${params}`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.items && Array.isArray(data.items)) {
if (append) {
fractions = [...fractions, ...data.items];
} else {
fractions = data.items;
}
currentPage = data.page;
totalPages = data.pages;
hasMore = currentPage < totalPages;
} else {
console.error('Unexpected data format:', data);
if (!append) fractions = [];
}
} else {
error = `Error: ${response.status} - ${response.statusText}`;
console.error('Error response:', await response.text());
}
} catch (err) {
error = 'Error loading tariff fractions';
console.error('Error loading tariff fractions:', err);
} finally {
loading = false;
loadingMore = false;
}
}
function handleScroll(e: Event) {
if (!scrollContainer || loading || loadingMore || !hasMore) return;
const target = e.target as HTMLDivElement;
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
// Load more when within 200px of bottom
if (scrollBottom < 200) {
loadFractions(currentPage + 1, true);
}
}
function handleSelect(fraction: any) {
onSelect(fraction);
open = false;
}
// Effect to load initial data when dialog opens
$effect(() => {
if (open) {
fractions = [];
currentPage = 1;
hasMore = true;
loadFractions(1, false);
}
});
// Effect to reload when search changes
let searchDebounce: ReturnType<typeof setTimeout>;
$effect(() => {
if (open) {
clearTimeout(searchDebounce);
searchDebounce = setTimeout(() => {
fractions = [];
currentPage = 1;
hasMore = true;
loadFractions(1, false);
}, 300);
}
});
</script>
<Dialog.Root bind:open>
<Dialog.Content class="!max-w-[80vw] w-[80vw] max-h-[90vh] p-0 flex flex-col">
<Dialog.Header class="px-6 py-4 border-b">
<Dialog.Title class="text-lg font-semibold">FRACCIONES ARANCELARIAS</Dialog.Title>
</Dialog.Header>
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900">
<div class="flex items-center gap-2">
<Search class="w-4 h-4 text-zinc-400" />
<Input
bind:value={searchTerm}
placeholder="Buscar por código, fracción, descripción, NICO o UMT..."
class="flex-1 h-9"
/>
</div>
<p class="text-xs text-zinc-500 mt-2">
Mostrando {fractions.length} de {currentPage * pageSize} resultados
</p>
</div>
<div
bind:this={scrollContainer}
onscroll={handleScroll}
class="flex-1 overflow-auto px-6 py-4"
>
{#if loading}
<div class="flex items-center justify-center py-20">
<Loader2 class="w-8 h-8 animate-spin text-zinc-900 dark:text-zinc-100" />
</div>
{:else if error}
<div class="flex items-center justify-center py-20 text-red-600">
<p>{error}</p>
</div>
{:else}
<div class="border rounded-md overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white sticky top-0">
<tr>
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
>Código</th
>
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
>Fracción</th
>
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
>Descripción</th
>
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
>NICO</th
>
<th class="px-3 py-2 text-left font-semibold">UMT</th>
</tr>
</thead>
<tbody>
{#each fractions as fraction, i}
<tr
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
onclick={() => handleSelect(fraction)}
>
<td class="px-3 py-2 border-r">{fraction.code || ''}</td>
<td class="px-3 py-2 border-r">{fraction.fraction || ''}</td>
<td class="px-3 py-2 border-r">{fraction.description || ''}</td>
<td class="px-3 py-2 border-r text-center">{fraction.nico || ''}</td>
<td class="px-3 py-2 text-center">{fraction.umt || ''}</td>
</tr>
{/each}
{#if fractions.length === 0 && !loading}
<tr>
<td colspan="5" class="px-3 py-8 text-center text-zinc-500">
No se encontraron resultados
</td>
</tr>
{/if}
</tbody>
</table>
</div>
{#if loadingMore}
<div class="flex items-center justify-center py-4">
<Loader2 class="w-6 h-6 animate-spin text-zinc-900 dark:text-zinc-100" />
<span class="ml-2 text-sm text-zinc-600 dark:text-zinc-400">Cargando más...</span>
</div>
{/if}
{#if !hasMore && fractions.length > 0}
<div class="text-center py-4 text-sm text-zinc-500">
Todos los resultados cargados
</div>
{/if}
{/if}
</div>
<div class="px-6 py-3 border-t bg-zinc-50 dark:bg-zinc-900 flex items-center justify-end gap-2">
<Button variant="outline" size="sm">Editar</Button>
</div>
</Dialog.Content>
</Dialog.Root>
<TariffFractionSelector bind:open {onSelect} />

View File

@@ -76,11 +76,12 @@ class CompanyStore {
this._loading = true;
try {
const response = await fetch('/api/v1/a76/company/my-companies', {
credentials: 'include'
});
if (response.ok) {
const newCompanies = await response.json();
// Importamos dinámicamente para evitar dependencias circulares si las hubiera
const { api } = await import('$lib/api');
const response = await api.get<Company[]>('/v1/a76/company/my-companies');
if (response.data) {
const newCompanies = response.data;
// Detectar si el tenant ha cambiado
if (newCompanies.length > 0) {
@@ -88,7 +89,6 @@ class CompanyStore {
// Si el tenant cambió, limpiar el store primero
if (this._currentTenantId !== null && this._currentTenantId !== newTenantId) {
this.clear();
}
@@ -102,7 +102,7 @@ class CompanyStore {
this.setActiveCompany(this._companies[0], true); // silent=true para inicialización
}
} else {
console.error('Error loading companies:', response.statusText);
console.error('Error loading companies:', response.error);
// Si falla la carga (ej: 401), limpiar el store
if (response.status === 401) {
this.clear();

View File

@@ -13,14 +13,15 @@
import { obtenerAtajosListaFracciones } from '$lib/config/shortcuts/dashboard/general_catalogs/tariff_fractions/list';
import { onMount } from 'svelte';
// Estado
let tariffFractions = $state<TariffFraction[]>([]);
let filteredFractions = $state<TariffFraction[]>([]);
let searchQuery = $state('');
let isLoading = $state(false);
let currentPage = $state(1);
let totalPages = $state(1);
let totalRecords = $state(0);
const pageSize = 50;
let searchTimeout: NodeJS.Timeout;
// Atajos
useShortcuts(
@@ -35,24 +36,24 @@
loadTariffFractions();
});
// Filtrar fracciones cuando cambia la búsqueda
// Efecto para búsqueda con debounce
$effect(() => {
if (searchQuery.trim() === '') {
filteredFractions = tariffFractions;
} else {
const query = searchQuery.toLowerCase();
filteredFractions = tariffFractions.filter(
(fraction) =>
fraction.code.toLowerCase().includes(query) ||
fraction.fraction.toLowerCase().includes(query) ||
(fraction.description ?? '').toLowerCase().includes(query) ||
(fraction.nico ?? '').toLowerCase().includes(query) ||
(fraction.umt ?? '').toLowerCase().includes(query)
);
}
// Limpiar timeout anterior
clearTimeout(searchTimeout);
// Setup nuevo timeout
searchTimeout = setTimeout(() => {
// Resetear a página 1 cuando cambia la búsqueda
if (currentPage !== 1) {
currentPage = 1;
}
loadTariffFractions(1, searchQuery);
}, 500);
return () => clearTimeout(searchTimeout);
});
async function loadTariffFractions(page: number = 1) {
async function loadTariffFractions(page: number = 1, search: string = '') {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
console.error('No hay compañía activa');
@@ -61,10 +62,15 @@
isLoading = true;
try {
const response = await getTariffFractions(page, pageSize, companyId);
// Preparar filtros
const filters: Record<string, any> = {};
if (search.trim()) {
filters.search = search.trim();
}
const response = await getTariffFractions(page, pageSize, companyId, filters);
if (response.data) {
tariffFractions = response.data.items;
filteredFractions = response.data.items;
totalPages = response.data.pages;
totalRecords = response.data.total;
currentPage = response.data.page;
@@ -78,31 +84,31 @@
async function goToPage(page: number) {
if (page >= 1 && page <= totalPages && page !== currentPage) {
await loadTariffFractions(page);
await loadTariffFractions(page, searchQuery);
}
}
</script>
<div class="container mx-auto p-6">
<Card.Root>
<Card.Header class="text-center border-b">
<Card.Header class="border-b text-center">
<Card.Title class="text-2xl font-bold uppercase">
Catálogo de Fracciones SITAR - SCAII
</Card.Title>
<p class="text-muted-foreground mt-2">Nomenclatura arancelaria mexicana completa</p>
<p class="mt-2 text-muted-foreground">Nomenclatura arancelaria mexicana completa</p>
</Card.Header>
<Card.Content class="pt-6">
<div class="space-y-6">
<!-- Barra de búsqueda y acciones -->
<div class="flex gap-4 items-center">
<div class="flex-1 relative">
<div class="flex items-center gap-4">
<div class="relative flex-1">
<Search
class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
class="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground"
/>
<Input
bind:value={searchQuery}
placeholder="Buscar por código, fracción, descripción, NICO o UMT..."
placeholder="Buscar por código, fracción, descripción, NICO o UMT (Búsqueda en servidor)..."
class="pl-9"
/>
</div>
@@ -120,13 +126,13 @@
<span>Cargando...</span>
</div>
{:else}
Mostrando {filteredFractions.length} de {totalRecords} fracciones arancelarias
Mostrando {tariffFractions.length} de {totalRecords} fracciones arancelarias
{#if searchQuery}
(filtrado)
{/if}
{/if}
</div>
{#if !searchQuery && totalPages > 1}
{#if totalPages > 1}
<div class="flex items-center gap-2">
<span>Página {currentPage} de {totalPages}</span>
</div>
@@ -134,9 +140,9 @@
</div>
<!-- Tabla de fracciones -->
<div class="border rounded-md overflow-auto max-h-[600px]">
<div class="max-h-[600px] overflow-auto rounded-md border">
<Table.Root>
<Table.Header class="bg-background z-10">
<Table.Header class="z-10 bg-background">
<Table.Row>
<Table.Head class="w-[100px]">Código</Table.Head>
<Table.Head class="w-[120px]">Fracción</Table.Head>
@@ -148,9 +154,9 @@
</Table.Row>
</Table.Header>
<Table.Body>
{#if filteredFractions.length === 0}
{#if tariffFractions.length === 0}
<Table.Row>
<Table.Cell colspan={7} class="text-center py-8 text-muted-foreground">
<Table.Cell colspan={7} class="py-8 text-center text-muted-foreground">
{#if isLoading}
Cargando fracciones arancelarias...
{:else if searchQuery}
@@ -161,7 +167,7 @@
</Table.Cell>
</Table.Row>
{:else}
{#each filteredFractions as fraction (fraction.id)}
{#each tariffFractions as fraction (fraction.id)}
<Table.Row class="hover:bg-muted/50">
<Table.Cell class="font-mono text-sm">{fraction.code}</Table.Cell>
<Table.Cell class="font-mono text-sm font-medium"
@@ -182,7 +188,7 @@
</div>
<!-- Paginación -->
{#if !searchQuery && totalPages > 1}
{#if totalPages > 1}
<div class="flex items-center justify-center gap-2">
<Button
variant="outline"