/** * ZDDC Classifier — instantiate MDL deliverables from existing archive files. * * Catch-up flow: the archive already holds issued documents, but the Master * Deliverables List is empty. This reads a project's archive subtree as a flat * file list, lets the user build a selection set (autofilter + ctrl-shift via * the shared seltable), dedupes the selected files to one deliverable per * tracking number, and PUTs a new deliverable .yaml into the originator's * `archive//mdl/` on the server. Server-only (needs http + auth). * * A deliverable .yaml's filename IS its tracking number; the server pins * `originator` from the folder and composes the filename, so the body carries * only project/discipline/type/sequence/suffix + title. */ (function () { 'use strict'; if (!window.app) window.app = {}; if (!window.app.modules) window.app.modules = {}; function T(m, l, o) { if (window.zddc && window.zddc.toast) window.zddc.toast(m, l, o); } function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; } // ── pure core (test seams) ─────────────────────────────────────────────── // A tracking number → deliverable {tracking, originator, body{...}} or null // if it doesn't fit the MDL schema (needs orig-proj-disc-type-seq, + suffix). function deliverableFromFile(f) { var segs = String(f.tracking || '').split('-'); if (segs.length < 5) return null; var body = { project: segs[1], discipline: segs[2], type: segs[3], sequence: segs[4], title: f.title || '' }; if (segs.length >= 6) body.suffix = segs.slice(5).join('-'); return { tracking: f.tracking, originator: segs[0], body: body }; } // Dedupe a list of archive files to one deliverable per tracking number. function dedupe(files) { var seen = Object.create(null), out = []; (files || []).forEach(function (f) { if (seen[f.tracking]) return; var d = deliverableFromFile(f); if (d) { seen[f.tracking] = true; out.push(d); } }); return out; } // Recursively walk an archive directory handle → flat list of ZDDC-named // files (skips dot/underscore folders; non-ZDDC names like the mdl yamls // naturally fall out because parseFilename rejects them). async function walkArchive(rootHandle) { var out = []; async function walk(dirH, parts) { for await (var entry of dirH.values()) { var nm = String(entry.name || '').replace(/\/$/, ''); if (entry.kind === 'directory') { var c = nm.charAt(0); if (c === '.' || c === '_' || nm === 'mdl' || nm === 'rsk') continue; var childH = await dirH.getDirectoryHandle(nm); await walk(childH, parts.concat(nm)); } else { var p = window.zddc.parseFilename(nm); if (p && p.valid) { out.push({ id: parts.concat(nm).join('/'), party: parts[0] || '', slot: parts[1] || '', transmittal: parts[2] || '', name: nm, tracking: p.trackingNumber, revision: p.revision, status: p.status, title: p.title, }); } } } } await walk(rootHandle, []); return out; } // Write one deliverable into //mdl/.yaml. // Returns 'created' | 'skipped' (already present). Throws on server error. async function instantiateOne(archiveRoot, d) { var dir = await archiveRoot.getDirectoryHandle(d.originator, { create: true }); dir = await dir.getDirectoryHandle('mdl', { create: true }); var fname = d.tracking + '.yaml'; try { await dir.getFileHandle(fname); return 'skipped'; } catch (e) { /* NotFound → create */ } var yaml = window.jsyaml.dump(d.body); var fh = await dir.getFileHandle(fname, { create: true }); var w = await fh.createWritable(); await w.write(new Blob([yaml], { type: 'application/yaml' })); await w.close(); return 'created'; } async function instantiateAll(archiveRoot, deliverables, onProgress) { var s = { created: 0, skipped: 0, errors: 0 }; for (var i = 0; i < deliverables.length; i++) { if (onProgress) onProgress(i + 1, deliverables.length, deliverables[i].tracking); try { s[await instantiateOne(archiveRoot, deliverables[i])]++; } catch (e) { s.errors++; T('Failed to create ' + deliverables[i].tracking + ' — ' + (e.message || e), 'error'); } } return s; } // ── UI ─────────────────────────────────────────────────────────────────── var overlay = null, statusEl = null, table = null, files = [], archiveRoot = null; function close() { if (overlay) { overlay.remove(); overlay = null; table = null; } } function setStatus(t) { if (statusEl) statusEl.textContent = t; } async function open() { var copy = window.app.modules.copy; var src = window.zddc && window.zddc.source; if (!src || location.protocol === 'file:') { T('Populating the MDL from the archive needs the classifier served by a zddc-server (open it over http).', 'error'); return; } var projects = await copy.fetchAccessProjects(); if (projects == null) { T('Could not load your projects from the server.', 'error'); return; } if (!projects.length) { T('No projects you can access on this server.', 'warning'); return; } var proj = await copy.chooseProject(projects); if (!proj) return; buildOverlay(proj); await scan(proj); } function buildOverlay(proj) { close(); overlay = el('div', 'mdl-overlay'); var box = el('div', 'mdl-overlay__box'); var head = el('div', 'mdl-overlay__head'); head.appendChild(el('h2', null, 'Populate MDL from archive — ' + (proj.title || proj.name))); var x = el('button', 'mdl-overlay__close', '×'); x.title = 'Close'; x.addEventListener('click', close); head.appendChild(x); box.appendChild(head); statusEl = el('div', 'mdl-overlay__status', 'Scanning archive…'); box.appendChild(statusEl); var host = el('div', 'mdl-overlay__table'); box.appendChild(host); var foot = el('div', 'mdl-overlay__foot'); var create = el('button', 'btn btn-primary', 'Create deliverables'); create.addEventListener('click', function () { runCreate(create); }); foot.appendChild(create); var cancel = el('button', 'btn btn-secondary', 'Close'); cancel.addEventListener('click', close); foot.appendChild(cancel); box.appendChild(foot); overlay.appendChild(box); document.body.appendChild(overlay); table = window.app.modules.seltable.create({ container: host, filterPlaceholder: 'Filter by party, transmittal, tracking number, title…', rows: function () { return files; }, rowId: function (r) { return r.id; }, columns: [ { key: 'party', title: 'Party' }, { key: 'slot', title: 'Slot' }, { key: 'transmittal', title: 'Transmittal' }, { key: 'tracking', title: 'Tracking number' }, { key: 'revision', title: 'Rev', get: function (r) { return r.revision + (r.status ? ' (' + r.status + ')' : ''); } }, { key: 'title', title: 'Title' }, ], onSelectionChange: function (ids) { create.textContent = ids.length ? ('Create deliverables (' + dedupe(selectedFiles(ids)).length + ')') : 'Create deliverables'; }, }); table.render(); } function selectedFiles(ids) { var set = {}; ids.forEach(function (i) { set[i] = true; }); return files.filter(function (f) { return set[f.id]; }); } async function scan(proj) { var src = window.zddc.source; var rel = (proj.url || ('/' + proj.name + '/')); if (rel.charAt(rel.length - 1) !== '/') rel += '/'; try { archiveRoot = new src.HttpDirectoryHandle(new URL(rel + 'archive/', location.origin).href, 'archive'); setStatus('Scanning archive…'); files = await walkArchive(archiveRoot); table.renderBody(); setStatus(files.length + ' document file' + (files.length === 1 ? '' : 's') + ' found. Filter + ctrl-shift select, then “Create deliverables”.'); } catch (e) { setStatus('Archive scan failed — ' + (e.message || e)); T('Archive scan failed — ' + (e.message || e), 'error'); } } async function runCreate(btn) { if (!table) return; var sel = table.getSelection(); if (!sel.length) { T('Select some archive files first (filter + ctrl-shift).', 'warning'); return; } var deliverables = dedupe(selectedFiles(sel)); if (!deliverables.length) { T('None of the selected files have a tracking number that fits the deliverable schema.', 'warning'); return; } if (!confirm('Create ' + deliverables.length + ' deliverable(s) in the project MDL?\n\n' + 'One .yaml per tracking number, in archive//mdl/. Already-present ones are skipped.')) return; btn.disabled = true; var s = await instantiateAll(archiveRoot, deliverables, function (i, n, tn) { setStatus('Creating ' + i + '/' + n + ' — ' + tn); }); btn.disabled = false; setStatus(s.created + ' created, ' + s.skipped + ' already there' + (s.errors ? (', ' + s.errors + ' failed') : '') + '. ' + files.length + ' files scanned.'); T('MDL: ' + s.created + ' created, ' + s.skipped + ' already there' + (s.errors ? (', ' + s.errors + ' failed') : '') + '.', s.errors ? 'warning' : 'success'); } window.app.modules.mdlInstantiate = { open: open, // test seams deliverableFromFile: deliverableFromFile, dedupe: dedupe, walkArchive: walkArchive, instantiateOne: instantiateOne, instantiateAll: instantiateAll, }; })();