Merge pull request 'development' (#144) from development into feature/item-calculations
Reviewed-on: ADUANASOFT/anexo76#144
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user