This commit is contained in:
gsinghpal
2026-04-12 09:09:50 -04:00
parent d07159b9b5
commit be611876ad
470 changed files with 41761 additions and 51 deletions

View File

@@ -0,0 +1,668 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Fusion Claims — Workflow Explorer</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js"></script>
<script src="./workflows.js"></script>
<style>
:root {
--bg: #0f1115;
--panel: #161a22;
--panel-2: #1d2330;
--border: #2a3040;
--text: #e6e9ef;
--muted: #8a93a8;
--accent: #4f8cff;
--accent-soft: rgba(79,140,255,.15);
--ok: #3fbf7f;
--warn: #f4b400;
--err: #ff5c6c;
--entry: #9b5de5;
--terminal: #5f6b80;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; height: 100%; }
body { display: flex; min-height: 100vh; }
aside {
width: 280px; flex-shrink: 0;
background: var(--panel);
border-right: 1px solid var(--border);
padding: 20px 0;
overflow-y: auto;
}
aside h1 {
font-size: 13px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .08em;
margin: 0 20px 12px;
}
aside .subtitle {
font-size: 11px;
color: var(--muted);
margin: 0 20px 20px;
line-height: 1.4;
}
.wf-list { list-style: none; padding: 0; margin: 0; }
.wf-list li {
padding: 12px 20px;
cursor: pointer;
border-left: 3px solid transparent;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
transition: background .15s;
}
.wf-list li:hover { background: var(--panel-2); }
.wf-list li.active {
background: var(--accent-soft);
border-left-color: var(--accent);
}
.wf-list li .name { font-weight: 500; }
.wf-list li .gap-badge {
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
background: var(--err);
color: #fff;
min-width: 20px;
text-align: center;
}
.wf-list li .gap-badge.zero { background: var(--ok); }
main {
flex: 1;
padding: 32px 40px;
overflow-y: auto;
max-width: calc(100vw - 280px);
}
h2 {
margin: 0 0 4px;
font-size: 24px;
font-weight: 700;
}
.field-name {
color: var(--muted);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
margin-bottom: 20px;
}
.stats {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.stat {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
}
.stat .label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--muted);
}
.stat .value {
font-size: 22px;
font-weight: 700;
margin-top: 4px;
}
.stat.ok .value { color: var(--ok); }
.stat.err .value { color: var(--err); }
.stat.warn .value { color: var(--warn); }
.tabs {
display: flex;
gap: 2px;
border-bottom: 1px solid var(--border);
margin-bottom: 20px;
}
.tabs button {
background: transparent;
border: none;
color: var(--muted);
font: inherit;
padding: 10px 16px;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
font-weight: 500;
}
.tabs button:hover { color: var(--text); }
.tabs button.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
.mermaid-wrap {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 24px;
overflow-x: auto;
}
.mermaid-wrap svg { max-width: 100%; height: auto; }
table {
width: 100%;
border-collapse: collapse;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
th, td {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid var(--border);
font-size: 13px;
}
th {
background: var(--panel-2);
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--muted);
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--panel-2); }
tbody tr.has-issue td:first-child { border-left: 3px solid var(--err); }
code.state-key {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
background: var(--panel-2);
padding: 2px 6px;
border-radius: 4px;
color: var(--accent);
}
.pill {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .03em;
}
.pill.ok { background: rgba(63,191,127,.2); color: var(--ok); }
.pill.warn { background: rgba(244,180,0,.2); color: var(--warn); }
.pill.err { background: rgba(255,92,108,.2); color: var(--err); }
.pill.entry { background: rgba(155,93,229,.2); color: var(--entry); }
.pill.terminal { background: rgba(95,107,128,.35); color: #c2c9d9; }
.count {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
color: var(--muted);
}
.tr-list { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 8px; }
.tr-row {
display: grid;
grid-template-columns: 200px 30px 200px 1fr 140px;
gap: 12px;
padding: 10px 12px;
align-items: center;
border-bottom: 1px solid var(--border);
font-size: 13px;
}
.tr-row:last-child { border-bottom: none; }
.tr-row:hover { background: var(--panel-2); }
.tr-arrow { color: var(--muted); text-align: center; }
.tr-trigger { color: var(--muted); font-family: ui-monospace, monospace; font-size: 12px; word-break: break-all; }
.tr-trigger .file { color: #555; display: block; margin-top: 2px; }
.tr-kind {
text-align: right;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .04em;
}
.kind-wizard { color: #4f8cff; }
.kind-action_method { color: #9b5de5; }
.kind-cron { color: #f4b400; }
.kind-auto_write { color: #3fbf7f; }
.kind-ui_button { color: #ff5c6c; }
.gaps {
background: var(--panel);
border: 1px solid var(--border);
border-left: 4px solid var(--err);
border-radius: 8px;
padding: 20px 24px;
margin-bottom: 20px;
}
.gaps.zero { border-left-color: var(--ok); }
.gaps h3 {
margin: 0 0 12px;
font-size: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.gaps p { color: var(--muted); margin: 0; }
.gaps ul { margin: 0; padding-left: 20px; }
.gaps li { padding: 4px 0; font-size: 13px; }
.gaps li code {
font-family: ui-monospace, monospace;
background: var(--panel-2);
padding: 1px 5px;
border-radius: 3px;
color: var(--accent);
font-size: 12px;
}
.gap-kind {
display: inline-block;
font-size: 10px;
text-transform: uppercase;
font-weight: 700;
padding: 1px 6px;
border-radius: 3px;
margin-right: 6px;
letter-spacing: .05em;
}
.gap-kind.unreachable { background: rgba(244,180,0,.25); color: var(--warn); }
.gap-kind.dead-end { background: rgba(255,92,108,.25); color: var(--err); }
.gap-kind.missing-path { background: rgba(79,140,255,.25); color: var(--accent); }
.gap-kind.hold-loss { background: rgba(155,93,229,.25); color: var(--entry); }
.legend {
display: flex;
gap: 16px;
flex-wrap: wrap;
font-size: 12px;
color: var(--muted);
margin-bottom: 8px;
}
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.legend .swatch { width: 10px; height: 10px; border-radius: 2px; }
</style>
</head>
<body>
<aside>
<h1>Fusion Claims</h1>
<p class="subtitle">Workflow Explorer — 5 parallel state machines on <code style="color:var(--accent)">sale.order</code>. Click a workflow to inspect.</p>
<ul class="wf-list" id="wf-list"></ul>
</aside>
<main id="main">
<div id="wf-content"></div>
</main>
<script>
// ============================================================
// DOM helpers — no innerHTML, all createElement / textContent
// ============================================================
function el(tag, opts, children) {
const node = document.createElement(tag);
if (opts) {
if (opts.class) node.className = opts.class;
if (opts.id) node.id = opts.id;
if (opts.text != null) node.textContent = opts.text;
if (opts.style) Object.assign(node.style, opts.style);
if (opts.data) Object.entries(opts.data).forEach(([k,v]) => node.dataset[k] = v);
if (opts.on) Object.entries(opts.on).forEach(([evt,fn]) => node.addEventListener(evt, fn));
}
if (children) {
(Array.isArray(children) ? children : [children]).forEach(c => {
if (c == null) return;
node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
});
}
return node;
}
// Render a string that may contain <code>...</code> spans safely.
// Splits on our own markers and builds real DOM nodes.
function renderSafeInline(parent, text) {
// Only recognise <code>...</code> — everything else is literal text.
const parts = text.split(/(<code>[^<]*<\/code>)/);
parts.forEach(part => {
if (part.startsWith('<code>') && part.endsWith('</code>')) {
const codeText = part.slice(6, -7);
parent.appendChild(el('code', {text: codeText}));
} else if (part) {
parent.appendChild(document.createTextNode(part));
}
});
}
// ============================================================
// Gap analysis
// ============================================================
function analyseWorkflow(wf) {
const stateKeys = wf.states.map(s => s.key);
const inbound = new Map();
const outbound = new Map();
stateKeys.forEach(k => { inbound.set(k, []); outbound.set(k, []); });
wf.transitions.forEach(t => {
if (t.to && inbound.has(t.to)) inbound.get(t.to).push(t);
if (t.from && t.from !== '*' && outbound.has(t.from)) outbound.get(t.from).push(t);
});
const wildcardInTo = new Set();
wf.transitions.forEach(t => { if (t.from === '*') wildcardInTo.add(t.to); });
const terminal = new Set(wf.terminal || []);
const gaps = [];
const stateStatus = {};
stateKeys.forEach(key => {
const label = wf.states.find(s => s.key === key).label;
const isDefault = key === wf.default;
const isTerminal = terminal.has(key);
const hasInbound = inbound.get(key).length > 0 || wildcardInTo.has(key);
const hasOutbound = outbound.get(key).length > 0 || wf.transitions.some(t => t.from === key);
let status = 'ok';
const issues = [];
if (!isDefault && !hasInbound) {
status = 'err';
issues.push({kind: 'unreachable', msg: 'No code path sets this state. It will never be reached via normal workflow — only via manual DB edit or stale ORM context.'});
}
if (!isTerminal && !hasOutbound && !isDefault) {
status = 'err';
issues.push({kind: 'dead-end', msg: 'Once an order lands here, there is no action method or wizard to transition it out. Users will have to edit the record directly.'});
}
stateStatus[key] = {
status, issues, isDefault, isTerminal,
inbound: inbound.get(key),
outbound: wf.transitions.filter(t => t.from === key)
};
issues.forEach(iss => gaps.push({state: key, label, ...iss}));
});
// Workflow-specific heuristics
if (wf.field === 'x_fc_adp_application_status') {
if (!wf.transitions.some(t => t.to === 'rejected')) {
gaps.push({kind: 'missing-path', state: 'rejected', label: 'Rejected by ADP',
msg: 'No transition writes <code>rejected</code>. The state is declared but nothing reaches it. An ADP rejection has nowhere to land.'});
}
if (!wf.transitions.some(t => t.from === 'rejected')) {
gaps.push({kind: 'missing-path', state: 'rejected', label: 'Rejected by ADP',
msg: 'No <code>action_resubmit_from_rejected</code> exists (only <code>action_resubmit_from_withdrawn</code>). A rejected application cannot be brought back into the workflow.'});
}
if (!wf.transitions.some(t => t.to === 'denied')) {
gaps.push({kind: 'missing-path', state: 'denied', label: 'Application Denied',
msg: 'No code path sets <code>denied</code>. Declared as a selection value but has no action method to assign it.'});
}
if (!wf.transitions.some(t => t.to === 'expired')) {
gaps.push({kind: 'missing-path', state: 'expired', label: 'Application Expired',
msg: 'No cron or method sets <code>expired</code>. Declared but unreachable — the ADP expiry logic was never implemented.'});
}
if (!wf.transitions.some(t => t.to === 'cancelled')) {
gaps.push({kind: 'missing-path', state: 'cancelled', label: 'Cancelled',
msg: 'No action method writes <code>cancelled</code> on the ADP workflow.'});
}
if (!wf.transitions.some(t => t.to === 'withdrawn')) {
gaps.push({kind: 'missing-path', state: 'withdrawn', label: 'Withdrawn',
msg: '<code>action_resubmit_from_withdrawn</code> exists (line 3667) but no method WRITES <code>withdrawn</code> in the first place. Dead end on entry.'});
}
if (!wf.transitions.some(t => t.to === 'needs_correction')) {
gaps.push({kind: 'missing-path', state: 'needs_correction', label: 'Needs Correction',
msg: 'The write() override at line 6017 handles <code>needs_correction</code> document-clearing logic, but no code path sets the state TO <code>needs_correction</code>. Only reachable via manual edit.'});
}
}
if (wf.field === 'x_fc_mod_status') {
if (!wf.transitions.some(t => t.from === 'funding_denied')) {
gaps.push({kind: 'dead-end', state: 'funding_denied', label: 'Denied',
msg: 'No way to revive a denied MOD case. No resubmit, no cancellation path. Once denied, the order is stuck unless someone edits <code>x_fc_mod_status</code> directly.'});
}
}
if (['x_fc_sa_status', 'x_fc_odsp_std_status', 'x_fc_ow_status'].includes(wf.field)) {
const resume = wf.transitions.find(t => t.from === 'on_hold');
if (resume && resume.to === 'quotation') {
gaps.push({kind: 'hold-loss', state: 'on_hold', label: 'On Hold',
msg: '<code>action_odsp_resume</code> always resumes to <code>quotation</code>, losing all progress regardless of where the order was put on hold. An order held at <code>ready_delivery</code> is reset to the start.'});
}
if (!wf.transitions.some(t => t.from === 'denied')) {
gaps.push({kind: 'dead-end', state: 'denied', label: 'Denied',
msg: 'No path out of <code>denied</code>. Once set, the case is stuck.'});
}
}
return {gaps, stateStatus};
}
// ============================================================
// Mermaid flowchart builder — produces plain text, Mermaid parses it.
// ============================================================
function buildMermaid(wf, stateStatus) {
const lines = ['flowchart LR'];
wf.states.forEach(s => {
const st = stateStatus[s.key];
const safeLabel = s.label.replace(/"/g, '&quot;');
const shape = st.isTerminal ? `(("${safeLabel}"))` :
st.isDefault ? `(["${safeLabel}"])` :
`["${safeLabel}"]`;
lines.push(` ${s.key}${shape}`);
});
const seen = new Set();
wf.transitions.forEach(t => {
if (t.from === '*') return;
const key = `${t.from}->${t.to}`;
if (seen.has(key)) return;
seen.add(key);
lines.push(` ${t.from} --> ${t.to}`);
});
wf.states.forEach(s => {
const st = stateStatus[s.key];
let cls = 'ok';
if (st.status === 'err') {
if (st.issues.some(i => i.kind === 'unreachable')) cls = 'unreachable';
else cls = 'deadend';
} else if (st.isDefault) cls = 'entry';
else if (st.isTerminal) cls = 'terminal';
lines.push(` class ${s.key} ${cls}`);
});
lines.push(' classDef ok fill:#1d2330,stroke:#3fbf7f,color:#e6e9ef,stroke-width:1.5px');
lines.push(' classDef entry fill:#2b1d40,stroke:#9b5de5,color:#e6e9ef,stroke-width:2.5px');
lines.push(' classDef terminal fill:#1a2030,stroke:#5f6b80,color:#c2c9d9,stroke-width:1.5px');
lines.push(' classDef unreachable fill:#2a2418,stroke:#f4b400,color:#f4b400,stroke-width:2px,stroke-dasharray:5 3');
lines.push(' classDef deadend fill:#2a1820,stroke:#ff5c6c,color:#ff5c6c,stroke-width:2px');
return lines.join('\n');
}
// ============================================================
// Renderer — DOM-based, no innerHTML
// ============================================================
const wfData = window.WORKFLOWS_DATA;
const wfKeys = Object.keys(wfData);
let activeWf = wfKeys[0];
let activeTab = 'flow';
function renderSidebar() {
const list = document.getElementById('wf-list');
while (list.firstChild) list.removeChild(list.firstChild);
wfKeys.forEach(k => {
const wf = wfData[k];
const {gaps} = analyseWorkflow(wf);
const li = el('li', {
class: k === activeWf ? 'active' : '',
on: {click: () => { activeWf = k; activeTab = 'flow'; renderSidebar(); renderContent(); }}
}, [
el('span', {class: 'name', text: wf.label}),
el('span', {class: 'gap-badge' + (gaps.length === 0 ? ' zero' : ''), text: String(gaps.length)})
]);
list.appendChild(li);
});
}
function makeStat(label, value, cls) {
return el('div', {class: 'stat' + (cls ? ' ' + cls : '')}, [
el('div', {class: 'label', text: label}),
el('div', {class: 'value', text: String(value)})
]);
}
function makeGapListItem(g) {
const li = el('li');
const kind = el('span', {class: 'gap-kind ' + g.kind, text: g.kind.replace('-', ' ')});
li.appendChild(kind);
const strong = el('strong', {text: g.label});
li.appendChild(strong);
li.appendChild(document.createTextNode(' — '));
renderSafeInline(li, g.msg);
return li;
}
function renderContent() {
const wf = wfData[activeWf];
const {gaps, stateStatus} = analyseWorkflow(wf);
const container = document.getElementById('wf-content');
while (container.firstChild) container.removeChild(container.firstChild);
const wizardCount = wf.transitions.filter(t => t.kind === 'wizard').length;
const cronCount = wf.transitions.filter(t => t.kind === 'cron').length;
const autoCount = wf.transitions.filter(t => t.kind === 'auto_write').length;
container.appendChild(el('h2', {text: wf.label}));
const fn = el('div', {class: 'field-name'});
fn.appendChild(document.createTextNode(wf.field + ' · default: '));
fn.appendChild(el('code', {text: wf.default}));
container.appendChild(fn);
const stats = el('div', {class: 'stats'}, [
makeStat('States', wf.states.length),
makeStat('Transitions', wf.transitions.length),
makeStat('Gaps', gaps.length, gaps.length === 0 ? 'ok' : 'err'),
makeStat('Wizards', wizardCount),
makeStat('Crons / Auto', cronCount + autoCount)
]);
container.appendChild(stats);
// Gaps panel
const gapsBox = el('div', {class: 'gaps' + (gaps.length === 0 ? ' zero' : '')});
gapsBox.appendChild(el('h3', {text: gaps.length === 0
? '\u2713 No gaps detected'
: '\u26A0 ' + gaps.length + ' gap' + (gaps.length === 1 ? '' : 's') + ' detected'}));
if (gaps.length === 0) {
gapsBox.appendChild(el('p', {text: 'This workflow has full coverage: every declared state is reachable, every non-terminal state has an exit, and all transitions are backed by code paths.'}));
} else {
const ul = el('ul');
gaps.forEach(g => ul.appendChild(makeGapListItem(g)));
gapsBox.appendChild(ul);
}
container.appendChild(gapsBox);
// Tabs
const tabs = el('div', {class: 'tabs'});
const tabDefs = [
{key: 'flow', label: 'Flowchart'},
{key: 'states', label: 'States (' + wf.states.length + ')'},
{key: 'transitions', label: 'Transitions (' + wf.transitions.length + ')'}
];
tabDefs.forEach(t => {
tabs.appendChild(el('button', {
class: activeTab === t.key ? 'active' : '',
text: t.label,
on: {click: () => { activeTab = t.key; renderContent(); }}
}));
});
container.appendChild(tabs);
// Flow tab
if (activeTab === 'flow') {
const legend = el('div', {class: 'legend'}, [
el('span', null, [el('span', {class: 'swatch', style: {background: '#9b5de5'}}), 'Entry state']),
el('span', null, [el('span', {class: 'swatch', style: {background: '#3fbf7f'}}), 'Healthy']),
el('span', null, [el('span', {class: 'swatch', style: {background: '#f4b400'}}), 'Unreachable']),
el('span', null, [el('span', {class: 'swatch', style: {background: '#ff5c6c'}}), 'Dead-end']),
el('span', null, [el('span', {class: 'swatch', style: {background: '#5f6b80'}}), 'Terminal'])
]);
container.appendChild(legend);
const wrap = el('div', {class: 'mermaid-wrap'});
const mm = el('div', {class: 'mermaid', id: 'mermaid-' + activeWf});
mm.textContent = buildMermaid(wf, stateStatus);
wrap.appendChild(mm);
container.appendChild(wrap);
// Render mermaid async
mermaid.initialize({startOnLoad: false, theme: 'base', securityLevel: 'strict', themeVariables: {
background: '#161a22', primaryColor: '#1d2330', primaryTextColor: '#e6e9ef',
primaryBorderColor: '#3fbf7f', lineColor: '#4f8cff'
}});
const src = mm.textContent;
const renderId = 'mm-svg-' + activeWf + '-' + Date.now();
mermaid.render(renderId, src).then(result => {
while (mm.firstChild) mm.removeChild(mm.firstChild);
// mermaid.render returns an SVG string — parse via DOMParser, no innerHTML
const doc = new DOMParser().parseFromString(result.svg, 'image/svg+xml');
const svgNode = doc.documentElement;
mm.appendChild(document.importNode(svgNode, true));
}).catch(err => {
while (mm.firstChild) mm.removeChild(mm.firstChild);
const pre = el('pre', {style: {color: 'var(--err)', whiteSpace: 'pre-wrap'}});
pre.textContent = 'Mermaid error: ' + err.message + '\n\n' + src;
mm.appendChild(pre);
});
}
// States tab
if (activeTab === 'states') {
const table = el('table');
const thead = el('thead');
const headRow = el('tr');
['State', 'Key', 'Status', 'In', 'Out'].forEach(h => headRow.appendChild(el('th', {text: h})));
thead.appendChild(headRow);
table.appendChild(thead);
const tbody = el('tbody');
wf.states.forEach(s => {
const st = stateStatus[s.key];
let pillClass = 'ok', pillLabel = 'Healthy';
if (st.isDefault) { pillClass = 'entry'; pillLabel = 'Entry'; }
else if (st.isTerminal) { pillClass = 'terminal'; pillLabel = 'Terminal'; }
if (st.status === 'err') {
if (st.issues.some(i => i.kind === 'unreachable')) { pillClass = 'warn'; pillLabel = 'Unreachable'; }
else { pillClass = 'err'; pillLabel = 'Dead-end'; }
}
const tr = el('tr', {class: st.status === 'err' ? 'has-issue' : ''});
tr.appendChild(el('td', null, [el('strong', {text: s.label})]));
tr.appendChild(el('td', null, [el('code', {class: 'state-key', text: s.key})]));
tr.appendChild(el('td', null, [el('span', {class: 'pill ' + pillClass, text: pillLabel})]));
tr.appendChild(el('td', {class: 'count', text: String(st.inbound.length)}));
tr.appendChild(el('td', {class: 'count', text: String(st.outbound.length)}));
tbody.appendChild(tr);
});
table.appendChild(tbody);
container.appendChild(table);
}
// Transitions tab
if (activeTab === 'transitions') {
const list = el('div', {class: 'tr-list'});
wf.transitions.forEach(t => {
const row = el('div', {class: 'tr-row'});
row.appendChild(el('div', null, [el('code', {class: 'state-key', text: t.from})]));
row.appendChild(el('div', {class: 'tr-arrow', text: '\u2192'}));
row.appendChild(el('div', null, [el('code', {class: 'state-key', text: t.to})]));
const trig = el('div', {class: 'tr-trigger'});
trig.appendChild(document.createTextNode(t.trigger));
const fileLine = el('span', {class: 'file', text: t.file + (t.line ? ':' + t.line : '')});
trig.appendChild(fileLine);
row.appendChild(trig);
const kind = el('div', {class: 'tr-kind'});
kind.appendChild(el('span', {class: 'kind-' + t.kind, text: t.kind.replace('_', ' ')}));
row.appendChild(kind);
list.appendChild(row);
});
container.appendChild(list);
}
}
renderSidebar();
renderContent();
</script>
</body>
</html>

View File

@@ -0,0 +1,197 @@
// Workflow data extracted from fusion_claims/models/sale_order.py and wizard/*.py
// Generated 2026-04-08. If the code changes, regenerate this file.
window.WORKFLOWS_DATA = {
"adp_application": {
"field": "x_fc_adp_application_status",
"label": "ADP Application",
"default": "quotation",
"terminal": ["case_closed", "cancelled"],
"states": [
{"key": "quotation", "label": "Quotation Stage"},
{"key": "assessment_scheduled", "label": "Assessment Scheduled"},
{"key": "assessment_completed", "label": "Assessment Completed"},
{"key": "waiting_for_application", "label": "Waiting for Application"},
{"key": "application_received", "label": "Application Received"},
{"key": "ready_submission", "label": "Ready for Submission"},
{"key": "submitted", "label": "Application Submitted"},
{"key": "accepted", "label": "Accepted by ADP"},
{"key": "rejected", "label": "Rejected by ADP"},
{"key": "resubmitted", "label": "Application Resubmitted"},
{"key": "needs_correction", "label": "Needs Correction"},
{"key": "approved", "label": "Application Approved"},
{"key": "approved_deduction", "label": "Approved with Deduction"},
{"key": "ready_delivery", "label": "Ready for Delivery"},
{"key": "denied", "label": "Application Denied"},
{"key": "withdrawn", "label": "Application Withdrawn"},
{"key": "ready_bill", "label": "Ready to Bill"},
{"key": "billed", "label": "Billed to ADP"},
{"key": "case_closed", "label": "Case Closed"},
{"key": "on_hold", "label": "On Hold"},
{"key": "cancelled", "label": "Cancelled"},
{"key": "expired", "label": "Application Expired"}
],
"transitions": [
{"from": "quotation", "to": "assessment_scheduled", "trigger": "schedule_assessment_wizard.action_schedule", "file": "wizard/schedule_assessment_wizard.py", "line": 118, "kind": "wizard"},
{"from": "assessment_scheduled", "to": "assessment_completed", "trigger": "assessment_completed_wizard.action_confirm", "file": "wizard/assessment_completed_wizard.py", "line": 105, "kind": "wizard"},
{"from": "assessment_completed", "to": "waiting_for_application", "trigger": "auto-transition on status_email write()", "file": "models/sale_order.py", "line": 6017, "kind": "auto_write"},
{"from": "assessment_completed", "to": "application_received", "trigger": "application_received_wizard.action_confirm", "file": "wizard/application_received_wizard.py", "line": 136, "kind": "wizard"},
{"from": "waiting_for_application", "to": "application_received", "trigger": "application_received_wizard.action_confirm", "file": "wizard/application_received_wizard.py", "line": 136, "kind": "wizard"},
{"from": "application_received", "to": "ready_submission", "trigger": "ready_for_submission_wizard.action_confirm", "file": "wizard/ready_for_submission_wizard.py", "line": 159, "kind": "wizard"},
{"from": "ready_submission", "to": "submitted", "trigger": "submission_verification_wizard.action_confirm_submission", "file": "wizard/submission_verification_wizard.py", "line": 288, "kind": "wizard"},
{"from": "needs_correction", "to": "resubmitted", "trigger": "submission_verification_wizard.action_confirm_submission", "file": "wizard/submission_verification_wizard.py", "line": 288, "kind": "wizard"},
{"from": "submitted", "to": "accepted", "trigger": "action_mark_accepted", "file": "models/sale_order.py", "line": 3563, "kind": "action_method"},
{"from": "resubmitted", "to": "accepted", "trigger": "action_mark_accepted", "file": "models/sale_order.py", "line": 3563, "kind": "action_method"},
{"from": "submitted", "to": "approved", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
{"from": "resubmitted", "to": "approved", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
{"from": "accepted", "to": "approved", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
{"from": "submitted", "to": "approved_deduction", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
{"from": "resubmitted", "to": "approved_deduction", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
{"from": "accepted", "to": "approved_deduction", "trigger": "device_approval_wizard.action_confirm", "file": "wizard/device_approval_wizard.py", "line": 290, "kind": "wizard"},
{"from": "approved", "to": "ready_delivery", "trigger": "ready_for_delivery_wizard.action_confirm", "file": "wizard/ready_for_delivery_wizard.py", "line": 108, "kind": "wizard"},
{"from": "approved_deduction", "to": "ready_delivery", "trigger": "ready_for_delivery_wizard.action_confirm", "file": "wizard/ready_for_delivery_wizard.py", "line": 108, "kind": "wizard"},
{"from": "*", "to": "ready_delivery", "trigger": "technician_task complete", "file": "models/technician_task.py", "line": 228, "kind": "auto_write"},
{"from": "ready_delivery", "to": "approved", "trigger": "technician_task cancel", "file": "models/technician_task.py", "line": 327, "kind": "auto_write"},
{"from": "ready_delivery", "to": "ready_bill", "trigger": "ready_to_bill_wizard.action_confirm", "file": "wizard/ready_to_bill_wizard.py", "line": null, "kind": "wizard"},
{"from": "ready_bill", "to": "billed", "trigger": "adp_export_wizard.action_export", "file": "wizard/adp_export_wizard.py", "line": null, "kind": "wizard"},
{"from": "billed", "to": "case_closed", "trigger": "_cron_auto_close_billed_cases", "file": "models/sale_order.py", "line": 6852, "kind": "cron"},
{"from": "withdrawn", "to": "ready_submission", "trigger": "action_resubmit_from_withdrawn", "file": "models/sale_order.py", "line": 3667, "kind": "action_method"}
]
},
"mod": {
"field": "x_fc_mod_status",
"label": "March of Dimes",
"default": "need_to_schedule",
"terminal": ["case_closed", "cancelled"],
"states": [
{"key": "need_to_schedule", "label": "Schedule Assessment"},
{"key": "assessment_scheduled", "label": "Assessment Booked"},
{"key": "assessment_completed", "label": "Assessment Done"},
{"key": "processing_drawings", "label": "Processing Drawing"},
{"key": "quote_submitted", "label": "Quote Sent"},
{"key": "awaiting_funding", "label": "Awaiting Funding"},
{"key": "funding_approved", "label": "Approved"},
{"key": "funding_denied", "label": "Denied"},
{"key": "contract_received", "label": "PCA Received"},
{"key": "in_production", "label": "In Production"},
{"key": "project_complete", "label": "Complete"},
{"key": "pod_submitted", "label": "POD Sent"},
{"key": "case_closed", "label": "Closed"},
{"key": "on_hold", "label": "On Hold"},
{"key": "cancelled", "label": "Cancelled"}
],
"transitions": [
{"from": "need_to_schedule", "to": "assessment_scheduled", "trigger": "action_mod_schedule_assessment", "file": "models/sale_order.py", "line": 7018, "kind": "action_method"},
{"from": "assessment_scheduled", "to": "assessment_completed", "trigger": "action_mod_complete_assessment", "file": "models/sale_order.py", "line": 7025, "kind": "action_method"},
{"from": "assessment_completed", "to": "processing_drawings", "trigger": "action_mod_processing_drawing", "file": "models/sale_order.py", "line": 7035, "kind": "action_method"},
{"from": "processing_drawings", "to": "quote_submitted", "trigger": "send_to_mod_wizard.action_send (quote)", "file": "wizard/send_to_mod_wizard.py", "line": 203, "kind": "wizard"},
{"from": "quote_submitted", "to": "awaiting_funding", "trigger": "mod_awaiting_funding_wizard.action_confirm", "file": "wizard/mod_awaiting_funding_wizard.py", "line": 34, "kind": "wizard"},
{"from": "awaiting_funding", "to": "funding_approved", "trigger": "mod_funding_approved_wizard.action_confirm", "file": "wizard/mod_funding_approved_wizard.py", "line": 48, "kind": "wizard"},
{"from": "awaiting_funding", "to": "funding_denied", "trigger": "action_mod_funding_denied", "file": "models/sale_order.py", "line": 7076, "kind": "action_method"},
{"from": "funding_approved", "to": "contract_received", "trigger": "mod_pca_received_wizard.action_confirm", "file": "wizard/mod_pca_received_wizard.py", "line": 143, "kind": "wizard"},
{"from": "contract_received", "to": "in_production", "trigger": "action_mod_in_production", "file": "models/sale_order.py", "line": 7093, "kind": "action_method"},
{"from": "in_production", "to": "project_complete", "trigger": "action_mod_project_complete", "file": "models/sale_order.py", "line": 7100, "kind": "action_method"},
{"from": "project_complete", "to": "pod_submitted", "trigger": "send_to_mod_wizard.action_send (pod)", "file": "wizard/send_to_mod_wizard.py", "line": 221, "kind": "wizard"},
{"from": "pod_submitted", "to": "case_closed", "trigger": "action_mod_close_case", "file": "models/sale_order.py", "line": 7123, "kind": "action_method"},
{"from": "*", "to": "on_hold", "trigger": "action_mod_on_hold", "file": "models/sale_order.py", "line": 7129, "kind": "action_method"},
{"from": "on_hold", "to": "in_production", "trigger": "action_mod_resume", "file": "models/sale_order.py", "line": 7134, "kind": "action_method"},
{"from": "*", "to": "cancelled", "trigger": "action_cancel", "file": "models/sale_order.py", "line": 7142, "kind": "action_method"}
]
},
"sa_mobility": {
"field": "x_fc_sa_status",
"label": "SA Mobility",
"default": "quotation",
"terminal": ["case_closed", "cancelled"],
"states": [
{"key": "quotation", "label": "Quotation"},
{"key": "form_ready", "label": "SA Form Ready"},
{"key": "submitted_to_sa", "label": "Submitted to SA Mobility"},
{"key": "pre_approved", "label": "Pre-Approved"},
{"key": "ready_delivery", "label": "Ready for Delivery"},
{"key": "delivered", "label": "Delivered"},
{"key": "pod_submitted", "label": "POD Submitted"},
{"key": "payment_received", "label": "Payment Received"},
{"key": "case_closed", "label": "Case Closed"},
{"key": "on_hold", "label": "On Hold"},
{"key": "cancelled", "label": "Cancelled"},
{"key": "denied", "label": "Denied"}
],
"transitions": [
{"from": "quotation", "to": "form_ready", "trigger": "odsp_sa_mobility_wizard.action_confirm", "file": "wizard/odsp_sa_mobility_wizard.py", "line": null, "kind": "wizard"},
{"from": "form_ready", "to": "submitted_to_sa", "trigger": "odsp_submit_to_odsp_wizard.action_confirm", "file": "wizard/odsp_submit_to_odsp_wizard.py", "line": 105, "kind": "wizard"},
{"from": "submitted_to_sa", "to": "pre_approved", "trigger": "odsp_pre_approved_wizard.action_confirm", "file": "wizard/odsp_pre_approved_wizard.py", "line": 68, "kind": "wizard"},
{"from": "pre_approved", "to": "ready_delivery", "trigger": "odsp_ready_delivery_wizard.action_confirm", "file": "wizard/odsp_ready_delivery_wizard.py", "line": 170, "kind": "wizard"},
{"from": "ready_delivery", "to": "delivered", "trigger": "_odsp_advance_status('delivered')", "file": "models/sale_order.py", "line": 1212, "kind": "auto_write"},
{"from": "delivered", "to": "pod_submitted", "trigger": "_odsp_advance_status('pod_submitted')", "file": "models/sale_order.py", "line": 1225, "kind": "auto_write"},
{"from": "pod_submitted", "to": "payment_received", "trigger": "invoice payment posted", "file": "models/account_move.py", "line": 59, "kind": "auto_write"},
{"from": "payment_received", "to": "case_closed", "trigger": "_cron_auto_close_odsp_paid_cases", "file": "models/sale_order.py", "line": 6899, "kind": "cron"},
{"from": "*", "to": "on_hold", "trigger": "action_odsp_on_hold", "file": "models/sale_order.py", "line": 1396, "kind": "action_method"},
{"from": "on_hold", "to": "quotation", "trigger": "action_odsp_resume", "file": "models/sale_order.py", "line": 1401, "kind": "action_method"},
{"from": "*", "to": "denied", "trigger": "action_odsp_denied", "file": "models/sale_order.py", "line": 1405, "kind": "action_method"},
{"from": "*", "to": "cancelled", "trigger": "action_cancel", "file": "models/sale_order.py", "line": 1141, "kind": "action_method"}
]
},
"odsp_standard": {
"field": "x_fc_odsp_std_status",
"label": "ODSP Standard",
"default": "quotation",
"terminal": ["case_closed", "cancelled"],
"states": [
{"key": "quotation", "label": "Quotation"},
{"key": "submitted_to_odsp", "label": "Submitted to ODSP"},
{"key": "pre_approved", "label": "Pre-Approved"},
{"key": "ready_delivery", "label": "Ready for Delivery"},
{"key": "delivered", "label": "Delivered"},
{"key": "pod_submitted", "label": "POD Submitted"},
{"key": "payment_received", "label": "Payment Received"},
{"key": "case_closed", "label": "Case Closed"},
{"key": "on_hold", "label": "On Hold"},
{"key": "cancelled", "label": "Cancelled"},
{"key": "denied", "label": "Denied"}
],
"transitions": [
{"from": "quotation", "to": "submitted_to_odsp", "trigger": "odsp_submit_to_odsp_wizard.action_confirm", "file": "wizard/odsp_submit_to_odsp_wizard.py", "line": 105, "kind": "wizard"},
{"from": "submitted_to_odsp", "to": "pre_approved", "trigger": "odsp_pre_approved_wizard.action_confirm", "file": "wizard/odsp_pre_approved_wizard.py", "line": 68, "kind": "wizard"},
{"from": "pre_approved", "to": "ready_delivery", "trigger": "odsp_ready_delivery_wizard.action_confirm", "file": "wizard/odsp_ready_delivery_wizard.py", "line": 170, "kind": "wizard"},
{"from": "ready_delivery", "to": "delivered", "trigger": "_odsp_advance_status('delivered')", "file": "models/sale_order.py", "line": 1215, "kind": "auto_write"},
{"from": "delivered", "to": "pod_submitted", "trigger": "_odsp_advance_status('pod_submitted')", "file": "models/sale_order.py", "line": 1225, "kind": "auto_write"},
{"from": "pod_submitted", "to": "payment_received", "trigger": "invoice payment posted", "file": "models/account_move.py", "line": 59, "kind": "auto_write"},
{"from": "payment_received", "to": "case_closed", "trigger": "_cron_auto_close_odsp_paid_cases", "file": "models/sale_order.py", "line": 6899, "kind": "cron"},
{"from": "*", "to": "on_hold", "trigger": "action_odsp_on_hold", "file": "models/sale_order.py", "line": 1396, "kind": "action_method"},
{"from": "on_hold", "to": "quotation", "trigger": "action_odsp_resume", "file": "models/sale_order.py", "line": 1401, "kind": "action_method"},
{"from": "*", "to": "denied", "trigger": "action_odsp_denied", "file": "models/sale_order.py", "line": 1405, "kind": "action_method"},
{"from": "*", "to": "cancelled", "trigger": "action_cancel", "file": "models/sale_order.py", "line": 1141, "kind": "action_method"}
]
},
"ontario_works": {
"field": "x_fc_ow_status",
"label": "Ontario Works",
"default": "quotation",
"terminal": ["case_closed", "cancelled"],
"states": [
{"key": "quotation", "label": "Quotation"},
{"key": "documents_ready", "label": "Documents Ready"},
{"key": "submitted_to_ow", "label": "Submitted to Ontario Works"},
{"key": "payment_received", "label": "Payment Received"},
{"key": "ready_delivery", "label": "Ready for Delivery"},
{"key": "delivered", "label": "Delivered"},
{"key": "case_closed", "label": "Case Closed"},
{"key": "on_hold", "label": "On Hold"},
{"key": "cancelled", "label": "Cancelled"},
{"key": "denied", "label": "Denied"}
],
"transitions": [
{"from": "quotation", "to": "documents_ready", "trigger": "odsp_discretionary_wizard.action_confirm (docs)", "file": "wizard/odsp_discretionary_wizard.py", "line": 245, "kind": "wizard"},
{"from": "documents_ready", "to": "submitted_to_ow", "trigger": "odsp_discretionary_wizard.action_confirm (submit)", "file": "wizard/odsp_discretionary_wizard.py", "line": 260, "kind": "wizard"},
{"from": "submitted_to_ow", "to": "payment_received", "trigger": "invoice payment posted", "file": "models/account_move.py", "line": 59, "kind": "auto_write"},
{"from": "payment_received", "to": "ready_delivery", "trigger": "odsp_ready_delivery_wizard.action_confirm", "file": "wizard/odsp_ready_delivery_wizard.py", "line": 170, "kind": "wizard"},
{"from": "ready_delivery", "to": "delivered", "trigger": "_odsp_advance_status('delivered')", "file": "models/sale_order.py", "line": 1217, "kind": "auto_write"},
{"from": "delivered", "to": "case_closed", "trigger": "_cron_auto_close_odsp_paid_cases", "file": "models/sale_order.py", "line": 6899, "kind": "cron"},
{"from": "*", "to": "on_hold", "trigger": "action_odsp_on_hold", "file": "models/sale_order.py", "line": 1396, "kind": "action_method"},
{"from": "on_hold", "to": "quotation", "trigger": "action_odsp_resume", "file": "models/sale_order.py", "line": 1401, "kind": "action_method"},
{"from": "*", "to": "denied", "trigger": "action_odsp_denied", "file": "models/sale_order.py", "line": 1405, "kind": "action_method"},
{"from": "*", "to": "cancelled", "trigger": "action_cancel", "file": "models/sale_order.py", "line": 1141, "kind": "action_method"}
]
}
};