Merge remote-tracking branch 'origin/development' into feature/fraction-catalogs

This commit is contained in:
Galindo97
2026-02-17 13:04:54 -06:00
37 changed files with 2776 additions and 373 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,275 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import {
Loader2,
CheckCircle2,
AlertTriangle,
XCircle,
FileText,
UploadCloud
} from 'lucide-svelte';
let {
open = $bindable(false),
scanResults = null,
commitResults = null,
isUploading = false,
onConfirm,
onCancel,
onClose
}: {
open: boolean;
scanResults: any;
commitResults: any;
isUploading: boolean;
onConfirm: () => void;
onCancel: () => void;
onClose: () => void;
} = $props();
// Determine state
let isPending = $derived(!!scanResults && !commitResults);
let isFinished = $derived(!!commitResults);
// Derived metrics for UI logic
let hasErrors = $derived(
scanResults?.error_count > 0 ||
commitResults?.skipped_invalid > 0 ||
commitResults?.skipped_missing_fk > 0
);
let totalSkipped = $derived(
(commitResults?.skipped_invalid || 0) +
(commitResults?.skipped_missing_fk || 0) +
(commitResults?.skipped_missing_invoice || 0)
);
function handleOpenChange(newOpen: boolean) {
if (!newOpen) {
if (isPending) onCancel();
else onClose();
}
open = newOpen;
}
</script>
<Dialog.Root bind:open onOpenChange={handleOpenChange}>
<Dialog.Content
class="sm:max-w-[650px] p-0 gap-0 overflow-hidden border-0 shadow-xl flex flex-col max-h-[90vh] bg-background text-foreground"
>
<!-- Header Section with Theme Colors -->
<div
class="px-6 py-5 border-b flex items-start gap-4
{isPending
? 'bg-primary/5'
: isFinished && !hasErrors
? 'bg-green-50/50 dark:bg-green-900/10'
: 'bg-destructive/5'}"
>
<div
class="p-2 rounded-full ring-1 ring-inset
{isPending
? 'bg-primary/10 text-primary ring-primary/20'
: isFinished && !hasErrors
? 'bg-green-100 text-green-700 ring-green-200 dark:bg-green-900/20 dark:text-green-400 dark:ring-green-900/40'
: 'bg-destructive/10 text-destructive ring-destructive/20'}"
>
{#if isPending}
<FileText class="w-6 h-6" />
{:else if isFinished && !hasErrors}
<CheckCircle2 class="w-6 h-6" />
{:else}
<AlertTriangle class="w-6 h-6" />
{/if}
</div>
<div class="flex-1">
<Dialog.Title class="text-xl font-semibold tracking-tight text-foreground">
{#if isPending}
Validación de Importación
{:else if isFinished}
{hasErrors ? 'Importación con Observaciones' : 'Importación Exitosa'}
{/if}
</Dialog.Title>
<Dialog.Description class="mt-1 text-muted-foreground">
{#if isPending}
Revise el análisis preliminar antes de confirmar la carga de datos.
{:else if isFinished}
El proceso de importación ha finalizado.
{/if}
</Dialog.Description>
</div>
</div>
<!-- Scrollable Content -->
<div class="px-6 py-6 overflow-y-auto">
<!-- PENDING STATE CONTENT -->
{#if isPending}
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
<!-- Card: Total -->
<div
class="bg-card p-4 rounded-lg border flex flex-col items-center justify-center text-center shadow-sm"
>
<span class="text-muted-foreground text-xs uppercase font-bold tracking-wider mb-1"
>Total Filas</span
>
<span class="text-2xl font-bold text-foreground">{scanResults.total_rows || 0}</span>
</div>
<!-- Card: Valid -->
<div
class="bg-green-50/50 dark:bg-green-900/10 p-4 rounded-lg border border-green-100 dark:border-green-900/30 flex flex-col items-center justify-center text-center shadow-sm"
>
<span
class="text-green-600 dark:text-green-400 text-xs uppercase font-bold tracking-wider mb-1"
>Válidos</span
>
<span class="text-2xl font-bold text-green-700 dark:text-green-300"
>{scanResults.valid_rows || 0}</span
>
</div>
<!-- Card: Errors -->
<div
class="bg-destructive/5 p-4 rounded-lg border border-destructive/10 flex flex-col items-center justify-center text-center shadow-sm"
>
<span class="text-destructive text-xs uppercase font-bold tracking-wider mb-1"
>Errores</span
>
<span class="text-2xl font-bold text-destructive">{scanResults.error_count || 0}</span>
</div>
</div>
{#if scanResults.error_count > 0}
<div
class="rounded-md bg-destructive/5 border border-destructive/10 p-4 flex items-start gap-3"
>
<XCircle class="w-5 h-5 text-destructive mt-0.5 shrink-0" />
<div class="text-sm text-destructive-foreground/90">
<p class="font-semibold mb-1">Se detectaron problemas en el archivo</p>
<p>
Las filas con errores serán omitidas automáticamente. Solo se importarán los
registros válidos.
</p>
</div>
</div>
{:else}
<div class="rounded-md bg-primary/5 border border-primary/10 p-4 flex items-start gap-3">
<CheckCircle2 class="w-5 h-5 text-primary mt-0.5 shrink-0" />
<div class="text-sm text-primary/90">
<p class="font-semibold mb-1">Archivo validado correctamente</p>
<p>Todos los registros parecen correctos y listos para importar.</p>
</div>
</div>
{/if}
{/if}
<!-- FINISHED STATE CONTENT -->
{#if isFinished}
<div class="space-y-6">
<!-- Simplified 2-Column Stats Grid -->
<div class="grid grid-cols-2 gap-4">
<!-- Inserted (Green) -->
<div
class="bg-green-50/50 dark:bg-green-900/10 p-4 rounded-lg border border-green-100 dark:border-green-900/30 flex flex-col items-center justify-center text-center shadow-sm"
>
<div class="flex items-center gap-2 mb-1">
<CheckCircle2 class="w-4 h-4 text-green-600 dark:text-green-400" />
<span
class="text-green-600 dark:text-green-400 text-xs uppercase font-bold tracking-wider"
>Insertados</span
>
</div>
<span class="text-3xl font-bold text-green-700 dark:text-green-300"
>{commitResults.inserted || 0}</span
>
</div>
<!-- Rejected (Red) -->
<div
class="bg-destructive/5 p-4 rounded-lg border border-destructive/10 flex flex-col items-center justify-center text-center shadow-sm"
>
<div class="flex items-center gap-2 mb-1">
<XCircle class="w-4 h-4 text-destructive" />
<span class="text-destructive text-xs uppercase font-bold tracking-wider"
>Rechazados</span
>
</div>
<span class="text-3xl font-bold text-destructive">{totalSkipped}</span>
</div>
</div>
<!-- Error Details Table -->
{#if commitResults.skipped_details && commitResults.skipped_details.length > 0}
<div class="border rounded-lg overflow-hidden mt-2 shadow-sm">
<div class="bg-muted/50 px-4 py-2 border-b flex justify-between items-center">
<h5 class="text-xs font-bold text-foreground uppercase tracking-wide">
Detalle de Errores
</h5>
<span
class="text-[10px] bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border"
>
{commitResults.skipped_details.length} filas
</span>
</div>
<div class="max-h-60 overflow-y-auto bg-card relative">
<table class="w-full text-xs text-left">
<thead
class="text-muted-foreground font-medium bg-muted/30 sticky top-0 z-10 shadow-sm backdrop-blur-sm"
>
<tr>
<th class="px-4 py-2 w-20">Línea</th>
<th class="px-4 py-2 w-32">Referencia</th>
<th class="px-4 py-2">Motivo</th>
</tr>
</thead>
<tbody class="divide-y">
{#each commitResults.skipped_details as detail}
<tr class="hover:bg-muted/30 transition-colors">
<td class="px-4 py-2 font-mono text-muted-foreground">{detail.line}</td>
<td class="px-4 py-2 font-mono font-medium text-foreground"
>{detail.invoice || '-'}</td
>
<td class="px-4 py-2 text-destructive">{detail.reason}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</div>
{/if}
</div>
<!-- Footer Actions -->
<div class="px-6 py-4 bg-muted/20 border-t flex items-center justify-end gap-3">
{#if isPending}
<Button
variant="ghost"
onclick={onCancel}
disabled={isUploading}
class="text-muted-foreground hover:bg-muted/50"
>
Cancelar Operación
</Button>
<Button
onclick={onConfirm}
disabled={isUploading}
class="bg-primary hover:bg-primary/90 text-primary-foreground min-w-[140px] shadow-sm"
>
{#if isUploading}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
Procesando...
{:else}
<UploadCloud class="mr-2 h-4 w-4" />
Confirmar Carga
{/if}
</Button>
{:else if isFinished}
<Button variant="outline" onclick={onClose} class="min-w-[100px]">Cerrar</Button>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>

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

@@ -23,18 +23,21 @@
let isLoadingFractions = $state(false);
let searchTimeout: ReturnType<typeof setTimeout>;
// Cargar datos al abrir
let wasOpen = $state(false);
// Cargar datos al abrir
$effect(() => {
if (open) {
// Reiniciar estado al abrir
if (tariffFractions.length === 0) {
searchFraction = '';
currentPage = 1;
totalFractions = 0;
hasMoreFractions = true;
loadFractions('', 1);
}
if (open && !wasOpen) {
// Reiniciar estado SOLO al abrir
searchFraction = '';
currentPage = 1;
totalFractions = 0;
hasMoreFractions = true;
tariffFractions = [];
loadFractions('', 1);
}
wasOpen = open;
});
async function loadFractions(search: string, page: number = currentPage) {

View File

@@ -34,7 +34,7 @@
defaultOperationType = undefined,
exchangeRate = undefined,
invoiceType = undefined
}: {
}: {
invoice: Invoice | null;
formData?: any;
invoiceTypes?: InvoiceType[];
@@ -193,7 +193,10 @@
const soldToHeaderOptions = $derived([
{ value: 'consignado_a', label: 'Consignado a' },
{ value: 'vendido_a', label: 'Vendido a' },
{ value: operationType === 1 ? 'exportado_a' : 'importador', label: operationType === 1 ? 'Exportado a' : 'Importador' }
{
value: operationType === 1 ? 'exportado_a' : 'importador',
label: operationType === 1 ? 'Exportado a' : 'Importador'
}
]);
const shippedToHeaderOptions = $derived(
@@ -213,21 +216,21 @@
const shippedByHeaderOptions = $derived(
operationType === 1 || invoiceType === 'CR'
? [
{ value: 'enviado_por', label: 'Enviado Por' },
{ value: 'destinatario', label: 'Destinatario' },
{ value: 'vendido_por', label: 'Vendido Por' },
{ value: 'consignado_a', label: 'Consignado a' },
{ value: 'vendido_a', label: 'Vendido a' },
{ value: 'exportado_a', label: 'Exportado a' },
{ value: 'enviado_a', label: 'Enviado a' },
{ value: 'transferido_a', label: 'Transferido a' },
{ value: 'donado_a', label: 'Donado a' },
{ value: 'notificar_a', label: 'Notificar a' }
]
{ value: 'enviado_por', label: 'Enviado Por' },
{ value: 'destinatario', label: 'Destinatario' },
{ value: 'vendido_por', label: 'Vendido Por' },
{ value: 'consignado_a', label: 'Consignado a' },
{ value: 'vendido_a', label: 'Vendido a' },
{ value: 'exportado_a', label: 'Exportado a' },
{ value: 'enviado_a', label: 'Enviado a' },
{ value: 'transferido_a', label: 'Transferido a' },
{ value: 'donado_a', label: 'Donado a' },
{ value: 'notificar_a', label: 'Notificar a' }
]
: [
{ value: 'enviado_a', label: 'Enviado a' },
{ value: 'transferido_a', label: 'Transferido a' }
]
{ value: 'enviado_a', label: 'Enviado a' },
{ value: 'transferido_a', label: 'Transferido a' }
]
);
// Combinar clientes y proveedores para shipped_to, evitando duplicados de tipo "both"
@@ -427,80 +430,85 @@
<span class="text-red-500">*</span>
</div>
<div class="grid grid-cols-4 gap-3 space-y-1.5">
<Select.Root
type="single"
value={formData.shipped_to_header || shippedToHeaderOptions[0]?.value || ''}
onValueChange={(v) => {
formData.shipped_to_header = v ?? '';
}}
>
<Select.Trigger id="shipped_to_header" class="h-7 text-xs min-w-[125px] max-w-[250px]">
<span class="truncate">
{shippedToHeaderOptions.find(o => o.value === (formData.shipped_to_header || shippedToHeaderOptions[0]?.value))?.label || 'Selecciona encabezado...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each shippedToHeaderOptions as option}
<Select.Item value={option.value}>
{option.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<Select.Root
type="single"
value={formData.shipped_to_id ? String(formData.shipped_to_id) : ''}
onValueChange={(v) => {
formData.shipped_to_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="shipped_to_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
<span class="truncate">
{#if formData.shipped_to_id}
{allClientsProviders.find(cp => cp.id === formData.shipped_to_id)?.name || 'Selecciona...'}
{:else}
Selecciona...
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each allClientsProviders as cp}
<Select.Item value={String(cp.id)}>
{cp.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<span class="text-red-500">*</span>
</div>
<div class="grid grid-cols-4 gap-3 space-y-1.5">
<Select.Root
type="single"
value={formData.shipped_to_header || shippedToHeaderOptions[0]?.value || ''}
onValueChange={(v) => {
formData.shipped_to_header = v ?? '';
}}
>
<Select.Trigger id="shipped_to_header" class="h-7 text-xs min-w-[125px] max-w-[250px]">
<span class="truncate">
{shippedToHeaderOptions.find(
(o) => o.value === (formData.shipped_to_header || shippedToHeaderOptions[0]?.value)
)?.label || 'Selecciona encabezado...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each shippedToHeaderOptions as option}
<Select.Item value={option.value}>
{option.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<Select.Root
type="single"
value={formData.shipped_to_id ? String(formData.shipped_to_id) : ''}
onValueChange={(v) => {
formData.shipped_to_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="shipped_to_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
<span class="truncate">
{#if formData.shipped_to_id}
{allClientsProviders.find((cp) => cp.id === formData.shipped_to_id)?.name ||
'Selecciona...'}
{:else}
Selecciona...
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each allClientsProviders as cp}
<Select.Item value={String(cp.id)}>
{cp.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<span class="text-red-500">*</span>
</div>
<div class="space-y-1.5">
<Label for="customs_broker_id" class="text-xs">Agente Aduanal Mex: <span class="text-red-500">*</span></Label>
<Select.Root
type="single"
value={formData.customs_broker_id ? String(formData.customs_broker_id) : ''}
onValueChange={(v) => {
formData.customs_broker_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="customs_broker_id" class="h-7 text-xs min-w-[150px] max-w-[300px]">
<span class="truncate">
{formData.customs_broker_id
? customsBrokers.find(cb => cb.id === formData.customs_broker_id)?.name || 'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsBrokers as broker}
<Select.Item value={broker.id.toString()}>
{broker.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="customs_broker_id" class="text-xs"
>Agente Aduanal Mex: <span class="text-red-500">*</span></Label
>
<Select.Root
type="single"
value={formData.customs_broker_id ? String(formData.customs_broker_id) : ''}
onValueChange={(v) => {
formData.customs_broker_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="customs_broker_id" class="h-7 text-xs min-w-[150px] max-w-[300px]">
<span class="truncate">
{formData.customs_broker_id
? customsBrokers.find((cb) => cb.id === formData.customs_broker_id)?.name ||
'Selecciona...'
: 'Selecciona...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsBrokers as broker}
<Select.Item value={broker.id.toString()}>
{broker.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1.5">
<Label for="customs_broker_us_id" class="text-xs">Agente Aduanal Ame:</Label>
@@ -530,88 +538,101 @@
</div>
</div>
<!-- Columna Derecha: Tipo de Moneda y Transportista -->
<div class="space-y-3">
<!-- Tipo de Moneda - Pesos Netos y Brutos -->
<div class="border rounded-md p-3 space-y-2">
<div class="flex justify-between">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Tipo de Moneda - Pesos Netos y Brutos</h4>
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
Tipo de cambio:
<span class="text-primary ml-1">
{(exchangeRate !== undefined && exchangeRate !== null)
? (exchangeRate === 0 ? 'N/A' : Number(exchangeRate).toFixed(4))
: (formData.exchange_rate ? Number(formData.exchange_rate).toFixed(4) : 'N/A')}
</span>
</h4>
</div>
<!-- Radio buttons para tipo de moneda -->
<div class="space-y-1.5">
<RadioGroup.Root bind:value={formData.currency} class="flex gap-4">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="foreign" id="currency-foreign" class="h-4 w-4" />
<Label for="currency-foreign" class="text-xs font-normal cursor-pointer">Extranjera (Dlls)</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="local" id="currency-local" class="h-4 w-4" />
<Label for="currency-local" class="text-xs font-normal cursor-pointer">Nacional (Pesos)</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="manual" id="currency-manual" class="h-4 w-4" />
<Label for="currency-manual" class="text-xs font-normal cursor-pointer">De Captura</Label>
</div>
</RadioGroup.Root>
</div>
{#if formData.currency === 'manual'}
<div class="space-y-1.5">
<Label for="currency_type" class="text-xs">Moneda:</Label>
<Select.Root
type="single"
value={formData.currency_type || ''}
onValueChange={(v) => {
formData.currency_type = v ?? '';
}}
>
<Select.Trigger id="currency_type" class="h-7 text-xs min-w-[80px]">
<span class="truncate">
{formData.currency_type || '...'}
</span>
</Select.Trigger>
<Select.Content class="min-w-[80px] max-h-[300px]">
{#each currencyTypes as currencyType}
<Select.Item value={currencyType.code}>
{currencyType.code}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{/if}
<div class="grid grid-cols-3 gap-2">
<div class="space-y-1.5">
<Label for="weight_type" class="text-xs">Tipo Peso:</Label>
<Select.Root
type="single"
value={formData.weight_type || 'kgs'}
onValueChange={(v) => {
formData.weight_type = v ?? 'kgs';
}}
>
<Select.Trigger id="weight_type" class="min-w-[150px] h-7 text-xs">
<span class="truncate">
{weightTypeOptions.find(w => w.value === formData.weight_type)?.label || 'Kilogramos (kg)'}
</span>
</Select.Trigger>
<Select.Content>
{#each weightTypeOptions as weightType}
<Select.Item value={weightType.value}>
{weightType.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<!-- Columna Derecha: Tipo de Moneda y Transportista -->
<div class="space-y-3">
<!-- Tipo de Moneda - Pesos Netos y Brutos -->
<div class="border rounded-md p-3 space-y-2">
<div class="flex justify-between">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
Tipo de Moneda - Pesos Netos y Brutos
</h4>
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
Tipo de cambio:
<span class="text-primary ml-1">
{exchangeRate !== undefined && exchangeRate !== null
? exchangeRate === 0
? 'N/A'
: Number(exchangeRate).toFixed(4)
: formData.exchange_rate
? Number(formData.exchange_rate).toFixed(4)
: 'N/A'}
</span>
</h4>
</div>
<!-- Radio buttons para tipo de moneda -->
<div class="space-y-1.5">
<RadioGroup.Root bind:value={formData.currency} class="flex gap-4">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="foreign" id="currency-foreign" class="h-4 w-4" />
<Label for="currency-foreign" class="text-xs font-normal cursor-pointer"
>Extranjera (Dlls)</Label
>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="local" id="currency-local" class="h-4 w-4" />
<Label for="currency-local" class="text-xs font-normal cursor-pointer"
>Nacional (Pesos)</Label
>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="manual" id="currency-manual" class="h-4 w-4" />
<Label for="currency-manual" class="text-xs font-normal cursor-pointer"
>De Captura</Label
>
</div>
</RadioGroup.Root>
</div>
{#if formData.currency === 'manual'}
<div class="space-y-1.5">
<Label for="currency_type" class="text-xs">Moneda:</Label>
<Select.Root
type="single"
value={formData.currency_type || ''}
onValueChange={(v) => {
formData.currency_type = v ?? '';
}}
>
<Select.Trigger id="currency_type" class="h-7 text-xs min-w-[80px]">
<span class="truncate">
{formData.currency_type || '...'}
</span>
</Select.Trigger>
<Select.Content class="min-w-[80px] max-h-[300px]">
{#each currencyTypes as currencyType}
<Select.Item value={currencyType.code}>
{currencyType.code}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{/if}
<div class="grid grid-cols-3 gap-2">
<div class="space-y-1.5">
<Label for="weight_type" class="text-xs">Tipo Peso:</Label>
<Select.Root
type="single"
value={formData.weight_type || 'kgs'}
onValueChange={(v) => {
formData.weight_type = v ?? 'kgs';
}}
>
<Select.Trigger id="weight_type" class="min-w-[150px] h-7 text-xs">
<span class="truncate">
{weightTypeOptions.find((w) => w.value === formData.weight_type)?.label ||
'Kilogramos (kg)'}
</span>
</Select.Trigger>
<Select.Content>
{#each weightTypeOptions as weightType}
<Select.Item value={weightType.value}>
{weightType.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{#if operationType !== 1 && invoiceType !== 'MEX' && invoiceType !== 'CR' && invoiceType !== 'REP' && invoiceType !== 'REPAR'}
<div class="space-y-1.5">
@@ -647,44 +668,46 @@
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
<!-- Transportista -->
<div class="border rounded-md p-3 space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Transportista</h4>
<div class="grid grid-cols-3 gap-2">
<!-- Transportista -->
<div class="border rounded-md p-3 space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Transportista</h4>
<div class="grid grid-cols-3 gap-2">
{#if invoiceType !== 'MEX'}
<div class="space-y-1.5">
<Label for="carrier_id" class="text-xs">Transportista:</Label>
<Select.Root
type="single"
value={formData.carrier_id ? String(formData.carrier_id) : ''}
onValueChange={(v) => {
formData.carrier_id = v || null;
}}
>
<Select.Trigger id="carrier_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
<span class="truncate">
{#if formData.carrier_id}
{transporters.find(t => String(t.transporter_key) === String(formData.carrier_id))?.name || formData.carrier_id}
{:else if transporters.length > 0}
Selecciona transportista...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each transporters as transporter}
<Select.Item value={String(transporter.transporter_key)}>
{transporter.transporter_key}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<Select.Root
type="single"
value={formData.carrier_id ? String(formData.carrier_id) : ''}
onValueChange={(v) => {
formData.carrier_id = v || null;
}}
>
<Select.Trigger id="carrier_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
<span class="truncate">
{#if formData.carrier_id}
{transporters.find(
(t) => String(t.transporter_key) === String(formData.carrier_id)
)?.name || formData.carrier_id}
{:else if transporters.length > 0}
Selecciona transportista...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each transporters as transporter}
<Select.Item value={String(transporter.transporter_key)}>
{transporter.transporter_key}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{/if}
<div class="col-span-2 space-y-1.5">
@@ -811,33 +834,34 @@
{#if invoiceType !== 'MEX'}
<div class="space-y-1.5">
<Label for="aduana" class="text-xs">Aduana y Sección de Despacho:</Label>
<Select.Root
type="single"
value={formData.aduana || ''}
onValueChange={(v) => {
formData.aduana = v ?? '';
}}
>
<Select.Trigger id="aduana" class="h-7 text-xs w-full">
<span class="truncate">
{#if formData.aduana}
{customsSections.find(cs => cs.customs_code === formData.aduana)?.section_name || formData.aduana}
{:else if customsSections.length > 0}
Selecciona aduana...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsSections as section}
<Select.Item value={section.customs_code}>
{section.customs_code} - {section.section_name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<Select.Root
type="single"
value={formData.aduana || ''}
onValueChange={(v) => {
formData.aduana = v ?? '';
}}
>
<Select.Trigger id="aduana" class="h-7 text-xs w-full">
<span class="truncate">
{#if formData.aduana}
{customsSections.find((cs) => cs.customs_code === formData.aduana)
?.section_name || formData.aduana}
{:else if customsSections.length > 0}
Selecciona aduana...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsSections as section}
<Select.Item value={section.customs_code}>
{section.customs_code} - {section.section_name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{/if}
{#if invoiceType !== 'MEX'}
@@ -845,35 +869,36 @@
<Label for="document_type" class="text-xs"
>Clave de Régimen Aduanero: <span class="text-red-500">*</span></Label
>
<Select.Root
type="single"
value={formData.document_type || ''}
onValueChange={(v) => {
formData.document_type = v ?? '';
}}
>
<Select.Trigger id="document_type" class="h-7 text-xs w-full">
<span class="truncate">
{#if formData.document_type}
{codePedimentoRegimens.find(r => r.regimen_code === formData.document_type)?.regimen_code || formData.document_type}
{:else if filteredRegimens.length > 0}
Selecciona régimen...
{:else if operationType}
Sin regímenes para tipo {operationType}
{:else}
Selecciona tipo de operación primero
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each filteredRegimens as regimen}
<Select.Item value={regimen.regimen_code}>
{regimen.regimen_code}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<Select.Root
type="single"
value={formData.document_type || ''}
onValueChange={(v) => {
formData.document_type = v ?? '';
}}
>
<Select.Trigger id="document_type" class="h-7 text-xs w-full">
<span class="truncate">
{#if formData.document_type}
{codePedimentoRegimens.find((r) => r.regimen_code === formData.document_type)
?.regimen_code || formData.document_type}
{:else if filteredRegimens.length > 0}
Selecciona régimen...
{:else if operationType}
Sin regímenes para tipo {operationType}
{:else}
Selecciona tipo de operación primero
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each filteredRegimens as regimen}
<Select.Item value={regimen.regimen_code}>
{regimen.regimen_code}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{/if}
{#if invoiceType === 'MEX'}
@@ -891,8 +916,8 @@
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
</div>
<ManifestSelectorModal bind:open={showManifestModal} onSelect={handleManifestSelect} />

View File

@@ -15,6 +15,7 @@ import {
Shield,
Users,
Ship,
MoreHorizontal,
} from 'lucide-svelte';
import * as m from "$lib/paraglide/messages.js";
import { Title } from '../ui/alert';
@@ -459,6 +460,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: 'invoice_header',
templateUrl: '/csv/EstructuraEncFacImpoTemp.xls'
},
{
id: 'imp_temp_details',
title: 'Partidas',
icon: Package,
group: 'Impo. Temp.',
modelTarget: 'invoice_details',
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,
},
];