Merge branch 'fix/carga-csv-headers' into featuere/pedimento-logica-clarion
This commit is contained in:
@@ -7,11 +7,19 @@
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
onRowClick?: (row: TData) => void;
|
||||
onRowDoubleClick?: (row: TData) => void;
|
||||
selectedId?: string | number | null;
|
||||
idField?: keyof TData;
|
||||
};
|
||||
|
||||
let { data, columns, onRowClick, selectedId, idField }: DataTableProps<TData, TValue> = $props();
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
onRowClick,
|
||||
onRowDoubleClick,
|
||||
selectedId,
|
||||
idField
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = $derived(
|
||||
createSvelteTable({
|
||||
@@ -46,6 +54,7 @@
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row
|
||||
onclick={() => onRowClick?.(row.original)}
|
||||
ondblclick={() => onRowDoubleClick?.(row.original)}
|
||||
class="cursor-pointer transition-colors hover:bg-muted/50 {selectedId &&
|
||||
idField &&
|
||||
row.original[idField] === selectedId
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { DodaAltaLog } from '$lib/api/dashboard/a76/doda-alta-log';
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import { createRawSnippet } from 'svelte';
|
||||
import { renderSnippet } from '$lib/components/ui/data-table';
|
||||
|
||||
function formatDateTime(raw?: string | null): string {
|
||||
if (!raw) return '-';
|
||||
try {
|
||||
return new Date(raw).toLocaleString('es-MX', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
const STATUS_CLASSES: Record<string, string> = {
|
||||
success: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
|
||||
failed: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
|
||||
failure: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
|
||||
pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||||
processing: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
started: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
};
|
||||
|
||||
function statusBadge(status: string | null | undefined) {
|
||||
const s = (status || '').toLowerCase();
|
||||
const cls = STATUS_CLASSES[s] || 'bg-gray-100 text-gray-700';
|
||||
return createRawSnippet(() => ({
|
||||
render: () =>
|
||||
`<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${cls}">${status || '-'}</span>`
|
||||
}));
|
||||
}
|
||||
|
||||
export function createAltaLogColumns(): ColumnDef<DodaAltaLog>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: '#',
|
||||
size: 60,
|
||||
cell: ({ row }) => row.original.id
|
||||
},
|
||||
{
|
||||
accessorKey: 'doda_id',
|
||||
header: 'DODA ID',
|
||||
size: 80,
|
||||
cell: ({ row }) => row.original.doda_id ?? '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'integration_number',
|
||||
header: 'No. Integración',
|
||||
cell: ({ row }) => row.original.integration_number || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'variant',
|
||||
header: 'Tipo',
|
||||
size: 70,
|
||||
cell: ({ row }) => (row.original.variant || '-').toUpperCase()
|
||||
},
|
||||
{
|
||||
accessorKey: 'patent',
|
||||
header: 'Patente',
|
||||
size: 80,
|
||||
cell: ({ row }) => row.original.patent || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'dispatch_customs',
|
||||
header: 'Aduana',
|
||||
size: 80,
|
||||
cell: ({ row }) => row.original.dispatch_customs || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'operation_type',
|
||||
header: 'Operación',
|
||||
size: 90,
|
||||
cell: ({ row }) => row.original.operation_type || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Estatus',
|
||||
size: 110,
|
||||
cell: ({ row }) => renderSnippet(statusBadge(row.original.status), {})
|
||||
},
|
||||
{
|
||||
accessorKey: 'task_id',
|
||||
header: 'Task ID',
|
||||
cell: ({ row }) => {
|
||||
const id = row.original.task_id || '';
|
||||
const snippet = createRawSnippet(() => ({
|
||||
render: () =>
|
||||
`<span class="font-mono text-xs truncate max-w-[180px] block" title="${id}">${id || '-'}</span>`
|
||||
}));
|
||||
return renderSnippet(snippet, {});
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'message',
|
||||
header: 'Mensaje',
|
||||
cell: ({ row }) => {
|
||||
const msg = row.original.message || '';
|
||||
const snippet = createRawSnippet(() => ({
|
||||
render: () =>
|
||||
`<span class="text-xs truncate max-w-[220px] block text-muted-foreground" title="${msg}">${msg || '-'}</span>`
|
||||
}));
|
||||
return renderSnippet(snippet, {});
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Fecha',
|
||||
size: 130,
|
||||
cell: ({ row }) => formatDateTime(row.original.created_at)
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<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 { LoaderCircle } from 'lucide-svelte';
|
||||
import {
|
||||
dodaAltaLogApi,
|
||||
type DodaAltaLog,
|
||||
type DodaAltaLogUpdateDTO
|
||||
} from '$lib/api/dashboard/a76/doda-alta-log';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: DodaAltaLog | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? 'Detalle de Alta DODA' : 'Nuevo Registro de Alta DODA');
|
||||
|
||||
let formData = $state<DodaAltaLogUpdateDTO>({
|
||||
status: null,
|
||||
message: null,
|
||||
result_json: null
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
error = null;
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
if (item) {
|
||||
formData = {
|
||||
status: item.status ?? null,
|
||||
message: item.message ?? null,
|
||||
result_json: item.result_json ?? null
|
||||
};
|
||||
} else {
|
||||
formData = { status: null, message: null, result_json: null };
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading || !item) return;
|
||||
error = null;
|
||||
loading = true;
|
||||
try {
|
||||
const company = companyStore.activeCompany;
|
||||
if (!company) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
const res = await dodaAltaLogApi.update(item.id, formData, company.id);
|
||||
if (res.error) throw new Error(res.error);
|
||||
|
||||
open = false;
|
||||
onSuccess?.();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error desconocido';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!item || !confirm('¿Está seguro de eliminar este registro de alta DODA?')) return;
|
||||
const company = companyStore.activeCompany;
|
||||
if (!company) return;
|
||||
loading = true;
|
||||
try {
|
||||
await dodaAltaLogApi.delete(item.id, company.id);
|
||||
open = false;
|
||||
onSuccess?.();
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-[700px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
{#if item}
|
||||
<Dialog.Description>
|
||||
Registro #{item.id} — DODA {item.doda_id ?? '-'} — {(item.variant || 'doda').toUpperCase()}
|
||||
</Dialog.Description>
|
||||
{/if}
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="grid gap-6 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Datos de solo lectura del DODA -->
|
||||
{#if item}
|
||||
<div class="grid grid-cols-2 gap-4 rounded-lg border bg-muted/30 p-4 text-sm">
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">No. Integración</p>
|
||||
<p class="font-medium">{item.integration_number || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">Responsable</p>
|
||||
<p class="font-medium">{item.responsible || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">Patente</p>
|
||||
<p class="font-medium">{item.patent || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">Aduana Despacho</p>
|
||||
<p class="font-medium">{item.dispatch_customs || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">Tipo Operación</p>
|
||||
<p class="font-medium">{item.operation_type || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">Task ID</p>
|
||||
<p class="font-mono text-xs break-all">{item.task_id || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">Fecha Alta</p>
|
||||
<p class="font-medium">
|
||||
{item.created_at
|
||||
? new Date(item.created_at).toLocaleString('es-MX')
|
||||
: '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Campos editables -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium text-muted-foreground leading-none">Estado</h4>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="status">Estatus</Label>
|
||||
<Input
|
||||
id="status"
|
||||
value={formData.status ?? ''}
|
||||
oninput={(e) => (formData.status = (e.target as HTMLInputElement).value || null)}
|
||||
placeholder="pending / success / failed"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
<Label for="message">Mensaje</Label>
|
||||
<Input
|
||||
id="message"
|
||||
value={formData.message ?? ''}
|
||||
oninput={(e) => (formData.message = (e.target as HTMLInputElement).value || null)}
|
||||
placeholder="Mensaje del servicio externo"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
{#if item?.result_json}
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
<Label>Resultado JSON</Label>
|
||||
<pre class="text-xs rounded-md border bg-muted p-3 overflow-auto max-h-48 whitespace-pre-wrap break-all">{(() => {
|
||||
try { return JSON.stringify(JSON.parse(item.result_json || '{}'), null, 2); }
|
||||
catch { return item.result_json || ''; }
|
||||
})()}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer class="flex justify-between gap-2 pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onclick={handleDelete}
|
||||
disabled={loading || !item}
|
||||
>
|
||||
Eliminar
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
<Button type="button" variant="outline" onclick={() => (open = false)} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
{#if isEdit}
|
||||
<Button type="button" onclick={handleSubmit} disabled={loading}>
|
||||
{#if loading}<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />{/if}
|
||||
Guardar
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,184 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { Loader2, FileSpreadsheet } from 'lucide-svelte';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
import { exportDodaList, type DodaExportFileFormat, type DodaExportDateMode } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
companyId
|
||||
}: {
|
||||
open: boolean;
|
||||
companyId: number | undefined;
|
||||
} = $props();
|
||||
|
||||
function isoFirstDayOfMonth(): string {
|
||||
const d = new Date();
|
||||
const y = d.getFullYear();
|
||||
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
||||
return `${y}-${mo}-01`;
|
||||
}
|
||||
|
||||
function isoToday(): string {
|
||||
const d = new Date();
|
||||
const y = d.getFullYear();
|
||||
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${mo}-${day}`;
|
||||
}
|
||||
|
||||
let dateFrom = $state(isoFirstDayOfMonth());
|
||||
let dateTo = $state(isoToday());
|
||||
let fileFormat = $state<DodaExportFileFormat>('xls');
|
||||
/** Alineado al legado: “Imprimir Fecha Juliana” = fechas/hora en archivo numérico (date_mode=raw). */
|
||||
let printJulian = $state(false);
|
||||
let busy = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
dateFrom = isoFirstDayOfMonth();
|
||||
dateTo = isoToday();
|
||||
fileFormat = 'xls';
|
||||
printJulian = false;
|
||||
}
|
||||
});
|
||||
|
||||
function dateModeValue(): DodaExportDateMode {
|
||||
return printJulian ? 'raw' : 'formatted';
|
||||
}
|
||||
|
||||
async function download() {
|
||||
if (!companyId) {
|
||||
toast.error('Seleccione una compañía.');
|
||||
return;
|
||||
}
|
||||
if (!dateFrom || !dateTo) {
|
||||
toast.error(m['sidebar.doda_alta.export_excel_invalid_dates']());
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
try {
|
||||
await exportDodaList(companyId, {
|
||||
dateFrom,
|
||||
dateTo,
|
||||
format: fileFormat,
|
||||
dateMode: dateModeValue()
|
||||
});
|
||||
open = false;
|
||||
toast.success(m['sidebar.doda_alta.export_excel_success']());
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
const isEmpty =
|
||||
/no existen doda|no se encontr[óo] informaci[óo]n para exportar|no doda.*range/i.test(
|
||||
String(msg)
|
||||
);
|
||||
toast.error(
|
||||
isEmpty ? m['sidebar.doda_alta.export_no_data']() : (msg || m['sidebar.doda_alta.export_excel_error']())
|
||||
);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="gap-0 overflow-hidden p-0 sm:max-w-md" showCloseButton={true}>
|
||||
<Dialog.Header class="sr-only">
|
||||
<Dialog.Title>{m['sidebar.doda_alta.export_excel_badge']()} — {m['sidebar.doda_alta.export_report_heading']()}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div
|
||||
class="bg-slate-900 py-2.5 pr-10 pl-4 text-center text-sm font-medium tracking-wide text-slate-50 dark:bg-slate-950"
|
||||
>
|
||||
{m['sidebar.doda_alta.export_excel_badge']()}
|
||||
</div>
|
||||
|
||||
<div class="space-y-5 px-6 pb-1 pt-5">
|
||||
<h2
|
||||
class="text-foreground text-center text-sm font-semibold uppercase leading-snug tracking-wide sm:text-base"
|
||||
>
|
||||
{m['sidebar.doda_alta.export_report_heading']()}
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-3">
|
||||
<div class="space-y-2">
|
||||
<Label for="doda-exp-from" class="text-foreground/90 font-medium">{m['sidebar.doda_alta.export_fecha_inicio']()}</Label>
|
||||
<input
|
||||
id="doda-exp-from"
|
||||
type="date"
|
||||
bind:value={dateFrom}
|
||||
class="border-input bg-background text-foreground flex h-9 w-full rounded-md border px-3 text-sm shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="doda-exp-to" class="text-foreground/90 font-medium">{m['sidebar.doda_alta.export_fecha_final']()}</Label>
|
||||
<input
|
||||
id="doda-exp-to"
|
||||
type="date"
|
||||
bind:value={dateTo}
|
||||
class="border-input bg-background text-foreground flex h-9 w-full rounded-md border px-3 text-sm shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<Button
|
||||
size="default"
|
||||
class="w-full min-w-48"
|
||||
variant="default"
|
||||
onclick={download}
|
||||
disabled={busy || !companyId}
|
||||
>
|
||||
{#if busy}
|
||||
<Loader2 class="mr-2 h-4 w-4 shrink-0 animate-spin" />
|
||||
{:else}
|
||||
<FileSpreadsheet class="mr-2 h-4 w-4 shrink-0" />
|
||||
{/if}
|
||||
{m['sidebar.doda_alta.export_report_generar']()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2.5">
|
||||
<Checkbox id="doda-exp-julian" bind:checked={printJulian} class="mt-0.5" />
|
||||
<Label for="doda-exp-julian" class="text-muted-foreground text-sm font-normal leading-snug">
|
||||
{m['sidebar.doda_alta.export_julian_label']()}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-3">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<Label for="doda-exp-format" class="text-foreground/80 shrink-0 text-sm"
|
||||
>{m['sidebar.doda_alta.export_format']()}</Label
|
||||
>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={fileFormat}
|
||||
onValueChange={(v) => {
|
||||
if (v === 'csv' || v === 'xls' || v === 'txt') fileFormat = v;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="doda-exp-format" class="w-full min-w-0 sm:max-w-[12rem]">
|
||||
.{fileFormat}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="csv">.csv (coma)</Select.Item>
|
||||
<Select.Item value="xls">.xls (tabulador, Excel)</Select.Item>
|
||||
<Select.Item value="txt">.txt (|)</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer class="border-t px-6 py-4 sm:justify-end">
|
||||
<Button type="button" variant="outline" class="min-w-24" onclick={() => (open = false)} disabled={busy}>
|
||||
{m['sidebar.doda_alta.export_cancel']()}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,217 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Progress } from '$lib/components/ui/progress';
|
||||
import { Loader2, CheckCircle, XCircle } from 'lucide-svelte';
|
||||
import {
|
||||
getDodaAltaStatus,
|
||||
type DodaAltaStatusResponse
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
taskId,
|
||||
dodaId,
|
||||
variant = 'doda',
|
||||
onComplete,
|
||||
onCancel
|
||||
}: {
|
||||
open: boolean;
|
||||
taskId: string;
|
||||
dodaId?: number;
|
||||
variant?: 'doda' | 'pita';
|
||||
onComplete?: (result: DodaAltaStatusResponse) => void;
|
||||
onCancel?: () => void;
|
||||
} = $props();
|
||||
|
||||
type TaskState = 'PENDING' | 'PROGRESS' | 'SUCCESS' | 'FAILURE' | 'STARTED';
|
||||
|
||||
let state = $state<TaskState>('PENDING');
|
||||
let currentStep = $state<string>('Iniciando...');
|
||||
let progress = $state(0);
|
||||
let result = $state<DodaAltaStatusResponse | null>(null);
|
||||
let errorMsg = $state<string | null>(null);
|
||||
let consecutivePollErrors = $state(0);
|
||||
let pollHandle: ReturnType<typeof setTimeout> | null = null;
|
||||
let pollInFlight = false;
|
||||
let pollingActive = false;
|
||||
let pollingTaskId: string | null = null;
|
||||
|
||||
$effect(() => {
|
||||
if (open && taskId) {
|
||||
void startPolling();
|
||||
} else {
|
||||
stopPolling();
|
||||
if (!open) resetState();
|
||||
}
|
||||
return () => stopPolling();
|
||||
});
|
||||
|
||||
function resetState() {
|
||||
state = 'PENDING';
|
||||
currentStep = 'Iniciando...';
|
||||
progress = 0;
|
||||
result = null;
|
||||
errorMsg = null;
|
||||
consecutivePollErrors = 0;
|
||||
}
|
||||
|
||||
async function startPolling() {
|
||||
if (pollingActive && pollingTaskId === taskId) return;
|
||||
stopPolling();
|
||||
pollingActive = true;
|
||||
pollingTaskId = taskId;
|
||||
await poll();
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
pollingActive = false;
|
||||
pollingTaskId = null;
|
||||
if (pollHandle !== null) {
|
||||
clearTimeout(pollHandle);
|
||||
pollHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleNextPoll(delayMs = 2000) {
|
||||
if (!pollingActive || !taskId) return;
|
||||
if (pollHandle !== null) clearTimeout(pollHandle);
|
||||
pollHandle = setTimeout(() => void poll(), delayMs);
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
if (!taskId || !pollingActive || pollInFlight) return;
|
||||
pollInFlight = true;
|
||||
try {
|
||||
const res = await getDodaAltaStatus(taskId);
|
||||
|
||||
if (res.error) {
|
||||
consecutivePollErrors += 1;
|
||||
if (consecutivePollErrors >= 3) {
|
||||
state = 'FAILURE';
|
||||
errorMsg = res.error || 'No se pudo consultar el estado del alta DODA';
|
||||
stopPolling();
|
||||
} else {
|
||||
scheduleNextPoll();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.data) {
|
||||
scheduleNextPoll();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = res.data;
|
||||
consecutivePollErrors = 0;
|
||||
|
||||
const rawState = (data.state || data.status || 'PENDING').toUpperCase();
|
||||
state = (rawState === 'FAILED' ? 'FAILURE' : rawState) as TaskState;
|
||||
currentStep = (data.message as string) || 'Procesando...';
|
||||
|
||||
if (typeof (data as Record<string, unknown>)['progress'] === 'number') {
|
||||
progress = (data as Record<string, unknown>)['progress'] as number;
|
||||
}
|
||||
|
||||
if (rawState === 'SUCCESS') {
|
||||
result = data;
|
||||
stopPolling();
|
||||
onComplete?.(data);
|
||||
} else if (rawState === 'FAILURE' || rawState === 'FAILED') {
|
||||
errorMsg = data.error || data.message || 'Error en el alta DODA';
|
||||
stopPolling();
|
||||
} else {
|
||||
scheduleNextPoll();
|
||||
}
|
||||
} catch {
|
||||
consecutivePollErrors += 1;
|
||||
if (consecutivePollErrors >= 3) {
|
||||
state = 'FAILURE';
|
||||
errorMsg = 'No se pudo consultar el estado del alta DODA';
|
||||
stopPolling();
|
||||
} else {
|
||||
scheduleNextPoll();
|
||||
}
|
||||
} finally {
|
||||
pollInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
stopPolling();
|
||||
open = false;
|
||||
onCancel?.();
|
||||
}
|
||||
|
||||
const isTerminal = $derived(state === 'SUCCESS' || state === 'FAILURE');
|
||||
const variantLabel = $derived(variant === 'pita' ? 'PITA' : 'DODA');
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{m['sidebar.doda_alta.progress_title']()}</Dialog.Title>
|
||||
<Dialog.Description>Alta {variantLabel} — Task ID: {taskId}</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="space-y-4 py-2">
|
||||
{#if state === 'SUCCESS'}
|
||||
<div class="flex items-center gap-3 text-green-600">
|
||||
<CheckCircle class="h-6 w-6 shrink-0" />
|
||||
<p class="text-sm font-medium">{m['sidebar.doda_alta.progress_success']()}</p>
|
||||
</div>
|
||||
{#if result}
|
||||
<dl class="text-sm space-y-1">
|
||||
{#each Object.entries(result) as [key, value]}
|
||||
{#if key !== 'state' && value && typeof value === 'string'}
|
||||
<div class="flex gap-2">
|
||||
<dt class="text-muted-foreground min-w-[130px] capitalize">{key.replace(/_/g, ' ')}:</dt>
|
||||
<dd class="font-medium break-all">{value}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</dl>
|
||||
{/if}
|
||||
|
||||
{:else if state === 'FAILURE'}
|
||||
<div class="flex items-start gap-3 text-destructive">
|
||||
<XCircle class="h-6 w-6 shrink-0 mt-0.5" />
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-medium">{m['sidebar.doda_alta.progress_error']()}</p>
|
||||
{#if errorMsg}
|
||||
<p class="text-xs text-muted-foreground">{errorMsg}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<Loader2 class="h-5 w-5 animate-spin text-primary shrink-0" />
|
||||
<p class="text-sm text-muted-foreground">{currentStep}</p>
|
||||
</div>
|
||||
{#if progress > 0}
|
||||
<Progress value={progress} max={100} class="h-2" />
|
||||
<p class="text-xs text-right text-muted-foreground">{progress}%</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if taskId}
|
||||
<div class="rounded-md border bg-muted/30 px-3 py-2">
|
||||
<p class="text-xs text-muted-foreground">Task ID:</p>
|
||||
<p class="text-xs font-mono break-all">{taskId}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer class="flex justify-end">
|
||||
{#if isTerminal}
|
||||
<Button onclick={() => { open = false; }}>Cerrar</Button>
|
||||
{:else}
|
||||
<Button variant="outline" onclick={handleCancel}>Cancelar</Button>
|
||||
{/if}
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Plus, Pencil, Trash2 } from 'lucide-svelte';
|
||||
import { Plus, Pencil, Trash2, Inbox } from 'lucide-svelte';
|
||||
import { cn } from '$lib/utils';
|
||||
import { dodaFormT } from '$lib/i18n/doda-form-strings';
|
||||
|
||||
interface Column {
|
||||
header: string;
|
||||
@@ -12,56 +13,115 @@
|
||||
|
||||
let {
|
||||
title = '',
|
||||
/** `en` / `es` (viene del padre; evita leer `page` aquí, más seguro con SSR) */
|
||||
locale: localeProp = 'es',
|
||||
columns = [],
|
||||
data = [],
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
/** Se dispara al elegir una fila (click / Enter). Útil p. ej. para precintos por contenedor. */
|
||||
onRowSelect,
|
||||
/** p. ej. `max-h-48` para limitar altura y scroll interno en tablas amplias */
|
||||
bodyMaxClass = '',
|
||||
class: className = ''
|
||||
}: {
|
||||
title?: string;
|
||||
locale?: 'en' | 'es';
|
||||
columns: Column[];
|
||||
data: any[];
|
||||
onAdd?: () => void;
|
||||
onEdit?: (item: any, index: number) => void;
|
||||
onDelete?: (item: any, index: number) => void;
|
||||
onRowSelect?: (item: any, index: number) => void;
|
||||
bodyMaxClass?: string;
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
const dodaLoc = $derived((localeProp === 'en' ? 'en' : 'es') as 'en' | 'es');
|
||||
|
||||
let selectedIndex = $state<number | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (data.length === 0) {
|
||||
selectedIndex = null;
|
||||
} else if (selectedIndex != null && selectedIndex >= data.length) {
|
||||
selectedIndex = data.length - 1;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class={cn('space-y-4 rounded-xl border bg-card p-4 shadow-sm', className)}>
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class={cn(
|
||||
'space-y-0 overflow-hidden rounded-lg border border-border/60 bg-card/80 p-0 shadow-sm',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div class="flex items-center border-b border-border/50 bg-muted/25 px-3 py-1.5">
|
||||
{#if title}
|
||||
<h3 class="text-sm font-semibold tracking-wider text-muted-foreground uppercase">{title}</h3>
|
||||
<h3 class="text-xs font-medium text-muted-foreground">{title}</h3>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="relative overflow-hidden rounded-md border bg-background">
|
||||
<div
|
||||
class={cn(
|
||||
'relative overflow-x-auto border-b border-border/40 bg-background/50',
|
||||
bodyMaxClass
|
||||
)}
|
||||
>
|
||||
<Table.Root>
|
||||
<Table.Header class="bg-muted/50">
|
||||
<Table.Row inTabOrder={false}>
|
||||
<Table.Header class="bg-muted/40">
|
||||
<Table.Row inTabOrder={false} class="hover:bg-transparent">
|
||||
{#each columns as col}
|
||||
<Table.Head class="h-10 px-4 text-xs font-semibold whitespace-nowrap"
|
||||
>{col.header}</Table.Head
|
||||
<Table.Head
|
||||
class="h-7 px-2 py-1.5 text-left text-[0.65rem] font-medium uppercase leading-tight text-muted-foreground sm:px-3 sm:text-[0.7rem]"
|
||||
>
|
||||
{col.header}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if data.length === 0}
|
||||
<Table.Row inTabOrder={false}>
|
||||
<Table.Row inTabOrder={false} class="hover:bg-transparent">
|
||||
<Table.Cell
|
||||
colspan={columns.length}
|
||||
class="h-24 text-center text-sm text-muted-foreground"
|
||||
class="h-16 py-2 text-center"
|
||||
>
|
||||
No hay registros.
|
||||
<div
|
||||
class="flex flex-col items-center justify-center gap-1 text-muted-foreground"
|
||||
>
|
||||
<Inbox class="h-5 w-5 opacity-45" />
|
||||
<span class="text-xs">{dodaFormT(dodaLoc, 'child_empty')}</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each data as row, i}
|
||||
<Table.Row inTabOrder={false} class="group transition-colors hover:bg-muted/30">
|
||||
<Table.Row
|
||||
inTabOrder={false}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-state={selectedIndex === i ? 'selected' : undefined}
|
||||
class="group transition-colors hover:bg-muted/30 {selectedIndex === i
|
||||
? 'bg-primary/5 ring-1 ring-inset ring-primary/25'
|
||||
: ''}"
|
||||
onclick={() => {
|
||||
selectedIndex = i;
|
||||
onRowSelect?.(row, i);
|
||||
}}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
selectedIndex = i;
|
||||
onRowSelect?.(row, i);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each columns as col}
|
||||
<Table.Cell class="px-4 py-2 text-sm whitespace-nowrap">
|
||||
<Table.Cell
|
||||
class="max-w-[12rem] truncate px-2 py-1.5 text-xs sm:max-w-[14rem] sm:px-3 sm:text-sm"
|
||||
>
|
||||
{#if col.render}
|
||||
{col.render(row[col.key])}
|
||||
{:else}
|
||||
@@ -76,30 +136,38 @@
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" onclick={onAdd} class="h-8 gap-1 px-3 text-xs font-medium">
|
||||
<div class="flex flex-wrap items-center justify-end gap-2 border-t border-border/30 bg-muted/20 px-3 py-2 sm:px-4">
|
||||
<Button variant="outline" size="sm" onclick={onAdd} class="h-8 gap-1.5 px-3 text-xs font-medium">
|
||||
<Plus class="h-3.5 w-3.5" />
|
||||
Nuevo
|
||||
{dodaFormT(dodaLoc, 'child_new')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onclick={() => {}}
|
||||
onclick={() => {
|
||||
if (selectedIndex == null) return;
|
||||
const row = data[selectedIndex];
|
||||
if (row) onEdit?.(row, selectedIndex);
|
||||
}}
|
||||
class="h-8 gap-1 px-3 text-xs font-medium"
|
||||
disabled={data.length === 0}
|
||||
disabled={data.length === 0 || selectedIndex == null}
|
||||
>
|
||||
<Pencil class="h-3.5 w-3.5" />
|
||||
Editar
|
||||
{dodaFormT(dodaLoc, 'child_edit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onclick={() => {}}
|
||||
onclick={() => {
|
||||
if (selectedIndex == null) return;
|
||||
const row = data[selectedIndex];
|
||||
if (row) onDelete?.(row, selectedIndex);
|
||||
}}
|
||||
class="h-8 gap-1 px-3 text-xs font-medium"
|
||||
disabled={data.length === 0}
|
||||
disabled={data.length === 0 || selectedIndex == null}
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
Borrar
|
||||
{dodaFormT(dodaLoc, 'child_delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,121 +2,157 @@ import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { renderSnippet } from '$lib/components/ui/data-table';
|
||||
import { createRawSnippet } from 'svelte';
|
||||
import { getLocale } from '$lib/paraglide/runtime';
|
||||
import { dodaFormT, type DodaFormKey } from '$lib/i18n/doda-form-strings';
|
||||
|
||||
function formatDate(date?: string | null): string {
|
||||
if (!date) return '-';
|
||||
// Supposing created_at is an ISO string or similar
|
||||
/**
|
||||
* doda_date se almacena como Integer con formato YYYYMMDD (ej. 20180409).
|
||||
* Lo convertimos a DD/MM/YYYY para mostrar.
|
||||
*/
|
||||
function formatDodaDate(val?: number | string | null): string {
|
||||
if (!val) return '-';
|
||||
const s = String(val);
|
||||
if (s.length === 8) {
|
||||
const y = s.slice(0, 4);
|
||||
const m = s.slice(4, 6);
|
||||
const d = s.slice(6, 8);
|
||||
return `${d}/${m}/${y}`;
|
||||
}
|
||||
// Fallback: ISO string
|
||||
try {
|
||||
return new Date(date).toLocaleDateString('es-MX', {
|
||||
year: 'numeric',
|
||||
return new Date(s).toLocaleDateString(getLocale() === 'en' ? 'en-US' : 'es-MX', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
year: 'numeric'
|
||||
});
|
||||
} catch (e) {
|
||||
return date;
|
||||
} catch {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
export function createColumns(): ColumnDef<Doda>[] {
|
||||
const STATUS_CLASSES: Record<string, string> = {
|
||||
GENERADO: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
'EN PROCESO':'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300',
|
||||
VALIDADO: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
|
||||
PENDIENTE: 'bg-gray-100 text-gray-700 dark:bg-gray-800/50 dark:text-gray-300',
|
||||
ELIMINADO: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
|
||||
};
|
||||
|
||||
export function createColumns(loc: 'en' | 'es'): ColumnDef<Doda>[] {
|
||||
const t = (k: DodaFormKey) => dodaFormT(loc, k);
|
||||
return [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'Folio',
|
||||
header: t('list_col_folio'),
|
||||
size: 70,
|
||||
cell: ({ row }) => {
|
||||
const numberSnippet = createRawSnippet<[{ number: number }]>((getProps) => {
|
||||
const { number } = getProps();
|
||||
return {
|
||||
render: () =>
|
||||
`<code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">${number}</code>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(numberSnippet, { number: row.original.id });
|
||||
const n = row.original.id;
|
||||
const s = createRawSnippet(() => ({
|
||||
render: () =>
|
||||
`<code class="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">${n}</code>`
|
||||
}));
|
||||
return renderSnippet(s, {});
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Fecha doda',
|
||||
accessorKey: 'doda_date',
|
||||
header: t('list_col_doda_date'),
|
||||
size: 100,
|
||||
cell: ({ row }) => {
|
||||
const dateSnippet = createRawSnippet<[{ date: string }]>((getProps) => {
|
||||
const { date } = getProps();
|
||||
return {
|
||||
render: () =>
|
||||
`<div class="text-sm text-muted-foreground">${date}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(dateSnippet, { date: formatDate(row.original.created_at) });
|
||||
const d = formatDodaDate(row.original.doda_date);
|
||||
const s = createRawSnippet(() => ({
|
||||
render: () => `<span class="text-sm tabular-nums">${d}</span>`
|
||||
}));
|
||||
return renderSnippet(s, {});
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'dispatch_customs',
|
||||
header: 'Desp',
|
||||
cell: ({ row }) => row.original.dispatch_customs || 'N/A'
|
||||
header: t('list_col_desp'),
|
||||
size: 60,
|
||||
cell: ({ row }) => row.original.dispatch_customs || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'patent',
|
||||
header: 'Patente',
|
||||
cell: ({ row }) => row.original.patent || 'N/A'
|
||||
header: t('list_col_patent'),
|
||||
size: 70,
|
||||
cell: ({ row }) => row.original.patent || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'pedimentos',
|
||||
header: 'Pedimento(s)',
|
||||
cell: ({ row }) => row.original.pedimentos || 'N/A'
|
||||
header: t('list_col_pedimentos'),
|
||||
cell: ({ row }) => {
|
||||
const v = row.original.pedimentos || '-';
|
||||
const s = createRawSnippet(() => ({
|
||||
render: () =>
|
||||
`<span class="block truncate max-w-[160px]" title="${v}">${v}</span>`
|
||||
}));
|
||||
return renderSnippet(s, {});
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'shipments',
|
||||
header: 'Remesa(s)',
|
||||
cell: ({ row }) => row.original.shipments || 'N/A'
|
||||
header: t('list_col_remesas'),
|
||||
size: 90,
|
||||
cell: ({ row }) => row.original.shipments || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'integration_number',
|
||||
header: 'Integracion',
|
||||
header: t('list_col_integracion'),
|
||||
size: 110,
|
||||
cell: ({ row }) => {
|
||||
const numberSnippet = createRawSnippet<[{ number?: string | null }]>((getProps) => {
|
||||
const { number } = getProps();
|
||||
return {
|
||||
render: () =>
|
||||
`<code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">${number || 'N/A'}</code>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(numberSnippet, { number: row.original.integration_number });
|
||||
const v = row.original.integration_number;
|
||||
const s = createRawSnippet(() => ({
|
||||
render: () =>
|
||||
v
|
||||
? `<code class="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">${v}</code>`
|
||||
: `<span class="text-muted-foreground">-</span>`
|
||||
}));
|
||||
return renderSnippet(s, {});
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'transaction_number',
|
||||
header: 'No transaccion',
|
||||
cell: ({ row }) => row.original.transaction_number || 'N/A'
|
||||
header: t('list_col_trans'),
|
||||
cell: ({ row }) => {
|
||||
const v = row.original.transaction_number || '-';
|
||||
const s = createRawSnippet(() => ({
|
||||
render: () =>
|
||||
`<span class="font-mono text-xs block truncate max-w-[160px]" title="${v}">${v}</span>`
|
||||
}));
|
||||
return renderSnippet(s, {});
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'transport_identification',
|
||||
header: 'Id transporte',
|
||||
cell: ({ row }) => row.original.transport_identification || 'N/A'
|
||||
header: t('list_col_id_transport'),
|
||||
size: 120,
|
||||
cell: ({ row }) => row.original.transport_identification || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'caat',
|
||||
header: 'CAAT',
|
||||
cell: ({ row }) => row.original.caat || 'N/A'
|
||||
header: t('list_col_caat'),
|
||||
size: 70,
|
||||
cell: ({ row }) => row.original.caat || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'last_user',
|
||||
header: 'Usuario',
|
||||
cell: ({ row }) => row.original.last_user || 'N/A'
|
||||
header: t('list_col_user'),
|
||||
size: 90,
|
||||
cell: ({ row }) => row.original.last_user || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Estatus',
|
||||
header: t('list_col_status'),
|
||||
size: 110,
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const statusSnippet = createRawSnippet<[{ status?: string | null }]>((getProps) => {
|
||||
const { status } = getProps();
|
||||
const colorClass = status === 'VALIDADO' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
|
||||
return {
|
||||
render: () =>
|
||||
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
|
||||
${status || '-'}
|
||||
</span>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(statusSnippet, { status });
|
||||
const status = (row.original.status || '').toUpperCase();
|
||||
const cls = STATUS_CLASSES[status] ?? 'bg-gray-100 text-gray-700 dark:bg-gray-800/50 dark:text-gray-300';
|
||||
const s = createRawSnippet(() => ({
|
||||
render: () =>
|
||||
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium whitespace-nowrap ${cls}">${status || '-'}</span>`
|
||||
}));
|
||||
return renderSnippet(s, {});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
<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 { Switch } from '$lib/components/ui/switch';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { createDoda, updateDoda, type Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
|
||||
import { obtenerAtajosFormularioDoda } from '$lib/config/shortcuts/dashboard/general_catalogs/doda/edit';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Doda | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar DODA ${item?.integration_number || ''}` : 'Nuevo DODA');
|
||||
|
||||
// Estado del formulario
|
||||
let activeTab = $state('general');
|
||||
let formData = $state({
|
||||
integration_number: '',
|
||||
doda_date: undefined as number | undefined,
|
||||
doda_time: undefined as number | undefined,
|
||||
dispatch_customs: '',
|
||||
customs_sections: '',
|
||||
patent: '',
|
||||
pedimentos: '',
|
||||
caat: '',
|
||||
transport_identification: '',
|
||||
fast_id: '',
|
||||
operation_type: '',
|
||||
selected: false,
|
||||
user_selected: '',
|
||||
last_user: '',
|
||||
responsible: '',
|
||||
carrier: '',
|
||||
shipments: '',
|
||||
pedimento_type: '',
|
||||
original_chain: '',
|
||||
serial_number: '',
|
||||
electronic_signature: '',
|
||||
transaction_number: '',
|
||||
status: '',
|
||||
linq_sat_qr: '',
|
||||
sat_certificate: '',
|
||||
sat_digital_seal: '',
|
||||
xml_doda_sent_path: '',
|
||||
xml_doda_response_path: '',
|
||||
sat_original_chain: '',
|
||||
customs_clearance: undefined as number | undefined,
|
||||
unique_badge_number: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Atajos
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
activeTab = 'general';
|
||||
if (item) {
|
||||
formData = {
|
||||
integration_number: item.integration_number || '',
|
||||
doda_date: item.doda_date,
|
||||
doda_time: item.doda_time,
|
||||
dispatch_customs: item.dispatch_customs || '',
|
||||
customs_sections: item.customs_sections || '',
|
||||
patent: item.patent || '',
|
||||
pedimentos: item.pedimentos || '',
|
||||
caat: item.caat || '',
|
||||
transport_identification: item.transport_identification || '',
|
||||
fast_id: item.fast_id || '',
|
||||
operation_type: item.operation_type || '',
|
||||
selected: item.selected || false,
|
||||
user_selected: item.user_selected || '',
|
||||
last_user: item.last_user || '',
|
||||
responsible: item.responsible || '',
|
||||
carrier: item.carrier || '',
|
||||
shipments: item.shipments || '',
|
||||
pedimento_type: item.pedimento_type || '',
|
||||
original_chain: item.original_chain || '',
|
||||
serial_number: item.serial_number || '',
|
||||
electronic_signature: item.electronic_signature || '',
|
||||
transaction_number: item.transaction_number || '',
|
||||
status: item.status || '',
|
||||
linq_sat_qr: item.linq_sat_qr || '',
|
||||
sat_certificate: item.sat_certificate || '',
|
||||
sat_digital_seal: item.sat_digital_seal || '',
|
||||
xml_doda_sent_path: item.xml_doda_sent_path || '',
|
||||
xml_doda_response_path: item.xml_doda_response_path || '',
|
||||
sat_original_chain: item.sat_original_chain || '',
|
||||
customs_clearance: item.customs_clearance,
|
||||
unique_badge_number: item.unique_badge_number || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
integration_number: '',
|
||||
doda_date: undefined,
|
||||
doda_time: undefined,
|
||||
dispatch_customs: '',
|
||||
customs_sections: '',
|
||||
patent: '',
|
||||
pedimentos: '',
|
||||
caat: '',
|
||||
transport_identification: '',
|
||||
fast_id: '',
|
||||
operation_type: '',
|
||||
selected: false,
|
||||
user_selected: '',
|
||||
last_user: '',
|
||||
responsible: '',
|
||||
carrier: '',
|
||||
shipments: '',
|
||||
pedimento_type: '',
|
||||
original_chain: '',
|
||||
serial_number: '',
|
||||
electronic_signature: '',
|
||||
transaction_number: '',
|
||||
status: '',
|
||||
linq_sat_qr: '',
|
||||
sat_certificate: '',
|
||||
sat_digital_seal: '',
|
||||
xml_doda_sent_path: '',
|
||||
xml_doda_response_path: '',
|
||||
sat_original_chain: '',
|
||||
customs_clearance: undefined,
|
||||
unique_badge_number: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const idToUpdate = item?.id;
|
||||
|
||||
if (isEdit && idToUpdate) {
|
||||
await updateDoda(idToUpdate, formData, companyId);
|
||||
} else {
|
||||
await createDoda(formData, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar DODA';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
class="py-4"
|
||||
>
|
||||
{#if error}
|
||||
<div class="mb-4 rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Tabs.Root bind:value={activeTab} class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-4">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="transport">Aduana/Transp.</Tabs.Trigger>
|
||||
<Tabs.Trigger value="sat">SAT / Digital</Tabs.Trigger>
|
||||
<Tabs.Trigger value="other">Otros</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<!-- TAB: GENERAL -->
|
||||
<Tabs.Content value="general" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="integration_number">No. Integración</Label>
|
||||
<Input
|
||||
id="integration_number"
|
||||
bind:value={formData.integration_number}
|
||||
maxlength={30}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="status">Estatus</Label>
|
||||
<Input id="status" bind:value={formData.status} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="doda_date">Fecha (YYYYMMDD)</Label>
|
||||
<Input type="number" id="doda_date" bind:value={formData.doda_date} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="doda_time">Hora (HHMMSS)</Label>
|
||||
<Input type="number" id="doda_time" bind:value={formData.doda_time} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="operation_type">Tipo Operación</Label>
|
||||
<Input id="operation_type" bind:value={formData.operation_type} maxlength={1} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimentos">Pedimentos</Label>
|
||||
<Input id="pedimentos" bind:value={formData.pedimentos} maxlength={80} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimento_type">Tipo Pedimento</Label>
|
||||
<Input id="pedimento_type" bind:value={formData.pedimento_type} maxlength={30} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- TAB: ADUANA / TRANSPORTE -->
|
||||
<Tabs.Content value="transport" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent">Patente</Label>
|
||||
<Input id="patent" bind:value={formData.patent} maxlength={4} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="dispatch_customs">Aduana Despacho</Label>
|
||||
<Input id="dispatch_customs" bind:value={formData.dispatch_customs} maxlength={3} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_sections">Sección Aduanera</Label>
|
||||
<Input id="customs_sections" bind:value={formData.customs_sections} maxlength={3} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="caat">CAAT</Label>
|
||||
<Input id="caat" bind:value={formData.caat} maxlength={10} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="carrier">Transportista (Carrier)</Label>
|
||||
<Input id="carrier" bind:value={formData.carrier} maxlength={8} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="transport_identification">Ident. Transporte</Label>
|
||||
<Input
|
||||
id="transport_identification"
|
||||
bind:value={formData.transport_identification}
|
||||
maxlength={20}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fast_id">FAST ID</Label>
|
||||
<Input id="fast_id" bind:value={formData.fast_id} maxlength={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="shipments">Embarques (Shipments)</Label>
|
||||
<Input id="shipments" bind:value={formData.shipments} maxlength={80} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_clearance">Despacho Aduanero (ID)</Label>
|
||||
<Input type="number" id="customs_clearance" bind:value={formData.customs_clearance} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- TAB: SAT / DIGITAL -->
|
||||
<Tabs.Content value="sat" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="serial_number">Número de Serie</Label>
|
||||
<Input id="serial_number" bind:value={formData.serial_number} maxlength={21} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="transaction_number">No. Transacción</Label>
|
||||
<Input
|
||||
id="transaction_number"
|
||||
bind:value={formData.transaction_number}
|
||||
maxlength={30}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="unique_badge_number">Número Único de Gafete</Label>
|
||||
<Input
|
||||
id="unique_badge_number"
|
||||
bind:value={formData.unique_badge_number}
|
||||
maxlength={250}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="original_chain">Cadena Original</Label>
|
||||
<Textarea id="original_chain" bind:value={formData.original_chain} class="h-20" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="electronic_signature">Firma Electrónica</Label>
|
||||
<Textarea
|
||||
id="electronic_signature"
|
||||
bind:value={formData.electronic_signature}
|
||||
class="h-20"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_digital_seal">Sello Digital SAT</Label>
|
||||
<Textarea id="sat_digital_seal" bind:value={formData.sat_digital_seal} class="h-20" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_original_chain">Cadena Original SAT</Label>
|
||||
<Textarea
|
||||
id="sat_original_chain"
|
||||
bind:value={formData.sat_original_chain}
|
||||
class="h-20"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="xml_doda_sent_path">Ruta XML Enviado</Label>
|
||||
<Input id="xml_doda_sent_path" bind:value={formData.xml_doda_sent_path} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="xml_doda_response_path">Ruta XML Respuesta</Label>
|
||||
<Input id="xml_doda_response_path" bind:value={formData.xml_doda_response_path} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- TAB: OTROS -->
|
||||
<Tabs.Content value="other" class="space-y-4 py-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch id="selected" bind:checked={formData.selected} />
|
||||
<Label for="selected">Seleccionado</Label>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="user_selected">Usuario Selección</Label>
|
||||
<Input id="user_selected" bind:value={formData.user_selected} maxlength={30} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="last_user">Último Usuario</Label>
|
||||
<Input id="last_user" bind:value={formData.last_user} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="responsible">Responsable</Label>
|
||||
<Input id="responsible" bind:value={formData.responsible} maxlength={14} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="linq_sat_qr">LINQ SAT QR</Label>
|
||||
<Input id="linq_sat_qr" bind:value={formData.linq_sat_qr} maxlength={1000} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_certificate">Certificado SAT</Label>
|
||||
<Input id="sat_certificate" bind:value={formData.sat_certificate} maxlength={2001} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
<Dialog.Footer class="mt-6">
|
||||
<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>
|
||||
@@ -5,8 +5,8 @@
|
||||
import type { Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { deleteDoda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
item,
|
||||
@@ -17,9 +17,6 @@
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Doda | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar este registro DODA?')) {
|
||||
@@ -28,6 +25,7 @@
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
toast.error('Selecciona una compañía');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,19 +33,15 @@
|
||||
|
||||
try {
|
||||
await deleteDoda(item.id, companyId);
|
||||
toast.success('DODA eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el registro';
|
||||
console.error('Error deleting doda:', err);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : 'Error al eliminar DODA';
|
||||
toast.error(msg);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
@@ -61,7 +55,7 @@
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={() => goto(`/dashboard/general_catalogs/doda/edit/${item.id}`)}>
|
||||
<DropdownMenu.Item onclick={() => goto(`/dashboard/general_catalogs/doda?doda_id=${item.id}`)}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
@@ -76,8 +70,3 @@
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 { dodaFormT } from '$lib/i18n/doda-form-strings';
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
@@ -13,6 +14,8 @@
|
||||
selectedId?: number | null;
|
||||
onRowClick?: (row: TData) => void;
|
||||
onRowDoubleClick?: (row: TData) => void;
|
||||
/** Solo catálogo DODA: textos i18n de carga / vacío */
|
||||
locale?: 'en' | 'es';
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -23,9 +26,12 @@
|
||||
loadMore,
|
||||
selectedId = null,
|
||||
onRowClick,
|
||||
onRowDoubleClick
|
||||
onRowDoubleClick,
|
||||
locale = 'es'
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const loc = $derived((locale === 'en' ? 'en' : 'es') as 'en' | 'es');
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() {
|
||||
return data;
|
||||
@@ -108,7 +114,7 @@
|
||||
{:else}
|
||||
<Table.Row inTabOrder={false}>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center text-sm text-muted-foreground">
|
||||
No hay resultados.
|
||||
{dodaFormT(loc, 'list_no_results')}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
@@ -122,10 +128,10 @@
|
||||
<div
|
||||
class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"
|
||||
></div>
|
||||
<span class="text-sm text-muted-foreground">Cargando más...</span>
|
||||
<span class="text-sm text-muted-foreground">{dodaFormT(loc, 'list_loading_more')}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-muted-foreground">Desplázate para cargar más</div>
|
||||
<div class="text-sm text-muted-foreground">{dodaFormT(loc, 'list_scroll_for_more')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
type WithId = { id: number | string };
|
||||
|
||||
export function applyOptimisticDelete<T extends WithId>(
|
||||
items: T[],
|
||||
total: number,
|
||||
deletedId: number | string
|
||||
): { items: T[]; total: number } {
|
||||
return {
|
||||
items: items.filter((item) => String(item.id) !== String(deletedId)),
|
||||
total: Math.max(0, total - 1)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos';
|
||||
import type { DodaPedimento, DodaPedimentoCreate } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
|
||||
export type PedimentoDetailRow = DodaPedimentoCreate & { id?: number; pedimento_line?: number };
|
||||
|
||||
/**
|
||||
* Misma convención que digitalización: AA-Patente-Pedimento
|
||||
*/
|
||||
export function buildPedimentoLabel(pedimento: Pedimento): string {
|
||||
return `${pedimento.customs_office?.slice(0, 2) || ''}-${pedimento.license || ''}-${pedimento.pedimento_number || ''}`.replace(
|
||||
/^-+|-+$/g,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
export function yyyymmddTodayInt(): number {
|
||||
const d = new Date();
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return Number(`${y}${m}${day}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sincroniza el campo `shipments` a partir de remesas en detalle (coma-separado).
|
||||
*/
|
||||
export function syncShipmentsFromPedimentos(
|
||||
detail: Array<Pick<DodaPedimento | PedimentoDetailRow, 'shipment'>>
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
for (const p of detail) {
|
||||
const s = (p.shipment || '').toString().trim();
|
||||
if (s && !parts.includes(s)) parts.push(s);
|
||||
}
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Une etiquetas y números de documento al campo legado `pedimentos`.
|
||||
*/
|
||||
export function appendPedimentosString(
|
||||
current: string | undefined,
|
||||
segment: string
|
||||
): string {
|
||||
const next = (segment || '').trim();
|
||||
if (!next) return current || '';
|
||||
const cur = (current || '').trim();
|
||||
if (!cur) return next;
|
||||
if (cur.split(/[;,]/).map((s) => s.trim()).includes(next)) return cur;
|
||||
return `${cur}; ${next}`;
|
||||
}
|
||||
|
||||
export function pedimentoRowFromCatalog(
|
||||
p: Pedimento,
|
||||
authorizationPatent: string
|
||||
): { row: DodaPedimentoCreate; label: string } {
|
||||
const document =
|
||||
(p.pedimento_number && String(p.pedimento_number)) ||
|
||||
buildPedimentoLabel(p) ||
|
||||
'';
|
||||
return {
|
||||
row: {
|
||||
authorization_patent: (authorizationPatent || p.license || '').trim() || undefined,
|
||||
document: document
|
||||
},
|
||||
label: buildPedimentoLabel(p)
|
||||
};
|
||||
}
|
||||
|
||||
/** `customs_clearance === 1` = PITA (legacy DespachoAduanero '3'): no aplica catálogo de tipo americano. */
|
||||
export function isPitaCustomsClearance(customsClearance: number | undefined | null): boolean {
|
||||
return customsClearance === 1;
|
||||
}
|
||||
|
||||
/** Códigos de error para mapear a i18n (`sidebar.doda_form.*`). */
|
||||
export type AmericanPedimentoTipoError = 'required' | 'import_range' | 'export_range' | 'op_undefined';
|
||||
|
||||
/**
|
||||
* Valida `american_pedimento_type` frente a `operation_type` (legacy Clarion).
|
||||
* Importación: tipos 1–5. Exportación: tipos 6–8.
|
||||
*/
|
||||
export function validateAmericanPedimentoTipo(
|
||||
operationType: string | undefined,
|
||||
tipo: string | undefined
|
||||
): AmericanPedimentoTipoError | null {
|
||||
const t = (tipo || '').trim();
|
||||
if (!t) return 'required';
|
||||
|
||||
const op = (operationType || '').trim().toUpperCase();
|
||||
const isImport = op === 'I' || op === '1';
|
||||
const isExport = op === 'E' || op === '2';
|
||||
|
||||
if (isImport) {
|
||||
if (!['1', '2', '3', '4', '5'].includes(t)) {
|
||||
return 'import_range';
|
||||
}
|
||||
} else if (isExport) {
|
||||
if (!['6', '7', '8'].includes(t)) {
|
||||
return 'export_range';
|
||||
}
|
||||
} else {
|
||||
return 'op_undefined';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ import {
|
||||
Hash,
|
||||
LayoutDashboard,
|
||||
Package,
|
||||
PackageCheck,
|
||||
Settings2,
|
||||
Shield,
|
||||
Ship,
|
||||
@@ -283,10 +284,6 @@ export function getSidebarData(): SidebarData {
|
||||
title: m["sidebar.general_catalogs.customs_warehouses"](),
|
||||
url: "/dashboard/reference_data/customs_warehouses",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.general_catalogs.doda"](),
|
||||
url: "/dashboard/general_catalogs/doda",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.general_catalogs.prevalidators"](),
|
||||
url: "/dashboard/general_catalogs/prevalidators",
|
||||
@@ -519,10 +516,19 @@ export function getSidebarData(): SidebarData {
|
||||
],
|
||||
},
|
||||
{
|
||||
title: m["sidebar.digitalizacion.title"](),
|
||||
url: "/dashboard/digitalizacion",
|
||||
icon: FolderArchive,
|
||||
items: [],
|
||||
title: m["sidebar.despacho.title"](),
|
||||
url: "#",
|
||||
icon: PackageCheck,
|
||||
items: [
|
||||
{
|
||||
title: m["sidebar.despacho.digitalizacion"](),
|
||||
url: "/dashboard/digitalizacion",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.despacho.doda"](),
|
||||
url: "/dashboard/despacho/doda",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: m["sidebar.reference_data.configuracion"](),
|
||||
|
||||
Reference in New Issue
Block a user