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>
|
||||
|
||||
Reference in New Issue
Block a user