Each file needs a tracking number (revision + status + title) and a transmittal folder. Name it — build one under By tracking number or reuse one under By existing — then route it under By transmittal.
+
Each file needs a tracking number (revision + status + title) and a transmittal folder. Name it — build one under By tracking number, or drag onto a row under From a list (loaded from the archive/MDL or pasted from Excel) — then route it under By transmittal.
-
+
@@ -2536,14 +2581,19 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
placeholder="Filter the transmittal tree…" aria-label="Filter transmittal tree">
-
+
-
- “Load…”, tick the directories to scan, then drag files onto a row to assign its tracking number; set the Revision column (ctrl-shift select + ctrl-Enter to set many).
+
+
+
+
+
+ Drag files onto a row to name them; edit the Tracking number / Revision inline (ctrl-shift + ctrl-Enter sets many). Clearing the list keeps every assignment.
@@ -7475,7 +7525,7 @@ X.B(E,Y);return E}return J}())
function assignmentFor(key) {
var a = state.assignments[key];
if (!a) {
- a = { trackingNodeId: null, transmittalNodeId: null, mdlNodeId: null, excluded: false, titleOverride: null, titleFromDeliverable: true };
+ a = { trackingNodeId: null, transmittalNodeId: null, excluded: false, titleOverride: null };
state.assignments[key] = a;
}
return a;
@@ -7484,22 +7534,19 @@ X.B(E,Y);return E}return J}())
function getAssignment(key) { return state.assignments[key] || null; }
function cleanAssignment(key) {
var a = state.assignments[key];
- if (a && !a.trackingNodeId && !a.transmittalNodeId && !a.mdlNodeId && !a.excluded && !a.titleOverride) {
+ if (a && !a.trackingNodeId && !a.transmittalNodeId && !a.excluded && !a.titleOverride) {
delete state.assignments[key];
}
}
// Place keys onto a node along one axis ('tracking' | 'transmittal').
- // nodeId null clears that axis.
+ // nodeId null clears that axis. (The "From a list" tab also produces
+ // 'tracking' placements — see assignFromRow.)
function place(keys, nodeId, axis) {
- var field = axis === 'transmittal' ? 'transmittalNodeId' : axis === 'mdl' ? 'mdlNodeId' : 'trackingNodeId';
+ var field = axis === 'transmittal' ? 'transmittalNodeId' : 'trackingNodeId';
keys.forEach(function (k) {
var a = assignmentFor(k);
a[field] = nodeId || null;
- // Tracking and MDL are alternative NAME sources — placing on one
- // clears the other so the file has a single name origin.
- if (axis === 'mdl' && nodeId) a.trackingNodeId = null;
- else if (axis === 'tracking' && nodeId) a.mdlNodeId = null;
a.excluded = false; // placing un-excludes
cleanAssignment(k);
});
@@ -7510,7 +7557,7 @@ X.B(E,Y);return E}return J}())
keys.forEach(function (k) {
var a = assignmentFor(k);
a.excluded = !!excluded;
- if (excluded) { a.trackingNodeId = null; a.transmittalNodeId = null; a.mdlNodeId = null; }
+ if (excluded) { a.trackingNodeId = null; a.transmittalNodeId = null; }
cleanAssignment(k);
});
clearHashConflicts();
@@ -7550,9 +7597,8 @@ X.B(E,Y);return E}return J}())
});
});
});
- (state.mdlList || []).forEach(function (row) {
- nodeIndex[row.id] = { node: row, kind: 'mdl', parent: null };
- });
+ // Scratch-list rows are NOT placement targets — drops materialize real
+ // tracking-tree nodes (assignFromRow), so the list isn't indexed here.
}
function getNode(id) { return nodeIndex[id] ? nodeIndex[id].node : null; }
function infoFor(id) { return nodeIndex[id] || null; }
@@ -7685,26 +7731,10 @@ X.B(E,Y);return E}return J}())
};
if (out.excluded) return out;
- // Axis 1 — NAME. An MDL deliverable (alternative to the tracking tree)
- // supplies the tracking number + title; its revision comes from the
- // classifier-local revision cell. Otherwise the tracking tree.
- if (a.mdlNodeId) {
- var mi = infoFor(a.mdlNodeId);
- if (mi && mi.kind === 'mdl') {
- var row = mi.node;
- out.tracking = row.trackingNumber || '';
- var ml = parseLeafLabel(row.revisionCell || '');
- out.revision = ml.revision; out.status = ml.status;
- out.trackingLeaf = true;
- if (!a.titleOverride && a.titleFromDeliverable !== false && row.title) out.title = row.title;
- if (!out.tracking) out.errors.push('deliverable has no tracking number');
- if (!out.revision) out.errors.push('set a revision for this deliverable (e.g. "A (IFR)")');
- else if (out.status && !zddc.isValidStatus(out.status)) out.errors.push('unknown status "' + out.status + '"');
- else if (!out.status) out.errors.push('revision needs a "(STATUS)" — e.g. "A (IFR)"');
- } else {
- out.errors.push('deliverable no longer loaded');
- }
- } else if (a.trackingNodeId) {
+ // Axis 1 — NAME, always the tracking tree. The "From a list" tab drops
+ // also produce tracking-tree placements (assignFromRow), so there is a
+ // single name origin.
+ if (a.trackingNodeId) {
var ti = infoFor(a.trackingNodeId);
if (ti && ti.kind === 'tracking') {
var chain = trackingChain(ti); // [root … node]
@@ -7804,10 +7834,33 @@ X.B(E,Y);return E}return J}())
state.transmittalTree = obj.transmittalTree || [];
state.outputName = obj.outputName || null;
state.config = normalizeConfig(obj.config);
- state.mdlList = Array.isArray(obj.mdlList) ? obj.mdlList : [];
+ state.mdlList = (Array.isArray(obj.mdlList) ? obj.mdlList : []).map(normalizeRow);
rebuildIndex();
+ migrateLegacyMdl(obj.mdlList); // BEFORE anything can prune; materializes old mdl placements
notify();
}
+ // Pre-"From a list" workspaces stored a separate `mdlNodeId` axis pointing at
+ // a row id. Materialize each into a real tracking placement so the
+ // classification survives the model change, then drop the dead fields.
+ function migrateLegacyMdl(rawRows) {
+ var byOldId = Object.create(null);
+ (rawRows || []).forEach(function (r) { if (r && r.id) byOldId[r.id] = r; });
+ Object.keys(state.assignments).forEach(function (k) {
+ var a = state.assignments[k];
+ if (!a) return;
+ if (a.mdlNodeId) {
+ var r = byOldId[a.mdlNodeId];
+ if (!a.trackingNodeId && r && r.trackingNumber) {
+ var leaf = addTrackingPath(null, parseFolderLevels(r.trackingNumber + '_' + (r.revisionCell || PENDING_REV)));
+ a.trackingNodeId = leaf;
+ if (!a.titleOverride && r.title && a.titleFromDeliverable !== false) a.titleOverride = r.title;
+ }
+ delete a.mdlNodeId;
+ }
+ if ('titleFromDeliverable' in a) delete a.titleFromDeliverable;
+ cleanAssignment(k);
+ });
+ }
// Reset clears the CLASSIFICATION but keeps the pattern config — it's a
// per-project setting, not part of the data being cleared.
function reset() {
@@ -7834,41 +7887,192 @@ X.B(E,Y);return E}return J}())
function getTrackingFields() { return state.config.trackingFields; }
function setConfig(c) { state.config = normalizeConfig(c); notify(); }
- // ── MDL deliverables (the "By MDL" drop-target axis) ─────────────────────
- function setMdlList(rows) {
- state.mdlList = (rows || []).map(function (r) {
- return {
- id: r.id || uid(), party: r.party || '',
- trackingNumber: r.trackingNumber || '', title: r.title || '',
- inMdl: !!r.inMdl,
- archiveRevisions: Array.isArray(r.archiveRevisions) ? r.archiveRevisions : [],
- revisionCell: r.revisionCell || '',
- };
+ // ── "From a list" scratch worklist ───────────────────────────────────────
+ // A temporary list of known/typed tracking numbers (from the archive/MDL, a
+ // paste, or a name-match). Dropping a file on a row MATERIALIZES a real
+ // tracking-tree placement (assignFromRow) — the list is pure input, so it can
+ // be cleared without losing any classification. `placed` is a transient
+ // row→keys hint (not the source of truth, not serialized) used to re-stamp a
+ // row's files when its tracking number / revision is edited.
+ function rowSource(r) {
+ if (r && r.source) return { mdl: !!r.source.mdl, archive: !!r.source.archive, pasted: !!r.source.pasted };
+ return { mdl: !!(r && r.inMdl), archive: !!(r && Array.isArray(r.archiveRevisions) && r.archiveRevisions.length), pasted: false };
+ }
+ function normalizeRow(r) {
+ r = r || {};
+ return {
+ id: r.id || uid(), party: r.party || '',
+ trackingNumber: (r.trackingNumber || '').trim(), title: r.title || '',
+ revisionCell: r.revisionCell || '',
+ source: rowSource(r),
+ archiveRevisions: Array.isArray(r.archiveRevisions) ? r.archiveRevisions : [],
+ placed: Object.create(null),
+ };
+ }
+ function setMdlList(rows) { state.mdlList = (rows || []).map(normalizeRow); notify(); }
+ function appendMdlRows(rows) {
+ var byTn = Object.create(null);
+ state.mdlList.forEach(function (r) { if (r.trackingNumber) byTn[r.trackingNumber] = r; });
+ (rows || []).forEach(function (raw) {
+ var r = normalizeRow(raw), ex = r.trackingNumber ? byTn[r.trackingNumber] : null;
+ if (ex) {
+ if (!ex.revisionCell && r.revisionCell) ex.revisionCell = r.revisionCell;
+ if (!ex.title && r.title) ex.title = r.title;
+ ex.source.mdl = ex.source.mdl || r.source.mdl;
+ ex.source.archive = ex.source.archive || r.source.archive;
+ ex.source.pasted = ex.source.pasted || r.source.pasted;
+ if (r.archiveRevisions.length && !ex.archiveRevisions.length) ex.archiveRevisions = r.archiveRevisions;
+ } else {
+ state.mdlList.push(r);
+ if (r.trackingNumber) byTn[r.trackingNumber] = r;
+ }
});
- // Drop placements pointing at deliverables no longer loaded.
- var valid = Object.create(null);
- state.mdlList.forEach(function (r) { valid[r.id] = true; });
- Object.keys(state.assignments).forEach(function (k) {
- var a = state.assignments[k];
- if (a.mdlNodeId && !valid[a.mdlNodeId]) { a.mdlNodeId = null; cleanAssignment(k); }
- });
- rebuildIndex();
notify();
}
+ function clearMdlList() { state.mdlList = []; notify(); } // rows only — assignments survive
function getMdlList() { return state.mdlList; }
- function getMdlRow(id) { var i = infoFor(id); return (i && i.kind === 'mdl') ? i.node : null; }
+ function getMdlRow(id) { return state.mdlList.filter(function (r) { return r.id === id; })[0] || null; }
+
+ // Build (creating folders as needed) the tracking-tree leaf a row points at:
+ // "_". A tracking number's last segment is an
+ // ancestor and the revision is ALWAYS the leaf, so a blank revision gets a
+ // PENDING_REV placeholder leaf (the file shows incomplete until set; the
+ // placeholder is pruned when the real revision lands).
+ var PENDING_REV = 'pending';
+ function leafForRow(row) {
+ var tn = (row.trackingNumber || '').trim();
+ if (!tn) return null;
+ var rev = (row.revisionCell || '').trim() || PENDING_REV;
+ return addTrackingPath(null, parseFolderLevels(tn + '_' + rev));
+ }
+ function assignFromRow(keys, row) {
+ var leaf = leafForRow(row);
+ if (!leaf || !keys || !keys.length) return;
+ place(keys, leaf, 'tracking');
+ keys.forEach(function (k) {
+ row.placed[k] = true;
+ if (row.title && row.title.trim()) {
+ var a = state.assignments[k];
+ if (a && !a.titleOverride) setTitleOverride(k, row.title);
+ }
+ });
+ notify();
+ }
+ function nodeHasFiles(nodeId) {
+ for (var k in state.assignments) { if (state.assignments[k].trackingNodeId === nodeId) return true; }
+ return false;
+ }
+ // Delete a now-empty tracking leaf and any ancestors it leaves empty (so a
+ // re-stamp doesn't litter the By-tracking tree with stale folders).
+ function pruneEmptyTrackingChain(nodeId) {
+ var info = infoFor(nodeId);
+ while (info && info.kind === 'tracking') {
+ if ((info.node.children || []).length || nodeHasFiles(info.node.id)) break;
+ var parent = info.parent; // parent NODE (or null)
+ deleteNode(info.node.id); // rebuilds the index
+ info = parent ? infoFor(parent.id) : null;
+ }
+ }
+ // Re-point a row's already-dropped files at the row's current leaf (after its
+ // tracking number or revision was edited). Skips keys the user has since
+ // un-placed elsewhere; prunes the leaves it empties.
+ function restampRow(row) {
+ var keys = Object.keys(row.placed || {});
+ if (!keys.length) return;
+ var leaf = leafForRow(row);
+ if (!leaf) return;
+ var old = Object.create(null);
+ keys.forEach(function (k) {
+ var a = state.assignments[k];
+ if (a && a.trackingNodeId) { if (a.trackingNodeId !== leaf) old[a.trackingNodeId] = true; a.trackingNodeId = leaf; }
+ else delete row.placed[k]; // user un-placed it elsewhere — don't resurrect
+ });
+ clearHashConflicts();
+ Object.keys(old).forEach(pruneEmptyTrackingChain);
+ notify();
+ }
+ function unassignRowFile(row, key) {
+ var a = state.assignments[key], old = a ? a.trackingNodeId : null;
+ if (row && row.placed) delete row.placed[key];
+ place([key], null, 'tracking');
+ if (old) pruneEmptyTrackingChain(old);
+ }
+ function setRowTracking(rowId, tn) {
+ var r = getMdlRow(rowId); if (!r) return;
+ r.trackingNumber = (tn == null ? '' : String(tn)).trim();
+ restampRow(r); notify();
+ }
+ function setRowTitle(rowId, title) {
+ var r = getMdlRow(rowId); if (!r) return;
+ r.title = (title == null ? '' : String(title));
+ Object.keys(r.placed || {}).forEach(function (k) { if (state.assignments[k]) setTitleOverride(k, r.title); });
+ notify();
+ }
function setRevisionCell(rowId, value) { setRevisionCells([rowId], value); }
function setRevisionCells(rowIds, value) {
var set = Object.create(null); (rowIds || []).forEach(function (i) { set[i] = true; });
var changed = false;
- state.mdlList.forEach(function (r) { if (set[r.id]) { r.revisionCell = (value == null ? '' : String(value)); changed = true; } });
+ state.mdlList.forEach(function (r) {
+ if (set[r.id]) { r.revisionCell = (value == null ? '' : String(value)); restampRow(r); changed = true; }
+ });
if (changed) notify();
}
- function setTitleFromDeliverable(key, fromDeliverable) {
- var a = assignmentFor(key);
- a.titleFromDeliverable = !!fromDeliverable;
- cleanAssignment(key);
- notify();
+
+ // ── paste parsing + name matching (pure helpers, unit-tested) ─────────────
+ // Parse Excel/TSV text into scratch rows. Columns: Tracking ⇥ Rev(Status) ⇥
+ // Title; a 4th bare-status column merges into the revision; a lone cell that
+ // parses as a full ZDDC filename is split; a header row is skipped.
+ function parsePastedRows(text) {
+ function unq(s) {
+ s = (s == null ? '' : String(s)).trim();
+ if (s.length >= 2 && s.charAt(0) === '"' && s.charAt(s.length - 1) === '"') s = s.slice(1, -1).replace(/""/g, '"');
+ return s.trim();
+ }
+ var lines = String(text == null ? '' : text).replace(/\r\n?/g, '\n').split('\n');
+ var rows = [], skipped = [], sawData = false;
+ lines.forEach(function (raw, i) {
+ if (!raw.trim()) return;
+ var cells = raw.split('\t').map(unq);
+ var c0 = cells[0] || '';
+ if (!sawData && cells.length > 1 && /^(tracking|number|no\.?|doc(ument)?|drawing|item)\b/i.test(c0) && c0.indexOf('-') === -1) {
+ return; // header row
+ }
+ var tracking = '', rev = '', title = '';
+ if (cells.length === 1) {
+ var p = window.zddc.parseFilename(c0);
+ if (p && p.valid && p.trackingNumber) { tracking = p.trackingNumber; rev = p.revision + (p.status ? ' (' + p.status + ')' : ''); title = p.title || ''; }
+ else tracking = c0;
+ } else {
+ tracking = c0;
+ if (cells.length >= 4 && cells[2] && window.zddc.isValidStatus(cells[2])) { rev = (cells[1] + ' (' + cells[2] + ')').trim(); title = cells[3] || ''; }
+ else { rev = cells[1] || ''; title = cells[2] || ''; }
+ }
+ if (!tracking) { skipped.push({ line: i + 1, reason: 'no tracking number', text: raw }); return; }
+ sawData = true;
+ rows.push({ trackingNumber: tracking, revisionCell: rev.trim(), title: title, source: { pasted: true } });
+ });
+ return { rows: rows, skipped: skipped };
+ }
+ function normTok(s) { return String(s == null ? '' : s).toUpperCase().replace(/[^A-Z0-9]/g, ''); }
+ // Propose row matches for source files by finding a row whose tracking number
+ // appears in the filename. opts.fuzzy also matches on the digit-run.
+ function proposeMatches(files, rows, opts) {
+ opts = opts || {};
+ var out = [];
+ (files || []).forEach(function (f) {
+ var full = zddc.joinExtension(f.originalFilename, f.extension);
+ var nameNorm = normTok(full), nameDigits = nameNorm.replace(/[^0-9]/g, ''), best = null;
+ (rows || []).forEach(function (r) {
+ var tn = r.trackingNumber || ''; if (!tn) return;
+ var tnNorm = normTok(tn), conf = 0;
+ if (full.indexOf(tn) !== -1) conf = 1;
+ else if (tnNorm && nameNorm.indexOf(tnNorm) !== -1) conf = 0.8;
+ else if (opts.fuzzy) { var d = tnNorm.replace(/[^0-9]/g, ''); if (d && nameDigits.indexOf(d) !== -1) conf = 0.5; }
+ if (conf && (!best || conf > best.confidence)) best = { row: r, confidence: conf };
+ });
+ if (best) out.push({ file: f, row: best.row, confidence: best.confidence });
+ });
+ return out;
}
// ── add-folder pattern expansion ─────────────────────────────────────────
@@ -8045,9 +8249,12 @@ X.B(E,Y);return E}return J}())
transmittalRecord: transmittalRecord,
findOrAddParty: findOrAddParty, findOrAddTransmittalBin: findOrAddTransmittalBin,
getConfig: getConfig, setConfig: setConfig, getTrackingFields: getTrackingFields,
- setMdlList: setMdlList, getMdlList: getMdlList, getMdlRow: getMdlRow,
+ setMdlList: setMdlList, appendMdlRows: appendMdlRows, clearMdlList: clearMdlList,
+ getMdlList: getMdlList, getMdlRow: getMdlRow,
+ assignFromRow: assignFromRow, unassignRowFile: unassignRowFile,
+ setRowTracking: setRowTracking, setRowTitle: setRowTitle,
setRevisionCell: setRevisionCell, setRevisionCells: setRevisionCells,
- setTitleFromDeliverable: setTitleFromDeliverable,
+ parsePastedRows: parsePastedRows, proposeMatches: proposeMatches,
getNode: getNode, getTrackingTree: function () { return state.trackingTree; },
getTransmittalTree: function () { return state.transmittalTree; },
// derive + reverse
@@ -9717,6 +9924,17 @@ X.B(E,Y);return E}return J}())
(function() {
'use strict';
+ // ── Sorting ────────────────────────────────────────────────────────────
+ // Render the tree in a stable, human order: case-insensitive, natural
+ // (so "Rev 2" sorts before "Rev 10"). Non-mutating — sort copies at render.
+ function cmpName(a, b) { return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' }); }
+ function sortedFolders(list) { return (list || []).slice().sort(function (a, b) { return cmpName(a.name, b.name); }); }
+ function sortedFiles(list) {
+ return (list || []).slice().sort(function (a, b) {
+ return cmpName(window.zddc.joinExtension(a.originalFilename, a.extension), window.zddc.joinExtension(b.originalFilename, b.extension));
+ });
+ }
+
// ── Classify & Copy helpers ────────────────────────────────────────────
function classifyOn() {
var c = window.app.modules.classify;
@@ -9770,8 +9988,8 @@ X.B(E,Y);return E}return J}())
var tt = window.app.modules.targetTree;
return (tt && tt.activeAxis) ? tt.activeAxis() : 'tracking';
}
- function axisField(ax) { return ax === 'transmittal' ? 'transmittalNodeId' : ax === 'mdl' ? 'mdlNodeId' : 'trackingNodeId'; }
- // Bucket a file relative to the active axis (tracking | transmittal | mdl):
+ function axisField(ax) { return ax === 'transmittal' ? 'transmittalNodeId' : 'trackingNodeId'; }
+ // Bucket a file relative to the active axis (tracking | transmittal):
// 'excluded' | 'assigned' (on this axis) | 'partial' (assigned on a DIFFERENT
// axis only — the to-do for this tab) | 'unassigned' (no axis).
function fileCategory(file) {
@@ -9780,7 +9998,7 @@ X.B(E,Y);return E}return J}())
if (a && a.excluded) return 'excluded';
var ax = activeAxis();
if (a && a[axisField(ax)]) return 'assigned';
- var others = ['tracking', 'transmittal', 'mdl'].filter(function (x) { return x !== ax; });
+ var others = ['tracking', 'transmittal'].filter(function (x) { return x !== ax; });
var any = a && others.some(function (x) { return a[axisField(x)]; });
return any ? 'partial' : 'unassigned';
}
@@ -9887,7 +10105,7 @@ X.B(E,Y);return E}return J}())
return;
}
- window.app.folderTree.forEach(folder => {
+ sortedFolders(window.app.folderTree).forEach(folder => {
if (!folderShown(folder)) return;
const element = createFolderElement(folder);
container.appendChild(element);
@@ -10072,7 +10290,7 @@ X.B(E,Y);return E}return J}())
if ((folder.expanded || autoOpen(folder)) && folder.children && folder.children.length > 0) {
const childrenDiv = document.createElement('div');
childrenDiv.className = 'folder-children';
- folder.children.forEach(child => {
+ sortedFolders(folder.children).forEach(child => {
if (!folderShown(child)) return;
const childElement = createFolderElement(child, level + 1);
childrenDiv.appendChild(childElement);
@@ -10085,7 +10303,7 @@ X.B(E,Y);return E}return J}())
if (classifyOn() && (folder.expanded || autoOpen(folder)) && folder.files && folder.files.length > 0) {
const filesDiv = document.createElement('div');
filesDiv.className = 'folder-children folder-files';
- folder.files.forEach(function (file) {
+ sortedFiles(folder.files).forEach(function (file) {
if (!fileShown(file)) return;
filesDiv.appendChild(createFileElement(file, level + 1));
});
@@ -10792,8 +11010,10 @@ X.B(E,Y);return E}return J}())
var openForm = null; // { partyId, slot } when a bin form is open
var initialized = false;
var currentTab = 'tracking'; // 'tracking' | 'existing' | 'transmittal' — active tab
- var mdlTable = null; // the seltable controller for the catalog
- var mdlPlaced = {}; // latest placed.mdl map (read by the placed-file cell)
+ var mdlTable = null; // the seltable controller for the "From a list" tab
+ var mdlPlaced = {}; // trackingNumber -> placed files (read by the Files cell)
+ var hideAssigned = false; // "Hide assigned" toggle in the From-a-list toolbar
+ var listScanned = false; // a Load has run this session (drives the "new" badge)
function init() {
if (initialized) return;
@@ -10809,6 +11029,10 @@ X.B(E,Y);return E}return J}())
transmittalTree: document.getElementById('transmittalTree'),
mdlTree: document.getElementById('mdlTree'),
loadMdlBtn: document.getElementById('loadMdlBtn'),
+ pasteRowsBtn: document.getElementById('pasteRowsBtn'),
+ matchNamesBtn: document.getElementById('matchNamesBtn'),
+ clearListBtn: document.getElementById('clearListBtn'),
+ hideAssignedToggle: document.getElementById('hideAssignedToggle'),
addTrackingRootBtn: document.getElementById('addTrackingRootBtn'),
addPartyBtn: document.getElementById('addPartyBtn'),
stats: document.getElementById('classifyStats'),
@@ -10818,6 +11042,25 @@ X.B(E,Y);return E}return J}())
if (els.existingTab) els.existingTab.addEventListener('click', function () { showTab('existing'); });
els.transmittalTab.addEventListener('click', function () { showTab('transmittal'); });
if (els.loadMdlBtn) els.loadMdlBtn.addEventListener('click', loadMdl);
+ if (els.pasteRowsBtn) els.pasteRowsBtn.addEventListener('click', function () { openPasteDialog(''); });
+ if (els.matchNamesBtn) els.matchNamesBtn.addEventListener('click', openMatchDialog);
+ if (els.clearListBtn) els.clearListBtn.addEventListener('click', function () {
+ if (!C().getMdlList().length) return;
+ C().clearMdlList();
+ window.zddc.toast('List cleared — every assignment is kept (see By tracking number).', 'info');
+ });
+ if (els.hideAssignedToggle) els.hideAssignedToggle.addEventListener('change', function () {
+ hideAssigned = !!els.hideAssignedToggle.checked;
+ if (mdlTable) mdlTable.renderBody();
+ });
+ // Ctrl-V anywhere on the From-a-list panel opens the paste dialog prefilled.
+ if (els.mdlPanel) els.mdlPanel.addEventListener('paste', function (e) {
+ if (currentTab !== 'existing') return;
+ if (e.target && e.target.closest('input, textarea')) return; // let real inputs paste
+ var t = (e.clipboardData || window.clipboardData);
+ var text = t ? t.getData('text') : '';
+ if (text) { e.preventDefault(); openPasteDialog(text); }
+ });
els.addTrackingRootBtn.addEventListener('click', function () {
var name = prompt('Root folder name (a tracking-number segment, e.g. "ACME-PROJ").\n'
+ 'Brace patterns expand: BMB-{PM,EL}-{0001-0002,0005}_A (IFR)', '');
@@ -10858,15 +11101,20 @@ X.B(E,Y);return E}return J}())
}
// One pass: group files by the node they're placed in, per axis.
function buildPlaced(files) {
- var c = C(), byT = {}, byX = {}, byM = {};
+ var c = C(), byT = {}, byX = {}, byTn = {};
files.forEach(function (f) {
var a = c.getAssignment(c.srcKeyForFile(f));
if (!a) return;
- if (a.trackingNodeId) (byT[a.trackingNodeId] = byT[a.trackingNodeId] || []).push(f);
+ if (a.trackingNodeId) {
+ (byT[a.trackingNodeId] = byT[a.trackingNodeId] || []).push(f);
+ // Also index by tracking NUMBER so a "From a list" row can show
+ // the files placed under it (a row is a tracking number, not a node).
+ var tn = c.deriveTarget(f).tracking;
+ if (tn) (byTn[tn] = byTn[tn] || []).push(f);
+ }
if (a.transmittalNodeId) (byX[a.transmittalNodeId] = byX[a.transmittalNodeId] || []).push(f);
- if (a.mdlNodeId) (byM[a.mdlNodeId] = byM[a.mdlNodeId] || []).push(f);
});
- return { tracking: byT, transmittal: byX, mdl: byM };
+ return { tracking: byT, transmittal: byX, byTracking: byTn };
}
function showTab(which) {
@@ -10882,8 +11130,8 @@ X.B(E,Y);return E}return J}())
// with the active tab — re-render the left tree.
reRenderSource();
}
- // The active axis is the catalog ('mdl') on the "By existing" tab, else the tab's.
- function activeAxis() { return currentTab === 'existing' ? 'mdl' : (currentTab === 'transmittal' ? 'transmittal' : 'tracking'); }
+ // "From a list" drops materialize tracking placements, so its axis is 'tracking'.
+ function activeAxis() { return currentTab === 'transmittal' ? 'transmittal' : 'tracking'; }
function reRenderSource() { if (window.app.modules.tree && window.app.modules.tree.render) window.app.modules.tree.render(); }
// Expand a brace pattern into folder names and create them (confirming a
@@ -10909,7 +11157,7 @@ X.B(E,Y);return E}return J}())
var placed = buildPlaced(files);
renderTrackingInto(els.trackingTree, C().getTrackingTree(), placed.tracking);
renderTransmittalInto(els.transmittalTree, C().getTransmittalTree(), placed.transmittal);
- renderMdlInto(placed.mdl);
+ renderMdlInto(placed.byTracking);
renderStats(files);
}
@@ -11243,47 +11491,43 @@ X.B(E,Y);return E}return J}())
return form;
}
- // ── By MDL (deliverables as drop targets via the shared seltable) ───────
- function renderMdlInto(placedMdl) {
- mdlPlaced = placedMdl || {};
+ // ── "From a list" (scratch worklist via the shared seltable) ────────────
+ function renderMdlInto(placedByTracking) {
+ mdlPlaced = placedByTracking || {};
if (!C().getMdlList().length) {
mdlTable = null;
els.mdlTree.textContent = '';
- els.mdlTree.appendChild(el('div', 'target-empty', 'Nothing loaded yet — “Load…”, tick the directories to scan, and their existing files + MDL deliverables appear here (one row per tracking number, latest revision).'));
+ els.mdlTree.appendChild(el('div', 'target-empty', 'Empty — “Load…” numbers from the archive/MDL, “Paste rows…” from Excel, or “⚡ Match names”. Then drag files onto a row to name them. The list is a scratch pad — clearing it keeps every assignment (see By tracking number).'));
return;
}
ensureMdlTable();
mdlTable.renderBody();
}
+ function rowPlaced(r) { var f = mdlPlaced[r.trackingNumber]; return f && f.length ? f : null; }
function ensureMdlTable() {
if (mdlTable) return mdlTable;
var c = C();
- // One column per configured tracking-number field (split positionally),
- // then Title, MDL (✓), Archive revisions (informational), and the editable
- // classifier-local Revision. Each column has its own autofilter.
- var cols = c.getTrackingFields().map(function (f, i) {
- return { key: 'f' + i, title: f.name, get: function (r) { return (r.trackingNumber || '').split('-')[i] || ''; } };
- });
- cols.push({ key: 'title', title: 'Title', get: function (r) { return r.title || ''; } });
- cols.push({ key: 'mdl', title: 'MDL', cls: 'catalog-mdl', get: function (r) { return r.inMdl ? '✓' : ''; } });
- cols.push({ key: 'latest', title: 'Latest rev', get: function (r) { return latestRevOf(r.archiveRevisions); } });
- cols.push({
- key: 'rev', title: 'Revision', cls: 'mdl-rev', get: function (r) { return r.revisionCell; },
- render: function (r, td) {
- var inp = document.createElement('input');
- inp.type = 'text'; inp.className = 'mdl-rev__input'; inp.value = r.revisionCell || '';
- inp.placeholder = 'A (IFR)'; inp.setAttribute('data-no-select', '');
- inp.addEventListener('change', function () { c.setRevisionCell(r.id, inp.value.trim()); });
- td.appendChild(inp);
- },
- });
+ var cols = [
+ { key: 'tn', title: 'Tracking number', cls: 'fromlist-tn', get: function (r) { return r.trackingNumber || ''; },
+ render: function (r, td) { editCell(td, 'fromlist-tn__input', r.trackingNumber, 'ACME-…-0001', function (v) { c.setRowTracking(r.id, v); }, tnWarn(r)); } },
+ { key: 'title', title: 'Title', cls: 'fromlist-title', get: function (r) { return r.title || ''; },
+ render: function (r, td) { editCell(td, 'fromlist-title__input', r.title, 'title', function (v) { c.setRowTitle(r.id, v); }); } },
+ { key: 'src', title: 'Source', cls: 'fromlist-src', get: function (r) { var s = r.source || {}; return [s.mdl ? 'mdl' : '', s.archive ? 'arch' : '', s.pasted ? 'pasted' : ''].filter(Boolean).join(' '); },
+ render: function (r, td) { renderSource(r, td); } },
+ { key: 'latest', title: 'Latest rev', get: function (r) { return latestRevOf(r.archiveRevisions); } },
+ { key: 'rev', title: 'Revision', cls: 'mdl-rev', get: function (r) { return r.revisionCell; },
+ render: function (r, td) { editCell(td, 'mdl-rev__input', r.revisionCell, 'A (IFR)', function (v) { c.setRevisionCell(r.id, v); }); } },
+ ];
mdlTable = window.app.modules.seltable.create({
container: els.mdlTree,
extraTitle: 'Files',
- rows: function () { return c.getMdlList(); },
+ rows: function () {
+ var list = c.getMdlList();
+ return hideAssigned ? list.filter(function (r) { return !rowPlaced(r); }) : list;
+ },
rowId: function (r) { return r.id; },
columns: cols,
- onRowDrop: function (rowId, keys) { c.place(keys, rowId, 'mdl'); },
+ onRowDrop: function (rowId, keys) { var row = c.getMdlRow(rowId); if (row) c.assignFromRow(keys, row); },
onActivate: function (ids) {
if (!ids.length) return;
var v = prompt('Set the revision on ' + ids.length + ' selected row(s) (e.g. "A (IFR)"):', '');
@@ -11294,8 +11538,37 @@ X.B(E,Y);return E}return J}())
mdlTable.render();
return mdlTable;
}
+ // An editable seltable cell: an that commits on change. `warn` is an
+ // optional tooltip that flags (without blocking) a questionable value.
+ function editCell(td, cls, value, placeholder, onCommit, warn) {
+ var inp = document.createElement('input');
+ inp.type = 'text'; inp.className = cls + (warn ? ' is-warn' : ''); inp.value = value || '';
+ inp.placeholder = placeholder || ''; inp.spellcheck = false; inp.setAttribute('data-no-select', '');
+ if (warn) inp.title = warn;
+ inp.addEventListener('change', function () { onCommit(inp.value.trim()); });
+ td.appendChild(inp);
+ }
+ function tnWarn(r) {
+ var tn = (r.trackingNumber || '').trim(); if (!tn) return '';
+ var n = tn.split('-').length, want = C().getTrackingFields().length;
+ return n < want - 1 || n > want ? ('Has ' + n + ' segments; the pattern expects ' + want + '.') : '';
+ }
+ function renderSource(row, td) {
+ var s = row.source || {};
+ if (s.mdl) td.appendChild(el('span', 'src-badge src-badge--mdl', 'MDL'));
+ if (s.archive) td.appendChild(el('span', 'src-badge src-badge--arch', 'arch'));
+ if (s.pasted && !s.mdl && !s.archive) {
+ // A pasted number matching nothing known: a likely typo / a brand-new number.
+ var isNew = listScanned;
+ var b = el('span', 'src-badge src-badge--new', isNew ? 'new' : 'unverified');
+ b.title = isNew ? 'This tracking number isn’t in the scanned archive/MDL — you’re inventing it.' : 'Not checked against the archive/MDL — Load a directory to verify.';
+ td.appendChild(b);
+ } else if (s.pasted) {
+ td.appendChild(el('span', 'src-badge src-badge--pasted', 'pasted'));
+ }
+ }
function renderMdlPlaced(row, td) {
- var c = C(), files = mdlPlaced[row.id] || [];
+ var c = C(), files = rowPlaced(row) || [];
files.forEach(function (f) {
var d = c.deriveTarget(f);
var a = c.getAssignment(d.key) || {};
@@ -11305,13 +11578,14 @@ X.B(E,Y);return E}return J}())
var nm = el('span', 'mdlfile__name', d.filename || '(set a revision)');
nm.title = 'from ' + f.originalFilename + (f.extension ? '.' + f.extension : '');
line.appendChild(nm);
- var tgl = el('button', 'tnode__act', a.titleFromDeliverable === false ? 'Title: file' : 'Title: MDL');
- tgl.title = 'Use the deliverable’s title or the file’s own';
- tgl.addEventListener('click', function () { c.setTitleFromDeliverable(d.key, a.titleFromDeliverable === false); });
+ var usingRow = a.titleOverride != null && row.title && a.titleOverride === row.title.trim();
+ var tgl = el('button', 'tnode__act', usingRow ? 'Title: row' : 'Title: file');
+ tgl.title = 'Use the row’s title or the file’s own';
+ tgl.addEventListener('click', function () { c.setTitleOverride(d.key, usingRow ? '' : row.title); });
line.appendChild(tgl);
var rm = el('button', 'tnode__act tfile__remove', '✕');
- rm.title = 'Remove from this deliverable';
- rm.addEventListener('click', function () { c.place([d.key], null, 'mdl'); });
+ rm.title = 'Remove this file from the row';
+ rm.addEventListener('click', function () { c.unassignRowFile(row, d.key); });
line.appendChild(rm);
td.appendChild(line);
});
@@ -11414,13 +11688,123 @@ X.B(E,Y);return E}return J}())
}
}
function finishLoad(rows) {
- C().setMdlList(rows);
+ listScanned = true;
+ C().appendMdlRows(rows); // APPEND — the list accumulates across batches
showTab('existing');
window.zddc.toast(rows.length
- ? ('Catalog: ' + rows.length + ' tracking number' + (rows.length === 1 ? '' : 's') + ' from the selected directories. Drag files on, set revisions.')
+ ? ('Added ' + rows.length + ' tracking number' + (rows.length === 1 ? '' : 's') + ' from the selected directories. Drag files on, set revisions.')
: 'No files or deliverables in the selected directories.', rows.length ? 'success' : 'warning');
}
+ // ── paste + match dialogs (reuse the .copy-choice modal shell) ──────────
+ function scratchModal(titleText, hintText) {
+ var done = false;
+ function close() { if (done) return; done = true; document.removeEventListener('keydown', onKey); back.remove(); }
+ function onKey(e) { if (e.key === 'Escape') close(); }
+ var back = el('div', 'copy-choice__backdrop');
+ var box = el('div', 'copy-choice copy-choice--wide');
+ box.appendChild(el('h3', null, titleText));
+ if (hintText) box.appendChild(el('p', null, hintText));
+ var body = el('div', 'scratch-modal__body'); box.appendChild(body);
+ var foot = el('div', 'copy-choice__btns'); box.appendChild(foot);
+ back.appendChild(box);
+ back.addEventListener('click', function (e) { if (e.target === back) close(); });
+ document.addEventListener('keydown', onKey);
+ document.body.appendChild(back);
+ return { body: body, foot: foot, close: close };
+ }
+ function openPasteDialog(prefill) {
+ var c = C();
+ var m = scratchModal('Paste rows from Excel', 'Columns: Tracking · Rev (Status) · Title — tab-separated, as Excel copies. A header row is skipped; a pasted full filename is split.');
+ var ta = document.createElement('textarea');
+ ta.className = 'scratch-paste__ta'; ta.rows = 6; ta.spellcheck = false;
+ ta.placeholder = 'ACME-AR-DWG-0001\tA (IFR)\tFloor plan';
+ ta.value = prefill || '';
+ m.body.appendChild(ta);
+ var preview = el('div', 'scratch-paste__preview'); m.body.appendChild(preview);
+ var add = el('button', 'btn btn-primary', 'Add rows'); add.disabled = true;
+ var cancel = el('button', 'btn btn-secondary', 'Cancel'); cancel.addEventListener('click', m.close);
+ m.foot.appendChild(add); m.foot.appendChild(cancel);
+ var parsed = { rows: [], skipped: [] };
+ function refresh() {
+ parsed = c.parsePastedRows(ta.value);
+ preview.textContent = '';
+ if (parsed.rows.length) {
+ var tbl = el('table', 'scratch-preview__table');
+ var head = el('tr'); ['Tracking number', 'Revision', 'Title'].forEach(function (h) { head.appendChild(el('th', null, h)); }); tbl.appendChild(head);
+ parsed.rows.slice(0, 50).forEach(function (r) {
+ var tr = el('tr');
+ tr.appendChild(el('td', null, r.trackingNumber));
+ tr.appendChild(el('td', null, r.revisionCell || ''));
+ tr.appendChild(el('td', null, r.title || ''));
+ tbl.appendChild(tr);
+ });
+ preview.appendChild(tbl);
+ if (parsed.rows.length > 50) preview.appendChild(el('div', 'scratch-preview__more', '…and ' + (parsed.rows.length - 50) + ' more'));
+ }
+ parsed.skipped.forEach(function (s) { preview.appendChild(el('div', 'scratch-preview__skip', 'Line ' + s.line + ' skipped — ' + s.reason)); });
+ add.disabled = !parsed.rows.length;
+ add.textContent = parsed.rows.length ? ('Add ' + parsed.rows.length + ' row' + (parsed.rows.length === 1 ? '' : 's')) : 'Add rows';
+ }
+ add.addEventListener('click', function () {
+ var n = parsed.rows.length;
+ c.appendMdlRows(parsed.rows);
+ m.close(); showTab('existing');
+ window.zddc.toast('Added ' + n + ' pasted row' + (n === 1 ? '' : 's') + '.', 'success');
+ });
+ ta.addEventListener('input', refresh);
+ refresh(); ta.focus();
+ }
+ function openMatchDialog() {
+ var c = C();
+ var rows = c.getMdlList();
+ if (!rows.length) { window.zddc.toast('Load or paste some tracking numbers first.', 'warning'); return; }
+ var files = allFiles().filter(function (f) {
+ var a = c.getAssignment(c.srcKeyForFile(f));
+ return !(a && (a.trackingNodeId || a.excluded));
+ });
+ if (!files.length) { window.zddc.toast('No unassigned files to match.', 'info'); return; }
+ var m = scratchModal('Match names', 'Files whose name contains a known tracking number. Review, then assign the checked matches.');
+ var opts = { fuzzy: false };
+ var fuzzyLbl = el('label', 'scratch-match__fuzzy');
+ var fuzzy = document.createElement('input'); fuzzy.type = 'checkbox';
+ fuzzyLbl.appendChild(fuzzy); fuzzyLbl.appendChild(document.createTextNode(' Looser matching (digits only)'));
+ m.body.appendChild(fuzzyLbl);
+ var list = el('div', 'scratch-match__list'); m.body.appendChild(list);
+ var accept = el('button', 'btn btn-primary', 'Assign');
+ var cancel = el('button', 'btn btn-secondary', 'Cancel'); cancel.addEventListener('click', m.close);
+ m.foot.appendChild(accept); m.foot.appendChild(cancel);
+ var proposals = [];
+ function refresh() {
+ proposals = c.proposeMatches(files, rows, opts);
+ list.textContent = '';
+ if (!proposals.length) { list.appendChild(el('div', 'scratch-preview__skip', 'No matches found.')); accept.disabled = true; accept.textContent = 'Assign'; return; }
+ proposals.forEach(function (p, i) {
+ var rowEl = el('label', 'scratch-match__row');
+ var cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = true; cb.dataset.i = i;
+ rowEl.appendChild(cb);
+ rowEl.appendChild(el('span', 'scratch-match__file', zddc.joinExtension(p.file.originalFilename, p.file.extension)));
+ rowEl.appendChild(el('span', 'scratch-match__arrow', '→'));
+ rowEl.appendChild(el('span', 'scratch-match__tn', p.row.trackingNumber));
+ rowEl.appendChild(el('span', 'scratch-match__conf', Math.round(p.confidence * 100) + '%'));
+ list.appendChild(rowEl);
+ });
+ accept.disabled = false; accept.textContent = 'Assign ' + proposals.length;
+ }
+ accept.addEventListener('click', function () {
+ var n = 0;
+ Array.prototype.forEach.call(list.querySelectorAll('input[type=checkbox]'), function (cb) {
+ if (!cb.checked) return;
+ var p = proposals[Number(cb.dataset.i)];
+ if (p) { c.assignFromRow([c.srcKeyForFile(p.file)], p.row); n++; }
+ });
+ m.close(); showTab('existing');
+ window.zddc.toast('Assigned ' + n + ' file' + (n === 1 ? '' : 's') + ' by name match.', n ? 'success' : 'info');
+ });
+ fuzzy.addEventListener('change', function () { opts.fuzzy = fuzzy.checked; refresh(); });
+ refresh();
+ }
+
// ── events ─────────────────────────────────────────────────────────────
function closestNodeId(target) {
var n = target.closest('[data-id]');
@@ -11622,10 +12006,7 @@ X.B(E,Y);return E}return J}())
function reveal(key) {
var a = C().getAssignment(key);
if (!a) return;
- if (a.mdlNodeId) {
- showTab('existing');
- if (mdlTable) { mdlTable.renderBody(); }
- } else if (a.trackingNodeId) {
+ if (a.trackingNodeId) {
showTab('tracking'); collapsed = {}; render();
flashNode(els.trackingTree, a.trackingNodeId);
} else if (a.transmittalNodeId) {
diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html
index 6da181d..56da175 100644
--- a/zddc/internal/apps/embedded/index.html
+++ b/zddc/internal/apps/embedded/index.html
@@ -1778,7 +1778,7 @@ body {