/** * ZDDC Classifier — target-tree pane (Classify & Copy mode). * * Renders the two orthogonal target trees the user maps files onto: * - "By tracking number": folders that join with "-" into the tracking * number; the leaf folder ("A (IFR)") is the revision+status. * - "By transmittal": /{received,issued}/. * * Structure here, placements in classify.js. Drag-and-drop assignment is wired * in source-dnd.js / phase 3; this module owns rendering + folder/bin CRUD and * shows the derived filename for each placed file. */ (function () { 'use strict'; var SLOTS = ['received', 'issued']; var els = {}; var collapsed = {}; // nodeId -> true when collapsed (default expanded) var openForm = null; // { partyId, slot } when a bin form is open var initialized = false; function init() { if (initialized) return; initialized = true; els = { trackingTab: document.getElementById('trackingTab'), transmittalTab: document.getElementById('transmittalTab'), trackingPanel: document.getElementById('trackingPanel'), transmittalPanel: document.getElementById('transmittalPanel'), trackingTree: document.getElementById('trackingTree'), transmittalTree: document.getElementById('transmittalTree'), addTrackingRootBtn: document.getElementById('addTrackingRootBtn'), addPartyBtn: document.getElementById('addPartyBtn'), stats: document.getElementById('classifyStats'), }; els.trackingTab.addEventListener('click', function () { showTab('tracking'); }); els.transmittalTab.addEventListener('click', function () { showTab('transmittal'); }); els.addTrackingRootBtn.addEventListener('click', function () { var name = prompt('Root folder name (a tracking-number segment, e.g. "ACME-PROJ"):', ''); if (name && name.trim()) C().addTrackingNode(null, name.trim()); }); els.addPartyBtn.addEventListener('click', function () { var name = prompt('Party name (also the transmittal-number prefix):', ''); if (name && name.trim()) C().addParty(name.trim()); }); els.trackingTree.addEventListener('click', onTrackingClick); els.transmittalTree.addEventListener('click', onTransmittalClick); setupDropZone(els.trackingTree, 'tracking'); setupDropZone(els.transmittalTree, 'transmittal'); C().on(render); if (window.app.modules.store && window.app.modules.store.on) { window.app.modules.store.on('files', render); } render(); } function C() { return window.app.modules.classify; } function allFiles() { var s = window.app.modules.store; return s && s.getAllFiles ? s.getAllFiles() : []; } function showTab(which) { var t = which === 'transmittal'; els.trackingTab.classList.toggle('active', !t); els.transmittalTab.classList.toggle('active', t); els.trackingPanel.hidden = t; els.transmittalPanel.hidden = !t; } // ── render ─────────────────────────────────────────────────────────────── function render() { if (!initialized || !C().isEnabled()) return; var files = allFiles(); renderTrackingInto(els.trackingTree, C().getTrackingTree(), files); renderTransmittalInto(els.transmittalTree, C().getTransmittalTree(), files); renderStats(files); } function renderStats(files) { if (!els.stats) return; var s = C().stats(files); els.stats.textContent = s.done + ' done · ' + s.partial + ' in progress · ' + s.none + ' unassigned · ' + s.excluded + ' excluded'; } function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; } function nodeActions(extra) { var wrap = el('span', 'tnode__actions'); (extra || []).forEach(function (a) { var b = el('button', 'tnode__act', a.label); b.dataset.act = a.act; b.title = a.title || ''; wrap.appendChild(b); }); return wrap; } function fileList(files) { var box = el('div', 'tnode__files'); files.forEach(function (f) { var d = C().deriveTarget(f); var row = el('div', 'tfile' + (d.errors.length ? ' tfile--err' : '')); row.title = d.errors.length ? d.errors.join('; ') : ''; row.appendChild(el('span', 'tfile__orig', f.originalFilename + (f.extension ? '.' + f.extension : ''))); row.appendChild(el('span', 'tfile__arrow', '→')); row.appendChild(el('span', 'tfile__name', d.filename || '(incomplete)')); box.appendChild(row); }); return box; } // Tracking tree (recursive) function renderTrackingInto(container, nodes, files) { container.textContent = ''; if (!nodes.length) { container.appendChild(el('div', 'target-empty', 'No tracking folders yet — “+ Root folder” to start.')); return; } nodes.forEach(function (n) { container.appendChild(trackingNode(n, files)); }); } function trackingNode(n, files) { var isLeaf = (n.children || []).length === 0; var wrap = el('div', 'tnode' + (isLeaf ? ' tnode--leaf' : '')); wrap.dataset.id = n.id; var row = el('div', 'tnode__row'); var toggle = el('button', 'tnode__toggle', isLeaf ? '·' : (collapsed[n.id] ? '▸' : '▾')); if (!isLeaf) toggle.dataset.act = 'toggle'; row.appendChild(toggle); row.appendChild(el('span', 'tnode__name', n.name)); var placed = C().filesInNode(n.id, 'tracking', files); if (placed.length) row.appendChild(el('span', 'tnode__badge', String(placed.length))); row.appendChild(nodeActions([ { act: 'add', label: '+', title: 'Add child folder' }, { act: 'rename', label: '✎', title: 'Rename' }, { act: 'del', label: '🗑', title: 'Delete' }, ])); wrap.appendChild(row); if (placed.length) wrap.appendChild(fileList(placed)); if (!isLeaf && !collapsed[n.id]) { var kids = el('div', 'tnode__children'); (n.children || []).forEach(function (c) { kids.appendChild(trackingNode(c, files)); }); wrap.appendChild(kids); } return wrap; } // Transmittal tree function renderTransmittalInto(container, parties, files) { container.textContent = ''; if (!parties.length) { container.appendChild(el('div', 'target-empty', 'No parties yet — “+ Party” to start.')); return; } parties.forEach(function (p) { container.appendChild(partyNode(p, files)); }); } function partyNode(party, files) { var wrap = el('div', 'tnode tnode--party'); wrap.dataset.id = party.id; var row = el('div', 'tnode__row'); row.appendChild(el('span', 'tnode__icon', '🏢')); row.appendChild(el('span', 'tnode__name', party.name)); row.appendChild(nodeActions([ { act: 'rename-party', label: '✎', title: 'Rename party' }, { act: 'del-party', label: '🗑', title: 'Delete party' }, ])); wrap.appendChild(row); SLOTS.forEach(function (slot) { var slotNode = (party.children || []).filter(function (s) { return s.slot === slot; })[0]; var sw = el('div', 'tslot'); sw.dataset.party = party.id; sw.dataset.slot = slot; var sr = el('div', 'tslot__row'); sr.appendChild(el('span', 'tslot__name', slot)); var addBtn = el('button', 'tnode__act', '+ Transmittal'); addBtn.dataset.act = 'addbin'; sr.appendChild(addBtn); sw.appendChild(sr); if (openForm && openForm.partyId === party.id && openForm.slot === slot) { sw.appendChild(binForm(party.id, slot)); } (slotNode ? slotNode.children : []).forEach(function (bin) { sw.appendChild(binNode(bin, files)); }); wrap.appendChild(sw); }); return wrap; } function binNode(bin, files) { var wrap = el('div', 'tnode tnode--bin'); wrap.dataset.id = bin.id; var row = el('div', 'tnode__row'); row.appendChild(el('span', 'tnode__name', bin.name || '(invalid — set date/seq)')); var placed = C().filesInNode(bin.id, 'transmittal', files); if (placed.length) row.appendChild(el('span', 'tnode__badge', String(placed.length))); row.appendChild(nodeActions([{ act: 'del', label: '🗑', title: 'Delete transmittal' }])); wrap.appendChild(row); if (placed.length) wrap.appendChild(fileList(placed)); return wrap; } var STATUSES = ['---', 'IFA', 'IFB', 'IFC', 'IFD', 'IFI', 'IFP', 'IFR', 'IFU', 'REC', 'RSA', 'RSB', 'RSC', 'RSD', 'RSI', 'TBD']; function binForm(partyId, slot) { var form = el('div', 'binform'); form.dataset.party = partyId; form.dataset.slot = slot; var date = el('input', 'binform__date'); date.type = 'date'; try { date.value = new Date().toISOString().slice(0, 10); } catch (_) { /* ok */ } var type = document.createElement('select'); type.className = 'binform__type'; ['TRN', 'SUB'].forEach(function (t) { var o = el('option', null, t); o.value = t; type.appendChild(o); }); var seq = el('input', 'binform__seq'); seq.type = 'text'; seq.placeholder = 'seq (e.g. 0007)'; var status = document.createElement('select'); status.className = 'binform__status'; STATUSES.forEach(function (s) { var o = el('option', null, s); o.value = s; status.appendChild(o); }); var title = el('input', 'binform__title'); title.type = 'text'; title.placeholder = 'title (optional)'; var add = el('button', 'btn btn-sm btn-primary', 'Add'); add.dataset.act = 'binadd'; var cancel = el('button', 'btn btn-sm btn-secondary', 'Cancel'); cancel.dataset.act = 'bincancel'; [date, type, seq, status, title, add, cancel].forEach(function (n) { form.appendChild(n); }); return form; } // ── events ───────────────────────────────────────────────────────────── function closestNodeId(target) { var n = target.closest('.tnode'); return n ? n.dataset.id : null; } function onTrackingClick(e) { var btn = e.target.closest('[data-act]'); if (!btn) return; var act = btn.dataset.act; var id = closestNodeId(btn); if (act === 'toggle') { collapsed[id] = !collapsed[id]; render(); return; } if (act === 'add') { var name = prompt('Child folder name (next tracking segment, or a leaf revision like "A (IFR)"):', ''); if (name && name.trim()) C().addTrackingNode(id, name.trim()); } else if (act === 'rename') { var node = C().getNode(id); var nn = prompt('Rename folder:', node ? node.name : ''); if (nn && nn.trim()) C().renameNode(id, nn.trim()); } else if (act === 'del') { if (confirm('Delete this folder and everything under it? Files placed here become unassigned.')) C().deleteNode(id); } } function onTransmittalClick(e) { var btn = e.target.closest('[data-act]'); if (!btn) return; var act = btn.dataset.act; if (act === 'addbin') { var slotEl = btn.closest('.tslot'); openForm = { partyId: slotEl.dataset.party, slot: slotEl.dataset.slot }; render(); return; } if (act === 'bincancel') { openForm = null; render(); return; } if (act === 'binadd') { var form = btn.closest('.binform'); var meta = { date: form.querySelector('.binform__date').value, type: form.querySelector('.binform__type').value, seq: form.querySelector('.binform__seq').value.trim(), status: form.querySelector('.binform__status').value, title: form.querySelector('.binform__title').value.trim(), }; if (!meta.date || !meta.seq) { window.zddc.toast('Transmittal needs at least a date and a sequence number.', 'warning'); return; } C().addTransmittalBin(form.dataset.party, form.dataset.slot, meta); openForm = null; // render() fires from classify.notify() return; } var id = closestNodeId(btn); if (act === 'rename-party') { var node = C().getNode(id); var nn = prompt('Rename party (re-derives its transmittal numbers):', node ? node.name : ''); if (nn && nn.trim()) C().renameNode(id, nn.trim()); } else if (act === 'del-party') { if (confirm('Delete this party and all its transmittals? Files placed there become unassigned.')) C().deleteNode(id); } else if (act === 'del') { if (confirm('Delete this transmittal? Files placed here become unassigned.')) C().deleteNode(id); } } // ── drop targets ─────────────────────────────────────────────────────── // Resolve the drop target under an event: // tracking → any folder node (.tnode) // transmittal → a transmittal bin only (.tnode--bin) function dropTarget(target, axis) { var sel = axis === 'transmittal' ? '.tnode--bin' : '.tnode'; var node = target.closest(sel); if (!node || !node.dataset.id) return null; return { id: node.dataset.id, row: node.querySelector('.tnode__row') || node }; } function clearHover(container) { var hot = container.querySelectorAll('.drop-hover'); for (var i = 0; i < hot.length; i++) hot[i].classList.remove('drop-hover'); } function setupDropZone(container, axis) { container.addEventListener('dragover', function (e) { if (!window.app.modules.dnd.active()) return; var t = dropTarget(e.target, axis); clearHover(container); if (!t) return; e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; t.row.classList.add('drop-hover'); }); container.addEventListener('dragleave', function (e) { if (e.target === container) clearHover(container); }); container.addEventListener('drop', function (e) { var t = dropTarget(e.target, axis); clearHover(container); if (!t) return; e.preventDefault(); var keys = window.app.modules.dnd.getDrag(); window.app.modules.dnd.clearDrag(); if (keys.length) C().place(keys, t.id, axis); }); } window.app.modules.targetTree = { init: init, render: render, showTab: showTab, }; })();