File size: 4,042 Bytes
7596726
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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));
}