265 lines
8.3 KiB
Svelte
265 lines
8.3 KiB
Svelte
<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>
|