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:
@@ -59,6 +59,7 @@
|
||||
"dependencies": {
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/marked": "^6.0.0",
|
||||
"chart.js": "^4.5.1",
|
||||
"dompurify": "^3.0.9",
|
||||
"keycloak-js": "^26.2.1",
|
||||
"lucide-svelte": "^0.553.0",
|
||||
|
||||
16
frontend/pnpm-lock.yaml
generated
16
frontend/pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@types/marked':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
chart.js:
|
||||
specifier: ^4.5.1
|
||||
version: 4.5.1
|
||||
dompurify:
|
||||
specifier: ^3.0.9
|
||||
version: 3.3.1
|
||||
@@ -415,6 +418,9 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@kurkle/color@0.3.4':
|
||||
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
|
||||
|
||||
'@lix-js/sdk@0.4.7':
|
||||
resolution: {integrity: sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -993,6 +999,10 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chart.js@4.5.1:
|
||||
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
|
||||
engines: {pnpm: '>=8'}
|
||||
|
||||
check-error@2.1.1:
|
||||
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
||||
engines: {node: '>= 16'}
|
||||
@@ -2289,6 +2299,8 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@kurkle/color@0.3.4': {}
|
||||
|
||||
'@lix-js/sdk@0.4.7':
|
||||
dependencies:
|
||||
'@lix-js/server-protocol-schema': 0.1.1
|
||||
@@ -2852,6 +2864,10 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chart.js@4.5.1:
|
||||
dependencies:
|
||||
'@kurkle/color': 0.3.4
|
||||
|
||||
check-error@2.1.1: {}
|
||||
|
||||
chokidar@4.0.3:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -63,26 +63,35 @@
|
||||
<div class="space-y-6">
|
||||
{#snippet headerSection()}
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight text-foreground">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground mb-1">
|
||||
<span>Dashboard</span>
|
||||
{#if stats?.company_name}
|
||||
<span>/</span>
|
||||
<span class="text-primary font-medium">{stats.company_name}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-foreground">
|
||||
{greeting()}, equipo.
|
||||
</h1>
|
||||
<p class="text-muted-foreground mt-1 flex items-center gap-2">
|
||||
<p class="text-muted-foreground text-sm mt-0.5">
|
||||
{#if stats?.company_name}
|
||||
<span class="font-semibold text-primary">{stats.company_name}</span>
|
||||
<span class="text-xs bg-muted px-2 py-0.5 rounded-full">Anexos 22/24/30</span>
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<span class="inline-block w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
Resumen operativo — Anexos 22/24/30
|
||||
</span>
|
||||
{:else}
|
||||
Sistema de gestión de comercio exterior
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" class="hidden sm:flex">
|
||||
<Calendar class="mr-2 h-4 w-4" />
|
||||
Hoy: {new Date().toLocaleDateString()}
|
||||
<Button variant="ghost" size="sm" class="hidden sm:flex text-muted-foreground">
|
||||
<Calendar class="mr-1.5 h-3.5 w-3.5" />
|
||||
{new Date().toLocaleDateString('es-MX', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' })}
|
||||
</Button>
|
||||
<Button onclick={loadDashboardData} variant="outline" disabled={loading}>
|
||||
<RefreshCw class={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
<Button onclick={loadDashboardData} variant="outline" size="sm" disabled={loading}>
|
||||
<RefreshCw class={`h-3.5 w-3.5 mr-1.5 ${loading ? 'animate-spin' : ''}`} />
|
||||
{loading ? 'Cargando...' : 'Actualizar'}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -100,133 +109,171 @@
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- KPI skeleton -->
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each Array(6) as _}
|
||||
<div class="flex items-center gap-4 rounded-xl border bg-card px-4 py-3.5 animate-pulse">
|
||||
<div class="h-9 w-9 rounded-lg bg-muted shrink-0"></div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="h-3 w-24 bg-muted rounded-full"></div>
|
||||
<div class="h-5 w-16 bg-muted rounded-md"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Chart skeleton -->
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
{#each Array(2) as _}
|
||||
<Card.Root class="animate-pulse">
|
||||
<Card.Header class="space-y-2">
|
||||
<div class="h-4 w-24 bg-muted rounded"></div>
|
||||
<Card.Header>
|
||||
<div class="h-4 w-40 bg-muted rounded-full"></div>
|
||||
<div class="h-3 w-56 bg-muted rounded-full mt-2"></div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="h-8 w-16 bg-muted rounded"></div>
|
||||
<div class="h-56 bg-muted rounded-lg"></div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if stats}
|
||||
<!-- KPIs Grid -->
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<KpiCard metric={stats.total_invoices} icon={FileText} iconColor="text-blue-600" />
|
||||
<KpiCard metric={stats.total_pedimentos} icon={FileText} iconColor="text-green-600" />
|
||||
<KpiCard metric={stats.total_clients} icon={Users} iconColor="text-purple-600" />
|
||||
<KpiCard metric={stats.total_providers} icon={Users} iconColor="text-orange-600" />
|
||||
<KpiCard metric={stats.active_items} icon={Package} iconColor="text-cyan-600" />
|
||||
<KpiCard metric={stats.pending_approvals} icon={Clock} iconColor="text-yellow-600" />
|
||||
</div>
|
||||
<!--
|
||||
Bento grid:
|
||||
┌──────┬──────┬──────┬──────┬──────┬──────┐ row 1 — KPIs
|
||||
├────────────────────────┬─────────────────┤ row 2
|
||||
│ TrendChart (8) │ Donut (4) │
|
||||
├───────────────┬────────┴─┬───────────────┤ row 3
|
||||
│ Clientes (4) │Proveed(4) │ Accesos (4) │
|
||||
├────────────────────────┬─────────────────┤ row 4
|
||||
│ ActivityFeed (8, ×3) │ Documentos (4) │
|
||||
│ ├─────────────────┤ row 5
|
||||
│ │ Contactos (4) │
|
||||
│ ├─────────────────┤ row 6
|
||||
│ │ Items (4) │
|
||||
└────────────────────────┴─────────────────┘
|
||||
-->
|
||||
<div class="grid grid-cols-2 md:grid-cols-6 lg:grid-cols-12 gap-3">
|
||||
|
||||
<!-- Main Charts -->
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<TrendChart monthlyData={stats.invoices_by_month} />
|
||||
<DonutChart title="Distribución de Operaciones" data={stats.operations_by_type} />
|
||||
</div>
|
||||
|
||||
<!-- Analytics Row -->
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<ChartCard
|
||||
title="Top 5 Clientes"
|
||||
description="Por número de facturas"
|
||||
data={stats.top_clients}
|
||||
type="bar"
|
||||
/>
|
||||
<ChartCard
|
||||
title="Top 5 Proveedores"
|
||||
description="Por número de facturas"
|
||||
data={stats.top_providers}
|
||||
type="bar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Activity Feed & Quick Actions -->
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2">
|
||||
<ActivityFeed activities={stats.recent_activity} />
|
||||
<!-- ── Row 1: KPI tiles ── -->
|
||||
<div class="col-span-1 md:col-span-2 lg:col-span-2">
|
||||
<KpiCard metric={stats.total_invoices} icon={FileText} iconColor="text-blue-600" />
|
||||
</div>
|
||||
<div class="col-span-1 md:col-span-2 lg:col-span-2">
|
||||
<KpiCard metric={stats.total_pedimentos} icon={FileText} iconColor="text-green-600" />
|
||||
</div>
|
||||
<div class="col-span-1 md:col-span-2 lg:col-span-2">
|
||||
<KpiCard metric={stats.total_clients} icon={Users} iconColor="text-purple-600" />
|
||||
</div>
|
||||
<div class="col-span-1 md:col-span-2 lg:col-span-2">
|
||||
<KpiCard metric={stats.total_providers} icon={Users} iconColor="text-orange-600" />
|
||||
</div>
|
||||
<div class="col-span-1 md:col-span-3 lg:col-span-2">
|
||||
<KpiCard metric={stats.active_items} icon={Package} iconColor="text-cyan-600" />
|
||||
</div>
|
||||
<div class="col-span-1 md:col-span-3 lg:col-span-2">
|
||||
<KpiCard metric={stats.pending_approvals} icon={Clock} iconColor="text-yellow-600" />
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Accesos Rápidos</Card.Title>
|
||||
<Card.Description>Funciones más utilizadas</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-2">
|
||||
<a
|
||||
href="/dashboard/invoices"
|
||||
class="flex items-center gap-3 p-3 rounded-lg border hover:bg-accent transition-colors"
|
||||
>
|
||||
<FileText class="h-5 w-5 text-primary" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Facturas</div>
|
||||
<div class="text-xs text-muted-foreground">Gestionar facturas</div>
|
||||
</div>
|
||||
</a>
|
||||
<!-- ── Row 2: TrendChart (8) | Donut (4) — altura natural, sin row-span ── -->
|
||||
<div class="col-span-2 md:col-span-4 lg:col-span-8">
|
||||
<TrendChart monthlyData={stats.invoices_by_month} class="h-full" />
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4">
|
||||
<DonutChart title="Distribución de Operaciones" data={stats.operations_by_type} class="h-full" />
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/dashboard/clients_and_providers"
|
||||
class="flex items-center gap-3 p-3 rounded-lg border hover:bg-accent transition-colors"
|
||||
>
|
||||
<Users class="h-5 w-5 text-primary" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Clientes y Proveedores</div>
|
||||
<div class="text-xs text-muted-foreground">Administrar contactos</div>
|
||||
</div>
|
||||
</a>
|
||||
<!-- ── Row 3: Clientes (4) | Proveedores (4) | Accesos Rápidos (4) ── -->
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4">
|
||||
<ChartCard
|
||||
title="Top 5 Clientes"
|
||||
description="Por número de facturas"
|
||||
data={stats.top_clients}
|
||||
type="bar"
|
||||
class="h-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4">
|
||||
<ChartCard
|
||||
title="Top 5 Proveedores"
|
||||
description="Por número de facturas"
|
||||
data={stats.top_providers}
|
||||
type="bar"
|
||||
class="h-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4">
|
||||
<Card.Root class="h-full overflow-hidden">
|
||||
<Card.Header class="pb-3">
|
||||
<Card.Title class="text-base">Accesos Rápidos</Card.Title>
|
||||
<Card.Description>Módulos más utilizados</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-1.5">
|
||||
<a href="/dashboard/invoices" class="flex items-center gap-3 p-3 rounded-lg border hover:bg-accent hover:border-primary/30 transition-all group">
|
||||
<div class="p-1.5 rounded-md bg-blue-50 dark:bg-blue-950/40 group-hover:bg-blue-100 dark:group-hover:bg-blue-900/40 transition-colors">
|
||||
<FileText class="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Facturas</div>
|
||||
<div class="text-xs text-muted-foreground">Gestionar facturas</div>
|
||||
</div>
|
||||
<span class="text-muted-foreground/40 group-hover:text-primary transition-colors text-lg leading-none">›</span>
|
||||
</a>
|
||||
<a href="/dashboard/clients_and_providers" class="flex items-center gap-3 p-3 rounded-lg border hover:bg-accent hover:border-primary/30 transition-all group">
|
||||
<div class="p-1.5 rounded-md bg-purple-50 dark:bg-purple-950/40 group-hover:bg-purple-100 dark:group-hover:bg-purple-900/40 transition-colors">
|
||||
<Users class="h-4 w-4 text-purple-600" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Clientes y Proveedores</div>
|
||||
<div class="text-xs text-muted-foreground">Administrar contactos</div>
|
||||
</div>
|
||||
<span class="text-muted-foreground/40 group-hover:text-primary transition-colors text-lg leading-none">›</span>
|
||||
</a>
|
||||
<a href="/dashboard/goods" class="flex items-center gap-3 p-3 rounded-lg border hover:bg-accent hover:border-primary/30 transition-all group">
|
||||
<div class="p-1.5 rounded-md bg-cyan-50 dark:bg-cyan-950/40 group-hover:bg-cyan-100 dark:group-hover:bg-cyan-900/40 transition-colors">
|
||||
<Package class="h-4 w-4 text-cyan-600" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Mercancías</div>
|
||||
<div class="text-xs text-muted-foreground">Catálogo de productos</div>
|
||||
</div>
|
||||
<span class="text-muted-foreground/40 group-hover:text-primary transition-colors text-lg leading-none">›</span>
|
||||
</a>
|
||||
<a href="/dashboard/reference_data/code_pedimento_regimens" class="flex items-center gap-3 p-3 rounded-lg border hover:bg-accent hover:border-primary/30 transition-all group">
|
||||
<div class="p-1.5 rounded-md bg-orange-50 dark:bg-orange-950/40 group-hover:bg-orange-100 dark:group-hover:bg-orange-900/40 transition-colors">
|
||||
<BarChart3 class="h-4 w-4 text-orange-600" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Datos de Referencia</div>
|
||||
<div class="text-xs text-muted-foreground">Catálogos del SAT</div>
|
||||
</div>
|
||||
<span class="text-muted-foreground/40 group-hover:text-primary transition-colors text-lg leading-none">›</span>
|
||||
</a>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/dashboard/goods"
|
||||
class="flex items-center gap-3 p-3 rounded-lg border hover:bg-accent transition-colors"
|
||||
>
|
||||
<Package class="h-5 w-5 text-primary" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Mercancías</div>
|
||||
<div class="text-xs text-muted-foreground">Catálogo de productos</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/dashboard/reference_data/code_pedimento_regimens"
|
||||
class="flex items-center gap-3 p-3 rounded-lg border hover:bg-accent transition-colors"
|
||||
>
|
||||
<BarChart3 class="h-5 w-5 text-primary" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium">Datos de Referencia</div>
|
||||
<div class="text-xs text-muted-foreground">Catálogos del SAT</div>
|
||||
</div>
|
||||
</a>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
<!-- Footer Info -->
|
||||
<Card.Root class="bg-muted/50">
|
||||
<Card.Content class="pt-6">
|
||||
<div class="grid gap-4 md:grid-cols-3 text-center">
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-primary">
|
||||
{stats.total_invoices.value + stats.total_pedimentos.value}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Total de Documentos</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-primary">
|
||||
{stats.total_clients.value + stats.total_providers.value}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Total de Contactos</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-primary">{stats.active_items.value}</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Items Activos</div>
|
||||
</div>
|
||||
<!-- ── Rows 4-6: ActivityFeed (8, row-span-3) | 3 stat tiles ── -->
|
||||
<div class="col-span-2 md:col-span-4 lg:col-span-8 lg:row-span-3">
|
||||
<ActivityFeed activities={stats.recent_activity} class="h-full" />
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4 rounded-xl border bg-card p-5 flex flex-col items-center justify-center text-center">
|
||||
<div class="text-3xl font-bold text-foreground tabular-nums">
|
||||
{(stats.total_invoices.value + stats.total_pedimentos.value).toLocaleString()}
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="text-xs text-muted-foreground mt-1.5 font-medium uppercase tracking-wide">Total Documentos</div>
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4 rounded-xl border bg-card p-5 flex flex-col items-center justify-center text-center">
|
||||
<div class="text-3xl font-bold text-foreground tabular-nums">
|
||||
{(stats.total_clients.value + stats.total_providers.value).toLocaleString()}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1.5 font-medium uppercase tracking-wide">Total Contactos</div>
|
||||
</div>
|
||||
<div class="col-span-2 md:col-span-2 lg:col-span-4 rounded-xl border bg-card p-5 flex flex-col items-center justify-center text-center">
|
||||
<div class="text-3xl font-bold text-foreground tabular-nums">
|
||||
{stats.active_items.value.toLocaleString()}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1.5 font-medium uppercase tracking-wide">Items Activos</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
# 6. Relación usuario-tenant en tabla user_tenants
|
||||
# 7. Actualización del tenant_id del usuario con el valor real
|
||||
# 8. Licencia Enterprise para el tenant (ilimitada, 1 año de vigencia)
|
||||
# 9. [OPCIONAL] Datos iniciales de ejemplo si se pasa --seed-data
|
||||
#
|
||||
# Uso:
|
||||
# ./init_first_time.sh # Solo configuración básica
|
||||
# ./init_first_time.sh --seed-data # Configuración + datos de ejemplo
|
||||
#
|
||||
# Requisitos:
|
||||
# - Keycloak corriendo en http://localhost:8080
|
||||
@@ -19,8 +24,6 @@
|
||||
# - Base de datos anexo76_core creada
|
||||
# - jq instalado (para procesamiento JSON)
|
||||
#
|
||||
# Puertos: Keycloak en 18080/19000, frontend en 15173, API en 18000.
|
||||
#
|
||||
# Nota: El atributo tenant_id se crea con valor inicial "1" y luego
|
||||
# se actualiza con el ID real del tenant creado en PostgreSQL.
|
||||
###############################################################################
|
||||
@@ -30,6 +33,22 @@ set -euo pipefail # Modo strict: exit on error, undefined vars, pipe failures
|
||||
# Trap para cleanup en caso de error
|
||||
trap 'echo -e "\n${RED}✗ Error en línea $LINENO. Script abortado.${NC}" >&2' ERR
|
||||
|
||||
# Parsear argumentos
|
||||
SEED_DATA=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--seed-data)
|
||||
SEED_DATA=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Uso: $0 [--seed-data]"
|
||||
echo " --seed-data: Carga datos de ejemplo en las tablas"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Colores para output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
@@ -125,7 +144,265 @@ create_tenant_mapper() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Variables de configuración (puertos con prefijo 1 hardcodeados)
|
||||
###############################################################################
|
||||
# Funciones de seed data
|
||||
###############################################################################
|
||||
|
||||
# Insertar datos de ejemplo para customs_brokers
|
||||
seed_customs_brokers() {
|
||||
echo " → Insertando customs brokers..."
|
||||
exec_pg_sql "
|
||||
INSERT INTO a76.customs_brokers (tenant_id, company_id, type, broker_key, name, address, postal_code, city, state, phone, email, country, tax_id, license, company, contact, created_at, updated_at)
|
||||
VALUES
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'persona', 'CB001', 'Agente Aduanal García', 'Av. Reforma 123', '01000', 'Ciudad de México', 'CDMX', '5555555555', 'garcia@aduanas.com', 'MEX', 'GAAR800101ABC', '1234', 'García y Asociados', 'Juan García', now(), now()),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'persona', 'CB002', 'Agente Aduanal López', 'Blvd. Díaz Ordaz 456', '22000', 'Tijuana', 'BC', '6641234567', 'lopez@customs.com', 'MEX', 'LOPL750505XYZ', '2345', 'López Customs', 'María López', now(), now()),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'persona', 'CB003', 'Agente Aduanal Martínez', 'Calle Industria 789', '45000', 'Guadalajara', 'JAL', '3339876543', 'martinez@broker.com', 'MEX', 'MARM850315DEF', '3456', 'Martínez Brokerage', 'Pedro Martínez', now(), now())
|
||||
ON CONFLICT (broker_key, tenant_id, company_id) DO NOTHING;
|
||||
" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Insertar datos de ejemplo para clients_and_providers
|
||||
seed_clients_and_providers() {
|
||||
echo " → Insertando clientes y proveedores..."
|
||||
exec_pg_sql "
|
||||
INSERT INTO a76.clients_and_providers (tenant_id, company_id, type_nat_foreign, name, short_name, rfc, client_or_provider, web_key, is_active, created_at, updated_at)
|
||||
VALUES
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'N', 'Proveedor Tecnológico SA de CV', 'PROVTECH', 'PTE901201ABC', 'BOTH', 'PROV001', true, now(), now()),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'N', 'Cliente Industrial del Norte SA', 'CINORTE', 'CIN850615XYZ', 'BOTH', 'CLI001', true, now(), now()),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'E', 'Global Supplies Inc', 'GLOBSUP', 'GSI123456789', 'BOTH', 'BOTH001', true, now(), now()),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'N', 'Manufacturas del Bajío SA', 'MANBAJIO', 'MDB920310DEF', 'BOTH', 'CLI002', true, now(), now())
|
||||
RETURNING id;
|
||||
" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Insertar datos de ejemplo para packages
|
||||
seed_packages() {
|
||||
echo " → Insertando tipos de paquete..."
|
||||
exec_pg_sql "
|
||||
INSERT INTO a76.packages (tenant_id, company_id, key, description_es, description_en, weight_unit, plurals, plural_in, code_ace, code_aamex, created_at, updated_at)
|
||||
VALUES
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'PK01', 'Caja de Cartón', 'Cardboard Box', 0.5, 'CAJS', 'BOXS', 'CB01', 'CAJA001', now(), now()),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'PK02', 'Pallet de Madera', 'Wooden Pallet', 15.0, 'PLTS', 'PLTS', 'WP01', 'PALL001', now(), now()),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'PK03', 'Tambor Metálico', 'Metal Drum', 10.0, 'TMBS', 'DRMS', 'MD01', 'TAMB001', now(), now()),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'PK04', 'Contenedor', 'Container', 2000.0, 'CONT', 'CONT', 'CT01', 'CONT001', now(), now())
|
||||
ON CONFLICT (tenant_id, company_id, key) DO NOTHING;
|
||||
" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Insertar datos de ejemplo para classes
|
||||
seed_classes() {
|
||||
echo " → Insertando clases..."
|
||||
exec_pg_sql "INSERT INTO a76.classes (tenant_id, company_id, class_code, description_es, description_en, material_key, unit_of_measure, fraction, us_fraction, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, 'CLS001', 'Componentes Electrónicos', 'Electronic Components', 'MP', 'PZA', '8542.31.01', '8542.31.0000', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'CLS002', 'Partes Automotrices', 'Automotive Parts', 'MP', 'KGS', '8708.29.99', '8708.29.9900', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'CLS003', 'Textiles y Telas', 'Textiles and Fabrics', 'MP', 'MT', '5407.20.01', '5407.20.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'CLS004', 'Equipo de Computación', 'Computer Equipment', 'MP', 'PZA', '8471.30.01', '8471.30.0100', now(), now()) ON CONFLICT (tenant_id, company_id, class_code) DO NOTHING;" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Insertar datos de ejemplo para parts
|
||||
seed_parts() {
|
||||
echo " → Insertando partes/componentes..."
|
||||
|
||||
# Obtener un client_id para asociar las partes
|
||||
local client_id
|
||||
client_id=$(exec_pg_sql "SELECT id FROM a76.clients_and_providers WHERE tenant_id = ${TENANT_ID} AND company_id = ${COMPANY_ID} LIMIT 1;" | xargs)
|
||||
|
||||
if [ -n "$client_id" ]; then
|
||||
exec_pg_sql "INSERT INTO a76.parts (tenant_id, company_id, client_id, part_number, description_spanish, description_english, part_class, currency_key, unit_of_measure, unit_cost, fraction, us_fraction, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-001', 'Microcontrolador ARM Cortex-M4', 'ARM Cortex-M4 Microcontroller', 'CLS001', 'USD', 'PZA', 15.50, '8542.31.01', '8542.31.0000', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-002', 'Filtro de Aceite Automotriz', 'Automotive Oil Filter', 'CLS002', 'USD', 'PZA', 8.75, '8421.23.01', '8421.23.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-003', 'Tela de Algodón para Tapicería', 'Cotton Upholstery Fabric', 'CLS003', 'USD', 'MT', 12.00, '5208.31.01', '5208.31.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-004', 'Disco Duro SSD 500GB', '500GB SSD Hard Drive', 'CLS004', 'USD', 'PZA', 65.00, '8471.70.01', '8471.70.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-005', 'Sensor de Temperatura Digital', 'Digital Temperature Sensor', 'CLS001', 'USD', 'PZA', 5.25, '9025.19.01', '9025.19.0100', now(), now()) ON CONFLICT (tenant_id, company_id, part_number) DO NOTHING;" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
# Insertar datos de ejemplo para pedimentos
|
||||
seed_pedimentos() {
|
||||
echo " → Insertando pedimentos..."
|
||||
|
||||
# Primero obtener IDs de clientes
|
||||
local client_ids
|
||||
client_ids=$(exec_pg_sql "SELECT id FROM a76.clients_and_providers WHERE tenant_id = ${TENANT_ID} AND company_id = ${COMPANY_ID} AND client_or_provider IN ('CLIENT', 'BOTH') LIMIT 2;")
|
||||
local client_id_1=$(echo "$client_ids" | sed -n '1p' | xargs)
|
||||
local client_id_2=$(echo "$client_ids" | sed -n '2p' | xargs)
|
||||
|
||||
if [ -n "$client_id_1" ]; then
|
||||
exec_pg_sql "INSERT INTO a76.pedimentos (tenant_id, company_id, year, customs_office, license, pedimento_number, client_id, operation_type, pedimento_type, pedimento_code, regime, status, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, '24', '47', '3807', '8001234', ${client_id_1}, 'imp', 'normal', 'V1', 'ITE', 'draft', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, '24', '47', '3807', '8001235', ${client_id_1}, 'exp', 'normal', 'V1', 'ETE', 'draft', now(), now()) ON CONFLICT (tenant_id, company_id, year, customs_office, license, pedimento_number) DO NOTHING;" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [ -n "$client_id_2" ]; then
|
||||
exec_pg_sql "INSERT INTO a76.pedimentos (tenant_id, company_id, year, customs_office, license, pedimento_number, client_id, operation_type, pedimento_type, pedimento_code, regime, status, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, '24', '47', '3807', '8001236', ${client_id_2}, 'imp', 'consolidated', 'V1', 'ITE', 'draft', now(), now()) ON CONFLICT (tenant_id, company_id, year, customs_office, license, pedimento_number) DO NOTHING;" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
# Insertar datos de ejemplo para invoices
|
||||
# Genera ~56 facturas distribuidas en los últimos 12 meses para alimentar la gráfica de tendencia
|
||||
seed_invoices() {
|
||||
echo " → Insertando facturas de los últimos 12 meses..."
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 1. invoice_header — una fila por factura con fecha real en cada mes
|
||||
# -------------------------------------------------------------------------
|
||||
exec_pg_sql "INSERT INTO a76.invoice_header
|
||||
(tenant_id, company_id, system, operation_type, invoice_type, invoice_number, invoice_date, is_updated, created_at, updated_at)
|
||||
VALUES
|
||||
-- Abril 2025 (4 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-04-001', '2025-04-03', false, '2025-04-03 08:00:00', '2025-04-03 08:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2025-04-002', '2025-04-11', false, '2025-04-11 10:30:00', '2025-04-11 10:30:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2025-04-003', '2025-04-18', false, '2025-04-18 14:00:00', '2025-04-18 14:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'DEF', 'SEED-2025-04-004', '2025-04-25', false, '2025-04-25 09:15:00', '2025-04-25 09:15:00'),
|
||||
-- Mayo 2025 (5 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-05-001', '2025-05-02', false, '2025-05-02 08:00:00', '2025-05-02 08:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2025-05-002', '2025-05-07', false, '2025-05-07 11:00:00', '2025-05-07 11:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2025-05-003', '2025-05-14', false, '2025-05-14 13:30:00', '2025-05-14 13:30:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-05-004', '2025-05-20', false, '2025-05-20 09:00:00', '2025-05-20 09:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'DEF', 'SEED-2025-05-005', '2025-05-28', false, '2025-05-28 16:00:00', '2025-05-28 16:00:00'),
|
||||
-- Junio 2025 (3 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2025-06-001', '2025-06-05', false, '2025-06-05 08:30:00', '2025-06-05 08:30:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2025-06-002', '2025-06-17', false, '2025-06-17 12:00:00', '2025-06-17 12:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-06-003', '2025-06-27', false, '2025-06-27 10:00:00', '2025-06-27 10:00:00'),
|
||||
-- Julio 2025 (6 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-07-001', '2025-07-02', false, '2025-07-02 08:00:00', '2025-07-02 08:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'DEF', 'SEED-2025-07-002', '2025-07-07', false, '2025-07-07 10:00:00', '2025-07-07 10:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2025-07-003', '2025-07-11', false, '2025-07-11 09:30:00', '2025-07-11 09:30:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2025-07-004', '2025-07-16', false, '2025-07-16 14:00:00', '2025-07-16 14:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-07-005', '2025-07-22', false, '2025-07-22 11:00:00', '2025-07-22 11:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2025-07-006', '2025-07-29', false, '2025-07-29 15:00:00', '2025-07-29 15:00:00'),
|
||||
-- Agosto 2025 (4 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2025-08-001', '2025-08-04', false, '2025-08-04 08:00:00', '2025-08-04 08:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-08-002', '2025-08-12', false, '2025-08-12 10:30:00', '2025-08-12 10:30:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'DEF', 'SEED-2025-08-003', '2025-08-19', false, '2025-08-19 13:00:00', '2025-08-19 13:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2025-08-004', '2025-08-26', false, '2025-08-26 09:00:00', '2025-08-26 09:00:00'),
|
||||
-- Septiembre 2025 (5 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-09-001', '2025-09-02', false, '2025-09-02 08:00:00', '2025-09-02 08:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2025-09-002', '2025-09-09', false, '2025-09-09 11:30:00', '2025-09-09 11:30:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2025-09-003', '2025-09-15', false, '2025-09-15 14:00:00', '2025-09-15 14:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'DEF', 'SEED-2025-09-004', '2025-09-22', false, '2025-09-22 09:30:00', '2025-09-22 09:30:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-09-005', '2025-09-29', false, '2025-09-29 16:00:00', '2025-09-29 16:00:00'),
|
||||
-- Octubre 2025 (7 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-10-001', '2025-10-01', false, '2025-10-01 08:00:00', '2025-10-01 08:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2025-10-002', '2025-10-06', false, '2025-10-06 10:00:00', '2025-10-06 10:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2025-10-003', '2025-10-10', false, '2025-10-10 09:00:00', '2025-10-10 09:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'DEF', 'SEED-2025-10-004', '2025-10-15', false, '2025-10-15 13:30:00', '2025-10-15 13:30:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-10-005', '2025-10-20', false, '2025-10-20 11:00:00', '2025-10-20 11:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2025-10-006', '2025-10-24', false, '2025-10-24 14:30:00', '2025-10-24 14:30:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2025-10-007', '2025-10-29', false, '2025-10-29 08:30:00', '2025-10-29 08:30:00'),
|
||||
-- Noviembre 2025 (4 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-11-001', '2025-11-04', false, '2025-11-04 09:00:00', '2025-11-04 09:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'DEF', 'SEED-2025-11-002', '2025-11-12', false, '2025-11-12 11:00:00', '2025-11-12 11:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2025-11-003', '2025-11-19', false, '2025-11-19 14:00:00', '2025-11-19 14:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2025-11-004', '2025-11-26', false, '2025-11-26 10:00:00', '2025-11-26 10:00:00'),
|
||||
-- Diciembre 2025 (3 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2025-12-001', '2025-12-03', false, '2025-12-03 08:00:00', '2025-12-03 08:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2025-12-002', '2025-12-11', false, '2025-12-11 12:00:00', '2025-12-11 12:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2025-12-003', '2025-12-19', false, '2025-12-19 09:30:00', '2025-12-19 09:30:00'),
|
||||
-- Enero 2026 (5 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2026-01-001', '2026-01-07', false, '2026-01-07 08:00:00', '2026-01-07 08:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'DEF', 'SEED-2026-01-002', '2026-01-13', false, '2026-01-13 10:30:00', '2026-01-13 10:30:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2026-01-003', '2026-01-17', false, '2026-01-17 13:00:00', '2026-01-17 13:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2026-01-004', '2026-01-22', false, '2026-01-22 09:00:00', '2026-01-22 09:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2026-01-005', '2026-01-29', false, '2026-01-29 14:30:00', '2026-01-29 14:30:00'),
|
||||
-- Febrero 2026 (6 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2026-02-001', '2026-02-03', false, '2026-02-03 08:00:00', '2026-02-03 08:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2026-02-002', '2026-02-06', false, '2026-02-06 10:00:00', '2026-02-06 10:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2026-02-003', '2026-02-11', false, '2026-02-11 09:30:00', '2026-02-11 09:30:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'DEF', 'SEED-2026-02-004', '2026-02-17', false, '2026-02-17 13:00:00', '2026-02-17 13:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2026-02-005', '2026-02-21', false, '2026-02-21 11:00:00', '2026-02-21 11:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2026-02-006', '2026-02-26', false, '2026-02-26 15:30:00', '2026-02-26 15:30:00'),
|
||||
-- Marzo 2026 (5 facturas)
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2026-03-001', '2026-03-02', false, '2026-03-02 08:00:00', '2026-03-02 08:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'DEF', 'SEED-2026-03-002', '2026-03-05', false, '2026-03-05 10:00:00', '2026-03-05 10:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'SEED-2026-03-003', '2026-03-06', false, '2026-03-06 09:00:00', '2026-03-06 09:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'SEED-2026-03-004', '2026-03-07', false, '2026-03-07 14:00:00', '2026-03-07 14:00:00'),
|
||||
(${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'SEED-2026-03-005', '2026-03-08', false, '2026-03-08 08:30:00', '2026-03-08 08:30:00')
|
||||
ON CONFLICT DO NOTHING;" >/dev/null 2>&1
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 2. invoice_financials — INSERT ... SELECT desde invoice_header
|
||||
# -------------------------------------------------------------------------
|
||||
exec_pg_sql "INSERT INTO a76.invoice_financials
|
||||
(tenant_id, company_id, invoice_id, currency, exchange_rate, created_at, updated_at)
|
||||
SELECT
|
||||
ih.tenant_id,
|
||||
ih.company_id,
|
||||
ih.id,
|
||||
CASE WHEN ih.operation_type = 'imp' THEN 'foreign' ELSE 'local' END,
|
||||
CASE WHEN ih.operation_type = 'imp' THEN 17.50 ELSE 1.00 END,
|
||||
ih.created_at,
|
||||
ih.updated_at
|
||||
FROM a76.invoice_header ih
|
||||
WHERE ih.tenant_id = ${TENANT_ID}
|
||||
AND ih.company_id = ${COMPANY_ID}
|
||||
AND ih.invoice_number LIKE 'SEED-%'
|
||||
ON CONFLICT DO NOTHING;" >/dev/null 2>&1
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 3. invoice_compliance_mx — asignar proveedores rotando entre los disponibles
|
||||
# -------------------------------------------------------------------------
|
||||
exec_pg_sql "INSERT INTO a76.invoice_compliance_mx
|
||||
(tenant_id, company_id, invoice_id, provider_id, created_at, updated_at)
|
||||
SELECT
|
||||
ih.tenant_id,
|
||||
ih.company_id,
|
||||
ih.id,
|
||||
cp.id,
|
||||
ih.created_at,
|
||||
ih.updated_at
|
||||
FROM a76.invoice_header ih
|
||||
JOIN LATERAL (
|
||||
SELECT id FROM a76.clients_and_providers
|
||||
WHERE tenant_id = ${TENANT_ID}
|
||||
AND company_id = ${COMPANY_ID}
|
||||
AND client_or_provider IN ('PROVIDER', 'BOTH')
|
||||
ORDER BY id
|
||||
OFFSET (ROW_NUMBER() OVER (ORDER BY ih.id) - 1) % (
|
||||
SELECT COUNT(*) FROM a76.clients_and_providers
|
||||
WHERE tenant_id = ${TENANT_ID} AND company_id = ${COMPANY_ID}
|
||||
AND client_or_provider IN ('PROVIDER', 'BOTH')
|
||||
)
|
||||
LIMIT 1
|
||||
) cp ON true
|
||||
WHERE ih.tenant_id = ${TENANT_ID}
|
||||
AND ih.company_id = ${COMPANY_ID}
|
||||
AND ih.invoice_number LIKE 'SEED-%'
|
||||
ON CONFLICT DO NOTHING;" >/dev/null 2>&1
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 4. invoice_logistics — rotar incoterms y transport_type
|
||||
# -------------------------------------------------------------------------
|
||||
exec_pg_sql "INSERT INTO a76.invoice_logistics
|
||||
(tenant_id, company_id, invoice_id, transport_type, weight_type, incoterm, created_at, updated_at)
|
||||
SELECT
|
||||
ih.tenant_id,
|
||||
ih.company_id,
|
||||
ih.id,
|
||||
'none',
|
||||
'kgs',
|
||||
(ARRAY['FOB','CIF','EXW','DDP','DAP'])[((ROW_NUMBER() OVER (ORDER BY ih.id) - 1) % 5) + 1],
|
||||
ih.created_at,
|
||||
ih.updated_at
|
||||
FROM a76.invoice_header ih
|
||||
WHERE ih.tenant_id = ${TENANT_ID}
|
||||
AND ih.company_id = ${COMPANY_ID}
|
||||
AND ih.invoice_number LIKE 'SEED-%'
|
||||
ON CONFLICT DO NOTHING;" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Ejecutar todas las funciones de seed
|
||||
execute_seed_data() {
|
||||
echo -e "\n${YELLOW}[SEED] Cargando datos de ejemplo...${NC}"
|
||||
|
||||
seed_customs_brokers
|
||||
seed_clients_and_providers
|
||||
seed_packages
|
||||
seed_classes
|
||||
seed_parts
|
||||
seed_pedimentos
|
||||
seed_invoices
|
||||
|
||||
echo -e "${GREEN}✓ Datos de ejemplo cargados exitosamente${NC}"
|
||||
echo -e "${YELLOW} • 3 Agentes aduanales${NC}"
|
||||
echo -e "${YELLOW} • 4 Clientes/Proveedores${NC}"
|
||||
echo -e "${YELLOW} • 4 Tipos de paquete${NC}"
|
||||
echo -e "${YELLOW} • 4 Clases${NC}"
|
||||
echo -e "${YELLOW} • 5 Parts/Componentes${NC}"
|
||||
echo -e "${YELLOW} • 3 Pedimentos${NC}"
|
||||
echo -e "${YELLOW} • 56 Facturas (12 meses: Abr 2025 → Mar 2026)${NC}"
|
||||
}
|
||||
|
||||
# Variables de configuración
|
||||
KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8080/kcauth}"
|
||||
KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN:-admin}"
|
||||
KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD:-admin}"
|
||||
@@ -145,7 +422,7 @@ DEMO_EMAIL="demo@aduanasoft.com"
|
||||
DEMO_FIRSTNAME="Demo"
|
||||
DEMO_LASTNAME="User"
|
||||
|
||||
TENANT_NAME="Aduanasoft"A
|
||||
TENANT_NAME="Aduanasoft"
|
||||
TENANT_SLUG="aduanasoft"
|
||||
COMPANY_NAME="Aduanasoft S.A. de C.V."
|
||||
COMPANY_RFC="ADS010101AAA"
|
||||
@@ -735,6 +1012,13 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
# 9. Cargar datos de ejemplo (opcional)
|
||||
###############################################################################
|
||||
if [ "$SEED_DATA" = true ]; then
|
||||
execute_seed_data
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
# Resumen final
|
||||
###############################################################################
|
||||
@@ -773,6 +1057,18 @@ echo -e " ${GREEN}✓${NC} Plan: Enterprise (ilimitado)"
|
||||
echo -e " ${GREEN}✓${NC} Status: Activa"
|
||||
echo -e " ${GREEN}✓${NC} Features: API, Reportes Avanzados, Integraciones, Soporte Dedicado"
|
||||
echo -e " ${GREEN}✓${NC} Vigencia: 1 año"
|
||||
|
||||
if [ "$SEED_DATA" = true ]; then
|
||||
echo -e "\n${YELLOW}Datos de ejemplo:${NC}"
|
||||
echo -e " ${GREEN}✓${NC} Agentes aduanales: 3"
|
||||
echo -e " ${GREEN}✓${NC} Clientes/Proveedores: 4"
|
||||
echo -e " ${GREEN}✓${NC} Tipos de paquete: 4"
|
||||
echo -e " ${GREEN}✓${NC} Clases: 4"
|
||||
echo -e " ${GREEN}✓${NC} Parts/Componentes: 5"
|
||||
echo -e " ${GREEN}✓${NC} Pedimentos: 3"
|
||||
echo -e " ${GREEN}✓${NC} Facturas: 3"
|
||||
fi
|
||||
|
||||
echo -e "\n${YELLOW}Puedes acceder al sistema en:${NC}"
|
||||
echo -e " ${GREEN}http://localhost:5173${NC}"
|
||||
echo -e "\n${GREEN}════════════════════════════════════════════════════════${NC}\n"
|
||||
|
||||
Reference in New Issue
Block a user