feat: Implement auto-fill descriptions for key fields and enhance form layout in the fixed asset class form.

This commit is contained in:
2026-03-04 10:42:11 -07:00
parent 9081117410
commit 311d1c7259
7 changed files with 787 additions and 508 deletions

View File

@@ -1,16 +1,16 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { CustomsBroker } from "./columns.js";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
import { goto } from "$app/navigation";
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
import { Button } from '$lib/components/ui/button/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import type { CustomsBroker } from './columns.js';
import DetailsDialog from './details-dialog.svelte';
import DeleteDialog from './delete-dialog.svelte';
import { goto } from '$app/navigation';
let {
let {
broker,
onSuccess
}: {
}: {
broker: CustomsBroker;
onSuccess?: () => void;
} = $props();
@@ -32,10 +32,6 @@
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
@@ -43,33 +39,25 @@
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button
variant="ghost"
size="icon"
class="relative h-8 w-8 p-0"
>
<Button variant="ghost" size="icon" class="relative h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
<EllipsisIcon class="h-4 w-4" />
</Button>
</DropdownMenu.Trigger>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyKey}>
Copiar clave
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleCopyKey}>Copiar clave</DropdownMenu.Item>
{#if broker.tax_id}
<DropdownMenu.Item onclick={handleCopyTaxId}>
Copiar RFC
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleCopyTaxId}>Copiar RFC</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item onclick={handleViewDetails}>
Ver detalles
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => goto(`/dashboard/customs_brokers/edit/${broker.broker_key}`)}>
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item
onclick={() => goto(`/dashboard/customs_brokers/edit/${broker.broker_key}`)}
>
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-destructive">

View File

@@ -76,6 +76,8 @@
material_description: '',
unit_of_measure_description: '',
unit_measure_key: '',
fraction_description: '',
us_fraction_description: '',
fraction_umt: '',
fraction_uma_key: '',
us_fraction_ad_valorem: '',
@@ -108,7 +110,8 @@
formData.import_tariff_code = snap.import_tariff_code ?? '';
formData.import_tariff_type = snap.import_tariff_type ?? '';
formData.export_tariff_code = snap.export_tariff_code ?? '';
formData.export_tariff_type = snap.export_tariff_type ?? '';
// Si tenemos initialData con claves cargadas en modo edición, deberíamos resolver sus descripciones si no las tenemos.
// (Opcional si el backend ya manda la descripción)
} else {
// Reset form when initialData is null (new class)
formData.class_code = '';
@@ -160,6 +163,7 @@
function selectFraction(fraction: TariffFraction) {
formData.fraction = fraction.fraction;
formData.fraction_description = fraction.description || '';
formData.fraction_umt = (fraction.umt ?? '') as string;
formData.fraction_uma_key = (fraction.nico ?? '') as string;
// Actualizar tarifa de importación
@@ -317,6 +321,7 @@
function selectUSFraction(fraction: USTariffFraction) {
formData.us_fraction = fraction.code;
formData.us_fraction_description = fraction.description || '';
formData.us_fraction_ad_valorem = fraction.ad_valorem?.toString() || '0.00';
formData.us_fraction_fixed_rate = fraction.fixed_cost?.toString() || '0.00000000';
// Actualizar tarifa de exportación
@@ -449,6 +454,137 @@
return Object.keys(errors).length === 0;
}
// Auto-fill callbacks
function handleMaterialKeyBlur() {
const keyUpper = formData.material_key?.trim().toUpperCase() || '';
formData.material_key = keyUpper;
if (!keyUpper) {
formData.material_description = '';
validateField('material_key');
return;
}
if (materialTypes.length === 0) {
const companyId = companyStore.activeCompany?.id;
if (companyId) {
materialTypesApi
.list(1, 100, 'ACTIVO FIJO')
.then((response) => {
if (response.data) {
materialTypes = response.data.items;
const mat = materialTypes.find((m) => m.key.toUpperCase() === keyUpper);
formData.material_description = mat ? mat.description : '';
}
})
.catch(console.error);
}
} else {
const mat = materialTypes.find((m) => m.key.toUpperCase() === keyUpper);
formData.material_description = mat ? mat.description : '';
}
validateField('material_key');
}
function handleUnitOfMeasureBlur() {
const umUpper = formData.unit_of_measure?.trim().toUpperCase() || '';
formData.unit_of_measure = umUpper;
if (!umUpper) {
formData.unit_of_measure_description = '';
formData.unit_measure_key = '';
validateField('unit_of_measure');
return;
}
if (unitsOfMeasureData.length === 0) {
loadUnitsOfMeasure()
.then(() => {
const um = unitsOfMeasureData.find((u) => u.code.toUpperCase() === umUpper);
if (um) {
formData.unit_of_measure_description = um.description;
formData.unit_measure_key = um.claveMexicana;
} else {
formData.unit_of_measure_description = '';
formData.unit_measure_key = '';
}
})
.catch(console.error);
} else {
const um = unitsOfMeasureData.find((u) => u.code.toUpperCase() === umUpper);
if (um) {
formData.unit_of_measure_description = um.description;
formData.unit_measure_key = um.claveMexicana;
} else {
formData.unit_of_measure_description = '';
formData.unit_measure_key = '';
}
}
validateField('unit_of_measure');
}
function handleFractionBlur() {
const val = formData.fraction?.trim() || '';
formData.fraction = val;
if (!val) {
formData.fraction_description = '';
formData.fraction_umt = '';
validateField('fraction');
return;
}
if (companyStore.activeCompany?.id) {
getTariffFractions(1, 10, companyStore.activeCompany.id, { fraction: val })
.then((response) => {
if (response.data && response.data.items.length > 0) {
// Intentar encontrar coincidencia exacta primero
const item =
response.data.items.find((i) => i.fraction === val) || response.data.items[0];
formData.fraction_description = item.description || '';
formData.fraction_umt = item.umt || '';
} else {
formData.fraction_description = '';
formData.fraction_umt = '';
}
})
.catch(console.error);
}
validateField('fraction');
}
function handleUSFractionBlur() {
const val = formData.us_fraction?.trim() || '';
formData.us_fraction = val;
if (!val) {
formData.us_fraction_description = '';
formData.us_fraction_ad_valorem = '';
formData.us_fraction_fixed_rate = '';
// No tiene validación mandatoria según el código actual, pero llamamos por si acaso
return;
}
if (companyStore.activeCompany?.id) {
getUSTariffFractions(1, 10, companyStore.activeCompany.id, { code: val })
.then((response) => {
if (response.data && response.data.items.length > 0) {
const item = response.data.items.find((i) => i.code === val) || response.data.items[0];
formData.us_fraction_description = item.description || '';
formData.us_fraction_ad_valorem = item.ad_valorem?.toString() || '';
formData.us_fraction_fixed_rate = item.fixed_cost?.toString() || '';
} else {
formData.us_fraction_description = '';
formData.us_fraction_ad_valorem = '';
formData.us_fraction_fixed_rate = '';
}
})
.catch(console.error);
}
}
// Validar campo individual (para validación en blur)
function validateField(fieldName: string) {
if (!showErrors) return; // Solo validar si ya se intentó guardar
@@ -522,14 +658,19 @@
}
// Escuchar el evento de guardado del padre
if (typeof document !== 'undefined') {
document.addEventListener('save-form', handleSave);
}
$effect(() => {
if (typeof document !== 'undefined') {
document.addEventListener('save-form', handleSave);
return () => {
document.removeEventListener('save-form', handleSave);
};
}
});
</script>
<div class="space-y-3">
<!-- Clase y Requiere Revisión Física -->
<div class="flex items-end gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="flex-1 space-y-2">
<Label for="class_code" class="font-bold">
Clase: <span class="text-red-500">*</span>
@@ -569,18 +710,21 @@
? 'border-red-500 focus-visible:ring-red-500'
: ''}"
maxlength={10}
onblur={() => validateField('material_key')}
onblur={handleMaterialKeyBlur}
/>
<Button type="button" variant="outline" size="icon" onclick={openMaterialSearch}>
<Folder class="h-4 w-4" />
</Button>
<span class="cursor-pointer text-sm text-blue-600 hover:underline">
{formData.material_description || ''}
</span>
</div>
{#if validationErrors.material_key}
<p class="mt-1 text-sm text-red-500">{validationErrors.material_key}</p>
{/if}
<div class="min-h-[20px]">
{#if validationErrors.material_key}
<p class="text-sm text-red-500">{validationErrors.material_key}</p>
{:else if formData.material_description}
<span class="block truncate text-sm text-blue-600">
{formData.material_description}
</span>
{/if}
</div>
</div>
</div>
@@ -629,18 +773,21 @@
? 'border-red-500 focus-visible:ring-red-500'
: ''}"
maxlength={5}
onblur={() => validateField('unit_of_measure')}
onblur={handleUnitOfMeasureBlur}
/>
<Button type="button" variant="outline" size="icon" onclick={openUnitOfMeasureSearch}>
<Folder class="h-4 w-4" />
</Button>
<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="mt-1 text-sm text-red-500">{validationErrors.unit_of_measure}</p>
{/if}
<div class="min-h-[20px]">
{#if validationErrors.unit_of_measure}
<p class="text-sm text-red-500">{validationErrors.unit_of_measure}</p>
{:else if formData.unit_of_measure_description}
<span class="block truncate text-sm text-blue-600">
{formData.unit_of_measure_description}
</span>
{/if}
</div>
</div>
<div class="space-y-2">
@@ -656,15 +803,21 @@
? 'border-red-500 focus-visible:ring-red-500'
: ''}"
maxlength={10}
onblur={() => validateField('fraction')}
onblur={handleFractionBlur}
/>
<Button type="button" variant="outline" size="icon" onclick={openFractionSearch}>
<Folder class="h-4 w-4" />
</Button>
</div>
{#if validationErrors.fraction}
<p class="mt-1 text-sm text-red-500">{validationErrors.fraction}</p>
{/if}
<div class="min-h-[20px]">
{#if validationErrors.fraction}
<p class="text-sm text-red-500">{validationErrors.fraction}</p>
{:else if formData.fraction_description}
<span class="block truncate text-sm text-blue-600">
{formData.fraction_description}
</span>
{/if}
</div>
</div>
</div>
@@ -677,13 +830,21 @@
id="us_fraction"
bind:value={formData.us_fraction}
placeholder="Fracción americana"
class="flex-1"
maxlength={16}
class="flex-1 uppercase"
maxlength={10}
onblur={handleUSFractionBlur}
/>
<Button type="button" variant="outline" size="icon" onclick={openUSFractionSearch}>
<Folder class="h-4 w-4" />
</Button>
</div>
<div class="min-h-[20px]">
{#if formData.us_fraction_description}
<span class="block truncate text-sm text-blue-600">
{formData.us_fraction_description}
</span>
{/if}
</div>
</div>
<div class="space-y-2">

View File

@@ -121,13 +121,16 @@
</script>
<Sheet.Root bind:open={helpStore.isOpen}>
<Sheet.Trigger asChild>
<button
class="fixed right-6 bottom-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-transform hover:scale-110 active:scale-95"
aria-label="Ayuda"
>
<HelpCircle size={28} />
</button>
<Sheet.Trigger>
{#snippet child({ props })}
<button
{...props}
class="fixed right-6 bottom-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-transform hover:scale-110 active:scale-95"
aria-label="Ayuda"
>
<HelpCircle size={28} />
</button>
{/snippet}
</Sheet.Trigger>
<Sheet.Content side="right" class="w-[400px] sm:w-[540px]">
<Sheet.Header>

View File

@@ -49,8 +49,9 @@
role="dialog"
aria-modal="true"
>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="w-full max-w-2xl rounded-xl bg-white p-6 shadow-2xl dark:bg-gray-900 text-gray-900 dark:text-gray-100 max-h-[80vh] overflow-y-auto"
class="max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-xl bg-white p-6 text-gray-900 shadow-2xl dark:bg-gray-900 dark:text-gray-100"
role="document"
bind:this={modalRef}
onkeydown={handleFocusTrap}
@@ -83,12 +84,14 @@
<!-- Global Navigation -->
<div>
<h3
class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
class="mb-3 text-sm font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Global Navigation (Alt)
</h3>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="space-y-2 max-h-64 overflow-y-auto pr-1"
class="max-h-64 space-y-2 overflow-y-auto pr-1"
role="region"
aria-label="Global Navigation shortcuts"
bind:this={globalList}
@@ -118,15 +121,17 @@
<!-- Local Actions -->
<div>
<h3
class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
class="mb-3 text-sm font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Active Actions (Alt+Shift)
</h3>
{#if localShortcuts.length === 0}
<p class="text-sm italic text-gray-400">No specific actions for this view.</p>
<p class="text-sm text-gray-400 italic">No specific actions for this view.</p>
{:else}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="space-y-2 max-h-64 overflow-y-auto pr-1"
class="max-h-64 space-y-2 overflow-y-auto pr-1"
role="region"
aria-label="Active Actions shortcuts"
bind:this={localList}

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import { useSidebar } from "$lib/components/ui/sidebar/context.svelte.js";
import ChevronRight from "@lucide/svelte/icons/chevron-right";
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte.js';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
let {
items,
items
}: {
items: {
title: string;
@@ -37,30 +37,30 @@
}
function handleTriggerEnter(title: string) {
if (sidebar.state !== "collapsed") return;
if (sidebar.state !== 'collapsed') return;
activeTitle = title;
}
function handleTriggerLeave(event: PointerEvent, title: string) {
if (sidebar.state !== "collapsed") return;
if (sidebar.state !== 'collapsed') return;
// Si nos movemos al contenido (o nos quedamos en el trigger), no cerramos
if (shouldKeepOpen(event, title)) return;
activeTitle = null;
}
function handleContentEnter(title: string) {
if (sidebar.state !== "collapsed") return;
if (sidebar.state !== 'collapsed') return;
activeTitle = title;
}
function handleContentLeave(event: PointerEvent, title: string) {
if (sidebar.state !== "collapsed") return;
if (sidebar.state !== 'collapsed') return;
// Si nos movemos de vuelta al trigger (o dentro del contenido), no cerramos
if (shouldKeepOpen(event, title)) return;
activeTitle = null;
}
@@ -81,23 +81,26 @@
<Sidebar.Menu>
{#each items as item (item.title)}
{#if item.items && item.items.length > 0}
{#if sidebar.state === "collapsed"}
{#if sidebar.state === 'collapsed'}
<!-- Sidebar Colapsado: Dropdown controlado por eventos estrictos -->
<Sidebar.MenuItem>
<DropdownMenu.Root
open={activeTitle === item.title}
onOpenChange={(v) => onOpenChange(v, item.title)}
modal={false}
>
<DropdownMenu.Trigger asChild>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<div
id={`trigger-${item.title}`}
class="relative z-30 w-full flex justify-center"
class="relative z-30 flex w-full justify-center"
onpointerenter={() => handleTriggerEnter(item.title)}
onpointerleave={(e) => handleTriggerLeave(e, item.title)}
>
<Sidebar.MenuButton {...props} tooltipContent={null} class="justify-center">
<Sidebar.MenuButton
{...props}
tooltipContent={undefined}
class="justify-center"
>
{#if item.icon}
<item.icon />
{:else}
@@ -113,7 +116,7 @@
side="right"
align="start"
sideOffset={0}
class="z-50 w-64 rounded-lg p-0 shadow-lg overflow-visible"
class="z-50 w-64 overflow-visible rounded-lg p-0 shadow-lg"
id={`content-${item.title}`}
onpointerenter={() => handleContentEnter(item.title)}
onpointerleave={(e) => handleContentLeave(e, item.title)}
@@ -123,7 +126,9 @@
Posicionado con right-full para estar exactamente donde el trigger termina (offset 0).
Usamos w-8 h-8 para coincidir con un botón de tamaño estándar de sidebar.
-->
<div class="absolute right-full top-0 flex items-center justify-center bg-sidebar-accent text-sidebar-accent-foreground rounded-l-lg w-8 h-8 shadow-none border border-sidebar-border border-r-0 z-50">
<div
class="absolute top-0 right-full z-50 flex h-8 w-8 items-center justify-center rounded-l-lg border border-r-0 border-sidebar-border bg-sidebar-accent text-sidebar-accent-foreground shadow-none"
>
{#if item.icon}
<item.icon class="size-4 shrink-0" />
{:else}
@@ -134,19 +139,28 @@
<!--
Panel Principal
-->
<div class="w-full h-full p-1 pointer-events-auto bg-popover rounded-lg rounded-tl-none border border-sidebar-border ml-[0px]">
<div
class="pointer-events-auto ml-[0px] h-full w-full rounded-lg rounded-tl-none border border-sidebar-border bg-popover p-1"
>
<!-- Título en el panel principal -->
<div class="px-2 py-2 text-sm font-medium border-b text-sidebar-foreground truncate">
<div
class="truncate border-b px-2 py-2 text-sm font-medium text-sidebar-foreground"
>
{item.title}
</div>
<DropdownMenu.DropdownMenuGroup class="mt-1 max-h-80 overflow-y-auto">
{#each item.items as subItem (subItem.title)}
<DropdownMenu.DropdownMenuItem asChild>
<a href={subItem.url} class="flex w-full items-center gap-2 overflow-hidden">
<span class="truncate">{subItem.title}</span>
</a>
<DropdownMenu.DropdownMenuItem>
{#snippet child({ props })}
<a
href={subItem.url}
{...props}
class="flex w-full items-center gap-2 overflow-hidden"
>
<span class="truncate">{subItem.title}</span>
</a>
{/snippet}
</DropdownMenu.DropdownMenuItem>
{/each}
</DropdownMenu.DropdownMenuGroup>
@@ -209,4 +223,3 @@
{/each}
</Sidebar.Menu>
</Sidebar.Group>

View File

@@ -9,8 +9,8 @@ import Description from "./dialog-description.svelte";
import Trigger from "./dialog-trigger.svelte";
import Close from "./dialog-close.svelte";
const Root = Dialog.Root;
const Portal = Dialog.Portal;
const Root = Dialog?.Root ?? (class { } as any);
const Portal = Dialog?.Portal ?? (class { } as any);
export {
Root,

File diff suppressed because it is too large Load Diff