feature/frontend-integration
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
366
frontend/src/lib/config/csv-upload.ts
Normal file
366
frontend/src/lib/config/csv-upload.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
107
frontend/src/routes/dashboard/csv-upload/+page.svelte
Normal file
107
frontend/src/routes/dashboard/csv-upload/+page.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user