feature/parametros-csv

This commit is contained in:
hreyes
2026-03-04 10:56:40 -07:00
parent ccafe8d16c
commit 196b70924e
4 changed files with 247 additions and 24 deletions

View File

@@ -0,0 +1,124 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label/index.js';
import { Settings2 } from 'lucide-svelte';
import { globalCsvParams, tabSettings, type CsvUploadField } from '$lib/config/csv-upload';
let {
globalSettings = $bindable(),
activeTab,
tabSettingsValues = $bindable()
}: {
globalSettings: Record<string, string>;
activeTab: string;
tabSettingsValues: Record<string, any>;
} = $props();
const globalParamNames = $derived(new Set(globalCsvParams.map((p) => p.name)));
// Tab fields excluding those already in global params (e.g. mode) to avoid duplication
let currentTabFields = $derived(
((tabSettings[activeTab] || []) as CsvUploadField[]).filter((f) => !globalParamNames.has(f.name))
);
// Guard: parent may pass undefined on first tick; avoid reading from undefined
let safeGlobalSettings = $derived(globalSettings ?? {});
let safeTabSettings = $derived(tabSettingsValues ?? {});
const selectClass =
'flex h-8 min-w-[120px] rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50';
</script>
<!-- Fixed bar: z-index below help bubble (help uses z-50) so help stays on top -->
<div
class="fixed right-0 bottom-0 left-0 z-40 ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
>
<div class="mx-auto max-w-[1400px] px-4 py-3">
<div class="flex flex-wrap items-end gap-6">
<!-- Parámetros globales -->
<div class="flex flex-wrap items-center gap-4">
<div class="flex items-center gap-2 text-muted-foreground border-r border-border pr-4">
<Settings2 class="h-4 w-4 shrink-0" />
<span class="text-xs font-semibold uppercase tracking-wider whitespace-nowrap"
>Parámetros globales</span
>
</div>
{#each globalCsvParams as param}
<div class="flex flex-col gap-1">
<Label for="global-{param.name}" class="text-xs font-medium text-muted-foreground"
>{param.label}</Label
>
<select
id="global-{param.name}"
class={selectClass}
value={safeGlobalSettings[param.name]}
oninput={(e) => {
if (globalSettings) globalSettings[param.name] = e.currentTarget.value;
}}
>
{#each param.options as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
</div>
{/each}
</div>
<!-- Configuración del tab actual -->
{#if currentTabFields.length > 0}
<div class="flex flex-wrap items-center gap-4 border-l border-border pl-4">
<span class="text-xs font-semibold uppercase tracking-wider text-muted-foreground whitespace-nowrap"
>Configuración: {activeTab}</span
>
{#each currentTabFields as field}
<div class="flex flex-col gap-1">
{#if field.type !== 'boolean'}
<Label for="tab-{field.name}" class="text-xs font-medium text-muted-foreground"
>{field.label}</Label
>
{/if}
{#if field.type === 'select' && field.options}
<select
id="tab-{field.name}"
class={selectClass}
value={safeTabSettings[field.name]}
oninput={(e) => {
if (tabSettingsValues) tabSettingsValues[field.name] = e.currentTarget.value;
}}
>
{#each field.options as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
{:else if field.type === 'radio' && field.options}
<div class="flex gap-3 items-center h-8">
{#each field.options as opt}
<label class="flex items-center gap-2 cursor-pointer text-sm">
<input
type="radio"
name="tab-{field.name}"
value={opt.value}
checked={safeTabSettings[field.name] === opt.value}
onchange={() => {
if (tabSettingsValues) tabSettingsValues[field.name] = opt.value;
}}
/>
{opt.label}
</label>
{/each}
</div>
{:else if field.type === 'boolean'}
<label class="flex items-center gap-2 cursor-pointer text-sm h-8">
<input
type="checkbox"
checked={!!safeTabSettings[field.name]}
onchange={(e) => {
if (tabSettingsValues) tabSettingsValues[field.name] = e.currentTarget.checked;
}}
/>
{field.label}
</label>
{/if}
</div>
{/each}
</div>
{/if}
</div>
</div>
</div>

View File

@@ -44,6 +44,80 @@ export interface CsvUploadField {
defaultValue?: any;
}
/** Global parameters shown in the CSV upload footer bar (one option or the other via select). */
export interface GlobalCsvParam {
name: string;
label: string;
type: 'select';
options: { label: string; value: string }[];
defaultValue: string;
}
/** Global parameters for all CSV loads; merged with tab-specific settings when sending footer_config. */
export const globalCsvParams: GlobalCsvParam[] = [
{
name: 'mode',
label: 'Modo de Carga',
type: 'select',
options: [
{ label: 'Actualizar', value: 'update' },
{ label: 'Reemplazar', value: 'replace' }
],
defaultValue: 'update'
},
{
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'
},
{
name: 'weight_unit',
label: 'Unidad de Peso',
type: 'select',
options: [
{ label: 'Kilos (Kgs)', value: 'kgs' },
{ label: 'Libras (Lbs)', value: 'lbs' }
],
defaultValue: 'kgs'
},
{
name: 'autonumber_series',
label: 'Autonumerar Partidas/Series',
type: 'select',
options: [
{ label: 'Sí', value: 'true' },
{ label: 'No', value: 'false' }
],
defaultValue: 'false'
},
{
name: 'load_subpartidas',
label: 'Levantar Subpartidas',
type: 'select',
options: [
{ label: 'Sí', value: 'true' },
{ label: 'No', value: 'false' }
],
defaultValue: 'false'
},
{
name: 'recalculate_pedimento_date',
label: 'Recalcular Fecha Pedimento',
type: 'select',
options: [
{ label: 'Sí', value: 'true' },
{ label: 'No', value: 'false' }
],
defaultValue: 'false'
}
];
// Map of Tab ID -> Array of Fields
export const tabSettings: Record<string, CsvUploadField[]> = {
catalogos: [

View File

@@ -57,7 +57,7 @@
-->
</div>
</header>
<div class="flex flex-1 flex-col gap-4 overflow-x-hidden p-4 pt-0">
<div class="flex flex-1 flex-col min-h-0 gap-4 overflow-x-hidden p-4 pt-0">
<!-- Contenido de cada página -->
{@render children?.()}
</div>

View File

@@ -1,7 +1,7 @@
<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 CsvParamsBar from '$lib/components/dashboard/csv-upload/CsvParamsBar.svelte';
import ProcessingResultModal from '$lib/components/dashboard/csv-upload/ProcessingResultModal.svelte';
import {
catalogosConfig,
@@ -9,6 +9,7 @@
importacionConfig,
exportacionConfig,
tabSettings,
globalCsvParams,
type CsvUploadItem
} from '$lib/config/csv-upload';
import { api } from '$lib/api';
@@ -49,17 +50,41 @@
// Cuando es true, usamos API de importación de BOMs (boms/imports)
let useBomImport = $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;
// Initialize settings for all tabs upfront to avoid reactivity loops (sync init so child never receives undefined)
const _initialSettings: Record<string, any> = {};
for (const tab in tabSettings) {
_initialSettings[tab] = {};
tabSettings[tab].forEach((f) => {
_initialSettings[tab][f.name] = f.defaultValue;
});
}
let allSettings = $state<Record<string, any>>(_initialSettings);
// Global parameters for all CSV loads (merged with tab settings when sending footer_config)
const _initialGlobal: Record<string, string> = {};
globalCsvParams.forEach((p) => {
_initialGlobal[p.name] = String(p.defaultValue);
});
let globalSettings = $state<Record<string, string>>(_initialGlobal);
/** Coerce string 'true'/'false' to boolean for API payload. */
function coerceFooterConfig(obj: Record<string, any>): Record<string, any> {
const booleanKeys = [
'autonumber_series',
'load_subpartidas',
'recalculate_pedimento_date',
'autonumber_remesas',
'recalculate_dates',
'is_regime_change'
];
const out = { ...obj };
for (const k of booleanKeys) {
if (k in out && typeof out[k] === 'string') {
out[k] = out[k] === 'true';
}
}
return out;
}
async function handleUpload(file: File, config: CsvUploadItem) {
console.log('handleUpload started', { file, config });
@@ -297,8 +322,11 @@
return;
}
const currentSettings = allSettings[activeTab] || {};
const footerConfig = { ...currentSettings };
const currentTabSettings = allSettings[activeTab] || {};
const footerConfig = coerceFooterConfig({
...globalSettings,
...currentTabSettings
});
if (activeTab === 'importacion') {
footerConfig.invoice_type = config.id?.startsWith('imp_def_') ? 'DEF' : 'TEM';
}
@@ -432,9 +460,9 @@
}
</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 flex-col flex-1 min-h-0 -m-4 overflow-hidden">
<!-- Single scroll: content scrolls here; padding at bottom reserves space for fixed params bar -->
<div class="flex-1 min-h-0 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>
@@ -481,15 +509,12 @@
</div>
</Tabs.Root>
<div class="h-4"></div>
<!-- Spacer at end of page: reserves space for fixed params bar so content is not cut off -->
<div class="h-[var(--csv-params-bar-height,6rem)] shrink-0" aria-hidden="true"></div>
</div>
<!-- Fixed Footer Area -->
{#if allSettings[activeTab]}
<div class="flex-none z-20">
<ConfigFooter {activeTab} bind:settings={allSettings[activeTab]} />
</div>
{/if}
<!-- Fixed params bar (always visible on this view) -->
<CsvParamsBar bind:globalSettings {activeTab} bind:tabSettingsValues={allSettings[activeTab]} />
</div>
{#if scanResults || commitResults}