diff --git a/docs/reference/file-map.md b/docs/reference/file-map.md index 6ce4bbb..d4d6854 100644 --- a/docs/reference/file-map.md +++ b/docs/reference/file-map.md @@ -290,7 +290,7 @@ python tests/creator_dialogs_check.py # S1 creator - 0 natives, errors at fields Wave 8 adds these: ```bash -python tests/kitting_check.py # CR-009 - status set + off for Micron EUV 14 checks +python tests/kitting_check.py # CR-009/CR-010 - statuses, owner, filter 21 checks ``` **Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live diff --git a/html/wp-creation-app.js b/html/wp-creation-app.js index 7d2874b..81e0d71 100644 --- a/html/wp-creation-app.js +++ b/html/wp-creation-app.js @@ -59,6 +59,34 @@ const WP_PRIORITIES = ['Normal', 'High', 'Urgent']; // ('Open'). A value stored before this set existed is kept and shown as a // legacy option - renamed sets must not orphan recorded data (CR-016's rule). const KIT_STATUSES = ['Not Started', 'Picking', 'Staged', 'In Transit', 'Delivered']; + +// CR-010 / T8.2: the warehouse owner is an ACCOUNT on the package - the person +// fulfillment bottlenecks through, named explicitly so CR-011 can notify them. +// pkgKitOwnerId is the routing; wp_kit_owner (hidden) keeps the display name so +// exports and old data keep rendering. A stored name with no matching account +// (typed before this field was account-backed, or someone since removed from +// the project) is KEPT as a selected "(no account)" option - existing packages +// must never break because the roster changed. +let pkgKitOwnerId = ''; +function buildKitOwnerPicker(){ + const sel=document.getElementById('wp_kit_owner_sel'); if(!sel) return; + const curId=pkgKitOwnerId||''; + const curName=gv('wp_kit_owner'); + let html=''+ + (projectMembers||[]).map(u=>``).join(''); + const known=(projectMembers||[]).some(u=>u.id===curId); + if(curName && !known){ + html+=``; + } + sel.innerHTML=html; + sel.onchange=function(){ + if(this.value==='__orphan__') return; // re-picking the legacy name changes nothing + const u=(projectMembers||[]).find(x=>x.id===this.value); + pkgKitOwnerId=u?u.id:''; + const hid=document.getElementById('wp_kit_owner'); + if(hid) hid.value=u?(u.full_name||u.username):''; + }; +} function buildKitStatusOptions(current){ const sel=document.getElementById('wp_kit_status'); if(!sel) return; const cur=(current!=null?current:sel.value)||''; @@ -1577,7 +1605,7 @@ function collectPackage(){ gateOverride:pkgGateOverride || undefined, assets:pkgAssets.filter(a=>a.tag||a.link||a.desc), materials:pkgMaterials.filter(m=>m.qty||m.desc), attachments:pkgAttach.filter(a=>a.doc), - kitStatus:gv('wp_kit_status'), kitOwner:gv('wp_kit_owner'), kitDate:gv('wp_kit_date'), + kitStatus:gv('wp_kit_status'), kitOwner:gv('wp_kit_owner'), kitOwnerId:pkgKitOwnerId||'', kitDate:gv('wp_kit_date'), mimoTime:gv('wp_mimo_time'), mimoLoc:gv('wp_mimo_loc'), constraints:pkgConstraints.map(c=>({name:c.name,status:c.status,comment:c.comment})), qc:gv('wp_qc'), photo:gv('wp_photo'), hold:gv('wp_hold'), @@ -2707,7 +2735,9 @@ function loadPackageIntoForm(p){ set('wp_assignee',p.assigneeId); set('wp_due',p.due); set('wp_spec',p.spec); set('wp_desc',p.desc); set('wp_hours',p.hours); buildKitStatusOptions(p.kitStatus||''); + pkgKitOwnerId=p.kitOwnerId||''; set('wp_kit_owner',p.kitOwner); set('wp_kit_date',p.kitDate); set('wp_mimo_time',p.mimoTime); set('wp_mimo_loc',p.mimoLoc); + buildKitOwnerPicker(); set('wp_actual_hrs',p.actualHrs); set('wp_installed_qty',p.installedQty); set('wp_redlines',p.redlines); set('wp_lessons',p.lessons); set('wp_bimlink',p.bimlink); set('wp_iff',p.iff); set('wp_model_area',p.modelArea); set('wp_clash',p.clash); set('wp_scan_link',p.scanLink); @@ -2793,7 +2823,7 @@ function newPackage(){ if(typeof WPUrl !== 'undefined' && window.wpCreatorReady && WPUrl.get('wp')) urlSyncPackage('', {replace:true}); editingId=null; ['wp_subject','wp_system','wp_location','wp_p6_id','wp_p6_desc','wp_wbs','wp_assignee','wp_assignees','wp_distribution','wp_due','wp_spec','wp_desc','wp_hours','wp_kit_owner','wp_kit_date','wp_mimo_time','wp_mimo_loc','wp_actual_hrs','wp_installed_qty','wp_redlines','wp_lessons','wp_bimlink','wp_model_area','wp_scan_link'].forEach(id=>{const el=document.getElementById(id); if(el) el.value='';}); - document.getElementById('wp_type').value=''; buildKitStatusOptions(''); document.getElementById('wp_cost').value=''; + document.getElementById('wp_type').value=''; buildKitStatusOptions(''); pkgKitOwnerId=''; buildKitOwnerPicker(); document.getElementById('wp_cost').value=''; const prio=document.getElementById('wp_priority'); if(prio) prio.value=WP_PRIORITY_DEFAULT; // CR-003 buildLocationPickers({building:'', floor:'', sector:''}); // CR-004 ['wp_iff','wp_clash'].forEach(id=>{const el=document.getElementById(id); if(el) el.value='';}); @@ -2837,7 +2867,7 @@ const WPData = { if(typeof ProjectData!=='undefined' && ProjectData.pushWP) ProjectData.pushWP(p, activeProjectId); return true; }, }; -let dashFilter={status:'',discipline:'',q:'',flag:'',priority:'',building:'',floor:'',sector:''}; +let dashFilter={status:'',discipline:'',q:'',flag:'',priority:'',building:'',floor:'',sector:'',kitOwner:''}; // A write has to reach the server before the server can count it. The outbox is // the only path writes take, so flush it and then re-read - otherwise the refresh // races the push and shows the pre-write totals, which is the same stale number @@ -3241,11 +3271,17 @@ function renderDashboard(){ const discList=Object.keys(byDisc); const discOpts=[''].concat(discList.map(d=>``)).join(''); const prioOpts=[''].concat(WP_PRIORITIES.map(v=>``)).join(''); + // CR-010: filter by warehouse owner, so the person doing fulfillment sees + // exactly their queue. Options are the owners PRESENT in the data - a filter + // offering people with nothing to fulfill is noise. + const kitOwners=[...new Set(all.map(p=>(p.kitOwner||'').trim()).filter(Boolean))].sort(); + const kitOpts=[''].concat(kitOwners.map(v=>``)).join(''); h+=`
+ ${locFilterSelects()}
`; @@ -3257,6 +3293,7 @@ function renderDashboard(){ if(dashFilter.status && p.status!==dashFilter.status) return false; if(dashFilter.discipline && !((p.disciplines||[]).includes(dashFilter.discipline))) return false; if(dashFilter.priority && wpPriorityOf(p)!==dashFilter.priority) return false; + if(dashFilter.kitOwner && (p.kitOwner||'').trim()!==dashFilter.kitOwner) return false; // CR-004: filtering by a BUILDING matches every package under it, because the // stored value is a path. Filtering by the floor code alone could not do that. if(dashFilter.building && !((p.building||'')===dashFilter.building @@ -3555,6 +3592,7 @@ async function loadMembers(){ (others.length?`${others.map(opt).join('')}`:''); if(cur) sel.value=cur; else defaultOwnerToMe(); + buildKitOwnerPicker(); // CR-010: same roster, second picker // The people pickers list the same accounts, so (re)render them now that the // member list has arrived. if(editingId){ const p=savedPackages.find(x=>x.id===editingId); if(p) loadPeopleFromPkg(p); } diff --git a/html/wp-creation-index.html b/html/wp-creation-index.html index b2e04b3..6d74efe 100644 --- a/html/wp-creation-index.html +++ b/html/wp-creation-index.html @@ -389,7 +389,7 @@
Options come from KIT_STATUSES (CR-009): an explicit set, never free text. A pre-CR-009 value on a saved package is kept as a legacy option.
-
+
scheduled material-move date & time
diff --git a/tests/kitting_check.py b/tests/kitting_check.py index 37dc31c..a3ffaab 100644 --- a/tests/kitting_check.py +++ b/tests/kitting_check.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Are kitting statuses a set, and is kitting off for Micron EUV? — CR-009, T8.1. +"""Kitting: statuses, the owner, the filter — CR-009 (T8.1) + CR-010 (T8.2). The statuses become an explicit five (Not Started / Picking / Staged / In Transit / Delivered) - fulfillment states, not free text. A value stored @@ -131,6 +131,61 @@ def main(): (page.eval("collectPackage().kitStatus"), page.eval("collectPackage().kitOwner")))) + # ── 4. CR-010: the warehouse owner (T8.2) ──────────────────────────── + print(chr(10) + "4. CR-010: the warehouse owner") + page.eval("set_sop_reload = 0") # marker only + page.goto(base + "/wp-creation-index.html?project=projA") + dismiss_dialogs(page) + wait_creator(page) + settle(1.6) + page.eval("window.alert=()=>{}; window.confirm=()=>false; window.prompt=()=>null;") + chk("the warehouse owner is a dropdown of project members, not free text", + page.eval("document.getElementById('wp_kit_owner_sel').tagName") == "SELECT" + and page.eval("document.getElementById('wp_kit_owner_sel').options.length") > 1, + page.eval("document.getElementById('wp_kit_owner_sel').options.length")) + page.eval("""(() => { + const sel=document.getElementById('wp_kit_owner_sel'); + sel.value=[...sel.options].find(o=>o.value==='user_sue').value; + sel.onchange.call(sel); + })()""") + got = json.loads(page.eval( + "JSON.stringify({name: collectPackage().kitOwner, id: collectPackage().kitOwnerId})")) + chk("picking a member persists BOTH the display name and the account id " + "(the id is what CR-011 notifications will read)", + got["id"] == "user_sue" and got["name"], ascii_(got)) + + page.eval("""loadPackageIntoForm({subject:'legacy owner', type:'Conduit Install', + kitOwner:'Paul Coonrod', constraints:[], holds:[]})""") + settle(0.8) + chk("a stored name with NO account renders as a kept '(no account)' option " + "- removing someone from the project breaks nothing", + page.eval("document.getElementById('wp_kit_owner_sel').value") == "__orphan__" + and "no account" in page.eval( + "document.getElementById('wp_kit_owner_sel').selectedOptions[0].textContent")) + chk("...and the stored name still round-trips", + page.eval("collectPackage().kitOwner") == "Paul Coonrod") + + # the dashboard filter + page.eval("""(() => { + const mk=(id,num,owner)=>({id, number:num, subject:'kit '+num, + type:'Conduit Install', status:'Draft', kitOwner:owner, + constraints:[], holds:[], projectId:activeProjectId}); + savedPackages.push(mk('wpKA','WP70-KA','Paul Coonrod')); + savedPackages.push(mk('wpKB','WP71-KB','Sue')); + saveStore(); renderSavedList(); + })()""") + page.eval("showDashboard()") + settle(1.2) + page.eval("dashFilter.kitOwner='Paul Coonrod'; dashPage=0; renderDashboard()") + settle(0.8) + board = page.eval("(document.getElementById('dash-body')||{textContent:''}).textContent") + chk("the dashboard filters by warehouse owner - their package shows", + "WP70-KA" in board, ascii_(board, 120)) + chk("...and the other owner's does not", "WP71-KB" not in board) + chk("...through a labelled control on the board", + page.eval("!!document.querySelector('[aria-label=%s]')" + % json.dumps("Filter by warehouse owner"))) + js_errors = [e for e in page.js_errors() if "beforeunload" not in e] chk("no JavaScript errors anywhere in this run", not js_errors, ascii_(js_errors[:2]))