JS
document.addEventListener('DOMContentLoaded', function () {
const WIDGET_CONFIG_KEY = 'scenarioSimulatorWidgetConfig_v23_init_fix';
const PALETTE = ["#1E88E5", "#43A047", "#FB8C00", "#E53535", "#8E24AA", "#00838F", "#D81B60"];
const HIGHLIGHT_COLOR = 'rgba(220, 53, 69, 1)', HIGHLIGHT_RADIUS = 7, DEFAULT_RADIUS = 3;
// --- GLOBAL HELPER FUNCTIONS ---
window.globalWidgetFunctions = {
handleLockChange: (checkbox) => { const optionsDiv = checkbox.closest('.variable-config-row').querySelector('.lock-options'); optionsDiv.classList.toggle('hidden', !checkbox.checked); },
updateSliderAndInput: (sourceElement) => { const varId = sourceElement.dataset.variableId; const slider = document.getElementById(`slider-${varId}`); const numberInput = document.getElementById(`number-${varId}`); if (sourceElement.type === 'range') { numberInput.value = sourceElement.value; } else { slider.value = sourceElement.value; } window.handleSliderChange(); },
toggleCollapse: (header) => { const content = document.getElementById(header.dataset.target); content.classList.toggle('collapsed'); header.classList.toggle('collapsed'); }
};
// --- DATA & CONFIG MODULES ---
const GristDataManager = (() => { const state = { tables: null, columns: null }; let _tableMeta = null; async function _loadMeta() { if (!state.tables) state.tables = await grist.docApi.fetchTable('_grist_Tables'); if (!state.columns) state.columns = await grist.docApi.fetchTable('_grist_Tables_column'); } function _getNumericTableId(tableId) { const idx = state.tables.tableId.findIndex(t => t === tableId); return idx !== -1 ? state.tables.id[idx] : null; } function _getColumnsForTable(numericId) { const cols = []; const allCols = state.columns; const parentIdKey = allCols.parentId ? 'parentId' : 'tableId'; const colIdKey = allCols.colId ? 'colId' : 'columnId'; for (let i = 0; i < allCols.id.length; i++) { if (allCols[parentIdKey][i] === numericId) { cols.push({ id: allCols[colIdKey][i], label: allCols.label[i], type: allCols.type[i], isFormula: !!allCols.formula[i], formula: allCols.formula[i] || '' }); } } return cols; } async function fetchAll() { try { const tableId = await grist.selectedTable.getTableId(); if (!tableId) throw new Error("No table is linked to this widget."); await _loadMeta(); const numericId = _getNumericTableId(tableId); if (!numericId) throw new Error(`Could not find metadata for table: ${tableId}`); _tableMeta = { id: tableId, columns: _getColumnsForTable(numericId) }; return { tableMeta: _tableMeta }; } catch (e) { console.error("GDM Error:", e); _tableMeta = null; throw e; } } return { fetchAll, getTableMeta: () => _tableMeta }; })();
const WidgetConfigManager = (() => { const DEFAULTS = { formulaColumnId: null, variableSettings: {}, showChart: true, compactMode: false, widgetTitle: "Scenario Simulator", titleColumnId: null, debugMode: false }; let _currentConfig = { ...DEFAULTS }; async function loadConfig() { const stored = await grist.getOption(WIDGET_CONFIG_KEY); _currentConfig = { ...DEFAULTS, ...(stored || {}) }; return _currentConfig; } async function saveConfig(config) { _currentConfig = { ..._currentConfig, ...config }; await grist.setOption(WIDGET_CONFIG_KEY, _currentConfig); } async function clearConfig() { _currentConfig = { ...DEFAULTS }; await grist.setOption(WIDGET_CONFIG_KEY, null); } return { loadConfig, saveConfig, getConfig: () => _currentConfig, clearConfig }; })();
// --- DOM ELEMENTS & APP STATE ---
const dom = { body: document.body, errorMsg: document.getElementById('error-msg'), configScreen: document.getElementById('config-screen'), simulationScreen: document.getElementById('simulation-screen'), formulaSelect: document.getElementById('formula-column-select'), variablesStep: document.getElementById('variables-step'), variableConfigContainer: document.getElementById('variable-config-container'), saveConfigBtn: document.getElementById('save-config-btn'), resetConfigBtn: document.getElementById('reset-config-btn'), reconfigBtn: document.getElementById('reconfig-btn'), resultLabel: document.getElementById('result-label'), resultValue: document.getElementById('result-value'), slidersContainer: document.getElementById('sliders-container'), formulasContainer: document.getElementById('formulas-container'), chartContainer: document.getElementById('chart-container'), chartCanvas: document.getElementById('sensitivity-chart'), xAxisSelect: document.getElementById('xaxis-select'), showChartToggle: document.getElementById('show-chart-toggle'), compactModeToggle: document.getElementById('compact-mode-toggle'), widgetTitleInput: document.getElementById('widget-title-input'), titleColumnSelect: document.getElementById('title-column-select'), simulationTitle: document.getElementById('simulation-title'), saveToGristBtn: document.getElementById('save-to-grist-btn'), debugContainer: document.getElementById('debug-container'), debugLog: document.getElementById('debug-log'), debugModeToggle: document.getElementById('debug-mode-toggle'), clearLogBtn: document.getElementById('clear-log-btn') };
let state = { formula: null, evaluator: null, varIds: [], varMeta: {}, record: null, chart: null, isReady: false };
const log = (message, data = null) => { if (!WidgetConfigManager.getConfig().debugMode) return; const logEl = document.getElementById('debug-log'); const timestamp = new Date().toLocaleTimeString(); const entry = document.createElement('div'); entry.innerHTML = `<strong>[${timestamp}] ${message}</strong>`; if (data) { const dataEl = document.createElement('span'); dataEl.textContent = JSON.stringify(data, null, 2); entry.appendChild(dataEl); } logEl.appendChild(entry); logEl.scrollTop = logEl.scrollHeight; };
// --- CORE LOGIC (All stable, no changes) ---
const tokenizeFormula = (formula) => { const regex = /\$[\w_]+|-?\d*\.?\d+|\*\*|[()*/+-]/g; return formula.match(regex) || []; };
const parseFormulaForVariables = (formula) => { const matches = formula.match(/\$(\w+)/g); return matches ? [...new Set(matches.map(v => v.substring(1)))] : []; };
const buildFormulaEvaluator = (formula) => { if (!formula) return null; let body = formula.replace(/\$(\w+)/g, '$1'); try { return new Function('data', `with (data) { return ${body}; }`); } catch (e) { showError(`The formula "${formula}" appears to be invalid.`); return null; } };
const setupChart = () => { if (state.chart) state.chart.destroy(); const ctx = dom.chartCanvas.getContext('2d'); state.chart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'Result', data: [], borderColor: 'rgba(13, 110, 253, 0.8)', backgroundColor: 'rgba(13, 110, 253, 0.1)', fill: true, tension: 0.1, pointRadius: DEFAULT_RADIUS }] }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index' } } }); };
const updateChart = () => { if (!state.chart || !WidgetConfigManager.getConfig().showChart) { dom.chartContainer.classList.add('hidden'); return; } const xAxisVarId = dom.xAxisSelect.value; if (!xAxisVarId) { dom.chartContainer.classList.add('hidden'); return; } const config = WidgetConfigManager.getConfig(); const xAxisSettings = config.variableSettings[xAxisVarId]; if (!xAxisSettings || typeof xAxisSettings.min === 'undefined') return; const labels = [], dataPoints = []; const { min, max, step: userStep } = xAxisSettings; if (userStep <= 0) { dom.chartContainer.classList.add('hidden'); return; } const MAX_POINTS_TO_PLOT = 101; const totalPossiblePoints = Math.floor((max - min) / userStep) + 1; let effectiveStep = userStep; if (totalPossiblePoints > MAX_POINTS_TO_PLOT) { const samplingFactor = Math.ceil(totalPossiblePoints / MAX_POINTS_TO_PLOT); effectiveStep = userStep * samplingFactor; } const baseArgs = {}; state.varIds.forEach(varId => { if (varId === xAxisVarId) return; const settings = config.variableSettings[varId]; if (settings.isLocked) { baseArgs[varId] = settings.lockMode === 'table' && state.record ? state.record[varId] : settings.lockValue; } else { baseArgs[varId] = parseFloat(document.getElementById(`number-${varId}`).value); } }); for (let currentX = min; currentX <= max; currentX += effectiveStep) { labels.push(currentX); const loopArgs = { ...baseArgs, [xAxisVarId]: currentX }; try { dataPoints.push(state.evaluator(loopArgs)); } catch { dataPoints.push(null); } } const lastLabelValue = labels.length > 0 ? labels[labels.length - 1] : min; if (lastLabelValue < max) { labels.push(max); const finalArgs = { ...baseArgs, [xAxisVarId]: max }; try { dataPoints.push(state.evaluator(finalArgs)); } catch { dataPoints.push(null); } } const currentXValue = parseFloat(document.getElementById(`number-${xAxisVarId}`).value); let closestPointIndex = -1; if (labels.length > 0) { closestPointIndex = labels.reduce((closestIdx, currentVal, currentIdx) => { const closestDistance = Math.abs(labels[closestIdx] - currentXValue); const currentDistance = Math.abs(currentVal - currentXValue); return currentDistance < closestDistance ? currentIdx : closestIdx; }, 0); } const pointRadii = new Array(labels.length).fill(DEFAULT_RADIUS); const pointBackgroundColors = new Array(labels.length).fill(state.chart.data.datasets[0].borderColor); if (closestPointIndex !== -1) { pointRadii[closestPointIndex] = HIGHLIGHT_RADIUS; pointBackgroundColors[closestPointIndex] = HIGHLIGHT_COLOR; } state.chart.data.labels = labels.map(l => l.toLocaleString()); state.chart.data.datasets[0].data = dataPoints; state.chart.data.datasets[0].pointRadius = pointRadii; state.chart.data.datasets[0].pointBackgroundColor = pointBackgroundColors; const resultLabel = dom.resultLabel.textContent.split('(')[1]?.split(')')[0] || 'Result'; state.chart.data.datasets[0].label = `${resultLabel} vs ${state.varMeta[xAxisVarId].label}`; state.chart.update(); dom.chartContainer.classList.remove('hidden'); };
const recalculateAndDisplay = () => { if (!state.evaluator) return; const config = WidgetConfigManager.getConfig(); const argObject = {}; state.varIds.forEach(varId => { const settings = config.variableSettings[varId]; if (settings.isLocked) { argObject[varId] = settings.lockMode === 'table' && state.record ? state.record[varId] : settings.lockValue; } else { argObject[varId] = parseFloat(document.getElementById(`number-${varId}`).value); } }); try { const result = state.evaluator(argObject); dom.resultValue.textContent = typeof result === 'number' ? result.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(result); } catch (e) { dom.resultValue.textContent = "Calculation Error"; } updateLiveValuesDisplay(); updateChart(); };
window.handleSliderChange = recalculateAndDisplay;
const showScreen = (screen) => { dom.configScreen.classList.add('hidden'); dom.simulationScreen.classList.add('hidden'); if (screen === 'config') dom.configScreen.classList.remove('hidden'); else if (screen === 'simulation') dom.simulationScreen.classList.remove('hidden'); };
const showError = (message) => { dom.errorMsg.textContent = message; dom.errorMsg.classList.remove('hidden'); dom.configScreen.classList.add('hidden'); dom.simulationScreen.classList.add('hidden'); };
const renderFormulaPills = () => { dom.formulasContainer.innerHTML = `<div class="collapsible-header" data-target="formula-display-content" onclick="window.globalWidgetFunctions.toggleCollapse(this)"><span class="arrow">▼</span> Formula</div><div id="formula-display-content" class="collapsible-content"><div class="formula-display-box" id="formula-display-container"></div><div class="formula-display-box" id="live-values-container"></div></div>`; dom.formulaDisplay = document.getElementById('formula-display-container'); dom.liveValues = document.getElementById('live-values-container'); const parts = tokenizeFormula(state.formula); const varCounters = {}; parts.forEach(part => { let pill, op; if (part.startsWith('$')) { const varId = part.substring(1); const variable = state.varMeta[varId]; if (varCounters[varId] === undefined) { varCounters[varId] = 0; } if (variable) { pill = document.createElement('span'); pill.className = 'pill'; pill.textContent = variable.label; pill.style.backgroundColor = variable.color; dom.formulaDisplay.appendChild(pill); pill = document.createElement('span'); pill.className = 'pill'; pill.id = `live-pill-${varId}-${varCounters[varId]}`; pill.textContent = '...'; pill.style.backgroundColor = variable.color; dom.liveValues.appendChild(pill); varCounters[varId]++; } } else { op = document.createElement('span'); op.className = 'formula-operator'; op.textContent = ` ${part} `; dom.formulaDisplay.appendChild(op); op = document.createElement('span'); op.className = 'formula-operator'; op.textContent = ` ${part} `; dom.liveValues.appendChild(op); } }); };
const updateLiveValuesDisplay = () => { const config = WidgetConfigManager.getConfig(); state.varIds.forEach(varId => { let value; const settings = config.variableSettings[varId]; if (settings.isLocked) { value = settings.lockMode === 'table' && state.record ? state.record[varId] : settings.lockValue; } else { const numInput = document.getElementById(`number-${varId}`); value = numInput ? parseFloat(numInput.value) : 0; } const allPillsForVar = document.querySelectorAll(`[id^="live-pill-${varId}-"]`); allPillsForVar.forEach(pill => { pill.textContent = (value ?? 0).toLocaleString(); }); }); };
const buildVariableConfigUI = (variableIds, existingSettings = {}) => { dom.variableConfigContainer.innerHTML = ''; const tableMeta = GristDataManager.getTableMeta(); if (!variableIds.length) { dom.variablesStep.classList.add('hidden'); dom.saveConfigBtn.disabled = true; return; } variableIds.forEach(varId => { const column = tableMeta.columns.find(c => c.id === varId); if (!column) return; const settings = existingSettings[varId] || {}; const row = document.createElement('div'); row.className = 'variable-config-row'; row.dataset.variableId = varId; row.innerHTML = `<div class="variable-header"><strong>Variable: ${column.label}</strong><label class="config-toggle"><input type="checkbox" class="lock-checkbox" onchange="window.globalWidgetFunctions.handleLockChange(this)" ${settings.isLocked ? 'checked' : ''}> Lock</label></div><div class="variable-inputs"><div><label for="min-${varId}">Min</label><input type="number" id="min-${varId}" value="${settings.min ?? ''}" placeholder="e.g. 0"></div><div><label for="max-${varId}">Max</label><input type="number" id="max-${varId}" value="${settings.max ?? ''}" placeholder="e.g. 1000"></div><div><label for="step-${varId}">Step</label><input type="number" id="step-${varId}" value="${settings.step ?? ''}" placeholder="e.g. 1"></div></div><div class="lock-options ${settings.isLocked ? '' : 'hidden'}"><label><input type="radio" name="lock-mode-${varId}" value="table" ${settings.lockMode === 'custom' ? '' : 'checked'}> Use value from table</label><label style="display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem;"><input type="radio" name="lock-mode-${varId}" value="custom" ${settings.lockMode === 'custom' ? 'checked' : ''}> Use fixed value:<input type="number" class="lock-value-input" value="${settings.lockValue ?? 0}" style="flex-grow:1;"></label></div>`; dom.variableConfigContainer.appendChild(row); }); dom.variablesStep.classList.remove('hidden'); dom.saveConfigBtn.disabled = false; };
const initializeConfigScreen = (prefill = false) => { const tableMeta = GristDataManager.getTableMeta(); const config = WidgetConfigManager.getConfig(); const formulaColumns = tableMeta.columns.filter(c => c.isFormula); dom.formulaSelect.innerHTML = '<option value="">-- Please select a formula --</option>'; formulaColumns.forEach(col => dom.formulaSelect.add(new Option(`${col.label} (${col.id})`, col.id))); dom.titleColumnSelect.innerHTML = '<option value="">-- None (use Record ID) --</option>'; tableMeta.columns.forEach(col => { if (!col.isFormula) dom.titleColumnSelect.add(new Option(col.label, col.id)); }); dom.widgetTitleInput.value = config.widgetTitle; dom.titleColumnSelect.value = config.titleColumnId || ''; dom.showChartToggle.checked = config.showChart; dom.compactModeToggle.checked = config.compactMode; dom.debugModeToggle.checked = config.debugMode; if (prefill && config.formulaColumnId) { dom.formulaSelect.value = config.formulaColumnId; const formulaColumn = tableMeta.columns.find(c => c.id === config.formulaColumnId); buildVariableConfigUI(parseFormulaForVariables(formulaColumn.formula), config.variableSettings); } else { dom.variablesStep.classList.add('hidden'); dom.saveConfigBtn.disabled = true; } showScreen('config'); };
const initializeSimulationScreen = () => { const config = WidgetConfigManager.getConfig(); const tableMeta = GristDataManager.getTableMeta(); const formulaColumn = tableMeta.columns.find(c => c.id === config.formulaColumnId); dom.body.classList.toggle('compact', config.compactMode); dom.debugContainer.classList.toggle('hidden', !config.debugMode); if (config.compactMode) { const formulaLabel = formulaColumn.label.replace(/_/g, ' '); dom.resultLabel.textContent = `${formulaLabel} =`; } else { dom.resultLabel.textContent = `Result (${formulaColumn.label})`; } state.varIds = Object.keys(config.variableSettings); state.formula = formulaColumn.formula; state.varMeta = {}; state.varIds.forEach((varId, index) => { const column = tableMeta.columns.find(c => c.id === varId); state.varMeta[varId] = { id: varId, label: column.label, color: PALETTE[index % PALETTE.length] }; }); state.evaluator = buildFormulaEvaluator(state.formula); if (!state.evaluator) { WidgetConfigManager.clearConfig(); main(); return; } renderFormulaPills(); if (config.showChart) { dom.chartContainer.innerHTML = `<div class="collapsible-header" data-target="chart-content" onclick="window.globalWidgetFunctions.toggleCollapse(this)"><span class="arrow">▼</span> Analysis Chart</div><div id="chart-content" class="collapsible-content"><div id="chart-controls"><label for="xaxis-select">Analyze on X-Axis:</label><select id="xaxis-select"></select></div><div id="chart-wrapper"><canvas id="sensitivity-chart"></canvas></div></div>`; dom.xAxisSelect = document.getElementById('xaxis-select'); dom.chartCanvas = document.getElementById('sensitivity-chart'); state.varIds.forEach(varId => { if (!config.variableSettings[varId].isLocked) dom.xAxisSelect.add(new Option(state.varMeta[varId].label, varId)); }); dom.xAxisSelect.addEventListener('change', updateChart); setupChart(); } else { dom.chartContainer.innerHTML = ''; } dom.resultValue.textContent = "-- select a record --"; dom.slidersContainer.innerHTML = ''; state.varIds.forEach(varId => { const settings = config.variableSettings[varId]; if (settings.isLocked) return; const group = document.createElement('div'); group.className = 'slider-group'; group.innerHTML = `<label for="slider-${varId}" class="slider-label"><span class="pill" style="background-color: ${state.varMeta[varId].color};">${state.varMeta[varId].label}</span></label><div class="input-with-slider"><input type="range" id="slider-${varId}" data-variable-id="${varId}" min="${settings.min}" max="${settings.max}" step="${settings.step}" oninput="window.globalWidgetFunctions.updateSliderAndInput(this)"><input type="number" id="number-${varId}" data-variable-id="${varId}" min="${settings.min}" max="${settings.max}" step="${settings.step}" oninput="window.globalWidgetFunctions.updateSliderAndInput(this)"></div>`; dom.slidersContainer.appendChild(group); }); showScreen('simulation'); };
// --- EVENT HANDLERS ---
dom.formulaSelect.addEventListener('change', () => { const selectedColumnId = dom.formulaSelect.value; if (!selectedColumnId) { dom.variablesStep.classList.add('hidden'); dom.saveConfigBtn.disabled = true; return; } const formulaColumn = GristDataManager.getTableMeta().columns.find(c => c.id === selectedColumnId); buildVariableConfigUI(parseFormulaForVariables(formulaColumn.formula)); });
dom.saveConfigBtn.addEventListener('click', async () => { const formulaColumnId = dom.formulaSelect.value; const variableSettings = {}; let isValid = true; dom.variableConfigContainer.querySelectorAll('.variable-config-row').forEach(row => { const varId = row.dataset.variableId; const isLocked = row.querySelector('.lock-checkbox').checked; const minText = row.querySelector(`#min-${varId}`).value; const maxText = row.querySelector(`#max-${varId}`).value; const stepText = row.querySelector(`#step-${varId}`).value; const min = minText === '' ? 0 : parseFloat(minText); const max = maxText === '' ? 100 : parseFloat(maxText); const step = stepText === '' ? 1 : parseFloat(stepText); if (!isLocked && (isNaN(min) || isNaN(max) || isNaN(step))) { isValid = false; } variableSettings[varId] = { isLocked: isLocked, min: min, max: max, step: step, lockMode: row.querySelector('input[name="lock-mode-'+varId+'"]:checked').value, lockValue: parseFloat(row.querySelector('.lock-value-input').value) }; }); if (!isValid) { alert("Please ensure Min, Max, and Step are valid numbers for all unlocked variables."); return; } const showChart = dom.showChartToggle.checked; const compactMode = dom.compactModeToggle.checked; const widgetTitle = dom.widgetTitleInput.value; const titleColumnId = dom.titleColumnSelect.value; const debugMode = dom.debugModeToggle.checked; await WidgetConfigManager.saveConfig({ formulaColumnId, variableSettings, showChart, compactMode, widgetTitle, titleColumnId, debugMode }); initializeSimulationScreen(); if (state.record) onRecordHandler(state.record); });
dom.reconfigBtn.addEventListener('click', () => initializeConfigScreen(true));
dom.resetConfigBtn.addEventListener('click', async () => { if (confirm("Are you sure you want to start over? All settings will be lost.")) { await WidgetConfigManager.clearConfig(); initializeConfigScreen(false); } });
dom.clearLogBtn.addEventListener('click', () => { dom.debugLog.innerHTML = ''; });
dom.simulationScreen.addEventListener('click', (e) => { if (e.target.matches('.collapsible-header')) { window.globalWidgetFunctions.toggleCollapse(e.target); } });
dom.saveToGristBtn.addEventListener('click', async () => { if (!state.record) { alert("Please select a record in the Grist table first."); return; } const config = WidgetConfigManager.getConfig(); const table = grist.getTable(GristDataManager.getTableMeta().id); const valuesToUpdate = {}; let updatedCount = 0; state.varIds.forEach(varId => { const settings = config.variableSettings[varId]; if (!settings.isLocked) { const currentValue = parseFloat(document.getElementById(`number-${varId}`).value); if (state.record[varId] !== currentValue) { valuesToUpdate[varId] = currentValue; updatedCount++; } } }); if (updatedCount > 0) { try { await table.update({id: state.record.id, fields: valuesToUpdate}); dom.saveToGristBtn.innerHTML = `✓ Saved!`; setTimeout(() => { dom.saveToGristBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M2 1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H9.5a1 1 0 0 0-1 1v4.5h2a.5.5 0 0 1 .354.854l-2.5 2.5a.5.5 0 0 1-.708 0l-2.5-2.5A.5.5 0 0 1 5.5 6.5h2V2a2 2 0 0 1 2-2H14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h2.5a.5.5 0 0 1 0 1H2z"/></svg> Save to Grist`; }, 2000); } catch (err) { alert(`Error saving to Grist: ${err.message}`); } } else { alert("No changes to save. Slider values already match the Grist record."); } });
// --- GRIST LISTENERS & INITIALIZATION ---
const onRecordHandler = (record) => {
if (!state.isReady) { return; } // Do nothing if the widget isn't fully initialized
log("onRecordHandler received:", record);
state.record = record;
const config = WidgetConfigManager.getConfig();
const titleSuffix = config.titleColumnId && record ? record[config.titleColumnId] : (record ? `ID: ${record.id}`: '');
dom.simulationTitle.textContent = `${config.widgetTitle} - ${titleSuffix}`;
if (!record || dom.simulationScreen.classList.contains('hidden')) {
if(!dom.simulationScreen.classList.contains('hidden')) dom.resultValue.textContent = "-- select a record --";
dom.saveToGristBtn.disabled = true;
return;
}
dom.saveToGristBtn.disabled = false;
state.varIds.forEach(varId => {
log(`Processing variable: ${varId}`);
const settings = config.variableSettings[varId]; if (settings.isLocked) { log(` -> ${varId} is locked. Skipping slider update.`); return; }
const slider = document.getElementById(`slider-${varId}`); const numberInput = document.getElementById(`number-${varId}`);
if (slider && numberInput) {
const recordValue = record[varId] ?? 0;
log(` -> Value from record for ${varId}:`, recordValue);
const value = Math.max(settings.min, Math.min(settings.max, recordValue));
log(` -> Clamped value for ${varId}:`, value);
slider.value = value; numberInput.value = value;
} else { log(` -> Slider or number input not found for ${varId}`); }
});
recalculateAndDisplay();
};
// --- THIS IS THE NEW, ROBUST INITIALIZATION ---
async function main() {
dom.errorMsg.classList.add('hidden');
try {
// First, fetch all data and configuration. This is the critical step.
await GristDataManager.fetchAll();
const config = await WidgetConfigManager.loadConfig();
// Now that we have all the info, attach the event listeners.
grist.onRecord(onRecordHandler);
grist.onRecords(() => {
log("onRecords triggered. Re-initializing.");
main(); // Re-run the main function if the whole table changes
});
// Now, build the UI based on the loaded config.
if (config.formulaColumnId) {
initializeSimulationScreen();
} else {
initializeConfigScreen();
}
state.isReady = true; // Mark the widget as fully initialized
log("Widget is ready.");
} catch (e) {
showError(e.message);
}
}
grist.ready({ requiredAccess: 'full' });
main(); // Start the main initialization process
});