feat: Implement API clients and UI components for managing units of measure and locations

- Added API client for OMA units of measure with CRUD operations.
- Created API client for general unit measures with CRUD functionality.
- Developed UI components for displaying and managing locations in a data table format.
- Implemented data table actions for unit measures, including create, edit, and delete functionalities.
- Integrated pagination and refresh capabilities in the data tables for both customs and general unit measures.
- Enhanced user experience with dialogs for creating and editing units of measure.
- Added server-side loading logic for fetching locations and units of measure with error handling.
This commit is contained in:
2025-12-08 17:34:03 -06:00
parent 648197b171
commit e8b8599335
38 changed files with 2060 additions and 225 deletions

View File

@@ -0,0 +1,21 @@
/**
* Índice de exportación para catálogos generales A76
*/
// Unit Measures - Main
export * from './unit-measures';
// Unit Measures - Customs (Mexican)
export * from './um-customs-mex';
// Unit Measures - American
export * from './um-customs-ame';
// Unit Measures - ACE
export * from './um-ace';
// Unit Measures - OMA
export * from './um-oma';
// Locations (from ports)
export * from './locations';

View File

@@ -0,0 +1,52 @@
/**
* API Client para Locations - Ubicaciones relacionadas con puertos
* Basado en los campos location_code y location_description del módulo de puertos
*/
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Location {
location_code: string;
location_description: string | null;
}
/**
* Nota: Las ubicaciones están integradas en el módulo de puertos.
* Este archivo proporciona tipos para trabajar con ubicaciones,
* pero las operaciones se realizan a través del módulo de puertos.
*
* Ver: /a76/ports para operaciones relacionadas con ubicaciones
*/
/**
* Obtiene ubicaciones únicas de los puertos
* Esta función extrae las ubicaciones únicas de la lista de puertos
*/
export async function getLocationsFromPorts(): Promise<ApiResponse<Location[]>> {
const portsResponse = await api.get('/a76/ports?page_size=1000');
if (portsResponse.error || !portsResponse.data) {
return {
error: portsResponse.error || 'Error al obtener puertos',
status: portsResponse.status
};
}
// Extraer ubicaciones únicas
const locationMap = new Map<string, Location>();
const ports = portsResponse.data.items || [];
ports.forEach((port: any) => {
if (port.location_code && !locationMap.has(port.location_code)) {
locationMap.set(port.location_code, {
location_code: port.location_code,
location_description: port.location_description
});
}
});
return {
data: Array.from(locationMap.values()),
status: 200
};
}

View File

@@ -0,0 +1,75 @@
/**
* API Client para UM ACE - Unidades de medida ACE
*/
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface UMACE {
id: number;
code: string;
description: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface UMACECreate {
code: string;
description?: string | null;
}
export interface UMACEUpdate {
code?: string;
description?: string | null;
}
export interface UMACEListResponse {
items: UMACE[];
total: number;
page: number;
page_size: number;
pages: number;
}
/**
* Lista todas las unidades de medida ACE
*/
export async function getUMACE(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<UMACEListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/units-of-measure/ace?${queryParams.toString()}`);
}
/**
* Obtiene una unidad de medida por ID
*/
export async function getUMACEById(id: number): Promise<ApiResponse<UMACE>> {
return await api.get(`/a76/units-of-measure/ace/${id}`);
}
/**
* Crea una nueva unidad de medida
*/
export async function createUMACE(data: UMACECreate): Promise<ApiResponse<UMACE>> {
return await api.post('/a76/units-of-measure/ace', data);
}
/**
* Actualiza una unidad de medida
*/
export async function updateUMACE(id: number, data: UMACEUpdate): Promise<ApiResponse<UMACE>> {
return await api.put(`/a76/units-of-measure/ace/${id}`, data);
}
/**
* Elimina una unidad de medida
*/
export async function deleteUMACE(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/units-of-measure/ace/${id}`);
}

View File

@@ -0,0 +1,75 @@
/**
* API Client para UM American - Unidades de medida americanas
*/
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface UMCustomsAme {
id: number;
code: string;
description: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface UMCustomsAmeCreate {
code: string;
description?: string | null;
}
export interface UMCustomsAmeUpdate {
code?: string;
description?: string | null;
}
export interface UMCustomsAmeListResponse {
items: UMCustomsAme[];
total: number;
page: number;
page_size: number;
pages: number;
}
/**
* Lista todas las unidades de medida americanas
*/
export async function getUMCustomsAme(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<UMCustomsAmeListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/units-of-measure/american?${queryParams.toString()}`);
}
/**
* Obtiene una unidad de medida por ID
*/
export async function getUMCustomsAmeById(id: number): Promise<ApiResponse<UMCustomsAme>> {
return await api.get(`/a76/units-of-measure/american/${id}`);
}
/**
* Crea una nueva unidad de medida
*/
export async function createUMCustomsAme(data: UMCustomsAmeCreate): Promise<ApiResponse<UMCustomsAme>> {
return await api.post('/a76/units-of-measure/american', data);
}
/**
* Actualiza una unidad de medida
*/
export async function updateUMCustomsAme(id: number, data: UMCustomsAmeUpdate): Promise<ApiResponse<UMCustomsAme>> {
return await api.put(`/a76/units-of-measure/american/${id}`, data);
}
/**
* Elimina una unidad de medida
*/
export async function deleteUMCustomsAme(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/units-of-measure/american/${id}`);
}

View File

@@ -0,0 +1,75 @@
/**
* API Client para UM Customs (Mexican) - Unidades de medida para aduanas mexicanas
*/
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface UMCustomsMex {
id: number;
code: string;
description: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface UMCustomsMexCreate {
code: string;
description?: string | null;
}
export interface UMCustomsMexUpdate {
code?: string;
description?: string | null;
}
export interface UMCustomsMexListResponse {
items: UMCustomsMex[];
total: number;
page: number;
page_size: number;
pages: number;
}
/**
* Lista todas las unidades de medida para aduanas mexicanas
*/
export async function getUMCustomsMex(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<UMCustomsMexListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/units-of-measure/customs?${queryParams.toString()}`);
}
/**
* Obtiene una unidad de medida por ID
*/
export async function getUMCustomsMexById(id: number): Promise<ApiResponse<UMCustomsMex>> {
return await api.get(`/a76/units-of-measure/customs/${id}`);
}
/**
* Crea una nueva unidad de medida
*/
export async function createUMCustomsMex(data: UMCustomsMexCreate): Promise<ApiResponse<UMCustomsMex>> {
return await api.post('/a76/units-of-measure/customs', data);
}
/**
* Actualiza una unidad de medida
*/
export async function updateUMCustomsMex(id: number, data: UMCustomsMexUpdate): Promise<ApiResponse<UMCustomsMex>> {
return await api.put(`/a76/units-of-measure/customs/${id}`, data);
}
/**
* Elimina una unidad de medida
*/
export async function deleteUMCustomsMex(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/units-of-measure/customs/${id}`);
}

View File

@@ -0,0 +1,75 @@
/**
* API Client para UM OMA - Unidades de medida OMA
*/
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface UMOMA {
id: number;
code: string;
description: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface UMOMACreate {
code: string;
description?: string | null;
}
export interface UMOMAUpdate {
code?: string;
description?: string | null;
}
export interface UMOMAListResponse {
items: UMOMA[];
total: number;
page: number;
page_size: number;
pages: number;
}
/**
* Lista todas las unidades de medida OMA
*/
export async function getUMOMA(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<UMOMAListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/units-of-measure/oma?${queryParams.toString()}`);
}
/**
* Obtiene una unidad de medida por ID
*/
export async function getUMOMAById(id: number): Promise<ApiResponse<UMOMA>> {
return await api.get(`/a76/units-of-measure/oma/${id}`);
}
/**
* Crea una nueva unidad de medida
*/
export async function createUMOMA(data: UMOMACreate): Promise<ApiResponse<UMOMA>> {
return await api.post('/a76/units-of-measure/oma', data);
}
/**
* Actualiza una unidad de medida
*/
export async function updateUMOMA(id: number, data: UMOMAUpdate): Promise<ApiResponse<UMOMA>> {
return await api.put(`/a76/units-of-measure/oma/${id}`, data);
}
/**
* Elimina una unidad de medida
*/
export async function deleteUMOMA(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/units-of-measure/oma/${id}`);
}

View File

@@ -0,0 +1,75 @@
/**
* API Client para Unit Measures - Catálogo principal de unidades de medida
*/
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface UnitMeasure {
id: number;
code: string;
description: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface UnitMeasureCreate {
code: string;
description?: string | null;
}
export interface UnitMeasureUpdate {
code?: string;
description?: string | null;
}
export interface UnitMeasureListResponse {
items: UnitMeasure[];
total: number;
page: number;
page_size: number;
pages: number;
}
/**
* Lista todas las unidades de medida
*/
export async function getUnitMeasures(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<UnitMeasureListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/units-of-measure?${queryParams.toString()}`);
}
/**
* Obtiene una unidad de medida por ID
*/
export async function getUnitMeasure(id: number): Promise<ApiResponse<UnitMeasure>> {
return await api.get(`/a76/units-of-measure/${id}`);
}
/**
* Crea una nueva unidad de medida
*/
export async function createUnitMeasure(data: UnitMeasureCreate): Promise<ApiResponse<UnitMeasure>> {
return await api.post('/a76/units-of-measure', data);
}
/**
* Actualiza una unidad de medida
*/
export async function updateUnitMeasure(id: number, data: UnitMeasureUpdate): Promise<ApiResponse<UnitMeasure>> {
return await api.put(`/a76/units-of-measure/${id}`, data);
}
/**
* Elimina una unidad de medida
*/
export async function deleteUnitMeasure(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/units-of-measure/${id}`);
}

View File

@@ -0,0 +1,20 @@
import type { ColumnDef } from '@tanstack/table-core';
export interface Location {
location_code: string;
location_description: string | null;
}
export function createColumns(): ColumnDef<Location>[] {
return [
{
accessorKey: 'location_code',
header: 'Código',
},
{
accessorKey: 'location_description',
header: 'Descripción',
cell: ({ row }) => row.original.location_description || '-'
}
];
}

View File

@@ -0,0 +1,76 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
});
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems} ubicaciones únicas
</div>
</div>

View File

@@ -0,0 +1,32 @@
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export interface UMCustomsMex {
id: number;
code: string;
description: string | null;
}
export function createColumns(onSuccess?: () => void): ColumnDef<UMCustomsMex>[] {
return [
{
accessorKey: 'code',
header: 'Código',
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,113 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { createUMCustomsMex, updateUMCustomsMex, type UMCustomsMex } from "$lib/api/dashboard/a76/general_catalogs/um-customs-mex";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: UMCustomsMex | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Unidad Customs MEX" : "Nueva Unidad Customs MEX");
let formData = $state({
code: '',
description: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (open) {
if (isEdit && item) {
formData = {
code: item.code,
description: item.description || ''
};
} else {
formData = {
code: '',
description: ''
};
}
error = null;
}
});
async function handleSubmit() {
loading = true;
error = null;
try {
let response;
if (isEdit && item) {
response = await updateUMCustomsMex(item.id, {
code: formData.code,
description: formData.description || null
});
} else {
response = await createUMCustomsMex({
code: formData.code,
description: formData.description || null
});
}
if (response.error) {
error = response.error;
return;
}
open = false;
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<div class="grid gap-4 py-4">
{#if error}
<div class="text-red-500 text-sm mb-2">{error}</div>
{/if}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="code" class="text-right">Código</Label>
<Input id="code" bind:value={formData.code} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="description" class="text-right">Descripción</Label>
<Input id="description" bind:value={formData.description} class="col-span-3" disabled={loading} />
</div>
</div>
<div class="flex justify-end gap-2">
<Button variant="outline" onclick={() => open = false} disabled={loading}>
Cancelar
</Button>
<Button onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteUMCustomsMex, type UMCustomsMex } from "$lib/api/dashboard/a76/general_catalogs/um-customs-mex";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: UMCustomsMex;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar la unidad "${item.code}"?`)) {
return;
}
loading = true;
error = null;
try {
const response = await deleteUMCustomsMex(item.id);
if (response.error) {
error = response.error;
alert(`Error al eliminar: ${response.error}`);
return;
}
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
alert('Error de conexión al eliminar');
} finally {
loading = false;
}
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
<EllipsisVertical class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog
bind:open={dialogOpen}
mode="edit"
{item}
{onSuccess}
/>

View File

@@ -0,0 +1,106 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount: pageCount,
});
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url);
}
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems}
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
>
Siguiente
</Button>
</div>
</div>

View File

@@ -0,0 +1,32 @@
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export interface UnitMeasure {
id: number;
code: string;
description: string | null;
}
export function createColumns(onSuccess?: () => void): ColumnDef<UnitMeasure>[] {
return [
{
accessorKey: 'code',
header: 'Código',
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,113 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { createUnitMeasure, updateUnitMeasure, type UnitMeasure } from "$lib/api/dashboard/a76/general_catalogs/unit-measures";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: UnitMeasure | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Unidad de Medida" : "Nueva Unidad de Medida");
let formData = $state({
code: '',
description: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (open) {
if (isEdit && item) {
formData = {
code: item.code,
description: item.description || ''
};
} else {
formData = {
code: '',
description: ''
};
}
error = null;
}
});
async function handleSubmit() {
loading = true;
error = null;
try {
let response;
if (isEdit && item) {
response = await updateUnitMeasure(item.id, {
code: formData.code,
description: formData.description || null
});
} else {
response = await createUnitMeasure({
code: formData.code,
description: formData.description || null
});
}
if (response.error) {
error = response.error;
return;
}
open = false;
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<div class="grid gap-4 py-4">
{#if error}
<div class="text-red-500 text-sm mb-2">{error}</div>
{/if}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="code" class="text-right">Código</Label>
<Input id="code" bind:value={formData.code} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="description" class="text-right">Descripción</Label>
<Input id="description" bind:value={formData.description} class="col-span-3" disabled={loading} />
</div>
</div>
<div class="flex justify-end gap-2">
<Button variant="outline" onclick={() => open = false} disabled={loading}>
Cancelar
</Button>
<Button onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteUnitMeasure, type UnitMeasure } from "$lib/api/dashboard/a76/general_catalogs/unit-measures";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: UnitMeasure;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar la unidad "${item.code}"?`)) {
return;
}
loading = true;
error = null;
try {
const response = await deleteUnitMeasure(item.id);
if (response.error) {
error = response.error;
alert(`Error al eliminar: ${response.error}`);
return;
}
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
alert('Error de conexión al eliminar');
} finally {
loading = false;
}
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
<EllipsisVertical class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog
bind:open={dialogOpen}
mode="edit"
{item}
{onSuccess}
/>

View File

@@ -0,0 +1,106 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount: pageCount,
});
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url);
}
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems}
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
>
Siguiente
</Button>
</div>
</div>

View File

@@ -179,7 +179,7 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.incoterms"](),
url: "#",
url: "/dashboard/reference_data/incoterms",
},
{
title: m["sidebar.general_catalogs.inpc"](),
@@ -195,7 +195,7 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.valuation_methods"](),
url: "#",
url: "/dashboard/reference_data/valuation_methods",
},
{
title: m["sidebar.general_catalogs.countries"](),
@@ -207,23 +207,23 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.unit_measures"](),
url: "/dashboard/general_catalogs/units_of_measures",
url: "/dashboard/general_catalogs/units_of_measure/general",
},
{
title: m["sidebar.general_catalogs.um_customs_mex"](),
url: "#",
url: "/dashboard/general_catalogs/units_of_measure/customs",
},
{
title: m["sidebar.general_catalogs.um_customs_ame"](),
url: "#",
url: "/dashboard/general_catalogs/units_of_measure/american",
},
{
title: m["sidebar.general_catalogs.um_ace"](),
url: "#",
url: "/dashboard/general_catalogs/units_of_measure/ace",
},
{
title: m["sidebar.general_catalogs.um_oma"](),
url: "#",
url: "/dashboard/general_catalogs/units_of_measure/oma",
},
{
title: m["sidebar.general_catalogs.conversions"](),
@@ -239,7 +239,7 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.currency_types"](),
url: "#",
url: "/dashboard/reference_data/currency_types",
},
{
title: m["sidebar.general_catalogs.multi_currency"](),
@@ -247,7 +247,7 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.invoice_types"](),
url: "#",
url: "/dashboard/reference_data/invoice_types",
},
{
title: m["sidebar.general_catalogs.electronic_signatures"](),
@@ -259,20 +259,16 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.customs_warehouses"](),
url: "#",
url: "/dashboard/reference_data/customs_warehouses",
},
{
title: m["sidebar.general_catalogs.locations"](),
url: "#",
url: "/dashboard/general_catalogs/locations",
},
{
title: m["sidebar.general_catalogs.doda"](),
url: "/dashboard/general_catalogs/doda",
},
{
title: m["sidebar.general_catalogs.packing_list"](),
url: "#",
},
{
title: m["sidebar.general_catalogs.prevalidators"](),
url: "/dashboard/general_catalogs/prevalidators",
@@ -281,10 +277,6 @@ export function getSidebarData(): SidebarData {
title: m["sidebar.general_catalogs.electronic_notices"](),
url: "/dashboard/general_catalogs/electronic_notices",
},
{
title: m["sidebar.general_catalogs.back_flush"](),
url: "#",
},
{
title: m["sidebar.general_catalogs.crossing_notice"](),
url: "#",

View File

@@ -1,20 +1,34 @@
import { getClassificationConcepts } from '$lib/api/dashboard/a76/classification-concepts';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const classification = url.searchParams.get('classification');
const description = url.searchParams.get('description');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
if (classification) filters.classification = classification;
if (description) filters.description = description;
if (!accessToken) {
return { error: 'No authenticated', classifications: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await getClassificationConcepts(page, pageSize, filters);
return {
classifications: response.data
};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const classification = url.searchParams.get('classification');
const description = url.searchParams.get('description');
if (classification) filters.classification = classification;
if (description) filters.description = description;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/classification-concepts?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', classifications: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { classifications: await response.json() };
} catch (error) {
console.error('Error loading classification concepts:', error);
return { error: 'Error loading', classifications: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,20 +1,79 @@
import { getConcepts } from '$lib/api/dashboard/a76/concepts';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
// Esperar a que el layout padre valide/refresque el token
const parentData = await parent();
if (code) filters.code = code;
if (description) filters.description = description;
const { accessToken } = getAuthTokens(cookies);
const response = await getConcepts(page, pageSize, filters);
return {
concepts: response.data
};
if (!accessToken) {
return {
error: 'No authenticated',
concepts: {
items: [],
total: 0,
page: 1,
page_size: 50,
pages: 0
}
};
}
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
// Construir URL con parámetros
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await authenticatedFetch(
`v1/a76/concepts?${queryParams.toString()}`,
{ method: 'GET' },
cookies,
fetch
);
if (!response.ok) {
return {
error: 'Failed to load concepts',
concepts: {
items: [],
total: 0,
page: page,
page_size: pageSize,
pages: 0
}
};
}
const data = await response.json();
return {
concepts: data
};
} catch (error) {
console.error('Error loading concepts:', error);
return {
error: 'Error loading concepts',
concepts: {
items: [],
total: 0,
page: 1,
page_size: 50,
pages: 0
}
};
}
};

View File

@@ -1,20 +1,34 @@
import { getCustomsBrokerConcepts } from '$lib/api/dashboard/a76/customs-broker-concepts';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
if (code) filters.code = code;
if (description) filters.description = description;
if (!accessToken) {
return { error: 'No authenticated', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await getCustomsBrokerConcepts(page, pageSize, filters);
return {
concepts: response.data
};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/customs-broker-concepts?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', concepts: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { concepts: await response.json() };
} catch (error) {
console.error('Error loading customs broker concepts:', error);
return { error: 'Error loading', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,20 +1,34 @@
import { getDODAs } from '$lib/api/dashboard/a76/doda';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
if (code) filters.code = code;
if (description) filters.description = description;
if (!accessToken) {
return { error: 'No authenticated', dodas: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await getDODAs(page, pageSize, filters);
return {
dodas: response.data
};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/doda?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', dodas: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { dodas: await response.json() };
} catch (error) {
console.error('Error loading DODAs:', error);
return { error: 'Error loading', dodas: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,20 +1,34 @@
import { getElectronicNotices } from '$lib/api/dashboard/a76/electronic-notices';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
if (code) filters.code = code;
if (description) filters.description = description;
if (!accessToken) {
return { error: 'No authenticated', notices: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await getElectronicNotices(page, pageSize, filters);
return {
notices: response.data
};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/electronic-notices?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', notices: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { notices: await response.json() };
} catch (error) {
console.error('Error loading electronic notices:', error);
return { error: 'Error loading', notices: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,20 +1,34 @@
import { getEquivalencies } from '$lib/api/dashboard/a76/equivalencies';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const fraccion_mex = url.searchParams.get('fraccion_mex');
const fraccion_us = url.searchParams.get('fraccion_us');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
if (fraccion_mex) filters.fraccion_mex = fraccion_mex;
if (fraccion_us) filters.fraccion_us = fraccion_us;
if (!accessToken) {
return { error: 'No authenticated', equivalencies: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await getEquivalencies(page, pageSize, filters);
return {
equivalencies: response.data
};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const fraccion_mex = url.searchParams.get('fraccion_mex');
const fraccion_us = url.searchParams.get('fraccion_us');
if (fraccion_mex) filters.fraccion_mex = fraccion_mex;
if (fraccion_us) filters.fraccion_us = fraccion_us;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/equivalencies?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', equivalencies: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { equivalencies: await response.json() };
} catch (error) {
console.error('Error loading equivalencies:', error);
return { error: 'Error loading', equivalencies: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,20 +1,34 @@
import { getErrorCatalogs } from '$lib/api/dashboard/a76/error-catalogs';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
if (code) filters.code = code;
if (description) filters.description = description;
if (!accessToken) {
return { error: 'No authenticated', errors: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await getErrorCatalogs(page, pageSize, filters);
return {
errors: response.data
};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/error-catalogs?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', errors: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { errors: await response.json() };
} catch (error) {
console.error('Error loading error catalogs:', error);
return { error: 'Error loading', errors: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,20 +1,34 @@
import { getINPCs } from '$lib/api/dashboard/a76/inpc';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const year = url.searchParams.get('year');
const month = url.searchParams.get('month');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
if (year) filters.year = year;
if (month) filters.month = month;
if (!accessToken) {
return { error: 'No authenticated', inpcs: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await getINPCs(page, pageSize, filters);
return {
inpcs: response.data
};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const year = url.searchParams.get('year');
const month = url.searchParams.get('month');
if (year) filters.year = year;
if (month) filters.month = month;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/inpc?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', inpcs: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { inpcs: await response.json() };
} catch (error) {
console.error('Error loading INPCs:', error);
return { error: 'Error loading', inpcs: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,20 +1,34 @@
import { getLegends } from '$lib/api/dashboard/a76/legends';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
if (code) filters.code = code;
if (description) filters.description = description;
if (!accessToken) {
return { error: 'No authenticated', legends: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await getLegends(page, pageSize, filters);
return {
legends: response.data
};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/legends?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', legends: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { legends: await response.json() };
} catch (error) {
console.error('Error loading legends:', error);
return { error: 'Error loading', legends: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -0,0 +1,71 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 1000;
const apiUrl = getServerApiUrl();
// Get locations from ports
const endpoint = `${apiUrl}api/v1/a76/ports?page=${page}&page_size=${pageSize}`;
try {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
console.error(`Error fetching Ports for locations: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page: 1,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
}
const data = await response.json();
// Extract unique locations
const locationMap = new Map();
data.items.forEach((port: any) => {
if (port.location_code && !locationMap.has(port.location_code)) {
locationMap.set(port.location_code, {
location_code: port.location_code,
location_description: port.location_description
});
}
});
const locations = Array.from(locationMap.values());
return {
items: locations,
total: locations.length,
page: 1,
pageSize: locations.length,
pages: 1
};
} catch (error) {
console.error('Error fetching Locations:', error);
return {
items: [],
total: 0,
page: 1,
pageSize,
pages: 0,
error: 'Error al cargar datos'
};
}
};

View File

@@ -0,0 +1,48 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/locations/columns';
import DataTable from '$lib/components/dashboard/locations/data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let loading = $state(false);
const columns = createColumns();
async function refreshData() {
loading = true;
await invalidateAll();
loading = false;
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Ubicaciones</h2>
<p class="text-muted-foreground">
Catálogo de ubicaciones extraídas de puertos
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
</Button>
</div>
</div>
<Card.Root>
<Card.Content class="p-0">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
/>
</Card.Content>
</Card.Root>
</div>

View File

@@ -1,20 +1,34 @@
import { getMultiCurrencyTypes } from '$lib/api/dashboard/a76/multi-currency-types';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const key = url.searchParams.get('key');
const description = url.searchParams.get('description');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
if (key) filters.key = key;
if (description) filters.description = description;
if (!accessToken) {
return { error: 'No authenticated', types: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await getMultiCurrencyTypes(page, pageSize, filters);
return {
types: response.data
};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const key = url.searchParams.get('key');
const description = url.searchParams.get('description');
if (key) filters.key = key;
if (description) filters.description = description;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/multi-currency-types?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', types: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { types: await response.json() };
} catch (error) {
console.error('Error loading multi currency types:', error);
return { error: 'Error loading', types: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,20 +1,79 @@
import { getPackages } from '$lib/api/dashboard/a76/packages';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const key = url.searchParams.get('key');
const description_es = url.searchParams.get('description_es');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
// Esperar a que el layout padre valide/refresque el token
const parentData = await parent();
if (key) filters.key = key;
if (description_es) filters.description_es = description_es;
const { accessToken } = getAuthTokens(cookies);
const response = await getPackages(page, pageSize, filters);
return {
packages: response.data
};
if (!accessToken) {
return {
error: 'No authenticated',
packages: {
items: [],
total: 0,
page: 1,
page_size: 50,
pages: 0
}
};
}
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const key = url.searchParams.get('key');
const description_es = url.searchParams.get('description_es');
if (key) filters.key = key;
if (description_es) filters.description_es = description_es;
// Construir URL con parámetros
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await authenticatedFetch(
`v1/a76/packages?${queryParams.toString()}`,
{ method: 'GET' },
cookies,
fetch
);
if (!response.ok) {
return {
error: 'Failed to load packages',
packages: {
items: [],
total: 0,
page: page,
page_size: pageSize,
pages: 0
}
};
}
const data = await response.json();
return {
packages: data
};
} catch (error) {
console.error('Error loading packages:', error);
return {
error: 'Error loading packages',
packages: {
items: [],
total: 0,
page: 1,
page_size: 50,
pages: 0
}
};
}
};

View File

@@ -1,20 +1,34 @@
import { getPrevalidators } from '$lib/api/dashboard/a76/prevalidators';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
if (code) filters.code = code;
if (description) filters.description = description;
if (!accessToken) {
return { error: 'No authenticated', prevalidators: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await getPrevalidators(page, pageSize, filters);
return {
prevalidators: response.data
};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/prevalidators?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', prevalidators: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { prevalidators: await response.json() };
} catch (error) {
console.error('Error loading prevalidators:', error);
return { error: 'Error loading', prevalidators: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,20 +1,34 @@
import { getSignatures } from '$lib/api/dashboard/a76/signatures';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const name = url.searchParams.get('name');
const position = url.searchParams.get('position');
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
if (name) filters.name = name;
if (position) filters.position = position;
if (!accessToken) {
return { error: 'No authenticated', signatures: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
const response = await getSignatures(page, pageSize, filters);
return {
signatures: response.data
};
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const name = url.searchParams.get('name');
const position = url.searchParams.get('position');
if (name) filters.name = name;
if (position) filters.position = position;
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/signatures?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', signatures: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { signatures: await response.json() };
} catch (error) {
console.error('Error loading signatures:', error);
return { error: 'Error loading', signatures: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -1,15 +1,29 @@
import { getUnitConversions } from '$lib/api/dashboard/a76/unit-conversions';
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
await parent();
const { accessToken } = getAuthTokens(cookies);
const response = await getUnitConversions(page, pageSize, filters);
return {
conversions: response.data
};
if (!accessToken) {
return { error: 'No authenticated', conversions: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), ...filters });
const response = await authenticatedFetch(`v1/a76/unit-conversions?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', conversions: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
}
return { conversions: await response.json() };
} catch (error) {
console.error('Error loading unit conversions:', error);
return { error: 'Error loading', conversions: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
}
};

View File

@@ -0,0 +1,56 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 50;
const apiUrl = getServerApiUrl();
const endpoint = `${apiUrl}api/v1/a76/units-of-measure/customs?page=${page}&page_size=${pageSize}`;
try {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
console.error(`Error fetching Customs units: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
}
const data = await response.json();
return {
items: data.items,
total: data.total,
page: data.page,
pageSize: data.page_size,
pages: data.pages
};
} catch (error) {
console.error('Error fetching Customs units:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Error al cargar datos'
};
}
};

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/units_of_measure/customs/columns';
import CreateEditDialog from '$lib/components/dashboard/units_of_measure/customs/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/customs/data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let loading = $state(false);
const columns = createColumns(() => {
refreshData();
});
async function refreshData() {
loading = true;
await invalidateAll();
loading = false;
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Unidades de Medida Aduanas MEX</h2>
<p class="text-muted-foreground">
Catálogo de unidades de medida para aduanas mexicanas
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
</Button>
<Button onclick={() => createDialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo
</Button>
</div>
</div>
<Card.Root>
<Card.Content class="p-0">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
/>
</Card.Content>
</Card.Root>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
/>
</div>

View File

@@ -0,0 +1,56 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 50;
const apiUrl = getServerApiUrl();
const endpoint = `${apiUrl}api/v1/a76/units-of-measure?page=${page}&page_size=${pageSize}`;
try {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
console.error(`Error fetching General units: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
}
const data = await response.json();
return {
items: data.items,
total: data.total,
page: data.page,
pageSize: data.page_size,
pages: data.pages
};
} catch (error) {
console.error('Error fetching General units:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Error al cargar datos'
};
}
};

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/units_of_measure/general/columns';
import CreateEditDialog from '$lib/components/dashboard/units_of_measure/general/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/general/data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let loading = $state(false);
const columns = createColumns(() => {
refreshData();
});
async function refreshData() {
loading = true;
await invalidateAll();
loading = false;
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Unidades de Medida Generales</h2>
<p class="text-muted-foreground">
Catálogo general de unidades de medida
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
</Button>
<Button onclick={() => createDialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo
</Button>
</div>
</div>
<Card.Root>
<Card.Content class="p-0">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
/>
</Card.Content>
</Card.Root>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
/>
</div>