Files
plantillas-proyectos/frontend/src/lib/components/dashboard/donut-chart.svelte
AlexeerCT 2aab5c8fdc 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.
2026-03-08 01:15:08 -06:00

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>