Merge pull request 'feature/dashboard-lenguage' (#359) from feature/dashboard-lenguage into development
Reviewed-on: ADUANASOFT/anexo76#359
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user