feature/api-doda-update

This commit is contained in:
2026-04-27 07:04:03 -06:00
parent 9d232998d6
commit 7f8599ac43
17 changed files with 4086 additions and 2793 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -552,6 +552,43 @@ async function fetchApiFormDataPost<T = any>(
});
}
/**
* Convierte cuerpos de error (JSON o texto) en un mensaje legible para toasts/UX.
* Evita mostrar JSON crudo p. ej. `{"error":"HTTP_ERROR","message":"..."}`.
*/
function messageFromBlobErrorResponse(text: string, status: number): string {
const raw = (text || '').trim();
if (!raw) {
return status === 404
? 'No se encontró el recurso. Prueba otro rango o vuelve a intentar.'
: `Error ${status} al descargar el archivo.`;
}
try {
const data = JSON.parse(raw) as Record<string, unknown>;
if (typeof data.message === 'string' && data.message.trim()) {
return data.message.trim();
}
const d = data.detail;
if (typeof d === 'string' && d.trim()) {
return d.trim();
}
if (Array.isArray(d) && d[0] && typeof (d[0] as { msg?: string }).msg === 'string') {
return String((d[0] as { msg: string }).msg).trim();
}
} catch {
// no es JSON: usar texto plano si es corto y legible
}
if (raw.length < 500 && !raw.startsWith('{')) {
return raw;
}
if (raw.startsWith('{')) {
return status === 404
? 'No se encontró información para exportar. Prueba otras fechas o amplía el rango.'
: `Error ${status} al descargar el archivo.`;
}
return raw;
}
async function fetchBlob(endpoint: string, options: RequestInit = {}): Promise<Blob> {
const token = getToken();
const headers: Record<string, string> = {
@@ -568,9 +605,8 @@ async function fetchBlob(endpoint: string, options: RequestInit = {}): Promise<B
});
if (!response.ok) {
// Try to extract some useful message for debugging/UI.
const text = await response.text().catch(() => '');
throw new Error(text || `Error ${response.status} descargando archivo`);
throw new Error(messageFromBlobErrorResponse(text, response.status));
}
return await response.blob();
}

View File

@@ -398,6 +398,29 @@ export async function exportDodaList(
URL.revokeObjectURL(url);
}
/**
* GET /v1/a76/doda/export/pedimentos/{id} — líneas de pedimento del DODA (TSV/csv/txt).
*/
export async function exportDodaPedimentosDetail(
dodaId: number,
companyId: number,
format: DodaExportFileFormat = 'xls'
): Promise<void> {
const params = new URLSearchParams({
company_id: String(companyId),
format
});
const blob = await api.getBlob(`/v1/a76/doda/export/pedimentos/${dodaId}?${params.toString()}`);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `doda_pedimentos_${dodaId}.${format}`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
// ── Alta DODA API ─────────────────────────────────────────────────────────── //
export interface DodaAltaResponse {

View File

@@ -73,7 +73,13 @@
toast.success(m['sidebar.doda_alta.export_excel_success']());
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
toast.error(msg || m['sidebar.doda_alta.export_excel_error']());
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;
}

View File

@@ -3,6 +3,7 @@
import { Button } from '$lib/components/ui/button';
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,6 +13,8 @@
let {
title = '',
/** `en` / `es` (viene del padre; evita leer `page` aquí, más seguro con SSR) */
locale: localeProp = 'es',
columns = [],
data = [],
onAdd,
@@ -24,6 +27,7 @@
class: className = ''
}: {
title?: string;
locale?: 'en' | 'es';
columns: Column[];
data: any[];
onAdd?: () => void;
@@ -34,6 +38,8 @@
class?: string;
} = $props();
const dodaLoc = $derived((localeProp === 'en' ? 'en' : 'es') as 'en' | 'es');
let selectedIndex = $state<number | null>(null);
$effect(() => {
@@ -86,7 +92,7 @@
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">Sin filas. «Nuevo» para añadir.</span>
<span class="text-xs">{dodaFormT(dodaLoc, 'child_empty')}</span>
</div>
</Table.Cell>
</Table.Row>
@@ -133,7 +139,7 @@
<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"
@@ -147,7 +153,7 @@
disabled={data.length === 0 || selectedIndex == null}
>
<Pencil class="h-3.5 w-3.5" />
Editar
{dodaFormT(dodaLoc, 'child_edit')}
</Button>
<Button
variant="destructive"
@@ -161,7 +167,7 @@
disabled={data.length === 0 || selectedIndex == null}
>
<Trash2 class="h-3.5 w-3.5" />
Borrar
{dodaFormT(dodaLoc, 'child_delete')}
</Button>
</div>
</div>

View File

@@ -2,6 +2,8 @@ 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';
/**
* doda_date se almacena como Integer con formato YYYYMMDD (ej. 20180409).
@@ -18,7 +20,7 @@ function formatDodaDate(val?: number | string | null): string {
}
// Fallback: ISO string
try {
return new Date(s).toLocaleDateString('es-MX', {
return new Date(s).toLocaleDateString(getLocale() === 'en' ? 'en-US' : 'es-MX', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
@@ -36,11 +38,12 @@ const STATUS_CLASSES: Record<string, string> = {
ELIMINADO: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
};
export function createColumns(): ColumnDef<Doda>[] {
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 n = row.original.id;
@@ -53,7 +56,7 @@ export function createColumns(): ColumnDef<Doda>[] {
},
{
accessorKey: 'doda_date',
header: 'Fecha Doda',
header: t('list_col_doda_date'),
size: 100,
cell: ({ row }) => {
const d = formatDodaDate(row.original.doda_date);
@@ -65,19 +68,19 @@ export function createColumns(): ColumnDef<Doda>[] {
},
{
accessorKey: 'dispatch_customs',
header: 'Desp.',
header: t('list_col_desp'),
size: 60,
cell: ({ row }) => row.original.dispatch_customs || '-'
},
{
accessorKey: 'patent',
header: 'Patente',
header: t('list_col_patent'),
size: 70,
cell: ({ row }) => row.original.patent || '-'
},
{
accessorKey: 'pedimentos',
header: 'Pedimento(s)',
header: t('list_col_pedimentos'),
cell: ({ row }) => {
const v = row.original.pedimentos || '-';
const s = createRawSnippet(() => ({
@@ -89,13 +92,13 @@ export function createColumns(): ColumnDef<Doda>[] {
},
{
accessorKey: 'shipments',
header: 'Remesa(s)',
header: t('list_col_remesas'),
size: 90,
cell: ({ row }) => row.original.shipments || '-'
},
{
accessorKey: 'integration_number',
header: 'Integración',
header: t('list_col_integracion'),
size: 110,
cell: ({ row }) => {
const v = row.original.integration_number;
@@ -110,7 +113,7 @@ export function createColumns(): ColumnDef<Doda>[] {
},
{
accessorKey: 'transaction_number',
header: 'No. Transacción',
header: t('list_col_trans'),
cell: ({ row }) => {
const v = row.original.transaction_number || '-';
const s = createRawSnippet(() => ({
@@ -122,25 +125,25 @@ export function createColumns(): ColumnDef<Doda>[] {
},
{
accessorKey: 'transport_identification',
header: 'Id. Transporte',
header: t('list_col_id_transport'),
size: 120,
cell: ({ row }) => row.original.transport_identification || '-'
},
{
accessorKey: 'caat',
header: 'CAAT',
header: t('list_col_caat'),
size: 70,
cell: ({ row }) => row.original.caat || '-'
},
{
accessorKey: 'last_user',
header: 'Usuario',
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 || '').toUpperCase();

View File

@@ -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>

View File

@@ -72,6 +72,9 @@ export function isPitaCustomsClearance(customsClearance: number | undefined | nu
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 15. Exportación: tipos 68.
@@ -79,9 +82,9 @@ export function isPitaCustomsClearance(customsClearance: number | undefined | nu
export function validateAmericanPedimentoTipo(
operationType: string | undefined,
tipo: string | undefined
): string | null {
): AmericanPedimentoTipoError | null {
const t = (tipo || '').trim();
if (!t) return 'El tipo de pedimento americano es obligatorio.';
if (!t) return 'required';
const op = (operationType || '').trim().toUpperCase();
const isImport = op === 'I' || op === '1';
@@ -89,14 +92,14 @@ export function validateAmericanPedimentoTipo(
if (isImport) {
if (!['1', '2', '3', '4', '5'].includes(t)) {
return 'El tipo de pedimento americano no es correcto para importación (debe ser 1, 2, 3, 4 o 5).';
return 'import_range';
}
} else if (isExport) {
if (!['6', '7', '8'].includes(t)) {
return 'El tipo de pedimento americano no es correcto para exportación (debe ser 6, 7 u 8).';
return 'export_range';
}
} else {
return 'Define el tipo de operación (I/E) antes de validar el pedimento americano.';
return 'op_undefined';
}
return null;
}

View File

@@ -0,0 +1,60 @@
/**
* Catálogo duplicado desde `messages/{locale}.json` → `sidebar.doda_form`.
* Los JSON viven bajo `src/` para que Vite los empaquete; importar `../../../messages/*`
* provocaba `GET /messages/*.json?import` → 403 con la config actual del dev server.
*/
import { getLocale } from '$lib/paraglide/runtime';
import esDoda from './doda-form/messages.es.json';
import enDoda from './doda-form/messages.en.json';
import type { AmericanPedimentoTipoError } from '$lib/components/dashboard/general_catalogs/doda/doda-form-helpers';
const esBlock = esDoda;
const enBlock = enDoda ?? esBlock;
if (!esBlock) {
throw new Error('doda-form: empty messages.es.json');
}
const blocks = { es: esBlock, en: enBlock! };
export type DodaFormKey = keyof typeof esDoda;
const AMERICAN_TIPO_ERR: Record<AmericanPedimentoTipoError, DodaFormKey> = {
required: 'err_american_tipo_required',
import_range: 'err_american_tipo_import',
export_range: 'err_american_tipo_export',
op_undefined: 'err_american_op_undefined'
};
function interpolate(template: string, params?: Record<string, string | number>) {
if (!params) return template;
return template.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? ''));
}
/**
* Idioma real de la app (cookie / Paraglie). No usar solo el path: el dashboard suele
* ser `/dashboard/...` con locale en `PARAGLIDE_LOCALE`, no `/en/dashboard/...`.
*/
export function dodaFormLocaleFromRuntime(): 'en' | 'es' {
return getLocale() === 'en' ? 'en' : 'es';
}
/** Si alguna ruta usara prefijo URL `/en/...`, puedes combinar con runtime. */
export function dodaFormLocaleFromPathname(pathname: string): 'en' | 'es' {
return pathname.split('/').filter(Boolean)[0] === 'en' ? 'en' : 'es';
}
export function dodaFormT(
locale: 'en' | 'es',
key: DodaFormKey,
params?: Record<string, string | number>
): string {
const block = blocks[locale] ?? blocks.es;
const raw = block[key] ?? blocks.es[key] ?? String(key);
return interpolate(String(raw), params);
}
export function dodaFormAmericanTipoMessage(
locale: 'en' | 'es',
code: AmericanPedimentoTipoError
): string {
return dodaFormT(locale, AMERICAN_TIPO_ERR[code]);
}

View File

@@ -0,0 +1,195 @@
{
"shortcuts_scope": "DODA form",
"title_new": "New DODA",
"title_edit": "Edit DODA",
"description_catalog": "Catalogs · DODA",
"tab_general": "General",
"tab_seals_sat": "Seals and SAT",
"shortcuts_hint": "Alt+1/2 · Ctrl+S save · Esc cancel",
"btn_cancel": "Cancel",
"btn_save": "Save",
"btn_saving": "Saving...",
"btn_save_changes": "Save changes",
"btn_create_doda": "Create DODA",
"btn_accept": "OK",
"card_broker_customs": "Customs agent and office",
"card_transport": "Transport",
"card_control": "Control and dispatch",
"card_sat_chain": "Original chain and signatures (SAT)",
"label_responsible": "Broker",
"label_patent": "Patent",
"label_dispatch": "Dispatch office",
"label_section_es": "E/S section",
"label_operation_type": "Operation type",
"label_transporter": "Carrier",
"label_transport_id": "Transport ID",
"label_caat": "CAAT",
"label_doda_date": "DODA date",
"label_status": "Status",
"label_dispatch_type": "Dispatch type",
"label_unique_badge": "Unique badge",
"label_integration_num": "Integration No.",
"label_transaction_num": "Transaction No.",
"label_fast_id": "Fast ID",
"label_last_user": "Last user",
"label_original_chain": "Original chain",
"label_serial_cert": "Serial (certificate)",
"label_uuid_cp": "Carta porte UUID",
"label_electronic_sig": "Electronic signature",
"label_sat_cert": "SAT certificate",
"label_sat_chain": "Original SAT chain",
"ph_aga": "AGA key",
"ph_0000": "0000",
"ph_000": "000",
"ph_select": "Select",
"ph_plate": "Plate / vehicle ID",
"ph_dash": "—",
"ph_yyyymmdd": "YYYYMMDD",
"ph_badge_pita": "N/A — PITA",
"ph_badge_num": "Badge no.",
"ph_example_container": "E.g. 53056",
"op_import": "I — Import",
"op_export": "E — Export",
"type_pita": "PITA",
"type_doda": "DODA",
"vu_checking": "Verifying agent VU DODA…",
"vu_incomplete": "VU DODA incomplete: agent needs .cer, .key, and DODA FIEL password.",
"vu_complete": "VU DODA complete for API submission.",
"badge_required_hint": "Required for DODA filing API.",
"pedimentos": "Pedimentos",
"lines": "lines",
"containers": "Containers",
"american_pedimentos": "U.S. pedimentos",
"seals_block_title": "Seals — total in DODA: {n} / 8",
"seals_help": "Select a container. Maximum 8 seals per DODA (SCAII).",
"seals_select_container": "Select a container in the table to view or edit its seals.",
"container_no_id_warning": "Container not saved on server. Enter value, press Save; new containers are sent and reloaded with id for seals.",
"container_line_info": "Container:",
"seal_on_line": "seal(s) on this line",
"line_word": "Line",
"btn_add_seal": "Add seal",
"btn_seal_delete": "Delete",
"seals_empty_line": "No seals on this container.",
"col_line": "Line",
"col_auth_patent": "Auth. patent",
"col_document": "Document",
"col_remesa": "Shipment",
"col_cove": "COVE",
"col_umc": "UMC",
"col_cash_usd": "Cash USD",
"col_diff_usd": "Difference USD",
"col_dta_niu": "DTA NIU",
"col_art7": "Art. 7",
"col_container": "Container",
"col_seals": "Seals",
"col_seal_value": "Seal",
"col_american_type": "Type",
"col_american_ped": "U.S. pedimento",
"col_pedimento_only": "U.S. pedimento",
"yes": "Yes",
"no": "No",
"child_empty": "No rows. “New” to add.",
"child_new": "New",
"child_edit": "Edit",
"child_delete": "Delete",
"modal_container_new": "New container",
"modal_container_edit": "Edit container",
"modal_container_desc": "Enter the container value for the DODA declaration.",
"label_container_value": "Container value",
"modal_seals_in_container": "Seals in container",
"seal_modal_title": "Containers > Seal",
"seal_modal_desc": "Enter the seal value for the selected container.",
"label_seal": "Seal",
"ph_seal": "Seal value",
"american_modal_title": "U.S. pedimento",
"american_modal_desc": "Enter type and value of the U.S. pedimento.",
"label_american_type_short": "U.S. type",
"label_american_value": "U.S. pedimento",
"ph_american_value": "U.S. pedimento value",
"line_label": "Line:",
"select_type": "Select type",
"american_cat_6": "AMERICAN PEDIMENTO",
"american_cat_7": "SELF-DECLARATION",
"american_cat_8": "NOT PRESENT",
"err_american_tipo_required": "U.S. pedimento type is required.",
"err_american_tipo_import": "U.S. pedimento type is not valid for import (must be 1, 2, 3, 4, or 5).",
"err_american_tipo_export": "U.S. pedimento type is not valid for export (must be 6, 7, or 8).",
"err_american_op_undefined": "Set operation type (I/E) before validating the U.S. pedimento.",
"err_company": "Select a company",
"err_responsible": "Broker is required",
"err_patent": "Patent is required",
"err_transport": "Transport ID is required. Select a vehicle.",
"err_badge": "Unique badge number is required for DODA filing.",
"err_vu_wait": "Wait for agent VU DODA check to finish, then try again.",
"err_vu_config": "The customs agent does not have full VU DODA config (.cer, .key, DODA FIEL password).",
"err_min_containers": "Add at least one container for API submission.",
"err_american_new_lines": "Enter the U.S. pedimento value for each new line.",
"err_save": "Error saving",
"toast_saved": "Changes saved successfully.",
"toast_created": "DODA created successfully.",
"load_error": "Could not load DODA",
"warn_vu_incomplete": "This DODAs agent does not have full VU DODA (.cer, .key, DODA FIEL password).",
"warn_vu_fetch": "Could not validate the agents VU settings.",
"warn_broker_select": "Selected agent has incomplete VU DODA. Configure in Customs agents before generating.",
"seal_save_first": "Save the DODA before managing seals.",
"seal_pick_container": "Select a container in the table.",
"seal_not_persisted": "This container is not on the server yet. Save the DODA and reload.",
"seal_empty": "Seal cannot be empty.",
"seal_max": "DODA already has the maximum 8 seals.",
"seal_add_err": "Error adding seal",
"seal_delete_err": "Error removing seal",
"pedimento_remove_blocked": "Cannot remove pedimentos already saved on the server here.",
"container_delete_err": "Error deleting container",
"american_delete_err": "Error deleting U.S. pedimento",
"container_update_err": "Error updating container",
"american_cannot_edit_persisted": "To change saved U.S. pedimentos, remove and add again.",
"err_american_value": "Enter the U.S. pedimento value.",
"err_american_type_or_value": "Enter type and/or U.S. pedimento value.",
"err_containers_max": "A DODA can have at most 4 containers.",
"err_container_empty": "Container value cannot be empty.",
"err_container_not_found": "Container to edit not found.",
"pedimento_selector_title": "Containers > Seal",
"list_page_subtitle": "Manage your Customs Operation Documents (DODA)",
"list_btn_new": "New DODA",
"list_card_title": "DODA list",
"list_ph_folio": "Folio",
"list_ph_patent": "Patent",
"list_filter_status_ph": "Status",
"list_filter_status_all": "All",
"list_filter_op_import": "Import",
"list_filter_op_export": "Export",
"list_filter_op": "Operation",
"list_filter_op_all": "All",
"list_btn_clear": "Clear",
"list_showing": "Showing {a} of {b} records",
"list_active_filters": "Active filters: {n}",
"list_btn_edit": "Edit",
"list_btn_print": "Print",
"list_toast_reload_error": "Error reloading data",
"list_elig_error_prefix": "Error checking eligibility: ",
"list_elig_not_meet": "This DODA does not meet the filing requirements.",
"list_alta_error_prefix": "Error sending DODA filing: ",
"list_print_error": "Error generating DODA PDF",
"list_alta_complete": "DODA filing completed successfully",
"list_shortcuts_scope": "DODA list",
"list_col_folio": "Folio",
"list_col_doda_date": "DODA date",
"list_col_desp": "Cstm.",
"list_col_patent": "Patent",
"list_col_pedimentos": "Pedimento(s)",
"list_col_remesas": "Shipment(s)",
"list_col_integracion": "Integration",
"list_col_trans": "Trans. no.",
"list_col_id_transport": "Transport ID",
"list_col_caat": "CAAT",
"list_col_user": "User",
"list_col_status": "Status",
"list_loading_more": "Loading more...",
"list_scroll_for_more": "Scroll to load more",
"list_confirm_delete": "Are you sure you want to delete this DODA record?",
"list_toast_delete_ok": "DODA deleted successfully",
"list_toast_delete_err": "Error deleting DODA",
"list_filter_i": "I — Import",
"list_filter_e": "E — Export",
"list_no_results": "No results."
}

View File

@@ -0,0 +1,195 @@
{
"shortcuts_scope": "Formulario DODA",
"title_new": "Nuevo DODA",
"title_edit": "Editar DODA",
"description_catalog": "Catálogos · DODA",
"tab_general": "General",
"tab_seals_sat": "Sellos y SAT",
"shortcuts_hint": "Alt+1/2 · Ctrl+S guardar · Esc cancelar",
"btn_cancel": "Cancelar",
"btn_save": "Guardar",
"btn_saving": "Guardando...",
"btn_save_changes": "Guardar cambios",
"btn_create_doda": "Crear DODA",
"btn_accept": "Aceptar",
"card_broker_customs": "Agente aduanal y aduana",
"card_transport": "Transporte",
"card_control": "Control y despacho",
"card_sat_chain": "Cadena original y firmas (SAT)",
"label_responsible": "Responsable",
"label_patent": "Patente",
"label_dispatch": "Aduana despacho",
"label_section_es": "Aduana sección E/S",
"label_operation_type": "Tipo operación",
"label_transporter": "Transportista",
"label_transport_id": "ID transporte",
"label_caat": "CAAT",
"label_doda_date": "Fecha DODA",
"label_status": "Estatus",
"label_dispatch_type": "Tipo despacho",
"label_unique_badge": "Gafete único",
"label_integration_num": "Núm. integración",
"label_transaction_num": "Núm. transacción",
"label_fast_id": "Fast ID",
"label_last_user": "Último usuario",
"label_original_chain": "Cadena original",
"label_serial_cert": "Núm. serie (certificado)",
"label_uuid_cp": "UUID carta porte",
"label_electronic_sig": "Firma electrónica",
"label_sat_cert": "Certificado SAT",
"label_sat_chain": "Cadena original SAT",
"ph_aga": "Clave AGA",
"ph_0000": "0000",
"ph_000": "000",
"ph_select": "Seleccionar",
"ph_plate": "Placa / ID vehículo",
"ph_dash": "—",
"ph_yyyymmdd": "AAAAMMDD",
"ph_badge_pita": "N/A — PITA",
"ph_badge_num": "Núm. gafete",
"ph_example_container": "Ej. 53056",
"op_import": "I — Importación",
"op_export": "E — Exportación",
"type_pita": "PITA",
"type_doda": "DODA",
"vu_checking": "Verificando VU DODA del agente…",
"vu_incomplete": "VU DODA incompleta: se requiere .cer, .key y clave FIEL DODA del agente.",
"vu_complete": "VU DODA completa para envío a API.",
"badge_required_hint": "Requerido para alta DODA en API.",
"pedimentos": "Pedimentos",
"lines": "líneas",
"containers": "Contenedores",
"american_pedimentos": "Pedimentos americanos",
"seals_block_title": "Precintos (candados) — total en el DODA: {n} / 8",
"seals_help": "Selecciona un contenedor en la tabla. Máximo 8 precintos en todo el DODA (regla SCAII).",
"seals_select_container": "Selecciona un contenedor en la tabla de contenedores para ver o editar sus precintos.",
"container_no_id_warning": "Contenedor sin id en el servidor. Completa el valor, pulsa Guardar (arriba); al guardar se envían contenedores nuevos y se recargan con id para precintos.",
"container_line_info": "Contenedor:",
"seal_on_line": "precinto(s) en esta línea",
"line_word": "Línea",
"btn_add_seal": "Agregar precinto",
"btn_seal_delete": "Eliminar",
"seals_empty_line": "Sin precintos en este contenedor.",
"col_line": "Línea",
"col_auth_patent": "Patente auth.",
"col_document": "Documento",
"col_remesa": "Remesa",
"col_cove": "COVE",
"col_umc": "UMC",
"col_cash_usd": "Efectivo USD",
"col_diff_usd": "Diferencia USD",
"col_dta_niu": "DTA NIU",
"col_art7": "Art. 7",
"col_container": "Contenedor",
"col_seals": "Precintos",
"col_seal_value": "Precinto",
"col_american_type": "Tipo",
"col_american_ped": "Pedimento americano",
"col_pedimento_only": "Pedimento americano",
"yes": "Sí",
"no": "No",
"child_empty": "Sin filas. «Nuevo» para añadir.",
"child_new": "Nuevo",
"child_edit": "Editar",
"child_delete": "Borrar",
"modal_container_new": "Nuevo contenedor",
"modal_container_edit": "Editar contenedor",
"modal_container_desc": "Captura el valor del contenedor para la declaración DODA.",
"label_container_value": "Valor contenedor",
"modal_seals_in_container": "Precintos del contenedor",
"seal_modal_title": "Contenedores > Precinto",
"seal_modal_desc": "Captura el valor del precinto para el contenedor seleccionado.",
"label_seal": "Precinto",
"ph_seal": "Valor del precinto",
"american_modal_title": "Pedimento Americano",
"american_modal_desc": "Captura el tipo y valor del pedimento americano.",
"label_american_type_short": "Tipo Ped. Americano",
"label_american_value": "Pedimento Americano",
"ph_american_value": "Valor pedimento americano",
"line_label": "Línea:",
"select_type": "Selecciona tipo",
"american_cat_6": "PEDIMENTO AMERICANO",
"american_cat_7": "AUTODECLARACION",
"american_cat_8": "NO PRESENTA",
"err_american_tipo_required": "El tipo de pedimento americano es obligatorio.",
"err_american_tipo_import": "El tipo de pedimento americano no es correcto para importación (debe ser 1, 2, 3, 4 o 5).",
"err_american_tipo_export": "El tipo de pedimento americano no es correcto para exportación (debe ser 6, 7 u 8).",
"err_american_op_undefined": "Define el tipo de operación (I/E) antes de validar el pedimento americano.",
"err_company": "Selecciona una compañía",
"err_responsible": "El Responsable es requerido",
"err_patent": "El Agente Aduanal (Patente) es requerido",
"err_transport": "La Identificación de Transporte es requerida. Selecciona un vehículo.",
"err_badge": "El Número de Gafete Único es requerido para Alta DODA.",
"err_vu_wait": "Espera a que termine la verificación VU DODA del agente e intenta de nuevo.",
"err_vu_config": "El agente aduanal no tiene configuración VU DODA completa (.cer, .key y clave FIEL DODA).",
"err_min_containers": "Agrega al menos un contenedor con valor para el envío a API.",
"err_american_new_lines": "Indique el valor del pedimento americano en cada línea nueva.",
"err_save": "Error al guardar",
"toast_saved": "Cambios guardados correctamente.",
"toast_created": "DODA creado correctamente.",
"load_error": "No se pudo cargar la información del DODA",
"warn_vu_incomplete": "El agente aduanal de este DODA no tiene VU DODA completa (.cer, .key y clave FIEL DODA).",
"warn_vu_fetch": "No se pudo validar la configuración VU del agente aduanal.",
"warn_broker_select": "El agente seleccionado no tiene VU DODA completa (.cer, .key y clave FIEL DODA). Configúralo en Agentes Aduanales antes de generar.",
"seal_save_first": "Guarda el DODA antes de gestionar precintos.",
"seal_pick_container": "Selecciona un contenedor en la tabla.",
"seal_not_persisted": "Este contenedor aún no está guardado en el servidor. Guarda el DODA (Guardar) y vuelve a abrir o recarga.",
"seal_empty": "El precinto no puede estar vacío.",
"seal_max": "El DODA ya tiene el máximo de 8 precintos.",
"seal_add_err": "Error al agregar el precinto",
"seal_delete_err": "Error al eliminar el precinto",
"pedimento_remove_blocked": "Los pedimentos guardados en servidor no se pueden quitar aquí.",
"container_delete_err": "Error al eliminar el contenedor",
"american_delete_err": "Error al eliminar el pedimento americano",
"container_update_err": "Error al actualizar el contenedor",
"american_cannot_edit_persisted": "Para editar pedimentos americanos guardados, elimínalo y créalo nuevamente.",
"err_american_value": "Indique el valor del pedimento americano.",
"err_american_type_or_value": "Capture tipo o valor del pedimento americano.",
"err_containers_max": "El DODA solo puede tener máximo 4 contenedores.",
"err_container_empty": "El valor del contenedor no puede estar vacío.",
"err_container_not_found": "No se encontró el contenedor a editar.",
"pedimento_selector_title": "Contenedores &gt; Precinto",
"list_page_subtitle": "Gestiona tus Documentos de Operación Aduanera (DODA)",
"list_btn_new": "Nuevo DODA",
"list_card_title": "Listado de DODA",
"list_ph_folio": "Folio",
"list_ph_patent": "Patente",
"list_filter_status_ph": "Estatus",
"list_filter_status_all": "Todos",
"list_filter_op_import": "Importación",
"list_filter_op_export": "Exportación",
"list_filter_op": "Operación",
"list_filter_op_all": "Todas",
"list_btn_clear": "Limpiar",
"list_showing": "Mostrando {a} de {b} registros",
"list_active_filters": "Filtros activos: {n}",
"list_btn_edit": "Editar",
"list_btn_print": "Imprimir",
"list_toast_reload_error": "Error al recargar datos",
"list_elig_error_prefix": "Error al verificar elegibilidad: ",
"list_elig_not_meet": "El DODA no cumple con los requisitos de alta.",
"list_alta_error_prefix": "Error al enviar alta DODA: ",
"list_print_error": "Error al generar el PDF del DODA",
"list_alta_complete": "Alta DODA completada correctamente",
"list_shortcuts_scope": "Lista DODA",
"list_col_folio": "Folio",
"list_col_doda_date": "Fecha DODA",
"list_col_desp": "Desp.",
"list_col_patent": "Patente",
"list_col_pedimentos": "Pedimento(s)",
"list_col_remesas": "Remesa(s)",
"list_col_integracion": "Integración",
"list_col_trans": "Núm. Transacción",
"list_col_id_transport": "Id. Transporte",
"list_col_caat": "CAAT",
"list_col_user": "Usuario",
"list_col_status": "Estatus",
"list_loading_more": "Cargando más...",
"list_scroll_for_more": "Desplázate para cargar más",
"list_confirm_delete": "¿Está seguro de eliminar este registro DODA?",
"list_toast_delete_ok": "DODA eliminado correctamente",
"list_toast_delete_err": "Error al eliminar DODA",
"list_filter_i": "I - Importación",
"list_filter_e": "E - Exportación",
"list_no_results": "No hay resultados."
}

View File

@@ -13,7 +13,8 @@
RotateCcw,
Send,
Loader2,
FileSpreadsheet
FileSpreadsheet,
Table
} from 'lucide-svelte';
import * as Card from '$lib/components/ui/card';
import * as Select from '$lib/components/ui/select';
@@ -32,6 +33,7 @@
import {
getDodas,
deleteDoda,
exportDodaPedimentosDetail,
postDodaAlta,
getDodaElegibilidad,
type Doda
@@ -71,6 +73,7 @@
let currentVariant = $state<'doda' | 'pita'>('doda');
let altaLoading = $state(false);
let deleteLoading = $state(false);
let pedimentosExportLoading = $state(false);
$effect(() => {
if (data.dodas) {
@@ -164,6 +167,28 @@
}
}
async function handleExportPedimentos() {
if (pedimentosExportLoading) return;
if (!companyStore.activeCompany) {
toast.error(m['sidebar.doda_alta.delete_missing_company']());
return;
}
if (selectedDodaIds.length !== 1 || !selectedDoda) {
toast.error(m['sidebar.doda_alta.delete_select_one']());
return;
}
pedimentosExportLoading = true;
try {
await exportDodaPedimentosDetail(selectedDoda.id, companyStore.activeCompany.id, 'xls');
toast.success(m['sidebar.doda_alta.export_pedimentos_success']());
} catch (e) {
const msg = e instanceof Error ? e.message : m['sidebar.doda_alta.export_pedimentos_error']();
toast.error(msg || m['sidebar.doda_alta.export_pedimentos_error']());
} finally {
pedimentosExportLoading = false;
}
}
function clearFilters() {
filters = { integration_number: '', patent: '', status: '', operation_type: '' };
}
@@ -341,6 +366,21 @@
<FileSpreadsheet size={16} class="mr-2" />
{m['sidebar.doda_alta.action_export_excel']()}
</Button>
<Button
size="sm"
variant="secondary"
class="border border-primary/25 bg-primary/10 text-primary hover:bg-primary/15"
onclick={handleExportPedimentos}
disabled={selectedDodaIds.length !== 1 || pedimentosExportLoading || !companyStore.activeCompany}
title={m['sidebar.doda_alta.action_export_pedimentos']()}
>
{#if pedimentosExportLoading}
<Loader2 size={16} class="mr-2 animate-spin" />
{:else}
<Table size={16} class="mr-2" />
{/if}
{m['sidebar.doda_alta.action_export_pedimentos']()}
</Button>
<Separator orientation="vertical" class="mx-1 h-8 hidden sm:block" />
<Button
size="sm"

View File

@@ -1,10 +1,11 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import {
getDodas,
deleteDoda,
exportDodaPedimentosDetail,
printDoda,
postDodaAlta,
getDodaElegibilidad,
@@ -12,6 +13,11 @@
} from '$lib/api/dashboard/a76/general_catalogs/doda';
import { toast } from 'svelte-sonner';
import { m } from '$lib/i18n/messages';
import {
dodaFormT,
dodaFormLocaleFromRuntime,
type DodaFormKey
} from '$lib/i18n/doda-form-strings';
import { isPitaCustomsClearance } from '$lib/components/dashboard/general_catalogs/doda/doda-form-helpers';
import {
Plus,
@@ -24,7 +30,8 @@
LayoutGrid,
Printer,
Send,
Loader2
Loader2,
Table
} from 'lucide-svelte';
import * as Card from '$lib/components/ui/card';
import * as Select from '$lib/components/ui/select';
@@ -42,25 +49,36 @@
let { data } = $props();
const dodaIdFromUrl = $derived($page.url.searchParams.get('doda_id') ?? '');
const dodaLoc = $derived.by(() => {
void page.url.pathname;
return dodaFormLocaleFromRuntime();
});
function dt(key: DodaFormKey, params?: Record<string, string | number>) {
return dodaFormT(dodaFormLocaleFromRuntime(), key, params);
}
const tableColumns = $derived(createColumns(dodaLoc));
const dodaIdFromUrl = $derived(page.url.searchParams.get('doda_id') ?? '');
function closeDodaForm() {
if (!browser) return;
const u = new URL($page.url);
const u = new URL(page.url);
u.searchParams.delete('doda_id');
const q = u.searchParams.toString();
void goto(u.pathname + (q ? `?${q}` : ''), { replaceState: true, noScroll: true });
}
function setDodaIdQuery(id: number | 'new') {
const u = new URL($page.url);
const u = new URL(page.url);
u.searchParams.set('doda_id', id === 'new' ? 'new' : String(id));
const q = u.searchParams.toString();
void goto(`${u.pathname}?${q}`, { noScroll: true });
}
function onCreatedDodaToEdit(dodaId: number) {
const u = new URL($page.url);
const u = new URL(page.url);
u.searchParams.set('doda_id', String(dodaId));
const q = u.searchParams.toString();
void goto(`${u.pathname}?${q}`, { replaceState: true, noScroll: true });
@@ -69,10 +87,10 @@
// Filtros centralizados
let filters = $state({
integration_number: $page.url.searchParams.get('integration_number') || '',
patent: $page.url.searchParams.get('patent') || '',
status: $page.url.searchParams.get('status') || '',
operation_type: $page.url.searchParams.get('operation_type') || ''
integration_number: page.url.searchParams.get('integration_number') || '',
patent: page.url.searchParams.get('patent') || '',
status: page.url.searchParams.get('status') || '',
operation_type: page.url.searchParams.get('operation_type') || ''
});
let timeout: ReturnType<typeof setTimeout>;
@@ -96,6 +114,7 @@
let altaLoading = $state(false);
let printLoading = $state(false);
let deleteLoading = $state(false);
let pedimentosExportLoading = $state(false);
let progressDialogOpen = $state(false);
let currentTaskId = $state('');
let currentVariant = $state<'doda' | 'pita'>('doda');
@@ -121,7 +140,7 @@
if (filters.status) params.set('status', filters.status);
if (filters.operation_type) params.set('operation_type', filters.operation_type);
const modalId = $page.url.searchParams.get('doda_id');
const modalId = page.url.searchParams.get('doda_id');
if (modalId) params.set('doda_id', modalId);
const queryString = params.toString();
@@ -146,7 +165,7 @@
// Atajos
useShortcuts(
'Lista DODA',
dodaFormT(dodaLoc, 'list_shortcuts_scope'),
obtenerAtajosListaDoda({
manejarNuevo: handleCreateClick,
manejarActualizar: reloadData
@@ -202,7 +221,7 @@
} catch (e) {
console.error('Error reloading DODAs:', e);
// Evitar mostrar error si es solo carga inicial y falla por falta de login etc
if (allItems.length > 0) toast.error('Error al recargar datos');
if (allItems.length > 0) toast.error(dt('list_toast_reload_error'));
} finally {
loading = false;
}
@@ -260,6 +279,28 @@
}
}
async function handleExportPedimentos() {
if (pedimentosExportLoading) return;
if (!companyStore.activeCompany) {
toast.error(m['sidebar.doda_alta.delete_missing_company']());
return;
}
if (selectedIds.length !== 1 || !selectedDoda) {
toast.error(m['sidebar.doda_alta.delete_select_one']());
return;
}
pedimentosExportLoading = true;
try {
await exportDodaPedimentosDetail(selectedDoda.id, companyStore.activeCompany.id, 'xls');
toast.success(m['sidebar.doda_alta.export_pedimentos_success']());
} catch (e) {
const msg = e instanceof Error ? e.message : m['sidebar.doda_alta.export_pedimentos_error']();
toast.error(msg || m['sidebar.doda_alta.export_pedimentos_error']());
} finally {
pedimentosExportLoading = false;
}
}
async function handleAlta() {
if (!selectedDoda || !companyStore.activeCompany) return;
const companyId = companyStore.activeCompany.id;
@@ -269,18 +310,18 @@
try {
const elig = await getDodaElegibilidad(dodaId, companyId, variant);
if (elig.error) {
toast.error(`Error al verificar elegibilidad: ${elig.error}`);
toast.error(`${dt('list_elig_error_prefix')}${elig.error}`);
return;
}
if (elig.data && !elig.data.can_alta) {
const msgs = elig.data.reasons.map((r) => `- ${r.message}`).join('\n');
toast.error(msgs || 'El DODA no cumple con los requisitos de alta.');
toast.error(msgs || dt('list_elig_not_meet'));
return;
}
const resp = await postDodaAlta(dodaId, companyId, variant);
if (resp.error) {
toast.error(`Error al enviar alta DODA: ${resp.error}`);
toast.error(`${dt('list_alta_error_prefix')}${resp.error}`);
return;
}
@@ -300,7 +341,7 @@
try {
await printDoda(dodaId, companyId);
} catch (e) {
const msg = e instanceof Error ? e.message : 'Error al generar el PDF del DODA';
const msg = e instanceof Error ? e.message : dt('list_print_error');
toast.error(msg);
} finally {
printLoading = false;
@@ -310,7 +351,7 @@
function onAltaComplete() {
progressDialogOpen = false;
reloadData();
toast.success('Alta DODA completada correctamente');
toast.success(dt('list_alta_complete'));
}
function handleRowClick(row: Doda) {
@@ -323,12 +364,12 @@
<div class="flex-none flex items-center justify-between">
<div class="space-y-1">
<h1 class="text-2xl font-bold tracking-tight">DODA</h1>
<p class="text-muted-foreground">Gestiona tus Documentos de Operación Aduanera (DODA)</p>
<p class="text-muted-foreground">{dodaFormT(dodaLoc, 'list_page_subtitle')}</p>
</div>
<div class="flex items-center gap-3">
<Button class="h-9" onclick={handleCreateClick}>
<Plus class="mr-2 h-4 w-4" />
Nuevo DODA
{dodaFormT(dodaLoc, 'list_btn_new')}
</Button>
</div>
</div>
@@ -336,27 +377,31 @@
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
<Card.Header>
<div class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<Card.Title>Listado de DODA</Card.Title>
<Card.Title>{dodaFormT(dodaLoc, 'list_card_title')}</Card.Title>
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-[220px_180px_170px_170px_auto] xl:items-center">
<div class="relative">
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Folio"
placeholder={dodaFormT(dodaLoc, 'list_ph_folio')}
bind:value={filters.integration_number}
class="h-9 bg-card pl-9"
/>
</div>
<Input placeholder="Patente" bind:value={filters.patent} class="h-9 bg-card" />
<Input
placeholder={dodaFormT(dodaLoc, 'list_ph_patent')}
bind:value={filters.patent}
class="h-9 bg-card"
/>
<Select.Root
type="single"
value={filters.status}
onValueChange={(v) => (filters.status = v)}
>
<Select.Trigger class="h-9 w-full bg-card">
{filters.status || 'Estatus'}
{filters.status || dodaFormT(dodaLoc, 'list_filter_status_ph')}
</Select.Trigger>
<Select.Content>
<Select.Item value="">Todos</Select.Item>
<Select.Item value="">{dodaFormT(dodaLoc, 'list_filter_status_all')}</Select.Item>
<Select.Item value="PENDIENTE">PENDIENTE</Select.Item>
<Select.Item value="GENERADO">GENERADO</Select.Item>
<Select.Item value="VALIDADO">VALIDADO</Select.Item>
@@ -370,20 +415,20 @@
>
<Select.Trigger class="h-9 w-full bg-card">
{filters.operation_type === 'I'
? 'Importación'
? dodaFormT(dodaLoc, 'list_filter_op_import')
: filters.operation_type === 'E'
? 'Exportación'
: 'Operación'}
? dodaFormT(dodaLoc, 'list_filter_op_export')
: dodaFormT(dodaLoc, 'list_filter_op')}
</Select.Trigger>
<Select.Content>
<Select.Item value="">Todas</Select.Item>
<Select.Item value="I">I - Importación</Select.Item>
<Select.Item value="E">E - Exportación</Select.Item>
<Select.Item value="">{dodaFormT(dodaLoc, 'list_filter_op_all')}</Select.Item>
<Select.Item value="I">{dodaFormT(dodaLoc, 'list_filter_i')}</Select.Item>
<Select.Item value="E">{dodaFormT(dodaLoc, 'list_filter_e')}</Select.Item>
</Select.Content>
</Select.Root>
<Button variant="outline" size="sm" class="h-9" onclick={clearFilters}>
<RotateCcw class="mr-2 h-4 w-4" />
Limpiar
{dodaFormT(dodaLoc, 'list_btn_clear')}
</Button>
</div>
</div>
@@ -391,8 +436,9 @@
<Card.Content class="min-h-0 p-0">
<div class="rounded-md border bg-background">
<DataTable
locale={dodaLoc}
data={allItems}
columns={createColumns()}
columns={tableColumns}
{loading}
{hasMore}
{loadMore}
@@ -405,9 +451,13 @@
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">
Mostrando {allItems.length} de {totalItems} registros
{dodaFormT(dodaLoc, 'list_showing', { a: allItems.length, b: totalItems })}
<span class="ml-2"></span>
<span class="ml-2">Filtros activos: {Object.values(filters).filter((value) => value !== '').length}</span>
<span class="ml-2"
>{dodaFormT(dodaLoc, 'list_active_filters', {
n: Object.values(filters).filter((value) => value !== '').length
})}</span
>
</div>
<div class="h-20"></div>
@@ -426,7 +476,7 @@
disabled={selectedIds.length !== 1}
>
<Pencil size={16} class="mr-2" />
Editar
{dodaFormT(dodaLoc, 'list_btn_edit')}
</Button>
<Button
variant="outline"
@@ -442,6 +492,21 @@
{/if}
{m['sidebar.doda_alta.action_delete']()}
</Button>
<Button
size="sm"
variant="secondary"
class="border border-primary/25 bg-primary/10 text-primary hover:bg-primary/15"
onclick={handleExportPedimentos}
disabled={selectedIds.length !== 1 || pedimentosExportLoading || !companyStore.activeCompany}
title={m['sidebar.doda_alta.action_export_pedimentos']()}
>
{#if pedimentosExportLoading}
<Loader2 size={16} class="mr-2 animate-spin" />
{:else}
<Table size={16} class="mr-2" />
{/if}
{m['sidebar.doda_alta.action_export_pedimentos']()}
</Button>
<Separator orientation="vertical" class="mx-1 h-8" />
<Button
variant="secondary"
@@ -454,7 +519,7 @@
{:else}
<Printer size={16} class="mr-2" />
{/if}
Imprimir
{dodaFormT(dodaLoc, 'list_btn_print')}
</Button>
<Separator orientation="vertical" class="mx-1 h-8" />
<Button