Merge remote-tracking branch 'origin/development' into feature/fraction-catalogs
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,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>
|
||||
@@ -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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: '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,
|
||||
},
|
||||
];
|
||||
209
frontend/src/routes/dashboard/csv-upload/+page.svelte
Normal file
209
frontend/src/routes/dashboard/csv-upload/+page.svelte
Normal file
@@ -0,0 +1,209 @@
|
||||
<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 ProcessingResultModal from '$lib/components/dashboard/csv-upload/ProcessingResultModal.svelte';
|
||||
import {
|
||||
catalogosConfig,
|
||||
transportesConfig,
|
||||
importacionConfig,
|
||||
exportacionConfig,
|
||||
tabSettings,
|
||||
type CsvUploadItem
|
||||
} from '$lib/config/csv-upload';
|
||||
import { api } from '$lib/api';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
// We no longer need modal state
|
||||
let activeTab = $state('catalogos');
|
||||
|
||||
let isUploading = $state(false);
|
||||
let currentJobId = $state<string | null>(null);
|
||||
let activeModelTarget = $state<string | null>(null);
|
||||
let scanResults = $state<any>(null);
|
||||
let commitResults = $state<any>(null);
|
||||
let showResultModal = $state(false);
|
||||
|
||||
// Initialize settings for all tabs upfront to avoid reactivity loops
|
||||
let allSettings = $state<Record<string, any>>(() => {
|
||||
const initial: Record<string, any> = {};
|
||||
for (const tab in tabSettings) {
|
||||
initial[tab] = {};
|
||||
tabSettings[tab].forEach((f) => {
|
||||
initial[tab][f.name] = f.defaultValue;
|
||||
});
|
||||
}
|
||||
return initial;
|
||||
});
|
||||
|
||||
async function handleUpload(file: File, config: CsvUploadItem) {
|
||||
isUploading = true;
|
||||
activeModelTarget = config.modelTarget || null;
|
||||
scanResults = null;
|
||||
const currentSettings = allSettings[activeTab] || {};
|
||||
const companyId = companyStore.activeCompany?.id || 1;
|
||||
const opType = activeTab === 'exportacion' ? 'exp' : 'imp';
|
||||
|
||||
const res = await api.imports.upload(
|
||||
file,
|
||||
config.modelTarget || '',
|
||||
currentSettings,
|
||||
companyId,
|
||||
opType
|
||||
);
|
||||
if (res.data?.job_id) {
|
||||
currentJobId = res.data.job_id;
|
||||
pollStatus();
|
||||
} else {
|
||||
toast.error('Error al subir el archivo');
|
||||
isUploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function pollStatus() {
|
||||
if (!currentJobId) return;
|
||||
|
||||
const res = await api.imports.status(currentJobId);
|
||||
if (res.data?.status === 'waiting_confirmation') {
|
||||
scanResults = res.data;
|
||||
showResultModal = true;
|
||||
toast.success('Escaneo completado. Revisa los resultados.');
|
||||
isUploading = false;
|
||||
} else if (res.data?.status === 'failed') {
|
||||
toast.error('Error en el procesamiento: ' + (res.data.error || 'Error desconocido'));
|
||||
isUploading = false;
|
||||
currentJobId = null;
|
||||
scanResults = null;
|
||||
commitResults = null;
|
||||
showResultModal = false;
|
||||
} else if (res.data?.status === 'warning') {
|
||||
// Caso cuando no se insertaron registros pero hay información de rechazo
|
||||
commitResults = res.data;
|
||||
showResultModal = true;
|
||||
const inserted = res.data?.inserted || 0;
|
||||
const skippedInvalid = res.data?.skipped_invalid || 0;
|
||||
const skippedFk = res.data?.skipped_missing_fk || 0;
|
||||
const totalSkipped = skippedInvalid + skippedFk;
|
||||
|
||||
if (inserted === 0) {
|
||||
toast.error(`No se insertaron registros. ${totalSkipped} fueron rechazados.`);
|
||||
} else {
|
||||
toast.warning(`Solo se insertaron ${inserted} de ${inserted + totalSkipped} registros.`);
|
||||
}
|
||||
isUploading = false;
|
||||
} else if (res.data?.status === 'finished') {
|
||||
commitResults = res.data;
|
||||
showResultModal = true;
|
||||
const inserted = res.data?.inserted || 0;
|
||||
const skippedInvalid = res.data?.skipped_invalid || 0;
|
||||
const skippedFk = res.data?.skipped_missing_fk || 0;
|
||||
const skippedDetails = res.data?.skipped_details || [];
|
||||
|
||||
if (inserted > 0) {
|
||||
toast.success(`Importación completada: ${inserted} registros insertados`);
|
||||
if (skippedInvalid > 0 || skippedFk > 0) {
|
||||
const totalSkipped = skippedInvalid + skippedFk;
|
||||
toast.warning(`${totalSkipped} registros fueron rechazados`);
|
||||
}
|
||||
} else {
|
||||
toast.error('No se insertaron registros. Revisa los errores a continuación.');
|
||||
}
|
||||
isUploading = false;
|
||||
} else {
|
||||
// Continue polling
|
||||
setTimeout(pollStatus, 2000);
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
||||
<ProcessingResultModal
|
||||
bind:open={showResultModal}
|
||||
{scanResults}
|
||||
{commitResults}
|
||||
{isUploading}
|
||||
onConfirm={async () => {
|
||||
if (currentJobId && activeModelTarget) {
|
||||
try {
|
||||
isUploading = true;
|
||||
const res = await api.imports.commit(currentJobId, activeModelTarget);
|
||||
if (res.data?.commit_job_id) {
|
||||
currentJobId = res.data.commit_job_id;
|
||||
pollStatus();
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('Error al iniciar la importación');
|
||||
isUploading = false;
|
||||
}
|
||||
}
|
||||
}}
|
||||
onCancel={() => {
|
||||
currentJobId = null;
|
||||
scanResults = null;
|
||||
commitResults = null;
|
||||
showResultModal = false;
|
||||
}}
|
||||
onClose={() => {
|
||||
currentJobId = null;
|
||||
scanResults = null;
|
||||
commitResults = null;
|
||||
showResultModal = false;
|
||||
}}
|
||||
/>
|
||||
1
frontend/static/csv/EstructuraCatAgenteAduanal.xls
Normal file
1
frontend/static/csv/EstructuraCatAgenteAduanal.xls
Normal file
@@ -0,0 +1 @@
|
||||
TIPO(MEX=Mexicano,AME=AMERICANO) CLAVE AADUANAL PATENTE NOMBRE RFC DIRECCION CODIGO POSTAL CIUDAD ESTADO PAIS TELEFONO NUMERO FAX CORREO ELECTRONICO CURP
|
||||
1
frontend/static/csv/EstructuraCatClasesAF.xls
Normal file
1
frontend/static/csv/EstructuraCatClasesAF.xls
Normal file
@@ -0,0 +1 @@
|
||||
CLAVE CLASE DESCRIPCION ESPA<50>OL DESCRIPCION INGLES TIPO DE MATERIAL U.M. COMERCIAL FRACCION ARANCELARIA FRACCION AMERICANA TASA DE DEPRECIACION REVISION FISICA (1/0) CODIGO DE PRODUCTO/SERVICIO CP
|
||||
1
frontend/static/csv/EstructuraCatClienteProv.xls
Normal file
1
frontend/static/csv/EstructuraCatClienteProv.xls
Normal file
@@ -0,0 +1 @@
|
||||
PROCEDENCIA CLIENTE(E=Extranjero, N=Nacional) TIPO(C=Cliente,P=Proveedor,A=Ambos) CLAVE CLIENTE NOMBRE RFC CALLES NUM. EXTERIOR CODIGO POSTAL COLONIA o PARQUE IND. CIUDAD ESTADO PAIS TELEFONO NUMERO FAX CORREO ELECTRONICO CURP TIPO DE PROGRAMA SECON NUMERO DE PROGRAMA SECON FECHA AUT. SECON ##/##/#### ES PROGRAMA PROSEC? (SI o NO) NUMERO DE PROGRAMA PROSEC VINCULACION ES EMPRESA CERTIFICADA? REGISTRO DE EMPRESA CERT. INFORMACION ADICIONAL CONTACTO CLAVE MANUFACTURERO TAX I.D. CLAVE BROKER AMERICANO EXPO CLAVE BROKER AMERICANO IMPO CLAVE TRANSFERENCIA A.A. TRANSFORMADOR/SUBMAQUILA CLAVE INTERFACE
|
||||
1
frontend/static/csv/EstructuraCatConductor.xls
Normal file
1
frontend/static/csv/EstructuraCatConductor.xls
Normal file
@@ -0,0 +1 @@
|
||||
TRANSPORTISTA LINEA CLAVE CONDUCTOR LICENCIA PERMISO LINEA EXPRESS IDENTIFICACION ACE FECHA NACIMIENTO SEXO PAIS NACIMIENTO TRANSPORTA MAT. PELIGROSO? PERMISO MAT. PELIGROSO NOMBRE(S) APELLIDO PATERNO FORMA IDENTIFICACION 1 NUM. IDENTIFICACION 1 ESTADO PAIS FORMA IDENTIFICACION 2 NUM. IDENTIFICACION 2 ESTADO PAIS
|
||||
1
frontend/static/csv/EstructuraCatFraccAme.xls
Normal file
1
frontend/static/csv/EstructuraCatFraccAme.xls
Normal file
@@ -0,0 +1 @@
|
||||
FRACCION ARANCELARIA PREFIJO UNIDAD DE MEDIDA DESCRIPCION TIPO DE ADVALOREM ADVALOREM % ADVALOREM DLLS
|
||||
1
frontend/static/csv/EstructuraCatPartesAF.xls
Normal file
1
frontend/static/csv/EstructuraCatPartesAF.xls
Normal file
@@ -0,0 +1 @@
|
||||
NUMERO DE PARTE DESCRIPCION EN ESPA<50>OL DESCRIPCION EN INGLES CLASE UNIDAD DE MEDIDA COMERCIAL COSTO UNITARIO TIPO MONEDA COSTO CLAVE MONEDA PESO UNITARIO TIPO PESO FRACCION PAIS PREFERENCIA SECTOR RUTA DE LA IMAGEN
|
||||
1
frontend/static/csv/EstructuraCatPedimentos.xls
Normal file
1
frontend/static/csv/EstructuraCatPedimentos.xls
Normal file
@@ -0,0 +1 @@
|
||||
NUMERO DE PEDIMENTO (##-####-######) TIPO MOV(I=Impotaci<63>n,E=Expotaci<63>n) CLAVE PEDIMENTO REGIMEN FECHA INICIO FECHA FINAL FECHA DE PAGO ADUANA Y SECCION DE CRUCE ACUSE ELECTRONICO INDIVIDUAL o CONSOLIDADO (IND,CON) MET TRANS ENTRADA MET TRANS ARRIVO MET TRANS SALIDA IEPS DTA CNT PREVALIDACION MONTO TIGIE PAGO IMPUESTO? (S/N) ES MIXTO (SI/NO) OBS RECTIFICA OPCION DESTINO(Interior del Pais/Regi<67>n Fronteriza/Franja Fronteriza) VALOR IVA VALOR ME VALOR ADUANAS FLETE VALOR SEGUROS SEGUROS EMBALAJES OTROS INCREMENTABLES ESTATUS (ABIERTO/CERRADO) PERSONA REV FECHA CIERRE FECHA REVISION FECHA AUTORIZACION FECHA RECIBIDO REPRESENTANTE AA CLAVE DEST ORIGEN FECHA ENTRADA RECINTO FECHA EXTRACCION RECINTO ERRORES FORMA PAGO DTA FORMA PAGO IGI FORMA PAGO PREVAL FORMA PAGO IVA RECARGOS MULTAS IVA DE PREV CUOTAS CONPENSATORIAS IDENTIFICADORES IEPS 2 FORMA DE PAGO IEPS 2 DTA 2 FORMA DE PAGO DTA 2 IVA 2 FORMA DE PAGO IVA 2 IGI 2 FORMA DE PAGO IGI 2 PREVALIDACION FORMA DE PAGO PREVALIDACION 2 CNT 2 FORMA DE PAGO CNT 2
|
||||
1
frontend/static/csv/EstructuraCatTiposCambio.xls
Normal file
1
frontend/static/csv/EstructuraCatTiposCambio.xls
Normal file
@@ -0,0 +1 @@
|
||||
FECHA (##/##/####) TIPO DE CAMBIO
|
||||
1
frontend/static/csv/EstructuraCatTrailers.xls
Normal file
1
frontend/static/csv/EstructuraCatTrailers.xls
Normal file
@@ -0,0 +1 @@
|
||||
CLAVE TRAILER/CAJA NUMERO ACE TIPO DE TRAILER PRECINTO CODIGO DE ENTIDAD PLACAS ESTADO PAIS
|
||||
1
frontend/static/csv/EstructuraCatTransportes.xls
Normal file
1
frontend/static/csv/EstructuraCatTransportes.xls
Normal file
@@ -0,0 +1 @@
|
||||
CLAVE CLAVE ACE CLAVE TRANSPORTE VIN TIPO TRANSPORTE CODIGO DE ENTIDAD TRANSPONDEDOR NUMERO DOT PLACAS CIUDAD ESTADO PAIS PRECINTO EMPRESA ASEGURADORA NUM. ASEGURADORA MONTO ASEGURADO FECHA DE ASEGURADORA
|
||||
1
frontend/static/csv/EstructuraEncFacExpoCamReg.xls
Normal file
1
frontend/static/csv/EstructuraEncFacExpoCamReg.xls
Normal file
@@ -0,0 +1 @@
|
||||
PEDIMENTO REMESA NUMERO FACTURA FECHA FACTURA TIPO DE CAMBIO REGIMEN CLAVE PROVEEDOR CLAVE VENDIDO A: CLAVE ENVIADO A AGENTE ADUANAL CLAVE TRANSPORTISTA NOMBRE CONDUCTOR TIPO TRANSPORTE NUMERO TRANSPORTE TIPO MONEDA CLAVE MONEDA FLETES VALOR SEGUROS SEGUROS EMBALAJES OTROS INCREMENTABLES CLAVE INCOTERM PRECINTO TIPO PESO MANIFIESTO E-DOCUMENT NUM. OPERACION ENVIADO POR ADUANA DE CRUCE OBSERVACIONES E OBSERVACIONES I FACTURA ALTERNA
|
||||
1
frontend/static/csv/EstructuraEncFacImpoDef.xls
Normal file
1
frontend/static/csv/EstructuraEncFacImpoDef.xls
Normal file
@@ -0,0 +1 @@
|
||||
PEDIMENTO REMESA NUMERO FACTURA FECHA FACTURA TIPO DE CAMBIO REGIMEN CLAVE PROVEEDOR CLAVE VENDIDO A: CLAVE ENVIADO A AGENTE ADUANAL CLAVE TRANSPORTISTA NOMBRE CONDUCTOR TIPO TRANSPORTE NUMERO TRANSPORTE TIPO MONEDA CLAVE MONEDA FLETES VALOR SEGUROS SEGUROS EMBALAJES OTROS INCREMENTABLES CLAVE INCOTERM PRECINTO FECHA EMISION TIPO PESO E-DOCUMENT NUM. OPERACION ADUANA DE CRUCE OBSERVACIONES E OBSERVACIONES I
|
||||
1
frontend/static/csv/EstructuraEncFacImpoTemp.xls
Normal file
1
frontend/static/csv/EstructuraEncFacImpoTemp.xls
Normal file
@@ -0,0 +1 @@
|
||||
PEDIMENTO REMESA NUMERO FACTURA FECHA FACTURA TIPO DE CAMBIO REGIMEN CLAVE PROVEEDOR CLAVE VENDIDO A: CLAVE ENVIADO A AGENTE ADUANAL CLAVE TRANSPORTISTA NOMBRE CONDUCTOR TIPO TRANSPORTE NUMERO TRANSPORTE TIPO MONEDA CLAVE MONEDA FLETES VALOR SEGUROS SEGUROS EMBALAJES OTROS INCREMENTABLES CLAVE INCOTERM PRECINTO FECHA EMISION TIPO PESO E-DOCUMENT NUM. OPERACION ADUANA DE CRUCE OBSERVACIONES E OBSERVACIONES I FACTURA ALTERNA
|
||||
1
frontend/static/csv/EstructuraParExpoCamReg.xls
Normal file
1
frontend/static/csv/EstructuraParExpoCamReg.xls
Normal file
@@ -0,0 +1 @@
|
||||
NUMERO FACTURA EXPO. LINEA EXPO. TIPO DE IMPO. FACTURA IMPO. LINEA IMPO. GENERA DESCARGA CANTIDAD EXPORTADA/DESCARGAR COSTO UNITARIO PESO NETO PESO BRUTO SE PAGO IMPUESTO? (SI o NO) FORMA DE PAGO DESCRIPCION EXTRA INFORMACION ADICIONAL AGREGAR(A)/SUSTITUIR(S) LOTE NUMERO ENTRADA ES PARTIDA/SUBPARTIDA LINEA PRINCIPAL FRACCION AMERICANA FRACCION ARANCELARIA
|
||||
1
frontend/static/csv/EstructuraParFacImpoDefAF.xls
Normal file
1
frontend/static/csv/EstructuraParFacImpoDefAF.xls
Normal file
@@ -0,0 +1 @@
|
||||
NUMERO FACTURA LINEA CLASE CANTIDAD IMPORTADA UNIDAD DE MEDIDA COSTO UNITARIO PESO NETO PESO BRUTO CANTIDAD BULTOS CLAVE BULTOS PAIS ORIGEN FRACCION ARANCELARIA PREFERENCIA ARANCELARIA SECTOR FRACCION AMERICANA ORDEN DE COMPRA DESCRIPCION ESPA<50>OL DESCRIPCION INGLES MARCA MODELO ES PARTIDA O SUBPARTIDA LINEA PRINCIPAL NUM. PARTE SE PAGO IMPUESTO? (SI o NO) FORMA DE PAGO METODO DE VALORACION DESCRIPCION EXTRA INFORMACION ADICIONAL AGREGAR(A)/SUSTITUIR(S) VALOR TOTAL LOTE NUMERO ENTRADA ID TYPE
|
||||
1
frontend/static/csv/EstructuraParFacImpoTempAF.xls
Normal file
1
frontend/static/csv/EstructuraParFacImpoTempAF.xls
Normal file
@@ -0,0 +1 @@
|
||||
NUMERO FACTURA LINEA CLASE CANTIDAD IMPORTADA UNIDAD DE MEDIDA COSTO UNITARIO PESO NETO PESO BRUTO CANTIDAD BULTOS CLAVE BULTOS PAIS ORIGEN FRACCION ARANCELARIA PREFERENCIA ARANCELARIA SECTOR FRACCION AMERICANA ORDEN DE COMPRA DESCRIPCION ESPA<50>OL DESCRIPCION INGLES MARCA MODELO ES PARTIDA O SUBPARTIDA LINEA PRINCIPAL NUM. PARTE SE PAGO IMPUESTO? (SI o NO) FORMA DE PAGO METODO DE VALORACION DESCRIPCION EXTRA INFORMACION ADICIONAL AGREGAR(A)/SUSTITUIR(S) TOTAL NUMERO ENTRADA LOTE ID TYPE
|
||||
Reference in New Issue
Block a user