feature/parametros-csv
This commit is contained in:
@@ -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>
|
||||
@@ -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: [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user