Merge pull request 'fix/idioma-bitacota' (#296) from fix/idioma-bitacota into development
Reviewed-on: ADUANASOFT/anexo76#296
This commit is contained in:
252
frontend/src/lib/utils/audit-log-i18n.ts
Normal file
252
frontend/src/lib/utils/audit-log-i18n.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { getLocale } from '$lib/paraglide/runtime';
|
||||
|
||||
type SupportedLocale = 'en' | 'es';
|
||||
type LocalizedText = Record<SupportedLocale, string>;
|
||||
|
||||
const PROCEDURE_TRANSLATIONS: Record<string, LocalizedText> = {
|
||||
'IMPORT INVOICE BROWSE': { en: 'Import invoice lookup', es: 'Consulta de factura de importación' },
|
||||
'IMPORT INVOICE UPDATE': { en: 'Import invoice update', es: 'Actualización de factura de importación' },
|
||||
'PEDIMENTO BROWSE': { en: 'Pedimento lookup', es: 'Consulta de pedimento' },
|
||||
'SYSTEM SCAF': { en: 'SCAF system', es: 'Sistema SCAF' },
|
||||
'SYSTEM AUTH': { en: 'System authentication', es: 'Autenticación del sistema' },
|
||||
CATALOGS: { en: 'Catalogs', es: 'Catálogos' },
|
||||
'ELECTRONIC NOTICES': { en: 'Electronic notices', es: 'Avisos electrónicos' },
|
||||
DODA: { en: 'DODA', es: 'DODA' }
|
||||
};
|
||||
|
||||
const MOVEMENT_TRANSLATIONS: Record<string, LocalizedText> = {
|
||||
'ADD IMPORT_INVOICE': { en: 'Add import invoice', es: 'Agregar factura de importación' },
|
||||
'EDIT IMPORT_INVOICE': { en: 'Edit import invoice', es: 'Editar factura de importación' },
|
||||
'DELETE IMPORT_INVOICE': { en: 'Delete import invoice', es: 'Eliminar factura de importación' },
|
||||
'ADD IMPORT_INVOICE_ITEM': { en: 'Add import invoice item', es: 'Agregar partida de factura de importación' },
|
||||
'EDIT IMPORT_INVOICE_ITEM': { en: 'Edit import invoice item', es: 'Editar partida de factura de importación' },
|
||||
'DELETE IMPORT_INVOICE_ITEM': { en: 'Delete import invoice item', es: 'Eliminar partida de factura de importación' },
|
||||
'ADD PEDIMENTO': { en: 'Add pedimento', es: 'Agregar pedimento' },
|
||||
'EDIT PEDIMENTO': { en: 'Edit pedimento', es: 'Editar pedimento' },
|
||||
'DELETE PEDIMENTO': { en: 'Delete pedimento', es: 'Eliminar pedimento' },
|
||||
'SYSTEM LOGIN': { en: 'System login', es: 'Inicio de sesión del sistema' },
|
||||
'SYSTEM LOGOUT': { en: 'System logout', es: 'Cierre de sesión del sistema' },
|
||||
'ADD CLASS': { en: 'Add class', es: 'Agregar clase' },
|
||||
'EDIT CLASS': { en: 'Edit class', es: 'Editar clase' },
|
||||
'DELETE CLASS': { en: 'Delete class', es: 'Eliminar clase' },
|
||||
'ADD DODA': { en: 'Add DODA', es: 'Agregar DODA' },
|
||||
'EDIT DODA': { en: 'Edit DODA', es: 'Editar DODA' },
|
||||
'DELETE DODA': { en: 'Delete DODA', es: 'Eliminar DODA' },
|
||||
'ADD COMPANY': { en: 'Add company', es: 'Agregar empresa' },
|
||||
'EDIT COMPANY': { en: 'Edit company', es: 'Editar empresa' },
|
||||
'DELETE COMPANY': { en: 'Delete company', es: 'Eliminar empresa' }
|
||||
};
|
||||
|
||||
const ENTITY_TRANSLATIONS: Record<string, LocalizedText> = {
|
||||
'IMPORT INVOICE ITEM': { en: 'import invoice item', es: 'partida de factura de importación' },
|
||||
'IMPORT INVOICE': { en: 'import invoice', es: 'factura de importación' },
|
||||
'UNIT CONVERSIONS': { en: 'unit conversions', es: 'conversiones de unidades' },
|
||||
UNITS: { en: 'units', es: 'unidades' },
|
||||
CONVERSIONS: { en: 'conversions', es: 'conversiones' },
|
||||
PEDIMENTO: { en: 'pedimento', es: 'pedimento' },
|
||||
CLASS: { en: 'class', es: 'clase' },
|
||||
COMPANY: { en: 'company', es: 'empresa' },
|
||||
USER: { en: 'user', es: 'usuario' },
|
||||
USERS: { en: 'users', es: 'usuarios' },
|
||||
SESSION: { en: 'session', es: 'sesión' },
|
||||
SESSIONS: { en: 'sessions', es: 'sesiones' },
|
||||
ITEM: { en: 'item', es: 'artículo' },
|
||||
ITEMS: { en: 'items', es: 'artículos' },
|
||||
CLIENT: { en: 'client', es: 'cliente' },
|
||||
CLIENTS: { en: 'clients', es: 'clientes' },
|
||||
PROVIDER: { en: 'provider', es: 'proveedor' },
|
||||
PROVIDERS: { en: 'providers', es: 'proveedores' },
|
||||
CATALOGS: { en: 'catalogs', es: 'catálogos' },
|
||||
'ELECTRONIC NOTICES': { en: 'electronic notices', es: 'avisos electrónicos' },
|
||||
DODA: { en: 'DODA', es: 'DODA' },
|
||||
'SYSTEM SCAF': { en: 'SCAF system', es: 'sistema SCAF' },
|
||||
'SYSTEM AUTH': { en: 'system authentication', es: 'autenticación del sistema' }
|
||||
};
|
||||
|
||||
const VERB_TRANSLATIONS: Record<string, LocalizedText> = {
|
||||
ADD: { en: 'Add', es: 'Agregar' },
|
||||
EDIT: { en: 'Edit', es: 'Editar' },
|
||||
DELETE: { en: 'Delete', es: 'Eliminar' },
|
||||
CREATE: { en: 'Create', es: 'Crear' },
|
||||
UPDATE: { en: 'Update', es: 'Actualizar' }
|
||||
};
|
||||
|
||||
const TOKEN_TRANSLATIONS: Record<string, LocalizedText> = {
|
||||
IMPORT: { en: 'import', es: 'importación' },
|
||||
INVOICE: { en: 'invoice', es: 'factura' },
|
||||
ITEM: { en: 'item', es: 'artículo' },
|
||||
ITEMS: { en: 'items', es: 'artículos' },
|
||||
PEDIMENTO: { en: 'pedimento', es: 'pedimento' },
|
||||
SYSTEM: { en: 'system', es: 'sistema' },
|
||||
AUTH: { en: 'authentication', es: 'autenticación' },
|
||||
CATALOGS: { en: 'catalogs', es: 'catálogos' },
|
||||
ELECTRONIC: { en: 'electronic', es: 'electrónicos' },
|
||||
NOTICES: { en: 'notices', es: 'avisos' },
|
||||
UNIT: { en: 'unit', es: 'unidad' },
|
||||
UNITS: { en: 'units', es: 'unidades' },
|
||||
CONVERSIONS: { en: 'conversions', es: 'conversiones' },
|
||||
COMPANY: { en: 'company', es: 'empresa' },
|
||||
CLASS: { en: 'class', es: 'clase' },
|
||||
USER: { en: 'user', es: 'usuario' },
|
||||
USERS: { en: 'users', es: 'usuarios' },
|
||||
SESSION: { en: 'session', es: 'sesión' },
|
||||
SESSIONS: { en: 'sessions', es: 'sesiones' },
|
||||
CLIENT: { en: 'client', es: 'cliente' },
|
||||
CLIENTS: { en: 'clients', es: 'clientes' },
|
||||
PROVIDER: { en: 'provider', es: 'proveedor' },
|
||||
PROVIDERS: { en: 'providers', es: 'proveedores' }
|
||||
};
|
||||
|
||||
function normalizeKey(value: string) {
|
||||
return value.trim().replace(/_/g, ' ').replace(/\s+/g, ' ').toUpperCase();
|
||||
}
|
||||
|
||||
function currentLocale(): SupportedLocale {
|
||||
return getLocale() === 'es' ? 'es' : 'en';
|
||||
}
|
||||
|
||||
function capitalize(value: string) {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
function translateEntity(value: string) {
|
||||
const locale = currentLocale();
|
||||
const normalized = normalizeKey(value);
|
||||
const exact = ENTITY_TRANSLATIONS[normalized];
|
||||
if (exact) return exact[locale];
|
||||
|
||||
return normalized
|
||||
.split(' ')
|
||||
.map((token) => TOKEN_TRANSLATIONS[token]?.[locale] ?? token.toLowerCase())
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
export function translateAuditProcedure(value: string) {
|
||||
if (!value) return value;
|
||||
|
||||
const locale = currentLocale();
|
||||
const normalized = normalizeKey(value);
|
||||
const exact = PROCEDURE_TRANSLATIONS[normalized];
|
||||
if (exact) return exact[locale];
|
||||
|
||||
if (normalized.endsWith(' BROWSE')) {
|
||||
const entity = translateEntity(normalized.slice(0, -' BROWSE'.length));
|
||||
return locale === 'es' ? `Consulta de ${entity}` : `${capitalize(entity)} lookup`;
|
||||
}
|
||||
|
||||
if (normalized.endsWith(' UPDATE')) {
|
||||
const entity = translateEntity(normalized.slice(0, -' UPDATE'.length));
|
||||
return locale === 'es' ? `Actualización de ${entity}` : `${capitalize(entity)} update`;
|
||||
}
|
||||
|
||||
const fallback = translateEntity(normalized);
|
||||
return capitalize(fallback);
|
||||
}
|
||||
|
||||
export function translateAuditMovement(value: string) {
|
||||
if (!value) return value;
|
||||
|
||||
const locale = currentLocale();
|
||||
const normalized = normalizeKey(value);
|
||||
const exact = MOVEMENT_TRANSLATIONS[normalized];
|
||||
if (exact) return exact[locale];
|
||||
|
||||
const [verb, ...rest] = normalized.split(' ');
|
||||
const translatedVerb = VERB_TRANSLATIONS[verb];
|
||||
if (!translatedVerb || rest.length === 0) {
|
||||
return capitalize(translateEntity(normalized));
|
||||
}
|
||||
|
||||
return `${translatedVerb[locale]} ${translateEntity(rest.join(' '))}`;
|
||||
}
|
||||
|
||||
// ── Tareas en segundo plano (Celery) ──────────────────────────────────────
|
||||
|
||||
const TASK_GROUP_TRANSLATIONS: Record<string, LocalizedText> = {
|
||||
factura_cove: { en: 'Invoice COVE', es: 'COVE de factura' },
|
||||
layouts_csv: { en: 'CSV import', es: 'Importación CSV' },
|
||||
reports: { en: 'Reports', es: 'Reportes' },
|
||||
invoices: { en: 'Invoices', es: 'Facturas' }
|
||||
};
|
||||
|
||||
const TASK_NAME_TRANSLATIONS: Record<string, LocalizedText> = {
|
||||
// invoices
|
||||
process_invoice_task: { en: 'Process import invoice', es: 'Procesar factura de importación' },
|
||||
revert_import_invoice_task: { en: 'Revert import invoice', es: 'Revertir factura de importación' },
|
||||
revert_invoice_task: { en: 'Revert import invoice', es: 'Revertir factura de importación' },
|
||||
process_export_invoice_task: { en: 'Process export invoice', es: 'Procesar factura de exportación' },
|
||||
revert_export_invoice_task: { en: 'Revert export invoice', es: 'Revertir factura de exportación' },
|
||||
// factura_cove
|
||||
factura_cove_generate: { en: 'Generate invoice COVE', es: 'Generar COVE de factura' },
|
||||
// reports
|
||||
generate_invoice_movements_async: { en: 'Generate invoice movements', es: 'Generar movimientos de facturas' },
|
||||
generate_saldos_temporales_async: { en: 'Generate temporary balances', es: 'Generar saldos temporales' },
|
||||
generate_vencimiento_csv_async: { en: 'Generate expiration CSV', es: 'Generar CSV de vencimiento' },
|
||||
generar_packing_list_async: { en: 'Generate packing list', es: 'Generar packing list' },
|
||||
generar_pdf_consolidado_async: { en: 'Generate consolidated PDF', es: 'Generar PDF consolidado' },
|
||||
generar_pdf_factura_async: { en: 'Generate invoice PDF', es: 'Generar PDF de factura' },
|
||||
generate_pedimentos_winsaai_task: { en: 'Generate WinSAAI pedimentos', es: 'Generar pedimentos WinSAAI' },
|
||||
generar_transmission_definitiva_async: { en: 'Generate definitive transmission', es: 'Generar transmisión definitiva' },
|
||||
generar_transmission_temporal_async: { en: 'Generate temporary transmission', es: 'Generar transmisión temporal' },
|
||||
generate_descarga_pdf_task: { en: 'Generate discharge PDF', es: 'Generar PDF de descarga' },
|
||||
generate_winsaai_task: { en: 'Generate WinSAAI file', es: 'Generar archivo WinSAAI' },
|
||||
generar_pdf_aviso_consolidado_exp_async: { en: 'Generate export consolidated notice PDF', es: 'Generar PDF aviso consolidado exportación' },
|
||||
generar_transmission_file_async: { en: 'Generate transmission file', es: 'Generar archivo de transmisión' },
|
||||
// layouts_csv
|
||||
vehicles_scan_file: { en: 'Scan vehicles CSV file', es: 'Escanear archivo de vehículos CSV' },
|
||||
classes_scan_file: { en: 'Scan classes CSV file', es: 'Escanear archivo de clases CSV' },
|
||||
classes_insert_valid_rows: { en: 'Import valid classes CSV rows', es: 'Importar clases válidas CSV' },
|
||||
customs_brokers_scan_file: { en: 'Scan customs brokers CSV file', es: 'Escanear archivo de agentes aduanales CSV' },
|
||||
customs_brokers_insert_valid_rows: { en: 'Import valid customs brokers CSV rows', es: 'Importar agentes aduanales válidos CSV' },
|
||||
trailers_scan_file: { en: 'Scan trailers CSV file', es: 'Escanear archivo de remolques CSV' },
|
||||
exchange_rate_scan_file: { en: 'Scan exchange rate CSV file', es: 'Escanear archivo de tipo de cambio CSV' },
|
||||
exchange_rate_insert_valid_rows: { en: 'Import valid exchange rate CSV rows', es: 'Importar tipo de cambio válido CSV' },
|
||||
exportacion_scan_file: { en: 'Scan export CSV file', es: 'Escanear archivo de exportación CSV' },
|
||||
exportacion_insert_valid_rows: { en: 'Import valid export CSV rows', es: 'Importar exportación válida CSV' },
|
||||
boms_scan_file: { en: 'Scan BOM CSV file', es: 'Escanear archivo de listas de materiales CSV' },
|
||||
boms_insert_valid_rows: { en: 'Import valid BOM CSV rows', es: 'Importar listas de materiales válidas CSV' },
|
||||
clients_and_providers_scan_file: { en: 'Scan clients and providers CSV file', es: 'Escanear archivo de clientes y proveedores CSV' },
|
||||
clients_and_providers_insert_valid_rows: { en: 'Import valid clients and providers CSV rows', es: 'Importar clientes y proveedores válidos CSV' },
|
||||
pedmientos_scan_file: { en: 'Scan pedimentos CSV file', es: 'Escanear archivo de pedimentos CSV' },
|
||||
pedmientos_insert_valid_rows: { en: 'Import valid pedimentos CSV rows', es: 'Importar pedimentos válidos CSV' },
|
||||
us_tariff_fractions_scan_file: { en: 'Scan US tariff fractions CSV file', es: 'Escanear archivo de fracciones arancelarias EUA CSV' },
|
||||
us_tariff_fractions_insert_valid_rows: { en: 'Import valid US tariff fractions CSV rows', es: 'Importar fracciones arancelarias EUA válidas CSV' },
|
||||
parts_scan_file: { en: 'Scan parts CSV file', es: 'Escanear archivo de partes CSV' },
|
||||
parts_insert_valid_rows: { en: 'Import valid parts CSV rows', es: 'Importar partes válidas CSV' },
|
||||
cambio_regimen_regularizacion_scan_file: { en: 'Scan regime change CSV file', es: 'Escanear archivo de cambio de régimen CSV' },
|
||||
cambio_regimen_regularizacion_insert_valid_rows: { en: 'Import valid regime change CSV rows', es: 'Importar cambio de régimen válido CSV' },
|
||||
facturas_scan_file: { en: 'Scan invoices CSV file', es: 'Escanear archivo de facturas CSV' },
|
||||
facturas_insert_valid_rows: { en: 'Import valid invoices CSV rows', es: 'Importar facturas válidas CSV' },
|
||||
transportistas_scan_file: { en: 'Scan carriers CSV file', es: 'Escanear archivo de transportistas CSV' },
|
||||
cleanup_orphan_layout_imports: { en: 'Clean orphan CSV imports', es: 'Limpiar importaciones CSV huérfanas' },
|
||||
// expediente archivos
|
||||
expediente_archivos_digitalizar: { en: 'Digitize expediente file', es: 'Digitalizar archivo de expediente' }
|
||||
};
|
||||
|
||||
const CELERY_STATE_TRANSLATIONS: Record<string, LocalizedText> = {
|
||||
SUCCESS: { en: 'Succeeded', es: 'Exitoso' },
|
||||
FAILURE: { en: 'Failed', es: 'Fallido' },
|
||||
PENDING: { en: 'Pending', es: 'Pendiente' },
|
||||
STARTED: { en: 'Started', es: 'Iniciado' },
|
||||
RETRY: { en: 'Retrying', es: 'Reintentando' },
|
||||
REVOKED: { en: 'Revoked', es: 'Cancelado' },
|
||||
PROGRESS: { en: 'In progress', es: 'En progreso' }
|
||||
};
|
||||
|
||||
export function translateTaskGroup(value: string): string {
|
||||
if (!value) return value;
|
||||
const locale = currentLocale();
|
||||
return TASK_GROUP_TRANSLATIONS[value]?.[locale] ?? value;
|
||||
}
|
||||
|
||||
export function translateTaskName(value: string): string {
|
||||
if (!value) return value;
|
||||
const locale = currentLocale();
|
||||
return TASK_NAME_TRANSLATIONS[value]?.[locale] ?? value.replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
export function translateCeleryState(value: string): string {
|
||||
if (!value) return value;
|
||||
const locale = currentLocale();
|
||||
return CELERY_STATE_TRANSLATIONS[value.toUpperCase()]?.[locale] ?? value;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { getLocale } from '$lib/paraglide/runtime';
|
||||
import {
|
||||
AuditLogAPI,
|
||||
type AuditLog,
|
||||
@@ -9,6 +10,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { translateAuditMovement, translateAuditProcedure } from '$lib/utils/audit-log-i18n';
|
||||
import { RefreshCw, Search } from 'lucide-svelte';
|
||||
|
||||
let logs: AuditLog[] = [];
|
||||
@@ -29,6 +31,14 @@
|
||||
let hasMore = true;
|
||||
let sentinel: HTMLElement;
|
||||
|
||||
function isSpanish() {
|
||||
return getLocale() === 'es';
|
||||
}
|
||||
|
||||
function text(es: string, en: string) {
|
||||
return isSpanish() ? es : en;
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
@@ -98,25 +108,31 @@
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string, timestamp?: string): string {
|
||||
const locale = isSpanish() ? 'es-MX' : 'en-US';
|
||||
if (timestamp) {
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
return new Date(timestamp).toLocaleDateString(locale);
|
||||
}
|
||||
if (!dateStr) return '';
|
||||
const [y, m, d] = dateStr.split('-');
|
||||
return `${d}/${m}/${y}`;
|
||||
return isSpanish() ? `${d}/${m}/${y}` : `${m}/${d}/${y}`;
|
||||
}
|
||||
|
||||
function formatTime(timeStr: string, timestamp?: string): string {
|
||||
const locale = isSpanish() ? 'es-MX' : 'en-US';
|
||||
if (timestamp) {
|
||||
return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
return new Date(timestamp).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
if (!timeStr) return '';
|
||||
try {
|
||||
const [h, m] = timeStr.split(':');
|
||||
let hour = parseInt(h, 10);
|
||||
const ampm = hour >= 12 ? 'PM' : 'AM';
|
||||
hour = hour % 12;
|
||||
hour = hour ? hour : 12;
|
||||
const rawHour = parseInt(h, 10);
|
||||
if (isSpanish()) {
|
||||
const date = new Date();
|
||||
date.setHours(rawHour, parseInt(m, 10), 0, 0);
|
||||
return date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
const hour = rawHour % 12 || 12;
|
||||
const ampm = rawHour >= 12 ? 'PM' : 'AM';
|
||||
return `${hour.toString().padStart(2, '0')}:${m} ${ampm}`;
|
||||
} catch {
|
||||
return timeStr;
|
||||
@@ -156,15 +172,15 @@
|
||||
<Card.Header class="flex-none py-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Bitácora de Auditoría</Card.Title>
|
||||
<Card.Description class="text-xs">Mostrando {logs.length} de {total} registros</Card.Description>
|
||||
<Card.Title>{text('Bitácora de Auditoría', 'Audit Log')}</Card.Title>
|
||||
<Card.Description class="text-xs">{text(`Mostrando ${logs.length} de ${total} registros`, `Showing ${logs.length} of ${total} records`)}</Card.Description>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="relative">
|
||||
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search"
|
||||
placeholder="Ref, Mov, Usuario..."
|
||||
placeholder={text('Ref, Mov, Usuario...', 'Ref, Move, User...')}
|
||||
class="h-9 w-48 pl-8"
|
||||
bind:value={search}
|
||||
oninput={handleSearchInput}
|
||||
@@ -172,7 +188,7 @@
|
||||
</div>
|
||||
<Input
|
||||
id="username"
|
||||
placeholder="Usuario"
|
||||
placeholder={text('Usuario', 'User')}
|
||||
class="h-9 w-32"
|
||||
bind:value={usernameFilter}
|
||||
oninput={handleSearchInput}
|
||||
@@ -182,11 +198,11 @@
|
||||
bind:value={procedureFilter}
|
||||
onchange={handleFilterChange}
|
||||
class="flex h-9 w-[180px] rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
title="Procedimiento"
|
||||
title={text('Procedimiento', 'Procedure')}
|
||||
>
|
||||
<option value="">Procedimiento: Todos</option>
|
||||
<option value="">{text('Procedimiento: Todos', 'Procedure: All')}</option>
|
||||
{#each procedures as proc}
|
||||
<option value={proc}>{proc}</option>
|
||||
<option value={proc}>{translateAuditProcedure(proc)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Input
|
||||
@@ -195,7 +211,7 @@
|
||||
class="h-9 w-36"
|
||||
bind:value={dateFrom}
|
||||
onchange={handleFilterChange}
|
||||
title="Desde"
|
||||
title={text('Desde', 'From')}
|
||||
/>
|
||||
<span class="text-sm text-muted-foreground">-</span>
|
||||
<Input
|
||||
@@ -204,10 +220,10 @@
|
||||
class="h-9 w-36"
|
||||
bind:value={dateTo}
|
||||
onchange={handleFilterChange}
|
||||
title="Hasta"
|
||||
title={text('Hasta', 'To')}
|
||||
/>
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={clearFilters}>
|
||||
Limpiar
|
||||
{text('Limpiar', 'Clear')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -222,7 +238,7 @@
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
{text('Actualizar', 'Refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,7 +246,7 @@
|
||||
<Card.Content class="flex min-h-0 flex-1 flex-col p-0 px-6 pb-6">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-red-50 p-4 text-center text-red-500 dark:bg-red-950/30 dark:text-red-400">
|
||||
Error al cargar datos: {error}
|
||||
{text('Error al cargar datos', 'Error loading data')}: {error}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="relative min-h-0 flex-1 overflow-y-auto rounded-md border bg-card shadow-inner">
|
||||
@@ -238,25 +254,25 @@
|
||||
<Table.Header class="sticky top-0 z-10 bg-background/95 shadow-sm backdrop-blur-sm">
|
||||
<Table.Row>
|
||||
<Table.Head class="w-[80px]">ID</Table.Head>
|
||||
<Table.Head class="w-[180px]">Referencia</Table.Head>
|
||||
<Table.Head>Procedimiento</Table.Head>
|
||||
<Table.Head>Movimiento</Table.Head>
|
||||
<Table.Head class="w-[150px]">Usuario</Table.Head>
|
||||
<Table.Head class="w-[120px]">Fecha</Table.Head>
|
||||
<Table.Head class="w-[120px]">Hora</Table.Head>
|
||||
<Table.Head class="w-[180px]">{text('Referencia', 'Reference')}</Table.Head>
|
||||
<Table.Head>{text('Procedimiento', 'Procedure')}</Table.Head>
|
||||
<Table.Head>{text('Movimiento', 'Movement')}</Table.Head>
|
||||
<Table.Head class="w-[150px]">{text('Usuario', 'User')}</Table.Head>
|
||||
<Table.Head class="w-[120px]">{text('Fecha', 'Date')}</Table.Head>
|
||||
<Table.Head class="w-[120px]">{text('Hora', 'Time')}</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if loading && logs.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={7} class="h-24 text-center text-muted-foreground italic">
|
||||
Cargando...
|
||||
{text('Cargando...', 'Loading...')}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else if logs.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={7} class="h-24 text-center text-muted-foreground">
|
||||
No se encontraron registros
|
||||
{text('No se encontraron registros', 'No records found')}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
@@ -266,8 +282,8 @@
|
||||
<Table.Cell class="text-sm font-bold text-blue-600 dark:text-blue-400">
|
||||
{log.reference}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-sm">{log.procedure}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{log.movement}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{translateAuditProcedure(log.procedure)}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{translateAuditMovement(log.movement)}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{log.username}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{formatDate(log.date, log.timestamp)}</Table.Cell>
|
||||
<Table.Cell class="text-sm">{formatTime(log.time, log.timestamp)}</Table.Cell>
|
||||
@@ -281,7 +297,7 @@
|
||||
{#if loading && logs.length > 0}
|
||||
<div class="flex items-center gap-2">
|
||||
<RefreshCw class="h-4 w-4 animate-spin text-primary" />
|
||||
<span class="text-sm text-muted-foreground">Cargando más registros...</span>
|
||||
<span class="text-sm text-muted-foreground">{text('Cargando más registros...', 'Loading more records...')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getLocale } from '$lib/paraglide/runtime';
|
||||
import {
|
||||
coreTasksApi,
|
||||
type UnifiedTask,
|
||||
@@ -6,6 +7,7 @@
|
||||
type UnifiedTaskStatus
|
||||
} from '$lib/api/dashboard/a76/tasks';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { translateCeleryState, translateTaskGroup, translateTaskName } from '$lib/utils/audit-log-i18n';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -26,6 +28,14 @@
|
||||
let detailError = $state<string | null>(null);
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function isSpanish() {
|
||||
return getLocale() === 'es';
|
||||
}
|
||||
|
||||
function text(es: string, en: string) {
|
||||
return isSpanish() ? es : en;
|
||||
}
|
||||
|
||||
function hasActive(items: UnifiedTask[]) {
|
||||
return items.some((x) => x.status === 'pending' || x.status === 'active');
|
||||
}
|
||||
@@ -56,7 +66,7 @@
|
||||
});
|
||||
|
||||
if (response.error || !response.data) {
|
||||
error = response.error || 'Error cargando tareas';
|
||||
error = response.error || text('Error cargando tareas', 'Error loading tasks');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
@@ -81,13 +91,13 @@
|
||||
}
|
||||
|
||||
function statusLabel(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'En cola',
|
||||
active: 'En progreso',
|
||||
completed: 'Completada',
|
||||
failed: 'Fallida'
|
||||
const map: Record<string, { en: string; es: string }> = {
|
||||
pending: { en: 'Queued', es: 'En cola' },
|
||||
active: { en: 'In progress', es: 'En progreso' },
|
||||
completed: { en: 'Completed', es: 'Completada' },
|
||||
failed: { en: 'Failed', es: 'Fallida' }
|
||||
};
|
||||
return map[status] ?? status;
|
||||
return map[status]?.[isSpanish() ? 'es' : 'en'] ?? status;
|
||||
}
|
||||
|
||||
function statusClass(status: string) {
|
||||
@@ -123,7 +133,7 @@
|
||||
if (!detailOpen) return;
|
||||
detailLoading = false;
|
||||
if (res.error || !res.data) {
|
||||
detailError = res.error || 'No se pudo sincronizar el detalle';
|
||||
detailError = res.error || text('No se pudo sincronizar el detalle', 'Could not sync task details');
|
||||
return;
|
||||
}
|
||||
selectedDetail = res.data;
|
||||
@@ -151,25 +161,25 @@
|
||||
<Card.Header class="flex-none py-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Tareas del Sistema</Card.Title>
|
||||
<Card.Description class="text-xs">Total: {total} tareas encontradas</Card.Description>
|
||||
<Card.Title>{text('Tareas del Sistema', 'System Tasks')}</Card.Title>
|
||||
<Card.Description class="text-xs">{text(`Total: ${total} tareas encontradas`, `Total: ${total} tasks found`)}</Card.Description>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar por task_id / nombre / error" bind:value={search} class="h-9 w-64" />
|
||||
<Input placeholder={text('Buscar por task_id / nombre / error', 'Search by task_id / name / error')} bind:value={search} class="h-9 w-64" />
|
||||
<select
|
||||
bind:value={statusFilter}
|
||||
class="flex h-9 w-[160px] rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
title="Estado"
|
||||
title={text('Estado', 'Status')}
|
||||
>
|
||||
<option value="all">Estado: Todos</option>
|
||||
<option value="pending">En cola</option>
|
||||
<option value="active">En progreso</option>
|
||||
<option value="completed">Completadas</option>
|
||||
<option value="failed">Fallidas</option>
|
||||
<option value="all">{text('Estado: Todos', 'Status: All')}</option>
|
||||
<option value="pending">{statusLabel('pending')}</option>
|
||||
<option value="active">{statusLabel('active')}</option>
|
||||
<option value="completed">{statusLabel('completed')}</option>
|
||||
<option value="failed">{statusLabel('failed')}</option>
|
||||
</select>
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={applyFilters}>Aplicar Filtros</Button>
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={applyFilters}>{text('Aplicar Filtros', 'Apply Filters')}</Button>
|
||||
<Button size="sm" class="h-9" onclick={() => void loadTasks(true)} disabled={loading}>
|
||||
{loading ? 'Cargando...' : 'Refrescar'}
|
||||
{loading ? text('Cargando...', 'Loading...') : text('Refrescar', 'Refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,22 +195,22 @@
|
||||
<table class="w-full text-sm">
|
||||
<thead class="sticky top-0 z-10 bg-background/95 shadow-sm backdrop-blur-sm">
|
||||
<tr>
|
||||
<th class="p-2 text-left">Task ID</th>
|
||||
<th class="p-2 text-left">Tipo</th>
|
||||
<th class="p-2 text-left">Estado</th>
|
||||
<th class="p-2 text-left">Progreso</th>
|
||||
<th class="p-2 text-left">Reintentos</th>
|
||||
<th class="p-2 text-left">Actualizado</th>
|
||||
<th class="p-2 text-left">{text('ID de tarea', 'Task ID')}</th>
|
||||
<th class="p-2 text-left">{text('Tipo', 'Type')}</th>
|
||||
<th class="p-2 text-left">{text('Estado', 'Status')}</th>
|
||||
<th class="p-2 text-left">{text('Progreso', 'Progress')}</th>
|
||||
<th class="p-2 text-left">{text('Reintentos', 'Retries')}</th>
|
||||
<th class="p-2 text-left">{text('Actualizado', 'Updated')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if tasks.length === 0 && !loading}
|
||||
<tr>
|
||||
<td class="p-4 text-center text-muted-foreground" colspan="6">Sin tareas registradas</td>
|
||||
<td class="p-4 text-center text-muted-foreground" colspan="6">{text('Sin tareas registradas', 'No tasks found')}</td>
|
||||
</tr>
|
||||
{:else if tasks.length === 0 && loading}
|
||||
<tr>
|
||||
<td class="p-4 text-center text-muted-foreground italic" colspan="6">Cargando...</td>
|
||||
<td class="p-4 text-center text-muted-foreground italic" colspan="6">{text('Cargando...', 'Loading...')}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each tasks as task}
|
||||
@@ -211,11 +221,11 @@
|
||||
onclick={() => void openDetail(task)}
|
||||
>
|
||||
<td class="p-2 font-mono text-xs">{task.task_id}</td>
|
||||
<td class="p-2">{task.task_group} / {task.task_name}</td>
|
||||
<td class="p-2">{translateTaskGroup(task.task_group)} / {translateTaskName(task.task_name)}</td>
|
||||
<td class={`p-2 font-medium ${statusClass(task.status)}`}>
|
||||
{statusLabel(task.status)}
|
||||
<span class="ml-1 text-xs font-normal text-muted-foreground"
|
||||
>({task.celery_state_raw})</span
|
||||
>({translateCeleryState(task.celery_state_raw)})</span
|
||||
>
|
||||
</td>
|
||||
<td class="p-2">
|
||||
@@ -241,9 +251,9 @@
|
||||
onclick={() => {
|
||||
page -= 1;
|
||||
void loadTasks(false);
|
||||
}}>Anterior</Button
|
||||
}}>{text('Anterior', 'Previous')}</Button
|
||||
>
|
||||
<span>Página {page}</span>
|
||||
<span>{text('Página', 'Page')} {page}</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -251,7 +261,7 @@
|
||||
onclick={() => {
|
||||
page += 1;
|
||||
void loadTasks(false);
|
||||
}}>Siguiente</Button
|
||||
}}>{text('Siguiente', 'Next')}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,21 +276,21 @@
|
||||
>
|
||||
<Dialog.Content class="max-h-[70vh] min-h-[320px] w-full max-w-xl overflow-y-auto sm:max-w-xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Detalle de tarea</Dialog.Title>
|
||||
<Dialog.Title>{text('Detalle de tarea', 'Task Details')}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{#if detailLoading}
|
||||
Sincronizando estado con Celery…
|
||||
{text('Sincronizando estado con Celery…', 'Syncing state with Celery...')}
|
||||
{:else if selectedDetail}
|
||||
<span class="font-mono text-xs">{selectedDetail.task_id}</span>
|
||||
{:else}
|
||||
Cargando…
|
||||
{text('Cargando…', 'Loading...')}
|
||||
{/if}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
{#if detailLoading}
|
||||
<div class="flex items-center gap-2 py-4">
|
||||
<Badge variant="secondary">Sincronizando…</Badge>
|
||||
<Badge variant="secondary">{text('Sincronizando…', 'Syncing...')}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -291,43 +301,43 @@
|
||||
{#if selectedDetail}
|
||||
<div class="space-y-2 text-sm">
|
||||
<div>
|
||||
<strong>Task:</strong>
|
||||
<strong>{text('Tarea', 'Task')}:</strong>
|
||||
<span class="ml-1 font-mono text-xs">{selectedDetail.task_id}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Estado:</strong>
|
||||
<strong>{text('Estado', 'Status')}:</strong>
|
||||
<span class={statusClass(selectedDetail.status)}>{statusLabel(selectedDetail.status)}</span>
|
||||
<span class="text-muted-foreground">({selectedDetail.celery_state_raw})</span>
|
||||
<span class="text-muted-foreground">({translateCeleryState(selectedDetail.celery_state_raw)})</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Progreso:</strong>
|
||||
<strong>{text('Progreso', 'Progress')}:</strong>
|
||||
{formatPercent(selectedDetail)}
|
||||
{#if selectedDetail.progress?.message}
|
||||
— {selectedDetail.progress.message}
|
||||
{/if}
|
||||
</div>
|
||||
<div><strong>Origen:</strong> {selectedDetail.task_origin || '—'}</div>
|
||||
<div><strong>Solicitante:</strong> {selectedDetail.requested_by_user || '—'}</div>
|
||||
<div><strong>{text('Origen', 'Origin')}:</strong> {selectedDetail.task_origin || '—'}</div>
|
||||
<div><strong>{text('Solicitante', 'Requested by')}:</strong> {selectedDetail.requested_by_user || '—'}</div>
|
||||
<div>
|
||||
<strong>Error:</strong>
|
||||
<strong>{text('Error', 'Error')}:</strong>
|
||||
{selectedDetail.error?.type || '—'} — {selectedDetail.error?.message || '—'}
|
||||
</div>
|
||||
{#if selectedDetail.started_at}
|
||||
<div><strong>Inicio:</strong> {new Date(selectedDetail.started_at).toLocaleString()}</div>
|
||||
<div><strong>{text('Inicio', 'Started')}:</strong> {new Date(selectedDetail.started_at).toLocaleString(isSpanish() ? 'es-MX' : 'en-US')}</div>
|
||||
{/if}
|
||||
{#if selectedDetail.finished_at}
|
||||
<div><strong>Fin:</strong> {new Date(selectedDetail.finished_at).toLocaleString()}</div>
|
||||
<div><strong>{text('Fin', 'Finished')}:</strong> {new Date(selectedDetail.finished_at).toLocaleString(isSpanish() ? 'es-MX' : 'en-US')}</div>
|
||||
{/if}
|
||||
{#if selectedDetail.traceback_excerpt}
|
||||
<div>
|
||||
<strong class="block">Traceback (extracto)</strong>
|
||||
<strong class="block">{text('Traceback (extracto)', 'Traceback (excerpt)')}</strong>
|
||||
<pre
|
||||
class="mt-1 max-h-48 overflow-auto rounded border bg-muted/50 p-2 text-xs whitespace-pre-wrap">{selectedDetail.traceback_excerpt}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedDetail.result_summary && Object.keys(selectedDetail.result_summary).length > 0}
|
||||
<div>
|
||||
<strong class="block">Resultado (resumen)</strong>
|
||||
<strong class="block">{text('Resultado (resumen)', 'Result (summary)')}</strong>
|
||||
<pre
|
||||
class="mt-1 max-h-40 overflow-auto whitespace-pre-wrap break-words rounded border bg-muted/50 p-2 text-xs">{JSON.stringify(
|
||||
selectedDetail.result_summary,
|
||||
@@ -340,7 +350,7 @@
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer class="gap-2 sm:gap-0">
|
||||
<Button variant="outline" onclick={closeDetail}>Cerrar</Button>
|
||||
<Button variant="outline" onclick={closeDetail}>{text('Cerrar', 'Close')}</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
Reference in New Issue
Block a user