- 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.
137 lines
4.5 KiB
Svelte
137 lines
4.5 KiB
Svelte
<script lang="ts">
|
|
import * as Card from '$lib/components/ui/card';
|
|
import { PieChart } from 'lucide-svelte';
|
|
import type { ChartDataPoint } from '$lib/api/dashboard/types';
|
|
|
|
interface Props {
|
|
title: string;
|
|
data: ChartDataPoint[];
|
|
class?: string;
|
|
}
|
|
|
|
let { title, data, class: cls = '' }: Props = $props();
|
|
|
|
// 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(
|
|
sorted.map((d, i) => ({
|
|
...d,
|
|
pct: total > 0 ? (d.value / total) * 100 : 0,
|
|
color: PALETTE[i % PALETTE.length]
|
|
}))
|
|
);
|
|
|
|
// 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 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 class="pt-4">
|
|
{#if data.length === 0}
|
|
<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 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="88" cy="88" r={R}
|
|
fill="none"
|
|
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>
|
|
<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 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="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>
|
|
</Card.Root>
|