feature/continuacion-vista-tablas-location

This commit is contained in:
hreyes
2026-03-12 16:14:34 -06:00
parent 678f91ecdc
commit 8d304fb98b
23 changed files with 1337 additions and 717 deletions

View File

@@ -1,64 +1,84 @@
import type { PaginatedResponse } from '$lib/types';
import { api } from '$lib/api';
/** Contexto de uso: Fixed Asset (activo fijo) o inventario */
export type LocationSystem = 'fixed_asset' | 'inventory';
export interface Location {
id: number;
location_code: string;
location_description: string | null;
company_id: number;
tenant_id: number;
clave_localizacion: string;
localizacion: string | null;
system: LocationSystem;
}
export interface LocationCreate {
location_code: string;
location_description?: string | null;
clave_localizacion: string;
localizacion?: string | null;
system: LocationSystem;
/** Used when system === 'fixed_asset' */
department?: string | null;
responsible?: string | null;
observations?: string | null;
}
export interface LocationUpdate {
location_description?: string | null;
clave_localizacion?: string;
localizacion?: string | null;
system?: LocationSystem;
}
export interface LocationListResponse extends PaginatedResponse {
export interface LocationListResponse {
items: Location[];
total: number;
page: number;
page_size: number;
}
export interface LocationFilters {
location_code?: string;
location_description?: string;
clave_localizacion?: string;
localizacion?: string;
system?: LocationSystem;
page?: number;
page_size?: number;
}
import { portsApi, PortType } from './ports';
const BASE_URL = '/v1/a76/locations';
function buildQuery(companyId: number, params?: LocationFilters): string {
const search = new URLSearchParams();
search.set('company_id', String(companyId));
if (params?.page != null) search.set('page', String(params.page));
if (params?.page_size != null) search.set('page_size', String(params.page_size));
if (params?.clave_localizacion) search.set('clave_localizacion', params.clave_localizacion);
if (params?.localizacion) search.set('localizacion', params.localizacion);
if (params?.system) search.set('system', params.system);
return search.toString();
}
export async function getLocations(
companyId: number,
filters?: LocationFilters
): Promise<LocationListResponse> {
const res = await portsApi.list(companyId, filters || {});
return (res.data || res) as unknown as LocationListResponse;
const q = buildQuery(companyId, filters);
const res = await api.get<LocationListResponse>(`${BASE_URL}/?${q}`);
return (res.data ?? res) as LocationListResponse;
}
export async function getLocation(
locationId: number,
companyId: number
): Promise<Location> {
const res = await portsApi.get(locationId, companyId);
return (res.data || res) as unknown as Location;
const q = new URLSearchParams({ company_id: String(companyId) });
const res = await api.get<Location>(`${BASE_URL}/${locationId}?${q}`);
return (res.data ?? res) as Location;
}
export async function createLocation(
data: LocationCreate,
companyId: number
): Promise<Location> {
const res = await portsApi.create({
port_code: data.location_code,
location_code: data.location_code,
description: null,
location_description: data.location_description || null,
port_type: PortType.ENTRY
}, companyId);
return (res.data || res) as unknown as Location;
const q = new URLSearchParams({ company_id: String(companyId) });
const res = await api.post<Location>(`${BASE_URL}/?${q}`, data);
return (res.data ?? res) as Location;
}
export async function updateLocation(
@@ -66,15 +86,15 @@ export async function updateLocation(
data: LocationUpdate,
companyId: number
): Promise<Location> {
const res = await portsApi.update(locationId, {
location_description: data.location_description
}, companyId);
return (res.data || res) as unknown as Location;
const q = new URLSearchParams({ company_id: String(companyId) });
const res = await api.put<Location>(`${BASE_URL}/${locationId}/?${q}`, data);
return (res.data ?? res) as Location;
}
export async function deleteLocation(
locationId: number,
companyId: number
): Promise<void> {
await portsApi.delete(locationId, companyId);
}
const q = new URLSearchParams({ company_id: String(companyId) });
await api.delete(`${BASE_URL}/${locationId}?${q}`);
}

View File

@@ -6,7 +6,8 @@
import {
createLocation,
updateLocation,
type Location
type Location,
type LocationSystem
} from '$lib/api/dashboard/a76/general_catalogs/locations';
import { companyStore } from '$lib/stores/company.svelte';
import { obtenerAtajosFormularioLocalidades } from '$lib/config/shortcuts/dashboard/general_catalogs/locations/edit';
@@ -21,14 +22,18 @@
onSuccess?: () => void;
} = $props();
// Atajos
const isEdit = $derived(!!item);
const title = $derived(isEdit ? 'Editar Ubicación' : 'Nueva Ubicación');
const systemOptions: { value: LocationSystem; label: string }[] = [
{ value: 'fixed_asset', label: 'Activo fijo (FA)' },
{ value: 'inventory', label: 'Inventario' }
];
let formData = $state({
location_code: '',
location_description: ''
clave_localizacion: '',
localizacion: '',
system: 'fixed_asset' as LocationSystem
});
let loading = $state(false);
@@ -38,11 +43,16 @@
if (open) {
if (item) {
formData = {
location_code: item.location_code || '',
location_description: item.location_description || ''
clave_localizacion: item.clave_localizacion ?? '',
localizacion: item.localizacion ?? '',
system: item.system ?? 'fixed_asset'
};
} else {
formData = { location_code: '', location_description: '' };
formData = {
clave_localizacion: '',
localizacion: '',
system: 'fixed_asset'
};
}
error = null;
}
@@ -55,20 +65,31 @@
const companyId = companyStore.activeCompany?.id;
if (!companyId) throw new Error('No hay una compañía seleccionada');
if (!formData.location_code.trim()) throw new Error('El código es requerido');
if (!formData.clave_localizacion.trim()) throw new Error('La clave es requerida');
const basePayload = {
location_description: formData.location_description?.trim() || null
localizacion: formData.localizacion?.trim() || null
};
if (isEdit && item) {
await updateLocation(item.id, basePayload, companyId);
await updateLocation(
item.id,
{
...basePayload,
clave_localizacion: formData.clave_localizacion.trim(),
system: formData.system
},
companyId
);
} else {
const createPayload = {
location_code: formData.location_code.trim(),
...basePayload
};
await createLocation(createPayload, companyId);
await createLocation(
{
clave_localizacion: formData.clave_localizacion.trim(),
...basePayload,
system: formData.system
},
companyId
);
}
open = false;
@@ -107,12 +128,12 @@
<div class="grid gap-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="location_code" class="text-right">Código *</Label>
<Label for="clave_localizacion" class="text-right">Clave *</Label>
<div class="col-span-3">
<Input
id="location_code"
bind:value={formData.location_code}
maxlength={4}
id="clave_localizacion"
bind:value={formData.clave_localizacion}
maxlength={20}
disabled={loading || isEdit}
required
/>
@@ -120,16 +141,32 @@
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="location_description" class="text-right">Descripción</Label>
<Label for="localizacion" class="text-right">Localización</Label>
<div class="col-span-3">
<Input
id="location_description"
bind:value={formData.location_description}
maxlength={20}
id="localizacion"
bind:value={formData.localizacion}
maxlength={200}
disabled={loading}
/>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="system" class="text-right">Sistema</Label>
<div class="col-span-3">
<select
id="system"
bind:value={formData.system}
disabled={loading}
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{#each systemOptions as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
</div>
</div>
</div>
<Dialog.Footer>

View File

@@ -10,10 +10,12 @@
let {
open = $bindable(false),
regimen = 'Temporal',
operationType = 'imp' as 'imp' | 'exp',
onSelect
}: {
open: boolean;
regimen: string;
regimen?: string;
operationType?: 'imp' | 'exp';
onSelect: (invoice: Invoice) => void;
} = $props();
@@ -25,15 +27,16 @@
if (!companyStore.activeCompany) return;
loading = true;
try {
let filters: any = {
operation_type: 'imp',
invoice_number: searchTerm
const filters: any = {
operation_type: operationType,
invoice_number: searchTerm || undefined
};
if (regimen === 'Temporal' || regimen === 'TEMPORAL SCAF') {
filters.invoice_type = 'TEM';
} else if (regimen === 'Definitiva' || regimen === 'DEFINITIVO SCAF') {
filters.invoice_type = 'DEF';
if (operationType === 'imp' && regimen) {
if (regimen === 'Temporal' || regimen === 'TEMPORAL SCAF') {
filters.invoice_type = 'TEM';
} else if (regimen === 'Definitiva' || regimen === 'DEFINITIVO SCAF') {
filters.invoice_type = 'DEF';
}
}
const res = await invoicesApi.list(companyStore.activeCompany.id, 1, 50, filters);
@@ -60,12 +63,18 @@
});
</script>
<Dialog.Root bind:open>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title>Seleccionar Factura ({regimen})</Dialog.Title>
<Dialog.Title>
{operationType === 'exp' ? 'Seleccionar Factura de Exportación' : `Seleccionar Factura (${regimen})`}
</Dialog.Title>
<Dialog.Description>
Busca y selecciona una factura del catálogo de importación para el régimen {regimen}.
{#if operationType === 'exp'}
Busca y selecciona una factura del catálogo de exportación.
{:else}
Busca y selecciona una factura del catálogo de importación para el régimen {regimen}.
{/if}
</Dialog.Description>
</Dialog.Header>

View File

@@ -1,13 +1,21 @@
<script lang="ts">
import * as Sheet from '$lib/components/ui/sheet';
import * as Tabs from '$lib/components/ui/tabs';
import * as Select from '$lib/components/ui/select';
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { RadioGroup, RadioGroupItem } from '$lib/components/ui/radio-group';
import { Separator } from '$lib/components/ui/separator';
import { Loader2, Package, Save, X, FileText } from 'lucide-svelte';
import { Loader2, Package, Save, X, FileText, Folder } from 'lucide-svelte';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import type { Item } from '$lib/api/dashboard/a76/items';
import { invoicesApi } from '$lib/api/dashboard/a76/invoices';
import { itemsApi, type Item } from '$lib/api/dashboard/a76/items';
import { companyStore } from '$lib/stores/company.svelte';
// Child components
import InvoiceSelectorModal from '../../InvoiceSelectorModal.svelte';
import MainData from './main-data.svelte';
import ItemConfiguration from './item-configuration.svelte';
import PackagesSection from './packages-section.svelte';
@@ -63,15 +71,64 @@
}
});
const visibility = $derived.by(() => getVisibility(invoiceType ?? invoice?.invoice_type, operationType ?? invoice?.operation_type));
const showCrTrackingBlock = $derived.by(() => {
const normalizedOperationType = operationType ?? invoice?.operation_type;
if (normalizedOperationType === 1 || normalizedOperationType === 'exp') {
return false;
}
// Selectores de factura (FK): estado y líneas
let showImportInvoiceModal = $state(false);
let showExportInvoiceModal = $state(false);
let showExportLinePicker = $state(false);
let showImportLinePicker = $state(false);
let selectedImportInvoiceId = $state<number | null>(null);
let selectedExportInvoiceId = $state<number | null>(null);
let importInvoiceLines = $state<Item[]>([]);
let exportInvoiceLines = $state<Item[]>([]);
let loadingImportLines = $state(false);
let loadingExportLines = $state(false);
return visibility.showCrTrackingHeader;
});
async function loadImportLines(invoiceId: number) {
const companyId = companyStore?.activeCompany?.id;
if (!companyId) return;
loadingImportLines = true;
try {
const res = await itemsApi.listByInvoice(invoiceId, companyId);
importInvoiceLines = res.data?.items ?? [];
} catch {
importInvoiceLines = [];
} finally {
loadingImportLines = false;
}
}
async function loadExportLines(invoiceId: number) {
const companyId = companyStore?.activeCompany?.id;
if (!companyId) return;
loadingExportLines = true;
try {
const res = await itemsApi.listByInvoice(invoiceId, companyId);
exportInvoiceLines = res.data?.items ?? [];
} catch {
exportInvoiceLines = [];
} finally {
loadingExportLines = false;
}
}
function handleSelectImportInvoice(inv: Invoice) {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_invoice = inv.invoice_number ?? '';
editingItem.fa_data.movement_type_import = (inv.invoice_type as string) || 'TEM';
editingItem.fa_data.search_line = undefined;
selectedImportInvoiceId = inv.id ?? null;
if (selectedImportInvoiceId) loadImportLines(selectedImportInvoiceId);
}
function handleSelectExportInvoice(inv: Invoice) {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_invoice = inv.invoice_number ?? '';
editingItem.fa_data.search_line = undefined;
selectedExportInvoiceId = inv.id ?? null;
if (selectedExportInvoiceId) loadExportLines(selectedExportInvoiceId);
}
const visibility = $derived.by(() => getVisibility(invoiceType ?? invoice?.invoice_type, operationType ?? invoice?.operation_type));
/** Show link-to-import block for import (CR tracking) or for export when showExportLinkToImportBlock. */
const showLinkToImportBlock = $derived.by(() => {
const normalizedOperationType = operationType ?? invoice?.operation_type;
@@ -80,16 +137,81 @@
}
return visibility.showCrTrackingHeader;
});
const isExport = $derived.by(() => {
const op = operationType ?? invoice?.operation_type;
return op === 1 || op === 'exp';
});
/** Show repair-import block (Factura de Expo / Línea de Expo) for REP/REPAR. */
const showRepairBlock = $derived.by(() => {
const op = operationType ?? invoice?.operation_type;
if (op === 1 || op === 'exp') return false;
return visibility.showRepairLinkToExportBlock;
});
// Resolver invoice id desde search_invoice al abrir el sheet (para cargar líneas)
$effect(() => {
if (!open || !companyStore?.activeCompany?.id || !editingItem?.fa_data) return;
const num = editingItem.fa_data.search_invoice;
const movementType = editingItem.fa_data.movement_type_import;
if (showLinkToImportBlock && num && !selectedImportInvoiceId && !loadingImportLines) {
(async () => {
try {
const res = await invoicesApi.list(companyStore.activeCompany!.id, 1, 5, {
operation_type: 'imp',
invoice_number: num,
invoice_type: movementType === 'DEF' ? 'DEF' : 'TEM'
});
const items = res.data?.items ?? [];
const inv = items.find((i: Invoice) => i.invoice_number === num);
if (inv?.id) {
selectedImportInvoiceId = inv.id;
await loadImportLines(inv.id);
}
} catch {
// ignore
}
})();
}
if (showRepairBlock && num && !selectedExportInvoiceId && !loadingExportLines) {
(async () => {
try {
const res = await invoicesApi.list(companyStore.activeCompany!.id, 1, 5, {
operation_type: 'exp',
invoice_number: num
});
const items = res.data?.items ?? [];
const inv = items.find((i: Invoice) => i.invoice_number === num);
if (inv?.id) {
selectedExportInvoiceId = inv.id;
await loadExportLines(inv.id);
}
} catch {
// ignore
}
})();
}
});
$effect(() => {
if (!open) {
selectedImportInvoiceId = null;
selectedExportInvoiceId = null;
importInvoiceLines = [];
exportInvoiceLines = [];
}
});
const importRegimenLabel = $derived(
(editingItem.fa_data?.movement_type_import?.toUpperCase() === 'DEF' ? 'Definitiva' : 'Temporal')
);
const showCrTrackingBlock = $derived.by(() => {
const normalizedOperationType = operationType ?? invoice?.operation_type;
if (normalizedOperationType === 1 || normalizedOperationType === 'exp') {
return false;
}
return visibility.showCrTrackingHeader;
});
const isExport = $derived.by(() => {
const op = operationType ?? invoice?.operation_type;
return op === 1 || op === 'exp';
});
const visibleTabs = $derived.by(() => [
{ value: 'generales', label: 'General', visible: true },
{ value: 'continuacion', label: 'Continuación', visible: true },
@@ -137,114 +259,207 @@
{#if line}
{#if showRepairBlock}
<!-- Importación de Reparación: Genera Descarga? + Factura de Expo + Línea de Expo -->
<div class="space-y-2 rounded border border-zinc-200 bg-white p-2 dark:border-zinc-800 dark:bg-zinc-900">
<div class="flex items-center gap-4">
<span class="text-[10px] font-semibold uppercase tracking-wide text-zinc-500">Genera Descarga?</span>
<label class="flex items-center gap-1.5">
<input type="radio" name="fa_rep_download" value="si" checked={editingItem.fa_data?.download === true} onchange={() => (editingItem.fa_data = editingItem.fa_data || {}, editingItem.fa_data.download = true)} class="rounded border-input" />
<span class="text-xs"></span>
</label>
<label class="flex items-center gap-1.5">
<input type="radio" name="fa_rep_download" value="no" checked={editingItem.fa_data?.download === false || editingItem.fa_data?.download === undefined} onchange={() => (editingItem.fa_data = editingItem.fa_data || {}, editingItem.fa_data.download = false)} class="rounded border-input" />
<span class="text-xs">No</span>
</label>
<div class="space-y-3 rounded-md border border-zinc-200 bg-white p-3 dark:border-zinc-800 dark:bg-zinc-900">
<div class="space-y-1.5">
<Label class="text-xs font-medium text-muted-foreground">Genera Descarga?</Label>
<RadioGroup
value={editingItem.fa_data?.download === true ? 'si' : 'no'}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.download = v === 'si';
}}
class="flex gap-4"
>
<div class="flex items-center gap-2">
<RadioGroupItem value="si" id="fa_rep_download_si" />
<Label for="fa_rep_download_si" class="text-xs cursor-pointer"></Label>
</div>
<div class="flex items-center gap-2">
<RadioGroupItem value="no" id="fa_rep_download_no" />
<Label for="fa_rep_download_no" class="text-xs cursor-pointer">No</Label>
</div>
</RadioGroup>
</div>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-4">
<div class="rounded border border-zinc-200 bg-white p-2 dark:border-zinc-800 dark:bg-zinc-900">
<label for="fa_rep_search_invoice" class="mb-1 block text-[10px] font-semibold uppercase tracking-wide text-zinc-500">
Factura de Expo
</label>
<input
id="fa_rep_search_invoice"
type="text"
bind:value={editingItem.fa_data.search_invoice}
class="flex h-8 w-full rounded-md border border-input bg-background px-3 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
<!-- Fila ligada: selector de factura (FK) + línea (FK), estilo Input + Folder -->
<div class="flex flex-wrap items-end gap-3">
<div class="min-w-[120px] flex-1 space-y-1">
<Label class="text-xs">Factura de Expo</Label>
<div class="flex gap-1">
<Input
readonly
value={editingItem.fa_data?.search_invoice || ''}
class="h-8 flex-1 bg-muted cursor-pointer text-sm"
placeholder="Seleccionar factura..."
onclick={() => (showExportInvoiceModal = true)}
/>
<Button
type="button"
variant="outline"
size="icon"
class="h-8 w-8 shrink-0"
onclick={() => (showExportInvoiceModal = true)}
>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="rounded border border-zinc-200 bg-white p-2 dark:border-zinc-800 dark:bg-zinc-900">
<label for="fa_rep_search_line" class="mb-1 block text-[10px] font-semibold uppercase tracking-wide text-zinc-500">
Línea de Expo
</label>
<input
id="fa_rep_search_line"
type="number"
min="0"
bind:value={editingItem.fa_data.search_line}
class="flex h-8 w-full rounded-md border border-input bg-background px-3 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
<div class="min-w-[90px] flex-1 space-y-1">
<Label for="fa_rep_search_line" class="text-xs">Línea de Expo</Label>
<div class="flex gap-1">
<Input
id="fa_rep_search_line"
readonly
value={editingItem.fa_data?.search_line != null ? String(editingItem.fa_data.search_line) : ''}
class="h-8 flex-1 bg-muted cursor-pointer text-sm"
placeholder={loadingExportLines ? 'Cargando...' : 'Línea'}
disabled={!selectedExportInvoiceId || loadingExportLines}
onclick={() => selectedExportInvoiceId && !loadingExportLines && (showExportLinePicker = true)}
/>
<Button
type="button"
variant="outline"
size="icon"
class="h-8 w-8 shrink-0"
disabled={!selectedExportInvoiceId || loadingExportLines}
onclick={() => (showExportLinePicker = true)}
>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="rounded border border-zinc-200 bg-white p-2 dark:border-zinc-800 dark:bg-zinc-900">
<label for="fa_rep_search_type" class="mb-1 block text-[10px] font-semibold uppercase tracking-wide text-zinc-500">
Tipo Búsqueda
</label>
<input
id="fa_rep_search_type"
type="text"
bind:value={editingItem.fa_data.search_type}
class="flex h-8 w-full rounded-md border border-input bg-background px-3 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
<div class="min-w-[100px] flex-1 space-y-1">
<Label for="fa_rep_search_type" class="text-xs">Tipo Búsqueda</Label>
<Select.Root
type="single"
value={editingItem.fa_data?.search_type || ''}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_type = v ?? undefined;
}}
>
<Select.Trigger id="fa_rep_search_type" class="h-8 text-sm">
<span class="truncate">
{editingItem.fa_data?.search_type || 'Seleccionar...'}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="Factura">Factura</Select.Item>
<Select.Item value="NumParte">NumParte</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
</div>
{:else if showLinkToImportBlock}
<div class="space-y-2 rounded border border-zinc-200 bg-white p-2 dark:border-zinc-800 dark:bg-zinc-900">
<!-- Genera Descarga? visible when block is shown (expo or import CR per legacy). -->
<div class="flex items-center gap-4">
<span class="text-[10px] font-semibold uppercase tracking-wide text-zinc-500">Genera Descarga?</span>
<label class="flex items-center gap-1.5">
<input type="radio" name="fa_download" value="si" checked={editingItem.fa_data?.download === true} onchange={() => (editingItem.fa_data = editingItem.fa_data || {}, editingItem.fa_data.download = true)} class="rounded border-input" />
<span class="text-xs"></span>
</label>
<label class="flex items-center gap-1.5">
<input type="radio" name="fa_download" value="no" checked={editingItem.fa_data?.download === false || editingItem.fa_data?.download === undefined} onchange={() => (editingItem.fa_data = editingItem.fa_data || {}, editingItem.fa_data.download = false)} class="rounded border-input" />
<span class="text-xs">No</span>
</label>
<div class="space-y-3 rounded-md border border-zinc-200 bg-white p-3 dark:border-zinc-800 dark:bg-zinc-900">
<div class="space-y-1.5">
<Label class="text-xs font-medium text-muted-foreground">Genera Descarga?</Label>
<RadioGroup
value={editingItem.fa_data?.download === true ? 'si' : 'no'}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.download = v === 'si';
}}
class="flex gap-4"
>
<div class="flex items-center gap-2">
<RadioGroupItem value="si" id="fa_download_si" />
<Label for="fa_download_si" class="text-xs cursor-pointer"></Label>
</div>
<div class="flex items-center gap-2">
<RadioGroupItem value="no" id="fa_download_no" />
<Label for="fa_download_no" class="text-xs cursor-pointer">No</Label>
</div>
</RadioGroup>
</div>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-4">
<div class="rounded border border-zinc-200 bg-white p-2 dark:border-zinc-800 dark:bg-zinc-900">
<label for="fa_search_invoice" class="mb-1 block text-[10px] font-semibold uppercase tracking-wide text-zinc-500">
Factura Impo
</label>
<input
id="fa_search_invoice"
type="text"
bind:value={editingItem.fa_data.search_invoice}
class="flex h-8 w-full rounded-md border border-input bg-background px-3 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
<!-- Fila ligada: selector de factura (FK) + línea (FK) -->
<div class="flex flex-wrap items-end gap-3">
<div class="min-w-[100px] flex-1 space-y-1">
<Label class="text-xs">Tipo Importación</Label>
<Select.Root
type="single"
value={editingItem.fa_data?.movement_type_import || 'TEM'}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.movement_type_import = v || 'TEM';
selectedImportInvoiceId = null;
importInvoiceLines = [];
}}
>
<Select.Trigger class="h-8 text-sm">
<span>{editingItem.fa_data?.movement_type_import === 'DEF' ? 'DEF' : 'TEM'}</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="TEM">TEM (Temporal)</Select.Item>
<Select.Item value="DEF">DEF (Definitiva)</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="rounded border border-zinc-200 bg-white p-2 dark:border-zinc-800 dark:bg-zinc-900">
<label for="fa_search_line" class="mb-1 block text-[10px] font-semibold uppercase tracking-wide text-zinc-500">
Línea
</label>
<input
id="fa_search_line"
type="number"
min="0"
bind:value={editingItem.fa_data.search_line}
class="flex h-8 w-full rounded-md border border-input bg-background px-3 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
<div class="min-w-[120px] flex-1 space-y-1">
<Label class="text-xs">Factura Impo</Label>
<div class="flex gap-1">
<Input
readonly
value={editingItem.fa_data?.search_invoice || ''}
class="h-8 flex-1 bg-muted cursor-pointer text-sm"
placeholder="Seleccionar factura..."
onclick={() => (showImportInvoiceModal = true)}
/>
<Button
type="button"
variant="outline"
size="icon"
class="h-8 w-8 shrink-0"
onclick={() => (showImportInvoiceModal = true)}
>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="rounded border border-zinc-200 bg-white p-2 dark:border-zinc-800 dark:bg-zinc-900">
<label for="fa_search_type" class="mb-1 block text-[10px] font-semibold uppercase tracking-wide text-zinc-500">
Tipo Búsqueda
</label>
<input
id="fa_search_type"
type="text"
bind:value={editingItem.fa_data.search_type}
class="flex h-8 w-full rounded-md border border-input bg-background px-3 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
<div class="min-w-[90px] flex-1 space-y-1">
<Label for="fa_search_line" class="text-xs">Línea</Label>
<div class="flex gap-1">
<Input
id="fa_search_line"
readonly
value={editingItem.fa_data?.search_line != null ? String(editingItem.fa_data.search_line) : ''}
class="h-8 flex-1 bg-muted cursor-pointer text-sm"
placeholder={loadingImportLines ? 'Cargando...' : 'Línea'}
disabled={!selectedImportInvoiceId || loadingImportLines}
onclick={() => selectedImportInvoiceId && !loadingImportLines && (showImportLinePicker = true)}
/>
<Button
type="button"
variant="outline"
size="icon"
class="h-8 w-8 shrink-0"
disabled={!selectedImportInvoiceId || loadingImportLines}
onclick={() => (showImportLinePicker = true)}
>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="rounded border border-zinc-200 bg-white p-2 dark:border-zinc-800 dark:bg-zinc-900">
<label for="fa_movement_type_import" class="mb-1 block text-[10px] font-semibold uppercase tracking-wide text-zinc-500">
Tipo Importación / Tipo Movimiento
</label>
<input
id="fa_movement_type_import"
type="text"
bind:value={editingItem.fa_data.movement_type_import}
class="flex h-8 w-full rounded-md border border-input bg-background px-3 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
<div class="min-w-[100px] flex-1 space-y-1">
<Label for="fa_search_type" class="text-xs">Tipo Búsqueda</Label>
<Select.Root
type="single"
value={editingItem.fa_data?.search_type || ''}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_type = v ?? undefined;
}}
>
<Select.Trigger id="fa_search_type" class="h-8 text-sm">
<span class="truncate">
{editingItem.fa_data?.search_type || 'Seleccionar...'}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="Factura">Factura</Select.Item>
<Select.Item value="NumParte">NumParte</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
</div>
@@ -367,4 +582,66 @@
</footer>
</Sheet.Content>
</Sheet.Root>
</Sheet.Root>
<InvoiceSelectorModal
bind:open={showImportInvoiceModal}
regimen={importRegimenLabel}
operationType="imp"
onSelect={handleSelectImportInvoice}
/>
<InvoiceSelectorModal
bind:open={showExportInvoiceModal}
operationType="exp"
onSelect={handleSelectExportInvoice}
/>
<!-- Diálogo para elegir línea (Expo) -->
<Dialog.Root bind:open={showExportLinePicker}>
<Dialog.Content class="max-w-sm">
<Dialog.Header>
<Dialog.Title class="text-sm">Seleccionar línea</Dialog.Title>
</Dialog.Header>
<div class="max-h-[280px] overflow-y-auto py-2">
{#each exportInvoiceLines as lineItem}
{@const num = lineItem.line_number ?? lineItem.id}
<button
type="button"
class="w-full px-3 py-2 text-left text-sm hover:bg-muted rounded-md"
onclick={() => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_line = typeof num === 'number' ? num : undefined;
showExportLinePicker = false;
}}
>
Línea {num}
</button>
{/each}
</div>
</Dialog.Content>
</Dialog.Root>
<!-- Diálogo para elegir línea (Impo) -->
<Dialog.Root bind:open={showImportLinePicker}>
<Dialog.Content class="max-w-sm">
<Dialog.Header>
<Dialog.Title class="text-sm">Seleccionar línea</Dialog.Title>
</Dialog.Header>
<div class="max-h-[280px] overflow-y-auto py-2">
{#each importInvoiceLines as lineItem}
{@const num = lineItem.line_number ?? lineItem.id}
<button
type="button"
class="w-full px-3 py-2 text-left text-sm hover:bg-muted rounded-md"
onclick={() => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_line = typeof num === 'number' ? num : undefined;
showImportLinePicker = false;
}}
>
Línea {num}
</button>
{/each}
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,309 @@
<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 { Label } from '$lib/components/ui/label';
import { Loader2, Search, Plus, ArrowLeft } from 'lucide-svelte';
import { companyStore } from '$lib/stores/company.svelte';
import {
getLocations,
createLocation,
type Location,
type LocationSystem
} from '$lib/api/dashboard/a76/general_catalogs/locations';
let {
open = $bindable(false),
system = 'fixed_asset' as LocationSystem,
onSelect
}: {
open: boolean;
system?: LocationSystem;
onSelect: (location: Location) => void;
} = $props();
let locations: Location[] = $state([]);
let filtered: Location[] = $state([]);
let loading = $state(false);
let searchTerm = $state('');
let error = $state('');
let showRegisterForm = $state(false);
// Register form state
let formData = $state({
clave_localizacion: '',
localizacion: '',
department: '',
responsible: '',
observations: ''
});
let formLoading = $state(false);
let formError = $state('');
async function loadLocations() {
const companyId = companyStore?.activeCompany?.id;
if (!companyId) {
error = 'No hay compañía seleccionada';
return;
}
loading = true;
error = '';
try {
const res = await getLocations(companyId, {
system,
page_size: 500
});
locations = res.items ?? [];
filtered = locations;
} catch (e) {
error = 'Error al cargar ubicaciones';
console.error(e);
locations = [];
filtered = [];
} finally {
loading = false;
}
}
function applyFilter() {
if (!searchTerm.trim()) {
filtered = locations;
} else {
const t = searchTerm.toLowerCase();
filtered = locations.filter(
(loc) =>
(loc.clave_localizacion ?? '').toLowerCase().includes(t) ||
(loc.localizacion ?? '').toLowerCase().includes(t)
);
}
}
function handleSelect(loc: Location) {
onSelect(loc);
open = false;
}
function openRegisterForm() {
showRegisterForm = true;
formData = {
clave_localizacion: '',
localizacion: '',
department: '',
responsible: '',
observations: ''
};
formError = '';
}
function closeRegisterForm() {
showRegisterForm = false;
formError = '';
}
async function handleRegisterSubmit() {
const companyId = companyStore?.activeCompany?.id;
if (!companyId) {
formError = 'No hay compañía seleccionada';
return;
}
if (!formData.clave_localizacion.trim()) {
formError = 'La clave es requerida';
return;
}
formLoading = true;
formError = '';
try {
const created = await createLocation(
{
clave_localizacion: formData.clave_localizacion.trim(),
localizacion: formData.localizacion?.trim() || null,
system,
department: formData.department?.trim() || null,
responsible: formData.responsible?.trim() || null,
observations: formData.observations?.trim() || null
},
companyId
);
onSelect(created);
open = false;
} catch (e) {
formError = e instanceof Error ? e.message : 'Error al guardar';
} finally {
formLoading = false;
}
}
$effect(() => {
if (open && !showRegisterForm) loadLocations();
});
$effect(() => {
searchTerm;
applyFilter();
});
$effect(() => {
if (!open) showRegisterForm = false;
});
</script>
<Dialog.Root bind:open>
<Dialog.Content class="!max-w-[60vw] w-[60vw] max-h-[90vh] p-0 flex flex-col">
<Dialog.Header class="px-6 py-4 border-b">
<Dialog.Title class="text-lg font-semibold">Catálogo de ubicaciones (maquinaria y equipo)</Dialog.Title>
</Dialog.Header>
{#if showRegisterForm}
<!-- Registrar nueva ubicación (formulario) -->
<form
class="flex flex-col flex-1 overflow-hidden"
onsubmit={(e) => {
e.preventDefault();
handleRegisterSubmit();
}}
>
<div class="flex-1 overflow-auto px-6 py-4 space-y-4">
{#if formError}
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
{formError}
</div>
{/if}
<div class="grid gap-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="reg-clave" class="text-right">Clave *</Label>
<div class="col-span-3">
<Input
id="reg-clave"
bind:value={formData.clave_localizacion}
maxlength={20}
placeholder="Clave de localización"
class="h-9"
/>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="reg-localizacion" class="text-right">Localización</Label>
<div class="col-span-3">
<Input
id="reg-localizacion"
bind:value={formData.localizacion}
maxlength={200}
placeholder="Nombre o descripción"
class="h-9"
/>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="reg-department" class="text-right">Departamento</Label>
<div class="col-span-3">
<Input
id="reg-department"
bind:value={formData.department}
maxlength={100}
placeholder="Opcional"
class="h-9"
/>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="reg-responsible" class="text-right">Responsable</Label>
<div class="col-span-3">
<Input
id="reg-responsible"
bind:value={formData.responsible}
maxlength={200}
placeholder="Opcional"
class="h-9"
/>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="reg-observations" class="text-right">Observaciones</Label>
<div class="col-span-3">
<Input
id="reg-observations"
bind:value={formData.observations}
placeholder="Opcional"
class="h-9"
/>
</div>
</div>
</div>
</div>
<div class="px-6 py-4 border-t flex justify-end gap-2">
<Button type="button" variant="outline" onclick={closeRegisterForm} disabled={formLoading}>
<ArrowLeft class="mr-2 h-4 w-4" />
Volver al listado
</Button>
<Button type="submit" disabled={formLoading}>
{#if formLoading}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
{/if}
Guardar
</Button>
</div>
</form>
{:else}
<!-- Listado + búsqueda -->
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900 flex items-center gap-2">
<div class="flex items-center gap-2 flex-1">
<Search class="w-4 h-4 text-zinc-400 shrink-0" />
<Input
bind:value={searchTerm}
placeholder="Buscar por clave o localización..."
class="flex-1 h-9"
/>
</div>
<Button type="button" variant="default" size="sm" onclick={openRegisterForm}>
<Plus class="mr-2 h-4 w-4" />
Registrar nueva ubicación
</Button>
</div>
<div 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">
<tr>
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">Clave</th>
<th class="px-3 py-2 text-left font-semibold">Localización</th>
</tr>
</thead>
<tbody>
{#each filtered as loc}
<tr
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
onclick={() => handleSelect(loc)}
>
<td class="px-3 py-2 border-r">{loc.clave_localizacion ?? '—'}</td>
<td class="px-3 py-2">{loc.localizacion ?? '—'}</td>
</tr>
{/each}
{#if filtered.length === 0}
<tr>
<td colspan="2" class="px-3 py-8 text-center text-zinc-500">
No se encontraron resultados
</td>
</tr>
{/if}
</tbody>
</table>
</div>
{/if}
</div>
<div class="px-6 py-4 border-t flex justify-end gap-2">
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</div>
{/if}
</Dialog.Content>
</Dialog.Root>

View File

@@ -8,6 +8,7 @@
import type { Item, LineDescriptions } from '$lib/api/dashboard/a76/items';
import type { InvoiceItemVisibility } from '$lib/config/invoice-item-visibility';
import PaymentMethodDialog from './payment-method-dialog.svelte';
import LocationSelectorDialog from './location-selector-dialog.svelte';
let {
lineItem = $bindable(),
@@ -51,6 +52,7 @@
let paymentMethodDialogOpen = $state(false);
let payment_method_description = $state('');
let locationSelectorOpen = $state(false);
// Load payment method description when payment_method exists
$effect(() => {
@@ -89,6 +91,10 @@
lineItem.payment_method = method.key;
payment_method_description = method.description;
}
function handleLocationSelect(loc: { clave_localizacion: string; localizacion?: string | null }) {
descriptions.machinery_location = loc.localizacion ?? loc.clave_localizacion ?? '';
}
</script>
<div class="grid grid-cols-1 items-start gap-2 lg:grid-cols-2">
@@ -224,15 +230,27 @@
{/if}
{#if visibility.showContinuationLocation}
<!-- Location -->
<div class="space-y-1">
<!-- Location (catálogo a76, sistema fixed_asset) -->
<div class="space-y-1">
<div class="space-y-0.5">
<Label for="localizacion_maquinaria" class="text-xs">Machinery and equipment location:</Label>
<div class="flex gap-1">
<Input id="localizacion_maquinaria" bind:value={descriptions.machinery_location} class="h-6 text-xs" />
<button
id="localizacion_maquinaria"
type="button"
onclick={() => (locationSelectorOpen = true)}
class="flex h-6 min-w-[120px] flex-1 items-center rounded-md border border-input bg-transparent px-2 text-left text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{descriptions.machinery_location || 'Seleccionar ubicación...'}
</button>
</div>
<Label for="localizacion_maquinaria" class="text-xs">Location variable</Label>
<Label for="localizacion_maquinaria" class="text-xs text-muted-foreground">Location variable</Label>
</div>
<LocationSelectorDialog
bind:open={locationSelectorOpen}
system="fixed_asset"
onSelect={handleLocationSelect}
/>
</div>
{/if}

View File

@@ -3,24 +3,35 @@ import type { Location } from '$lib/api/dashboard/a76/general_catalogs/locations
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
const SYSTEM_LABELS: Record<string, string> = {
fixed_asset: 'Activo fijo (FA)',
inventory: 'Inventario'
};
export function createColumns(onSuccess?: () => void): ColumnDef<Location>[] {
return [
{
accessorKey: 'location_code',
header: 'Código',
},
{
accessorKey: 'location_description',
header: 'Descripción',
cell: ({ row }) => row.original.location_description || '—'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => renderComponent(DataTableActions, {
item: row.original,
onSuccess
})
}
];
return [
{
accessorKey: 'clave_localizacion',
header: 'Clave'
},
{
accessorKey: 'localizacion',
header: 'Localización',
cell: ({ row }) => row.original.localizacion ?? '—'
},
{
accessorKey: 'system',
header: 'Sistema',
cell: ({ row }) => SYSTEM_LABELS[row.original.system] ?? row.original.system
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) =>
renderComponent(DataTableActions, {
item: row.original,
onSuccess
})
}
];
}

View File

@@ -0,0 +1,156 @@
<script lang="ts">
import { createColumns } from '$lib/components/dashboard/locations/columns';
import CreateDialog from '$lib/components/dashboard/general_catalogs/locations/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/locations/data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import {
getLocations,
type Location,
type LocationSystem
} from '$lib/api/dashboard/a76/general_catalogs/locations';
let {
companyId,
system: initialSystem = undefined,
showSystemFilter = true,
compact = false
}: {
companyId: number;
/** Filtro por sistema cuando está embebido (ej. solo fixed_asset o solo inventory). */
system?: LocationSystem;
/** Mostrar selector de sistema (FA / Inventario). Por defecto true. */
showSystemFilter?: boolean;
/** Vista compacta sin título ni descripción. */
compact?: boolean;
} = $props();
let items = $state<Location[]>([]);
let total = $state(0);
let page = $state(1);
let pageSize = $state(50);
let pages = $state(0);
let loading = $state(true);
let dialogOpen = $state(false);
let searchClave = $state('');
let searchLocalizacion = $state('');
let filterSystem = $state<LocationSystem | ''>(initialSystem ?? '');
let timeout: ReturnType<typeof setTimeout>;
async function load() {
if (!companyId) return;
loading = true;
try {
const filters: Record<string, string | number> = {
page,
page_size: pageSize
};
if (searchClave) filters.clave_localizacion = searchClave;
if (searchLocalizacion) filters.localizacion = searchLocalizacion;
if (filterSystem) filters.system = filterSystem;
const res = await getLocations(companyId, filters);
items = res.items ?? [];
total = res.total ?? 0;
page = res.page ?? 1;
pageSize = res.page_size ?? 50;
pages = pageSize > 0 ? Math.ceil(total / pageSize) : 0;
} catch (e) {
console.error('Error loading locations:', e);
items = [];
total = 0;
pages = 0;
} finally {
loading = false;
}
}
function handleSuccess() {
load();
}
function handleSearch() {
clearTimeout(timeout);
timeout = setTimeout(() => {
page = 1;
load();
}, 400);
}
$effect(() => {
void companyId;
void page;
load();
});
</script>
<div class="flex flex-col gap-4">
{#if !compact}
<div class="flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold tracking-tight">Ubicaciones</h2>
<p class="text-sm text-muted-foreground">
Clave y localización por sistema (FA / Inventario)
</p>
</div>
<Button onclick={() => (dialogOpen = true)} size="sm">
<Plus class="mr-2 h-4 w-4" />
Nueva
</Button>
</div>
{:else}
<div class="flex items-center justify-end">
<Button onclick={() => (dialogOpen = true)} size="sm" variant="outline">
<Plus class="mr-2 h-4 w-4" />
Nueva ubicación
</Button>
</div>
{/if}
<div class="flex gap-4 items-end flex-wrap">
<div class="grid w-full max-w-[200px] items-center gap-1.5">
<Input
placeholder="Clave..."
bind:value={searchClave}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-[200px] items-center gap-1.5">
<Input
placeholder="Localización..."
bind:value={searchLocalizacion}
oninput={handleSearch}
/>
</div>
{#if showSystemFilter}
<div class="grid w-full max-w-[180px] items-center gap-1.5">
<select
bind:value={filterSystem}
onchange={handleSearch}
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm"
>
<option value="">Todos</option>
<option value="fixed_asset">Activo fijo (FA)</option>
<option value="inventory">Inventario</option>
</select>
</div>
{/if}
</div>
<div class="rounded-md border">
{#if loading}
<div class="flex items-center justify-center py-12 text-muted-foreground">
Cargando...
</div>
{:else}
<DataTable
data={items}
columns={createColumns(handleSuccess)}
pageCount={pages}
totalItems={total}
/>
{/if}
</div>
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
</div>

View File

@@ -278,10 +278,6 @@ export function getSidebarData(): SidebarData {
title: m["sidebar.general_catalogs.customs_warehouses"](),
url: "/dashboard/reference_data/customs_warehouses",
},
{
title: m["sidebar.general_catalogs.locations"](),
url: "/dashboard/general_catalogs/locations",
},
{
title: m["sidebar.general_catalogs.doda"](),
url: "/dashboard/general_catalogs/doda",

View File

@@ -1,69 +0,0 @@
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
return {
error: 'No authenticated',
locations: { items: [], total: 0, page: 1, page_size: 10, pages: 0 }
};
}
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 50;
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {
error: 'No company selected',
locations: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
};
}
const filters: Record<string, string> = {};
const location_code = url.searchParams.get('location_code');
const location_description = url.searchParams.get('location_description');
if (location_code) filters.location_code = location_code;
if (location_description) filters.location_description = location_description;
try {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
const response = await authenticatedFetch(
`v1/a76/ports/?${queryParams.toString()}`,
{ method: 'GET', cache: 'no-store' },
cookies,
fetch
);
if (!response.ok) {
return {
error: 'Failed to load',
locations: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
};
}
const data = await response.json();
return { locations: data };
} catch (error) {
console.error('Error loading locations:', error);
return {
error: 'Error loading',
locations: { items: [], total: 0, page: 1, page_size: pageSize, pages: 0 }
};
}
};

View File

@@ -1,89 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { createColumns } from '$lib/components/dashboard/locations/columns';
import CreateDialog from '$lib/components/dashboard/general_catalogs/locations/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/locations/data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaLocalidades } from '$lib/config/shortcuts/dashboard/general_catalogs/locations/list';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let dialogOpen = $state(false);
// Atajos
useShortcuts(
'Lista Localidades',
obtenerAtajosListaLocalidades({
manejarNuevo: () => (dialogOpen = true),
manejarActualizar: handleSuccess
})
);
let searchCode = $state($page.url.searchParams.get('location_code') || '');
let searchDesc = $state($page.url.searchParams.get('location_description') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchCode) url.searchParams.set('location_code', searchCode);
else url.searchParams.delete('location_code');
if (searchDesc) url.searchParams.set('location_description', searchDesc);
else url.searchParams.delete('location_description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Ubicaciones</h1>
<p class="text-muted-foreground">Catálogo de ubicaciones de puertos</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nueva Ubicación
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.locations?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.locations?.pages || 0}
totalItems={data.locations?.total || 0}
/>
</div>
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
</div>