feature/frontend-integration

This commit is contained in:
hreyes
2026-02-12 12:30:41 -06:00
parent 9d27066da1
commit eab10e6fbc
23 changed files with 1005 additions and 2 deletions

View File

@@ -0,0 +1,96 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
import { Settings2 } from 'lucide-svelte';
import { tabSettings } from '$lib/config/csv-upload';
let {
activeTab,
settings = $bindable()
}: {
activeTab: string;
settings: Record<string, any>;
} = $props();
let currentFields = $derived(tabSettings[activeTab] || []);
// Determine grid columns based on number of fields
let gridClass = $derived(
currentFields.length > 4
? 'grid-cols-4'
: currentFields.length > 2
? 'grid-cols-3'
: currentFields.length > 1
? 'grid-cols-2'
: 'grid-cols-1'
);
</script>
<!--
No positioning here. Parent layout controls placement.
Just styling the "Island".
-->
<div
class="border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 p-4 rounded-xl shadow-2xl mx-4 mb-4"
>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2 text-muted-foreground border-b pb-2">
<Settings2 class="h-4 w-4" />
<span class="text-xs font-semibold uppercase tracking-wider">Configuración: {activeTab}</span>
</div>
{#if currentFields.length > 0}
<div class="grid {gridClass} gap-6">
{#each currentFields as field}
<div class="flex flex-col gap-2">
{#if field.type !== 'boolean'}
<Label class="text-xs font-medium text-muted-foreground uppercase"
>{field.label}</Label
>
{/if}
{#if field.type === 'text'}
<Input type="text" bind:value={settings[field.name]} class="h-8" />
{:else if field.type === 'boolean'}
<div class="flex items-center space-x-2 h-8">
<Checkbox id={field.name} bind:checked={settings[field.name]} />
<Label
for={field.name}
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>{field.label}</Label
>
</div>
{:else if field.type === 'select' && field.options}
<select
class="flex h-8 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
bind:value={settings[field.name]}
>
{#each field.options as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
{:else if field.type === 'radio' && field.options}
<RadioGroup.Root
bind:value={settings[field.name]}
class="flex gap-4 h-8 items-center"
>
{#each field.options as opt}
<div class="flex items-center space-x-2">
<RadioGroup.Item value={opt.value} id={`${field.name}-${opt.value}`} />
<Label for={`${field.name}-${opt.value}`}>{opt.label}</Label>
</div>
{/each}
</RadioGroup.Root>
{/if}
</div>
{/each}
</div>
{:else}
<div class="flex items-center justify-center h-8 text-sm text-muted-foreground italic">
No hay configuraciones específicas para este módulo.
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,123 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { Upload, FileType } from 'lucide-svelte';
import type { CsvUploadItem } from '$lib/config/csv-upload';
let {
open = $bindable(false),
config,
currentSettings // Read-only prop passed from page
}: {
open: boolean;
config: CsvUploadItem;
currentSettings: Record<string, any>;
} = $props();
// State
let file: File | null = $state(null);
let isProcessing = $state(false);
function handleFileChange(e: Event) {
const target = e.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
file = target.files[0];
}
}
function handleProcess() {
isProcessing = true;
console.log('Processing Upload Request:', {
entityConfig: config,
file: file,
activeSettings: currentSettings // Log the global/tab settings being applied
});
// Mock processing time
setTimeout(() => {
isProcessing = false;
open = false;
alert(
`Procesando archivo para: ${config.title}\nConfiguración aplicada: ${JSON.stringify(currentSettings, null, 2)}`
);
}, 1000);
}
// Reset file state when config changes or modal opens
$effect(() => {
if (config) {
file = null;
}
});
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[500px]">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
{#if config.icon}
<config.icon class="h-5 w-5" />
{/if}
Importar {config.title}
</Dialog.Title>
<Dialog.Description>
Selecciona tu archivo CSV para procesar.
<br />
<span class="text-xs text-muted-foreground mt-1 block">
La configuración activa del pie de página se aplicará a esta carga.
</span>
</Dialog.Description>
</Dialog.Header>
<div class="grid gap-6 py-4">
<!-- Stage 1: File Selection -->
<div class="flex flex-col gap-3">
<div
class="border-2 border-dashed rounded-lg p-8 flex flex-col items-center justify-center gap-2 hover:bg-muted/50 transition-colors cursor-pointer relative"
>
<input
type="file"
accept=".csv,.txt"
class="absolute inset-0 opacity-0 cursor-pointer"
onchange={handleFileChange}
/>
{#if file}
<FileType class="h-12 w-12 text-primary" />
<div class="text-center">
<span class="font-medium text-sm block">{file.name}</span>
<span class="text-xs text-muted-foreground">{(file.size / 1024).toFixed(2)} KB</span>
</div>
{:else}
<Upload class="h-10 w-10 text-muted-foreground" />
<span class="font-medium text-sm">Arrastra tu archivo aquí o haz clic</span>
<span class="text-xs text-muted-foreground">Soporta CSV, TXT</span>
{/if}
</div>
</div>
<!-- Preview of Current Settings (Read Only) -->
{#if Object.keys(currentSettings).length > 0}
<div class="bg-muted/40 p-3 rounded text-xs text-muted-foreground">
<strong>Configuración Activa:</strong>
<ul class="list-disc pl-4 mt-1 space-y-0.5">
{#each Object.entries(currentSettings) as [key, value]}
<li>{key}: <span class="font-mono">{value}</span></li>
{/each}
</ul>
</div>
{/if}
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
<Button onclick={handleProcess} disabled={!file || isProcessing}>
{#if isProcessing}
Procesando...
{:else}
Procesar
{/if}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,264 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card/index.js';
import type { CsvUploadItem } from '$lib/config/csv-upload';
import { UploadCloud, Lock } from 'lucide-svelte';
import { cn } from '$lib/utils';
import { toast } from 'svelte-sonner';
let {
items,
onUpload
}: {
items: CsvUploadItem[];
onUpload: (file: File, config: CsvUploadItem) => void;
} = $props();
let dragOverId = $state<string | null>(null);
// Group items
let groupedItems = $derived.by(() => {
const groups: Record<string, CsvUploadItem[]> = {};
const ungrouped: CsvUploadItem[] = [];
items.forEach((item) => {
if (item.group) {
if (!groups[item.group]) groups[item.group] = [];
groups[item.group].push(item);
} else {
ungrouped.push(item);
}
});
return { groups, ungrouped };
});
function handleDragEnter(e: DragEvent, id: string, disabled?: boolean) {
if (disabled) return;
e.preventDefault();
e.stopPropagation();
dragOverId = id;
}
function handleDragLeave(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
dragOverId = null;
}
function handleDragOver(e: DragEvent, disabled?: boolean) {
if (disabled) return;
e.preventDefault();
e.stopPropagation();
dragOverId = null;
}
function validateAndUpload(file: File, item: CsvUploadItem) {
const isValidExtension = file.name.toLowerCase().endsWith('.csv');
if (!isValidExtension) {
toast.error('Formato inválido. Solo se permiten archivos .csv');
return;
}
onUpload(file, item);
}
function handleDrop(e: DragEvent, item: CsvUploadItem) {
if (item.disabled) return;
e.preventDefault();
e.stopPropagation();
dragOverId = null;
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
validateAndUpload(e.dataTransfer.files[0], item);
}
}
function handleClick(id: string, disabled?: boolean) {
if (disabled) return;
const input = document.getElementById(`file-input-${id}`) as HTMLInputElement;
if (input) input.click();
}
function handleFileChange(e: Event, item: CsvUploadItem) {
const target = e.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
validateAndUpload(target.files[0], item);
target.value = '';
}
}
function handleContextMenu(e: MouseEvent, item: CsvUploadItem) {
if (item.disabled) {
e.preventDefault();
return;
}
if (!item.templateUrl) return;
e.preventDefault();
const link = document.createElement('a');
link.href = item.templateUrl;
link.download = item.templateUrl.split('/').pop() || 'plantilla.xls';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.info(`Descargando plantilla para ${item.title}...`);
}
</script>
<div class="flex flex-col gap-6 select-none">
{#if groupedItems.ungrouped.length > 0}
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{#each groupedItems.ungrouped as item}
<div
class={cn(
'relative group transition-all duration-200 ease-in-out transform',
item.disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
!item.disabled && dragOverId === item.id ? 'scale-105' : ''
)}
ondragenter={(e) => handleDragEnter(e, item.id, item.disabled)}
ondragleave={handleDragLeave}
ondragover={(e) => handleDragOver(e, item.disabled)}
ondrop={(e) => handleDrop(e, item)}
oncontextmenu={(e) => handleContextMenu(e, item)}
roles="button"
tabindex={item.disabled ? -1 : 0}
onclick={() => handleClick(item.id, item.disabled)}
onkeydown={(e) => !item.disabled && e.key === 'Enter' && handleClick(item.id)}
>
<input
type="file"
id={`file-input-${item.id}`}
class="hidden"
accept=".csv"
onchange={(e) => handleFileChange(e, item)}
disabled={item.disabled}
/>
<Card.Root
class={cn(
'h-full border-2 border-dashed border-transparent transition-colors w-full text-left relative overflow-hidden',
!item.disabled && 'hover:border-primary/50 hover:shadow-md',
!item.disabled && dragOverId === item.id
? 'border-primary bg-primary/5 shadow-xl ring-2 ring-primary ring-offset-2'
: ''
)}
>
{#if item.disabled}
<div class="absolute inset-0 bg-background/50 z-20 flex items-center justify-center">
<span
class="bg-muted px-2 py-1 rounded text-xs font-semibold text-muted-foreground border flex items-center gap-1"
>
<Lock class="h-3 w-3" /> Próximamente
</span>
</div>
{/if}
<Card.Content
class="flex flex-col items-center justify-center p-6 gap-3 text-center h-full relative z-10"
>
{#if !item.disabled && dragOverId === item.id}
<div class="animate-bounce">
<UploadCloud class="h-8 w-8 text-primary" />
</div>
<span class="text-sm font-semibold text-primary">¡Suelta el archivo!</span>
{:else}
<div
class={cn(
'p-3 bg-muted rounded-full transition-transform duration-200',
!item.disabled && 'group-hover:scale-110'
)}
>
<item.icon class="h-6 w-6 text-primary" />
</div>
<div class="font-medium text-sm text-balance">{item.title}</div>
{/if}
</Card.Content>
</Card.Root>
</div>
{/each}
</div>
{/if}
{#each Object.entries(groupedItems.groups) as [groupName, groupItems]}
<div class="flex flex-col gap-3">
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider pl-1">
{groupName}
</h3>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{#each groupItems as item}
<div
class={cn(
'relative group transition-all duration-200 ease-in-out transform',
item.disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
!item.disabled && dragOverId === item.id ? 'scale-105' : ''
)}
ondragenter={(e) => handleDragEnter(e, item.id, item.disabled)}
ondragleave={handleDragLeave}
ondragover={(e) => handleDragOver(e, item.disabled)}
ondrop={(e) => handleDrop(e, item)}
oncontextmenu={(e) => handleContextMenu(e, item)}
role="button"
tabindex={item.disabled ? -1 : 0}
onclick={() => handleClick(item.id, item.disabled)}
onkeydown={(e) => !item.disabled && e.key === 'Enter' && handleClick(item.id)}
>
<input
type="file"
id={`file-input-${item.id}`}
class="hidden"
accept=".csv"
onchange={(e) => handleFileChange(e, item)}
disabled={item.disabled}
/>
<Card.Root
class={cn(
'h-full border-2 border-dashed border-transparent transition-colors w-full text-left relative overflow-hidden',
!item.disabled && 'hover:border-primary/50 hover:shadow-md',
!item.disabled && dragOverId === item.id
? 'border-primary bg-primary/5 shadow-xl ring-2 ring-primary ring-offset-2'
: ''
)}
>
{#if item.disabled}
<div
class="absolute inset-0 bg-background/50 z-20 flex items-center justify-center"
>
<span
class="bg-muted px-2 py-1 rounded text-xs font-semibold text-muted-foreground border flex items-center gap-1"
>
<Lock class="h-3 w-3" /> Próximamente
</span>
</div>
{/if}
<Card.Content
class="flex flex-col items-center justify-center p-6 gap-3 text-center h-full relative z-10"
>
{#if !item.disabled && dragOverId === item.id}
<div class="animate-bounce">
<UploadCloud class="h-8 w-8 text-primary" />
</div>
<span class="text-sm font-semibold text-primary">¡Suelta el archivo!</span>
{:else}
<div
class={cn(
'p-3 bg-muted rounded-full transition-transform duration-200',
!item.disabled && 'group-hover:scale-110'
)}
>
<item.icon class="h-6 w-6 text-primary" />
</div>
<div class="font-medium text-sm text-balance">{item.title}</div>
{/if}
</Card.Content>
</Card.Root>
</div>
{/each}
</div>
</div>
{/each}
</div>

View File

@@ -14,6 +14,7 @@ import {
Shield,
Users,
Ship,
MoreHorizontal,
} from 'lucide-svelte';
import * as m from "$lib/paraglide/messages.js";
import { Title } from '../ui/alert';
@@ -423,6 +424,7 @@ export function getSidebarData(): SidebarData {
},
],
},
{
title: m["sidebar.clients_and_providers"](),
url: "/dashboard/clients_and_providers",

View File

@@ -1,4 +1,5 @@
<script lang="ts">
<script lang="ts">
import { tick } from "svelte";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import { useSidebar } from "$lib/components/ui/sidebar/context.svelte.js";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
@@ -20,6 +21,17 @@
} = $props();
const sidebar = useSidebar();
let open = $state(false);
let position = $state({ x: 0, y: 0 });
async function handleMoreClick(e: MouseEvent) {
e.preventDefault();
open = false;
position = { x: e.clientX, y: e.clientY };
await tick();
open = true;
}
</script>
<Sidebar.Group class="group-data-[collapsible=icon]:hidden">
@@ -66,11 +78,28 @@
</DropdownMenu.Root>
</Sidebar.MenuItem>
{/each}
<Sidebar.MenuItem>
<Sidebar.MenuButton class="text-sidebar-foreground/70">
<Sidebar.MenuButton class="text-sidebar-foreground/70" onclick={handleMoreClick}>
<EllipsisIcon class="text-sidebar-foreground/70" />
<span>More</span>
</Sidebar.MenuButton>
<DropdownMenu.Root bind:open>
<DropdownMenu.Trigger class="fixed z-50 size-0" style="top: {position.y}px; left: {position.x}px" />
<DropdownMenu.Content
class="w-48 rounded-lg"
side="right"
align="start"
>
<DropdownMenu.Item>
<a href="/dashboard/csv-upload" class="flex items-center gap-2 w-full">
<FolderIcon class="size-4 text-muted-foreground" />
<span>Carga CSV</span>
</a>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>

View File

@@ -0,0 +1,366 @@
import {
User,
Users,
FileText,
Truck,
Container,
Ship,
Plane,
Package,
Briefcase,
Globe,
CreditCard,
DollarSign,
Calendar,
Hash,
MapPin,
ShieldCheck,
FileDigit,
Scale,
} from 'lucide-svelte';
// --- Interfaces ---
export interface CsvUploadItem {
id: string;
title: string;
icon: any;
group?: string; // For grouping within a tab
modelTarget?: string; // The backend model this maps to
description?: string;
templateUrl?: string; // Path to the template file in static/
disabled?: boolean; // New property to mark items as "Coming Soon"
}
export interface CsvUploadField {
name: string;
label: string;
type: 'text' | 'select' | 'boolean' | 'date' | 'radio';
options?: { label: string; value: string | boolean | number }[];
required?: boolean;
defaultValue?: any;
}
// Map of Tab ID -> Array of Fields
export const tabSettings: Record<string, CsvUploadField[]> = {
catalogos: [
{
name: 'mode',
label: 'Modo de Carga',
type: 'radio',
options: [
{ label: 'Actualizar', value: 'update' },
{ label: 'Reemplazar', value: 'replace' }
],
defaultValue: 'update'
}
],
transportes: [
{
name: 'mode',
label: 'Modo de Carga',
type: 'radio',
options: [
{ label: 'Actualizar', value: 'update' },
{ label: 'Reemplazar', value: 'replace' }
],
defaultValue: 'update'
}
],
importacion: [
{
name: 'autonumber_remesas',
label: 'Autonumerar Remesas',
type: 'boolean',
defaultValue: false
},
{
name: 'recalculate_dates',
label: 'Recalcular Fechas',
type: 'boolean',
defaultValue: false
},
{
name: 'dateFormat',
label: 'Formato de Fecha',
type: 'select',
options: [
{ label: 'DD/MM/YYYY', value: 'dd/mm/yyyy' },
{ label: 'MM/DD/YYYY', value: 'mm/dd/yyyy' },
{ label: 'YYYY-MM-DD', value: 'yyyy-mm-dd' }
],
defaultValue: 'dd/mm/yyyy'
}
],
exportacion: [
{
name: 'invoice_type',
label: 'Tipo de Factura',
type: 'select',
options: [
{ label: 'AFIJO', value: 'AFIJO' },
{ label: 'NORMAL', value: 'NORMAL' },
],
defaultValue: 'AFIJO',
},
{
name: 'is_regime_change',
label: 'Es Cambio de Régimen',
type: 'boolean',
defaultValue: false,
},
{
name: 'dateFormat',
label: 'Formato de Fecha',
type: 'select',
options: [
{ label: 'DD/MM/YYYY', value: 'dd/mm/yyyy' },
{ label: 'MM/DD/YYYY', value: 'mm/dd/yyyy' },
{ label: 'YYYY-MM-DD', value: 'yyyy-mm-dd' }
],
defaultValue: 'dd/mm/yyyy'
}
]
};
// --- DATA DEFINITIONS (Items only, no config) ---
export const catalogosConfig: CsvUploadItem[] = [
{
id: 'customs_brokers',
title: 'Agentes Aduanales',
icon: User,
modelTarget: 'CustomsBroker',
templateUrl: '/csv/EstructuraCatAgenteAduanal.xls'
},
{
id: 'clients_providers',
title: 'Clientes y Proveedores',
icon: Users,
modelTarget: 'ClientProvider',
templateUrl: '/csv/EstructuraCatClienteProv.xls'
},
{
id: 'exchange_rates',
title: 'Tipo de Cambios',
icon: DollarSign,
modelTarget: 'ExchangeRate',
templateUrl: '/csv/EstructuraCatTiposCambio.xls'
},
{
id: 'american_fractions',
title: 'Fracc. Ame.',
icon: Globe,
modelTarget: 'AmericanFraction',
templateUrl: '/csv/EstructuraCatFraccAme.xls'
},
{
id: 'material_classes',
title: 'Clases de Materiales',
icon: Package,
modelTarget: 'MaterialClass',
templateUrl: '/csv/EstructuraCatClasesAF.xls'
},
{
id: 'items',
title: 'Partidas (Permisos)',
icon: FileText,
group: 'Permisos',
modelTarget: 'ItemPermission',
templateUrl: '/csv/EstructuraCatPartesAF.xls'
},
{
id: 'headers',
title: 'Encabezados (Permisos)',
icon: FileText,
group: 'Permisos',
modelTarget: 'HeaderPermission',
disabled: true,
},
{
id: 'historical_fractions',
title: 'Fracciones Históricas',
icon: Calendar,
modelTarget: 'HistoricalFraction',
disabled: true,
},
{
id: 'pedimentos',
title: 'Pedimentos',
icon: FileDigit,
modelTarget: 'Pedimento',
templateUrl: '/csv/EstructuraCatPedimentos.xls'
},
];
export const transportesConfig: CsvUploadItem[] = [
{
id: 'transports',
title: 'Transportes',
icon: Truck,
modelTarget: 'Transport',
templateUrl: '/csv/EstructuraCatTransportes.xls'
},
{
id: 'drivers',
title: 'Conductores',
icon: User,
modelTarget: 'Driver',
templateUrl: '/csv/EstructuraCatConductor.xls'
},
{
id: 'trailers',
title: 'Trailers y Cajas',
icon: Container,
modelTarget: 'Trailer',
templateUrl: '/csv/EstructuraCatTrailers.xls'
},
];
export const importacionConfig: CsvUploadItem[] = [
// Impo Temp
{
id: 'imp_temp_header',
title: 'Encabezado',
icon: FileText,
group: 'Impo. Temp.',
modelTarget: 'InvoiceHeader',
templateUrl: '/csv/EstructuraEncFacImpoTemp.xls'
},
{
id: 'imp_temp_details',
title: 'Partidas',
icon: Package,
group: 'Impo. Temp.',
modelTarget: 'InvoiceSalesDetails',
templateUrl: '/csv/EstructuraParFacImpoTempAF.xls'
},
{
id: 'imp_temp_series',
title: 'Series',
icon: Hash,
group: 'Impo. Temp.',
modelTarget: 'InvoiceSeries',
disabled: true,
},
// Impo Def
{
id: 'imp_def_header',
title: 'Encabezado',
icon: FileText,
group: 'Impo. Def.',
modelTarget: 'InvoiceHeader',
templateUrl: '/csv/EstructuraEncFacImpoDef.xls'
},
{
id: 'imp_def_details',
title: 'Partidas',
icon: Package,
group: 'Impo. Def.',
modelTarget: 'InvoiceSalesDetails',
templateUrl: '/csv/EstructuraParFacImpoDefAF.xls'
},
{
id: 'imp_def_series',
title: 'Series',
icon: Hash,
group: 'Impo. Def.',
modelTarget: 'InvoiceSeries',
disabled: true,
},
// Compras Mex
{
id: 'comp_mex_header',
title: 'Encabezado',
icon: FileText,
group: 'Compras Mex.',
modelTarget: 'InvoiceHeader',
disabled: true,
},
{
id: 'comp_mex_details',
title: 'Partidas',
icon: Package,
group: 'Compras Mex.',
modelTarget: 'InvoiceSalesDetails',
disabled: true,
},
{
id: 'comp_mex_series',
title: 'Series',
icon: Hash,
group: 'Compras Mex.',
modelTarget: 'InvoiceSeries',
disabled: true,
},
];
export const exportacionConfig: CsvUploadItem[] = [
// Expo Def / Cam. Reg.
{
id: 'exp_def_header',
title: 'Encabezado',
icon: FileText,
group: 'Expo. Def./Cam. Reg.',
modelTarget: 'InvoiceHeader',
templateUrl: '/csv/EstructuraEncFacExpoCamReg.xls'
},
{
id: 'exp_def_details',
title: 'Partidas',
icon: Package,
group: 'Expo. Def./Cam. Reg.',
modelTarget: 'InvoiceSalesDetails',
templateUrl: '/csv/EstructuraParExpoCamReg.xls'
},
{
id: 'exp_def_series',
title: 'Series',
icon: Hash,
group: 'Expo. Def./Cam. Reg.',
modelTarget: 'InvoiceSeries',
disabled: true,
},
{
id: 'exp_def_nodes',
title: 'NODES',
icon: Briefcase,
group: 'Expo. Def./Cam. Reg.',
modelTarget: 'Nodes',
disabled: true,
},
// Expo Rep
{
id: 'exp_rep_header',
title: 'Encabezado',
icon: FileText,
group: 'Expo. Rep.',
modelTarget: 'InvoiceHeader',
disabled: true,
},
{
id: 'exp_rep_details',
title: 'Partidas',
icon: Package,
group: 'Expo. Rep.',
modelTarget: 'InvoiceSalesDetails',
disabled: true,
},
{
id: 'exp_rep_series',
title: 'Series',
icon: Hash,
group: 'Expo. Rep.',
modelTarget: 'InvoiceSeries',
disabled: true,
},
// Manifiesto
{
id: 'manifest_header',
title: 'Encabezado',
icon: FileText,
group: 'Manifiesto',
modelTarget: 'Manifest',
disabled: true,
},
];

View File

@@ -0,0 +1,107 @@
<script lang="ts">
import * as Tabs from '$lib/components/ui/tabs/index.js';
import UploadLauncherGrid from '$lib/components/dashboard/csv-upload/UploadLauncherGrid.svelte';
import ConfigFooter from '$lib/components/dashboard/csv-upload/ConfigFooter.svelte';
import {
catalogosConfig,
transportesConfig,
importacionConfig,
exportacionConfig,
tabSettings,
type CsvUploadItem
} from '$lib/config/csv-upload';
import { toast } from 'svelte-sonner';
// We no longer need modal state
let activeTab = $state('catalogos');
let allSettings = $state<Record<string, any>>({});
$effect(() => {
const fields = tabSettings[activeTab] || [];
if (!allSettings[activeTab]) {
allSettings[activeTab] = {};
fields.forEach((f) => {
allSettings[activeTab][f.name] = f.defaultValue;
});
}
});
function handleUpload(file: File, config: CsvUploadItem) {
const currentSettings = allSettings[activeTab] || {};
console.log('🚀 Starting Direct Upload Processing', {
file: file.name,
size: file.size,
target: config.modelTarget,
config: config.title,
settings: currentSettings
});
// Mock Processing Feedback
const promise = new Promise((resolve) => setTimeout(resolve, 2000));
toast.promise(promise, {
loading: `Procesando ${file.name} para ${config.title}...`,
success: `Archivo cargado correctamente con configuración: ${JSON.stringify(currentSettings)}`,
error: 'Error al cargar el archivo'
});
}
</script>
<div class="flex flex-col h-[calc(100vh-4rem)] -m-4 overflow-hidden">
<!-- Scrollable Content Area -->
<div class="flex-1 overflow-y-auto p-4 md:p-8 space-y-4">
<div class="flex items-center gap-4">
<h1 class="text-lg font-semibold md:text-2xl">Importación Masiva de Datos (CSV)</h1>
</div>
<Tabs.Root bind:value={activeTab} class="w-full">
<Tabs.List class="grid w-full grid-cols-2 md:grid-cols-4 lg:w-auto">
<Tabs.Trigger value="catalogos">Catálogos</Tabs.Trigger>
<Tabs.Trigger value="transportes">Transportes</Tabs.Trigger>
<Tabs.Trigger value="importacion">Importación</Tabs.Trigger>
<Tabs.Trigger value="exportacion">Exportación</Tabs.Trigger>
</Tabs.List>
<div class="mt-6">
<Tabs.Content value="catalogos" class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-lg font-medium tracking-tight">Catálogos Generales</h2>
</div>
<UploadLauncherGrid items={catalogosConfig} onUpload={handleUpload} />
</Tabs.Content>
<Tabs.Content value="transportes" class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-lg font-medium tracking-tight">Logística y Transporte</h2>
</div>
<UploadLauncherGrid items={transportesConfig} onUpload={handleUpload} />
</Tabs.Content>
<Tabs.Content value="importacion" class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-lg font-medium tracking-tight">Operaciones de Importación</h2>
</div>
<UploadLauncherGrid items={importacionConfig} onUpload={handleUpload} />
</Tabs.Content>
<Tabs.Content value="exportacion" class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-lg font-medium tracking-tight">Operaciones de Exportación</h2>
</div>
<UploadLauncherGrid items={exportacionConfig} onUpload={handleUpload} />
</Tabs.Content>
</div>
</Tabs.Root>
<div class="h-4"></div>
</div>
<!-- Fixed Footer Area -->
{#if allSettings[activeTab]}
<div class="flex-none z-20">
<ConfigFooter {activeTab} bind:settings={allSettings[activeTab]} />
</div>
{/if}
</div>