feat: add optional seed data functionality to init_first_time.sh

- Implemented argument parsing to allow for optional loading of example data with --seed-data flag.
- Added functions to seed example data for customs brokers, clients and providers, packages, classes, parts, pedimentos, and invoices.
- Enhanced user feedback during the seeding process with detailed output of the seeded data counts.
- Updated usage instructions in the script header to reflect the new functionality.
This commit is contained in:
2026-03-08 01:15:08 -06:00
parent c05c6b9939
commit 2aab5c8fdc
9 changed files with 900 additions and 326 deletions

View File

@@ -14,9 +14,10 @@
interface Props {
activities: ActivityItem[];
class?: string;
}
let { activities }: Props = $props();
let { activities, class: cls = '' }: Props = $props();
const icons = {
invoice: FileText,
@@ -39,14 +40,24 @@
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
// Normalizar: si el string no tiene info de zona horaria, asumir UTC agregando 'Z'
const normalized = /[Z+\-]\d*$/.test(dateStr.trim()) ? dateStr : dateStr + 'Z';
const date = new Date(normalized);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
// Si la fecha es futura (diff negativo) o muy reciente, mostrar 'Ahora mismo'
if (diffMs < 0) {
return 'Ahora mismo';
}
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 60) {
if (diffMins < 1) {
return 'Ahora mismo';
} else if (diffMins < 60) {
return `Hace ${diffMins} min`;
} else if (diffHours < 24) {
return `Hace ${diffHours}h`;
@@ -58,48 +69,55 @@
}
</script>
<Card.Root>
<Card.Header>
<Card.Title>Actividad Reciente</Card.Title>
<Card.Description>Últimas operaciones registradas en el sistema</Card.Description>
<Card.Root class={`overflow-hidden ${cls}`}>
<Card.Header class="pb-3">
<div class="flex items-center justify-between">
<div>
<Card.Title class="flex items-center gap-2">
<Clock class="h-4 w-4 text-primary" />
Actividad Reciente
</Card.Title>
<Card.Description class="mt-0.5">Últimas operaciones registradas en el sistema</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>
{/if}
</div>
</Card.Header>
<Card.Content>
{#if activities.length === 0}
<div class="text-center text-muted-foreground py-8">
<Clock class="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>No hay actividad reciente</p>
<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>
</div>
{:else}
<div class="space-y-4">
{#each activities as activity}
{@const Icon = getIcon(activity.type)}
<div class="flex items-start gap-4">
<div class="mt-1">
<div class="p-2 rounded-lg bg-primary/10">
<Icon class="h-4 w-4 text-primary" />
<div class="relative">
<!-- Timeline connector line -->
<div class="absolute left-[18px] top-2 bottom-2 w-px bg-border"></div>
<div class="space-y-1">
{#each activities as activity, idx}
{@const Icon = getIcon(activity.type)}
<div class="flex items-start gap-3 relative py-2 rounded-lg px-1 hover:bg-muted/40 transition-colors">
<div class="mt-0.5 z-10">
<div class="p-1.5 rounded-lg bg-background border border-border shadow-sm">
<Icon class="h-3.5 w-3.5 text-primary" />
</div>
</div>
<div class="flex-1 min-w-0 pt-0.5">
<div class="flex items-start justify-between gap-2">
<p class="text-sm font-medium leading-snug">{activity.title}</p>
<span class="text-[11px] text-muted-foreground whitespace-nowrap shrink-0">
{formatDate(activity.timestamp)}
</span>
</div>
{#if activity.description}
<p class="text-xs text-muted-foreground mt-0.5">{activity.description}</p>
{/if}
</div>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-2">
<p class="text-sm font-medium leading-none">{activity.title}</p>
<span class="text-xs text-muted-foreground whitespace-nowrap">
{formatDate(activity.timestamp)}
</span>
</div>
{#if activity.description}
<p class="text-sm text-muted-foreground mt-1">{activity.description}</p>
{/if}
{#if activity.status}
<Badge
variant={statusColors[activity.status as keyof typeof statusColors] || 'default'}
class="mt-2"
>
{activity.status}
</Badge>
{/if}
</div>
</div>
{/each}
{/each}
</div>
</div>
{/if}
</Card.Content>

View File

@@ -1,41 +1,62 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import type { ChartDataPoint } from '$lib/api/dashboard/types';
import { onMount } from 'svelte';
interface Props {
title: string;
description?: string;
data: ChartDataPoint[];
type?: 'bar' | 'line' | 'pie';
class?: string;
}
let { title, description, data, type = 'bar' }: Props = $props();
let { title, description, data, type = 'bar', class: cls = '' }: Props = $props();
let maxValue = $derived(Math.max(...data.map((d) => d.value), 1));
const rankColors = [
'text-yellow-600 bg-yellow-50 dark:bg-yellow-950/40 border-yellow-200 dark:border-yellow-800',
'text-slate-500 bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700',
'text-orange-600 bg-orange-50 dark:bg-orange-950/40 border-orange-200 dark:border-orange-800',
'text-muted-foreground bg-muted border-border',
'text-muted-foreground bg-muted border-border'
];
const barGradients = [
'from-primary to-primary/60',
'from-primary/85 to-primary/50',
'from-primary/70 to-primary/40',
'from-primary/55 to-primary/30',
'from-primary/40 to-primary/20'
];
</script>
<Card.Root>
<Card.Header>
<Card.Title>{title}</Card.Title>
<Card.Root class={`overflow-hidden ${cls}`}>
<Card.Header class="pb-3">
<Card.Title class="text-base">{title}</Card.Title>
{#if description}
<Card.Description>{description}</Card.Description>
{/if}
</Card.Header>
<Card.Content>
{#if data.length === 0}
<div class="text-center text-muted-foreground py-8">No hay datos disponibles</div>
<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>
</div>
{:else if type === 'bar'}
<div class="space-y-3">
{#each data as item}
<div class="space-y-1">
<div class="flex items-center justify-between text-sm">
<span class="font-medium truncate">{item.label}</span>
<span class="text-muted-foreground">{item.value.toLocaleString()}</span>
{#each data as item, i}
<div class="space-y-1.5">
<div class="flex items-center gap-2 text-sm">
<span
class={`inline-flex items-center justify-center w-5 h-5 rounded text-[10px] font-bold border shrink-0 ${rankColors[i] ?? rankColors[3]}`}
>{i + 1}</span>
<span class="font-medium truncate flex-1">{item.label}</span>
<span class="text-muted-foreground font-medium tabular-nums">{item.value.toLocaleString()}</span>
</div>
<div class="h-2 bg-muted rounded-full overflow-hidden">
<div class="h-1.5 bg-muted rounded-full overflow-hidden">
<div
class="h-full bg-primary rounded-full transition-all duration-500"
class={`h-full rounded-full bg-gradient-to-r transition-all duration-700 ${barGradients[i] ?? barGradients[4]}`}
style="width: {(item.value / maxValue) * 100}%"
></div>
</div>
@@ -43,10 +64,10 @@
{/each}
</div>
{:else if type === 'pie'}
<div class="grid grid-cols-2 gap-4">
{#each data as item}
<div class="grid grid-cols-2 gap-3">
{#each data as item, i}
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-primary"></div>
<div class="w-2.5 h-2.5 rounded-full bg-primary" style="opacity: {1 - i * 0.15}"></div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium truncate">{item.label}</div>
<div class="text-xs text-muted-foreground">{item.value.toLocaleString()}</div>

View File

@@ -1,103 +1,135 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { PieChart } from 'lucide-svelte';
import type { ChartDataPoint } from '$lib/api/dashboard/types';
import { cn } from '$lib/utils';
interface Props {
title: string;
data: ChartDataPoint[];
colors?: string[];
class?: string;
}
let { title, data, colors = ['hsl(var(--primary))', 'hsl(var(--secondary))', 'hsl(var(--accent))', 'hsl(var(--muted))'] }: Props = $props();
let { title, data, class: cls = '' }: Props = $props();
let total = $derived(data.reduce((sum, d) => sum + d.value, 0));
// Palette: azul → índigo → cyan → violeta → teal → sky → amber → emerald
const PALETTE = [
{ bg: '#3b82f6', light: '#eff6ff', text: '#1d4ed8' },
{ bg: '#6366f1', light: '#eef2ff', text: '#4338ca' },
{ bg: '#06b6d4', light: '#ecfeff', text: '#0e7490' },
{ bg: '#8b5cf6', light: '#f5f3ff', text: '#6d28d9' },
{ bg: '#14b8a6', light: '#f0fdfa', text: '#0f766e' },
{ bg: '#0ea5e9', light: '#f0f9ff', text: '#0369a1' },
{ bg: '#f59e0b', light: '#fffbeb', text: '#b45309' },
{ bg: '#10b981', light: '#ecfdf5', text: '#047857' },
];
let total = $derived(data.reduce((sum, d) => sum + d.value, 0));
let sorted = $derived([...data].sort((a, b) => b.value - a.value));
let segments = $derived(
data.map((d, i) => ({
sorted.map((d, i) => ({
...d,
percentage: total > 0 ? (d.value / total) * 100 : 0,
color: colors[i % colors.length]
pct: total > 0 ? (d.value / total) * 100 : 0,
color: PALETTE[i % PALETTE.length]
}))
);
// Generate SVG donut chart
let radius = 80;
let strokeWidth = 30;
let circumference = 2 * Math.PI * radius;
let donutSegments = $derived(() => {
let cumulativePercentage = 0;
return segments.map((seg) => {
const offset = (cumulativePercentage / 100) * circumference;
const dashArray = `${(seg.percentage / 100) * circumference} ${circumference}`;
cumulativePercentage += seg.percentage;
return {
...seg,
offset,
dashArray
};
// SVG donut
const R = 72;
const SW = 22;
const CIRC = 2 * Math.PI * R;
const GAP = 0.018 * CIRC;
let donutSlices = $derived(() => {
let cum = 0;
return segments.map((s) => {
const arc = Math.max((s.pct / 100) * CIRC - GAP, 0);
const dashArray = `${arc} ${CIRC - arc}`;
const dashOffset = -(cum * CIRC / 100);
cum += s.pct;
return { ...s, dashArray, dashOffset };
});
});
</script>
<Card.Root>
<Card.Header>
<Card.Title>{title}</Card.Title>
<Card.Root class={cls}>
<Card.Header class="pb-0">
<div class="flex items-center justify-between">
<div>
<Card.Title class="flex items-center gap-2 text-base">
<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>
</div>
{#if total > 0}
<span class="text-sm font-semibold tabular-nums text-muted-foreground">{total.toLocaleString()} ops</span>
{/if}
</div>
</Card.Header>
<Card.Content>
<Card.Content class="pt-4">
{#if data.length === 0}
<div class="text-center text-muted-foreground py-8">No hay datos disponibles</div>
<div class="flex flex-col items-center justify-center py-14 gap-3 text-muted-foreground">
<div class="rounded-full bg-muted p-4">
<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>
</div>
</div>
{:else}
<div class="flex flex-col md:flex-row items-center justify-center gap-8">
<!-- Donut Chart -->
<div class="relative">
<svg width="200" height="200" viewBox="0 0 200 200" class="transform -rotate-90">
{#each donutSegments() as segment, i}
<div class="flex flex-col sm:flex-row items-center gap-6">
<!-- Donut SVG -->
<div class="relative shrink-0">
<svg width="176" height="176" viewBox="0 0 176 176" class="-rotate-90">
<circle cx="88" cy="88" r={R} fill="none"
stroke="currentColor" stroke-width={SW} stroke-opacity="0.06" />
{#each donutSlices() as s}
<circle
cx="100"
cy="100"
r={radius}
cx="88" cy="88" r={R}
fill="none"
stroke={segment.color}
stroke-width={strokeWidth}
stroke-dasharray={segment.dashArray}
stroke-dashoffset={-segment.offset}
stroke={s.color.bg}
stroke-width={SW}
stroke-dasharray={s.dashArray}
stroke-dashoffset={s.dashOffset}
stroke-linecap="butt"
class="transition-all duration-500"
/>
{/each}
</svg>
<!-- Center Text -->
<div class="absolute inset-0 flex items-center justify-center">
<div class="text-center">
<div class="text-3xl font-bold">{total.toLocaleString()}</div>
<div class="text-xs text-muted-foreground">Total</div>
</div>
<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>
</div>
</div>
<!-- Legend -->
<div class="space-y-2 flex-1">
{#each segments as segment, i}
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-2 min-w-0 flex-1">
<div
class="w-3 h-3 rounded-full flex-shrink-0"
style="background-color: {segment.color}"
></div>
<span class="text-sm font-medium truncate">{segment.label}</span>
<!-- Legend with progress bars -->
<div class="flex-1 w-full space-y-3 min-w-0">
{#each segments as s}
<div>
<div class="flex items-center justify-between mb-1 gap-2">
<div class="flex items-center gap-2 min-w-0">
<span class="inline-block h-2.5 w-2.5 rounded-full shrink-0" style="background:{s.color.bg}"></span>
<span class="text-xs font-medium truncate leading-snug">{s.label}</span>
</div>
<div class="flex items-center gap-1.5 shrink-0">
<span class="text-xs font-semibold tabular-nums">{s.value.toLocaleString()}</span>
<span class="text-[11px] text-muted-foreground w-10 text-right">{s.pct.toFixed(1)}%</span>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-muted-foreground">
{segment.value.toLocaleString()}
</span>
<span class="text-xs text-muted-foreground w-12 text-right">
({segment.percentage.toFixed(1)}%)
</span>
<div class="h-1.5 w-full rounded-full bg-muted overflow-hidden">
<div
class="h-full rounded-full transition-all duration-700"
style="width:{s.pct}%; background:{s.color.bg}; opacity:0.75"
></div>
</div>
</div>
{/each}
</div>
</div>
{/if}
</Card.Content>

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { TrendingUp, TrendingDown, Minus } from 'lucide-svelte';
import type { KPIMetric } from '$lib/api/dashboard/types';
import { cn } from '$lib/utils';
@@ -12,45 +11,44 @@
let { metric, icon: Icon, iconColor = 'text-primary' }: Props = $props();
const trendIcons = {
up: TrendingUp,
down: TrendingDown,
stable: Minus
const iconBgMap: Record<string, string> = {
'text-blue-600': 'bg-blue-50 dark:bg-blue-950/40',
'text-green-600': 'bg-green-50 dark:bg-green-950/40',
'text-purple-600': 'bg-purple-50 dark:bg-purple-950/40',
'text-orange-600': 'bg-orange-50 dark:bg-orange-950/40',
'text-cyan-600': 'bg-cyan-50 dark:bg-cyan-950/40',
'text-yellow-600': 'bg-yellow-50 dark:bg-yellow-950/40'
};
const trendColors = {
up: 'text-green-600',
down: 'text-red-600',
stable: 'text-gray-600'
up: 'text-emerald-600',
down: 'text-red-500',
stable: 'text-slate-400'
};
const TrendIcon = metric.trend ? trendIcons[metric.trend] : null;
const TrendIcon = metric.trend ? { up: TrendingUp, down: TrendingDown, stable: Minus }[metric.trend] : null;
let iconBg = $derived(iconBgMap[iconColor] ?? 'bg-primary/10');
</script>
<Card.Root class="overflow-hidden">
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
<Card.Title class="text-sm font-medium text-muted-foreground">
{metric.label}
</Card.Title>
{#if Icon}
<div class="flex items-center gap-4 rounded-xl border bg-card px-4 py-3.5 hover:shadow-sm transition-shadow">
{#if Icon}
<div class={cn('flex h-9 w-9 shrink-0 items-center justify-center rounded-lg', iconBg)}>
<Icon class={cn('h-4 w-4', iconColor)} />
{/if}
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">
{metric.value.toLocaleString()}
{#if metric.unit}
<span class="text-sm font-normal text-muted-foreground ml-1">{metric.unit}</span>
{/if}
</div>
{#if metric.percentage_change !== undefined && TrendIcon}
<div class="flex items-center text-xs mt-1">
<TrendIcon class={cn('h-3 w-3 mr-1', trendColors[metric.trend || 'stable'])} />
<span class={cn(trendColors[metric.trend || 'stable'])}>
{/if}
<div class="flex-1 min-w-0">
<p class="text-xs text-muted-foreground truncate">{metric.label}</p>
<div class="flex items-baseline gap-2 mt-0.5">
<span class="text-xl font-bold tabular-nums leading-none">
{metric.value.toLocaleString()}{#if metric.unit}<span class="text-sm font-normal text-muted-foreground ml-0.5">{metric.unit}</span>{/if}
</span>
{#if metric.percentage_change !== undefined && TrendIcon && metric.trend}
<span class={cn('flex items-center gap-0.5 text-xs font-medium', trendColors[metric.trend])}>
<TrendIcon class="h-3 w-3" />
{Math.abs(metric.percentage_change).toFixed(1)}%
</span>
<span class="text-muted-foreground ml-1">vs mes anterior</span>
</div>
{/if}
</Card.Content>
</Card.Root>
{/if}
</div>
</div>
</div>

View File

@@ -1,71 +1,216 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/components/ui/tabs';
import { TrendingUp, TrendingDown, Minus, LineChart } from 'lucide-svelte';
import type { ChartDataPoint } from '$lib/api/dashboard/types';
import { onDestroy, tick } from 'svelte';
interface Props {
monthlyData: ChartDataPoint[];
class?: string;
}
let { monthlyData }: Props = $props();
let { monthlyData, class: cls = '' }: Props = $props();
let total = $derived(monthlyData.reduce((sum, d) => sum + d.value, 0));
let average = $derived(monthlyData.length > 0 ? Math.round(total / monthlyData.length) : 0);
let maxValue = $derived(Math.max(...monthlyData.map((d) => d.value), 1));
// Trend: compare last two months
let trendPct = $derived(() => {
if (monthlyData.length < 2) return 0;
const last = monthlyData[monthlyData.length - 1].value;
const prev = monthlyData[monthlyData.length - 2].value;
if (prev === 0) return 0;
return Math.round(((last - prev) / prev) * 100);
});
let canvas: HTMLCanvasElement | undefined = $state();
let chartInstance: any;
async function buildChart() {
if (!canvas || monthlyData.length < 2) return;
const {
Chart,
LineController,
LineElement, PointElement,
CategoryScale, LinearScale,
Tooltip, Filler
} = await import('chart.js');
Chart.register(LineController, LineElement, PointElement, CategoryScale, LinearScale, Tooltip, Filler);
chartInstance?.destroy();
// Blue palette
const blue = 'rgb(59, 130, 246)'; // blue-500
const blueDark = 'rgb(37, 99, 235)'; // blue-600
const blueLight = 'rgba(59, 130, 246, 0.12)';
const blueMid = 'rgba(59, 130, 246, 0.5)';
const muted = '#94a3b8'; // slate-400
const gridColor = 'rgba(148, 163, 184, 0.12)';
const foreground = '#0f172a'; // slate-900
const popover = '#ffffff';
const ctx = canvas.getContext('2d')!;
const h = canvas.clientHeight || 260;
// Gradient fill under the area
const areaGrad = ctx.createLinearGradient(0, 0, 0, h);
areaGrad.addColorStop(0, blueMid);
areaGrad.addColorStop(0.6, blueLight);
areaGrad.addColorStop(1, 'rgba(59, 130, 246, 0)');
chartInstance = new Chart(canvas, {
type: 'line',
data: {
labels: monthlyData.map((d) => d.label),
datasets: [
{
label: 'Operaciones',
data: monthlyData.map((d) => d.value),
borderColor: blue,
borderWidth: 2.5,
pointBackgroundColor: blueDark,
pointBorderColor: popover,
pointBorderWidth: 2.5,
pointRadius: 5,
pointHoverRadius: 7,
pointHoverBorderWidth: 2.5,
pointHoverBackgroundColor: blueDark,
pointHoverBorderColor: popover,
tension: 0.45,
fill: true,
backgroundColor: areaGrad
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 800, easing: 'easeOutCubic' },
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: popover,
titleColor: foreground,
titleFont: { weight: 'bold', size: 12 },
bodyColor: muted,
borderColor: 'rgba(59,130,246,0.25)',
borderWidth: 1,
padding: { x: 14, y: 10 },
cornerRadius: 10,
boxPadding: 4,
callbacks: {
label: (ctx: any) => ` ${Number(ctx.raw).toLocaleString()} operaciones`
}
}
},
scales: {
x: {
grid: { display: false },
border: { display: false },
ticks: { color: muted, font: { size: 11 }, maxRotation: 0 }
},
y: {
beginAtZero: true,
border: { display: false, dash: [4, 4] },
grid: { color: gridColor },
ticks: {
color: muted,
font: { size: 11 },
maxTicksLimit: 5,
callback: (v: any) => Number(v).toLocaleString()
}
}
}
} as any
} as any);
}
onDestroy(() => { chartInstance?.destroy(); });
$effect(() => {
const data = monthlyData;
const el = canvas;
if (el && data.length >= 2) {
tick().then(() => buildChart());
}
});
</script>
<Card.Root>
<Card.Header>
<Card.Title>Tendencia de Operaciones</Card.Title>
<Card.Description>Evolución mensual de facturas y pedimentos</Card.Description>
</Card.Header>
<Card.Content>
{#if monthlyData.length === 0}
<div class="text-center text-muted-foreground py-12">
<p>No hay datos disponibles</p>
</div>
{:else}
<!-- Line Chart Visualization -->
<div class="h-64 flex items-end justify-between gap-2 border-b border-l p-4">
{#each monthlyData as point, i}
<div class="flex-1 flex flex-col items-center gap-2 group">
<!-- Bar -->
<div class="w-full flex items-end justify-center" style="height: 200px;">
<div
class="w-full max-w-16 bg-primary rounded-t-md transition-all duration-500 hover:bg-primary/80 relative group-hover:shadow-lg"
style="height: {(point.value / maxValue) * 100}%"
>
<!-- Tooltip on hover -->
<div
class="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-popover text-popover-foreground px-2 py-1 rounded text-xs opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap shadow-md border"
>
{point.value.toLocaleString()}
</div>
</div>
</div>
<!-- Label -->
<span class="text-xs text-muted-foreground font-medium">{point.label}</span>
</div>
{/each}
<Card.Root class={cls}>
<Card.Header class="pb-0">
<div class="flex items-start justify-between">
<div>
<Card.Title class="flex items-center gap-2 text-base">
<LineChart class="h-4 w-4 text-blue-500" />
Tendencia de Operaciones
</Card.Title>
<Card.Description class="mt-1">Evolución mensual de operaciones</Card.Description>
</div>
<!-- Stats Summary -->
<div class="grid grid-cols-3 gap-4 mt-4 pt-4 border-t">
<div class="text-center">
<div class="text-2xl font-bold text-primary">
{monthlyData.reduce((sum, d) => sum + d.value, 0).toLocaleString()}
</div>
<div class="text-xs text-muted-foreground mt-1">Total</div>
{#if monthlyData.length >= 2}
{@const pct = trendPct()}
<div class="flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium
{pct > 0 ? 'bg-green-50 text-green-600' : pct < 0 ? 'bg-red-50 text-red-600' : 'bg-muted text-muted-foreground'}">
{#if pct > 0}
<TrendingUp class="h-3 w-3" />+{pct}%
{:else if pct < 0}
<TrendingDown class="h-3 w-3" />{pct}%
{:else}
<Minus class="h-3 w-3" />0%
{/if}
<span class="opacity-60 ml-0.5">vs mes ant.</span>
</div>
{/if}
</div>
</Card.Header>
<Card.Content class="pt-4 pb-5">
{#if monthlyData.length === 0}
<div class="flex flex-col items-center justify-center py-14 text-muted-foreground gap-3">
<div class="rounded-full bg-blue-50 p-4">
<LineChart class="h-8 w-8 text-blue-300" />
</div>
<div class="text-center">
<div class="text-2xl font-bold text-primary">
{Math.round(
monthlyData.reduce((sum, d) => sum + d.value, 0) / monthlyData.length
).toLocaleString()}
</div>
<div class="text-xs text-muted-foreground mt-1">Promedio</div>
<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>
</div>
</div>
{:else if monthlyData.length === 1}
<div class="flex flex-col items-center justify-center py-8 gap-5">
<div class="text-center">
<div class="text-2xl font-bold text-primary">{maxValue.toLocaleString()}</div>
<div class="text-xs text-muted-foreground mt-1">Máximo</div>
<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>
</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
</div>
</div>
{:else}
<!-- Chart -->
<div class="h-56">
<canvas bind:this={canvas}></canvas>
</div>
<!-- Stats row -->
<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>
<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>
<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>
</div>
{/if}