feat: Implement general catalogs for INPC, Legends, Multi-Currency Types, Packages, Ports, Prevalidators, Signatures, Unit Conversions, and Units of Measure

- Added INPC management page with search functionality.
- Created Legends management page with filters for code and description.
- Implemented Multi-Currency Types management with search capabilities.
- Developed Packages management page with filtering options.
- Introduced Ports management page with authentication and data fetching.
- Added Prevalidators management page with search filters.
- Implemented Signatures management page with search by name and position.
- Created Unit Conversions management page for conversion factors.
- Developed Units of Measure management pages for ACE, American, and OMA with data tables and create/edit dialogs.
This commit is contained in:
2025-12-08 14:17:47 -06:00
parent f1bd2d63e8
commit 6acbd48e9e
152 changed files with 7334 additions and 2747 deletions

View File

@@ -0,0 +1,38 @@
import type { Company } from '$lib/api/dashboard/a76/company';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Company>[] {
return [
{
accessorKey: 'name',
header: 'Nombre',
cell: ({ row }) => row.original.name || '-'
},
{
accessorKey: 'rfc',
header: 'RFC',
cell: ({ row }) => row.original.rfc || '-'
},
{
accessorKey: 'program',
header: 'Programa',
cell: ({ row }) => row.original.program || '-'
},
{
accessorKey: 'program_number',
header: 'No. Programa',
cell: ({ row }) => row.original.program_number || '-'
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,127 @@
<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 { createCompany, updateCompany, type Company } from "$lib/api/dashboard/a76/company";
let {
open = $bindable(false),
item = null,
onSuccess
}: {
open: boolean;
item?: Company | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(!!item);
const title = $derived(isEdit ? "Editar Empresa" : "Nueva Empresa");
let formData = $state({
name: item?.name || '',
rfc: item?.rfc || '',
main_activity: item?.main_activity || '',
program: item?.program || '',
program_number: item?.program_number || ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (item) {
formData = {
name: item.name || '',
rfc: item.rfc || '',
main_activity: item.main_activity || '',
program: item.program || '',
program_number: item.program_number || ''
};
} else {
formData = {
name: '',
rfc: '',
main_activity: '',
program: '',
program_number: ''
};
}
});
async function handleSubmit() {
error = null;
loading = true;
try {
if (!formData.name.trim()) throw new Error('El nombre es requerido');
const dataToSend = {
name: formData.name.trim(),
rfc: formData.rfc.trim() || null,
main_activity: formData.main_activity.trim() || null,
program: formData.program.trim() || null,
program_number: formData.program_number.trim() || null
};
let response;
if (isEdit && item) {
response = await updateCompany(item.id, dataToSend);
} else {
response = await createCompany(dataToSend);
}
if (response.error) {
throw new Error(response.error);
}
open = false;
if (onSuccess) onSuccess();
} catch (e) {
error = e instanceof Error ? e.message : 'Error al guardar';
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-md">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
{#if error}
<div class="text-destructive text-sm">{error}</div>
{/if}
<div class="grid gap-2">
<Label for="name">Nombre <span class="text-destructive">*</span></Label>
<Input id="name" bind:value={formData.name} />
</div>
<div class="grid gap-2">
<Label for="rfc">RFC</Label>
<Input id="rfc" bind:value={formData.rfc} />
</div>
<div class="grid gap-2">
<Label for="program">Programa</Label>
<Input id="program" bind:value={formData.program} />
</div>
<div class="grid gap-2">
<Label for="program_number">No. Programa</Label>
<Input id="program_number" bind:value={formData.program_number} />
</div>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={() => open = false}>Cancelar</Button>
<Button type="submit" disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,78 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteCompany, type Company } from "$lib/api/dashboard/a76/company";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: Company;
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 empresa "${item.name}"?`)) {
return;
}
loading = true;
error = null;
try {
const response = await deleteCompany(item.id);
if (response.error) {
alert(`Error al eliminar: ${response.error}`);
return;
}
if (onSuccess) {
onSuccess();
}
} catch (e) {
alert('Error al eliminar el registro');
} 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.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#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}
item={item}
onSuccess={onSuccess}
/>

View File

@@ -0,0 +1,93 @@
<script lang="ts" generics="T extends Record<string, any>">
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 Column = {
key: string;
label: string;
format?: (value: any, row: T) => string;
};
type SimpleDataTableProps<T> = {
columns: Column[];
data: T[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: SimpleDataTableProps<T> = $props();
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url);
}
function getCellValue(row: T, column: Column): string {
const value = row[column.key];
if (column.format) {
return column.format(value, row);
}
return value !== null && value !== undefined ? String(value) : '-';
}
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
<Table.Row>
{#each columns as column (column.key)}
<Table.Head>{column.label}</Table.Head>
{/each}
</Table.Row>
</Table.Header>
<Table.Body>
{#each data as row, i (i)}
<Table.Row>
{#each columns as column (column.key)}
<Table.Cell>
{getCellValue(row, column)}
</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,37 @@
import type { Identifier } from '$lib/api/dashboard/a76/identifiers';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Identifier>[] {
return [
{
accessorKey: 'code',
header: 'Clave',
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
accessorKey: 'level',
header: 'Nivel',
cell: ({ row }) => row.original.level || '-'
},
{
accessorKey: 'complement',
header: 'Complemento',
cell: ({ row }) => row.original.complement || '-'
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,139 @@
<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 { Textarea } from "$lib/components/ui/textarea";
import { createIdentifier, updateIdentifier, type Identifier } from "$lib/api/dashboard/a76/identifiers";
import { companyStore } from "$lib/stores/company.svelte";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: Identifier | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Identificador" : "Nuevo Identificador");
let formData = $state({
code: '',
description: '',
level: '',
complement: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (open) {
if (isEdit && item) {
formData = {
code: item.code,
description: item.description || '',
level: item.level || '',
complement: item.complement || ''
};
} else {
formData = {
code: '',
description: '',
level: '',
complement: ''
};
}
error = null;
}
});
async function handleSubmit() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
error = 'No hay compañía seleccionada';
return;
}
loading = true;
error = null;
try {
let response;
if (isEdit && item) {
response = await updateIdentifier(item.id, {
code: formData.code,
description: formData.description || null,
level: formData.level || null,
complement: formData.complement || null
});
} else {
response = await createIdentifier({
code: formData.code,
description: formData.description || null,
level: formData.level || null,
complement: formData.complement || null,
company_id: companyId
});
}
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">Clave</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>
<Textarea id="description" bind:value={formData.description} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="level" class="text-right">Nivel</Label>
<Input id="level" bind:value={formData.level} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="complement" class="text-right">Complemento</Label>
<Textarea id="complement" bind:value={formData.complement} class="col-span-3" disabled={loading} />
</div>
</div>
<Dialog.Footer>
<Button type="submit" onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</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 { deleteIdentifier, type Identifier } from "$lib/api/dashboard/a76/identifiers";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: Identifier;
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 el identificador "${item.code}"?`)) {
return;
}
loading = true;
error = null;
try {
const response = await deleteIdentifier(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

@@ -95,9 +95,9 @@
let response;
if (isEdit && item) {
response = await updatePackage(item.id, dataToSend as PackageUpdate, companyId);
response = await updatePackage(item.id, dataToSend);
} else {
response = await createPackage(dataToSend as PackageCreate, companyId);
response = await createPackage({ ...dataToSend, company_id: companyId });
}
if (response.error) {

View File

@@ -7,10 +7,10 @@
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
package: item,
item,
onSuccess
}: {
package: Package;
item: Package;
onSuccess?: () => void;
} = $props();
@@ -24,17 +24,11 @@
return;
}
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
alert('No hay compañía seleccionada');
return;
}
loading = true;
error = null;
try {
const response = await deletePackage(item.id, companyId);
const response = await deletePackage(item.id);
if (response.error) {
if (response.status === 401) {

View File

@@ -0,0 +1,47 @@
import type { Port } from '$lib/api/dashboard/a76/ports';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Port>[] {
return [
{
accessorKey: 'port_code',
header: 'Código Puerto',
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
accessorKey: 'location_code',
header: 'Código Ubicación',
},
{
accessorKey: 'location_description',
header: 'Ubicación',
cell: ({ row }) => row.original.location_description || '-'
},
{
accessorKey: 'port_type',
header: 'Tipo',
cell: ({ row }) => {
const type = row.original.port_type;
if (type === 'ENTRY') return 'Entrada';
if (type === 'EXIT') return 'Salida';
if (type === 'BOTH') return 'Ambos';
return type;
}
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,154 @@
<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 * as Select from "$lib/components/ui/select";
import { createPort, updatePort, type Port, PortType } from "$lib/api/dashboard/a76/ports";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: Port | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Puerto" : "Nuevo Puerto");
let formData = $state({
port_code: '',
description: '',
location_code: '',
location_description: '',
port_type: PortType.ENTRY
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (open) {
if (isEdit && item) {
formData = {
port_code: item.port_code,
description: item.description || '',
location_code: item.location_code,
location_description: item.location_description || '',
port_type: item.port_type
};
} else {
formData = {
port_code: '',
description: '',
location_code: '',
location_description: '',
port_type: PortType.ENTRY
};
}
error = null;
}
});
async function handleSubmit() {
loading = true;
error = null;
try {
let response;
if (isEdit && item) {
response = await updatePort(item.id, {
port_code: formData.port_code,
description: formData.description || null,
location_code: formData.location_code,
location_description: formData.location_description || null,
port_type: formData.port_type
});
} else {
response = await createPort({
port_code: formData.port_code,
description: formData.description || null,
location_code: formData.location_code,
location_description: formData.location_description || null,
port_type: formData.port_type
});
}
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="port_code" class="text-right">Código Puerto</Label>
<Input id="port_code" bind:value={formData.port_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 class="grid grid-cols-4 items-center gap-4">
<Label for="location_code" class="text-right">Código Ubicación</Label>
<Input id="location_code" bind:value={formData.location_code} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="location_description" class="text-right">Ubicación</Label>
<Input id="location_description" bind:value={formData.location_description} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="port_type" class="text-right">Tipo</Label>
<div class="col-span-3">
<Select.Root type="single" bind:value={formData.port_type}>
<Select.Trigger>
{formData.port_type === 'ENTRY' ? 'Entrada' :
formData.port_type === 'EXIT' ? 'Salida' :
formData.port_type === 'BOTH' ? 'Ambos' : 'Seleccionar'}
</Select.Trigger>
<Select.Content>
<Select.Item value="ENTRY">Entrada</Select.Item>
<Select.Item value="EXIT">Salida</Select.Item>
<Select.Item value="BOTH">Ambos</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
</div>
<Dialog.Footer>
<Button type="submit" onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</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 { deletePort, type Port } from "$lib/api/dashboard/a76/ports";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: Port;
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 el puerto "${item.port_code}"?`)) {
return;
}
loading = true;
error = null;
try {
const response = await deletePort(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,27 @@
import type { UnitOfMeasureACE } from '$lib/api/dashboard/a76/units-of-measure';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureACE>[] {
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,110 @@
<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 { createUnitOfMeasureACE, updateUnitOfMeasureACE, type UnitOfMeasureACE } from "$lib/api/dashboard/a76/units-of-measure";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: UnitOfMeasureACE | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Unidad ACE" : "Nueva Unidad ACE");
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 updateUnitOfMeasureACE(item.id, {
code: formData.code,
description: formData.description || null
});
} else {
response = await createUnitOfMeasureACE({
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>
<Dialog.Footer>
<Button type="submit" onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</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 { deleteUnitOfMeasureACE, type UnitOfMeasureACE } from "$lib/api/dashboard/a76/units-of-measure";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: UnitOfMeasureACE;
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 deleteUnitOfMeasureACE(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,27 @@
import type { UnitOfMeasureAmerican } from '$lib/api/dashboard/a76/units-of-measure';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureAmerican>[] {
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,110 @@
<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 { createUnitOfMeasureAmerican, updateUnitOfMeasureAmerican, type UnitOfMeasureAmerican } from "$lib/api/dashboard/a76/units-of-measure";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: UnitOfMeasureAmerican | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Unidad Americana" : "Nueva Unidad Americana");
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 updateUnitOfMeasureAmerican(item.id, {
code: formData.code,
description: formData.description || null
});
} else {
response = await createUnitOfMeasureAmerican({
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>
<Dialog.Footer>
<Button type="submit" onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</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 { deleteUnitOfMeasureAmerican, type UnitOfMeasureAmerican } from "$lib/api/dashboard/a76/units-of-measure";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: UnitOfMeasureAmerican;
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 deleteUnitOfMeasureAmerican(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,27 @@
import type { UnitOfMeasureOMA } from '$lib/api/dashboard/a76/units-of-measure';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureOMA>[] {
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,110 @@
<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 { createUnitOfMeasureOMA, updateUnitOfMeasureOMA, type UnitOfMeasureOMA } from "$lib/api/dashboard/a76/units-of-measure";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: UnitOfMeasureOMA | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Unidad OMA" : "Nueva Unidad OMA");
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 updateUnitOfMeasureOMA(item.id, {
code: formData.code,
description: formData.description || null
});
} else {
response = await createUnitOfMeasureOMA({
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>
<Dialog.Footer>
<Button type="submit" onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</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 { deleteUnitOfMeasureOMA, type UnitOfMeasureOMA } from "$lib/api/dashboard/a76/units-of-measure";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: UnitOfMeasureOMA;
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 deleteUnitOfMeasureOMA(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

@@ -155,7 +155,7 @@ export function getSidebarData(): SidebarData {
items: [
{
title: m["sidebar.general_catalogs.company_information"](),
url: "#",
url: "/dashboard/general_catalogs/company_information",
},
{
title: m["sidebar.general_catalogs.packages"](),
@@ -163,15 +163,19 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.concepts"](),
url: "#",
url: "/dashboard/general_catalogs/concepts",
},
{
title: "Conceptos de Agente Aduanal",
url: "/dashboard/general_catalogs/customs_broker_concepts",
},
{
title: m["sidebar.general_catalogs.classification"](),
url: "/dashboard/general_catalogs/classes",
url: "/dashboard/general_catalogs/classification_concepts",
},
{
title: m["sidebar.general_catalogs.identifiers"](),
url: "#",
url: "/dashboard/general_catalogs/identifiers",
},
{
title: m["sidebar.general_catalogs.incoterms"](),
@@ -179,11 +183,11 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.inpc"](),
url: "#",
url: "/dashboard/general_catalogs/inpc",
},
{
title: m["sidebar.general_catalogs.fixed_legends"](),
url: "#",
url: "/dashboard/general_catalogs/legends",
},
{
title: m["sidebar.general_catalogs.seals"](),
@@ -199,11 +203,11 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.ports"](),
url: "#",
url: "/dashboard/general_catalogs/ports",
},
{
title: m["sidebar.general_catalogs.unit_measures"](),
url: "#",
url: "/dashboard/general_catalogs/units_of_measures",
},
{
title: m["sidebar.general_catalogs.um_customs_mex"](),
@@ -223,11 +227,11 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.conversions"](),
url: "#",
url: "/dashboard/general_catalogs/unit_conversions",
},
{
title: m["sidebar.general_catalogs.equivalences"](),
url: "#",
url: "/dashboard/general_catalogs/equivalencies",
},
{
title: m["sidebar.general_catalogs.exchange_rates"](),
@@ -239,7 +243,7 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.multi_currency"](),
url: "#",
url: "/dashboard/general_catalogs/multi_currency_types",
},
{
title: m["sidebar.general_catalogs.invoice_types"](),
@@ -247,11 +251,11 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.electronic_signatures"](),
url: "#",
url: "/dashboard/general_catalogs/signatures",
},
{
title: m["sidebar.general_catalogs.billing_errors"](),
url: "#",
url: "/dashboard/general_catalogs/error_catalogs",
},
{
title: m["sidebar.general_catalogs.customs_warehouses"](),
@@ -263,7 +267,7 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.doda"](),
url: "#",
url: "/dashboard/general_catalogs/doda",
},
{
title: m["sidebar.general_catalogs.packing_list"](),
@@ -271,11 +275,11 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.prevalidators"](),
url: "#",
url: "/dashboard/general_catalogs/prevalidators",
},
{
title: m["sidebar.general_catalogs.electronic_notices"](),
url: "#",
url: "/dashboard/general_catalogs/electronic_notices",
},
{
title: m["sidebar.general_catalogs.back_flush"](),