You've already forked Organizations_register
Import UAPF package
Update vdvc-register-viewer.html
This commit is contained in:
@@ -8,40 +8,111 @@
|
||||
:root { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||||
|
||||
/* Keep the upper row */
|
||||
header { padding: 12px 16px; border-bottom: 1px solid #ddd; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||||
header .meta { margin-left: auto; opacity: 0.85; font-size: 12px; }
|
||||
|
||||
main { display: grid; grid-template-columns: 1fr 420px; gap: 0; flex: 1; min-height: 0; overflow: hidden; }
|
||||
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; border: 1px solid #ddd; font-size: 12px; }
|
||||
.pill.warn { border-color: #f2c200; }
|
||||
.pill.bad { border-color: #d92d20; }
|
||||
|
||||
.panel { border-right: 1px solid #eee; display: flex; flex-direction: column; min-height: 0; }
|
||||
.panel:last-child { border-right: 0; }
|
||||
.toolbar { padding: 10px 12px; border-bottom: 1px solid #eee; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
button { padding: 8px 10px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer; }
|
||||
button:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||
.status { font-size: 12px; opacity: 0.85; }
|
||||
main { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
padding: 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow: auto;
|
||||
/* New single-column editor UI */
|
||||
.controls {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
pre { margin: 0; padding: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.35; white-space: pre-wrap; word-break: break-word; }
|
||||
.side { flex: 1; min-height: 0; overflow: auto; }
|
||||
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; border: 1px solid #ddd; font-size: 12px; }
|
||||
.warn { color: #b00; }
|
||||
.controls .spacer { flex: 1; }
|
||||
.controls input[type="search"]{
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||
|
||||
.status { padding: 8px 12px; font-size: 12px; opacity: 0.85; border-bottom: 1px solid #eee; }
|
||||
|
||||
.tableWrap { flex: 1; min-height: 0; overflow: auto; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
||||
thead th {
|
||||
position: sticky; top: 0;
|
||||
background: #fafafa;
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
tbody td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
vertical-align: top;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
tbody tr:hover { background: #fcfcfc; }
|
||||
tbody tr.selected { background: #eef6ff; }
|
||||
tbody tr.group { background: #f7f7f7; }
|
||||
tbody tr.group td { font-weight: 600; }
|
||||
|
||||
.cell input{
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
.cell input:focus{
|
||||
outline: none;
|
||||
border-color: #9ec5fe;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.muted { opacity: .7; }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
|
||||
/* Column widths */
|
||||
th.col-mincode, td.col-mincode { width: 110px; }
|
||||
th.col-minname, td.col-minname { width: 260px; }
|
||||
th.col-orgcode, td.col-orgcode { width: 110px; }
|
||||
th.col-nmr, td.col-nmr { width: 160px; }
|
||||
th.col-prefix, td.col-prefix { width: 120px; }
|
||||
th.col-orgname, td.col-orgname { width: 420px; }
|
||||
th.col-actions, td.col-actions { width: 110px; }
|
||||
td.col-actions { white-space: nowrap; }
|
||||
|
||||
.hint { font-size: 12px; opacity: .75; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- KEEP THIS UPPER ROW (as requested) -->
|
||||
<header>
|
||||
<strong>VDVC Register Viewer</strong>
|
||||
<span class="pill" id="dirtyPill">clean</span>
|
||||
@@ -50,26 +121,39 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="panel">
|
||||
<div class="toolbar">
|
||||
<button id="btnLoad">Load</button>
|
||||
<button id="btnSave" disabled>Save (commit)</button>
|
||||
<button id="btnFormat">Format</button>
|
||||
<button id="btnValidate">Check XML</button>
|
||||
<span class="status" id="status"></span>
|
||||
</div>
|
||||
<textarea id="editor" spellcheck="false"></textarea>
|
||||
</section>
|
||||
<!-- Remove the old buttons row + Info. Replace with one-column table editor -->
|
||||
<div class="controls">
|
||||
<input id="search" type="search" placeholder="Search (ministry / organization / codes)..." />
|
||||
<button id="btnAddMinistry" type="button">Add ministry</button>
|
||||
<button id="btnAddOrg" type="button">Add organization</button>
|
||||
<button id="btnDelete" type="button" disabled>Delete selected</button>
|
||||
<div class="spacer"></div>
|
||||
<span class="pill" id="xmlStatus">XML: unknown</span>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="toolbar">
|
||||
<strong>Info</strong>
|
||||
<span class="status" id="infoStatus"></span>
|
||||
</div>
|
||||
<div class="side">
|
||||
<pre id="info"></pre>
|
||||
</div>
|
||||
</section>
|
||||
<div class="status" id="status">
|
||||
<span class="hint">Edit cells directly. Select a row to add organizations under that ministry or delete a record. Use the parent “Save” button to commit.</span>
|
||||
</div>
|
||||
|
||||
<div class="tableWrap">
|
||||
<table id="grid" aria-label="VDVC register">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-mincode" data-sort="ministryCode">Min code</th>
|
||||
<th class="col-minname" data-sort="ministryName">Ministry name</th>
|
||||
<th class="col-orgcode" data-sort="orgCode">Org code</th>
|
||||
<th class="col-nmr mono" data-sort="nmr">NMR</th>
|
||||
<th class="col-prefix" data-sort="docPrefix">Doc prefix</th>
|
||||
<th class="col-orgname" data-sort="orgName">Organization name</th>
|
||||
<th class="col-actions">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Hidden XML output: on every edit we regenerate XML and send it to parent (best-effort). -->
|
||||
<textarea id="xmlOut" style="display:none"></textarea>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
@@ -78,164 +162,461 @@
|
||||
connPill: document.getElementById('connPill'),
|
||||
meta: document.getElementById('meta'),
|
||||
status: document.getElementById('status'),
|
||||
infoStatus: document.getElementById('infoStatus'),
|
||||
info: document.getElementById('info'),
|
||||
editor: document.getElementById('editor'),
|
||||
btnLoad: document.getElementById('btnLoad'),
|
||||
btnSave: document.getElementById('btnSave'),
|
||||
btnFormat: document.getElementById('btnFormat'),
|
||||
btnValidate: document.getElementById('btnValidate'),
|
||||
xmlStatus: document.getElementById('xmlStatus'),
|
||||
search: document.getElementById('search'),
|
||||
btnAddMinistry: document.getElementById('btnAddMinistry'),
|
||||
btnAddOrg: document.getElementById('btnAddOrg'),
|
||||
btnDelete: document.getElementById('btnDelete'),
|
||||
tbody: document.getElementById('tbody'),
|
||||
xmlOut: document.getElementById('xmlOut'),
|
||||
grid: document.getElementById('grid'),
|
||||
};
|
||||
|
||||
let payload = null;
|
||||
let dirty = false;
|
||||
let loadedText = '';
|
||||
|
||||
// Model:
|
||||
// ministries: [{ code, name, organizations: [{ code, nmr, docPrefix, name }] }]
|
||||
let model = { ministries: [], meta: { version: '1.0', created: '', lang: '' } };
|
||||
|
||||
// UI state
|
||||
let selected = { type: null, minIndex: -1, orgIndex: -1 };
|
||||
let sort = { key: null, dir: 1 }; // dir: 1 asc, -1 desc
|
||||
|
||||
function setDirty(v) {
|
||||
dirty = v;
|
||||
dirty = !!v;
|
||||
els.dirtyPill.textContent = dirty ? 'dirty' : 'clean';
|
||||
els.btnSave.disabled = !dirty;
|
||||
parent.postMessage({ type: 'PGV_DIRTY', dirty }, '*');
|
||||
}
|
||||
function setConnected(v) { els.connPill.textContent = v ? 'connected' : 'not connected'; }
|
||||
function setStatus(msg) { els.status.textContent = msg || ''; }
|
||||
function setInfoStatus(msg) { els.infoStatus.textContent = msg || ''; }
|
||||
|
||||
function safeJson(obj) { try { return JSON.stringify(obj, null, 2); } catch { return String(obj); } }
|
||||
function setConnected(v) {
|
||||
els.connPill.textContent = v ? 'connected' : 'not connected';
|
||||
}
|
||||
|
||||
function setStatus(msg) {
|
||||
if (!msg) return;
|
||||
els.status.innerHTML = '<span class="hint">' + escapeHtml(msg) + '</span>';
|
||||
}
|
||||
|
||||
function setXmlStatus(ok, msg) {
|
||||
els.xmlStatus.textContent = ok ? ('XML OK' + (msg ? ' · ' + msg : '')) : ('XML ERROR' + (msg ? ' · ' + msg : ''));
|
||||
els.xmlStatus.classList.toggle('bad', !ok);
|
||||
els.xmlStatus.classList.toggle('warn', !ok);
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
function toAbs(url) {
|
||||
try { return new URL(url, window.location.origin).toString(); } catch { return url; }
|
||||
}
|
||||
|
||||
async function pgvFetch(url) {
|
||||
const abs = toAbs(url);
|
||||
const r = await fetch(abs, { credentials: 'same-origin' });
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status + ' for ' + abs);
|
||||
return await r.text();
|
||||
}
|
||||
|
||||
async function loadXmlText() {
|
||||
// Prefer apiUrl if present (server-side content resolver), fallback to raw path guess.
|
||||
if (payload && payload.apiUrl) {
|
||||
const txt = await pgvFetch(payload.apiUrl);
|
||||
try {
|
||||
const obj = JSON.parse(txt);
|
||||
// Try common shapes
|
||||
if (typeof obj.content === 'string') return obj.content;
|
||||
if (obj.result && typeof obj.result.content === 'string') return obj.result.content;
|
||||
} catch {
|
||||
// ignore - not JSON or unexpected shape
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback raw guess (works on many ProcessGit setups)
|
||||
if (payload && payload.path) {
|
||||
const rawGuess =
|
||||
'/' + String(payload.repoLink || '').replace(/^\//,'') +
|
||||
'/raw/branch/' + (payload.branch || 'main') + '/' + payload.path;
|
||||
return await pgvFetch(rawGuess);
|
||||
}
|
||||
|
||||
throw new Error('Could not load XML (no apiUrl / raw target).');
|
||||
}
|
||||
|
||||
function parseXml(xmlText) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xmlText, 'application/xml');
|
||||
const perr = doc.getElementsByTagName('parsererror')[0];
|
||||
if (perr) throw new Error(perr.textContent || 'XML parse error');
|
||||
|
||||
const root = doc.documentElement;
|
||||
if (!root || root.nodeName !== 'vdvcRegister') throw new Error('Root element must be <vdvcRegister>');
|
||||
|
||||
const out = {
|
||||
ministries: [],
|
||||
meta: {
|
||||
version: root.getAttribute('version') || '1.0',
|
||||
created: root.getAttribute('created') || '',
|
||||
lang: root.getAttribute('xml:lang') || root.getAttribute('lang') || ''
|
||||
}
|
||||
};
|
||||
|
||||
const mins = Array.from(root.getElementsByTagName('ministry'));
|
||||
for (const m of mins) {
|
||||
const min = {
|
||||
code: m.getAttribute('code') || '',
|
||||
name: m.getAttribute('name') || '',
|
||||
organizations: [],
|
||||
};
|
||||
const orgs = Array.from(m.getElementsByTagName('organization'));
|
||||
for (const o of orgs) {
|
||||
const nameEl = o.getElementsByTagName('name')[0];
|
||||
min.organizations.push({
|
||||
code: o.getAttribute('code') || '',
|
||||
nmr: o.getAttribute('nmr') || '',
|
||||
docPrefix: o.getAttribute('docPrefix') || '',
|
||||
name: (nameEl && nameEl.textContent) ? nameEl.textContent : '',
|
||||
});
|
||||
}
|
||||
out.ministries.push(min);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildXmlFromModel() {
|
||||
const doc = document.implementation.createDocument('', '', null);
|
||||
const root = doc.createElement('vdvcRegister');
|
||||
root.setAttribute('xmlns', 'http://vdvc.gov.lv/schema/vdvc-register');
|
||||
root.setAttribute('version', model.meta.version || '1.0');
|
||||
if (model.meta.created) root.setAttribute('created', model.meta.created);
|
||||
if (model.meta.lang) root.setAttribute('xml:lang', model.meta.lang);
|
||||
doc.appendChild(root);
|
||||
|
||||
for (const m of model.ministries) {
|
||||
const min = doc.createElement('ministry');
|
||||
if (m.code) min.setAttribute('code', m.code);
|
||||
// Keep name attribute present (even when empty)
|
||||
min.setAttribute('name', m.name || '');
|
||||
for (const o of m.organizations) {
|
||||
const org = doc.createElement('organization');
|
||||
if (o.code) org.setAttribute('code', o.code);
|
||||
if (o.nmr) org.setAttribute('nmr', o.nmr);
|
||||
if (o.docPrefix) org.setAttribute('docPrefix', o.docPrefix);
|
||||
const nm = doc.createElement('name');
|
||||
nm.textContent = o.name || '';
|
||||
org.appendChild(nm);
|
||||
min.appendChild(org);
|
||||
}
|
||||
root.appendChild(min);
|
||||
}
|
||||
|
||||
const xml = new XMLSerializer().serializeToString(doc);
|
||||
return formatXml(xml);
|
||||
}
|
||||
|
||||
function formatXml(xml) {
|
||||
// Basic pretty printer (readable, stable)
|
||||
const PADDING = ' ';
|
||||
const reg = /(>)(<)(\/*)/g;
|
||||
let formatted = '';
|
||||
let indent = 0;
|
||||
xml = xml.replace(/>\s+</g, '><').trim();
|
||||
xml.split(/(?=<)/g).forEach(node => {
|
||||
if (!node) return;
|
||||
if (node.match(/^<\/\w/)) indent = Math.max(indent - 1, 0);
|
||||
formatted += PADDING.repeat(indent) + node.trim() + '\n';
|
||||
if (node.match(/^<\w([^>]*[^/])?>$/) && !node.startsWith('<?') && !node.startsWith('<!')) indent += 1;
|
||||
let pad = 0;
|
||||
xml = xml.replace(reg, '$1\n$2$3');
|
||||
xml.split('\n').forEach((node) => {
|
||||
if (!node.trim()) return;
|
||||
let indent = 0;
|
||||
if (node.match(/^<\//)) {
|
||||
pad = Math.max(pad - 1, 0);
|
||||
} else if (node.match(/^<[^!?][^>]*[^\/]>$/)) {
|
||||
indent = 1;
|
||||
}
|
||||
formatted += PADDING.repeat(pad) + node.trim() + '\n';
|
||||
pad += indent;
|
||||
});
|
||||
return formatted.trim() + '\n';
|
||||
}
|
||||
|
||||
function validateXml(xmlText) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xmlText, 'application/xml');
|
||||
const err = doc.querySelector('parsererror');
|
||||
return err ? (err.textContent || 'XML parse error') : null;
|
||||
function flattenRows(ministries) {
|
||||
const rows = [];
|
||||
ministries.forEach((m, mi) => {
|
||||
rows.push({ __type: 'group', mi });
|
||||
m.organizations.forEach((o, oi) => {
|
||||
rows.push({
|
||||
__type: 'org',
|
||||
mi, oi,
|
||||
ministryCode: m.code,
|
||||
ministryName: m.name,
|
||||
orgCode: o.code,
|
||||
nmr: o.nmr,
|
||||
docPrefix: o.docPrefix,
|
||||
orgName: o.name,
|
||||
});
|
||||
});
|
||||
if (m.organizations.length === 0) rows.push({ __type: 'empty', mi });
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
// -------- Parent fetch proxy (NO direct fetch in iframe) --------
|
||||
function pgvFetch(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reqId = Math.random().toString(36).slice(2);
|
||||
const onMsg = (ev) => {
|
||||
const m = ev.data;
|
||||
if (!m || typeof m !== 'object') return;
|
||||
if (m.type !== 'PGV_FETCH_RESULT') return;
|
||||
if (m.reqId !== reqId) return;
|
||||
window.removeEventListener('message', onMsg);
|
||||
if (m.ok) resolve(m.text || '');
|
||||
else reject(new Error(m.error || 'fetch failed'));
|
||||
};
|
||||
window.addEventListener('message', onMsg);
|
||||
parent.postMessage({ type: 'PGV_FETCH', reqId, url }, '*');
|
||||
function matchesSearch(row, q) {
|
||||
if (!q) return true;
|
||||
const hay = [
|
||||
row.ministryCode, row.ministryName, row.orgCode, row.nmr, row.docPrefix, row.orgName
|
||||
].join(' ').toLowerCase();
|
||||
return hay.includes(q);
|
||||
}
|
||||
|
||||
function applySort() {
|
||||
if (!sort.key) return;
|
||||
const key = sort.key;
|
||||
const dir = sort.dir;
|
||||
|
||||
// Sort organizations inside each ministry (keeps grouping)
|
||||
model.ministries.forEach((m) => {
|
||||
m.organizations.sort((a, b) => {
|
||||
const av =
|
||||
(key === 'orgName') ? (a.name||'') :
|
||||
(key === 'orgCode') ? (a.code||'') :
|
||||
(key === 'nmr') ? (a.nmr||'') :
|
||||
(key === 'docPrefix') ? (a.docPrefix||'') :
|
||||
'';
|
||||
const bv =
|
||||
(key === 'orgName') ? (b.name||'') :
|
||||
(key === 'orgCode') ? (b.code||'') :
|
||||
(key === 'nmr') ? (b.nmr||'') :
|
||||
(key === 'docPrefix') ? (b.docPrefix||'') :
|
||||
'';
|
||||
return String(av).localeCompare(String(bv), undefined, { numeric: true, sensitivity: 'base' }) * dir;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toAbs(url) {
|
||||
// payload urls are usually relative to same origin; parent will enforce allowlist
|
||||
return url;
|
||||
}
|
||||
function render() {
|
||||
const q = (els.search.value || '').trim().toLowerCase();
|
||||
|
||||
async function tryLoadViaApiUrl() {
|
||||
if (!payload?.apiUrl) return null;
|
||||
const text = await pgvFetch(toAbs(payload.apiUrl));
|
||||
// api might return JSON {content:"..."}
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
if (j && typeof j.content === 'string') return j.content;
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
applySort();
|
||||
let rows = flattenRows(model.ministries);
|
||||
|
||||
async function tryLoadViaRawTarget() {
|
||||
const t = payload?.targets || {};
|
||||
const xmlPath = t.xml || payload?.path || null;
|
||||
if (!xmlPath) return null;
|
||||
// Filter with group preservation
|
||||
if (q) {
|
||||
const keepMin = new Set();
|
||||
rows.forEach(r => { if (r.__type === 'org' && matchesSearch(r, q)) keepMin.add(r.mi); });
|
||||
rows = rows.filter(r => {
|
||||
if (r.__type === 'group' || r.__type === 'empty') return keepMin.has(r.mi);
|
||||
return r.__type === 'org' ? matchesSearch(r, q) : false;
|
||||
});
|
||||
}
|
||||
|
||||
// If it's already a /raw/... URL in payload.targets, use it; otherwise parent can resolve it if you pass a raw URL.
|
||||
// The safest is to ask for /raw/ path directly from ProcessGit; we assume parent will accept same-origin URLs.
|
||||
const maybeUrl = (typeof xmlPath === 'string') ? xmlPath : null;
|
||||
if (!maybeUrl) return null;
|
||||
els.tbody.innerHTML = '';
|
||||
|
||||
// If viewer config gave only "vdvc-register.xml", parent won't know how to map.
|
||||
// So we prefer when targets.xml is a full raw URL. If it isn't, just return null.
|
||||
if (!maybeUrl.includes('/raw/')) return null;
|
||||
for (const r of rows) {
|
||||
if (r.__type === 'group') {
|
||||
const m = model.ministries[r.mi];
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'group';
|
||||
tr.dataset.type = 'ministry';
|
||||
tr.dataset.mi = String(r.mi);
|
||||
tr.innerHTML = `
|
||||
<td class="col-mincode"><span class="mono">${escapeHtml(m.code || '')}</span></td>
|
||||
<td class="col-minname" colspan="5">${escapeHtml(m.name || '')} <span class="muted">(ministry)</span></td>
|
||||
<td class="col-actions"><button type="button">Select</button></td>
|
||||
`;
|
||||
tr.addEventListener('click', () => selectMinistry(r.mi));
|
||||
els.tbody.appendChild(tr);
|
||||
continue;
|
||||
}
|
||||
|
||||
const text = await pgvFetch(toAbs(maybeUrl));
|
||||
return text;
|
||||
}
|
||||
if (r.__type === 'empty') {
|
||||
const m = model.ministries[r.mi];
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.type = 'empty';
|
||||
tr.dataset.mi = String(r.mi);
|
||||
tr.innerHTML = `
|
||||
<td class="col-mincode"><span class="mono">${escapeHtml(m.code || '')}</span></td>
|
||||
<td class="col-minname">${escapeHtml(m.name || '')}</td>
|
||||
<td class="col-orgcode muted" colspan="4">No organizations</td>
|
||||
<td class="col-actions"><button type="button">Add</button></td>
|
||||
`;
|
||||
tr.querySelector('button')?.addEventListener('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
selectMinistry(r.mi);
|
||||
addOrganization();
|
||||
});
|
||||
els.tbody.appendChild(tr);
|
||||
continue;
|
||||
}
|
||||
|
||||
async function doLoad() {
|
||||
setStatus('Loading...');
|
||||
try {
|
||||
let content = await tryLoadViaApiUrl();
|
||||
if (content == null) content = await tryLoadViaRawTarget();
|
||||
// org row
|
||||
const m = model.ministries[r.mi];
|
||||
const o = m.organizations[r.oi];
|
||||
|
||||
if (content == null) throw new Error('No usable apiUrl or raw xml target.');
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.type = 'org';
|
||||
tr.dataset.mi = String(r.mi);
|
||||
tr.dataset.oi = String(r.oi);
|
||||
|
||||
els.editor.value = content;
|
||||
loadedText = content;
|
||||
setDirty(false);
|
||||
setStatus('Loaded.');
|
||||
} catch (e) {
|
||||
setStatus('Load failed.');
|
||||
console.error(e);
|
||||
if (selected.type === 'org' && selected.minIndex === r.mi && selected.orgIndex === r.oi) tr.classList.add('selected');
|
||||
|
||||
tr.innerHTML = `
|
||||
<td class="col-mincode"><span class="mono">${escapeHtml(m.code || '')}</span></td>
|
||||
<td class="col-minname"><div class="cell"><input data-field="ministryName" value="${escapeHtml(m.name || '')}" /></div></td>
|
||||
<td class="col-orgcode"><div class="cell"><input class="mono" data-field="orgCode" value="${escapeHtml(o.code || '')}" /></div></td>
|
||||
<td class="col-nmr"><div class="cell"><input class="mono" data-field="nmr" value="${escapeHtml(o.nmr || '')}" /></div></td>
|
||||
<td class="col-prefix"><div class="cell"><input data-field="docPrefix" value="${escapeHtml(o.docPrefix || '')}" /></div></td>
|
||||
<td class="col-orgname"><div class="cell"><input data-field="orgName" value="${escapeHtml(o.name || '')}" /></div></td>
|
||||
<td class="col-actions"><button type="button">Select</button></td>
|
||||
`;
|
||||
|
||||
tr.addEventListener('click', () => selectOrg(r.mi, r.oi));
|
||||
|
||||
tr.querySelectorAll('input').forEach((inp) => {
|
||||
inp.addEventListener('input', () => {
|
||||
const field = inp.getAttribute('data-field');
|
||||
if (!field) return;
|
||||
|
||||
if (field === 'ministryName') m.name = inp.value || '';
|
||||
else if (field === 'orgCode') o.code = inp.value || '';
|
||||
else if (field === 'nmr') o.nmr = inp.value || '';
|
||||
else if (field === 'docPrefix') o.docPrefix = inp.value || '';
|
||||
else if (field === 'orgName') o.name = inp.value || '';
|
||||
|
||||
onModelChanged();
|
||||
});
|
||||
});
|
||||
|
||||
els.tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function doFormat() {
|
||||
const v = els.editor.value || '';
|
||||
const err = validateXml(v);
|
||||
if (err) { setStatus('Cannot format: XML invalid'); return; }
|
||||
els.editor.value = formatXml(v);
|
||||
setDirty(els.editor.value !== loadedText);
|
||||
setStatus('Formatted.');
|
||||
function selectMinistry(mi) {
|
||||
selected = { type: 'ministry', minIndex: mi, orgIndex: -1 };
|
||||
els.btnDelete.disabled = false;
|
||||
render();
|
||||
}
|
||||
|
||||
function doValidate() {
|
||||
const v = els.editor.value || '';
|
||||
const err = validateXml(v);
|
||||
if (err) { setStatus('Invalid XML'); setInfoStatus('Invalid XML'); return; }
|
||||
setStatus('XML OK'); setInfoStatus('XML OK');
|
||||
function selectOrg(mi, oi) {
|
||||
selected = { type: 'org', minIndex: mi, orgIndex: oi };
|
||||
els.btnDelete.disabled = false;
|
||||
render();
|
||||
}
|
||||
|
||||
function doSave() {
|
||||
setStatus('Use parent Save to commit.');
|
||||
function addMinistry() {
|
||||
const code = prompt('Ministry code (e.g., 01):', '');
|
||||
if (code === null) return;
|
||||
const name = prompt('Ministry name:', '') ?? '';
|
||||
model.ministries.push({ code: (code||'').trim(), name: (name||'').trim(), organizations: [] });
|
||||
selected = { type: 'ministry', minIndex: model.ministries.length - 1, orgIndex: -1 };
|
||||
onModelChanged();
|
||||
render();
|
||||
}
|
||||
|
||||
els.editor.addEventListener('input', () => setDirty((els.editor.value || '') !== loadedText));
|
||||
els.btnLoad.addEventListener('click', doLoad);
|
||||
els.btnFormat.addEventListener('click', doFormat);
|
||||
els.btnValidate.addEventListener('click', doValidate);
|
||||
els.btnSave.addEventListener('click', doSave);
|
||||
function addOrganization() {
|
||||
let mi = selected.type ? selected.minIndex : -1;
|
||||
if (mi < 0 || mi >= model.ministries.length) {
|
||||
const code = prompt('Add to which ministry code?', '');
|
||||
if (code === null) return;
|
||||
mi = model.ministries.findIndex(m => (m.code||'') === (code||'').trim());
|
||||
if (mi < 0) {
|
||||
alert('Ministry not found. Add ministry first.');
|
||||
return;
|
||||
}
|
||||
selected = { type: 'ministry', minIndex: mi, orgIndex: -1 };
|
||||
}
|
||||
|
||||
const o = { code: '', nmr: '', docPrefix: '', name: '' };
|
||||
model.ministries[mi].organizations.push(o);
|
||||
selected = { type: 'org', minIndex: mi, orgIndex: model.ministries[mi].organizations.length - 1 };
|
||||
onModelChanged();
|
||||
render();
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
if (!selected.type) return;
|
||||
if (!confirm('Delete selected item?')) return;
|
||||
|
||||
const mi = selected.minIndex;
|
||||
if (mi < 0 || mi >= model.ministries.length) return;
|
||||
|
||||
if (selected.type === 'ministry') {
|
||||
model.ministries.splice(mi, 1);
|
||||
selected = { type: null, minIndex: -1, orgIndex: -1 };
|
||||
} else if (selected.type === 'org') {
|
||||
const oi = selected.orgIndex;
|
||||
const orgs = model.ministries[mi].organizations;
|
||||
if (oi >= 0 && oi < orgs.length) orgs.splice(oi, 1);
|
||||
selected = { type: 'ministry', minIndex: mi, orgIndex: -1 };
|
||||
}
|
||||
|
||||
onModelChanged();
|
||||
render();
|
||||
}
|
||||
|
||||
function onModelChanged() {
|
||||
try {
|
||||
const xml = buildXmlFromModel();
|
||||
els.xmlOut.value = xml;
|
||||
setXmlStatus(true);
|
||||
setDirty(true);
|
||||
|
||||
// Best-effort sync with host (ignored if host doesn't support it)
|
||||
parent.postMessage({ type: 'PGV_SET_CONTENT', path: payload?.path, content: xml }, '*');
|
||||
parent.postMessage({ type: 'PGV_CONTENT', path: payload?.path, content: xml }, '*');
|
||||
parent.postMessage({ type: 'PGV_PRIMARY_CONTENT', path: payload?.path, content: xml }, '*');
|
||||
} catch (e) {
|
||||
setXmlStatus(false, String(e && e.message ? e.message : e));
|
||||
setDirty(true);
|
||||
}
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
els.search.addEventListener('input', () => render());
|
||||
els.btnAddMinistry.addEventListener('click', addMinistry);
|
||||
els.btnAddOrg.addEventListener('click', addOrganization);
|
||||
els.btnDelete.addEventListener('click', deleteSelected);
|
||||
|
||||
// Click header to sort
|
||||
els.grid.querySelectorAll('thead th[data-sort]').forEach((th) => {
|
||||
th.addEventListener('click', () => {
|
||||
const key = th.getAttribute('data-sort');
|
||||
if (!key) return;
|
||||
if (sort.key === key) sort.dir = sort.dir * -1;
|
||||
else { sort.key = key; sort.dir = 1; }
|
||||
render();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function initWithPayload(p) {
|
||||
payload = p || null;
|
||||
setConnected(true);
|
||||
els.meta.textContent = payload ? `${payload.branch || ''} ${payload.path || ''}`.trim() : '';
|
||||
bindEvents();
|
||||
|
||||
try {
|
||||
const xmlText = await loadXmlText();
|
||||
model = parseXml(xmlText);
|
||||
setXmlStatus(true);
|
||||
setDirty(false);
|
||||
|
||||
// Build initial normalized XML output too
|
||||
els.xmlOut.value = buildXmlFromModel();
|
||||
render();
|
||||
} catch (e) {
|
||||
setXmlStatus(false, (e && e.message) ? e.message : String(e));
|
||||
setStatus('Failed to load XML. Check console/network.');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', (ev) => {
|
||||
const msg = ev.data;
|
||||
if (!msg || typeof msg !== 'object') return;
|
||||
|
||||
if (msg.type === 'PGV_INIT') {
|
||||
payload = msg.payload || null;
|
||||
setConnected(true);
|
||||
els.meta.textContent = payload ? `${payload.branch || ''} ${payload.path || ''}`.trim() : '';
|
||||
els.info.textContent = payload ? safeJson(payload) : 'No payload';
|
||||
doLoad();
|
||||
initWithPayload(msg.payload || null);
|
||||
}
|
||||
|
||||
if (msg.type === 'PGV_COMMIT_OK') {
|
||||
loadedText = els.editor.value || '';
|
||||
setDirty(false);
|
||||
setStatus('Committed.');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user