/* Field view — a touch-optimized screen for updating a Work Package's status, constraints, and a photo/note log from the work face. Reads the same shared data as the desktop creator (via project-data.js) and saves through the sync outbox, so it works offline and syncs when the network returns. */ 'use strict'; var PID = '', PROJECT = null, WPS = [], curId = null, pendingPhoto = '', draftNote = ''; var STATUSES = ['Draft', 'Scheduled', 'Issued', 'In Progress', 'QC', 'Closed', 'Issue']; var GATED = ['Issued', 'In Progress', 'QC', 'Closed']; // need all constraints cleared to enter function esc(s) { return s == null ? '' : String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } function nsKey(id) { return 'wp_iwp_v1__' + id; } function stLabel(s) { return s === 'Issue' ? 'Issue (Hold)' : s; } function openCount(p) { return ((p && p.constraints) || []).filter(function (c) { return c.status === 'open'; }).length; } // Predecessor packages that aren't Closed yet. A package waiting on upstream work // is not release-ready either, so the field list must not call it Ready — the // server would refuse to issue it (see enforce_release_gates). function waitingCount(p, all) { var preds = (p && p.predecessors) || []; if (!preds.length) return 0; var byId = {}; (all || []).forEach(function (x) { byId[x.id] = x; }); return preds.filter(function (id) { var q = byId[id]; return q && q.status !== 'Closed'; }).length; } function fmtTs(s) { try { return wpFormatDateTime(s); } catch (e) { return s || ''; } } function me() { try { return (window.WP_USER && (window.WP_USER.full_name || window.WP_USER.username)) || ''; } catch (e) { return ''; } } function toast(m) { var t = document.getElementById('toast'); if (!t) return; t.textContent = m; t.classList.add('show'); clearTimeout(toast._t); toast._t = setTimeout(function () { t.classList.remove('show'); }, 2000); } // ── boot / data ────────────────────────────────────────────────────────────── function boot() { var params = new URLSearchParams(location.search); PID = params.get('project') || (ProjectData.getActiveId && ProjectData.getActiveId()) || ''; if (!PID) { showNoProject(); return; } if (ProjectData.getActiveId && ProjectData.getActiveId() !== PID) { try { ProjectData.setActive({ id: PID }); } catch (e) {} } if (ProjectData.get) { ProjectData.get(PID).then(function (p) { PROJECT = p; renderCtx(); }).catch(function () {}); } loadWPs(); } function renderCtx() { var el = document.getElementById('fld-ctx'); if (!el) return; if (PROJECT) el.innerHTML = 'Project: ' + esc(PROJECT.name || '') + '' + (PROJECT.number ? ' · ' + esc(PROJECT.number) : ''); else el.textContent = 'Project: ' + PID; } function readCache() { try { return JSON.parse(localStorage.getItem(nsKey(PID)) || '[]') || []; } catch (e) { return []; } } function writeCache() { try { localStorage.setItem(nsKey(PID), JSON.stringify(WPS)); } catch (e) {} } function activePkgs(list) { return list.filter(function (p) { return !p.split && !p.archived; }); } // real work, not masters/archived function loadWPs() { WPS = activePkgs(readCache()); // offline-first: show cached packages immediately renderList(); if (ProjectData.pullProject) { ProjectData.pullProject(PID).then(function () { WPS = activePkgs(readCache()); if (!curId) renderList(); else renderDetail(); }).catch(function () {}); } } function showNoProject() { var s = document.getElementById('screen-list'); if (s) s.innerHTML = '
No project selected.
Pick a project on the home page, then reopen the field view.
'; } // ── list ─────────────────────────────────────────────────────────────────── function renderList() { var box = document.getElementById('wp-list'); if (!box) return; var q = ((document.getElementById('fld-search') || {}).value || '').toLowerCase(); var rows = WPS.filter(function (p) { return !q || ((p.number || '') + ' ' + (p.subject || '') + ' ' + (p.type || '')).toLowerCase().indexOf(q) >= 0; }); if (!rows.length) { box.innerHTML = '
' + (WPS.length ? 'No packages match your search.' : 'No work packages for this project yet.') + '
'; return; } box.innerHTML = rows.map(function (p) { var open = openCount(p); var waiting = waitingCount(p, WPS); // the full set, not the filtered rows var cls = p.status === 'Issue' ? 'hold' : ((open === 0 && !waiting) ? 'ready' : ''); var readyPill = p.status === 'Issue' ? 'On hold' : (open ? '' + open + ' open' : (waiting ? 'waits on ' + waiting + '' : 'Ready')); return ''; }).join(''); } // ── detail ───────────────────────────────────────────────────────────────── function curWP() { return WPS.find(function (p) { return p.id === curId; }); } function openWP(id) { curId = id; pendingPhoto = ''; draftNote = ''; renderDetail(); window.scrollTo(0, 0); } function backToList() { curId = null; pendingPhoto = ''; draftNote = ''; document.getElementById('screen-detail').style.display = 'none'; document.getElementById('screen-list').style.display = ''; renderList(); } function renderDetail() { var p = curWP(); if (!p) { backToList(); return; } document.getElementById('screen-list').style.display = 'none'; var d = document.getElementById('screen-detail'); d.style.display = ''; var stBtns = STATUSES.map(function (s) { return ''; }).join(''); var cx = (p.constraints) || []; var cxRows = cx.length ? cx.map(function (c, i) { var st = c.status || 'open'; return '
' + esc(c.name) + '
' + '
'; }).join('') : '
No constraints on this package.
'; var log = ((p.fieldLog) || []).slice().reverse().map(function (e) { return '
' + esc(e.by || '—') + ' · ' + esc(fmtTs(e.ts)) + (e.status ? ' · ' + esc(stLabel(e.status)) : '') + '
' + (e.note ? esc(e.note) : '') + (e.photo && /^data:image\//.test(e.photo) ? 'site photo' : '') + '
'; }).join('') || '
No field updates yet.
'; d.innerHTML = '' + '
' + esc(p.number || '(no number)') + '
' + '
' + esc(p.subject || '') + (p.type ? ' · ' + esc(p.type) : '') + '
' + '

Status

' + stBtns + '
' + '

Constraints — ' + openCount(p) + ' open

' + cxRows + '
' + '

Add field update

' + '' + '
' + '' + (pendingPhoto ? 'Photo attached ✓' : '') + '
' + '
' + '
' + '

Field log

' + log + '
'; } // ── mutations (each auto-saves via the outbox; the global sync badge shows state) ── function saveWP(p) { var ix = WPS.findIndex(function (x) { return x.id === p.id; }); if (ix >= 0) WPS[ix] = p; writeCache(); if (typeof ProjectData !== 'undefined' && ProjectData.pushWP) ProjectData.pushWP(p, PID); } function setStatus(s) { var p = curWP(); if (!p) return; if (GATED.indexOf(s) >= 0 && openCount(p) > 0) { toast('Clear all constraints before moving to ' + stLabel(s)); return; } if (p.status === s) return; p.status = s; if (s === 'Issued' && !p.issuedAt) p.issuedAt = new Date().toISOString(); saveWP(p); renderDetail(); toast('Status: ' + stLabel(s)); } function cycleConstraint(i) { var p = curWP(); if (!p || !p.constraints || !p.constraints[i]) return; var order = ['open', 'cleared', 'na']; var cur = p.constraints[i].status || 'open'; p.constraints[i].status = order[(order.indexOf(cur) + 1) % 3]; saveWP(p); renderDetail(); } function onPhoto(ev) { var f = ev.target.files && ev.target.files[0]; if (!f) return; var st = document.getElementById('photo-status'); if (st) st.textContent = 'Processing…'; var url = URL.createObjectURL(f); var img = new Image(); img.onload = function () { var max = 1280, w = img.width, h = img.height, scale = Math.min(1, max / Math.max(w, h)); var cv = document.createElement('canvas'); cv.width = Math.round(w * scale); cv.height = Math.round(h * scale); cv.getContext('2d').drawImage(img, 0, 0, cv.width, cv.height); try { pendingPhoto = cv.toDataURL('image/jpeg', 0.7); } catch (e) { pendingPhoto = ''; } URL.revokeObjectURL(url); if (st) st.textContent = pendingPhoto ? 'Photo attached ✓' : 'Could not read photo'; }; img.onerror = function () { URL.revokeObjectURL(url); if (st) st.textContent = 'Could not read photo'; }; img.src = url; } function addUpdate() { var p = curWP(); if (!p) return; var note = (draftNote || '').trim(); if (!note && !pendingPhoto) { toast('Add a note or photo first'); return; } if (!p.fieldLog) p.fieldLog = []; p.fieldLog.push({ ts: new Date().toISOString(), by: me(), note: note, photo: pendingPhoto || '', status: p.status }); pendingPhoto = ''; draftNote = ''; saveWP(p); renderDetail(); toast('Update added to log'); } boot();