You've already forked Organizations_register
Import UAPF package
Update vdvc-register-viewer.html
This commit is contained in:
@@ -10,10 +10,18 @@
|
|||||||
body { margin: 0; display: flex; flex-direction: column; overflow: hidden; }
|
body { margin: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
|
||||||
/* Keep the upper row */
|
/* Keep the upper row */
|
||||||
header { padding: 12px 16px; border-bottom: 1px solid #ddd; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
header .meta { margin-left: auto; opacity: 0.85; font-size: 12px; }
|
header .meta { margin-left: auto; opacity: 0.85; font-size: 12px; }
|
||||||
|
|
||||||
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; border: 1px solid #ddd; font-size: 12px; }
|
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; border: 1px solid #ddd; font-size: 12px; background: #fff; }
|
||||||
.pill.warn { border-color: #f2c200; }
|
.pill.warn { border-color: #f2c200; }
|
||||||
.pill.bad { border-color: #d92d20; }
|
.pill.bad { border-color: #d92d20; }
|
||||||
|
|
||||||
@@ -27,6 +35,7 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls .spacer { flex: 1; }
|
.controls .spacer { flex: 1; }
|
||||||
@@ -46,9 +55,18 @@
|
|||||||
}
|
}
|
||||||
button:disabled { opacity: 0.55; cursor: not-allowed; }
|
button:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||||
|
|
||||||
.status { padding: 8px 12px; font-size: 12px; opacity: 0.85; border-bottom: 1px solid #eee; }
|
.status {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.tableWrap { flex: 1; min-height: 0; overflow: auto; }
|
.tableWrap { flex: 1; min-height: 0; overflow: auto; background: #fff; }
|
||||||
|
|
||||||
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
||||||
thead th {
|
thead th {
|
||||||
@@ -97,6 +115,7 @@
|
|||||||
|
|
||||||
.muted { opacity: .7; }
|
.muted { opacity: .7; }
|
||||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||||
|
.hint { opacity: .8; }
|
||||||
|
|
||||||
/* Column widths */
|
/* Column widths */
|
||||||
th.col-mincode, td.col-mincode { width: 110px; }
|
th.col-mincode, td.col-mincode { width: 110px; }
|
||||||
@@ -108,11 +127,58 @@
|
|||||||
th.col-actions, td.col-actions { width: 110px; }
|
th.col-actions, td.col-actions { width: 110px; }
|
||||||
td.col-actions { white-space: nowrap; }
|
td.col-actions { white-space: nowrap; }
|
||||||
|
|
||||||
.hint { font-size: 12px; opacity: .75; }
|
/* In-page modal (works in sandbox without allow-modals) */
|
||||||
|
.modal-backdrop{
|
||||||
|
position: fixed; inset: 0;
|
||||||
|
background: rgba(0,0,0,.35);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.modal{
|
||||||
|
width: min(560px, calc(100vw - 24px));
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,.2);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.modal header{
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding: 12px 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.modal header strong{ flex: 1; }
|
||||||
|
.modal header button{
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.modal .body{ padding: 12px 14px; display: grid; gap: 10px; }
|
||||||
|
.modal .body label{ font-size: 12px; opacity: .85; display: grid; gap: 6px; }
|
||||||
|
.modal .body input{
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.modal .footer{
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.modal .footer button.primary{
|
||||||
|
border-color: #2b6cb0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- KEEP THIS UPPER ROW (as requested) -->
|
<!-- KEEP THIS UPPER ROW -->
|
||||||
<header>
|
<header>
|
||||||
<strong>VDVC Register Viewer</strong>
|
<strong>VDVC Register Viewer</strong>
|
||||||
<span class="pill" id="dirtyPill">clean</span>
|
<span class="pill" id="dirtyPill">clean</span>
|
||||||
@@ -121,7 +187,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<!-- Remove the old buttons row + Info. Replace with one-column table editor -->
|
<!-- Replace old button row + Info with one-column table editor controls -->
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<input id="search" type="search" placeholder="Search (ministry / organization / codes)..." />
|
<input id="search" type="search" placeholder="Search (ministry / organization / codes)..." />
|
||||||
<button id="btnAddMinistry" type="button">Add ministry</button>
|
<button id="btnAddMinistry" type="button">Add ministry</button>
|
||||||
@@ -132,7 +198,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="status" id="status">
|
<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>
|
<span class="hint">Select a ministry (gray row) or an organization row. Edit cells directly. Use the parent “Save” button to commit.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tableWrap">
|
<div class="tableWrap">
|
||||||
@@ -152,10 +218,22 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hidden XML output: on every edit we regenerate XML and send it to parent (best-effort). -->
|
<!-- hidden regenerated XML -->
|
||||||
<textarea id="xmlOut" style="display:none"></textarea>
|
<textarea id="xmlOut" style="display:none"></textarea>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- In-page modal -->
|
||||||
|
<div class="modal-backdrop" id="modalBackdrop" aria-hidden="true">
|
||||||
|
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
||||||
|
<header>
|
||||||
|
<strong id="modalTitle">Modal</strong>
|
||||||
|
<button type="button" id="modalClose">✕</button>
|
||||||
|
</header>
|
||||||
|
<div class="body" id="modalBody"></div>
|
||||||
|
<div class="footer" id="modalFooter"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const els = {
|
const els = {
|
||||||
dirtyPill: document.getElementById('dirtyPill'),
|
dirtyPill: document.getElementById('dirtyPill'),
|
||||||
@@ -170,6 +248,11 @@
|
|||||||
tbody: document.getElementById('tbody'),
|
tbody: document.getElementById('tbody'),
|
||||||
xmlOut: document.getElementById('xmlOut'),
|
xmlOut: document.getElementById('xmlOut'),
|
||||||
grid: document.getElementById('grid'),
|
grid: document.getElementById('grid'),
|
||||||
|
modalBackdrop: document.getElementById('modalBackdrop'),
|
||||||
|
modalTitle: document.getElementById('modalTitle'),
|
||||||
|
modalBody: document.getElementById('modalBody'),
|
||||||
|
modalFooter: document.getElementById('modalFooter'),
|
||||||
|
modalClose: document.getElementById('modalClose'),
|
||||||
};
|
};
|
||||||
|
|
||||||
let payload = null;
|
let payload = null;
|
||||||
@@ -183,6 +266,10 @@
|
|||||||
let selected = { type: null, minIndex: -1, orgIndex: -1 };
|
let selected = { type: null, minIndex: -1, orgIndex: -1 };
|
||||||
let sort = { key: null, dir: 1 }; // dir: 1 asc, -1 desc
|
let sort = { key: null, dir: 1 }; // dir: 1 asc, -1 desc
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
|
}
|
||||||
|
|
||||||
function setDirty(v) {
|
function setDirty(v) {
|
||||||
dirty = !!v;
|
dirty = !!v;
|
||||||
els.dirtyPill.textContent = dirty ? 'dirty' : 'clean';
|
els.dirtyPill.textContent = dirty ? 'dirty' : 'clean';
|
||||||
@@ -195,7 +282,7 @@
|
|||||||
|
|
||||||
function setStatus(msg) {
|
function setStatus(msg) {
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
els.status.innerHTML = '<span class="hint">' + escapeHtml(msg) + '</span>';
|
els.status.textContent = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setXmlStatus(ok, msg) {
|
function setXmlStatus(ok, msg) {
|
||||||
@@ -204,46 +291,88 @@
|
|||||||
els.xmlStatus.classList.toggle('warn', !ok);
|
els.xmlStatus.classList.toggle('warn', !ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(s) {
|
// ---------- Modal (in-page; sandbox-safe) ----------
|
||||||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
function openModal({ title, bodyHtml, buttons }) {
|
||||||
|
els.modalTitle.textContent = title;
|
||||||
|
els.modalBody.innerHTML = bodyHtml;
|
||||||
|
els.modalFooter.innerHTML = '';
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
els.modalBackdrop.style.display = 'none';
|
||||||
|
els.modalBackdrop.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
els.modalClose.onclick = close;
|
||||||
|
els.modalBackdrop.onclick = (e) => { if (e.target === els.modalBackdrop) close(); };
|
||||||
|
|
||||||
|
for (const b of buttons) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.textContent = b.label;
|
||||||
|
if (b.primary) btn.classList.add('primary');
|
||||||
|
btn.onclick = () => b.onClick({ close });
|
||||||
|
els.modalFooter.appendChild(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
els.modalBackdrop.style.display = 'flex';
|
||||||
|
els.modalBackdrop.setAttribute('aria-hidden', 'false');
|
||||||
|
|
||||||
|
const firstInput = els.modalBody.querySelector('input');
|
||||||
|
if (firstInput) setTimeout(() => firstInput.focus(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toAbs(url) {
|
// ---------- Fetch via parent proxy (avoids Origin:null CORS) ----------
|
||||||
try { return new URL(url, window.location.origin).toString(); } catch { return url; }
|
function pgvFetch(url, timeoutMs = 15000) {
|
||||||
}
|
// If not in iframe / no parent proxy, try direct fetch as fallback.
|
||||||
|
const canUseParent = !!(window.parent && window.parent !== window);
|
||||||
|
|
||||||
async function pgvFetch(url) {
|
if (!canUseParent) {
|
||||||
const abs = toAbs(url);
|
return fetch(url, { credentials: 'same-origin' }).then(r => {
|
||||||
const r = await fetch(abs, { credentials: 'same-origin' });
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
if (!r.ok) throw new Error('HTTP ' + r.status + ' for ' + abs);
|
return r.text();
|
||||||
return await r.text();
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
window.removeEventListener('message', onMsg);
|
||||||
|
reject(new Error('PGV_FETCH timeout'));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
function onMsg(ev) {
|
||||||
|
const m = ev.data;
|
||||||
|
if (!m || typeof m !== 'object') return;
|
||||||
|
if (m.type !== 'PGV_FETCH_RESULT') return;
|
||||||
|
if (m.url !== url) return;
|
||||||
|
|
||||||
|
clearTimeout(t);
|
||||||
|
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', url }, '*');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadXmlText() {
|
async function loadXmlText() {
|
||||||
// Prefer apiUrl if present (server-side content resolver), fallback to raw path guess.
|
// Prefer explicit target from payload (raw URL)
|
||||||
if (payload && payload.apiUrl) {
|
if (payload?.targets?.xml) {
|
||||||
const txt = await pgvFetch(payload.apiUrl);
|
return await pgvFetch(payload.targets.xml);
|
||||||
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)
|
// fallback: build raw URL if payload has repoLink/branch/path
|
||||||
if (payload && payload.path) {
|
if (payload?.repoLink && payload?.branch && payload?.path) {
|
||||||
const rawGuess =
|
const raw = `${payload.repoLink}/raw/branch/${payload.branch}/${payload.path}`;
|
||||||
'/' + String(payload.repoLink || '').replace(/^\//,'') +
|
return await pgvFetch(raw);
|
||||||
'/raw/branch/' + (payload.branch || 'main') + '/' + payload.path;
|
|
||||||
return await pgvFetch(rawGuess);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Could not load XML (no apiUrl / raw target).');
|
throw new Error('No payload.targets.xml and cannot build raw URL.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- XML parse/build ----------
|
||||||
function parseXml(xmlText) {
|
function parseXml(xmlText) {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(xmlText, 'application/xml');
|
const doc = parser.parseFromString(xmlText, 'application/xml');
|
||||||
@@ -284,6 +413,25 @@
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatXml(xml) {
|
||||||
|
const PADDING = ' ';
|
||||||
|
const reg = /(>)(<)(\/*)/g;
|
||||||
|
let formatted = '';
|
||||||
|
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 buildXmlFromModel() {
|
function buildXmlFromModel() {
|
||||||
const doc = document.implementation.createDocument('', '', null);
|
const doc = document.implementation.createDocument('', '', null);
|
||||||
const root = doc.createElement('vdvcRegister');
|
const root = doc.createElement('vdvcRegister');
|
||||||
@@ -296,7 +444,6 @@
|
|||||||
for (const m of model.ministries) {
|
for (const m of model.ministries) {
|
||||||
const min = doc.createElement('ministry');
|
const min = doc.createElement('ministry');
|
||||||
if (m.code) min.setAttribute('code', m.code);
|
if (m.code) min.setAttribute('code', m.code);
|
||||||
// Keep name attribute present (even when empty)
|
|
||||||
min.setAttribute('name', m.name || '');
|
min.setAttribute('name', m.name || '');
|
||||||
for (const o of m.organizations) {
|
for (const o of m.organizations) {
|
||||||
const org = doc.createElement('organization');
|
const org = doc.createElement('organization');
|
||||||
@@ -311,243 +458,255 @@
|
|||||||
root.appendChild(min);
|
root.appendChild(min);
|
||||||
}
|
}
|
||||||
|
|
||||||
const xml = new XMLSerializer().serializeToString(doc);
|
return formatXml(new XMLSerializer().serializeToString(doc));
|
||||||
return formatXml(xml);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatXml(xml) {
|
|
||||||
// Basic pretty printer (readable, stable)
|
|
||||||
const PADDING = ' ';
|
|
||||||
const reg = /(>)(<)(\/*)/g;
|
|
||||||
let formatted = '';
|
|
||||||
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 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Grid helpers ----------
|
||||||
function applySort() {
|
function applySort() {
|
||||||
if (!sort.key) return;
|
if (!sort.key) return;
|
||||||
|
|
||||||
const key = sort.key;
|
const key = sort.key;
|
||||||
const dir = sort.dir;
|
const dir = sort.dir;
|
||||||
|
|
||||||
// Sort organizations inside each ministry (keeps grouping)
|
// sort ministries if key is ministry-based
|
||||||
model.ministries.forEach((m) => {
|
if (key === 'ministryCode' || key === 'ministryName') {
|
||||||
m.organizations.sort((a, b) => {
|
model.ministries.sort((a, b) => {
|
||||||
const av =
|
const av = key === 'ministryCode' ? (a.code || '') : (a.name || '');
|
||||||
(key === 'orgName') ? (a.name||'') :
|
const bv = key === 'ministryCode' ? (b.code || '') : (b.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;
|
return String(av).localeCompare(String(bv), undefined, { numeric: true, sensitivity: 'base' }) * dir;
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// sort organizations inside each ministry for org-based keys
|
||||||
|
if (key === 'orgCode' || key === 'nmr' || key === 'docPrefix' || key === 'orgName') {
|
||||||
|
model.ministries.forEach((m) => {
|
||||||
|
m.organizations.sort((a, b) => {
|
||||||
|
const av =
|
||||||
|
key === 'orgCode' ? (a.code || '') :
|
||||||
|
key === 'nmr' ? (a.nmr || '') :
|
||||||
|
key === 'docPrefix' ? (a.docPrefix || '') :
|
||||||
|
(a.name || '');
|
||||||
|
const bv =
|
||||||
|
key === 'orgCode' ? (b.code || '') :
|
||||||
|
key === 'nmr' ? (b.nmr || '') :
|
||||||
|
key === 'docPrefix' ? (b.docPrefix || '') :
|
||||||
|
(b.name || '');
|
||||||
|
return String(av).localeCompare(String(bv), undefined, { numeric: true, sensitivity: 'base' }) * dir;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
const q = (els.search.value || '').trim().toLowerCase();
|
const q = (els.search.value || '').trim().toLowerCase();
|
||||||
|
|
||||||
applySort();
|
applySort();
|
||||||
let rows = flattenRows(model.ministries);
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
els.tbody.innerHTML = '';
|
els.tbody.innerHTML = '';
|
||||||
|
|
||||||
for (const r of rows) {
|
model.ministries.forEach((m, mi) => {
|
||||||
if (r.__type === 'group') {
|
// group row
|
||||||
const m = model.ministries[r.mi];
|
if (!q || `${m.code} ${m.name}`.toLowerCase().includes(q) || m.organizations.some(o => `${o.code} ${o.nmr} ${o.docPrefix} ${o.name}`.toLowerCase().includes(q))) {
|
||||||
const tr = document.createElement('tr');
|
const trG = document.createElement('tr');
|
||||||
tr.className = 'group';
|
trG.className = 'group' + (selected.type === 'ministry' && selected.minIndex === mi ? ' selected' : '');
|
||||||
tr.dataset.type = 'ministry';
|
trG.innerHTML = `
|
||||||
tr.dataset.mi = String(r.mi);
|
<td class="col-mincode"><span class="mono">${escapeHtml(m.code)}</span></td>
|
||||||
tr.innerHTML = `
|
<td class="col-minname" colspan="5">${escapeHtml(m.name)} <span class="muted">(ministry)</span></td>
|
||||||
<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>
|
<td class="col-actions"><button type="button">Select</button></td>
|
||||||
`;
|
`;
|
||||||
tr.addEventListener('click', () => selectMinistry(r.mi));
|
trG.addEventListener('click', () => selectMinistry(mi));
|
||||||
els.tbody.appendChild(tr);
|
els.tbody.appendChild(trG);
|
||||||
continue;
|
|
||||||
|
// org rows
|
||||||
|
const orgs = m.organizations;
|
||||||
|
if (orgs.length === 0) {
|
||||||
|
const trE = document.createElement('tr');
|
||||||
|
trE.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>
|
||||||
|
`;
|
||||||
|
trE.querySelector('button')?.addEventListener('click', (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
selectMinistry(mi);
|
||||||
|
addOrganization();
|
||||||
|
});
|
||||||
|
els.tbody.appendChild(trE);
|
||||||
|
} else {
|
||||||
|
orgs.forEach((o, oi) => {
|
||||||
|
const hay = `${m.code} ${m.name} ${o.code} ${o.nmr} ${o.docPrefix} ${o.name}`.toLowerCase();
|
||||||
|
if (q && !hay.includes(q)) return;
|
||||||
|
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
if (selected.type === 'org' && selected.minIndex === mi && selected.orgIndex === 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(mi, 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// org row
|
|
||||||
const m = model.ministries[r.mi];
|
|
||||||
const o = m.organizations[r.oi];
|
|
||||||
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
tr.dataset.type = 'org';
|
|
||||||
tr.dataset.mi = String(r.mi);
|
|
||||||
tr.dataset.oi = String(r.oi);
|
|
||||||
|
|
||||||
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 selectMinistry(mi) {
|
function selectMinistry(mi) {
|
||||||
selected = { type: 'ministry', minIndex: mi, orgIndex: -1 };
|
selected = { type: 'ministry', minIndex: mi, orgIndex: -1 };
|
||||||
els.btnDelete.disabled = false;
|
els.btnDelete.disabled = false;
|
||||||
|
setStatus(`Selected ministry: ${model.ministries[mi]?.code || ''} ${model.ministries[mi]?.name || ''}`);
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectOrg(mi, oi) {
|
function selectOrg(mi, oi) {
|
||||||
selected = { type: 'org', minIndex: mi, orgIndex: oi };
|
selected = { type: 'org', minIndex: mi, orgIndex: oi };
|
||||||
els.btnDelete.disabled = false;
|
els.btnDelete.disabled = false;
|
||||||
|
const m = model.ministries[mi];
|
||||||
|
const o = m?.organizations?.[oi];
|
||||||
|
setStatus(`Selected org: ${m?.code || ''} → ${o?.code || ''} ${o?.name || ''}`);
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Actions (no prompt/confirm) ----------
|
||||||
function addMinistry() {
|
function addMinistry() {
|
||||||
const code = prompt('Ministry code (e.g., 01):', '');
|
openModal({
|
||||||
if (code === null) return;
|
title: 'Add ministry',
|
||||||
const name = prompt('Ministry name:', '') ?? '';
|
bodyHtml: `
|
||||||
model.ministries.push({ code: (code||'').trim(), name: (name||'').trim(), organizations: [] });
|
<label>Ministry code
|
||||||
selected = { type: 'ministry', minIndex: model.ministries.length - 1, orgIndex: -1 };
|
<input id="mCode" placeholder="e.g., 01" />
|
||||||
onModelChanged();
|
</label>
|
||||||
render();
|
<label>Ministry name
|
||||||
|
<input id="mName" placeholder="e.g., Ministry of ..." />
|
||||||
|
</label>
|
||||||
|
`,
|
||||||
|
buttons: [
|
||||||
|
{ label: 'Cancel', onClick: ({ close }) => close() },
|
||||||
|
{
|
||||||
|
label: 'Add',
|
||||||
|
primary: true,
|
||||||
|
onClick: ({ close }) => {
|
||||||
|
const code = (document.getElementById('mCode').value || '').trim();
|
||||||
|
const name = (document.getElementById('mName').value || '').trim();
|
||||||
|
if (!code) { setStatus('Ministry code is required.'); return; }
|
||||||
|
|
||||||
|
model.ministries.push({ code, name, organizations: [] });
|
||||||
|
selected = { type: 'ministry', minIndex: model.ministries.length - 1, orgIndex: -1 };
|
||||||
|
|
||||||
|
onModelChanged();
|
||||||
|
render();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addOrganization() {
|
function addOrganization() {
|
||||||
let mi = selected.type ? selected.minIndex : -1;
|
const mi = selected.type ? selected.minIndex : -1;
|
||||||
if (mi < 0 || mi >= model.ministries.length) {
|
if (mi < 0 || mi >= model.ministries.length) {
|
||||||
const code = prompt('Add to which ministry code?', '');
|
setStatus('Select a ministry first (click a gray ministry row), then click “Add organization”.');
|
||||||
if (code === null) return;
|
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: '' };
|
openModal({
|
||||||
model.ministries[mi].organizations.push(o);
|
title: 'Add organization',
|
||||||
selected = { type: 'org', minIndex: mi, orgIndex: model.ministries[mi].organizations.length - 1 };
|
bodyHtml: `
|
||||||
onModelChanged();
|
<label>Organization code
|
||||||
render();
|
<input id="oCode" placeholder="e.g., 0001" />
|
||||||
|
</label>
|
||||||
|
<label>NMR
|
||||||
|
<input id="oNmr" placeholder="e.g., 90000038578" />
|
||||||
|
</label>
|
||||||
|
<label>Doc prefix
|
||||||
|
<input id="oPrefix" placeholder="e.g., 01-0001" />
|
||||||
|
</label>
|
||||||
|
<label>Organization name
|
||||||
|
<input id="oName" placeholder="Organization name" />
|
||||||
|
</label>
|
||||||
|
`,
|
||||||
|
buttons: [
|
||||||
|
{ label: 'Cancel', onClick: ({ close }) => close() },
|
||||||
|
{
|
||||||
|
label: 'Add',
|
||||||
|
primary: true,
|
||||||
|
onClick: ({ close }) => {
|
||||||
|
const o = {
|
||||||
|
code: (document.getElementById('oCode').value || '').trim(),
|
||||||
|
nmr: (document.getElementById('oNmr').value || '').trim(),
|
||||||
|
docPrefix: (document.getElementById('oPrefix').value || '').trim(),
|
||||||
|
name: (document.getElementById('oName').value || '').trim(),
|
||||||
|
};
|
||||||
|
model.ministries[mi].organizations.push(o);
|
||||||
|
selected = { type: 'org', minIndex: mi, orgIndex: model.ministries[mi].organizations.length - 1 };
|
||||||
|
|
||||||
|
onModelChanged();
|
||||||
|
render();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSelected() {
|
function deleteSelected() {
|
||||||
if (!selected.type) return;
|
if (!selected.type) return;
|
||||||
if (!confirm('Delete selected item?')) return;
|
|
||||||
|
|
||||||
const mi = selected.minIndex;
|
const msg = selected.type === 'ministry'
|
||||||
if (mi < 0 || mi >= model.ministries.length) return;
|
? 'Delete the selected ministry and all its organizations?'
|
||||||
|
: 'Delete the selected organization?';
|
||||||
|
|
||||||
if (selected.type === 'ministry') {
|
openModal({
|
||||||
model.ministries.splice(mi, 1);
|
title: 'Confirm delete',
|
||||||
selected = { type: null, minIndex: -1, orgIndex: -1 };
|
bodyHtml: `<div>${escapeHtml(msg)}</div>`,
|
||||||
} else if (selected.type === 'org') {
|
buttons: [
|
||||||
const oi = selected.orgIndex;
|
{ label: 'Cancel', onClick: ({ close }) => close() },
|
||||||
const orgs = model.ministries[mi].organizations;
|
{
|
||||||
if (oi >= 0 && oi < orgs.length) orgs.splice(oi, 1);
|
label: 'Delete',
|
||||||
selected = { type: 'ministry', minIndex: mi, orgIndex: -1 };
|
primary: true,
|
||||||
}
|
onClick: ({ close }) => {
|
||||||
|
const mi = selected.minIndex;
|
||||||
|
if (mi < 0 || mi >= model.ministries.length) return;
|
||||||
|
|
||||||
onModelChanged();
|
if (selected.type === 'ministry') {
|
||||||
render();
|
model.ministries.splice(mi, 1);
|
||||||
|
selected = { type: null, minIndex: -1, orgIndex: -1 };
|
||||||
|
} else {
|
||||||
|
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();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onModelChanged() {
|
function onModelChanged() {
|
||||||
@@ -557,12 +716,10 @@
|
|||||||
setXmlStatus(true);
|
setXmlStatus(true);
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
|
|
||||||
// Best-effort sync with host (ignored if host doesn't support it)
|
// Let host know the primary file content changed (host should wire this into Save)
|
||||||
parent.postMessage({ type: 'PGV_SET_CONTENT', path: payload?.path, content: xml }, '*');
|
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) {
|
} catch (e) {
|
||||||
setXmlStatus(false, String(e && e.message ? e.message : e));
|
setXmlStatus(false, String(e?.message || e));
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -588,25 +745,32 @@
|
|||||||
async function initWithPayload(p) {
|
async function initWithPayload(p) {
|
||||||
payload = p || null;
|
payload = p || null;
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
els.meta.textContent = payload ? `${payload.branch || ''} ${payload.path || ''}`.trim() : '';
|
|
||||||
|
// keep the existing look: "main vdvc-register.xml"
|
||||||
|
const br = payload?.branch || '';
|
||||||
|
const path = payload?.path || '';
|
||||||
|
els.meta.textContent = `${br} ${path}`.trim();
|
||||||
|
|
||||||
bindEvents();
|
bindEvents();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setStatus('Loading XML...');
|
||||||
const xmlText = await loadXmlText();
|
const xmlText = await loadXmlText();
|
||||||
model = parseXml(xmlText);
|
model = parseXml(xmlText);
|
||||||
setXmlStatus(true);
|
setXmlStatus(true);
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
|
|
||||||
// Build initial normalized XML output too
|
|
||||||
els.xmlOut.value = buildXmlFromModel();
|
els.xmlOut.value = buildXmlFromModel();
|
||||||
|
setStatus('Loaded. Select a ministry and start editing.');
|
||||||
render();
|
render();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setXmlStatus(false, (e && e.message) ? e.message : String(e));
|
setXmlStatus(false, String(e?.message || e));
|
||||||
setStatus('Failed to load XML. Check console/network.');
|
setStatus('Failed to load XML. (Host must provide PGV_FETCH proxy.)');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Message handling ----
|
||||||
window.addEventListener('message', (ev) => {
|
window.addEventListener('message', (ev) => {
|
||||||
const msg = ev.data;
|
const msg = ev.data;
|
||||||
if (!msg || typeof msg !== 'object') return;
|
if (!msg || typeof msg !== 'object') return;
|
||||||
@@ -617,9 +781,15 @@
|
|||||||
|
|
||||||
if (msg.type === 'PGV_COMMIT_OK') {
|
if (msg.type === 'PGV_COMMIT_OK') {
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
|
setStatus('Saved (commit) OK.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'PGV_COMMIT_ERR') {
|
||||||
|
setStatus('Save (commit) failed.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Boot handshake
|
||||||
parent.postMessage({ type: 'PGV_READY' }, '*');
|
parent.postMessage({ type: 'PGV_READY' }, '*');
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user