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

@@ -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",

View File

@@ -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:

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;
// SVG donut
const R = 72;
const SW = 22;
const CIRC = 2 * Math.PI * R;
const GAP = 0.018 * CIRC;
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
};
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}

View File

@@ -64,25 +64,34 @@
{#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 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">&rsaquo;</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">&rsaquo;</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">&rsaquo;</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">&rsaquo;</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>

View File

@@ -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"