T8.2 - CR-010: the warehouse owner is an account, on the package
A named person owns fulfillment of the kit - today that is Paul Coonrod, informally, and everything bottlenecks through him. The package now records it explicitly: - The Warehouse owner control is a dropdown of project members (the same roster the Owner picker reads). Picking someone stores BOTH the display name (kitOwner - exports and old renderers keep working) and the account id (kitOwnerId - the routing CR-011's notifications will read at T8.3). - Confirmed Aug 18: the field lives ON the work package, not the project - a package retargeted to a different warehouse notifies the right person without touching the project. The wizard gets no field. - A stored name with no matching account - typed before the field was account-backed, or someone since removed from the project - is KEPT as a selected "(no account)" option and round-trips unchanged. Removing someone from the project breaks nothing. - The dashboard filters by warehouse owner, options drawn from the owners actually present in the data - a filter offering people with nothing to fulfill is noise. Verification (each probe run alone): kitting_check.py extended to 21/21 (T8.2 section: picker, id+name persistence, orphan survival, board filter). Regression: generalinfo_check 49/49. Items: CR-010 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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='<option value="">— not assigned —</option>'+
|
||||
(projectMembers||[]).map(u=>`<option value="${esc(u.id)}"${u.id===curId?' selected':''}>${esc(u.full_name||u.username)}</option>`).join('');
|
||||
const known=(projectMembers||[]).some(u=>u.id===curId);
|
||||
if(curName && !known){
|
||||
html+=`<option value="__orphan__" selected>${esc(curName)} (no account)</option>`;
|
||||
}
|
||||
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=['<option value="">All disciplines</option>'].concat(discList.map(d=>`<option ${dashFilter.discipline===d?'selected':''}>${esc(d)}</option>`)).join('');
|
||||
const prioOpts=['<option value="">All priorities</option>'].concat(WP_PRIORITIES.map(v=>`<option ${dashFilter.priority===v?'selected':''}>${esc(v)}</option>`)).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=['<option value="">All warehouse owners</option>'].concat(kitOwners.map(v=>`<option ${dashFilter.kitOwner===v?'selected':''}>${esc(v)}</option>`)).join('');
|
||||
h+=`<div class="dash-filters">
|
||||
<input type="search" placeholder="Search WP # / subject / type…" value="${(dashFilter.q||'').replace(/"/g,'"')}" oninput="dashFilter.q=this.value;dashPage=0;renderDashboard()">
|
||||
<select onchange="dashFilter.status=this.value;dashPage=0;renderDashboard()">${statusOpts}</select>
|
||||
<select onchange="dashFilter.discipline=this.value;dashPage=0;renderDashboard()">${discOpts}</select>
|
||||
<select aria-label="Filter by priority" onchange="dashFilter.priority=this.value;dashPage=0;renderDashboard()">${prioOpts}</select>
|
||||
<select aria-label="Filter by warehouse owner" onchange="dashFilter.kitOwner=this.value;dashPage=0;renderDashboard()">${kitOpts}</select>
|
||||
${locFilterSelects()}
|
||||
<label class="dash-arch-toggle"><input type="checkbox" ${dashShowArchived?'checked':''} onchange="dashToggleArchived(this.checked)"> Show archived${dashShowArchived?' ('+dashArchived.length+')':''}</label>
|
||||
</div>`;
|
||||
@@ -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?`<optgroup label="${onTeam.length?'Others on this project':'On this project'}">${others.map(opt).join('')}</optgroup>`:'');
|
||||
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); }
|
||||
|
||||
@@ -389,7 +389,7 @@
|
||||
<div class="field"><label>Kitting status</label>
|
||||
<select id="wp_kit_status"><option value="">—</option></select>
|
||||
<div class="field-hint">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.</div></div>
|
||||
<div class="field"><label>Warehouse owner</label><input type="text" id="wp_kit_owner" placeholder="name"></div>
|
||||
<div class="field"><label>Warehouse owner <span class="help-tip" data-tip="Who owns fulfillment of this kit — a user account on this project. They are a default recipient of kitting notifications (CR-011). Recorded ON the package: retargeting one package notifies the right warehouse without touching the project.">i</span></label><select id="wp_kit_owner_sel"><option value="">— not assigned —</option></select><input type="hidden" id="wp_kit_owner"></div>
|
||||
<div class="field"><label>Kitting need date</label><input type="date" id="wp_kit_date"></div>
|
||||
<div class="field"><label>MIMO sch. time</label><input type="datetime-local" id="wp_mimo_time"><div class="field-hint">scheduled material-move date & time</div></div>
|
||||
<div class="field"><label>MIMO location</label><input type="text" id="wp_mimo_loc" placeholder="staging / move location"></div>
|
||||
|
||||
@@ -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]))
|
||||
|
||||
Reference in New Issue
Block a user