- Implemented DTOs for KPIs, activity items, chart data points, and dashboard statistics. - Created API routes for fetching dashboard statistics, operations overview, and inventory metrics. - Developed a service layer to handle the logic for generating dashboard statistics, including invoice metrics, client/provider metrics, and recent activity. - Added frontend API client methods for fetching dashboard data. - Created TypeScript types for dashboard data structures. - Developed Svelte components for displaying recent activity, KPI cards, trend charts, and donut charts.
105 lines
3.0 KiB
Svelte
105 lines
3.0 KiB
Svelte
<script lang="ts">
|
|
import * as Card from '$lib/components/ui/card';
|
|
import type { ChartDataPoint } from '$lib/api/dashboard/types';
|
|
import { cn } from '$lib/utils';
|
|
|
|
interface Props {
|
|
title: string;
|
|
data: ChartDataPoint[];
|
|
colors?: string[];
|
|
}
|
|
|
|
let { title, data, colors = ['hsl(var(--primary))', 'hsl(var(--secondary))', 'hsl(var(--accent))', 'hsl(var(--muted))'] }: Props = $props();
|
|
|
|
let total = $derived(data.reduce((sum, d) => sum + d.value, 0));
|
|
|
|
let segments = $derived(
|
|
data.map((d, i) => ({
|
|
...d,
|
|
percentage: total > 0 ? (d.value / total) * 100 : 0,
|
|
color: colors[i % colors.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
|
|
};
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title>{title}</Card.Title>
|
|
</Card.Header>
|
|
<Card.Content>
|
|
{#if data.length === 0}
|
|
<div class="text-center text-muted-foreground py-8">No hay datos disponibles</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}
|
|
<circle
|
|
cx="100"
|
|
cy="100"
|
|
r={radius}
|
|
fill="none"
|
|
stroke={segment.color}
|
|
stroke-width={strokeWidth}
|
|
stroke-dasharray={segment.dashArray}
|
|
stroke-dashoffset={-segment.offset}
|
|
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>
|
|
</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>
|
|
</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>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</Card.Content>
|
|
</Card.Root>
|