Spaces:
Sleeping
Sleeping
| import { buildAnalysisBody } from './schedule/analysis-modal.mjs'; | |
| import { createAppShell } from './shell/app-shell.mjs'; | |
| import { createAppState } from './shell/app-state.mjs'; | |
| import { loadAppConfig } from './shell/config-loader.mjs'; | |
| import { renderDataTables } from './shell/data-panel.mjs'; | |
| import { createSolverController } from './shell/solver-controller.mjs'; | |
| import { createViewRegistry } from './views/registry.mjs'; | |
| // Browser entrypoint that wires together config loading, the shared UI shell, | |
| // hospital-specific view renderers, and the retained-job controller. | |
| export async function bootApp(root = globalThis) { | |
| const document = root.document; | |
| const sf = root.SF; | |
| const appElement = document && document.getElementById('sf-app'); | |
| if (!document || !appElement) return null; | |
| if (!sf) { | |
| throw new Error('SolverForge UI must be loaded before bootApp()'); | |
| } | |
| const { config, uiModel, backend, demoId } = await loadAppConfig(root); | |
| const state = createAppState(uiModel.views[0].id); | |
| const statusBar = sf.createStatusBar({ constraints: uiModel.constraints }); | |
| const shell = createAppShell({ | |
| root, | |
| sf, | |
| appElement, | |
| config, | |
| uiModel, | |
| demoId, | |
| statusBar, | |
| activeTab: state.activeTab, | |
| actions: { | |
| onSolve: () => startSolve(), | |
| onPause: () => controller.pause(), | |
| onResume: () => controller.resume(), | |
| onCancel: () => controller.cancel(), | |
| onAnalyze: () => openAnalysis(), | |
| onTabChange(tabId) { | |
| state.activeTab = tabId; | |
| }, | |
| }, | |
| }); | |
| const views = createViewRegistry(); | |
| const controller = createSolverController({ | |
| sf, | |
| backend, | |
| statusBar, | |
| onPlan(plan) { | |
| renderAll(plan); | |
| }, | |
| onAnalysis() {}, | |
| onMeta() {}, | |
| onLifecycle(markers) { | |
| shell.syncLifecycleMarkers(markers); | |
| }, | |
| onError(error) { | |
| root.console.error('Solver lifecycle failed:', error); | |
| }, | |
| }); | |
| try { | |
| const demoData = await backend.getDemoData(demoId); | |
| renderAll(demoData); | |
| } catch (error) { | |
| root.console.error('Initial demo load failed:', error); | |
| } | |
| return { | |
| backend, | |
| controller, | |
| shell, | |
| state, | |
| uiModel, | |
| }; | |
| // Re-renders both schedule tabs and the raw data tables from the latest plan. | |
| function renderAll(data) { | |
| state.currentPlan = clonePlan(data); | |
| renderViews(data); | |
| renderDataTables({ sf, container: shell.dataRoot, uiModel, data }); | |
| } | |
| // Dispatches each configured view to the renderer registered for its `kind`. | |
| function renderViews(data) { | |
| uiModel.views.forEach((view) => { | |
| const container = shell.viewRoots[view.id]; | |
| const renderView = views[view.kind]; | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| if (!renderView) { | |
| container.appendChild(sf.el('p', null, `No renderer is registered for ${view.kind}.`)); | |
| return; | |
| } | |
| renderView({ sf, container, data, view }); | |
| }); | |
| } | |
| // Starts solving from the last rendered plan snapshot. | |
| async function startSolve() { | |
| const plan = await resolvePlanForSolve(); | |
| await controller.start(() => Promise.resolve(clonePlan(plan))); | |
| } | |
| // Lazily loads demo data if the user clicks Solve before the first fetch finishes. | |
| async function resolvePlanForSolve() { | |
| if (state.currentPlan) { | |
| return state.currentPlan; | |
| } | |
| const demoData = await backend.getDemoData(demoId); | |
| renderAll(demoData); | |
| return state.currentPlan; | |
| } | |
| // Fetches exact retained-snapshot analysis and opens it in the shared modal. | |
| async function openAnalysis() { | |
| if (!controller.getJobId()) return Promise.resolve(); | |
| try { | |
| const currentAnalysis = await controller.analyzeSnapshot(); | |
| shell.openAnalysis(buildAnalysisBody(document, currentAnalysis, uiModel.constraints)); | |
| } catch (error) { | |
| root.console.error('Analysis failed:', error); | |
| } | |
| } | |
| } | |
| // Defensive deep clone so the UI never mutates the last backend payload in place. | |
| function clonePlan(data) { | |
| return JSON.parse(JSON.stringify(data)); | |
| } | |