feat: traduje el dashboard al ingles y ademas el modal del tipo de cambio
This commit is contained in:
@@ -1,6 +1,106 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from en!",
|
||||
"dashboard": {
|
||||
"greeting_morning": "Good morning",
|
||||
"greeting_afternoon": "Good afternoon",
|
||||
"greeting_evening": "Good evening",
|
||||
"team_suffix": ", team.",
|
||||
"operational_summary": "Operational Summary — Annexes 22/24/30",
|
||||
"management_system": "Foreign Trade Management System",
|
||||
"loading": "Loading...",
|
||||
"update": "Update",
|
||||
"operations_distribution": "Operations Distribution",
|
||||
"top_clients": "Top 5 Clients",
|
||||
"top_providers": "Top 5 Providers",
|
||||
"by_invoice_number": "By number of invoices",
|
||||
"quick_access": "Quick Access",
|
||||
"most_used_modules": "Most Used Modules",
|
||||
"invoices": "Invoices",
|
||||
"manage_invoices": "Manage invoices",
|
||||
"clients_and_providers": "Clients and Providers",
|
||||
"manage_contacts": "Manage contacts",
|
||||
"goods": "Goods",
|
||||
"product_catalog": "Product catalog",
|
||||
"reference_data": "Reference Data",
|
||||
"sat_catalogs": "SAT Catalogs",
|
||||
"total_documents": "Total Documents",
|
||||
"total_contacts": "Total Contacts",
|
||||
"active_items_stat": "Active Items",
|
||||
"no_active_company": "No active company selected",
|
||||
"load_error": "Error loading dashboard",
|
||||
"pedimentos": "Pedimentos",
|
||||
"clients": "Clients",
|
||||
"providers": "Providers",
|
||||
"pending_approvals": "Pending",
|
||||
"operations_trend": "Operations Trend",
|
||||
"monthly_evolution": "Monthly evolution of operations",
|
||||
"recent_activity": "Recent Activity",
|
||||
"latest_operations": "Latest operations registered in the system",
|
||||
"no_recent_activity": "No recent activity",
|
||||
"records_suffix": "records",
|
||||
"just_now": "Just now",
|
||||
"ago_suffix": "Ago",
|
||||
"min_short": "min",
|
||||
"h_short": "h",
|
||||
"d_short": "d",
|
||||
"no_data_available": "No data available",
|
||||
"data_will_appear_here": "Data will appear here once registered",
|
||||
"operations_in": "operations in",
|
||||
"more_than_one_month_needed": "The graph will appear with more than one month of data",
|
||||
"average_per_month": "Average / month",
|
||||
"maximum": "Maximum",
|
||||
"total": "Total",
|
||||
"operations": "operations",
|
||||
"previous_month": "prev. month",
|
||||
"operations_breakdown": "Operations Breakdown",
|
||||
"ops_short": "ops",
|
||||
"operations_will_appear_here": "Operations will appear here once registered",
|
||||
"no_data_available_short": "No data available"
|
||||
},
|
||||
"exchange_rate": {
|
||||
"new_title": "New Exchange Rate",
|
||||
"edit_title": "Edit Exchange Rate",
|
||||
"required_title": "Exchange Rate Required",
|
||||
"required_description": "To proceed with saving, it is necessary to register the official exchange rate for this date.",
|
||||
"applicable_date": "Applicable Date",
|
||||
"exchange_rate_label": "Exchange Rate (MXN/USD)",
|
||||
"required_badge": "Required",
|
||||
"consulting": "Consulting...",
|
||||
"consult_dof": "Consult DOF",
|
||||
"example_suffix": "Eg.",
|
||||
"cancel": "Cancel",
|
||||
"ok": "Ok",
|
||||
"confirm_title": "Are you sure?",
|
||||
"confirm_description_create": "The exchange rate with value {value} will be created for the day {date}.",
|
||||
"confirm_description_update": "The exchange rate with value {value} will be updated for the day {date}.",
|
||||
"confirm_action": "Confirm",
|
||||
"toast_dof_success": "Exchange rate obtained from DOF: {value}",
|
||||
"toast_dof_error": "Could not obtain data from DOF",
|
||||
"toast_dof_service_error": "Error consulting DOF service",
|
||||
"error_no_company": "No company selected",
|
||||
"error_date_required": "Date is required",
|
||||
"error_value_required": "Exchange rate is required",
|
||||
"error_value_positive": "Exchange rate must be a value greater than 0",
|
||||
"toast_create_success": "Exchange rate created successfully",
|
||||
"toast_update_success": "Exchange rate updated successfully"
|
||||
},
|
||||
"multi_currency": {
|
||||
"new_title": "New Multi-Currency Exchange Rate",
|
||||
"edit_title": "Edit Multi-Currency Exchange Rate",
|
||||
"error_currency_required": "Currency code is required",
|
||||
"error_date_required": "Publication date is required",
|
||||
"currency_label": "Currency",
|
||||
"currency_help": "Currency code (FK).",
|
||||
"country_label": "Country",
|
||||
"country_help": "Country M3 key (FK).",
|
||||
"date_label": "Date",
|
||||
"date_help": "Saved as integer (YYYYMMDD).",
|
||||
"factor_label": "Factor",
|
||||
"saving": "Saving...",
|
||||
"update": "Update",
|
||||
"create": "Create"
|
||||
},
|
||||
"sidebar": {
|
||||
"dashboard": "Dashboard",
|
||||
"reference_data": {
|
||||
|
||||
@@ -1,6 +1,106 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from es!",
|
||||
"dashboard": {
|
||||
"greeting_morning": "Buenos días",
|
||||
"greeting_afternoon": "Buenas tardes",
|
||||
"greeting_evening": "Buenas noches",
|
||||
"team_suffix": ", equipo.",
|
||||
"operational_summary": "Resumen operativo — Anexos 22/24/30",
|
||||
"management_system": "Sistema de gestión de comercio exterior",
|
||||
"loading": "Cargando...",
|
||||
"update": "Actualizar",
|
||||
"operations_distribution": "Distribución de Operaciones",
|
||||
"top_clients": "Top 5 Clientes",
|
||||
"top_providers": "Top 5 Proveedores",
|
||||
"by_invoice_number": "Por número de facturas",
|
||||
"quick_access": "Accesos Rápidos",
|
||||
"most_used_modules": "Módulos más utilizados",
|
||||
"invoices": "Facturas",
|
||||
"manage_invoices": "Gestionar facturas",
|
||||
"clients_and_providers": "Clientes y Proveedores",
|
||||
"manage_contacts": "Administrar contactos",
|
||||
"goods": "Mercancías",
|
||||
"product_catalog": "Catálogo de productos",
|
||||
"reference_data": "Datos de Referencia",
|
||||
"sat_catalogs": "Catálogos del SAT",
|
||||
"total_documents": "Total Documentos",
|
||||
"total_contacts": "Total Contactos",
|
||||
"active_items_stat": "Items Activos",
|
||||
"no_active_company": "No hay compañía activa seleccionada",
|
||||
"load_error": "Error al cargar el dashboard",
|
||||
"pedimentos": "Pedimentos",
|
||||
"clients": "Clientes",
|
||||
"providers": "Proveedores",
|
||||
"pending_approvals": "Pendientes",
|
||||
"operations_trend": "Tendencia de Operaciones",
|
||||
"monthly_evolution": "Evolución mensual de operaciones",
|
||||
"recent_activity": "Actividad Reciente",
|
||||
"latest_operations": "Últimas operaciones registradas en el sistema",
|
||||
"no_recent_activity": "No hay actividad reciente",
|
||||
"records_suffix": "registros",
|
||||
"just_now": "Ahora mismo",
|
||||
"ago_suffix": "Hace",
|
||||
"min_short": "min",
|
||||
"h_short": "h",
|
||||
"d_short": "d",
|
||||
"no_data_available": "Sin datos disponibles",
|
||||
"data_will_appear_here": "Los datos aparecerán aquí una vez registrados",
|
||||
"operations_in": "operaciones en",
|
||||
"more_than_one_month_needed": "La gráfica aparecerá con más de un mes de datos",
|
||||
"average_per_month": "Promedio / mes",
|
||||
"maximum": "Máximo",
|
||||
"total": "Total",
|
||||
"operations": "operaciones",
|
||||
"previous_month": "mes ant.",
|
||||
"operations_breakdown": "Desglose por tipo de operación",
|
||||
"ops_short": "ops",
|
||||
"operations_will_appear_here": "Las operaciones aparecerán aquí una vez registradas",
|
||||
"no_data_available_short": "No hay datos disponibles"
|
||||
},
|
||||
"exchange_rate": {
|
||||
"new_title": "Nuevo Tipo de Cambio",
|
||||
"edit_title": "Editar Tipo de Cambio",
|
||||
"required_title": "Tipo de Cambio Requerido",
|
||||
"required_description": "Para continuar con el guardado, es necesario registrar el tipo de cambio oficial para esta fecha.",
|
||||
"applicable_date": "Fecha Aplicable",
|
||||
"exchange_rate_label": "Tipo de Cambio (MXN/USD)",
|
||||
"required_badge": "Requerida",
|
||||
"consulting": "Consultando...",
|
||||
"consult_dof": "Consultar DOF",
|
||||
"example_suffix": "Ej.",
|
||||
"cancel": "Cancelar",
|
||||
"ok": "Ok",
|
||||
"confirm_title": "¿Estás seguro?",
|
||||
"confirm_description_create": "Se creará el tipo de cambio con valor {value} para el día {date}.",
|
||||
"confirm_description_update": "Se actualizará el tipo de cambio con valor {value} para el día {date}.",
|
||||
"confirm_action": "Confirmar",
|
||||
"toast_dof_success": "Tipo de cambio obtenido del DOF: {value}",
|
||||
"toast_dof_error": "No se pudo obtener el dato del DOF",
|
||||
"toast_dof_service_error": "Error al consultar el servicio del DOF",
|
||||
"error_no_company": "No hay una compañía seleccionada",
|
||||
"error_date_required": "La fecha es requerida",
|
||||
"error_value_required": "El tipo de cambio es requerido",
|
||||
"error_value_positive": "El tipo de cambio debe ser un valor mayor a 0",
|
||||
"toast_create_success": "Tipo de cambio creado correctamente",
|
||||
"toast_update_success": "Tipo de cambio actualizado correctamente"
|
||||
},
|
||||
"multi_currency": {
|
||||
"new_title": "Nuevo Tipo de Cambio Múltiple",
|
||||
"edit_title": "Editar Tipo de Cambio Múltiple",
|
||||
"error_currency_required": "El código de moneda es requerido",
|
||||
"error_date_required": "La fecha de publicación es requerida",
|
||||
"currency_label": "Moneda",
|
||||
"currency_help": "Código de moneda (FK).",
|
||||
"country_label": "País",
|
||||
"country_help": "Clave M3 del país (FK).",
|
||||
"date_label": "Fecha",
|
||||
"date_help": "Se guarda como entero (YYYYMMDD).",
|
||||
"factor_label": "Factor",
|
||||
"saving": "Guardando...",
|
||||
"update": "Actualizar",
|
||||
"create": "Crear"
|
||||
},
|
||||
"sidebar": {
|
||||
"dashboard": "Dashboard",
|
||||
"reference_data": {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
AlertCircle,
|
||||
TruckIcon
|
||||
} from 'lucide-svelte';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
import { getLocale } from '$lib/paraglide/runtime';
|
||||
|
||||
interface Props {
|
||||
activities: ActivityItem[];
|
||||
@@ -48,7 +50,7 @@
|
||||
|
||||
// Si la fecha es futura (diff negativo) o muy reciente, mostrar 'Ahora mismo'
|
||||
if (diffMs < 0) {
|
||||
return 'Ahora mismo';
|
||||
return m.dashboard_just_now();
|
||||
}
|
||||
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
@@ -56,15 +58,16 @@
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) {
|
||||
return 'Ahora mismo';
|
||||
return m.dashboard_just_now();
|
||||
} else if (diffMins < 60) {
|
||||
return `Hace ${diffMins} min`;
|
||||
return `${m.dashboard_ago_suffix()} ${diffMins} ${m.dashboard_min_short()}`;
|
||||
} else if (diffHours < 24) {
|
||||
return `Hace ${diffHours}h`;
|
||||
return `${m.dashboard_ago_suffix()} ${diffHours}${m.dashboard_h_short()}`;
|
||||
} else if (diffDays < 7) {
|
||||
return `Hace ${diffDays}d`;
|
||||
return `${m.dashboard_ago_suffix()} ${diffDays}${m.dashboard_d_short()}`;
|
||||
} else {
|
||||
return date.toLocaleDateString('es-MX', { month: 'short', day: 'numeric' });
|
||||
const dateLocale = getLocale() === 'es' ? 'es-MX' : 'en-US';
|
||||
return date.toLocaleDateString(dateLocale, { month: 'short', day: 'numeric' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -75,12 +78,12 @@
|
||||
<div>
|
||||
<Card.Title class="flex items-center gap-2">
|
||||
<Clock class="h-4 w-4 text-primary" />
|
||||
Actividad Reciente
|
||||
{m.dashboard_recent_activity()}
|
||||
</Card.Title>
|
||||
<Card.Description class="mt-0.5">Últimas operaciones registradas en el sistema</Card.Description>
|
||||
<Card.Description class="mt-0.5">{m.dashboard_latest_operations()}</Card.Description>
|
||||
</div>
|
||||
{#if activities.length > 0}
|
||||
<span class="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-full">{activities.length} registros</span>
|
||||
<span class="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-full">{activities.length} {m.dashboard_records_suffix()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Header>
|
||||
@@ -88,7 +91,7 @@
|
||||
{#if activities.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-10 text-muted-foreground gap-3">
|
||||
<Clock class="h-8 w-8 opacity-30" />
|
||||
<p class="text-sm">No hay actividad reciente</p>
|
||||
<p class="text-sm">{m.dashboard_no_recent_activity()}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="relative">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import type { ChartDataPoint } from '$lib/api/dashboard/types';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -41,7 +42,7 @@
|
||||
<Card.Content>
|
||||
{#if data.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-8 gap-2 text-muted-foreground">
|
||||
<p class="text-sm">No hay datos disponibles</p>
|
||||
<p class="text-sm">{m.dashboard_no_data_available_short()}</p>
|
||||
</div>
|
||||
{:else if type === 'bar'}
|
||||
<div class="space-y-3">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { PieChart } from 'lucide-svelte';
|
||||
import type { ChartDataPoint } from '$lib/api/dashboard/types';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -60,10 +61,10 @@
|
||||
<PieChart class="h-4 w-4 text-blue-500" />
|
||||
{title}
|
||||
</Card.Title>
|
||||
<Card.Description class="mt-1">Desglose por tipo de operación</Card.Description>
|
||||
<Card.Description class="mt-1">{m.dashboard_operations_breakdown()}</Card.Description>
|
||||
</div>
|
||||
{#if total > 0}
|
||||
<span class="text-sm font-semibold tabular-nums text-muted-foreground">{total.toLocaleString()} ops</span>
|
||||
<span class="text-sm font-semibold tabular-nums text-muted-foreground">{total.toLocaleString()} {m.dashboard_ops_short()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Header>
|
||||
@@ -75,8 +76,8 @@
|
||||
<PieChart class="h-8 w-8 opacity-30" />
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-sm font-medium">Sin datos disponibles</p>
|
||||
<p class="text-xs text-muted-foreground/70 mt-0.5">Las operaciones aparecerán aquí una vez registradas</p>
|
||||
<p class="text-sm font-medium">{m.dashboard_no_data_available()}</p>
|
||||
<p class="text-xs text-muted-foreground/70 mt-0.5">{m.dashboard_operations_will_appear_here()}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -102,7 +103,7 @@
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span class="text-2xl font-bold tabular-nums leading-none">{total.toLocaleString()}</span>
|
||||
<span class="text-[11px] text-muted-foreground mt-0.5">total</span>
|
||||
<span class="text-[11px] text-muted-foreground mt-0.5">{m.dashboard_total()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
} from 'lucide-svelte';
|
||||
import { fly, scale } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
@@ -41,10 +42,10 @@
|
||||
const isMissingRateContext = $derived(!isEdit && initialDate);
|
||||
const title = $derived(
|
||||
isEdit
|
||||
? 'Editar Tipo de Cambio'
|
||||
? m.exchange_rate_edit_title()
|
||||
: isMissingRateContext
|
||||
? 'Tipo de Cambio Requerido'
|
||||
: 'Nuevo Tipo de Cambio'
|
||||
? m.exchange_rate_required_title()
|
||||
: m.exchange_rate_new_title()
|
||||
);
|
||||
|
||||
let formData = $state({
|
||||
@@ -98,16 +99,16 @@
|
||||
// response.data contains { success: boolean, value: number, message: string }
|
||||
if (response.data?.success && response.data?.value) {
|
||||
formData.value = response.data.value;
|
||||
toast.success(`Tipo de cambio obtenido del DOF: ${response.data.value}`);
|
||||
toast.success(m.exchange_rate_toast_dof_success({ value: String(response.data.value) }));
|
||||
} else {
|
||||
toast.error(
|
||||
response.data?.message || response.error || 'No se pudo obtener el dato del DOF'
|
||||
response.data?.message || response.error || m.exchange_rate_toast_dof_error()
|
||||
);
|
||||
// No bloquear, permitir manual
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error('Error al consultar el servicio del DOF');
|
||||
toast.error(m.exchange_rate_toast_dof_service_error());
|
||||
} finally {
|
||||
scraping = false;
|
||||
}
|
||||
@@ -117,20 +118,20 @@
|
||||
error = null;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
if (!formData.date) throw new Error('La fecha es requerida');
|
||||
if (!companyId) throw new Error(m.exchange_rate_error_no_company());
|
||||
if (!formData.date) throw new Error(m.exchange_rate_error_date_required());
|
||||
if (
|
||||
formData.value === null ||
|
||||
formData.value === undefined ||
|
||||
String(formData.value).trim() === ''
|
||||
)
|
||||
throw new Error('El tipo de cambio es requerido');
|
||||
throw new Error(m.exchange_rate_error_value_required());
|
||||
if (Number(formData.value) <= 0)
|
||||
throw new Error('El tipo de cambio debe ser un valor mayor a 0');
|
||||
throw new Error(m.exchange_rate_error_value_positive());
|
||||
|
||||
showConfirmation = true;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al validar';
|
||||
error = e instanceof Error ? e.message : 'Error';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +140,7 @@
|
||||
error = null;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
if (!companyId) throw new Error(m.exchange_rate_error_no_company());
|
||||
|
||||
const dataToSend = {
|
||||
date: formData.date,
|
||||
@@ -150,17 +151,17 @@
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateExchangeRate(item.id, dataToSend, companyId);
|
||||
toast.success('Tipo de cambio actualizado correctamente');
|
||||
toast.success(m.exchange_rate_toast_update_success());
|
||||
} else {
|
||||
await createExchangeRate(dataToSend, companyId);
|
||||
toast.success('Tipo de cambio creado correctamente');
|
||||
toast.success(m.exchange_rate_toast_create_success());
|
||||
}
|
||||
|
||||
showConfirmation = false;
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar';
|
||||
error = e instanceof Error ? e.message : 'Error';
|
||||
showConfirmation = false;
|
||||
} finally {
|
||||
loading = false;
|
||||
@@ -192,8 +193,7 @@
|
||||
{title}
|
||||
</h3>
|
||||
<p class="text-sm leading-relaxed text-amber-800/80 dark:text-amber-200/80">
|
||||
Para continuar con el guardado, es necesario registrar el tipo de cambio oficial
|
||||
para esta fecha.
|
||||
{m.exchange_rate_required_description()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -226,7 +226,7 @@
|
||||
<div class="grid gap-5">
|
||||
<div class="grid gap-2">
|
||||
<Label for="date" class="ml-1 text-sm font-medium text-muted-foreground"
|
||||
>Fecha Aplicable</Label
|
||||
>{m.exchange_rate_applicable_date()}</Label
|
||||
>
|
||||
<div class="relative">
|
||||
<Input
|
||||
@@ -241,7 +241,7 @@
|
||||
<div
|
||||
class="absolute top-1/2 right-3 -translate-y-1/2 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-600"
|
||||
>
|
||||
Requerida
|
||||
{m.exchange_rate_required_badge()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -249,7 +249,7 @@
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="value" class="ml-1 text-sm font-medium text-muted-foreground"
|
||||
>Tipo de Cambio (MXN/USD)</Label
|
||||
>{m.exchange_rate_exchange_rate_label()}</Label
|
||||
>
|
||||
<div class="group relative">
|
||||
<div
|
||||
@@ -280,12 +280,12 @@
|
||||
onclick={() => fetchFromDof(formData.date)}
|
||||
>
|
||||
{#if scraping}
|
||||
<span class="mr-2 animate-spin">⟳</span> Consultando...
|
||||
<span class="mr-2 animate-spin">⟳</span> {m.exchange_rate_consulting()}
|
||||
{:else}
|
||||
<CloudDownload size={15} class="mr-2" /> Consultar DOF
|
||||
<CloudDownload size={15} class="mr-2" /> {m.exchange_rate_consult_dof()}
|
||||
{/if}
|
||||
</Button>
|
||||
<p class="mt-2 px-1 text-right text-[11px] text-muted-foreground">Ej. 24.1234</p>
|
||||
<p class="mt-2 px-1 text-right text-[11px] text-muted-foreground">{m.exchange_rate_example_suffix()} 24.1234</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -298,13 +298,13 @@
|
||||
disabled={loading}
|
||||
class="w-full hover:bg-muted/50"
|
||||
>
|
||||
Cancelar
|
||||
{m.exchange_rate_cancel()}
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading} class="w-full font-medium shadow-sm">
|
||||
{#if loading}
|
||||
<span class="mr-2 animate-spin">⟳</span>
|
||||
{:else}
|
||||
Ok
|
||||
{m.exchange_rate_ok()}
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
@@ -317,15 +317,18 @@
|
||||
<AlertDialog.Root bind:open={showConfirmation}>
|
||||
<AlertDialog.Content class="z-[10002]">
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>¿Estás seguro?</AlertDialog.Title>
|
||||
<AlertDialog.Title>{m.exchange_rate_confirm_title()}</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
Se {isEdit ? 'actualizará' : 'creará'} el tipo de cambio con valor {formData.value} para el día
|
||||
{formData.date}.
|
||||
{#if isEdit}
|
||||
{m.exchange_rate_confirm_description_update({ value: String(formData.value), date: formData.date })}
|
||||
{:else}
|
||||
{m.exchange_rate_confirm_description_create({ value: String(formData.value), date: formData.date })}
|
||||
{/if}
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel onclick={() => (showConfirmation = false)}>Cancelar</AlertDialog.Cancel>
|
||||
<AlertDialog.Action onclick={confirmSubmit}>Confirmar</AlertDialog.Action>
|
||||
<AlertDialog.Cancel onclick={() => (showConfirmation = false)}>{m.exchange_rate_cancel()}</AlertDialog.Cancel>
|
||||
<AlertDialog.Action onclick={confirmSubmit}>{m.exchange_rate_confirm_action()}</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/multi-currency-types';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { obtenerAtajosFormularioMonedas } from '$lib/config/shortcuts/dashboard/general_catalogs/multi_currency_types/edit';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
@@ -26,7 +27,7 @@
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(
|
||||
isEdit ? 'Editar Tipo de Cambio Múltiple' : 'Nuevo Tipo de Cambio Múltiple'
|
||||
isEdit ? m.multi_currency_edit_title() : m.multi_currency_new_title()
|
||||
);
|
||||
|
||||
// Estado del formulario
|
||||
@@ -78,11 +79,11 @@
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
if (!companyId) throw new Error(m.exchange_rate_error_no_company());
|
||||
|
||||
// Validaciones
|
||||
if (!formData.currency_type_code.trim()) throw new Error('El código de moneda es requerido');
|
||||
if (!formData.date_str) throw new Error('La fecha de publicación es requerida');
|
||||
if (!formData.currency_type_code.trim()) throw new Error(m.multi_currency_error_currency_required());
|
||||
if (!formData.date_str) throw new Error(m.multi_currency_error_date_required());
|
||||
|
||||
// Truco: Convertir String ("2025-12-31") -> Entero (20251231)
|
||||
// Quitamos los guiones y parseamos a int
|
||||
@@ -110,7 +111,7 @@
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar';
|
||||
error = e instanceof Error ? e.message : 'Error';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -139,38 +140,38 @@
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="currency_code" class="text-right"
|
||||
>Moneda <span class="text-destructive">*</span></Label
|
||||
>{m.multi_currency_currency_label()} <span class="text-destructive">*</span></Label
|
||||
>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="currency_code"
|
||||
bind:value={formData.currency_type_code}
|
||||
placeholder="Ej: USD"
|
||||
placeholder="{m.exchange_rate_example_suffix()} USD"
|
||||
maxlength={3}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Código de moneda (FK).</p>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">{m.multi_currency_currency_help()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="country_key" class="text-right">País</Label>
|
||||
<Label for="country_key" class="text-right">{m.multi_currency_country_label()}</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="country_key"
|
||||
bind:value={formData.country_key}
|
||||
placeholder="Ej: MEX"
|
||||
placeholder="{m.exchange_rate_example_suffix()} MEX"
|
||||
maxlength={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Clave M3 del país (FK).</p>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">{m.multi_currency_country_help()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="pub_date" class="text-right"
|
||||
>Fecha <span class="text-destructive">*</span></Label
|
||||
>{m.multi_currency_date_label()} <span class="text-destructive">*</span></Label
|
||||
>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
@@ -180,12 +181,12 @@
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Se guarda como entero (YYYYMMDD).</p>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">{m.multi_currency_date_help()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="factor" class="text-right">Factor</Label>
|
||||
<Label for="factor" class="text-right">{m.multi_currency_factor_label()}</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="factor"
|
||||
@@ -201,10 +202,10 @@
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => (open = false)} disabled={loading}>
|
||||
Cancelar
|
||||
{m.exchange_rate_cancel()}
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
{loading ? m.multi_currency_saving() : isEdit ? m.multi_currency_update() : m.multi_currency_create()}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { TrendingUp, TrendingDown, Minus, LineChart } from 'lucide-svelte';
|
||||
import type { ChartDataPoint } from '$lib/api/dashboard/types';
|
||||
import { onDestroy, tick } from 'svelte';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
|
||||
interface Props {
|
||||
monthlyData: ChartDataPoint[];
|
||||
@@ -67,7 +68,7 @@
|
||||
labels: monthlyData.map((d) => d.label),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Operaciones',
|
||||
label: m.dashboard_operations(),
|
||||
data: monthlyData.map((d) => d.value),
|
||||
borderColor: blue,
|
||||
borderWidth: 2.5,
|
||||
@@ -103,7 +104,7 @@
|
||||
cornerRadius: 10,
|
||||
boxPadding: 4,
|
||||
callbacks: {
|
||||
label: (ctx: any) => ` ${Number(ctx.raw).toLocaleString()} operaciones`
|
||||
label: (ctx: any) => ` ${Number(ctx.raw).toLocaleString()} ${m.dashboard_operations()}`
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -146,9 +147,9 @@
|
||||
<div>
|
||||
<Card.Title class="flex items-center gap-2 text-base">
|
||||
<LineChart class="h-4 w-4 text-blue-500" />
|
||||
Tendencia de Operaciones
|
||||
{m.dashboard_operations_trend()}
|
||||
</Card.Title>
|
||||
<Card.Description class="mt-1">Evolución mensual de operaciones</Card.Description>
|
||||
<Card.Description class="mt-1">{m.dashboard_monthly_evolution()}</Card.Description>
|
||||
</div>
|
||||
|
||||
{#if monthlyData.length >= 2}
|
||||
@@ -162,7 +163,7 @@
|
||||
{:else}
|
||||
<Minus class="h-3 w-3" />0%
|
||||
{/if}
|
||||
<span class="opacity-60 ml-0.5">vs mes ant.</span>
|
||||
<span class="opacity-60 ml-0.5">vs {m.dashboard_previous_month()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -175,8 +176,8 @@
|
||||
<LineChart class="h-8 w-8 text-blue-300" />
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-sm font-medium">Sin datos disponibles</p>
|
||||
<p class="text-xs text-muted-foreground/70 mt-0.5">Los datos aparecerán aquí una vez registrados</p>
|
||||
<p class="text-sm font-medium">{m.dashboard_no_data_available()}</p>
|
||||
<p class="text-xs text-muted-foreground/70 mt-0.5">{m.dashboard_data_will_appear_here()}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if monthlyData.length === 1}
|
||||
@@ -184,12 +185,12 @@
|
||||
<div class="text-center">
|
||||
<div class="text-5xl font-bold text-blue-600 tabular-nums">{monthlyData[0].value.toLocaleString()}</div>
|
||||
<div class="text-sm text-muted-foreground mt-2">
|
||||
operaciones en <span class="font-semibold text-foreground">{monthlyData[0].label}</span>
|
||||
{m.dashboard_operations_in()} <span class="font-semibold text-foreground">{monthlyData[0].label}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground bg-blue-50 text-blue-600 rounded-full px-4 py-2">
|
||||
<LineChart class="h-3.5 w-3.5 shrink-0" />
|
||||
La gráfica aparecerá con más de un mes de datos
|
||||
{m.dashboard_more_than_one_month_needed()}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -202,15 +203,15 @@
|
||||
<div class="grid grid-cols-3 divide-x border-t mt-4 pt-4">
|
||||
<div class="px-4 first:pl-0 last:pr-0">
|
||||
<div class="text-lg font-semibold tabular-nums text-blue-600">{total.toLocaleString()}</div>
|
||||
<div class="text-xs text-muted-foreground mt-0.5">Total</div>
|
||||
<div class="text-xs text-muted-foreground mt-0.5">{m.dashboard_total()}</div>
|
||||
</div>
|
||||
<div class="px-4">
|
||||
<div class="text-lg font-semibold tabular-nums">{average.toLocaleString()}</div>
|
||||
<div class="text-xs text-muted-foreground mt-0.5">Promedio / mes</div>
|
||||
<div class="text-xs text-muted-foreground mt-0.5">{m.dashboard_average_per_month()}</div>
|
||||
</div>
|
||||
<div class="px-4 text-right">
|
||||
<div class="text-lg font-semibold tabular-nums">{maxValue.toLocaleString()}</div>
|
||||
<div class="text-xs text-muted-foreground mt-0.5">Máximo</div>
|
||||
<div class="text-xs text-muted-foreground mt-0.5">{m.dashboard_maximum()}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
import type { DashboardStats } from '$lib/api/dashboard/types';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
import { getLocale } from '$lib/paraglide/runtime';
|
||||
|
||||
let stats = $state<DashboardStats | null>(null);
|
||||
let loading = $state(true);
|
||||
@@ -28,14 +30,14 @@
|
||||
|
||||
let greeting = $derived(() => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) return 'Buenos días';
|
||||
if (hour < 18) return 'Buenas tardes';
|
||||
return 'Buenas noches';
|
||||
if (hour < 12) return m.dashboard_greeting_morning();
|
||||
if (hour < 18) return m.dashboard_greeting_afternoon();
|
||||
return m.dashboard_greeting_evening();
|
||||
});
|
||||
|
||||
async function loadDashboardData() {
|
||||
if (!companyStore.activeCompany) {
|
||||
error = 'No hay compañía activa seleccionada';
|
||||
error = m.dashboard_no_active_company();
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
@@ -46,12 +48,14 @@
|
||||
stats = await getDashboardStats(companyStore.activeCompany.id);
|
||||
} catch (err) {
|
||||
console.error('Error loading dashboard:', err);
|
||||
error = err instanceof Error ? err.message : 'Error al cargar el dashboard';
|
||||
error = err instanceof Error ? err.message : m.dashboard_load_error();
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
const dateLocale = $derived(getLocale() === 'es' ? 'es-MX' : 'en-US');
|
||||
|
||||
// Reaccionar cuando cambie la compañía activa
|
||||
$effect(() => {
|
||||
if (companyStore.activeCompany) {
|
||||
@@ -72,27 +76,27 @@
|
||||
{/if}
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-foreground">
|
||||
{greeting()}, equipo.
|
||||
{greeting()}{m.dashboard_team_suffix()}
|
||||
</h1>
|
||||
<p class="text-muted-foreground text-sm mt-0.5">
|
||||
{#if stats?.company_name}
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<span class="inline-block w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
Resumen operativo — Anexos 22/24/30
|
||||
{m.dashboard_operational_summary()}
|
||||
</span>
|
||||
{:else}
|
||||
Sistema de gestión de comercio exterior
|
||||
{m.dashboard_management_system()}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" class="hidden sm:flex text-muted-foreground">
|
||||
<Calendar class="mr-1.5 h-3.5 w-3.5" />
|
||||
{new Date().toLocaleDateString('es-MX', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' })}
|
||||
{new Date().toLocaleDateString(dateLocale, { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' })}
|
||||
</Button>
|
||||
<Button onclick={loadDashboardData} variant="outline" size="sm" disabled={loading}>
|
||||
<RefreshCw class={`h-3.5 w-3.5 mr-1.5 ${loading ? 'animate-spin' : ''}`} />
|
||||
{loading ? 'Cargando...' : 'Actualizar'}
|
||||
{loading ? m.dashboard_loading() : m.dashboard_update()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,22 +159,22 @@
|
||||
|
||||
<!-- ── Row 1: KPI tiles ── -->
|
||||
<div class="col-span-1 md:col-span-2 lg:col-span-2">
|
||||
<KpiCard metric={stats.total_invoices} icon={FileText} iconColor="text-blue-600" />
|
||||
<KpiCard metric={{...stats.total_invoices, label: m.dashboard_invoices()}} icon={FileText} iconColor="text-blue-600" />
|
||||
</div>
|
||||
<div class="col-span-1 md:col-span-2 lg:col-span-2">
|
||||
<KpiCard metric={stats.total_pedimentos} icon={FileText} iconColor="text-green-600" />
|
||||
<KpiCard metric={{...stats.total_pedimentos, label: m.dashboard_pedimentos()}} icon={FileText} iconColor="text-green-600" />
|
||||
</div>
|
||||
<div class="col-span-1 md:col-span-2 lg:col-span-2">
|
||||
<KpiCard metric={stats.total_clients} icon={Users} iconColor="text-purple-600" />
|
||||
<KpiCard metric={{...stats.total_clients, label: m.dashboard_clients()}} icon={Users} iconColor="text-purple-600" />
|
||||
</div>
|
||||
<div class="col-span-1 md:col-span-2 lg:col-span-2">
|
||||
<KpiCard metric={stats.total_providers} icon={Users} iconColor="text-orange-600" />
|
||||
<KpiCard metric={{...stats.total_providers, label: m.dashboard_providers()}} icon={Users} iconColor="text-orange-600" />
|
||||
</div>
|
||||
<div class="col-span-1 md:col-span-3 lg:col-span-2">
|
||||
<KpiCard metric={stats.active_items} icon={Package} iconColor="text-cyan-600" />
|
||||
<KpiCard metric={{...stats.active_items, label: m.dashboard_active_items_stat()}} icon={Package} iconColor="text-cyan-600" />
|
||||
</div>
|
||||
<div class="col-span-1 md:col-span-3 lg:col-span-2">
|
||||
<KpiCard metric={stats.pending_approvals} icon={Clock} iconColor="text-yellow-600" />
|
||||
<KpiCard metric={{...stats.pending_approvals, label: m.dashboard_pending_approvals()}} icon={Clock} iconColor="text-yellow-600" />
|
||||
</div>
|
||||
|
||||
<!-- ── Row 2: TrendChart (8) | Donut (4) — altura natural, sin row-span ── -->
|
||||
@@ -178,14 +182,14 @@
|
||||
<TrendChart monthlyData={stats.invoices_by_month} class="h-full" />
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4">
|
||||
<DonutChart title="Distribución de Operaciones" data={stats.operations_by_type} class="h-full" />
|
||||
<DonutChart title={m.dashboard_operations_distribution()} data={stats.operations_by_type} class="h-full" />
|
||||
</div>
|
||||
|
||||
<!-- ── Row 3: Clientes (4) | Proveedores (4) | Accesos Rápidos (4) ── -->
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4">
|
||||
<ChartCard
|
||||
title="Top 5 Clientes"
|
||||
description="Por número de facturas"
|
||||
title={m.dashboard_top_clients()}
|
||||
description={m.dashboard_by_invoice_number()}
|
||||
data={stats.top_clients}
|
||||
type="bar"
|
||||
class="h-full"
|
||||
@@ -193,8 +197,8 @@
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4">
|
||||
<ChartCard
|
||||
title="Top 5 Proveedores"
|
||||
description="Por número de facturas"
|
||||
title={m.dashboard_top_providers()}
|
||||
description={m.dashboard_by_invoice_number()}
|
||||
data={stats.top_providers}
|
||||
type="bar"
|
||||
class="h-full"
|
||||
@@ -203,8 +207,8 @@
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4">
|
||||
<Card.Root class="h-full overflow-hidden">
|
||||
<Card.Header class="pb-3">
|
||||
<Card.Title class="text-base">Accesos Rápidos</Card.Title>
|
||||
<Card.Description>Módulos más utilizados</Card.Description>
|
||||
<Card.Title class="text-base">{m.dashboard_quick_access()}</Card.Title>
|
||||
<Card.Description>{m.dashboard_most_used_modules()}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-1.5">
|
||||
<a href="/dashboard/invoices" class="flex items-center gap-3 p-3 rounded-lg border hover:bg-accent hover:border-primary/30 transition-all group">
|
||||
@@ -212,8 +216,8 @@
|
||||
<FileText class="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Facturas</div>
|
||||
<div class="text-xs text-muted-foreground">Gestionar facturas</div>
|
||||
<div class="text-sm font-medium">{m.dashboard_invoices()}</div>
|
||||
<div class="text-xs text-muted-foreground">{m.dashboard_manage_invoices()}</div>
|
||||
</div>
|
||||
<span class="text-muted-foreground/40 group-hover:text-primary transition-colors text-lg leading-none">›</span>
|
||||
</a>
|
||||
@@ -222,8 +226,8 @@
|
||||
<Users class="h-4 w-4 text-purple-600" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Clientes y Proveedores</div>
|
||||
<div class="text-xs text-muted-foreground">Administrar contactos</div>
|
||||
<div class="text-sm font-medium">{m.dashboard_clients_and_providers()}</div>
|
||||
<div class="text-xs text-muted-foreground">{m.dashboard_manage_contacts()}</div>
|
||||
</div>
|
||||
<span class="text-muted-foreground/40 group-hover:text-primary transition-colors text-lg leading-none">›</span>
|
||||
</a>
|
||||
@@ -232,8 +236,8 @@
|
||||
<Package class="h-4 w-4 text-cyan-600" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Mercancías</div>
|
||||
<div class="text-xs text-muted-foreground">Catálogo de productos</div>
|
||||
<div class="text-sm font-medium">{m.dashboard_goods()}</div>
|
||||
<div class="text-xs text-muted-foreground">{m.dashboard_product_catalog()}</div>
|
||||
</div>
|
||||
<span class="text-muted-foreground/40 group-hover:text-primary transition-colors text-lg leading-none">›</span>
|
||||
</a>
|
||||
@@ -242,8 +246,8 @@
|
||||
<BarChart3 class="h-4 w-4 text-orange-600" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Datos de Referencia</div>
|
||||
<div class="text-xs text-muted-foreground">Catálogos del SAT</div>
|
||||
<div class="text-sm font-medium">{m.dashboard_reference_data()}</div>
|
||||
<div class="text-xs text-muted-foreground">{m.dashboard_sat_catalogs()}</div>
|
||||
</div>
|
||||
<span class="text-muted-foreground/40 group-hover:text-primary transition-colors text-lg leading-none">›</span>
|
||||
</a>
|
||||
@@ -259,19 +263,19 @@
|
||||
<div class="text-3xl font-bold text-foreground tabular-nums">
|
||||
{(stats.total_invoices.value + stats.total_pedimentos.value).toLocaleString()}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1.5 font-medium uppercase tracking-wide">Total Documentos</div>
|
||||
<div class="text-xs text-muted-foreground mt-1.5 font-medium uppercase tracking-wide">{m.dashboard_total_documents()}</div>
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4 rounded-xl border bg-card p-5 flex flex-col items-center justify-center text-center">
|
||||
<div class="text-3xl font-bold text-foreground tabular-nums">
|
||||
{(stats.total_clients.value + stats.total_providers.value).toLocaleString()}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1.5 font-medium uppercase tracking-wide">Total Contactos</div>
|
||||
<div class="text-xs text-muted-foreground mt-1.5 font-medium uppercase tracking-wide">{m.dashboard_total_contacts()}</div>
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4 rounded-xl border bg-card p-5 flex flex-col items-center justify-center text-center">
|
||||
<div class="text-3xl font-bold text-foreground tabular-nums">
|
||||
{stats.active_items.value.toLocaleString()}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1.5 font-medium uppercase tracking-wide">Items Activos</div>
|
||||
<div class="text-xs text-muted-foreground mt-1.5 font-medium uppercase tracking-wide">{m.dashboard_active_items_stat()}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user