feature/search-select

This commit is contained in:
hreyes
2026-02-11 16:55:32 -06:00
parent 573dd00056
commit cfdbd7771e
8 changed files with 442 additions and 92 deletions

View File

@@ -0,0 +1,123 @@
<script lang="ts">
import { Combobox as ComboboxPrimitive } from 'bits-ui';
import { Check, ChevronsUpDown, X } from 'lucide-svelte';
import { cn } from '$lib/utils';
interface ComboboxItem {
value: string;
label: string;
disabled?: boolean;
}
interface ComboboxProps {
items: ComboboxItem[];
value?: string;
onValueChange?: (value: string) => void;
placeholder?: string;
disabled?: boolean;
searchPlaceholder?: string;
emptyMessage?: string;
class?: string;
}
let {
items = [],
value = $bindable(''),
onValueChange,
placeholder = 'Seleccionar...',
disabled = false,
searchPlaceholder = 'Buscar...',
emptyMessage = 'No se encontraron resultados',
class: className = ''
}: ComboboxProps = $props();
let searchQuery = $state('');
let open = $state(false);
// Filter items based on search query
const filteredItems = $derived(
searchQuery
? items.filter((item) => item.label.toLowerCase().includes(searchQuery.toLowerCase()))
: items
);
// Get selected item label
const selectedLabel = $derived(items.find((item) => item.value === value)?.label || placeholder);
function handleValueChange(newValue: string) {
value = newValue;
onValueChange?.(newValue);
open = false;
searchQuery = '';
}
function handleClear(e: Event) {
e.stopPropagation();
handleValueChange('');
}
</script>
<ComboboxPrimitive.Root bind:open {disabled}>
<div class="relative {className}">
<ComboboxPrimitive.Trigger
class={cn(
'border-input data-[placeholder]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 shadow-xs flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
!value && 'text-muted-foreground'
)}
>
<span class="truncate">{selectedLabel}</span>
<div class="flex items-center gap-1">
{#if value && !disabled}
<button
type="button"
onclick={handleClear}
class="hover:bg-accent rounded-sm p-0.5 transition-colors"
>
<X class="size-4" />
</button>
{/if}
<ChevronsUpDown class="size-4 opacity-50" />
</div>
</ComboboxPrimitive.Trigger>
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Content
class={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-[--bits-combobox-trigger-width] rounded-md border p-1 shadow-md outline-none'
)}
sideOffset={4}
>
<div class="border-input mb-1 flex items-center rounded-md border px-3">
<ComboboxPrimitive.Input
bind:value={searchQuery}
class="placeholder:text-muted-foreground flex h-9 w-full bg-transparent py-2 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50"
placeholder={searchPlaceholder}
autofocus
/>
</div>
<div class="max-h-[300px] overflow-y-auto">
{#if filteredItems.length === 0}
<div class="text-muted-foreground py-6 text-center text-sm">
{emptyMessage}
</div>
{:else}
{#each filteredItems as item (item.value)}
<ComboboxPrimitive.Item
value={item.value}
disabled={item.disabled}
onSelect={() => handleValueChange(item.value)}
class={cn(
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50'
)}
>
<Check class={cn('size-4', value === item.value ? 'opacity-100' : 'opacity-0')} />
<span class="flex-1 truncate">{item.label}</span>
</ComboboxPrimitive.Item>
{/each}
{/if}
</div>
</ComboboxPrimitive.Content>
</ComboboxPrimitive.Portal>
</div>
</ComboboxPrimitive.Root>

View File

@@ -0,0 +1 @@
export { default as Combobox } from './combobox.svelte';

View File

@@ -1,37 +1,46 @@
import { Select as SelectPrimitive } from "bits-ui";
import Group from "./select-group.svelte";
import Label from "./select-label.svelte";
import Item from "./select-item.svelte";
import Content from "./select-content.svelte";
import Item from "./select-item.svelte";
import Label from "./select-label.svelte";
import Trigger from "./select-trigger.svelte";
import Separator from "./select-separator.svelte";
import ScrollDownButton from "./select-scroll-down-button.svelte";
import ScrollUpButton from "./select-scroll-up-button.svelte";
import ScrollDownButton from "./select-scroll-down-button.svelte";
import GroupHeading from "./select-group-heading.svelte";
import SearchableSelect from "./select-searchable.svelte";
const Root = SelectPrimitive.Root;
const Group = SelectPrimitive.Group;
const Input = SelectPrimitive.Input;
const Value = SelectPrimitive.Value;
export {
Root,
Group,
Input,
Label,
Item,
Value,
Content,
Trigger,
Separator,
ScrollDownButton,
ScrollUpButton,
ScrollDownButton,
GroupHeading,
SearchableSelect,
//
Root as Select,
Group as SelectGroup,
Input as SelectInput,
Label as SelectLabel,
Item as SelectItem,
Value as SelectValue,
Content as SelectContent,
Trigger as SelectTrigger,
Separator as SelectSeparator,
ScrollDownButton as SelectScrollDownButton,
ScrollUpButton as SelectScrollUpButton,
ScrollDownButton as SelectScrollDownButton,
GroupHeading as SelectGroupHeading,
SearchableSelect as SelectSearchable,
};

View File

@@ -3,6 +3,7 @@
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
import { cn, type WithoutChild } from "$lib/utils.js";
import { setContext } from 'svelte';
let {
ref = $bindable(null),
@@ -10,10 +11,32 @@
sideOffset = 4,
portalProps,
children,
searchable = true,
searchPlaceholder = "Buscar...",
autoFocusSearch = true,
...restProps
}: WithoutChild<SelectPrimitive.ContentProps> & {
portalProps?: SelectPrimitive.PortalProps;
searchable?: boolean;
searchPlaceholder?: string;
autoFocusSearch?: boolean;
} = $props();
let searchQuery = $state('');
let searchInputRef = $state<HTMLInputElement | null>(null);
// Provide search context to child items
setContext('select-search', {
get query() { return searchQuery; }
});
function handleContentKeydown(event: KeyboardEvent) {
if (!searchable || !searchInputRef) return;
if (event.ctrlKey || event.metaKey || event.altKey) return;
if (event.key.length === 1) {
searchInputRef.focus();
}
}
</script>
<SelectPrimitive.Portal {...portalProps}>
@@ -21,6 +44,7 @@
bind:ref
{sideOffset}
data-slot="select-content"
onkeydown={handleContentKeydown}
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-select-content-available-height) origin-(--bits-select-content-transform-origin) relative z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
@@ -28,6 +52,22 @@
{...restProps}
>
<SelectScrollUpButton />
{#if searchable}
<div class="border-input sticky top-0 z-10 bg-popover px-2 py-1.5">
<input
type="text"
bind:this={searchInputRef}
bind:value={searchQuery}
placeholder={searchPlaceholder}
autofocus={autoFocusSearch}
class="placeholder:text-muted-foreground flex h-8 w-full rounded-md border border-input bg-background px-3 py-1 text-sm outline-none focus:border-ring focus:ring-1 focus:ring-ring"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
/>
</div>
{/if}
<SelectPrimitive.Viewport
class={cn(
"h-(--bits-select-anchor-height) min-w-(--bits-select-anchor-width) w-full scroll-my-1 p-1"

View File

@@ -2,39 +2,56 @@
import CheckIcon from "@lucide/svelte/icons/check";
import { Select as SelectPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "$lib/utils.js";
import { getContext } from 'svelte';
let {
ref = $bindable(null),
class: className,
value,
label,
searchText,
children: childrenProp,
...restProps
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
}: WithoutChild<SelectPrimitive.ItemProps> & { searchText?: string } = $props();
// Get search context
const searchContext = getContext<{ query: string } | undefined>('select-search');
// Determine if item should be visible based on search
const isVisible = $derived(() => {
if (!searchContext) return true;
const query = searchContext.query.toLowerCase();
if (!query) return true;
const itemLabel = (searchText || label || value || '').toString().toLowerCase();
return itemLabel.includes(query);
});
</script>
<SelectPrimitive.Item
bind:ref
{value}
data-slot="select-item"
tabindex="0"
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
className
)}
{...restProps}
>
{#snippet children({ selected, highlighted })}
<span class="absolute right-2 flex size-3.5 items-center justify-center">
{#if selected}
<CheckIcon class="size-4" />
{#if isVisible()}
<SelectPrimitive.Item
bind:ref
{value}
data-slot="select-item"
tabindex="0"
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
className
)}
{...restProps}
>
{#snippet children({ selected, highlighted })}
<span class="absolute right-2 flex size-3.5 items-center justify-center">
{#if selected}
<CheckIcon class="size-4" />
{/if}
</span>
{#if childrenProp}
{@render childrenProp({ selected, highlighted })}
{:else}
{label || value}
{/if}
</span>
{#if childrenProp}
{@render childrenProp({ selected, highlighted })}
{:else}
{label || value}
{/if}
{/snippet}
</SelectPrimitive.Item>
{/snippet}
</SelectPrimitive.Item>
{/if}

View File

@@ -0,0 +1,157 @@
<script lang="ts">
import { Combobox as ComboboxPrimitive } from 'bits-ui';
import { ChevronDown, X } from 'lucide-svelte';
import { cn } from '$lib/utils.js';
interface Item {
value: string;
label: string;
}
let {
items = [],
value = $bindable(''),
onValueChange,
placeholder = 'Seleccionar...',
searchPlaceholder = 'Buscar...',
emptyMessage = 'No se encontraron resultados',
class: className = '',
disabled = false
}: {
items: Item[];
value?: string;
onValueChange?: (value: string) => void;
placeholder?: string;
searchPlaceholder?: string;
emptyMessage?: string;
class?: string;
disabled?: boolean;
} = $props();
let searchQuery = $state('');
let open = $state(false);
let isTyping = $state(false);
let containerWidth = $state(0);
// Get selected item label
const selectedLabel = $derived((items ?? []).find((item) => item.value === value)?.label || '');
// Filter items based on search query
// Show all items unless the user is explicitly typing
const filteredItems = $derived(
!isTyping || searchQuery.trim() === ''
? (items ?? [])
: (items ?? []).filter(
(item) =>
(item.label ?? '').toLowerCase().includes(searchQuery.toLowerCase()) ||
(item.value ?? '').toLowerCase().includes(searchQuery.toLowerCase())
)
);
function handleValueChange(v: string) {
value = v;
onValueChange?.(v);
open = false;
isTyping = false;
}
function handleClear(e: Event) {
e.stopPropagation();
value = '';
onValueChange?.('');
searchQuery = '';
isTyping = false;
}
function handleInput() {
isTyping = true;
}
function handleFocus() {
if (!open) {
open = true;
isTyping = false; // Show all items on initial focus
}
}
// Sincronizar el input con el valor seleccionado cuando se cierra
$effect(() => {
if (!open) {
searchQuery = selectedLabel;
isTyping = false;
}
});
// Sincronización inicial o cambio externo
$effect(() => {
if (!open && value) {
searchQuery = selectedLabel;
}
});
</script>
<ComboboxPrimitive.Root bind:open {disabled} onValueChange={handleValueChange}>
<div class="relative {className}" bind:clientWidth={containerWidth}>
<div class="relative">
<ComboboxPrimitive.Input
bind:value={searchQuery}
{placeholder}
aria-label={placeholder}
class={cn(
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 shadow-xs flex h-9 w-full items-center rounded-md border bg-transparent px-3 py-2 pr-24 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
!value && !searchQuery && 'text-muted-foreground'
)}
oninput={handleInput}
onfocus={handleFocus}
/>
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
{#if value && !disabled}
<button
type="button"
tabindex="-1"
onclick={handleClear}
aria-label="Limpiar selección"
class="hover:bg-accent focus-visible:bg-accent rounded-sm p-1 transition-colors outline-none"
>
<X class="size-4" />
</button>
{/if}
<ComboboxPrimitive.Trigger
tabindex="-1"
aria-label="Abrir opciones"
class="hover:bg-accent focus-visible:bg-accent rounded-sm p-1 transition-colors outline-none"
>
<ChevronDown class="size-4 opacity-50" />
</ComboboxPrimitive.Trigger>
</div>
</div>
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Content
class={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[300px] overflow-y-auto rounded-md border p-1 shadow-md outline-none'
)}
style="width: {containerWidth > 0 ? containerWidth + 'px' : '300px'}"
sideOffset={4}
>
{#if filteredItems.length === 0}
<div class="text-muted-foreground py-6 text-center text-sm italic">
{emptyMessage}
</div>
{:else}
{#each filteredItems as item (item.value)}
<ComboboxPrimitive.Item
value={item.value}
label={item.label}
class={cn(
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50'
)}
>
<span class="flex-1 truncate">{item.label}</span>
</ComboboxPrimitive.Item>
{/each}
{/if}
</ComboboxPrimitive.Content>
</ComboboxPrimitive.Portal>
</div>
</ComboboxPrimitive.Root>

View File

@@ -58,30 +58,46 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
// Si el ID es "new", es una creación
if (params.id === 'new') {
const [pedimentoCodesResponse, customsSectionsResponse, customsBrokersResponse, clientsResponse, codePedimentoRegimensResponse] = await Promise.all([
pedimentoCodesPromise,
customsSectionsPromise,
customsBrokersPromise,
clientsPromise,
codePedimentoRegimensPromise
]);
try {
const [pedimentoCodesResponse, customsSectionsResponse, customsBrokersResponse, clientsResponse, codePedimentoRegimensResponse] = await Promise.all([
pedimentoCodesPromise,
customsSectionsPromise,
customsBrokersPromise,
clientsPromise,
codePedimentoRegimensPromise
]);
const pedimentoCodes = pedimentoCodesResponse.ok ? await pedimentoCodesResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
const pedimentoCodes = pedimentoCodesResponse.ok ? await pedimentoCodesResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
return {
pedimento: null,
pedimentoId: null,
isCreate: true,
pedimentoCodes: pedimentoCodes.items || [],
customsSections: customsSections.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || []
};
return {
pedimento: null,
pedimentoId: null,
isCreate: true,
pedimentoCodes: pedimentoCodes.items || [],
customsSections: customsSections.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || []
};
} catch (e) {
console.error('❌ Error loading new pedimento data:', e);
// Graceful degradation: return empty data so the page loads, but show error
return {
pedimento: null,
pedimentoId: null,
isCreate: true,
pedimentoCodes: [],
customsSections: [],
customsBrokers: [],
clients: [],
codePedimentoRegimens: [],
error: 'Error al cargar catálogos. Verifique la conexión con el backend.'
};
}
}
const pedimentoId = parseInt(params.id);

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { goto, invalidateAll } from '$app/navigation';
import * as Tabs from '$lib/components/ui/tabs';
import * as Alert from '$lib/components/ui/alert';
import { Button } from '$lib/components/ui/button';
import { toast } from 'svelte-sonner';
import { Badge } from '$lib/components/ui/badge';
import { Separator } from '$lib/components/ui/separator';
import {
@@ -43,13 +43,17 @@
import ValidationTabForm from '$lib/components/dashboard/pedimentos/edit/validation-tab-form.svelte';
// Importar solo la API de pedimentos
import { pedimentosApi, type CreatePedimentoData, type UpdatePedimentoData } from '$lib/api/dashboard/a76/pedimentos';
import {
pedimentosApi,
type CreatePedimentoData,
type UpdatePedimentoData
} from '$lib/api/dashboard/a76/pedimentos';
import type { PedimentoCode } from '$lib/api/dashboard/reference_data/pedimento_codes';
import type { CustomsSection } from '$lib/api/dashboard/reference_data/customs_sections';
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
import type { CodePedimentoRegimen } from '$lib/api/dashboard/reference_data/code_pedimento_regimens';
// Get sidebar context
const sidebar = useSidebar();
@@ -83,8 +87,6 @@
});
let generalTabInstance = $state<any>(null); // Todavía útil para otras cosas
let saving = $state(false);
let error = $state<string | null>(null);
let success = $state(false);
useShortcuts(
'Pedimento Edit Main Tabs',
@@ -122,6 +124,8 @@
// ID del pedimento
let pedimentoId = $state<number | null>(data.pedimentoId ?? null);
const pedimento = data.pedimento;
// Referencias a los componentes de formulario para obtener sus datos
let generalFormData = $state<any>(null);
let observacionesFormData = $state<any>(null);
@@ -323,8 +327,6 @@
async function handleSaveAll() {
saving = true;
error = null;
success = false;
try {
// Verificar tipo de cambio antes de guardar si hay instancia del tab general y hay fecha de pago
@@ -829,14 +831,15 @@
}
}
success = true;
setTimeout(() => {
success = false;
}, 3000);
toast.success(
isCreating
? 'Pedimento creado exitosamente'
: 'Todos los cambios se guardaron correctamente'
);
} catch (e) {
if (e instanceof Error) {
if (e.message.includes('401')) {
error = 'Sesión expirada. Recargando página...';
toast.error('Sesión expirada. Recargando página...');
setTimeout(() => {
window.location.reload();
}, 1500);
@@ -862,7 +865,6 @@
) {
// Interceptar error de tipo de cambio
console.log('Interceptor: Exchange rate missing error caught (Pedimento).');
error = null;
const dateMatch = errorStr.match(/(\d{4}-\d{2}-\d{2})/);
const missingDate = dateMatch ? dateMatch[0] : generalFormData?.payment_date || '';
@@ -874,22 +876,24 @@
}
// Fallback normal: intentar mostrar algo legible
let displayError = '';
if (errorStr.includes('{')) {
// Si parece JSON, intentar formatearlo un poco o mostrar mensaje genérico
try {
const errObj = JSON.parse(errorStr);
// Si es del formato {"field": ["msg"]}
const values = Object.values(errObj).flat();
error = values.join(', ');
displayError = values.join(', ');
} catch {
error = 'Error al guardar (ver consola)';
displayError = 'Error al guardar (ver consola)';
}
} else {
error = errorStr;
displayError = errorStr;
}
toast.error(displayError);
}
} else {
error = 'Error al guardar los cambios';
toast.error('Error al guardar los cambios');
}
console.error('Error saving all:', e);
} finally {
@@ -910,7 +914,7 @@
{#if data.isCreate}
Nuevo Pedimento
{:else}
Pedimento #{data.pedimento.id}
Pedimento #{data.pedimento?.id ?? ''}
{/if}
</h1>
{#if data.isCreate}
@@ -926,8 +930,8 @@
{#if generalFormData?.year && generalFormData?.customs_office && generalFormData?.license && generalFormData?.pedimento_number}
Número: {generalFormData.year}-{generalFormData.customs_office}-{generalFormData.license}-{generalFormData.pedimento_number}
{:else if !data.isCreate && data.pedimento?.pedimento_number}
Número: {data.pedimento.year}-{data.pedimento.customs_office}-{data.pedimento
.license}-{data.pedimento.pedimento_number}
Número: {data.pedimento?.year ?? ''}-{data.pedimento?.customs_office ?? ''}-{data
.pedimento?.license ?? ''}-{data.pedimento?.pedimento_number ?? ''}
{:else}
Edita los detalles del pedimento
{/if}
@@ -937,23 +941,6 @@
<Separator />
<!-- Alertas globales -->
{#if error}
<Alert.Root variant="destructive">
<CircleAlert size={16} />
<Alert.Title>Error</Alert.Title>
<Alert.Description>{error}</Alert.Description>
</Alert.Root>
{/if}
{#if success}
<Alert.Root>
<CircleCheck size={16} />
<Alert.Title>Éxito</Alert.Title>
<Alert.Description>Todos los cambios se guardaron correctamente</Alert.Description>
</Alert.Root>
{/if}
<!-- Tabs con footer fijo -->
<Tabs.Root
value={activeTab}
@@ -963,7 +950,7 @@
class="space-y-4"
>
<!-- Tab content with bottom padding for floating footer -->
<div id="main-form-content" class="pb-56">
<div id="main-form-content" class="pb-24">
<Tabs.Content value="general">
<GeneralTabForm
bind:this={generalTabInstance}
@@ -991,7 +978,7 @@
generalFormData?.pedimento_number
? `${generalFormData.year}-${generalFormData.customs_office}-${generalFormData.license}-${generalFormData.pedimento_number}`
: !data.isCreate && data.pedimento?.pedimento_number
? `${data.pedimento.year}-${data.pedimento.customs_office}-${data.pedimento.license}-${data.pedimento.pedimento_number}`
? `${data.pedimento?.year ?? ''}-${data.pedimento?.customs_office ?? ''}-${data.pedimento?.license ?? ''}-${data.pedimento?.pedimento_number ?? ''}`
: ''}
/>
</Tabs.Content>