
/* ===== app.js ===== */
const seed={sites:[],gangs:[],users:[],items:[],notifications:[],calendarNotes:[],auditLog:[],siteDiary:[],siteDrawings:[],attendanceShifts:[],attendanceAttempts:[],attendanceAbsences:[],timesheetApprovals:[],assistUsage:[],leaveRequests:[],dayworks:[],hrRecords:[],dayRateFullOverrides:[],payrollLocks:[],workingPatternTemplates:[],peopleArchive:[],teamArchive:[],payrollExportProfiles:[],offlineClockRequests:[],cisProfiles:[],cisContractor:{contractorName:'',contractorUTR:'',employerTaxReference:'',accountsOfficeReference:''},pushSubscriptions:[]};
function iso(d=new Date()){return d.toISOString().slice(0,10)}
function addDays(base,n){let d=new Date(base+'T12:00:00');d.setDate(d.getDate()+n);return iso(d)}

function updateSyncUI(){let e=document.getElementById('sync-indicator');if(!e)return;let text=syncState==='synced'?'✓ Shared · Synced':syncState==='saving'?'↻ Saving…':syncState==='offline'?'⚠ Offline · changes waiting':syncState==='error'?'⚠ Sync problem':syncState==='connecting'?'↻ Connecting…':'○ Local only';e.textContent=text;e.className='online sync-indicator sync-'+syncState;e.title=syncError||(lastSyncAt?`Last synced ${new Date(lastSyncAt).toLocaleTimeString()}`:'')}
function syncIndicator(){let text=syncState==='synced'?'✓ Shared · Synced':syncState==='saving'?'↻ Saving…':syncState==='offline'?'⚠ Offline · changes waiting':syncState==='error'?'⚠ Sync problem':syncState==='connecting'?'↻ Connecting…':'○ Local only';return `<button id="sync-indicator" class="online sync-indicator sync-${syncState}" onclick="syncDetails()" title="${syncError||''}">${text}</button>`}
function syncDetails(){let mode=sharedMode?'Cloud-connected SiteSight':'Local browser mode';modal=`<div class="modal"><div class="modalbox"><h2>SiteSight Sync</h2><div class="sync-detail-status">${syncIndicator()}</div><p><b>${mode}</b></p><p class="muted">${sharedMode?'Changes are saved to the connected SiteSight service. If connection drops while this page remains open, records and photos can wait locally and retry when the connection returns.':'This browser is not connected to the SiteSight service. Connect to the hosted SiteSight address to share records between devices.'}</p>${lastSyncAt?`<p class="muted">Last successful sync: ${new Date(lastSyncAt).toLocaleString()}</p>`:''}${syncConflictCount?`<p class="muted">Shared changes automatically merged this session: ${syncConflictCount}</p>`:''}${syncError?`<div class="sync-warning">${syncError}</div>`:''}<div class="actions" style="margin-top:12px">${sharedMode?'<button class="btn blue" onclick="modal=null;render();syncToServer(true)">Retry sync</button>':''}<button class="btn light" onclick="modal=null;render()">Close</button></div></div></div>`;render()}
function threeWayMerge(base,local,remote){
 if(JSON.stringify(local)===JSON.stringify(base))return cloneDB(remote);if(JSON.stringify(remote)===JSON.stringify(base))return cloneDB(local);
 if(local===null||remote===null||base===null||typeof local!=='object'||typeof remote!=='object'||typeof base!=='object')return cloneDB(local);
 if(Array.isArray(local)&&Array.isArray(remote)&&Array.isArray(base)){let keyed=a=>a.every(v=>v&&typeof v==='object'&&'id' in v);if(keyed(local)&&keyed(remote)&&keyed(base)){let bm=new Map(base.map(v=>[v.id,v])),lm=new Map(local.map(v=>[v.id,v])),rm=new Map(remote.map(v=>[v.id,v])),ids=[...new Set([...bm.keys(),...lm.keys(),...rm.keys()])],out=[];ids.forEach(id=>{let b=bm.get(id),l=lm.get(id),r=rm.get(id);if(l===undefined){if(r!==undefined&&JSON.stringify(r)!==JSON.stringify(b))out.push(cloneDB(r));return}if(r===undefined){out.push(cloneDB(l));return}if(b===undefined){out.push(JSON.stringify(l)===JSON.stringify(r)?cloneDB(l):threeWayMerge({},l,r));return}out.push(threeWayMerge(b,l,r))});return out}return cloneDB(local)}
 if(Array.isArray(local)||Array.isArray(remote)||Array.isArray(base))return cloneDB(local);
 let out={},keys=new Set([...Object.keys(base||{}),...Object.keys(local||{}),...Object.keys(remote||{})]);keys.forEach(k=>{if(!(k in local)){if(k in remote&&JSON.stringify(remote[k])!==JSON.stringify(base[k]))out[k]=cloneDB(remote[k])}else if(!(k in remote))out[k]=cloneDB(local[k]);else out[k]=threeWayMerge(base[k],local[k],remote[k])});return out
}
async function fetchSharedData(){let r=await fetch('/api/data',{cache:'no-store'});if(!r.ok)throw new Error(`Shared data request failed (${r.status})`);return r.json()}
async function syncToServer(force=false){
 if(!sharedMode||!syncReady||(!pendingSave&&!force))return;if(!navigator.onLine){syncState='offline';updateSyncUI();return}syncState='saving';syncError='';updateSyncUI();
 try{await flushPendingMedia();let r=await fetch('/api/data',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({revision:sharedRevision,data:db,clientId:currentUserId||'setup'})});if(r.status===409){let remote=await r.json(),base=lastSyncedDB||remote.data||seed;db=threeWayMerge(base,db,remote.data||seed);sharedRevision=remote.revision||0;lastSyncedDB=cloneDB(remote.data||seed);syncConflictCount++;persistCache();return syncToServer(true)}if(r.status===401){syncState='error';syncError='Your secure session has ended. Sign in again to save changes.';updateSyncUI();return}if(!r.ok)throw new Error(`Save failed (${r.status})`);let result=await r.json();sharedRevision=result.revision;if(result.data)db=result.data;lastSyncedDB=cloneDB(db);lastSyncAt=new Date().toISOString();pendingSave=false;syncState='synced';try{localStorage.setItem(BASE_KEY,JSON.stringify(lastSyncedDB));localStorage.removeItem(PENDING_KEY)}catch(e){};persistCache();updateSyncUI()}
 catch(e){pendingSave=true;syncState='offline';syncError=e.message||'Unable to reach the shared SiteSight computer.';try{localStorage.setItem(PENDING_KEY,'1')}catch(x){};updateSyncUI()}
}
async function pollSharedData(){if(!sharedMode||!syncReady||!navigator.onLine)return;try{let r=await fetch(`/api/data?revision=${sharedRevision}`,{cache:'no-store'});if(!r.ok)return;let incoming=await r.json();if(incoming.unchanged||incoming.revision===sharedRevision)return;if(pendingSave){db=threeWayMerge(lastSyncedDB||seed,db,incoming.data||seed);sharedRevision=incoming.revision;lastSyncedDB=cloneDB(incoming.data||seed);persistCache();scheduleSync(50)}else{db=incoming.data||seed;sharedRevision=incoming.revision;lastSyncedDB=cloneDB(db);migrate(false);persistCache();lastSyncAt=new Date().toISOString();syncState='synced';render()}}catch(e){if(pendingSave){syncState='offline';updateSyncUI()}}}
function startSyncPolling(){if(syncPollTimer)clearInterval(syncPollTimer);syncPollTimer=setInterval(pollSharedData,6000)}
window.addEventListener('online',()=>{if(sharedMode){syncState='saving';updateSyncUI();syncToServer(true)}});window.addEventListener('offline',()=>{if(sharedMode){syncState='offline';updateSyncUI()}});



function takeoffDrawing(){
 if(!takeoffContext)return null;
 if(takeoffContext.scope==='library')return (db.siteDrawings||[]).find(d=>d.id===takeoffContext.drawingId)||null;
 let x=(db.items||[]).find(x=>x.id===takeoffContext.itemId);
 return x?.drawings?.find(d=>d.id===takeoffContext.drawingId)||null
}
function takeoffSiteId(){
 if(!takeoffContext)return '';
 if(takeoffContext.scope==='library')return takeoffDrawing()?.siteId||'';
 return db.items.find(x=>x.id===takeoffContext.itemId)?.site||''
}
function takeoffAuditEntity(){
 if(takeoffContext?.scope==='library')return {type:'drawing library',id:takeoffContext.drawingId,label:takeoffDrawing()?.name||'Drawing'};
 let x=db.items.find(x=>x.id===takeoffContext?.itemId);
 return {type:'work item',id:x?.id||takeoffContext?.itemId,label:`${x?.ref||'Work Item'} · ${takeoffDrawing()?.name||'Drawing'}`}
}
function takeoffNormalPoint(evt){
 let svg=document.getElementById('takeoff-svg');if(!svg)return null;
 let r=svg.getBoundingClientRect();if(!r.width||!r.height)return null;
 return {x:Math.max(0,Math.min(1,(evt.clientX-r.left)/r.width)),y:Math.max(0,Math.min(1,(evt.clientY-r.top)/r.height))}
}
function takeoffSvgSize(){
 let svg=document.getElementById('takeoff-svg');return {w:svg?.clientWidth||svg?.getBoundingClientRect?.().width||1000,h:svg?.clientHeight||svg?.getBoundingClientRect?.().height||700}
}
function takeoffPxPoint(p,size=takeoffSvgSize()){return {x:Number(p.x||0)*size.w,y:Number(p.y||0)*size.h}}
function takeoffDistancePx(a,b,size=takeoffSvgSize()){let A=takeoffPxPoint(a,size),B=takeoffPxPoint(b,size);return Math.hypot(B.x-A.x,B.y-A.y)}
function takeoffPolylinePx(points,size=takeoffSvgSize()){let n=0;for(let i=1;i<points.length;i++)n+=takeoffDistancePx(points[i-1],points[i],size);return n}
function takeoffPolygonPx2(points,size=takeoffSvgSize()){
 if(points.length<3)return 0;let sum=0;
 for(let i=0;i<points.length;i++){let a=takeoffPxPoint(points[i],size),b=takeoffPxPoint(points[(i+1)%points.length],size);sum+=a.x*b.y-b.x*a.y}
 return Math.abs(sum)/2
}
function parseTakeoffDistance(v){
 let s=String(v||'').trim().toLowerCase().replace(/\s+/g,''),n=parseFloat(s.replace(/[^0-9.+-]/g,''));if(!(n>0))return 0;
 if(s.endsWith('mm'))return n/1000;if(s.endsWith('cm'))return n/100;if(s.endsWith('m'))return n;return n
}
function takeoffMetersPerPixel(d=takeoffDrawing(),size=takeoffSvgSize()){
 let c=d?.takeoffCalibration;if(!c?.points||c.points.length!==2||!(Number(c.distanceM)>0))return 0;
 let px=takeoffDistancePx(c.points[0],c.points[1],size);return px?Number(c.distanceM)/px:0
}
function takeoffQuantityFromGeometry(kind,points,size=takeoffSvgSize()){
 if(kind==='Count')return points.length;
 let mpp=takeoffMetersPerPixel(takeoffDrawing(),size);if(!mpp)return 0;
 if(kind==='Length')return takeoffPolylinePx(points,size)*mpp;
 let area=takeoffPolygonPx2(points,size)*mpp*mpp;return kind==='Deduct Area'?-area:area
}
function takeoffUnit(kind){return kind==='Length'?'m':kind==='Count'?'No.':'m²'}
function takeoffRateUnit(kind){return kind==='Length'?'£/m':kind==='Count'?'£/No.':'£/m²'}
function takeoffFmtQty(t){
 let q=Number(t.quantity||0),u=t.unit||takeoffUnit(t.kind);
 return `${q<0?'−':''}${Math.abs(q).toLocaleString('en-GB',{maximumFractionDigits:u==='No.'?0:2,minimumFractionDigits:u==='No.'?0:2})} ${u}`
}
function takeoffValue(t){return Number(t.quantity||0)*Number(t.rate||0)}
function takeoffTotals(d=takeoffDrawing()){
 let a=d?.takeoffs||[],gross=a.filter(t=>t.kind==='Area').reduce((n,t)=>n+Math.abs(Number(t.quantity||0)),0),
 deductions=a.filter(t=>t.kind==='Deduct Area').reduce((n,t)=>n+Math.abs(Number(t.quantity||0)),0),
 length=a.filter(t=>t.kind==='Length').reduce((n,t)=>n+Number(t.quantity||0),0),
 count=a.filter(t=>t.kind==='Count').reduce((n,t)=>n+Number(t.quantity||0),0),
 value=a.reduce((n,t)=>n+takeoffValue(t),0);
 return {gross,deductions,net:gross-deductions,length,count,value}
}
function drawingTakeoffSummary(d){
 let t=takeoffTotals(d),n=(d.takeoffs||[]).length;if(!n)return '';
 return `<div class="drawing-takeoff-summary"><span>📐 ${n} take-off item${n===1?'':'s'}</span>${t.gross||t.deductions?`<b>${Math.max(0,t.net).toFixed(2)} m² net</b>`:''}${hasPerm('view_financials')&&t.value?`<b>${money(t.value)}</b>`:''}</div>`
}
function takeoffCalibrationText(d=takeoffDrawing()){
 let c=d?.takeoffCalibration;if(!c)return 'Not calibrated';
 return `${Number(c.distanceM).toLocaleString('en-GB',{maximumFractionDigits:3})} m reference · ${c.by||'Unknown user'}${c.createdAt?' · '+new Date(c.createdAt).toLocaleString():''}`
}
function takeoffGeometrySvg(t,size=takeoffSvgSize(),isDraft=false){
 let pts=(t.points||[]).map(p=>takeoffPxPoint(p,size)),cls=isDraft?'takeoff-geometry draft':`takeoff-geometry type-${String(t.kind||'').toLowerCase().replace(/\s+/g,'-')}`;
 if(t.kind==='Count')return pts.map((p,i)=>`<g class="${cls}"><circle cx="${p.x}" cy="${p.y}" r="8"/><text x="${p.x}" y="${p.y+3}" text-anchor="middle">${i+1}</text></g>`).join('');
 if(!pts.length)return '';let list=pts.map(p=>`${p.x},${p.y}`).join(' ');
 if(t.kind==='Area'||t.kind==='Deduct Area')return `<polygon class="${cls}" points="${list}"></polygon>${pts.map(p=>`<circle class="${cls} point" cx="${p.x}" cy="${p.y}" r="4"></circle>`).join('')}`;
 return `<polyline class="${cls}" points="${list}"></polyline>${pts.map(p=>`<circle class="${cls} point" cx="${p.x}" cy="${p.y}" r="4"></circle>`).join('')}`
}
function takeoffCalibrationSvg(d=takeoffDrawing(),size=takeoffSvgSize()){
 let c=d?.takeoffCalibration;if(!c?.points||c.points.length!==2)return '';
 let a=takeoffPxPoint(c.points[0],size),b=takeoffPxPoint(c.points[1],size),mx=(a.x+b.x)/2,my=(a.y+b.y)/2;
 return `<g class="takeoff-calibration-line"><line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}"></line><circle cx="${a.x}" cy="${a.y}" r="5"></circle><circle cx="${b.x}" cy="${b.y}" r="5"></circle><text x="${mx}" y="${my-8}" text-anchor="middle">${Number(c.distanceM).toLocaleString('en-GB',{maximumFractionDigits:3})} m</text></g>`
}
function renderTakeoffSvg(){
 let svg=document.getElementById('takeoff-svg'),d=takeoffDrawing();if(!svg||!d)return;
 let size={w:svg.clientWidth||1000,h:svg.clientHeight||700};svg.setAttribute('viewBox',`0 0 ${size.w} ${size.h}`);
 let saved=(d.takeoffs||[]).map(t=>takeoffGeometrySvg(t,size,false)).join(''),
 draft=takeoffDraft.length?takeoffGeometrySvg({kind:takeoffTool,points:takeoffDraft},size,true):'',
 pointer=takeoffPointer&&takeoffDraft.length&&takeoffTool!=='Count'&&takeoffTool!=='Calibration'?(()=>{let a=takeoffPxPoint(takeoffDraft.at(-1),size),b=takeoffPxPoint(takeoffPointer,size);return `<line class="takeoff-pointer-line" x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}"></line>`})():'';
 svg.innerHTML=takeoffCalibrationSvg(d,size)+saved+draft+pointer
}
function takeoffCanvasMove(evt){if(!takeoffDraft.length)return;takeoffPointer=takeoffNormalPoint(evt);renderTakeoffSvg()}
function takeoffCanvasLeave(){takeoffPointer=null;renderTakeoffSvg()}
function takeoffCanvasClick(evt){
 if(!takeoffContext||takeoffTool==='select')return;
 let p=takeoffNormalPoint(evt);if(!p)return;
 if(takeoffTool==='Calibration'){takeoffDraft.push(p);renderTakeoffSvg();if(takeoffDraft.length===2)setTimeout(finishTakeoffCalibration,20);return}
 if(takeoffTool!=='Count'&&!takeoffDrawing()?.takeoffCalibration)return toast('Calibrate the drawing first.');
 takeoffDraft.push(p);takeoffPointer=null;renderTakeoffSvg();updateTakeoffDraftStatus()
}
function updateTakeoffDraftStatus(){
 let e=document.getElementById('takeoff-draft-status');if(!e)return;
 let min=takeoffTool==='Length'?2:(takeoffTool==='Area'||takeoffTool==='Deduct Area'?3:1);
 e.textContent=takeoffDraft.length?`${takeoffDraft.length} point${takeoffDraft.length===1?'':'s'} selected${takeoffDraft.length>=min?' · ready to finish':''}`:'Click points on the drawing'
}
function chooseTakeoffTool(kind){
 if(kind!=='Count'&&kind!=='Calibration'&&!takeoffDrawing()?.takeoffCalibration)return toast('Calibrate the drawing first.');
 takeoffTool=kind;takeoffDraft=[];takeoffPending=null;takeoffPointer=null;renderTakeoffWorkspace()
}
function cancelTakeoffDraft(){takeoffDraft=[];takeoffPending=null;takeoffPointer=null;renderTakeoffWorkspace()}
function finishTakeoffCalibration(){
 if(takeoffDraft.length!==2)return;
 let input=prompt('Enter the REAL distance between those two points. Use metres, or include mm/cm (for example: 5.4, 5400mm or 540cm).','5');
 let distanceM=parseTakeoffDistance(input);if(!distanceM){takeoffDraft=[];renderTakeoffWorkspace();return toast('Calibration cancelled — enter a valid distance next time.')}
 let d=takeoffDrawing(),had=(d.takeoffs||[]).length>0;
 d.takeoffCalibration={points:JSON.parse(JSON.stringify(takeoffDraft)),distanceM,createdAt:new Date().toISOString(),by:currentUser()?.name||'',userId:currentUser()?.id||''};
 let entity=takeoffAuditEntity();save();audit('drawing','drawing take-off',d.id||entity.id,`${entity.label}: drawing calibrated to ${distanceM} m`,takeoffSiteId());
 takeoffDraft=[];takeoffTool='select';takeoffPointer=null;
 if(had&&confirm('Recalculate the existing saved measurements using this new calibration?'))recalculateTakeoffsWithCurrentCalibration();
 renderTakeoffWorkspace();savedState('Drawing calibrated')
}
function recalculateTakeoffsWithCurrentCalibration(){
 let d=takeoffDrawing(),size=takeoffSvgSize();if(!d?.takeoffCalibration)return;
 (d.takeoffs||[]).forEach(t=>{if(t.kind!=='Count'&&Array.isArray(t.points)&&t.points.length)t.quantity=takeoffQuantityFromGeometry(t.kind,t.points,size)});
 save();let entity=takeoffAuditEntity();audit('drawing','drawing take-off',d.id||entity.id,`${entity.label}: existing take-off quantities recalculated`,takeoffSiteId())
}
function finishTakeoffGeometry(){
 let min=takeoffTool==='Length'?2:(takeoffTool==='Area'||takeoffTool==='Deduct Area'?3:(takeoffTool==='Count'?1:99));
 if(takeoffDraft.length<min)return toast(`Add at least ${min} point${min===1?'':'s'} first.`);
 let quantity=takeoffQuantityFromGeometry(takeoffTool,takeoffDraft);if(takeoffTool!=='Count'&&!quantity)return toast('Could not calculate this measurement. Check the calibration.');
 takeoffPending={kind:takeoffTool,points:JSON.parse(JSON.stringify(takeoffDraft)),quantity,unit:takeoffUnit(takeoffTool)};
 takeoffDraft=[];takeoffPointer=null;renderTakeoffWorkspace()
}
function saveTakeoffMeasurement(){
 let d=takeoffDrawing(),p=takeoffPending;if(!d||!p)return;
 let label=(document.getElementById('takeoff-label')?.value||'').trim();if(!label)return toast('Enter a description for this measurement.');
 let rate=hasPerm('view_financials')?Number(document.getElementById('takeoff-rate')?.value||0):0;
 let t={id:'tk'+Date.now(),kind:p.kind,label,points:p.points,quantity:p.quantity,unit:p.unit,rate,createdAt:new Date().toISOString(),by:currentUser()?.name||'',userId:currentUser()?.id||'',calibrationDistanceM:d.takeoffCalibration?.distanceM||0};
 d.takeoffs=d.takeoffs||[];d.takeoffs.push(t);
 let entity=takeoffAuditEntity();save();audit('drawing','drawing take-off',d.id||entity.id,`${entity.label}: ${t.kind} saved — ${t.label} · ${takeoffFmtQty(t)}${hasPerm('view_financials')&&rate?` · ${money(takeoffValue(t))}`:''}`,takeoffSiteId());
 takeoffPending=null;takeoffTool='select';renderTakeoffWorkspace();savedState('Take-off measurement saved')
}
function deleteTakeoffMeasurement(id){
 if(!hasPerm('manage_drawings'))return toast('You do not have permission to change drawing take-offs.');
 let d=takeoffDrawing(),idx=d?.takeoffs?.findIndex(t=>t.id===id);if(idx<0)return;
 let old=d.takeoffs[idx],entity=takeoffAuditEntity();d.takeoffs.splice(idx,1);save();audit('drawing','drawing take-off',d.id||entity.id,`${entity.label}: take-off removed — ${old.label}`,takeoffSiteId());renderTakeoffWorkspace();
 showUndo('Take-off item removed',()=>{d.takeoffs.splice(idx,0,old);save();audit('drawing','drawing take-off',d.id||entity.id,`${entity.label}: take-off removal undone — ${old.label}`,takeoffSiteId());renderTakeoffWorkspace()})
}
function clearDrawingCalibration(){
 if(!hasPerm('manage_drawings'))return;
 let d=takeoffDrawing();if(!d?.takeoffCalibration)return;
 if(!confirm('Remove the calibration? Existing saved quantities will stay as recorded, but new length/area measurements will require calibration again.'))return;
 d.takeoffCalibration=null;takeoffDraft=[];takeoffTool='select';save();let entity=takeoffAuditEntity();audit('drawing','drawing take-off',d.id||entity.id,`${entity.label}: drawing calibration removed`,takeoffSiteId());renderTakeoffWorkspace()
}
function takeoffPendingHtml(){
 if(!takeoffPending)return '';let p=takeoffPending;
 return `<div class="takeoff-pending"><span class="takeoff-kicker">SAVE MEASUREMENT</span><h3>${p.kind}</h3><div class="takeoff-pending-qty">${takeoffFmtQty(p)}</div><div class="field"><label>Description</label><input id="takeoff-label" class="input" placeholder="${p.kind==='Deduct Area'?'e.g. Windows & doors':p.kind==='Area'?'e.g. Front elevation brickwork':p.kind==='Length'?'e.g. Soldier course':'e.g. Air bricks'}"></div>${hasPerm('view_financials')?`<div class="field"><label>Rate (${takeoffRateUnit(p.kind)})</label><input id="takeoff-rate" class="input" type="number" min="0" step="0.01" placeholder="Optional"><div class="muted">Value = quantity × rate.</div></div>`:''}<div class="actions"><button class="btn green" onclick="saveTakeoffMeasurement()">✓ Save measurement</button><button class="btn light" onclick="takeoffPending=null;renderTakeoffWorkspace()">Cancel</button></div></div>`
}
function takeoffListHtml(d=takeoffDrawing()){
 let a=d?.takeoffs||[];if(!a.length)return `<div class="takeoff-empty">No saved measurements yet.</div>`;
 return `<div class="takeoff-list">${a.map(t=>`<div class="takeoff-row"><div class="takeoff-row-main"><span class="takeoff-kind kind-${String(t.kind).toLowerCase().replace(/\s+/g,'-')}">${t.kind}</span><b>${calendarSafe(t.label)}</b><small>${takeoffFmtQty(t)} · ${t.by||'Unknown user'}${t.createdAt?' · '+new Date(t.createdAt).toLocaleDateString('en-GB'):''}</small></div>${hasPerm('view_financials')?`<div class="takeoff-money"><span>${Number(t.rate||0)?money(t.rate)+' / '+t.unit:'No rate'}</span><b>${Number(t.rate||0)?money(takeoffValue(t)):'—'}</b></div>`:''}${hasPerm('manage_drawings')?`<button class="takeoff-delete" onclick="deleteTakeoffMeasurement('${t.id}')" title="Remove measurement">×</button>`:''}</div>`).join('')}</div>`
}
function takeoffTotalsHtml(d=takeoffDrawing()){
 let t=takeoffTotals(d);return `<div class="takeoff-totals"><div><span>Gross area</span><b>${t.gross.toFixed(2)} m²</b></div><div><span>Deductions</span><b>${t.deductions.toFixed(2)} m²</b></div><div class="takeoff-net"><span>Net area</span><b>${Math.max(0,t.net).toFixed(2)} m²</b></div><div><span>Total length</span><b>${t.length.toFixed(2)} m</b></div><div><span>Total count</span><b>${Math.round(t.count)} No.</b></div>${hasPerm('view_financials')?`<div class="takeoff-value"><span>Total value</span><b>${money(t.value)}</b></div>`:''}</div>`
}

function takeoffGuideHtml(){
 let calibrated=!!takeoffDrawing()?.takeoffCalibration,toolChosen=takeoffTool!=='select'&&takeoffTool!=='Calibration';
 return `<div class="takeoff-guide"><div class="${calibrated?'done':'active'}"><b>1</b><span><strong>Calibrate</strong><small>${calibrated?'Scale ready':'Click two known points'}</small></span></div><div class="${calibrated?(toolChosen?'done':'active'):''}"><b>2</b><span><strong>Choose tool</strong><small>Length, Area, Deduction or Count</small></span></div><div class="${toolChosen?'active':''}"><b>3</b><span><strong>Measure & Save</strong><small>Click points, Finish, then name it</small></span></div></div>`
}

function takeoffWorkspaceHtml(){
 let d=takeoffDrawing();if(!d||!takeoffContext)return '';
 let isPdf=takeoffContext.isPdf,canEdit=hasPerm('manage_drawings'),cal=!!d.takeoffCalibration,
 finishReady=takeoffTool==='Length'?takeoffDraft.length>=2:(takeoffTool==='Area'||takeoffTool==='Deduct Area'?takeoffDraft.length>=3:(takeoffTool==='Count'?takeoffDraft.length>=1:false));
 return `<div class="takeoff-workspace"><div class="takeoff-head"><div><span class="takeoff-kicker">DRAWING TAKE-OFF</span><h2>${calendarSafe(d.name||d.originalName||'Drawing')}</h2><div class="muted">${takeoffContext.scope==='work'?`${db.items.find(x=>x.id===takeoffContext.itemId)?.ref||''} · `:''}${siteName(takeoffSiteId())}${isPdf?' · PDF — keep page/zoom fixed after calibration':''}</div></div><div class="actions"><button class="btn light" onclick="closeDrawingTakeoff()">✕ Close take-off</button></div></div>${takeoffGuideHtml()}<div class="takeoff-body"><div class="takeoff-canvas-column"><div class="takeoff-toolbar">${canEdit?`<button class="${takeoffTool==='Calibration'?'active':''}" onclick="chooseTakeoffTool('Calibration')">↔ Calibrate</button><button class="${takeoffTool==='Length'?'active':''}" onclick="chooseTakeoffTool('Length')" ${!cal?'disabled':''}>╱ Length</button><button class="${takeoffTool==='Area'?'active':''}" onclick="chooseTakeoffTool('Area')" ${!cal?'disabled':''}>▱ Area</button><button class="${takeoffTool==='Deduct Area'?'active':''}" onclick="chooseTakeoffTool('Deduct Area')" ${!cal?'disabled':''}>⊟ Deduct Area</button><button class="${takeoffTool==='Count'?'active':''}" onclick="chooseTakeoffTool('Count')">● Count</button>${takeoffDraft.length?`<button class="finish" onclick="${takeoffTool==='Calibration'?'finishTakeoffCalibration()':'finishTakeoffGeometry()'}" ${takeoffTool!=='Calibration'&&!finishReady?'disabled':''}>✓ Finish</button><button onclick="cancelTakeoffDraft()">Clear points</button>`:''}`:''}<span id="takeoff-draft-status">${takeoffDraft.length?`${takeoffDraft.length} point${takeoffDraft.length===1?'':'s'} selected`:(canEdit?'Choose a tool, then click the drawing':'View-only take-off')}</span></div><div class="takeoff-drawing-stage ${isPdf?'takeoff-pdf':''}">${isPdf?`<iframe src="${takeoffContext.src}#toolbar=0&navpanes=0&view=FitH" title="${calendarSafe(d.name||'Drawing')}"></iframe>`:`<img src="${takeoffContext.src}" alt="${calendarSafe(d.name||'Drawing')}">`}<svg id="takeoff-svg" onclick="takeoffCanvasClick(event)" onmousemove="takeoffCanvasMove(event)" onmouseleave="takeoffCanvasLeave()"></svg></div>${isPdf?'<div class="takeoff-pdf-warning"><b>PDF measurement:</b> SiteSight measures the visible PDF view. Calibrate the sheet first and do not change the PDF zoom/page while measuring that sheet.</div>':''}</div><aside class="takeoff-sidebar"><div class="takeoff-scale ${cal?'calibrated':''}"><div><span>DRAWING SCALE</span><b>${cal?'✓ Calibrated':'Not calibrated'}</b></div><p>${cal?calendarSafe(takeoffCalibrationText(d)):'Click Calibrate, select two known points and enter their real distance.'}</p>${canEdit&&cal?'<button class="btn light smallbtn" onclick="chooseTakeoffTool(\'Calibration\')">Recalibrate</button> <button class="btn light smallbtn" onclick="clearDrawingCalibration()">Remove scale</button>':''}</div>${takeoffPendingHtml()}${takeoffTotalsHtml(d)}<div class="takeoff-list-head"><div><h3>Saved Take-off</h3><span>${(d.takeoffs||[]).length} item${(d.takeoffs||[]).length===1?'':'s'}</span></div></div>${takeoffListHtml(d)}</aside></div></div>`
}
function renderTakeoffWorkspace(){if(!takeoffContext)return;modal=takeoffWorkspaceHtml();render();setTimeout(()=>{renderTakeoffSvg();updateTakeoffDraftStatus()},0)}
async function openDrawingTakeoff(scope,drawingId,itemId=''){
 if(!hasPerm('view_drawings'))return toast('You do not have permission to view drawings.');
 let d=scope==='library'?(db.siteDrawings||[]).find(d=>d.id===drawingId):(db.items.find(x=>x.id===itemId)?.drawings||[]).find(d=>d.id===drawingId);if(!d?.data)return toast('Drawing file is unavailable');
 let src=await resolveMediaRef(d.data);if(!src)return toast('This drawing is stored in a different browser. Open SiteSight on the device where it was uploaded.');
 let isPdf=d.type==='application/pdf'||String(d.fileName||d.originalName||'').toLowerCase().endsWith('.pdf');
 takeoffContext={scope,drawingId,itemId,src,isPdf};takeoffTool='select';takeoffDraft=[];takeoffPending=null;takeoffPointer=null;renderTakeoffWorkspace()
}
function closeDrawingTakeoff(){takeoffContext=null;takeoffTool='select';takeoffDraft=[];takeoffPending=null;takeoffPointer=null;modal=null;render()}

function drawingBrowserDB(){return new Promise((resolve,reject)=>{let r=indexedDB.open('sitesight_browser_drawings',1);r.onupgradeneeded=()=>{if(!r.result.objectStoreNames.contains('drawings'))r.result.createObjectStore('drawings')};r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error)})}
async function putBrowserDrawing(key,blob){let d=await drawingBrowserDB();return new Promise((resolve,reject)=>{let tx=d.transaction('drawings','readwrite');tx.objectStore('drawings').put(blob,key);tx.oncomplete=()=>resolve();tx.onerror=()=>reject(tx.error)})}
async function getBrowserDrawing(key){let d=await drawingBrowserDB();return new Promise((resolve,reject)=>{let tx=d.transaction('drawings','readonly'),r=tx.objectStore('drawings').get(key);r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error)})}
async function deleteBrowserDrawingKey(key){let d=await drawingBrowserDB();return new Promise(resolve=>{let tx=d.transaction('drawings','readwrite');tx.objectStore('drawings').delete(key);tx.oncomplete=()=>resolve()})}
async function storeDrawingInBrowser(file){let blob=file;if(String(file?.type||'').startsWith('image/'))blob=dataUrlToBlob(await readPhotoFile(file,2000,.86));let key='drw-'+Date.now()+'-'+Math.random().toString(16).slice(2);await putBrowserDrawing(key,blob);return 'browser:'+key}
const browserDrawingUrls={};
async function resolveMediaRef(ref){if(!String(ref||'').startsWith('browser:'))return mediaSrc(ref);let key=String(ref).slice(8),blob=await getBrowserDrawing(key);if(!blob)return '';if(!browserDrawingUrls[key])browserDrawingUrls[key]=URL.createObjectURL(blob);return browserDrawingUrls[key]}
function drawingReferenceStillUsed(ref){return (db.siteDrawings||[]).some(d=>d.data===ref)||db.items.some(x=>(x.drawings||[]).some(d=>d.data===ref))}
async function cleanupDrawingRef(ref){if(!String(ref||'').startsWith('browser:')||drawingReferenceStillUsed(ref))return;let key=String(ref).slice(8);try{await deleteBrowserDrawingKey(key);if(browserDrawingUrls[key]){URL.revokeObjectURL(browserDrawingUrls[key]);delete browserDrawingUrls[key]}}catch(e){}}

function mediaDB(){return new Promise((resolve,reject)=>{let r=indexedDB.open('sitesight_trial_media',1);r.onupgradeneeded=()=>r.result.createObjectStore('media');r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error)})}
async function putPendingBlob(key,blob){let d=await mediaDB();return new Promise((resolve,reject)=>{let tx=d.transaction('media','readwrite');tx.objectStore('media').put(blob,key);tx.oncomplete=()=>resolve();tx.onerror=()=>reject(tx.error)})}
async function getPendingBlob(key){let d=await mediaDB();return new Promise((resolve,reject)=>{let tx=d.transaction('media','readonly'),r=tx.objectStore('media').get(key);r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error)})}
async function deletePendingBlob(key){let d=await mediaDB();return new Promise(resolve=>{let tx=d.transaction('media','readwrite');tx.objectStore('media').delete(key);tx.oncomplete=()=>resolve()})}
function dataUrlToBlob(data){let [head,b64]=data.split(','),mime=(head.match(/data:([^;]+)/)||[])[1]||'image/jpeg',bin=atob(b64),arr=new Uint8Array(bin.length);for(let i=0;i<bin.length;i++)arr[i]=bin.charCodeAt(i);return new Blob([arr],{type:mime})}
function mediaUploadSiteId(explicit){if(explicit!==undefined)return explicit;let item=(db.items||[]).find(row=>row.id===selected);if(item?.site)return item.site;if(site!=='all'&&canAccessSite(site))return site;let ids=userSiteIds();return ids.length===1?ids[0]:(activeSites().find(row=>canAccessSite(row.id))?.id||'')}
async function uploadBlobToServer(blob,name='site-photo.jpg',siteId=mediaUploadSiteId()){let r=await fetch('/api/upload',{method:'POST',headers:{'Content-Type':blob.type||'application/octet-stream','X-Filename':encodeURIComponent(name),'X-Site-Id':siteId||''},body:blob});if(!r.ok){let result=await r.json().catch(()=>({}));throw new Error(result.message||`Upload failed (${r.status})`)}return (await r.json()).url}
async function storeMediaFile(file,overrideData='',siteId=mediaUploadSiteId()){
 let blob,name=file?.name||'site-photo.jpg';
 if(overrideData)blob=dataUrlToBlob(overrideData);else if(String(file?.type||'').startsWith('image/'))blob=dataUrlToBlob(await readPhotoFile(file,1800,.82));else blob=file;
 if(sharedMode&&navigator.onLine){try{return await uploadBlobToServer(blob,name,siteId)}catch(e){}}
 if(sharedMode){let key='m'+Date.now()+Math.random().toString(16).slice(2);await putPendingBlob(key,blob);syncState='offline';updateSyncUI();return 'pending:'+key}
 if(overrideData)return overrideData;if(String(file?.type||'').startsWith('image/'))return await readPhotoFile(file,1800,.82);return await readUploadFile(file)
}
const pendingMediaUrls={};
function mediaSrc(ref){
 let s=String(ref||'');
 if(s.startsWith('browser:')){let key=s.slice(8);if(browserDrawingUrls[key])return browserDrawingUrls[key];setTimeout(async()=>{if(browserDrawingUrls[key])return;try{let b=await getBrowserDrawing(key);if(b){browserDrawingUrls[key]=URL.createObjectURL(b);document.querySelectorAll(`[data-browser-media="${key}"]`).forEach(e=>e.src=browserDrawingUrls[key])}}catch(e){}},0);return `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180"><rect width="100%" height="100%" fill="#edf2f5"/><text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="#627786" font-family="Arial" font-size="14">Drawing stored in browser</text></svg>')}`}
 if(!s.startsWith('pending:'))return ref||'';let key=s.slice(8);if(pendingMediaUrls[key])return pendingMediaUrls[key];setTimeout(async()=>{if(pendingMediaUrls[key])return;try{let b=await getPendingBlob(key);if(b){pendingMediaUrls[key]=URL.createObjectURL(b);document.querySelectorAll(`[data-pending-media="${key}"]`).forEach(e=>e.src=pendingMediaUrls[key])}}catch(e){}},0);return `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180"><rect width="100%" height="100%" fill="#e9eef1"/><text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="#627786" font-family="Arial" font-size="14">Waiting to sync</text></svg>')}`
}
function pendingMediaAttr(ref){let s=String(ref||'');if(s.startsWith('browser:'))return ` data-browser-media="${s.slice(8)}"`;if(s.startsWith('pending:'))return ` data-pending-media="${s.slice(8)}"`;return ''}
function collectPendingRefs(obj,out=[]){if(!obj||typeof obj!=='object')return out;if(Array.isArray(obj)){obj.forEach(v=>collectPendingRefs(v,out));return out}Object.keys(obj).forEach(k=>{let v=obj[k];if(typeof v==='string'&&v.startsWith('pending:'))out.push({obj,key:k,ref:v});else collectPendingRefs(v,out)});return out}
function mediaSiteForRef(ref){for(const item of db.items||[]){if((item.workPhotos||[]).includes(ref)||(item.drawings||[]).some(row=>row.data===ref)||(item.issues||[]).some(issue=>(issue.photos||[]).includes(ref)))return item.site||''}for(const row of db.siteDrawings||[])if(row.data===ref)return row.siteId||'';for(const row of db.siteDiary||[])if((row.photos||[]).includes(ref))return row.siteId||'';for(const row of db.documents||[])if(row.data===ref||(row.versions||[]).some(version=>version.data===ref))return row.siteId||'';return mediaUploadSiteId()}
async function flushPendingMedia(){if(!sharedMode||!navigator.onLine)return;let refs=collectPendingRefs(db);for(let entry of refs){let key=entry.ref.slice(8),blob=await getPendingBlob(key);if(!blob)continue;let url=await uploadBlobToServer(blob,'offline-upload'+(blob.type==='application/pdf'?'.pdf':'.jpg'),mediaSiteForRef(entry.ref));entry.obj[entry.key]=url;await deletePendingBlob(key)}if(refs.length)persistCache()}
async function bootSiteSight(){
 if(!sharedMode){migrate(false);syncReady=true;syncState='local';if(!db.users.length)firstSetupScreen();else render();return}
 document.getElementById('root').innerHTML=`<div class="boot-screen"><img src="assets/sitesight-login-logo.png" alt="SiteSight"><div class="boot-spinner"></div><b>Connecting to SiteSight…</b><span>Loading the latest site records</span></div>`;
 try{let remote=await fetchSharedData(),cached=null,pending=localStorage.getItem(PENDING_KEY)==='1';try{cached=JSON.parse(localStorage.getItem(CACHE_KEY)||'null')}catch(e){};sharedRevision=remote.revision||0;lastSyncedDB=cloneDB(remote.data||seed);if(pending&&cached){let base={};try{base=JSON.parse(localStorage.getItem(BASE_KEY)||'{}')}catch(e){};db=threeWayMerge(base,cached,remote.data||seed);pendingSave=true}else db=remote.data||seed;migrate(false);persistCache();syncReady=true;syncState='synced';lastSyncAt=new Date().toISOString();if(pendingSave)await syncToServer(true);startSyncPolling();if(!db.users.length)firstSetupScreen();else render()}
 catch(e){migrate(false);syncReady=true;syncState='offline';syncError='Shared SiteSight is unavailable. Existing cached records can still be viewed and updated while this page remains open.';if(!db.users.length)firstSetupScreen();else render()}
}

let db=seed;
const STORAGE_EDITION=/\bdemo\b/i.test(document.title)?'demo':'normal',CACHE_KEY=`sitesight_v187_${STORAGE_EDITION}_cache`,BASE_KEY=`sitesight_v187_${STORAGE_EDITION}_server_base`,PENDING_KEY=`sitesight_v187_${STORAGE_EDITION}_pending`,CURRENT_USER_KEY=`sitesight_v187_${STORAGE_EDITION}_current_user`;
try{let stored=localStorage.getItem(CACHE_KEY)||localStorage.getItem(`sitesight_v185_${STORAGE_EDITION}_cache`);if(stored)db=JSON.parse(stored)||seed}catch(e){db=seed}
let sharedMode=(location.protocol==='http:'||location.protocol==='https:')&&!window.SITESIGHT_LOCAL_PACKAGE,syncReady=false,sharedRevision=0,lastSyncedDB=null,pendingSave=false,syncTimer=null,syncPollTimer=null,syncState=sharedMode?'connecting':'local',syncError='',lastSyncAt='',syncConflictCount=0;
const cloneDB=o=>JSON.parse(JSON.stringify(o));
function persistCache(){try{localStorage.setItem(CACHE_KEY,JSON.stringify(db));return true}catch(e){syncState='error';syncError='This browser could not store the local safety copy.';setTimeout(()=>toast('Could not save local safety copy'),0);return false}}
function save(){let ok=persistCache();if(!syncReady)return ok;if(sharedMode){pendingSave=true;syncState=navigator.onLine?'saving':'offline';try{localStorage.setItem(PENDING_KEY,'1')}catch(e){};scheduleSync()}else syncState='local';updateSyncUI();return ok}
function scheduleSync(delay=350){if(!sharedMode||!syncReady)return;if(syncTimer)clearTimeout(syncTimer);syncTimer=setTimeout(()=>syncToServer(),delay)}

const permissionGroups=[
 {name:'Work',items:[
  ['dashboard','Dashboard'],
  ['work_items','View Work Items'],
  ['update_progress','Update Progress'],
  ['view_issues','View Issues'],
  ['raise_issues','Raise Issues'],
  ['manage_issues','Manage / Resolve Issues'],
  ['sign_off','Sign Off Work'],
  ['edit_lifts','Edit Lift Stages'],
  ['manage_workitems','Create / Manage Work Items']
 ]},
 {name:'Commercial',items:[
  ['view_financials','View Financials'],
  ['record_payments','Record Payments'],
  ['manage_payments','Manage / Remove Payments'],
  ['manage_variations','Create / Manage Variations'],
  ['approve_variations','Approve Variations']
 ]},
 {name:'Site Information',items:[
  ['view_drawings','View Drawings'],
  ['manage_drawings','Manage Drawings'],
  ['view_calendar','View Calendar'],
  ['manage_calendar','Manage Calendar'],
  ['view_site_diary','View Site Diary'],
  ['manage_site_diary','Manage Site Diary'],
  ['download_reports','Download Reports']
 ]},
 {name:'Time & Attendance',items:[
  ['view_attendance','View Time & Attendance'],
  ['clock_attendance','Clock In / Out'],
  ['manage_attendance','Manage / Correct Timesheets'],
  ['approve_timesheets','Approve Weekly Timesheets']
 ]},
 {name:'Management',items:[
  ['gangs','Team Dashboards'],
  ['analytics','View Analytics'],
  ['audit','View Audit Trail'],
  ['manage_gangs','Manage Teams'],
  ['manage_sites','Manage Sites'],
  ['view_archives','View Site Archives'],
  ['archive_sites','Archive / Restore Sites']
 ]},
 {name:'Users & Security',items:[
  ['manage_users','Manage Users'],
  ['reset_passwords','Reset Other Users Passwords'],
  ['permanent_delete','Permanent Deletion']
 ]}
];
const permissionDefs=permissionGroups.flatMap(g=>g.items);
const permissionLegacyMap={
 view_issues:'issues',raise_issues:'issues',manage_issues:'issues',
 view_drawings:'drawings',manage_drawings:'drawings',
 view_financials:'payments',record_payments:'payments',manage_payments:'payments',manage_variations:'payments',approve_variations:'payments',
 view_calendar:'calendar',manage_calendar:'calendar',
 view_site_diary:'site_diary',manage_site_diary:'site_diary',
 download_reports:'reports',
 view_archives:'manage_sites',archive_sites:'manage_sites'
};
function emptyPerms(){return Object.fromEntries(permissionDefs.map(([k])=>[k,false]))}
function defaultPerms(r){
 if(r==='Subcontractor')r='Operator';
 let p=emptyPerms(),on=(...keys)=>keys.forEach(k=>p[k]=true);
 if(r==='Director'){permissionDefs.forEach(([k])=>p[k]=true);return p}
 if(r==='Contracts Manager'){
  permissionDefs.forEach(([k])=>p[k]=true);
  p.manage_users=false;p.reset_passwords=false;p.permanent_delete=false;
  return p
 }
 if(r==='Site Manager'){
  on('dashboard','work_items','update_progress','view_issues','raise_issues','manage_issues','sign_off','edit_lifts','manage_workitems',
     'view_drawings','manage_drawings','view_calendar','manage_calendar','view_site_diary','manage_site_diary','download_reports',
     'gangs','analytics','manage_gangs','manage_sites','view_archives','archive_sites','view_attendance','clock_attendance','manage_attendance','approve_timesheets');
  return p
 }
 if(r==='Supervisor'){
  on('dashboard','work_items','update_progress','view_issues','raise_issues','manage_issues',
     'view_drawings','manage_drawings','view_calendar','view_site_diary','manage_site_diary','gangs','view_attendance','clock_attendance','manage_attendance');
  return p
 }
 if(r==='Quantity Surveyor'){
  on('dashboard','view_drawings',
     'view_financials','record_payments','manage_payments','manage_variations','approve_variations',
     'view_calendar','download_reports','gangs','analytics','view_archives','view_attendance');
  return p
 }
 if(r==='Accounts'){
  on('dashboard','view_financials','record_payments','manage_payments','download_reports','analytics','view_archives');
  return p
 }
 if(r==='Read Only'){
  on('dashboard','view_drawings','view_calendar','view_site_diary','gangs');
  return p
 }
 if(r==='Operator'){
  on('clock_attendance');
  return p
 }
 on('clock_attendance');
 return p
}
function migratePerms(role,legacy={}){
 let p=defaultPerms(role);
 Object.keys(p).forEach(k=>{
  if(Object.prototype.hasOwnProperty.call(legacy,k))p[k]=!!legacy[k];
  else{
   let old=permissionLegacyMap[k];
   if(old&&Object.prototype.hasOwnProperty.call(legacy,old))p[k]=!!legacy[old]
  }
 });
 return {...legacy,...p}
}
function migrate(persist=true){
 db.sites=(db.sites||[]).map(s=>({...s,archived:!!s.archived}));
 db.notifications=db.notifications||[];db.calendarNotes=Array.isArray(db.calendarNotes)?db.calendarNotes:[];db.auditLog=Array.isArray(db.auditLog)?db.auditLog:[];db.siteDiary=Array.isArray(db.siteDiary)?db.siteDiary:[];db.siteDiary=db.siteDiary.map(e=>({...e,amendments:Array.isArray(e.amendments)?e.amendments:[],createdAt:e.createdAt||''}));db.siteDrawings=Array.isArray(db.siteDrawings)?db.siteDrawings:[];db.siteDrawings=db.siteDrawings.map(d=>({...d,lastUsedAt:Number(d.lastUsedAt||0),takeoffs:Array.isArray(d.takeoffs)?d.takeoffs:[],takeoffCalibration:d.takeoffCalibration||null}));
 db.attendanceShifts=Array.isArray(db.attendanceShifts)?db.attendanceShifts:[];db.attendanceAttempts=Array.isArray(db.attendanceAttempts)?db.attendanceAttempts:[];db.attendanceAbsences=Array.isArray(db.attendanceAbsences)?db.attendanceAbsences:[];db.timesheetApprovals=Array.isArray(db.timesheetApprovals)?db.timesheetApprovals:[];
 db.sites=db.sites.map(s=>({...s,attendanceConfig:{enabled:s.attendanceConfig?.enabled!==false,latitude:(s.attendanceConfig?.latitude!==null&&s.attendanceConfig?.latitude!==undefined&&s.attendanceConfig?.latitude!==''&&Number.isFinite(Number(s.attendanceConfig.latitude)))?Number(s.attendanceConfig.latitude):null,longitude:(s.attendanceConfig?.longitude!==null&&s.attendanceConfig?.longitude!==undefined&&s.attendanceConfig?.longitude!==''&&Number.isFinite(Number(s.attendanceConfig.longitude)))?Number(s.attendanceConfig.longitude):null,radiusM:Number(s.attendanceConfig?.radiusM||150),maxAccuracyM:Number(s.attendanceConfig?.maxAccuracyM||75),maxShiftHours:Number(s.attendanceConfig?.maxShiftHours||12),locationSetAt:s.attendanceConfig?.locationSetAt||'',locationSetBy:s.attendanceConfig?.locationSetBy||'',demoPreset:!!s.attendanceConfig?.demoPreset}}));
 db.users=db.users||[];
 db.users=db.users.map((u,i)=>{if(u.role==='Manager')u.role='Site Manager';if(u.role==='Subcontractor'){u.role='Operator';u.isSubcontractor=true}return ({
   ...u,
   isSubcontractor:u.role==='Operator'&&u.isSubcontractor===true,
   username:u.username||((u.name||('user'+i)).toLowerCase().replace(/[^a-z0-9]+/g,'.').replace(/^\.+|\.+$/g,'')||('user'+i)),
   email:u.email||((u.username||('user'+i))+'@example.com'),
   password:u.password||'',
   resetCode:u.resetCode||'',resetExpires:u.resetExpires||0,
	   allowedSites:Array.isArray(u.allowedSites)?u.allowedSites:[],
	   // Site access is deliberately fail-closed. Company-wide access must be
	   // explicit (or belong to a Director); an empty list never grants access.
	   allSitesAccess:u.role==='Director'||u.allSitesAccess===true,
   notificationsEnabled:!!u.notificationsEnabled,
   trainingMode:u.trainingMode===undefined?(u.username!=='director'):!!u.trainingMode,
   onboardingSeen:u.onboardingSeen===undefined?true:!!u.onboardingSeen,
   onboardingCompletedAt:u.onboardingCompletedAt||'',
   loginCount:Number(u.loginCount||0),
   lastLoginAt:u.lastLoginAt||'',
   recentWorkItems:Array.isArray(u.recentWorkItems)?u.recentWorkItems:[],
   cameraIntroSeen:!!u.cameraIntroSeen,
   linkedMemberId:u.linkedMemberId||'',
   permissions:migratePerms(u.role||'Operator',u.permissions||{})
 })});
 db.gangs=(db.gangs||[]).map(g=>({...g,members:(Array.isArray(g.members)?g.members:[]).map(m=>({...m,notes:m.notes||'',photos:Array.isArray(m.photos)?m.photos:[],employmentType:m.employmentType||'Not tracked',hourlyRate:Number(m.hourlyRate||0),overtimeRate:Number(m.overtimeRate||m.hourlyRate||0),dayRate:Number(m.dayRate||0),standardDayHours:Number(m.standardDayHours||8),standardWeekHours:Number(m.standardWeekHours||40),defaultBreakMinutes:Number(m.defaultBreakMinutes===undefined?30:m.defaultBreakMinutes),linkedUserId:m.linkedUserId||'',activeForAttendance:m.activeForAttendance!==false}))}));
 db.items=(db.items||[]).map(x=>{
   x.drawings=(x.drawings||[]).map(d=>typeof d==='string'?{name:d,notes:'',data:'',uploadedDate:'',takeoffs:[],takeoffCalibration:null}:{...d,data:d.data||'',uploadedDate:d.uploadedDate||'',takeoffs:Array.isArray(d.takeoffs)?d.takeoffs:[],takeoffCalibration:d.takeoffCalibration||null});
   x.workPhotos=Array.isArray(x.workPhotos)?x.workPhotos:[];
   x.lastUpdated=x.lastUpdated||(x.completedDate||'');x.lastUpdatedAt=x.lastUpdatedAt||x.lastUpdated||'';x.lastUpdatedBy=x.lastUpdatedBy||'';x.signedOffBy=x.signedOffBy||'';x.signedOffAt=x.signedOffAt||'';x.signedOffNote=x.signedOffNote||'';x.variations=Array.isArray(x.variations)?x.variations.map(v=>({...v,payments:Array.isArray(v.payments)?v.payments.map(p=>({...p,enteredBy:p.enteredBy||'',enteredAt:p.enteredAt||''})):[],approvalStatus:v.approvalStatus||'Approved',approvedBy:v.approvedBy||'',approvedAt:v.approvedAt||''})):[];
   x.issues=(x.issues||[]).map(i=>({...i,photos:i.photos||[],assignedTo:i.assignedTo||'',targetDate:i.targetDate||'',priority:i.priority||'Normal',createdBy:i.createdBy||'',updatedAt:i.updatedAt||i.created||'',updatedBy:i.updatedBy||'',resolvedBy:i.resolvedBy||''}));
   if(x.type==='House Plot'){
     x.lifts=(x.lifts||[]).map((l,i)=>({...l,completedDate:l.completedDate||'',payments:(l.payments||((l.pct||0)?[{id:'legacy'+x.id+i,pct:l.pct,date:l.date||'',amount:Math.round((l.price||0)*(l.pct||0)/100),notes:l.paymentNotes||''}]:[])).map(p=>({...p,authorisedExtra:!!p.authorisedExtra,enteredBy:p.enteredBy||'',enteredAt:p.enteredAt||''}))}));
   }else{
     x.status=x.status||'Not started'; x.price=Number(x.price||0); x.pct=Number(x.pct||0); x.paidTo=x.paidTo||x.gang; x.paymentNotes=x.paymentNotes||'';
     x.completedDate=x.completedDate||'';
     x.payments=(x.payments||((x.pct||0)?[{id:'legacy'+x.id,pct:x.pct,date:x.date||'',amount:Math.round((x.price||0)*(x.pct||0)/100),notes:x.paymentNotes||''}]:[])).map(p=>({...p,authorisedExtra:!!p.authorisedExtra,enteredBy:p.enteredBy||'',enteredAt:p.enteredAt||''}));
   }
   return x
 });
 if(persist)save()
}
let currentUserId='';try{currentUserId=localStorage.getItem(CURRENT_USER_KEY)||'';trainingMode=localStorage.getItem('sitesight_v173_demo_training_mode')==='1'}catch(e){}
let role='',view='dashboard',site='all',gang='all',type='all',status='all',signed='all',q='',selected=null,selectedGang=null,openLift=-1,modal=null,fromDate='',toDate='',calendarPreset='all',settingsOpen=false,dirty=false,trainingMode=false,searchSuggestOpen=false,adminTab='sites',workMoreOpen=false,workFiltersExpanded=false,dashboardHelpOpen=false,drawingMoreFilters=false,drawingSite='all',drawingHouseType='all',drawingItem='all',drawingQuery='',drawingActionMenu='',auditSearch='',paymentSearch='',needsAttentionExpanded=false,calendarHistoryOpen=false,calendarNotesExpanded=false,attendanceTab='today',attendanceWeekStart='',attendanceSite='all',attendanceTeam='all',attendanceWorker='',adminSearch='',adminArchiveMode=false,adminArchiveSiteId=null,analyticsTeamFrom='',analyticsTeamTo='',guideActive=false,guideStep=0,guideFirstRun=false,guideReturnView='more',globalSearchQuery='',paymentPayeeFilter='all',paymentFrom='',paymentTo='',bulkImportRows=[],bulkImportSite='',takeoffContext=null,takeoffTool='select',takeoffDraft=[],takeoffPending=null,takeoffPointer=null,calendarMonth=iso(new Date(new Date().getFullYear(),new Date().getMonth(),1));
try{adminTab=localStorage.getItem('sitesight_v173_demo_admin_tab')||adminTab}catch(e){}
const currentUser=()=>db.users.find(u=>u.id===currentUserId)||null;
const siteName=id=>db.sites.find(x=>x.id===id)?.name||'';
const userSiteIds=(user=currentUser())=>user?.allowedSites||[];
function userCanAccessSite(user,id){return !!user&&(user.role==='Director'||user.allSitesAccess===true||(Array.isArray(user.allowedSites)&&user.allowedSites.includes(id)))}
const canAccessSite=id=>userCanAccessSite(currentUser(),id);
const activeSites=()=>db.sites.filter(s=>!s.archived&&canAccessSite(s.id));
const plotAccessRoles=new Set(['Director','Contracts Manager','Site Manager','Supervisor']);
const plotScopedPermissions=new Set(['work_items','update_progress','manage_workitems','sign_off','edit_lifts','view_issues','raise_issues','manage_issues','access_all_site_work','view_site_diary','manage_site_diary','analytics','audit']);
function roleCanAccessPlots(user=currentUser()){return !!user&&plotAccessRoles.has(user.role)}
function fieldWorkAssigned(item,user=currentUser()){
 let memberId=user?.linkedMemberId;if(!item||!memberId)return false;
 let assigned=row=>row?.assignedId===memberId||(row?.priceworkAllocations||[]).some(a=>a.memberId===memberId);
 return assigned(item)||(item.lifts||[]).some(assigned)
}
function fieldIssueAssigned(issue,user=currentUser()){
 if(!issue||!user)return false;return issue.assignedTo===`user:${user.id}`||issue.assignedTo===`person:${user.linkedMemberId}`
}
function issueRelevantToFieldUser(item,issue,user=currentUser()){
 if(!roleCanAccessPlots(user))return false;
 return true
}
function canViewItem(item,user=currentUser()){
 if(!item||!user||!userCanAccessSite(user,item.site)||db.sites.find(s=>s.id===item.site)?.archived)return false;
 if(!roleCanAccessPlots(user))return false;
 return !!(user.role==='Director'||user.permissions?.work_items)
}
function canUpdateItem(item,user=currentUser()){
 return !!item&&!!user&&roleCanAccessPlots(user)&&canViewItem(item,user)&&!!(user.role==='Director'||user.permissions?.update_progress)
}
const scopeItems=()=>db.items.filter(x=>canViewItem(x)&&(site==='all'||x.site===site));
const gangName=id=>id?(db.gangs.find(x=>x.id===id)?.name||'No team assigned'):'No team assigned';
const legacyPermAliases={issues:'view_issues',drawings:'view_drawings',payments:'view_financials',calendar:'view_calendar',reports:'download_reports',site_diary:'view_site_diary'};
function userHasPerm(user,p){let k=legacyPermAliases[p]||p;if(!user||plotScopedPermissions.has(k)&&!roleCanAccessPlots(user))return false;return user.role==='Director'||!!user.permissions?.[k]}
const hasPerm=p=>userHasPerm(currentUser(),p);
const canManager=()=>hasPerm('manage_payments');
const canDirector=()=>currentUser()?.role==='Director';
const canSupervisor=()=>hasPerm('update_progress');
const canRaiseIssues=()=>hasPerm('raise_issues');
function overall(x){if(x.type!=='House Plot')return x.status==='Complete'?(x.signed?'Signed Off':'Complete'):x.status==='Started'?'In progress':'Not started';let lifts=x.lifts||[],done=lifts.filter(l=>l.status==='Complete').length;if(lifts.length&&done===lifts.length)return x.signed?'Signed Off':'Complete';return done||lifts.some(l=>l.status==='Started')?'In progress':'Not started'}
function pct(x){
 if(x.type==='House Plot'){
  let lifts=Array.isArray(x.lifts)?x.lifts:[];if(!lifts.length)return x.signed?100:0;
  return x.signed?100:Math.round(lifts.filter(lift=>lift.status==='Complete').length/lifts.length*100)
 }
 if(x.signed||x.status==='Complete')return 100;
 return x.status==='Started'?50:0
}
function paymentPct(x){if(x.type!=='House Plot')return Math.max(0,Math.min(100,Number(x.pct||0)));let prices=(x.lifts||[]).reduce((sum,lift)=>sum+Math.max(0,Number(lift.price||0)),0),paid=(x.lifts||[]).reduce((sum,lift)=>sum+Math.max(0,Number(lift.price||0))*Math.max(0,Math.min(100,Number(lift.pct||0)))/100,0);return prices?Math.round(paid/prices*100):0}
function completionDate(x){if(x.type!=='House Plot')return x.completedDate||'';let ds=(x.lifts||[]).filter(l=>l.status==='Complete').map(l=>l.completedDate).filter(Boolean);return ds.length?ds.sort().at(-1):''}
function calendarItems(){let a=filtered().filter(x=>completionDate(x));if(!fromDate&&!toDate)return a;return a.filter(x=>{let d=completionDate(x);return (!fromDate||d>=fromDate)&&(!toDate||d<=toDate)})}
function issues(x){return x.issues.filter(i=>i.status==='Open').length}
function pay(p){return p===100?'Paid':p===0?'Unpaid':'Part Paid'}
function badge(s){let c=s==='Open'?'b-red':['Complete','Signed Off','Paid','Resolved'].includes(s)?'b-green':['In progress','Part '].includes(s)?'b-amber':s==='Started'?'b-blue':'b-grey';return `<span class="badge ${c}">${s}</span>`}
function icon(v){return v==='Open Issues'?'⚠':v==='In Progress'?'♟':v==='Not Started'?'◷':v==='Complete'?'✓':'★'}
function filtered(){return db.items.filter(x=>canViewItem(x)&&(site==='all'||x.site===site)&&(gang==='all'||x.gang===gang)&&(type==='all'||x.type===type)&&(status==='all'||overall(x)===status)&&(signed==='all'||String(x.signed)===signed)&&(!q||x.ref.toLowerCase().includes(q.toLowerCase())))}

function statusText(x){let o=overall(x);if(issues(x))return '<span class="status-text status-issue">⚠ Needs attention</span>';if(o==='Signed Off')return '<span class="status-text status-signed">★ Signed Off</span>';if(o==='Complete')return '<span class="status-text status-complete">✓ Complete</span>';if(o==='In progress')return '<span class="status-text status-in-progress">◐ In Progress</span>';return '<span class="status-text status-not-started">○ Not Started</span>'}

function setSiteContext(value){
 site=value||'all';
 let label=site==='all'?'All sites':siteName(site);
 render();
 setTimeout(()=>toast(`Now viewing: ${label}`),20)
}
function smartEmpty(title,text='',actionLabel='',actionJs='',icon='○'){
 return `<div class="smart-empty"><div class="smart-empty-icon">${icon}</div><div><b>${title}</b>${text?`<p>${text}</p>`:''}${actionLabel&&actionJs?`<button class="btn light smallbtn" onclick="${actionJs}">${actionLabel}</button>`:''}</div></div>`
}
let undoActionFn=null,undoTimer=null;
function showUndo(message,fn){
 undoActionFn=fn;
 if(undoTimer)clearTimeout(undoTimer);
 document.getElementById('undo-snackbar')?.remove();
 let e=document.createElement('div');e.id='undo-snackbar';e.className='undo-snackbar';
 e.innerHTML=`<span>✓ ${message}</span><button onclick="undoLastAction()">Undo</button>`;
 document.body.appendChild(e);
 undoTimer=setTimeout(()=>{undoActionFn=null;e.remove()},6500)
}
function undoLastAction(){
 if(!undoActionFn)return;
 let fn=undoActionFn;undoActionFn=null;
 if(undoTimer)clearTimeout(undoTimer);
 document.getElementById('undo-snackbar')?.remove();
 fn();
 savedState('Action undone')
}
function rememberRecentWorkItem(id){
 let u=currentUser(),x=db.items.find(x=>x.id===id);
 if(!u||!x||!canViewItem(x))return;
 let list=(u.recentWorkItems||[]).filter(r=>(typeof r==='string'?r:r.id)!==id);
 list.unshift({id,at:Date.now()});
 u.recentWorkItems=list.slice(0,12);
 save()
}
function recentViewedItems(){
 let u=currentUser(),list=u?.recentWorkItems||[];
 return list.map(r=>db.items.find(x=>x.id===(typeof r==='string'?r:r.id)))
   .filter(x=>x&&canViewItem(x)&&(site==='all'||x.site===site))
   .slice(0,5)
}
function recentViewedBlock(){
 if(!hasPerm('work_items'))return '';
 let rows=recentViewedItems();
 if(!rows.length)return '';
 return `<div class="panel dashboard-recent"><div class="sectionhead"><div><h3>Recently Viewed</h3><div class="muted">Jump back into the work you were looking at</div></div><button class="viewall" onclick="goNav('work-items')">All work items ›</button></div><div class="recent-work-strip">${rows.map(x=>`<button class="recent-work-card" onclick="openItem('${x.id}')"><span><b>${x.ref}</b><small>${x.type} · ${siteName(x.site)}</small></span>${statusText(x)}<strong>›</strong></button>`).join('')}</div></div>`
}
function userAttentionText(x){
 let parts=[],oi=issues(x),over=itemOverdueIssues(x).length;
 if(hasPerm('view_issues')&&over)parts.push(`${over} overdue issue${over===1?'':'s'}`);else if(hasPerm('view_issues')&&oi)parts.push(`${oi} open issue${oi===1?'':'s'}`);
 if((hasPerm('manage_workitems')||hasPerm('manage_gangs'))&&!x.gang)parts.push('Team to be confirmed');
 if(hasPerm('sign_off')&&overall(x)==='Complete'&&!x.signed)parts.push('Ready for sign-off');
 if(hasPerm('view_financials')){if(x.type==='House Plot'&&(x.lifts||[]).some(l=>l.status==='Complete'&&l.pct<100))parts.push('Completed lift not fully paid');if(x.type!=='House Plot'&&x.status==='Complete'&&x.pct<100)parts.push('Completed work not fully paid')}
 if(hasPerm('approve_variations')&&(x.variations||[]).some(v=>(v.approvalStatus||'Approved')!=='Approved'))parts.push('Variation awaiting approval');
 return parts.join(' · ')
}
function dashboardQuickCards(a){
 let prog=a.filter(x=>overall(x)==='In progress'),comp=a.filter(x=>overall(x)==='Complete'),
     not=a.filter(x=>overall(x)==='Not started'),signedRows=a.filter(x=>overall(x)==='Signed Off'),
     openCount=a.reduce((n,x)=>n+(hasPerm('view_issues')?issues(x):0),0),
     ready=a.filter(x=>overall(x)==='Complete'&&!x.signed).length,
     pendingVars=a.reduce((n,x)=>n+(x.variations||[]).filter(v=>(v.approvalStatus||'Approved')!=='Approved').length,0),
     total=a.reduce((n,x)=>n+itemTotalAuthorisedValue(x),0),
     paid=a.reduce((n,x)=>n+itemTotalPaidIncludingVariations(x),0),
     outstanding=Math.max(0,total-paid),role=currentUser()?.role||'';
 let cards=[];
 if(hasPerm('view_financials')&&(role==='Accounts'||(!hasPerm('update_progress')&&role!=='Director'))){
   cards.push([money(outstanding),'Outstanding',`goNav('payments-history')`]);
   cards.push([money(paid),'Paid',`goNav('payments-history')`]);
   if(hasPerm('approve_variations'))cards.push([pendingVars,'Variations Awaiting Approval',`goNav('work-items')`]);
   cards.push([a.length,'Work Items',`goNav('work-items')`])
 }else{
   cards.push([prog.length,'In Progress',`openDashboardGroup('inprogress')`]);
   if(hasPerm('view_issues'))cards.push([openCount,'Open Issues',`openDashboardGroup('issues')`]);
   if(hasPerm('sign_off'))cards.push([ready,'Ready for Sign-off',`openDashboardGroup('complete')`]);
   else cards.push([not.length,'Not Started',`openDashboardGroup('notstarted')`]);
   cards.push([hasPerm('sign_off')?signedRows.length:comp.length,hasPerm('sign_off')?'Signed Off':'Complete',`openDashboardGroup('${hasPerm('sign_off')?'signed':'complete'}')`])
 }
 return cards.slice(0,4).map(([v,l,js])=>`<div class="quickstat" onclick="${js}"><b>${v}</b><span>${l}</span></div>`).join('')
}
function dashboardRoleIntro(){
 let r=currentUser()?.role||'Your role',text='Your dashboard only surfaces information available to your account.';
 if(r==='Site Manager')text='Site progress, issues, teams and sign-off are prioritised for day-to-day management.';
 else if(r==='Supervisor')text='Active work and issues are prioritised so site updates stay quick.';
 else if(r==='Quantity Surveyor')text='Work progress and commercial information are combined for your role.';
 else if(r==='Accounts')text='Commercial information is prioritised while operational editing stays out of the way.';
 else if(r==='Operator'&&v1998OperatorHasWork?.(currentUser()))text='Your assigned plots, issues and drawings are kept front and centre.';
 return `<div class="dashboard-role-note"><span>${r}</span><p>${text}</p></div>`
}
function dashboardStatusGuide(){
 return `<div class="colour-guide"><b>Status guide:</b><span class="guideitem"><span class="status-text status-not-started">○ Not Started</span></span><span class="guideitem"><span class="status-text status-in-progress">◐ In Progress</span></span>${hasPerm('view_issues')?'<span class="guideitem"><span class="status-text status-issue">⚠ Issue</span></span>':''}<span class="guideitem"><span class="status-text status-complete">✓ Complete</span></span><span class="guideitem"><span class="status-text status-signed">★ Signed Off</span></span></div>`
}
function dashboardLearningHelp(){
 let newUser=Number(currentUser()?.loginCount||0)<=3,show=trainingMode||newUser||dashboardHelpOpen;
 if(!show)return `<button class="dashboard-help-toggle" onclick="dashboardHelpOpen=true;render()">ⓘ Show dashboard guide</button>`;
 return `<div class="dashboard-learning"><div class="dashboard-learning-head"><b>${newUser?'Getting started':'Dashboard guide'}</b><button onclick="dashboardHelpOpen=false;render()">Hide guide ×</button></div>${dashboardRoleIntro()}${dashboardStatusGuide()}</div>`
}
function selectedSiteHeading(){let label=site==='all'?'All Sites':siteName(site);return `<div class="site-context-title">Viewing: <b>${label}</b>${site!=='all'?` <button onclick="setSiteContext('all')">× Show all</button>`:''}</div>`}


function canQuickUpdate(){return hasPerm('update_progress')||hasPerm('raise_issues')||hasPerm('manage_workitems')}
function quickUpdate(id){
 let x=db.items.find(x=>x.id===id);if(!x)return;
 rememberRecentWorkItem(id);
 let actions=[];
 if(hasPerm('update_progress')||hasPerm('manage_workitems'))actions.push(`<button class="quick-update-action progress-action" onclick="quickProgress('${id}')"><i>◐</i><span><b>Update Progress</b><small>Change the current stage or status</small></span><strong>›</strong></button>`);
 if(hasPerm('update_progress')||hasPerm('manage_workitems'))actions.push(`<button class="quick-update-action photo-action" onclick="quickReturnAfterSave=true;selected='${id}';modal=null;render();setTimeout(()=>addWorkPhotos(),0)"><i>📷</i><span><b>Take Photo</b><small>Add a visual progress update</small></span><strong>›</strong></button>`);
 if(hasPerm('raise_issues'))actions.push(`<button class="quick-update-action issue-action" onclick="quickReturnAfterSave=true;selected='${id}';modal=null;render();setTimeout(()=>addIssue(),0)"><i>⚠</i><span><b>Raise Issue</b><small>Record a problem and add photos</small></span><strong>›</strong></button>`);
 modal=`<div class="modal"><div class="modalbox quick-update-modal"><div class="quick-update-title"><div><span class="quick-kicker">QUICK UPDATE</span><h2>${x.ref}</h2><p>${x.type} · ${siteName(x.site)} · ${gangName(x.gang)}</p></div>${statusText(x)}</div><div class="quick-update-actions">${actions.join('')}</div><button class="btn light quick-open-full" onclick="modal=null;openItem('${id}')">Open full Work Item</button> <button class="btn light" onclick="modal=null;render()">Close</button></div></div>`;render()
}
function quickProgress(id){
 if(!hasPerm('update_progress')&&!hasPerm('manage_workitems'))return toast('You do not have permission to update progress');
 let x=db.items.find(x=>x.id===id);if(!x)return;
 if(x.type==='House Plot'){
   let lifts=x.lifts||[];
   if(!lifts.length)return toast('No stages are available on this Work Item');
   let suggest=lifts.findIndex(l=>l.status!=='Complete');if(suggest<0)suggest=lifts.length-1;
   modal=`<div class="modal"><div class="modalbox quick-progress-modal"><span class="quick-kicker">QUICK PROGRESS</span><h2>${x.ref}</h2><p class="muted">Choose the stage and its new status.</p><div class="field"><label>Stage / Lift</label><select id="qp-lift" class="select">${lifts.map((l,i)=>`<option value="${i}" ${i===suggest?'selected':''}>${l.name} — ${l.status}</option>`).join('')}</select></div><div class="field"><label>New status</label><select id="qp-status" class="select"><option>Not started</option><option selected>Started</option><option>Complete</option></select></div><div class="quick-progress-buttons"><button class="btn orange" onclick="saveQuickProgress('${id}')">✓ Save progress</button><button class="btn light" onclick="quickUpdate('${id}')">Back</button></div></div></div>`
 }else{
   modal=`<div class="modal"><div class="modalbox quick-progress-modal"><span class="quick-kicker">QUICK PROGRESS</span><h2>${x.ref}</h2><p class="muted">Update the current work status.</p><div class="field"><label>New status</label><select id="qp-status" class="select">${['Not started','Started','Complete'].map(s=>`<option ${x.status===s?'selected':''}>${s}</option>`).join('')}</select></div><div class="quick-progress-buttons"><button class="btn orange" onclick="saveQuickProgress('${id}')">✓ Save progress</button><button class="btn light" onclick="quickUpdate('${id}')">Back</button></div></div></div>`
 }
 render()
}
function saveQuickProgress(id){
 if(!hasPerm('update_progress')&&!hasPerm('manage_workitems'))return toast('You do not have permission to update progress');
 let x=db.items.find(x=>x.id===id);if(!x)return;
 let status=document.getElementById('qp-status')?.value;if(!status)return;
 let desc=status;
 if(x.type==='House Plot'){
   let idx=Number(document.getElementById('qp-lift')?.value||0),l=x.lifts?.[idx];if(!l)return;
   l.status=status;if(status==='Complete'&&!l.completedDate)l.completedDate=iso();desc=`${l.name} — ${status}`
 }else{
   x.status=status;if(status==='Complete'&&!x.completedDate)x.completedDate=iso()
 }
 touch(x);save();audit('progress','work item',x.id,`${x.ref}: quick progress updated — ${desc}`,x.site);emitNotification('work',x.site,`${x.ref}: progress updated — ${desc}`,x.id);modal=null;savedState('Progress saved');render()
}

function card(x){let o=overall(x),p=pct(x),cl=o==='Signed Off'?'signed':o==='Complete'?'green':o==='In progress'?(issues(x)?'red':'amber'):o==='Not started'?'blue':'red',progressLabel=x.type==='House Plot'?'Plot complete':'Work complete';return `<article class="workcard ${cl}" role="link" tabindex="0" aria-label="Open ${calendarSafe(x.ref)}" onclick="openItem('${x.id}')" onkeydown="if(event.key==='Enter'){openItem('${x.id}')}" >${x.signed?'<span class="signed-ribbon">SIGNED OFF</span>':''}<div class="sectionhead"><div><h3>${calendarSafe(x.ref)}</h3><div class="type">${calendarSafe(x.type)}${x.type==='House Plot'&&x.houseType?' · '+calendarSafe(x.houseType):''}</div></div>${statusText(x)}</div><div class="meta"><span>⌖ ${calendarSafe(siteName(x.site))}</span><span>♟ ${calendarSafe(gangName(x.gang))}</span>${issues(x)?`<span>⚠ ${issues(x)} issue${issues(x)>1?'s':''}</span>`:''}</div><div class="progress-label">${progressLabel}</div><div class="progressrow"><div class="bar" role="progressbar" aria-label="${progressLabel}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${p}"><i style="width:${p}%"></i></div><span class="pct">${p}%</span></div><div class="last-updated">${calendarSafe(lastUpdatedText(x))}</div>${canQuickUpdate()?`<button class="workcard-quick-update" onclick="event.stopPropagation();quickUpdate('${x.id}')">⚡ Quick update</button>`:''}</article>`}
function section(title,arr,cls){let filter=title==='Open Issues'?'issues':title==='In Progress'?'inprogress':title==='Not Started'?'notstarted':title==='Complete'?'complete':title==='Signed Off'?'signed':'',emptyText=title==='Open Issues'?'No open issues in this view.':title==='In Progress'?'Nothing is currently marked In Progress.':title==='Not Started'?'No work is waiting to start.':title==='Complete'?'No completed work is waiting here.':'No signed-off work in this view.';return `<div class="section"><div class="sectionhead ${cls}"><h2>${icon(title)} ${title} <span class="pill">${arr.length}</span></h2><button class="viewall" onclick="openDashboardGroup('${filter}')">View all ›</button></div>${arr.length?`<div class="cards">${arr.slice(0,10).map(card).join('')}</div>`:smartEmpty(emptyText,title==='Open Issues'?'New issues will appear here automatically.':'This section updates as work progresses.','','','✓')}</div>`}
function openDashboardGroup(group){selected=null;selectedGang=null;q='';gang='all';type='all';signed='all';if(group==='issues'){view='issues';status='all';render();return}view='work-items';if(group==='inprogress')status='In progress';else if(group==='notstarted')status='Not started';else if(group==='complete')status='Complete';else if(group==='signed')status='Signed Off';else status='all';render()}function workTypes(){return [...new Set(['House Plot','Garden Wall',...db.items.map(x=>x.type).filter(Boolean)])]}
function filters(showSite=true){
 let advancedCount=(gang!=='all'?1:0)+(type!=='all'?1:0)+(status!=='all'?1:0)+(signed!=='all'?1:0),
     expanded=workFiltersExpanded||advancedCount>0;
 return `<div class="filterbar simple-work-filterbar"><div class="searchwrap"><input class="input search" placeholder="Search plot / work item…" value="${q}" oninput="q=this.value;searchSuggestOpen=!!q;searchReference(this)">${searchSuggestOpen&&q?`<div class="search-suggestions">${searchSuggestions(q).map(s=>`<div class="search-suggestion" onclick="selectSearchSuggestion('${s.id}')"><b>${s.ref} · ${s.type}</b><div class="muted">${siteName(s.site)} · ${gangName(s.gang)}</div></div>`).join('')||'<div class="search-suggestion muted">No matches</div>'}</div>`:''}</div>${showSite?`<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select>`:''}<button class="btn light more-filter-button" onclick="workFiltersExpanded=!workFiltersExpanded;render()">More filters${advancedCount?` (${advancedCount})`:''} ${expanded?'▲':'▼'}</button><div class="work-filter-extra ${expanded?'show':''}"><select class="select" onchange="gang=this.value;render()"><option value="all">All teams</option>${db.gangs.map(g=>`<option value="${g.id}" ${gang===g.id?'selected':''}>${g.name}</option>`).join('')}</select><select class="select" onchange="type=this.value;render()"><option value="all">All types</option>${workTypes().map(t=>`<option ${type===t?'selected':''}>${t}</option>`).join('')}</select><select class="select" onchange="status=this.value;render()"><option value="all">All status</option>${['Not started','In progress','Complete','Signed Off'].map(s=>`<option ${status===s?'selected':''}>${s}</option>`).join('')}</select><button class="btn light" onclick="resetWorkFilters()">Clear filters</button></div></div>`
}
function audit(action,entityType,entityId,summary,siteId='',details=''){
 db.auditLog=db.auditLog||[];
 let u=currentUser();
 db.auditLog.unshift({id:'au'+Date.now()+Math.random().toString(16).slice(2),createdAt:new Date().toISOString(),userId:u?.id||'',userName:u?.name||'System',action,entityType,entityId,summary,siteId,details});
 save()
}
function auditPage(){
 if(!hasPerm('audit'))return `<div class="panel empty">You do not have permission to view the Audit Trail.</div>`;
 let allRows=(db.auditLog||[]).filter(a=>!a.siteId||canAccessSite(a.siteId)&&(site==='all'||a.siteId===site)),
     qAudit=(auditSearch||'').trim().toLowerCase(),
     rows=qAudit?allRows.filter(a=>[
       a.userName,
       a.summary,
       a.entityType,
       a.action,
       a.entityId,
       a.siteId?siteName(a.siteId):'',
       a.createdAt?new Date(a.createdAt).toLocaleString():''
     ].join(' ').toLowerCase().includes(qAudit)):allRows;
 return `<div class="topbar"><div><h1>Audit Trail</h1><div class="sub">Who changed what, when, and where</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select></div></div><div class="panel audit-search-panel"><div class="audit-search-wrap"><div class="searchbox audit-searchbox"><span>⌕</span><input value="${auditSearch}" placeholder="Search user, plot, action, payment, issue, note…" oninput="auditSearch=this.value;render()"></div>${auditSearch?`<button class="btn light" onclick="auditSearch='';render()">Clear</button>`:''}<div class="audit-result-count"><b>${rows.length}</b><span>${auditSearch?'matching':'total'} record${rows.length===1?'':'s'}</span></div></div></div><div class="panel" style="padding:15px"><div class="audit-list">${rows.length?rows.map(a=>`<div class="audit-row"><span>${a.createdAt?new Date(a.createdAt).toLocaleString():'—'}</span><span>${a.userName||'Unknown user'}</span><b>${a.summary||a.action||'Audit event'}</b><span>${a.siteId?siteName(a.siteId):(a.entityType||'')}</span></div>`).join(''):`<div class="empty-help"><b>${auditSearch?'No audit records match your search.':'No audit events yet.'}</b><div style="margin-top:6px">${auditSearch?'Try a plot number, user name, action or different wording.':'Important changes will appear here automatically.'}</div></div>`}</div></div>`
}
function savedState(msg='Saved'){let old=document.getElementById('saved-toast');if(old)old.remove();let el=document.createElement('div');el.id='saved-toast';el.className='saved-toast';el.textContent='✓ '+msg;document.body.appendChild(el);setTimeout(()=>el.remove(),2300)}
function markDirty(){dirty=true}
function clearDirty(){dirty=false}
function confirmLeave(){if(!dirty)return true;return confirm('You have unsaved changes. Leave anyway?')}
function backAction(){if(!confirmLeave())return;dirty=false;if(selected){selected=null;render();return}if(selectedGang){selectedGang=null;render();return}history.back()}
function lastUpdatedText(x){let raw=x?.lastUpdatedAt||x?.lastUpdated||x?.completedDate||'';if(!raw)return 'No updates recorded yet';let label='';try{let d=new Date(raw.includes('T')?raw:raw+'T12:00:00'),today=new Date(),same=d.toDateString()===today.toDateString();label=same?'Updated today at '+d.toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit'}):'Updated '+d.toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'})+' at '+d.toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit'})}catch(e){label='Updated '+raw}return label+(x.lastUpdatedBy?' by '+x.lastUpdatedBy:'')}
function touch(x){if(x){x.lastUpdated=iso();x.lastUpdatedAt=new Date().toISOString();x.lastUpdatedBy=currentUser()?.name||''}}
function trainingTip(t){return trainingMode?`<div class="training-tip">💡 ${t}</div>`:''}
function resetWorkFilters(){gang='all';type='all';status='all';signed='all';q='';searchSuggestOpen=false;workFiltersExpanded=false;render()}
function showEverything(){site='all';gang='all';type='all';status='all';signed='all';q='';render()}
function filterSummary(){let bits=[];if(site!=='all')bits.push(`Site: ${siteName(site)}`);if(gang!=='all')bits.push(`Team: ${gangName(gang)}`);if(type!=='all')bits.push(`Type: ${type}`);if(status!=='all')bits.push(`Status: ${status}`);if(signed!=='all')bits.push(`Signed: ${signed}`);if(q)bits.push(`Search: ${q}`);return bits.length?`<div class="filter-summary">${bits.map(b=>`<span class="filter-chip">${b}</span>`).join('')}<button class="show-everything" onclick="showEverything()">Show Everything</button></div>`:''}
function toggleTrainingMode(v){trainingMode=!!v;let u=currentUser();if(u){u.trainingMode=trainingMode;save()}render()}
function searchSuggestions(term){let t=(term||'').trim().toLowerCase();if(!t)return [];return db.items.filter(x=>canAccessSite(x.site)&&[x.ref,x.type,x.houseType,siteName(x.site),gangName(x.gang)].join(' ').toLowerCase().includes(t)).slice(0,8)}
function selectSearchSuggestion(id){q='';searchSuggestOpen=false;openItem(id)}

function searchReference(el){q=el.value;let pos=el.selectionStart||q.length;render();setTimeout(()=>{let n=document.querySelector('.search');if(n){n.focus();try{n.setSelectionRange(pos,pos)}catch(e){}}},0)}

function itemProgress(x){
 if(x.type==='House Plot'){
   let lifts=x.lifts||[];
   if(!lifts.length)return x.signed?100:0;
   let stage=lifts.filter(l=>l.status==='Complete').length/lifts.length*100;
   if(x.signed)stage=100;
   return stage;
 }
 if(x.signed)return 100;
 return x.status==='Complete'?100:x.status==='Started'?50:0
}
function siteProgress(siteId){
 let items=db.items.filter(x=>x.site===siteId);
 if(!items.length)return 0;
 return Math.round(items.reduce((sum,x)=>sum+itemProgress(x),0)/items.length)
}
function dashboardSiteProgress(){
 if(site==='all')return '';
 let s=db.sites.find(s=>s.id===site);
 if(!s)return '';
 let p=siteProgress(site);
 return `<div class="siteprogress"><span>Site progress</span><b>${s.name}</b><div class="bar"><i style="width:${p}%;background:${p===100?'var(--green)':'var(--orange)'}"></i></div><span class="pctlabel">${p}%</span></div>`
}

function canSeeDailyBrief(){return canDirector()||(hasPerm('analytics')&&hasPerm('manage_workitems')&&hasPerm('manage_gangs'))}
function dateIsToday(v){
 if(!v)return false;
 try{let d=new Date(String(v).includes('T')?v:String(v)+'T12:00:00');return d.toDateString()===new Date().toDateString()}catch(e){return String(v).slice(0,10)===iso()}
}
function directorDailyBriefData(){
 let items=scopeItems(),updated=items.filter(x=>dateIsToday(x.lastUpdatedAt||x.lastUpdated)),completed=[],newIssues=[],resolved=[],overdue=[],photos=[],pending=[];
 items.forEach(x=>{
   if(x.type==='House Plot')(x.lifts||[]).forEach(l=>{if(l.status==='Complete'&&dateIsToday(l.completedDate))completed.push({x,label:l.name})});
   else if(x.status==='Complete'&&dateIsToday(x.completedDate))completed.push({x,label:x.type});
   if(hasPerm('view_issues'))(x.issues||[]).forEach(i=>{
     if(dateIsToday(i.created))newIssues.push({x,i});
     if(dateIsToday(i.resolved))resolved.push({x,i});if(i.status==='Open'&&issueDueState(i)==='overdue')overdue.push({x,i});
     (i.photos||[]).forEach(p=>{if(dateIsToday(p.created))photos.push({x,label:'Issue photo'})})
   });
   (x.workPhotos||[]).forEach(p=>{if(dateIsToday(p.created))photos.push({x,label:p.note||'Work photo'})});
   if(hasPerm('approve_variations'))(x.variations||[]).forEach(v=>{if((v.approvalStatus||'Approved')!=='Approved')pending.push({x,v})})
 });
 if(hasPerm('view_site_diary'))(db.siteDiary||[]).filter(e=>canAccessSite(e.siteId||e.site)&&(site==='all'||(e.siteId||e.site)===site)&&dateIsToday(e.date)).forEach(e=>(e.photos||[]).forEach(()=>photos.push({x:null,label:'Site Diary photo'})));
 return {items,updated,completed,newIssues,resolved,overdue,photos,pending}
}
function directorDailyBriefCard(){
 if(!canSeeDailyBrief())return '';
 let d=directorDailyBriefData(),label=canDirector()?'Director Daily Brief':'Daily Brief';
 return `<div class="panel director-brief"><div class="director-brief-head"><div><span class="brief-kicker">TODAY AT A GLANCE</span><h3>${label}</h3><p>${site==='all'?'Across your accessible active sites':siteName(site)}</p></div><button class="btn light smallbtn" onclick="openDirectorDailyBrief()">View detail ›</button></div><div class="director-brief-metrics"><button onclick="openDirectorDailyBrief()"><b>${d.updated.length}</b><span>Work items updated</span></button><button onclick="openDirectorDailyBrief()"><b>${d.completed.length}</b><span>Stages completed</span></button>${hasPerm('view_issues')?`<button onclick="openDirectorDailyBrief()"><b>${d.newIssues.length}</b><span>New issues</span></button><button onclick="openDirectorDailyBrief()"><b>${d.resolved.length}</b><span>Issues resolved</span></button><button class="${d.overdue.length?'brief-alert':''}" onclick="openDirectorDailyBrief()"><b>${d.overdue.length}</b><span>Overdue issues</span></button>`:''}<button onclick="openDirectorDailyBrief()"><b>${d.photos.length}</b><span>New photos</span></button>${hasPerm('approve_variations')?`<button onclick="openDirectorDailyBrief()"><b>${d.pending.length}</b><span>Awaiting approval</span></button>`:''}</div></div>`
}
function dailyBriefList(title,rows,renderRow){
 return `<div class="daily-brief-section"><div class="sectionhead"><h3>${title}</h3><span class="pill">${rows.length}</span></div>${rows.length?`<div class="daily-brief-list">${rows.slice(0,12).map(renderRow).join('')}</div>`:smartEmpty(`No ${title.toLowerCase()} today.`,'','','','✓')}</div>`
}
function openDirectorDailyBrief(){
 let d=directorDailyBriefData();
 modal=`<div class="modal daily-brief-modal"><div class="modalbox daily-brief-box"><div class="sectionhead"><div><span class="brief-kicker">TODAY AT A GLANCE</span><h2>${canDirector()?'Director Daily Brief':'Daily Brief'}</h2><div class="muted">${new Date().toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'})} · ${site==='all'?'All accessible sites':siteName(site)}</div></div><button class="btn light" onclick="modal=null;render()">✕ Close</button></div><div class="daily-brief-summary"><span><b>${d.updated.length}</b> updated work items</span><span><b>${d.completed.length}</b> completed stages</span>${hasPerm('view_issues')?`<span><b>${d.newIssues.length}</b> new issues</span><span><b>${d.resolved.length}</b> resolved</span><span class="${d.overdue.length?'brief-summary-alert':''}"><b>${d.overdue.length}</b> overdue issues</span>`:''}<span><b>${d.photos.length}</b> photos</span>${hasPerm('approve_variations')?`<span><b>${d.pending.length}</b> awaiting approval</span>`:''}</div>${dailyBriefList('Updated Work',d.updated,x=>`<button onclick="modal=null;openItem('${x.id}')"><span><b>${x.ref}</b><small>${x.type} · ${siteName(x.site)}</small></span><strong>Open ›</strong></button>`)}${dailyBriefList('Completed Stages',d.completed,r=>`<button onclick="modal=null;openItem('${r.x.id}')"><span><b>${r.x.ref} · ${r.label}</b><small>${siteName(r.x.site)}</small></span><strong>Complete ›</strong></button>`)}${hasPerm('view_issues')?dailyBriefList('New Issues',d.newIssues,r=>`<button onclick="modal=null;viewIssue('${r.x.id}','${r.i.id}')"><span><b>${r.i.title}</b><small>${r.x.ref} · ${siteName(r.x.site)}</small></span><strong>Open ›</strong></button>`):''}${hasPerm('view_issues')?dailyBriefList('Overdue Issues',d.overdue,r=>`<button onclick="modal=null;viewIssue('${r.x.id}','${r.i.id}')"><span><b>${r.x.ref} · ${r.i.title}</b><small>${issueAssigneeLabel(r.i.assignedTo)} · target ${r.i.targetDate}</small></span><strong>Review ›</strong></button>`):''}${hasPerm('approve_variations')?dailyBriefList('Variations Awaiting Approval',d.pending,r=>`<button onclick="modal=null;openItem('${r.x.id}')"><span><b>${r.v.name||'Variation'}</b><small>${r.x.ref} · ${siteName(r.x.site)}</small></span><strong>Review ›</strong></button>`):''}</div></div>`;render()
}

function dashboard(){
 let a=filtered(),open=hasPerm('view_issues')?a.filter(x=>issues(x)):[],prog=a.filter(x=>overall(x)==='In progress'),not=a.filter(x=>overall(x)==='Not started'),comp=a.filter(x=>overall(x)==='Complete'),sig=a.filter(x=>overall(x)==='Signed Off');
 return `<div class="topbar"><div><h1>Dashboard</h1><div class="sub">ENTERPRISE DEMO · four active developments and 351 plots under full operational use</div>${selectedSiteHeading()}</div><div class="actions"><select class="select" onchange="setSiteContext(this.value)"><option value="all">⌖ All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select>${hasPerm('manage_workitems')?'<button class="btn orange primary-action" onclick="newItem()">＋ Add work item</button>':''}</div></div><div class="dashboard-hero"><h2>${site==='all'?'All active sites':siteName(site)}</h2><div class="todayline">${new Date().toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'})}</div><div class="dashboard-quick">${dashboardQuickCards(a)}</div></div>${dashboardLearningHelp()}<div class="dashboard-priority-stack">${directorDailyBriefCard()}${dashboardNeedsAttentionBlock()}${dashboardTodaySummaryBlock()}${recentViewedBlock()}</div><div class="workspace"><div class="panel dashboard-work-status"><div class="dashboard-work-status-head"><div><h2>Work Status</h2><span>Search or filter only when you need to</span></div></div>${filters(false)}${section('In Progress',prog,'progress')}${hasPerm('view_issues')?section('Open Issues',open,'open'):''}${section('Not Started',not,'')}${section('Complete',comp,'complete')}${section('Signed Off',sig,'signed')}</div>${side()}</div>`
}
function itemActivityDates(x){
 let d=[];if(x.lastUpdated)d.push(x.lastUpdated);if(x.completedDate)d.push(x.completedDate);
 if(x.type==='House Plot')(x.lifts||[]).forEach(l=>{if(l.completedDate)d.push(l.completedDate);(l.payments||[]).forEach(p=>p.date&&d.push(p.date))});
 else (x.payments||[]).forEach(p=>p.date&&d.push(p.date));
 (x.issues||[]).forEach(i=>{if(i.created)d.push(i.created);if(i.resolved)d.push(i.resolved)});
 (x.drawings||[]).forEach(dr=>dr.uploadedDate&&d.push(dr.uploadedDate));
 return [...new Set(d.filter(Boolean))]
}
function itemsUpdatedBetween(a,b){return scopeItems().filter(x=>itemActivityDates(x).some(d=>(!a||d>=a)&&(!b||d<=b)))}
function todaysItems(){let d=iso();return itemsUpdatedBetween(d,d)}
function weeklyItems(){let end=new Date(),start=new Date();start.setDate(end.getDate()-6);return itemsUpdatedBetween(iso(start),iso(end))}
function todayItemsList(){let a=todaysItems();return `<div class="todayitems"><h4>Today's items</h4>${a.length?a.map(x=>`<div class="todayitem" onclick="openItem('${x.id}')"><div class="todayitemmain"><b>${x.ref}</b><span>${x.type}${x.houseType?' · '+x.houseType:''} · ${siteName(x.site)}</span></div><span class="todaystatus">${overall(x)}</span></div>`).join(''):'<div class="muted" style="font-size:10px;padding:5px 0">No items updated today.</div>'}</div>`}
let activityRange='today';
function openActivity(range){activityRange=range;selected=null;selectedGang=null;view='activity';render()}
function activityPage(){let weekly=activityRange==='week',items=weekly?weeklyItems():todaysItems(),title=weekly?'Weekly Summary':'Today’s Summary';return `<div class="topbar"><div><h1>${title}</h1><div class="sub">${weekly?'Items updated during the last 7 days':'Items updated today'}</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select></div></div><div class="panel"><div class="activityrow" style="background:#f7f9fc;font-weight:900"><span>Reference</span><span>Type</span><span class="activity-hide">Site / Team</span><span>Status</span><span></span></div>${items.length?items.map(x=>`<div class="activityrow" onclick="openItem('${x.id}')"><b>${x.ref}</b><span>${x.type}${x.houseType?' · '+x.houseType:''}</span><span class="activity-hide">${siteName(x.site)} · ${gangName(x.gang)}</span><span>${badge(overall(x))}</span><b>›</b></div>`).join(''):'<div class="empty">No items updated in this period.</div>'}</div>`}

function needsAttentionItems(){return scopeItems().filter(x=>userAttentionText(x))}
function toggleNeedsAttention(){needsAttentionExpanded=!needsAttentionExpanded;render()}
function openNeedsAttention(){if(!confirmLeave())return;dirty=false;selected=null;selectedGang=null;needsAttentionExpanded=false;view='needs-attention';render()}
function needsAttentionPage(){let items=needsAttentionItems();return `<div class="topbar"><div><h1>Needs Attention</h1><div class="sub">Work items that currently need action</div>${selectedSiteHeading()}</div><div class="actions"><button class="btn light" onclick="goNav('dashboard')">← Dashboard</button><select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select></div></div><div class="panel needs-attention-page"><div class="sectionhead"><div><h2>Needs Attention</h2><div class="muted">${items.length} work item${items.length===1?'':'s'} require attention</div></div></div><div class="needs-attention-grid">${items.length?items.map(x=>`<div class="needs-attention-full-card" onclick="openItem('${x.id}')"><div class="needs-attention-card-main"><div><b>${x.ref}</b><span>${x.type} · ${siteName(x.site)}</span></div><span class="status-text status-issue">⚠ Needs attention</span></div><div class="needs-attention-reason">${userAttentionText(x)}</div><div class="needs-attention-card-meta"><span>Team: ${gangName(x.gang)}</span><span>${lastUpdatedText(x)}</span><b>Open ›</b></div></div>`).join(''):'<div class="empty-help"><b>Nothing currently needs attention.</b><div style="margin-top:6px">Items will appear here automatically when they need action.</div></div>'}</div></div>`}


function todaySummaryGroups(){let items=todaysItems(),groups=[['Open Issues',[]],['In Progress',[]],['Not Started',[]],['Complete',[]],['Signed Off',[]]],map=Object.fromEntries(groups);items.forEach(x=>{let key=hasPerm('view_issues')&&issues(x)>0?'Open Issues':overall(x)==='In progress'?'In Progress':overall(x)==='Not started'?'Not Started':overall(x);if(map[key])map[key].push(x)});return groups.filter(([name])=>name!=='Open Issues'||hasPerm('view_issues')).map(([name,list])=>({name,list,count:list.length}))}
function todayDonutData(){let groups=todaySummaryGroups(),total=groups.reduce((n,g)=>n+g.count,0),circ=251.327,offset=0,colors={'Open Issues':'var(--red)','In Progress':'var(--amber)','Not Started':'#8b98aa','Complete':'var(--green)','Signed Off':'#f2b900'};return {total,groups:groups.map(g=>{let len=total?g.count/total*circ:0,row={...g,len,offset,color:colors[g.name]};offset+=len;return row})}}
function todayDonutHover(name){let box=document.getElementById('today-donut-detail');if(!box)return;let g=todaySummaryGroups().find(g=>g.name===name);if(!g)return;box.innerHTML=`<div class="today-donut-hover-head"><span class="today-donut-hover-dot ${name.replace(/\s+/g,'-').toLowerCase()}"></span><b>${name}</b><strong>${g.count}</strong></div>${g.list.length?`<div class="today-donut-hover-list">${g.list.slice(0,5).map(x=>`<span><b>${x.ref}</b><em>${x.type}${issues(x)?` · ${issues(x)} open issue${issues(x)===1?'':'s'}`:''}</em></span>`).join('')}${g.list.length>5?`<small>+ ${g.list.length-5} more</small>`:''}</div>`:'<div class="today-donut-hover-empty">No ${name.toLowerCase()} items today.</div>'}`}
function todayDonutClear(){let box=document.getElementById('today-donut-detail');if(!box)return;let total=todaysItems().length;box.innerHTML=`<div class="today-donut-default"><b>Hover over the chart</b><span>See today’s work behind each colour · ${total} item${total===1?'':'s'} today</span></div>`}
function todayDonut(){let d=todayDonutData();return `<div class="today-donut-wrap"><div class="today-donut-chart"><svg class="today-donut-svg" viewBox="0 0 100 100" role="img" aria-label="Today’s work status summary">${d.total?d.groups.map(g=>g.count?`<circle class="today-donut-segment" cx="50" cy="50" r="40" fill="none" stroke="${g.color}" stroke-width="16" stroke-dasharray="${g.len} ${251.327-g.len}" stroke-dashoffset="${-g.offset}" transform="rotate(-90 50 50)" onmouseenter="todayDonutHover('${g.name}')" onmouseleave="todayDonutClear()"><title>${g.name}: ${g.count}</title></circle>`:'').join(''):`<circle cx="50" cy="50" r="40" fill="none" stroke="#e6ebef" stroke-width="16"></circle>`}<circle cx="50" cy="50" r="29" fill="#fff"></circle></svg><div class="today-donut-centre"><b>${d.total}</b><span>today</span></div></div><div id="today-donut-detail" class="today-donut-detail"><div class="today-donut-default"><b>Hover over the chart</b><span>See today’s work behind each colour · ${d.total} item${d.total===1?'':'s'} today</span></div></div></div>`}


function dashboardNeedsAttentionBlock(){
 let attention=needsAttentionItems(),shown=needsAttentionExpanded?attention:attention.slice(0,6),hiddenCount=Math.max(0,attention.length-6);
 return `<div class="panel dashboard-attention"><div class="sectionhead"><div><h3>Needs Attention</h3><div class="muted">The items most likely to need action now</div></div><button class="viewall" onclick="openNeedsAttention()">View all</button></div>${shown.length?`<div class="dashboard-attention-grid">${shown.map(x=>`<button class="dashboard-attention-row" onclick="openItem('${x.id}')"><span><b>${x.ref} · ${x.type}</b><small>${userAttentionText(x)}</small></span><strong>›</strong></button>`).join('')}</div>`:smartEmpty('Nothing currently needs attention.','Open issues, unassigned Teams, sign-off and commercial exceptions will appear here.','','','✓')}${hiddenCount>0?`<button class="needs-expand-btn" onclick="toggleNeedsAttention()">${needsAttentionExpanded?'Show less ▲':`Show ${hiddenCount} more ▼`}</button>`:''}</div>`
}
function dashboardTodaySummaryBlock(){
 return `<div class="panel dashboard-today"><div class="sectionhead"><div><h3>Today's Summary</h3><div class="muted">What has moved today</div></div><button class="viewall" onclick="openActivity('today')">View today ›</button></div><div class="dashboard-today-grid"><div>${todayDonut()}</div><div>${todayItemsList()}<button class="weeksummary dashboard-weeksummary" onclick="openActivity('week')"><span><b>${weeklyItems().length}</b> items updated in the last 7 days</span><b>Weekly Summary ›</b></button></div></div></div>`
}

function side(){
 let scoped=scopeItems(),recent=hasPerm('view_issues')?scoped.flatMap(x=>x.issues.map(i=>({...i,ref:x.ref,site:x.site}))).filter(i=>i.status==='Open').slice(0,4):[];
 return `<div class="sidecol">${hasPerm('view_issues')?`<div class="panel sidepanel"><div class="sectionhead"><h3>Recent Issues</h3><button class="viewall" onclick="view='issues';selected=null;selectedGang=null;render()">View all</button></div><div class="list">${recent.map(i=>`<div class="listrow clickable-listrow" onclick="openItem('${db.items.find(x=>(x.issues||[]).some(v=>v.id===i.id))?.id||''}')"><span><b>${i.title}</b><br><span class="muted">${i.ref} · ${siteName(i.site)}</span></span><span class="badge b-red">1</span></div>`).join('')||'<div class="empty-help"><b>No open issues.</b><div style="margin-top:6px">New issues will appear here automatically.</div></div>'}</div></div>`:''}<div class="panel sidepanel"><div class="sectionhead"><h3>Active Teams</h3><button class="viewall" onclick="view='gangs';selected=null;selectedGang=null;render()">View all</button></div><div class="list">${db.gangs.filter(g=>g.active).map(g=>{let n=scoped.filter(x=>x.gang===g.id).length;return n||site==='all'?`<div class="listrow" style="cursor:pointer" onclick="openGang('${g.id}')"><span><b>${g.name}</b></span><span class="badge b-blue">${n}</span></div>`:''}).join('')}</div></div></div>`
}
function workItems(){let rows=filtered(),hasFilters=site!=='all'||gang!=='all'||type!=='all'||status!=='all'||signed!=='all'||!!q;return `<div class="topbar"><div><h1>Work Items</h1><div class="sub">Search and manage all work items</div>${selectedSiteHeading()}${trainingTip("Start with the Site filter, then search only if you need to.")}</div>${hasPerm('manage_workitems')?'<button class="btn orange primary-action" onclick="newItem()">＋ Add work item</button>':''}</div><div class="panel">${filters()}<div style="padding:14px">${filterSummary()}<div class="cards">${rows.map(card).join('')||(hasFilters?smartEmpty('No work items match these filters.','Clear the filters or try a different search.','Clear filters','resetWorkFilters()','⌕'):smartEmpty('No work items yet.','Create the first Work Item for this site to get started.',hasPerm('manage_workitems')?'＋ Create Work Item':'',hasPerm('manage_workitems')?'newItem()':'','▣'))}</div></div></div>`}
function calendarSafe(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[c]))}
function calendarNotesOnDate(date){return calendarNotesForView().filter(n=>n.date===date)}
function calendarNoteCellPreview(date){
 let notes=calendarNotesOnDate(date);if(!notes.length)return '';
 return `<div class="calendar-cell-notes">${notes.slice(0,2).map(n=>`<span class="calendar-cell-note-title">▣ ${calendarSafe(n.title)}</span>`).join('')}${notes.length>2?`<span class="calendar-cell-note-more">+${notes.length-2} more note${notes.length-2===1?'':'s'}</span>`:''}</div>`
}
function calendarHoverNotes(date){
 let notes=calendarNotesOnDate(date);if(!notes.length)return '';
 let label=new Date(date+'T12:00:00').toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long'});
 return `<div class="calendar-hover-notes" role="tooltip"><div class="calendar-hover-head"><b>${label}</b><span>${notes.length} calendar note${notes.length===1?'':'s'}</span></div>${notes.map(n=>`<div class="calendar-hover-note"><b>${calendarSafe(n.title)}</b><span>${calendarSafe(n.notes||'No additional notes')}</span></div>`).join('')}</div>`
}

function calendarMonthDate(){let d=new Date((calendarMonth||iso())+'T12:00:00');return isNaN(d)?new Date():d}
function shiftCalendarMonth(n){let d=calendarMonthDate();d=new Date(d.getFullYear(),d.getMonth()+n,1);calendarMonth=iso(d);render()}
function goCalendarToday(){let d=new Date();calendarMonth=iso(new Date(d.getFullYear(),d.getMonth(),1));render()}
function calendarCompletedEvents(date){
 let out=[];
 filtered().forEach(x=>{
  if(x.type==='House Plot')(x.lifts||[]).forEach(l=>{if(l.status==='Complete'&&l.completedDate===date)out.push({kind:'complete',title:`${x.ref} · ${l.name}`,sub:`${x.type} · ${gangName(x.gang)}`,itemId:x.id})});
  else if(x.status==='Complete'&&x.completedDate===date)out.push({kind:'complete',title:`${x.ref} · ${x.type}`,sub:gangName(x.gang),itemId:x.id})
 });
 return out
}
function calendarIssueDueEvents(date){
 if(!hasPerm('view_issues'))return [];
 return filtered().flatMap(x=>(x.issues||[]).filter(i=>i.status==='Open'&&i.targetDate===date).map(i=>({kind:'issue',title:`${x.ref} · ${i.title}`,sub:`${issueAssigneeLabel(i.assignedTo)} · ${i.priority||'Normal'}`,itemId:x.id,issueId:i.id})))
}
function calendarDiaryEvents(date){
 if(!hasPerm('view_site_diary'))return [];
 return (db.siteDiary||[]).filter(e=>canAccessSite(e.siteId||e.site)&&(site==='all'||(e.siteId||e.site)===site)&&e.date===date).map(e=>({kind:'diary',title:'Site Diary recorded',sub:`${e.teams||'Site record'}${e.by?' · '+e.by:''}`}))
}
function calendarNoteEvents(date){
 return calendarNotesOnDate(date).map(n=>({kind:'note',title:n.title,sub:`${siteName(n.siteId)}${n.notes?' · '+n.notes:''}`,noteId:n.id,notes:n.notes||''}))
}
function calendarDayEvents(date){return [...calendarIssueDueEvents(date),...calendarNoteEvents(date),...calendarCompletedEvents(date),...calendarDiaryEvents(date)]}
function calendarDayCounts(date){
 let events=calendarDayEvents(date),counts={issue:0,note:0,complete:0,diary:0};
 events.forEach(e=>counts[e.kind]=(counts[e.kind]||0)+1);
 return {events,counts}
}
function calendarEventSummary(counts){
 let chips=[];
 if(counts.issue)chips.push(`<span class="month-chip chip-issue">${counts.issue} issue${counts.issue===1?'':'s'} due</span>`);
 if(counts.note)chips.push(`<span class="month-chip chip-note">${counts.note} note${counts.note===1?'':'s'}</span>`);
 if(counts.complete)chips.push(`<span class="month-chip chip-complete">${counts.complete} complete</span>`);
 if(counts.diary)chips.push(`<span class="month-chip chip-diary">${counts.diary} diary</span>`);
 return chips.join('')
}
function fullCalendarMonth(){
 let current=calendarMonthDate(),year=current.getFullYear(),month=current.getMonth(),first=new Date(year,month,1),offset=(first.getDay()+6)%7,start=new Date(year,month,1-offset),today=iso(),cells=[];
 for(let i=0;i<42;i++){
  let d=new Date(start);d.setDate(start.getDate()+i);
  let ds=iso(d),info=calendarDayCounts(ds),notes=calendarNotesOnDate(ds),inMonth=d.getMonth()===month,
      nonNote={issue:info.counts.issue||0,note:0,complete:info.counts.complete||0,diary:info.counts.diary||0};
  cells.push(`<button class="month-day ${inMonth?'':'outside'} ${ds===today?'today':''} ${info.events.length?'has-events':''} ${notes.length?'has-calendar-note':''}" onclick="openCalendarDay('${ds}')"><span class="month-day-number">${d.getDate()}</span>${calendarNoteCellPreview(ds)}<div class="month-day-events month-day-other-events">${calendarEventSummary(nonNote)}</div>${info.events.length?`<span class="month-event-count">${info.events.length}</span>`:''}${calendarHoverNotes(ds)}</button>`)
 }
 return `<div class="panel full-calendar-panel"><div class="full-calendar-head"><div><span class="calendar-kicker">VISUAL CALENDAR</span><h2>${current.toLocaleDateString('en-GB',{month:'long',year:'numeric'})}</h2><div class="muted">Calendar notes appear directly on their date. Hover a highlighted date to read every note, or click it for the full day view.</div></div><div class="calendar-month-actions"><button class="btn light" onclick="shiftCalendarMonth(-1)">← Previous</button><button class="btn light" onclick="goCalendarToday()">Today</button><button class="btn light" onclick="shiftCalendarMonth(1)">Next →</button></div></div><div class="calendar-legend"><span><i class="legend-note"></i><b>Calendar note</b> — highlighted date</span><span><i class="legend-issue"></i>Issue due</span><span><i class="legend-complete"></i>Work completed</span>${hasPerm('view_site_diary')?'<span><i class="legend-diary"></i>Site Diary</span>':''}</div><div class="month-weekdays">${['Mon','Tue','Wed','Thu','Fri','Sat','Sun'].map(d=>`<b>${d}</b>`).join('')}</div><div class="month-grid">${cells.join('')}</div></div>`
}
function openCalendarDay(date){
 let {events}=calendarDayCounts(date),label=new Date(date+'T12:00:00').toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'});
 let notes=events.filter(e=>e.kind==='note'),other=events.filter(e=>e.kind!=='note');
 let eventRow=e=>`<button class="calendar-day-event event-${e.kind}" onclick="${e.issueId?`modal=null;viewIssue('${e.itemId}','${e.issueId}')`:e.itemId?`modal=null;openItem('${e.itemId}')`:`modal=null;goNav('diary')`}"><i>${e.kind==='issue'?'⚠':e.kind==='complete'?'✓':'✎'}</i><span><b>${calendarSafe(e.title)}</b><small>${calendarSafe(e.sub||'')}</small></span><strong>›</strong></button>`;
 modal=`<div class="modal"><div class="modalbox calendar-day-modal"><div class="sectionhead"><div><span class="calendar-kicker">DAY VIEW</span><h2>${label}</h2><div class="muted">${events.length} recorded / scheduled event${events.length===1?'':'s'}</div></div><div class="actions">${hasPerm('manage_calendar')?`<button class="btn orange smallbtn" onclick="modal=null;render();setTimeout(()=>addCalendarNoteForDate('${date}'),0)">＋ Add Calendar Note</button>`:''}<button class="btn light" onclick="modal=null;render()">✕ Close</button></div></div>${notes.length?`<div class="calendar-day-group calendar-day-note-group"><div class="calendar-day-group-head"><div><b>Calendar Notes</b><span>${notes.length}</span></div></div><div class="calendar-day-list">${notes.map(e=>`<div class="calendar-day-note-record"><button class="calendar-day-event event-note" onclick="openCalendarNote('${e.noteId}')"><i>▣</i><span><b>${calendarSafe(e.title)}</b><small>${calendarSafe(e.sub||'')}</small></span><strong>›</strong></button>${hasPerm('manage_calendar')?`<button class="calendar-note-remove" onclick="deleteCalendarNote('${e.noteId}')" title="Remove this calendar note">×</button>`:''}</div>`).join('')}</div></div>`:''}${other.length?`<div class="calendar-day-group"><div class="calendar-day-group-head"><div><b>Site Activity</b><span>${other.length}</span></div></div><div class="calendar-day-list">${other.map(eventRow).join('')}</div></div>`:''}${!events.length?smartEmpty('Nothing recorded for this date.','Add a Calendar Note if something happened or is due to happen.','','','▣'):''}</div></div>`;render()
}
function calendarNotesForView(){return (db.calendarNotes||[]).filter(n=>canAccessSite(n.siteId)&&((site==='all')||n.siteId===site)).sort((a,b)=>String(a.date).localeCompare(String(b.date)))}
function addCalendarNote(){addCalendarNoteForDate(iso())}

function addCalendarNoteForDate(date=iso()){
 if(!hasPerm('manage_calendar'))return toast('You do not have permission to manage the Calendar');
 let defaultSite=site==='all'?(activeSites()[0]?.id||''):site;
 modal=`<div class="modal"><div class="modalbox"><h2>Add Calendar Note</h2><p class="muted">Record something that happened, or something due to happen. It will appear directly on the monthly calendar.</p><div class="two"><div class="field"><label>Date</label><input id="cn-date" class="input" type="date" value="${date||iso()}"></div><div class="field"><label>Site</label><select id="cn-site" class="select">${activeSites().map(s=>`<option value="${s.id}" ${s.id===defaultSite?'selected':''}>${s.name}</option>`).join('')}</select></div></div><div class="field"><label>Title</label><input id="cn-title" class="input" placeholder="e.g. Brick delivery / Scaffold moved / Inspection"></div><div class="field"><label>Notes</label><textarea id="cn-notes" class="textarea" style="min-height:120px" placeholder="Add the useful detail you may need to refer back to later..."></textarea></div><button class="btn orange" onclick="saveCalendarNote()">Save note</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}

function saveCalendarNote(){
 if(!hasPerm('manage_calendar'))return toast('You do not have permission to manage the Calendar');
 let d=document.getElementById('cn-date')?.value||'',siteId=document.getElementById('cn-site')?.value||'',title=(document.getElementById('cn-title')?.value||'').trim(),notes=(document.getElementById('cn-notes')?.value||'').trim();
 if(!d||!siteId||!title)return toast('Date, site and title are required');
 db.calendarNotes.push({id:'cn'+Date.now(),date:d,siteId,title,notes,createdBy:currentUser()?.id||'',createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()});
 calendarMonth=d.slice(0,7)+'-01';
 save();emitNotification('site',siteId,`Calendar note added — ${title}`);modal=null;savedState('Calendar note saved');toast('Calendar note saved');render()
}
function openCalendarNote(id){let n=(db.calendarNotes||[]).find(n=>n.id===id);if(!n)return;modal=`<div class="modal"><div class="modalbox"><h2>${n.title}</h2><p class="muted">${new Date(n.date+'T12:00:00').toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'})} · ${siteName(n.siteId)}</p><div class="member-record-note">${(n.notes||'No additional notes.').replace(/</g,'&lt;').replace(/>/g,'&gt;')}</div><div style="margin-top:14px">${hasPerm('manage_calendar')?`<button class="btn light" onclick="editCalendarNote('${n.id}')">Edit</button> <button class="btn red" onclick="deleteCalendarNote('${n.id}')">Delete</button> `:'' }<button class="btn" onclick="modal=null;render()">Close</button></div></div></div>`;render()}
function editCalendarNote(id){if(!hasPerm('manage_calendar'))return toast('You do not have permission to manage the Calendar');let n=(db.calendarNotes||[]).find(n=>n.id===id);if(!n)return;modal=`<div class="modal"><div class="modalbox"><h2>Edit Calendar Note</h2><div class="two"><div class="field"><label>Date</label><input id="ecn-date" class="input" type="date" value="${n.date}"></div><div class="field"><label>Site</label><select id="ecn-site" class="select">${activeSites().map(s=>`<option value="${s.id}" ${s.id===n.siteId?'selected':''}>${s.name}</option>`).join('')}</select></div></div><div class="field"><label>Title</label><input id="ecn-title" class="input" value="${n.title.replace(/"/g,'&quot;')}"></div><div class="field"><label>Notes</label><textarea id="ecn-notes" class="textarea" style="min-height:120px">${n.notes||''}</textarea></div><button class="btn orange" onclick="saveEditedCalendarNote('${n.id}')">Save changes</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveEditedCalendarNote(id){
 if(!hasPerm('manage_calendar'))return toast('You do not have permission to manage the Calendar');
 let n=(db.calendarNotes||[]).find(n=>n.id===id);if(!n)return;
 n.date=document.getElementById('ecn-date')?.value||n.date;n.siteId=document.getElementById('ecn-site')?.value||n.siteId;n.title=(document.getElementById('ecn-title')?.value||n.title).trim();n.notes=(document.getElementById('ecn-notes')?.value||'').trim();
 n.updatedAt=new Date().toISOString();n.updatedBy=currentUser()?.id||'';
 calendarMonth=n.date.slice(0,7)+'-01';
 save();emitNotification('site',n.siteId,`Calendar note updated — ${n.title}`);modal=null;savedState('Calendar note updated');toast('Calendar note updated');render()
}
function deleteCalendarNote(id){if(!hasPerm('manage_calendar'))return toast('You do not have permission to manage the Calendar');let i=(db.calendarNotes||[]).findIndex(n=>n.id===id);if(i<0)return;let removed=db.calendarNotes[i];db.calendarNotes.splice(i,1);save();modal=null;render();showUndo('Calendar note removed',()=>{db.calendarNotes.splice(Math.min(i,db.calendarNotes.length),0,removed);save();render()})}
function calendarNotesPanel(){
 let notes=calendarNotesForView(),today=iso(),upcoming=notes.filter(n=>n.date>=today).sort((a,b)=>a.date.localeCompare(b.date)),past=notes.filter(n=>n.date<today).sort((a,b)=>b.date.localeCompare(a.date)),ordered=[...upcoming,...past],shown=calendarNotesExpanded?ordered:ordered.slice(0,6),hidden=Math.max(0,ordered.length-shown.length);
 return `<div class="panel calendar-notes-panel" style="padding:15px"><div class="sectionhead"><div><h2>Calendar Notes</h2><div class="muted">Upcoming notes first, then the most recent past records</div></div>${hasPerm('manage_calendar')?'<button class="btn orange smallbtn" onclick="addCalendarNote()">＋ Add note</button>':''}</div><div class="calendar-note-list">${shown.length?shown.map(n=>`<div class="calendar-note-wrap"><div class="calendar-note ${n.date>=today?'future':'past'}" onclick="openCalendarNote('${n.id}')"><div><div class="calendar-note-date">${new Date(n.date+'T12:00:00').toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'})}</div><span class="note-date-badge">${n.date===today?'Today':n.date>today?'Upcoming':'Past'}</span></div><div><div class="calendar-note-title">${calendarSafe(n.title)}</div><div class="calendar-note-text">${calendarSafe(siteName(n.siteId))}${n.notes?' · '+calendarSafe(n.notes):''}</div></div><b>›</b></div>${hasPerm('manage_calendar')?`<button class="calendar-note-list-remove" onclick="event.stopPropagation();deleteCalendarNote('${n.id}')" title="Remove note">×</button>`:''}</div>`).join(''):smartEmpty('No calendar notes yet.','Add something that happened or something due to happen.',hasPerm('manage_calendar')?'＋ Add Calendar Note':'',hasPerm('manage_calendar')?'addCalendarNote()':'','▣')}</div>${ordered.length>6?`<button class="calendar-notes-expand" onclick="calendarNotesExpanded=!calendarNotesExpanded;render()">${calendarNotesExpanded?'Show fewer notes ▲':`View all ${ordered.length} notes ▼`}</button>`:''}</div>`
}
function calendarPage(){
 if(!hasPerm('view_calendar'))return `<div class="panel empty-help"><b>You do not have permission to view the Calendar.</b></div>`;
 calendarPreset='custom';let ready=!!fromDate&&!!toDate,a=ready?calendarItems():[],groups={};a.forEach(x=>{let d=completionDate(x)||'No date';(groups[d]||(groups[d]=[])).push(x)});let dates=Object.keys(groups).filter(d=>d!=='No date').sort().reverse();
 let history=calendarHistoryOpen?`<div class="panel calendar-history-panel"><div class="sectionhead"><div><h2>Completion History</h2><div class="muted">Detailed review across a FROM / TO date range</div></div><button class="btn light smallbtn" onclick="calendarHistoryOpen=false;render()">Hide ▲</button></div><div class="calendarbar"><div class="datebox"><label>FROM</label><input class="input" type="date" value="${fromDate}" onchange="fromDate=this.value;calendarPreset='custom';render()"><label>TO</label><input class="input" type="date" value="${toDate}" onchange="toDate=this.value;calendarPreset='custom';render()"></div></div><div class="calendarstat"><div class="card"><div class="muted">Completed items</div><b>${a.length}</b></div><div class="card"><div class="muted">House plots</div><b>${a.filter(x=>x.type==='House Plot').length}</b></div><div class="card"><div class="muted">Other work</div><b>${a.filter(x=>x.type!=='House Plot').length}</b></div><div class="card"><div class="muted">Range</div><b>${ready?fromDate+' → '+toDate:'Choose dates'}</b></div></div>${!ready?'<div class="calendar-prompt"><b>Choose FROM and TO when you need a detailed completion history.</b></div>':`<div class="timeline">${dates.map(d=>`<div class="daygroup"><div class="dayhead"><b>${new Date(d+'T12:00:00').toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'})}</b><span class="badge b-green">${groups[d].length} complete</span></div><div class="timelineitems">${groups[d].map(card).join('')}</div></div>`).join('')||'<div class="panel empty-help"><b>No completed work in this date range.</b><div style="margin-top:6px">Try a wider date range.</div></div>'}</div>`}</div>`:`<button class="calendar-history-toggle" onclick="calendarHistoryOpen=true;render()"><span><b>Completion History</b><small>Optional FROM / TO review of completed work</small></span><strong>View history ▼</strong></button>`;
 return `<div class="topbar"><div><h1>Calendar</h1><div class="sub">See the whole month at a glance</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select>${hasPerm('manage_calendar')?'<button class="btn orange primary-action" onclick="addCalendarNote()">＋ Add Calendar Note</button>':''}${hasPerm('download_reports')?'<button class="btn green" onclick="openReportRange()">⇩ Download dated site report</button>':''}</div></div>${fullCalendarMonth()}${history}${calendarNotesPanel()}`
}
function issueSiteSelector(){return `<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select>`}

let issueFocusFilter='all',issueSearch='';
function issueAssigneeOptions(value=''){let opts=[['','Unassigned'],...db.gangs.map(g=>['gang:'+g.id,'Team · '+g.name]),...db.users.map(u=>['user:'+u.id,'User · '+u.name])];return opts.map(([v,l])=>`<option value="${v}" ${value===v?'selected':''}>${l}</option>`).join('')}
function issueAssigneeLabel(v){if(!v)return 'Unassigned';let [kind,id]=String(v).split(':');return kind==='gang'?gangName(id):kind==='user'?(db.users.find(u=>u.id===id)?.name||'User'):'Unassigned'}
function issueDueState(i){if(i.status==='Resolved'||!i.targetDate)return '';let today=iso();if(i.targetDate<today)return 'overdue';if(i.targetDate===today)return 'due-today';return 'future'}
function issuePriorityBadge(i){let p=i.priority||'Normal',cls=p==='Urgent'?'b-red':p==='High'?'b-amber':'b-blue';return `<span class="badge ${cls}">${p}</span>`}
function issueDueBadge(i){let s=issueDueState(i);if(!i.targetDate)return '';if(s==='overdue'){let days=Math.max(1,Math.floor((new Date(iso()+'T12:00:00')-new Date(i.targetDate+'T12:00:00'))/86400000));return `<span class="badge b-red">Overdue ${days}d</span>`}if(s==='due-today')return '<span class="badge b-amber">Due today</span>';return `<span class="badge b-blue">Due ${i.targetDate}</span>`}
function itemOverdueIssues(x){return (x.issues||[]).filter(i=>i.status==='Open'&&issueDueState(i)==='overdue')}
function editIssueRecord(xid,iid){if(!hasPerm('manage_issues'))return toast('You do not have permission to manage issues');let x=db.items.find(x=>x.id===xid),i=x?.issues?.find(i=>i.id===iid);if(!i)return;modal=`<div class="modal"><div class="modalbox"><h2>Manage Issue</h2><div class="field"><label>Assigned to</label><select id="ei-assigned" class="select">${issueAssigneeOptions(i.assignedTo)}</select></div><div class="two"><div class="field"><label>Priority</label><select id="ei-priority" class="select">${['Normal','High','Urgent'].map(p=>`<option ${i.priority===p?'selected':''}>${p}</option>`).join('')}</select></div><div class="field"><label>Target date</label><input id="ei-target" class="input" type="date" value="${i.targetDate||''}"></div></div><div class="field"><label>Details</label><textarea id="ei-details" class="textarea">${i.details||''}</textarea></div><div class="field"><label>Notes</label><textarea id="ei-notes" class="textarea">${i.notes||''}</textarea></div><button class="btn orange" onclick="saveIssueRecord('${xid}','${iid}')">✓ Save issue</button> <button class="btn light" onclick="viewIssue('${xid}','${iid}')">Cancel</button></div></div>`;render()}
function saveIssueRecord(xid,iid){let x=db.items.find(x=>x.id===xid),i=x?.issues?.find(i=>i.id===iid);if(!i)return;i.assignedTo=document.getElementById('ei-assigned')?.value||'';i.priority=document.getElementById('ei-priority')?.value||'Normal';i.targetDate=document.getElementById('ei-target')?.value||'';i.details=document.getElementById('ei-details')?.value||'';i.notes=document.getElementById('ei-notes')?.value||'';i.updatedAt=new Date().toISOString();i.updatedBy=currentUser()?.name||'';touch(x);save();audit('issue','issue',i.id,`${x.ref}: issue responsibility updated — ${i.title}`,x.site);savedState('Issue saved');viewIssue(xid,iid)}

function issuesPage(){if(!hasPerm('view_issues'))return `<div class="panel empty-help"><b>You do not have permission to view Issues.</b></div>`;let items=db.items.filter(x=>canAccessSite(x.site)&&!db.sites.find(s=>s.id===x.site)?.archived&&(site==='all'||x.site===site)),all=items.flatMap(x=>x.issues.map(i=>({...i,ref:x.ref,type:x.type,item:x,siteLabel:siteName(x.site)}))),search=issueSearch.trim().toLowerCase();if(search)all=all.filter(i=>[i.ref,i.title,i.details,i.notes,issueAssigneeLabel(i.assignedTo),i.priority].some(v=>String(v||'').toLowerCase().includes(search)));if(issueFocusFilter==='overdue')all=all.filter(i=>issueDueState(i)==='overdue');if(issueFocusFilter==='urgent')all=all.filter(i=>i.priority==='Urgent');if(issueFocusFilter==='mine')all=all.filter(i=>i.assignedTo==='user:'+currentUserId);let rank=i=>(issueDueState(i)==='overdue'?0:i.priority==='Urgent'?1:i.priority==='High'?2:i.targetDate?3:4);all.sort((a,b)=>rank(a)-rank(b)||String(a.targetDate||'9999').localeCompare(String(b.targetDate||'9999'))||String(b.created||'').localeCompare(String(a.created||'')));let open=all.filter(i=>i.status==='Open'),res=all.filter(i=>i.status==='Resolved');return `<div class="topbar"><div><h1>Issues</h1><div class="sub">Ownership, priority, target dates, photos and resolution history</div>${selectedSiteHeading()}</div><div class="actions">${issueSiteSelector()}</div></div><div class="panel issue-control-panel"><div class="issue-controls"><div class="searchbox"><span>⌕</span><input value="${issueSearch}" placeholder="Search issues, plots or assignees…" oninput="issueSearch=this.value;render()"></div><div class="issue-focus">${[['all','All'],['overdue','Overdue'],['urgent','Urgent'],['mine','Assigned to me']].map(([v,l])=>`<button class="${issueFocusFilter===v?'active':''}" onclick="issueFocusFilter='${v}';render()">${l}</button>`).join('')}</div></div></div><div class="panel" style="padding:15px"><div class="issue-section-title"><h2 style="color:var(--red)">Unresolved Issues</h2><span class="badge b-red">${open.length}</span></div><div class="issue-overview">${open.map(issueRow).join('')||smartEmpty('No unresolved issues match this view.','Change the Issue filter or search to see other records.','','','✓')}</div><div class="issue-section-title"><h2 style="color:var(--green)">Resolved History</h2><span class="badge b-green">${res.length}</span></div><div class="issue-overview">${res.map(issueRow).join('')||smartEmpty('No resolved issues match this view.','Resolved issues remain here as a permanent record.','','','✓')}</div></div>`}
function issueRow(i){return `<div class="issue-overview-row ${i.status==='Resolved'?'resolved':'open'} ${issueDueState(i)==='overdue'?'issue-row-overdue':''}" onclick="viewIssue('${i.item.id}','${i.id}')"><div><div class="iref">${i.ref}</div><div class="imeta">${i.type}</div></div><div><div class="ititle">${i.title}</div><div class="imeta">${i.details||'No details entered'}</div><div class="issue-row-tags">${issuePriorityBadge(i)}${issueDueBadge(i)}<span class="issue-owner">♟ ${issueAssigneeLabel(i.assignedTo)}</span></div></div><div class="imeta">${i.siteLabel} · Opened ${i.created||''}${i.createdBy?' by '+i.createdBy:''}${i.photos?.length?` · ${i.photos.length} photo(s)`:''}</div>${badge(i.status)}<span style="font-weight:900;color:#728197">›</span></div>`}
function viewIssue(xid,iid){let x=db.items.find(x=>x.id===xid),i=x?.issues?.find(i=>i.id===iid);if(!canViewItem(x))return toast('Plot cards and their issues are available to Supervisors and above');if(!x||!i)return;modal=`<div class="modal"><div class="modalbox"><div class="sectionhead"><div><div class="sub">${x.ref} · ${x.type}</div><h2>${i.title}</h2><div class="issue-detail-badges">${issuePriorityBadge(i)}${issueDueBadge(i)}</div></div>${badge(i.status)}</div><div class="issue-record-grid"><div><span>Assigned to</span><b>${issueAssigneeLabel(i.assignedTo)}</b></div><div><span>Target date</span><b>${i.targetDate||'No target set'}</b></div><div><span>Opened by</span><b>${i.createdBy||'Not recorded'}</b></div><div><span>Last updated</span><b>${i.updatedBy||i.createdBy||'—'}${i.updatedAt?' · '+new Date(i.updatedAt).toLocaleString():''}</b></div></div><p>${i.details||''}</p><p class="muted">${i.notes||''}</p><div class="sectionhead"><h3>Issue Photos</h3>${hasPerm('raise_issues')||hasPerm('manage_issues')?'<button class="btn red smallbtn" onclick="event.stopPropagation();addIssuePhotos(\''+xid+'\',\''+iid+'\')">＋ Add photos</button>':''}</div>${i.photos?.length?'<div class="photo-grid">'+i.photos.map((p,n)=>`<img src="${mediaSrc(p.data)}"${pendingMediaAttr(p.data)} alt="Issue photo ${n+1}" onclick="event.stopPropagation();previewPhoto(mediaSrc('${p.data}'))">`).join('')+'</div>':'<div class="photo-empty">No issue photos yet.</div>'}<p class="muted">Created ${i.created}${i.resolved?' · Resolved '+i.resolved+(i.resolvedBy?' by '+i.resolvedBy:''):''}</p><div class="actions">${hasPerm('manage_issues')&&i.status==='Open'?`<button class="btn orange" onclick="editIssueRecord('${xid}','${iid}')">Manage issue</button><button class="btn green" onclick="resolveIssue('${xid}','${iid}')">✓ Resolve issue</button>`:''}<button class="btn light" onclick="modal=null;render()">Close</button></div></div></div>`;render()}
function showPhoto(src){return previewPhoto(src)}
function previewPhoto(src){modal=`<div class="modal" onclick="modal=null;render()"><div class="modalbox" style="text-align:center"><img src="${src}" style="max-width:100%;max-height:70vh;border-radius:12px"><p class="muted">Click anywhere to close</p></div></div>`;render()}

let cameraFiles={},photoAnnotations={},photoAnnotator={sourceId:'',index:-1,tool:'arrow',start:null,history:[],base:''},quickReturnAfterSave=false;
function uploadFiles(id){return [...(document.getElementById(id)?.files||[]),...(cameraFiles[id]||[])]}
function clearCameraFiles(id){delete cameraFiles[id];delete photoAnnotations[id]}
function cameraStatus(id){let n=(cameraFiles[id]||[]).length,e=document.getElementById('camera-status-'+id);if(e)e.textContent=n?`${n} camera photo${n===1?'':'s'} ready to save`:''}
function takePhotoInto(id,afterFn=''){
 let u=currentUser();
 if(u&&!u.cameraIntroSeen){
   let ok=confirm('Use your camera with SiteSight?\n\nSiteSight only asks for camera access when you press Take Photo. Your device may ask you to allow camera access. The photo is then attached to the SiteSight record you are updating.\n\nContinue to camera?');
   if(!ok)return;
   u.cameraIntroSeen=true;save()
 }
 let input=document.createElement('input');
 input.type='file';input.accept='image/*';input.capture='environment';
 input.onchange=()=>{
   let f=input.files?.[0];if(!f)return;
   cameraFiles[id]=cameraFiles[id]||[];
   let target=document.getElementById(id);
   if(target?.multiple)cameraFiles[id].push(f);else cameraFiles[id]=[f];
   cameraStatus(id);
   if(afterFn&&typeof window[afterFn]==='function')window[afterFn]();
 };
 input.click()
}
function cameraPicker(id,label='Take photo',afterFn=''){return `<div class="camera-picker"><button type="button" class="btn camera-btn" onclick="takePhotoInto('${id}','${afterFn}')">📷 ${label}</button><span id="camera-status-${id}" class="camera-status">${(cameraFiles[id]||[]).length?`${(cameraFiles[id]||[]).length} camera photo${(cameraFiles[id]||[]).length===1?'':'s'} ready to save`:''}</span></div>`}
function readPhotoFile(file,maxDim=1600,quality=.78){
 return new Promise(resolve=>{
   if(!file||!String(file.type||'').startsWith('image/')){let r=new FileReader();r.onload=()=>resolve(r.result);r.onerror=()=>resolve('');r.readAsDataURL(file);return}
   let r=new FileReader();
   r.onload=()=>{
     let img=new Image();
     img.onload=()=>{
       let w=img.width,h=img.height,scale=Math.min(1,maxDim/Math.max(w,h));
       let c=document.createElement('canvas');c.width=Math.max(1,Math.round(w*scale));c.height=Math.max(1,Math.round(h*scale));
       c.getContext('2d').drawImage(img,0,0,c.width,c.height);
       try{resolve(c.toDataURL('image/jpeg',quality))}catch(e){resolve(r.result)}
     };
     img.onerror=()=>resolve(r.result);img.src=r.result
   };
   r.onerror=()=>resolve('');r.readAsDataURL(file)
 })
}
function readUploadFile(file,maxDim=1800){
 if(String(file?.type||'').startsWith('image/'))return readPhotoFile(file,maxDim,.82);
 return new Promise(resolve=>{let r=new FileReader();r.onload=()=>resolve(r.result);r.onerror=()=>resolve('');r.readAsDataURL(file)})
}
function savePhotosToArray(files,target,done,sourceId=''){if(!files.length)return done();let left=files.length;files.forEach(async(f,idx)=>{let annotated=sourceId&&photoAnnotations[sourceId]?.[idx];try{let data=await storeMediaFile(f,annotated||'');if(data)target.push({id:'ph'+Date.now()+Math.random().toString(16).slice(2),name:f.name||'Camera photo',data,created:iso(),createdAt:new Date().toISOString(),by:currentUser()?.name||'',annotated:!!annotated})}catch(e){toast('A photo could not be stored')}if(!--left)done()})}
function annotationButton(id){
 return `<button type="button" class="btn light annotation-open-btn" onclick="openPhotoAnnotator('${id}')">✎ Annotate latest photo</button>`
}
function annotatorPoint(e){
 let c=document.getElementById('photo-annotator-canvas'),r=c.getBoundingClientRect();
 return {x:(e.clientX-r.left)*c.width/r.width,y:(e.clientY-r.top)*c.height/r.height}
}
function annotatorSnapshot(){
 let c=document.getElementById('photo-annotator-canvas');if(!c)return;
 photoAnnotator.history.push(c.toDataURL('image/jpeg',.9));
 if(photoAnnotator.history.length>15)photoAnnotator.history.shift()
}
function annotatorLoad(data,cb){
 let c=document.getElementById('photo-annotator-canvas');if(!c)return;
 let ctx=c.getContext('2d'),img=new Image();
 img.onload=()=>{ctx.clearRect(0,0,c.width,c.height);ctx.drawImage(img,0,0,c.width,c.height);if(cb)cb()};
 img.src=data
}
function setAnnotationTool(tool,btn){
 photoAnnotator.tool=tool;
 document.querySelectorAll('.annotation-tool').forEach(b=>b.classList.remove('active'));
 if(btn)btn.classList.add('active')
}
function annotationPointerDown(e){
 e.preventDefault();
 let c=document.getElementById('photo-annotator-canvas');if(!c)return;
 if(photoAnnotator.tool==='label'){
   let p=annotatorPoint(e),text=prompt('Label for this photo:','');
   if(!text)return;
   let ctx=c.getContext('2d'),size=Math.max(18,Math.round(c.width/42)),pad=Math.max(8,Math.round(size*.4));
   ctx.font=`700 ${size}px Arial`;ctx.textBaseline='middle';
   let w=ctx.measureText(text).width+pad*2,h=size+pad*1.5;
   ctx.fillStyle='rgba(198,39,39,.92)';ctx.fillRect(p.x,p.y-h/2,w,h);
   ctx.fillStyle='#fff';ctx.fillText(text,p.x+pad,p.y);
   annotatorSnapshot();return
 }
 photoAnnotator.start=annotatorPoint(e)
}
function annotationPointerUp(e){
 if(!photoAnnotator.start)return;
 e.preventDefault();
 let c=document.getElementById('photo-annotator-canvas'),ctx=c.getContext('2d'),a=photoAnnotator.start,b=annotatorPoint(e);
 photoAnnotator.start=null;
 ctx.save();ctx.strokeStyle='#d02020';ctx.fillStyle='#d02020';ctx.lineWidth=Math.max(5,Math.round(c.width/240));ctx.lineCap='round';ctx.lineJoin='round';
 if(photoAnnotator.tool==='circle'){
   let cx=(a.x+b.x)/2,cy=(a.y+b.y)/2,rx=Math.abs(b.x-a.x)/2,ry=Math.abs(b.y-a.y)/2;
   ctx.beginPath();ctx.ellipse(cx,cy,Math.max(rx,8),Math.max(ry,8),0,0,Math.PI*2);ctx.stroke()
 }else{
   let ang=Math.atan2(b.y-a.y,b.x-a.x),head=Math.max(22,c.width/28);
   ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();
   ctx.beginPath();ctx.moveTo(b.x,b.y);
   ctx.lineTo(b.x-head*Math.cos(ang-Math.PI/6),b.y-head*Math.sin(ang-Math.PI/6));
   ctx.lineTo(b.x-head*Math.cos(ang+Math.PI/6),b.y-head*Math.sin(ang+Math.PI/6));
   ctx.closePath();ctx.fill()
 }
 ctx.restore();annotatorSnapshot()
}
function annotationUndo(){
 if(photoAnnotator.history.length<=1)return toast('Nothing else to undo');
 photoAnnotator.history.pop();annotatorLoad(photoAnnotator.history[photoAnnotator.history.length-1])
}
function annotationClear(){
 if(!photoAnnotator.base)return;
 annotatorLoad(photoAnnotator.base,()=>{photoAnnotator.history=[photoAnnotator.base]})
}
function closePhotoAnnotator(){document.getElementById('photo-annotator-overlay')?.remove()}
function savePhotoAnnotation(){
 let c=document.getElementById('photo-annotator-canvas');if(!c)return;
 photoAnnotations[photoAnnotator.sourceId]=photoAnnotations[photoAnnotator.sourceId]||{};
 photoAnnotations[photoAnnotator.sourceId][photoAnnotator.index]=c.toDataURL('image/jpeg',.88);
 closePhotoAnnotator();
 let e=document.getElementById('camera-status-'+photoAnnotator.sourceId);
 if(e)e.textContent='✓ Annotated photo ready to save';
 toast('Photo annotation saved')
}
function openPhotoAnnotator(sourceId){
 let files=uploadFiles(sourceId);
 if(!files.length)return toast('Take or choose a photo first');
 let index=files.length-1,file=files[index],existing=photoAnnotations[sourceId]?.[index];
 let start=data=>{
   closePhotoAnnotator();
   let overlay=document.createElement('div');overlay.id='photo-annotator-overlay';overlay.className='photo-annotator-overlay';
   overlay.innerHTML=`<div class="photo-annotator-shell"><div class="photo-annotator-head"><div><b>Annotate Photo</b><span>Show exactly what matters before saving.</span></div><button onclick="closePhotoAnnotator()">×</button></div><div class="photo-annotator-tools"><button class="annotation-tool active" onclick="setAnnotationTool('arrow',this)">➜ Arrow</button><button class="annotation-tool" onclick="setAnnotationTool('circle',this)">○ Circle</button><button class="annotation-tool" onclick="setAnnotationTool('label',this)">T Label</button><span></span><button onclick="annotationUndo()">↶ Undo</button><button onclick="annotationClear()">Clear</button></div><div class="photo-annotator-stage"><canvas id="photo-annotator-canvas"></canvas></div><div class="photo-annotator-foot"><span>Red annotations are added to the saved copy.</span><button class="btn light" onclick="closePhotoAnnotator()">Cancel</button><button class="btn red" onclick="savePhotoAnnotation()">✓ Use annotated photo</button></div></div>`;
   document.body.appendChild(overlay);
   let img=new Image();img.onload=()=>{
     let c=document.getElementById('photo-annotator-canvas'),scale=Math.min(1,1500/Math.max(img.width,img.height));
     c.width=Math.max(1,Math.round(img.width*scale));c.height=Math.max(1,Math.round(img.height*scale));
     c.getContext('2d').drawImage(img,0,0,c.width,c.height);
     let initial=c.toDataURL('image/jpeg',.9);
     photoAnnotator={sourceId,index,tool:'arrow',start:null,history:[initial],base:initial};
     c.addEventListener('pointerdown',annotationPointerDown);
     c.addEventListener('pointerup',annotationPointerUp);
     c.addEventListener('pointercancel',()=>photoAnnotator.start=null)
   };img.src=data
 };
 if(existing)start(existing);else readPhotoFile(file,1800,.9).then(start)
}
function cancelQuickEntry(){
 let wasQuick=quickReturnAfterSave;quickReturnAfterSave=false;modal=null;
 if(wasQuick)selected=null;
 render()
}

function addIssuePhotos(xid,iid){if(!hasPerm('raise_issues')&&!hasPerm('manage_issues'))return toast('You do not have permission to add issue photos');let x=db.items.find(x=>x.id===xid),i=x.issues.find(i=>i.id===iid);clearCameraFiles('ip');modal=`<div class="modal"><div class="modalbox"><h2>Add issue photos</h2><div class="field"><label>Photos from device</label><input id="ip" class="input" type="file" accept="image/*" multiple></div>${cameraPicker('ip','Take issue photo')}${annotationButton('ip')}<p class="muted">On a phone, Take issue photo opens the camera. Photos are compressed for site use and stored with the shared SiteSight records.</p><button class="btn red" onclick="saveIssuePhotos('${xid}','${iid}')">Save photos</button> <button class="btn light" onclick="clearCameraFiles('ip');cancelQuickEntry()">Cancel</button></div></div>`;render()}
function saveIssuePhotos(xid,iid){if(!hasPerm('raise_issues')&&!hasPerm('manage_issues'))return toast('You do not have permission to add issue photos');let files=uploadFiles('ip');if(!files.length)return cancelQuickEntry();let i=db.items.find(x=>x.id===xid).issues.find(i=>i.id===iid);i.photos=i.photos||[];savePhotosToArray(files,i.photos,()=>{clearCameraFiles('ip');save();let wasQuick=quickReturnAfterSave;quickReturnAfterSave=false;if(wasQuick){selected=null;modal=null;render();savedState('Issue photos saved')}else viewIssue(xid,iid)},'ip')}
function openTeamMember(teamId,memberId){let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!g||!m)return;let week=attendanceWeekTotals(m.id,attendanceWeekStartISO(new Date()),attendanceDefaultSiteId()),active=attendanceActiveShift(m.id),payVisible=hasPerm('view_financials');modal=`<div class="modal"><div class="modalbox"><h2>${m.name}</h2><p class="muted">${m.role||'Team member'} · ${g.name}</p>${hasPerm('view_attendance')||hasPerm('manage_attendance')?`<div class="attendance-member-mini"><div><span>THIS WEEK</span><b>${attendanceFmtHours(week.totalHours)}</b></div><div><span>STATUS</span><b>${active?'● Clocked In':'○ Not Clocked In'}</b></div>${payVisible&&m.employmentType==='Hourly'?`<div><span>EST. GROSS</span><b>${attendanceMoney(week.gross)}</b></div>`:''}</div>`:''}<div class="field"><label>Member notes</label><textarea id="tm-notes" class="textarea" style="min-height:130px" placeholder="Training, responsibilities, site notes, performance notes, etc.">${m.notes||''}</textarea></div><div class="field"><label>Add photos</label><input id="tm-photos" class="input" type="file" accept="image/*" multiple></div>${cameraPicker('tm-photos','Take team photo')}<div class="member-photo-grid">${(m.photos||[]).map((p,i)=>`<div class="member-photo"><img src="${mediaSrc(p.data)}"${pendingMediaAttr(p.data)} alt="${p.name||'Team member photo'}"><div class="caption">${p.name||'Photo'}<br><button class="btn red smallbtn danger-btn" onclick="removeTeamMemberPhoto('${teamId}','${memberId}',${i})">Delete</button></div></div>`).join('')||'<div class="muted">No photos saved for this member.</div>'}</div><div class="attendance-member-actions">${hasPerm('view_attendance')?`<button class="btn light" onclick="modal=null;attendanceWorker='${m.id}';attendanceTab='timesheets';goNav('attendance')">◷ View timesheet</button>`:''}${hasPerm('manage_attendance')?`<button class="btn light" onclick="editAttendanceMemberSettings('${teamId}','${memberId}')">Attendance settings</button>`:''}</div><div style="margin-top:14px"><button class="btn blue" onclick="saveTeamMemberRecord('${teamId}','${memberId}')">Save changes</button> <button class="btn light" onclick="modal=null;render()">Close</button></div></div></div>`;render()}
function saveTeamMemberRecord(teamId,memberId){let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!m)return;m.notes=document.getElementById('tm-notes')?.value||'';let files=uploadFiles('tm-photos');m.photos=m.photos||[];if(!files.length){save();toast('Member record saved');openTeamMember(teamId,memberId);return}savePhotosToArray(files,m.photos,()=>{clearCameraFiles('tm-photos');save();toast('Member record saved');openTeamMember(teamId,memberId)})}
function removeTeamMemberPhoto(teamId,memberId,index){let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!m)return;if(!confirm('Delete this member photo?'))return;m.photos.splice(index,1);save();openTeamMember(teamId,memberId)}


function editTeamName(id){if(!hasPerm('manage_gangs'))return toast('You do not have team admin access');let g=db.gangs.find(g=>g.id===id);if(!g)return;modal=`<div class="modal"><div class="modalbox"><h2>Edit Team Name</h2><div class="field"><label>Team name</label><input id="edit-team-name" class="input" value="${g.name.replace(/"/g,'&quot;')}"></div><button class="btn orange" onclick="saveTeamName('${g.id}')">Save changes</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveTeamName(id){let g=db.gangs.find(g=>g.id===id),name=(document.getElementById('edit-team-name')?.value||'').trim();if(!g)return;if(!name)return toast('Enter a team name');g.name=name;save();modal=null;savedState('Team name updated');toast('Team name updated');render()}

function addTeam(){if(!hasPerm('manage_gangs'))return toast('You do not have team admin access');modal=`<div class="modal"><div class="modalbox"><h2>Add Team</h2><div class="field"><label>Team name</label><input id="team-name" class="input" placeholder="e.g. Alpha Team"></div><div class="field"><label>Contact details</label><input id="team-contact" class="input" placeholder="Phone / email"></div><button class="btn orange" onclick="saveTeam()">Create team</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveTeam(){let name=(document.getElementById('team-name')?.value||'').trim(),contact=(document.getElementById('team-contact')?.value||'').trim();if(!name)return toast('Enter a team name');db.gangs.push({id:'g'+Date.now(),name,active:true,contact,members:[]});save();modal=null;savedState('Team saved');toast('Team added');render()}
function manageTeamMembers(id){if(!hasPerm('manage_gangs'))return toast('You do not have team admin access');let g=db.gangs.find(g=>g.id===id);if(!g)return;modal=`<div class="modal"><div class="modalbox"><h2>${g.name} — Team Members</h2><p class="muted">Add each person and their role so the whole team is visible from the Teams page.</p><div id="member-list">${(g.members||[]).map((m,i)=>`<div class="team-member"><div class="clickable" style="flex:1;cursor:pointer" onclick="openTeamMember('${g.id}','${m.id}')"><b>${m.name}</b><span>${m.role||'Team member'}${m.photos?.length?` · ${m.photos.length} photo(s)`:''}</span></div><button class="btn red smallbtn" onclick="event.stopPropagation();removeTeamMember('${g.id}',${i})">Remove</button></div>`).join('')||'<div class="empty">No members added yet.</div>'}</div><div class="member-editor-row"><div class="field"><label>Name</label><input id="member-name" class="input" placeholder="Full name"></div><div class="field"><label>Role</label><input id="member-role" class="input" placeholder="e.g. Bricklayer, Labourer, Foreman"></div><button class="btn blue" onclick="addTeamMember('${g.id}')">Add member</button></div><div style="margin-top:14px"><button class="btn" onclick="modal=null;render()">Done</button></div></div></div>`;render()}
function addTeamMember(id){let g=db.gangs.find(g=>g.id===id),name=(document.getElementById('member-name')?.value||'').trim(),role=(document.getElementById('member-role')?.value||'').trim();if(!g)return;if(!name)return toast('Enter a team member name');g.members=g.members||[];g.members.push({id:'tm'+Date.now(),name,role,notes:'',photos:[],employmentType:'Not tracked',hourlyRate:0,overtimeRate:0,dayRate:0,standardDayHours:8,standardWeekHours:40,defaultBreakMinutes:30,linkedUserId:'',activeForAttendance:true});save();toast('Team member added');manageTeamMembers(id)}
function removeTeamMember(id,index){let g=db.gangs.find(g=>g.id===id);if(!g)return;if(!confirm('Remove this team member?'))return;g.members.splice(index,1);save();manageTeamMembers(id)}

function gangsPage(){
 return `<div class="topbar"><div><h1>Teams</h1><div class="sub">Create teams, record team members and roles, and open each team's live workload dashboard${site!=='all'?' for '+siteName(site):''}</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select>${hasPerm('view_attendance')||hasPerm('clock_attendance')?'<button class="btn blue" onclick="goNav(\'attendance\')">◷ Time & Attendance</button>':''}${hasPerm('manage_gangs')?'<button class="btn orange primary-action" onclick="addTeam()">＋ Add Team</button>':''}</div></div><div class="cards">${db.gangs.map(g=>{let a=db.items.filter(x=>canAccessSite(x.site)&&x.gang===g.id&&!db.sites.find(s=>s.id===x.site)?.archived&&(site==='all'||x.site===site));if(site!=='all'&&!a.length)return '';let members=g.members||[],shown=members.slice(0,3);return `<div class="card gangcard"><div class="sectionhead"><h3 style="cursor:pointer" onclick="openGang('${g.id}')">${g.name}</h3>${g.active?badge('Active'):badge('Not started')}</div><p>Assigned <b>${a.length}</b> · In progress <b>${a.filter(x=>overall(x)==='In progress').length}</b></p><p>Complete <b>${a.filter(x=>['Complete','Signed Off'].includes(overall(x))).length}</b> · Issues <b>${a.reduce((n,x)=>n+issues(x),0)}</b></p><div class="team-members">${members.length?shown.map(m=>`<div class="team-member clickable" onclick="openTeamMember('${g.id}','${m.id}')"><div><b>${m.name}</b><span>${m.role||'Team member'}${m.notes?' · notes saved':''}${m.photos?.length?` · ${m.photos.length} photo(s)`:''}</span></div><b>›</b></div>`).join(''):'<div class="muted team-no-members">No team members added.</div>'}${members.length>3?`<button class="team-view-all" onclick="openGang('${g.id}')">View all ${members.length} members ›</button>`:''}</div><div class="team-actions"><button class="btn light smallbtn" onclick="openGang('${g.id}')">Open team dashboard</button>${hasPerm('manage_gangs')?`<button class="btn blue smallbtn" onclick="manageTeamMembers('${g.id}')">Manage members</button><button class="btn light smallbtn" onclick="editTeamName('${g.id}')">Edit name</button>`:''}</div></div>`}).join('')||smartEmpty('No teams are showing for this site.','Assign a team to a Work Item, or create a Team if you have permission.',hasPerm('manage_gangs')?'＋ Add Team':'',hasPerm('manage_gangs')?'addTeam()':'','♟')}</div>`
}
function openGang(id){if(!roleCanAccessPlots())return toast('Operational team dashboards are available to Supervisors and above');selected=null;selectedGang=id;view='gangs';render()}
function gangDashboard(g){let a=db.items.filter(x=>canAccessSite(x.site)&&x.gang===g.id&&!db.sites.find(s=>s.id===x.site)?.archived&&(site==='all'||x.site===site)),prog=a.filter(x=>overall(x)==='In progress'),not=a.filter(x=>overall(x)==='Not started'),comp=a.filter(x=>overall(x)==='Complete'),sig=a.filter(x=>overall(x)==='Signed Off');return `<div class="detailwrap"><button class="btn light back simple-back" onclick="backAction()">← Back</button><div class="ganghero"><div class="detailhead"><div><div class="sub">Team dashboard</div><h1 style="margin:2px 0">${g.name}</h1><div class="sub">${g.active?'Active':'Inactive'}</div></div><div class="actions"><button class="btn orange" onclick="gang='${g.id}';selectedGang=null;view='work-items';render()">View filtered work list</button>${hasPerm('manage_gangs')?`<button class="btn light" onclick="editTeamName('${g.id}')">Edit team name</button>`:''}</div></div></div><div class="card" style="margin-top:12px"><div class="sectionhead"><h3>Team Members</h3>${hasPerm('manage_gangs')?`<button class="btn light smallbtn" onclick="manageTeamMembers('${g.id}')">Manage</button>`:''}</div><div class="team-members">${(g.members||[]).length?(g.members||[]).map(m=>`<div class="team-member clickable" onclick="openTeamMember('${g.id}','${m.id}')"><div><b>${m.name}</b><span>${m.role||'Team member'}${m.notes?' · notes saved':''}${m.photos?.length?` · ${m.photos.length} photo(s)`:''}</span></div><b>›</b></div>`).join(''):'<div class="empty-help"><b>No team members added.</b><div style="margin-top:6px">Use Manage to add names and roles.</div></div>'}</div></div><div class="gangmetrics"><div class="card"><div class="muted">Current work</div><b>${a.length}</b></div><div class="card"><div class="muted">In progress</div><b>${prog.length}</b></div><div class="card"><div class="muted">Open issues</div><b>${a.reduce((n,x)=>n+issues(x),0)}</b></div><div class="card"><div class="muted">Signed off</div><b>${sig.length}</b></div></div><div class="panel"><div class="section gangsection">${section('Open Issues',a.filter(x=>issues(x)>0),'open')}</div>${section('In Progress',prog,'progress')}${section('Not Started',not,'')}${section('Complete',comp,'complete')}${section('Signed Off',sig,'signed')}</div></div>`}

// ============================================================
// V1.66 — Time & Attendance
// GPS is checked only when the user deliberately clocks in/out.
// There is no continuous worker tracking.
// ============================================================
function attendanceAllMembers(){let out=[];(db.gangs||[]).forEach(g=>(g.members||[]).forEach(m=>out.push({member:m,gang:g})));return out}
function attendanceMemberById(id){return attendanceAllMembers().find(x=>x.member.id===id)||null}
function attendanceLinkedMember(){let id=currentUser()?.linkedMemberId||'';return id?attendanceMemberById(id):null}
function attendanceCanViewAll(){return hasPerm('view_attendance')||hasPerm('manage_attendance')||hasPerm('approve_timesheets')}
function attendanceDefaultSiteId(){let allowed=activeSites();if(site!=='all'&&allowed.some(s=>s.id===site))return site;if(attendanceSite!=='all'&&allowed.some(s=>s.id===attendanceSite))return attendanceSite;let linked=attendanceActiveShift(currentUser()?.linkedMemberId||'');if(linked&&allowed.some(s=>s.id===linked.siteId))return linked.siteId;return allowed[0]?.id||''}
function attendanceSiteConfig(siteId){let s=db.sites.find(s=>s.id===siteId);if(!s)return null;s.attendanceConfig=s.attendanceConfig||{enabled:true,latitude:null,longitude:null,radiusM:150,maxAccuracyM:75,maxShiftHours:12,locationSetAt:'',locationSetBy:''};return s.attendanceConfig}
function attendanceLocationConfigured(siteId){let c=attendanceSiteConfig(siteId);return !!c&&c.latitude!==null&&c.latitude!==undefined&&c.latitude!==''&&c.longitude!==null&&c.longitude!==undefined&&c.longitude!==''&&Number.isFinite(Number(c.latitude))&&Number.isFinite(Number(c.longitude))}
function attendanceMoney(v){return Number(v||0).toLocaleString('en-GB',{style:'currency',currency:'GBP',minimumFractionDigits:2,maximumFractionDigits:2})}
function attendanceFmtHours(v){return `${Number(v||0).toFixed(2)} hrs`}
function attendanceDateOnly(v){if(!v)return '';let d=new Date(v);if(Number.isNaN(d.getTime()))return String(v).slice(0,10);let y=d.getFullYear(),m=String(d.getMonth()+1).padStart(2,'0'),day=String(d.getDate()).padStart(2,'0');return `${y}-${m}-${day}`}
function attendanceDateTimeLocalValue(v){if(!v)return '';let d=new Date(v);if(Number.isNaN(d.getTime()))return '';let p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`}
function attendanceLocalIsoFromInput(v){if(!v)return '';let d=new Date(v);return Number.isNaN(d.getTime())?'':d.toISOString()}
function attendanceWeekStartISO(value=new Date()){let d=value instanceof Date?new Date(value):new Date(String(value)+'T12:00:00');if(Number.isNaN(d.getTime()))d=new Date();let day=(d.getDay()+6)%7;d.setHours(12,0,0,0);d.setDate(d.getDate()-day);return attendanceDateOnly(d)}
function attendanceAddDays(date,n){let d=new Date(date+'T12:00:00');d.setDate(d.getDate()+n);return attendanceDateOnly(d)}
function attendanceWeekDates(start){return Array.from({length:7},(_,i)=>attendanceAddDays(start,i))}
function attendanceWeekLabel(start){let end=attendanceAddDays(start,6);return `${new Date(start+'T12:00:00').toLocaleDateString('en-GB',{day:'numeric',month:'short'})} – ${new Date(end+'T12:00:00').toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'})}`}
function attendanceShiftHours(s,at=new Date()){if(!s||s.voided||!s.clockIn?.time)return 0;let a=new Date(s.clockIn.time),b=s.clockOut?.time?new Date(s.clockOut.time):at;if(Number.isNaN(a.getTime())||Number.isNaN(b.getTime())||b<a)return 0;return Math.max(0,((b-a)/36e5)-(Number(s.breakMinutes||0)/60))}
function attendanceActiveShift(workerId){return (db.attendanceShifts||[]).filter(s=>s.workerId===workerId&&!s.voided&&!s.clockOut?.time).sort((a,b)=>String(b.clockIn?.time||'').localeCompare(String(a.clockIn?.time||'')))[0]||null}
function attendanceShiftsForWorkerWeek(workerId,start,siteId='all'){let end=attendanceAddDays(start,7);return (db.attendanceShifts||[]).filter(s=>s.workerId===workerId&&!s.voided&&(siteId==='all'||s.siteId===siteId)&&attendanceDateOnly(s.clockIn?.time)>=start&&attendanceDateOnly(s.clockIn?.time)<end).sort((a,b)=>String(a.clockIn?.time||'').localeCompare(String(b.clockIn?.time||'')))}
function attendanceAbsence(workerId,date,siteId='all'){return (db.attendanceAbsences||[]).find(a=>a.workerId===workerId&&a.date===date&&(siteId==='all'||a.siteId===siteId))||null}
function attendanceApproval(workerId,siteId,start){return (db.timesheetApprovals||[]).filter(a=>a.workerId===workerId&&a.siteId===siteId&&a.weekStart===start).sort((a,b)=>String(b.approvedAt||'').localeCompare(String(a.approvedAt||'')))[0]||null}
function attendanceWeekTotals(workerId,start,siteId='all'){let wm=attendanceMemberById(workerId),m=wm?.member||{},shifts=attendanceShiftsForWorkerWeek(workerId,start,siteId),totalHours=shifts.reduce((n,s)=>n+attendanceShiftHours(s),0),standard=Number(m.standardWeekHours||40),basicHours=Math.min(totalHours,standard),overtimeHours=Math.max(0,totalHours-standard),gross=0,days=new Set(shifts.map(s=>attendanceDateOnly(s.clockIn?.time))).size;if(m.employmentType==='Hourly')gross=basicHours*Number(m.hourlyRate||0)+overtimeHours*Number(m.overtimeRate||m.hourlyRate||0);else if(m.employmentType==='Day Rate')gross=days*Number(m.dayRate||0);return {shifts,totalHours,basicHours,overtimeHours,gross,days}}
function attendanceExpectedMembers(siteId){let gangIds=new Set(db.items.filter(x=>x.site===siteId&&!db.sites.find(s=>s.id===x.site)?.archived&&x.gang).map(x=>x.gang));return attendanceAllMembers().filter(x=>x.member.activeForAttendance!==false&&(gangIds.has(x.gang.id)||attendanceActiveShift(x.member.id)?.siteId===siteId))}
function attendanceVisibleMembers(siteId){let all=attendanceExpectedMembers(siteId);if(attendanceTeam!=='all')all=all.filter(x=>x.gang.id===attendanceTeam);if(attendanceWorker)all=all.filter(x=>x.member.id===attendanceWorker);return all}
function attendanceVerificationBadge(point){let s=point?.status||'';if(s==='Verified')return '<span class="att-badge verified">✓ Location Verified</span>';if(s==='Supervisor Entry')return '<span class="att-badge supervisor">⚠ Supervisor Entry</span>';if(s==='Rejected')return '<span class="att-badge rejected">✕ Rejected</span>';return '<span class="att-badge neutral">—</span>'}
function attendanceWorkItemName(id){let x=db.items.find(x=>x.id===id);return x?x.ref:'Unallocated'}
function attendanceSecureContextOkay(){return !!window.isSecureContext||location.hostname==='localhost'||location.hostname==='127.0.0.1'}
function attendanceDeviceInfo(){return {userAgent:String(navigator.userAgent||'').slice(0,260),platform:String(navigator.userAgentData?.platform||navigator.platform||'').slice(0,80),language:String(navigator.language||'').slice(0,40),screen:`${screen.width||0}x${screen.height||0}`}}
function attendanceSessionId(){let k='sitesight_v173_demo_session';try{let v=localStorage.getItem(k);if(v)return v;v=(crypto.randomUUID?crypto.randomUUID():'sess-'+Date.now()+'-'+Math.random().toString(16).slice(2));localStorage.setItem(k,v);return v}catch(e){return 'session-unavailable'}}
function attendanceGetPosition(){return new Promise((resolve,reject)=>{if(!attendanceSecureContextOkay())return reject(new Error('Location verification requires the HTTPS SiteSight address (or localhost for desktop testing).'));if(!navigator.geolocation)return reject(new Error('This device does not provide browser location services.'));navigator.geolocation.getCurrentPosition(p=>resolve({latitude:p.coords.latitude,longitude:p.coords.longitude,accuracy:p.coords.accuracy}),e=>{let msg=e.code===1?'Location permission was denied. Allow precise location for SiteSight and try again.':e.code===2?'Your phone could not determine a reliable location. Move outdoors and try again.':'Location timed out. Move somewhere with a clearer GPS signal and try again.';reject(new Error(msg))},{enableHighAccuracy:true,timeout:18000,maximumAge:0})})}
function attendanceGeoStatusText(siteId){let c=attendanceSiteConfig(siteId);if(!c)return '';if(!attendanceLocationConfigured(siteId))return '<span class="att-site-location missing">⚠ Site location not configured</span>';return `<span class="att-site-location ok">✓ Geofence set · ${Math.round(c.radiusM||150)}m radius · max ${Math.round(c.maxAccuracyM||75)}m GPS accuracy${c.demoPreset?' · demo preset':''}</span>`}
function openAttendanceClock(action){let linked=attendanceLinkedMember();if(!linked)return toast('Your SiteSight account is not linked to a Team member. Ask a manager to link it first.');if(!hasPerm('clock_attendance'))return toast('You do not have Clock In / Out permission');let active=attendanceActiveShift(linked.member.id),isOut=action==='out';if(isOut&&!active)return toast('You are not currently clocked in');if(!isOut&&active)return toast('You are already clocked in');let sid=isOut?active.siteId:attendanceDefaultSiteId(),sites=activeSites();if(!sid&&sites.length)sid=sites[0].id;let c=attendanceSiteConfig(sid);modal=`<div class="modal"><div class="modalbox attendance-clock-modal"><span class="att-kicker">TIME & ATTENDANCE</span><h2>${isOut?'Clock Out':'Clock In'}</h2><p><b>${linked.member.name}</b> · ${linked.gang.name}</p>${!isOut?`<div class="field"><label>Site</label><select id="att-clock-site" class="select" onchange="attendanceRefreshClockLocationHint()">${sites.map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${s.name}</option>`).join('')}</select></div>`:`<div class="att-clock-current"><span>Clocked in</span><b>${new Date(active.clockIn.time).toLocaleString()}</b><small>${siteName(active.siteId)}</small></div>`}<div id="att-clock-location-hint" class="att-location-hint">${attendanceGeoStatusText(sid)}</div><div class="att-privacy-note"><b>Location confirmation</b><span>SiteSight requests your phone location only when you press Clock In or Clock Out. It does not track your movements continuously. The record stores distance from site, GPS accuracy, server time and device/session information.</span></div><div id="att-clock-progress" class="att-clock-progress"></div><button id="att-clock-action" class="btn ${isOut?'red':'green'} attendance-big-clock" onclick="attendanceDoClock('${isOut?'clock-out':'clock-in'}')">${isOut?'■ VERIFY LOCATION & CLOCK OUT':'▶ VERIFY LOCATION & CLOCK IN'}</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function attendanceRefreshClockLocationHint(){let sid=document.getElementById('att-clock-site')?.value||attendanceDefaultSiteId(),el=document.getElementById('att-clock-location-hint');if(el)el.innerHTML=attendanceGeoStatusText(sid)}
async function attendanceDoClock(action){let linked=attendanceLinkedMember();if(!linked)return;let active=attendanceActiveShift(linked.member.id),sid=action==='clock-out'?active?.siteId:(document.getElementById('att-clock-site')?.value||attendanceDefaultSiteId());if(!sid)return toast('Select a Site');if(!attendanceLocationConfigured(sid))return toast('This Site does not have a verified geofence yet. Ask a manager to configure Site Location.');if(!sharedMode||!navigator.onLine)return toast('Verified clocking requires an online SiteSight connection for server time and location validation.');let btn=document.getElementById('att-clock-action'),prog=document.getElementById('att-clock-progress');if(btn){btn.disabled=true;btn.textContent='Checking precise location…'}if(prog)prog.textContent='Your phone may take a few seconds to establish an accurate GPS position.';try{if(pendingSave)await syncToServer(true);let pos=await attendanceGetPosition();if(prog)prog.textContent=`GPS found · accuracy ±${Math.round(pos.accuracy)}m · validating against SiteSight…`;let payload={action,workerId:linked.member.id,siteId:sid,actorId:currentUser()?.id||'',latitude:pos.latitude,longitude:pos.longitude,accuracy:pos.accuracy,sessionId:attendanceSessionId(),device:attendanceDeviceInfo()};let r=await fetch('/api/attendance/stamp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}),result=await r.json();if(result.data){db=result.data;sharedRevision=result.revision||sharedRevision;lastSyncedDB=cloneDB(db);pendingSave=false;syncState='synced';lastSyncAt=new Date().toISOString();migrate(false);persistCache();try{localStorage.setItem(BASE_KEY,JSON.stringify(db));localStorage.removeItem(PENDING_KEY)}catch(e){}}modal=null;view='attendance';attendanceTab='today';render();if(result.ok)savedState(action==='clock-in'?'Clocked in — location verified':'Clocked out — location verified');else toast(result.message||'Clock attempt rejected')}catch(e){if(btn){btn.disabled=false;btn.textContent=action==='clock-in'?'▶ VERIFY LOCATION & CLOCK IN':'■ VERIFY LOCATION & CLOCK OUT'}if(prog)prog.textContent=e.message||'Location could not be verified';toast(e.message||'Location could not be verified')}}
function configureAttendanceLocation(siteId){if(!hasPerm('manage_attendance')&&!hasPerm('manage_sites'))return toast('Attendance management permission is required');let s=db.sites.find(s=>s.id===siteId),c=attendanceSiteConfig(siteId);if(!s)return;modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">SITE GEOFENCE</span><h2>${s.name}</h2><p class="muted">Set this once while physically standing on the development. Workers must be inside the permitted radius at Clock In and Clock Out.</p>${c.demoPreset?'<div class="att-demo-warning"><b>Demo preset location</b><span>For a real mobile test, replace the demo location below with your phone’s current position while you are at the test location.</span></div>':''}<div class="att-config-grid"><div class="field"><label>Allowed radius</label><select id="att-radius" class="select">${[50,75,100,150,200,250,300].map(v=>`<option value="${v}" ${Number(c.radiusM)===v?'selected':''}>${v} metres</option>`).join('')}</select></div><div class="field"><label>Maximum GPS uncertainty</label><select id="att-accuracy" class="select">${[20,30,50,75,100,150].map(v=>`<option value="${v}" ${Number(c.maxAccuracyM)===v?'selected':''}>±${v} metres</option>`).join('')}</select></div><div class="field"><label>Long-shift warning</label><select id="att-maxshift" class="select">${[10,11,12,13,14,16].map(v=>`<option value="${v}" ${Number(c.maxShiftHours)===v?'selected':''}>${v} hours</option>`).join('')}</select></div></div><div class="att-coordinate-box">${attendanceLocationConfigured(siteId)?`<b>Current Site Location</b><span>${Number(c.latitude).toFixed(6)}, ${Number(c.longitude).toFixed(6)}</span><small>Set ${c.locationSetAt?new Date(c.locationSetAt).toLocaleString():'—'} by ${c.locationSetBy||'—'}</small>`:'<b>No Site Location stored yet.</b>'}</div><div id="att-config-status" class="att-clock-progress"></div><button class="btn orange" onclick="attendanceSetSiteLocationNow('${siteId}')">⌖ Set Site Location to This Device</button> <button class="btn blue" onclick="attendanceSaveSiteRules('${siteId}')">Save rules only</button> <button class="btn light" onclick="modal=null;render()">Close</button><div class="att-privacy-note"><b>Important</b><span>Do not set the Site Location from home or the office. Use the device while physically at the development. SiteSight does not use continuous background GPS tracking.</span></div></div></div>`;render()}
async function attendanceSetSiteLocationNow(siteId){let status=document.getElementById('att-config-status');if(status)status.textContent='Requesting a precise GPS position…';try{let pos=await attendanceGetPosition(),c=attendanceSiteConfig(siteId);c.latitude=pos.latitude;c.longitude=pos.longitude;c.radiusM=Number(document.getElementById('att-radius')?.value||150);c.maxAccuracyM=Number(document.getElementById('att-accuracy')?.value||75);c.maxShiftHours=Number(document.getElementById('att-maxshift')?.value||12);c.locationSetAt=new Date().toISOString();c.locationSetBy=currentUser()?.name||'';c.demoPreset=false;save();audit('attendance','site',siteId,`Attendance geofence set — ${siteName(siteId)} · ${c.radiusM}m radius · max GPS accuracy ${c.maxAccuracyM}m`,siteId);savedState('Site location saved');configureAttendanceLocation(siteId)}catch(e){if(status)status.textContent=e.message;toast(e.message)}}
function attendanceSaveSiteRules(siteId){let c=attendanceSiteConfig(siteId);c.radiusM=Number(document.getElementById('att-radius')?.value||150);c.maxAccuracyM=Number(document.getElementById('att-accuracy')?.value||75);c.maxShiftHours=Number(document.getElementById('att-maxshift')?.value||12);save();audit('attendance','site',siteId,`Attendance rules updated — ${siteName(siteId)} · ${c.radiusM}m radius`,siteId);savedState('Attendance rules saved');configureAttendanceLocation(siteId)}
function editAttendanceMemberSettings(teamId,memberId){if(!hasPerm('manage_attendance'))return toast('Attendance management permission is required');let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!m)return;let financial=hasPerm('view_financials'),availableUsers=db.users.filter(u=>!u.linkedMemberId||u.linkedMemberId===m.id);modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">WORKER SETTINGS</span><h2>${m.name}</h2><p class="muted">${g.name} · ${m.role||'Team member'}</p><div class="two"><div class="field"><label>Time / pay type</label><select id="att-emp-type" class="select">${['Hourly','Day Rate','Subcontract','Salary','Not tracked'].map(v=>`<option ${m.employmentType===v?'selected':''}>${v}</option>`).join('')}</select></div><div class="field"><label>Default unpaid break</label><input id="att-break" class="input" type="number" min="0" max="180" step="5" value="${Number(m.defaultBreakMinutes||0)}"><small class="muted">minutes per shift</small></div></div>${financial?`<div class="two"><div class="field"><label>Hourly rate</label><input id="att-rate" class="input" type="number" min="0" step="0.01" value="${Number(m.hourlyRate||0)}"></div><div class="field"><label>Overtime rate</label><input id="att-ot-rate" class="input" type="number" min="0" step="0.01" value="${Number(m.overtimeRate||0)}"></div></div><div class="two"><div class="field"><label>Day rate</label><input id="att-day-rate" class="input" type="number" min="0" step="0.01" value="${Number(m.dayRate||0)}"></div><div class="field"><label>Standard weekly hours</label><input id="att-week-hours" class="input" type="number" min="1" max="80" step="0.5" value="${Number(m.standardWeekHours||40)}"></div></div><div class="field"><label>Standard day</label><input id="att-day-hours" class="input" type="number" min="1" max="16" step="0.5" value="${Number(m.standardDayHours||8)}"></div>`:'<div class="att-pay-hidden">Pay rates are hidden because your account does not include View Financials.</div>'}<div class="field"><label>Linked SiteSight user</label><select id="att-linked-user" class="select"><option value="">No self-clocking login</option>${availableUsers.map(u=>`<option value="${u.id}" ${m.linkedUserId===u.id||u.linkedMemberId===m.id?'selected':''}>${u.name} · @${u.username}</option>`).join('')}</select><small class="muted">A linked worker can clock only their own attendance using their SiteSight login.</small></div><label class="att-checkbox"><input id="att-active-worker" type="checkbox" ${m.activeForAttendance!==false?'checked':''}> Include this person in Time & Attendance</label><div style="margin-top:14px"><button class="btn orange" onclick="saveAttendanceMemberSettings('${teamId}','${memberId}')">Save settings</button> <button class="btn light" onclick="openTeamMember('${teamId}','${memberId}')">Cancel</button></div></div></div>`;render()}
function saveAttendanceMemberSettings(teamId,memberId){let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!m||!hasPerm('manage_attendance'))return;m.employmentType=document.getElementById('att-emp-type')?.value||'Not tracked';m.defaultBreakMinutes=Math.max(0,Number(document.getElementById('att-break')?.value||0));m.activeForAttendance=!!document.getElementById('att-active-worker')?.checked;if(hasPerm('view_financials')){m.hourlyRate=Math.max(0,Number(document.getElementById('att-rate')?.value||0));m.overtimeRate=Math.max(0,Number(document.getElementById('att-ot-rate')?.value||0));m.dayRate=Math.max(0,Number(document.getElementById('att-day-rate')?.value||0));m.standardWeekHours=Math.max(1,Number(document.getElementById('att-week-hours')?.value||40));m.standardDayHours=Math.max(1,Number(document.getElementById('att-day-hours')?.value||8))}let uid=document.getElementById('att-linked-user')?.value||'';db.users.forEach(u=>{if(u.linkedMemberId===m.id)u.linkedMemberId=''});attendanceAllMembers().forEach(x=>{if(x.member.linkedUserId===uid&&x.member.id!==m.id)x.member.linkedUserId=''});m.linkedUserId=uid;if(uid){let u=db.users.find(u=>u.id===uid);if(u)u.linkedMemberId=m.id}save();audit('attendance','team member',m.id,`${m.name}: Time & Attendance settings updated`,attendanceDefaultSiteId());savedState('Attendance settings saved');openTeamMember(teamId,memberId)}
async function attendanceServerNow(){if(sharedMode&&navigator.onLine)try{let r=await fetch('/api/health',{cache:'no-store'}),j=await r.json();if(j.serverTime)return {time:j.serverTime,source:'Server'}}catch(e){}return {time:new Date().toISOString(),source:'Device fallback'}}
function supervisorAttendanceClock(workerId,action,siteId){if(!hasPerm('manage_attendance'))return toast('Attendance management permission is required');let wm=attendanceMemberById(workerId),active=attendanceActiveShift(workerId),isOut=action==='out';if(!wm)return;if(isOut&&!active)return toast('This person is not clocked in');if(!isOut&&active)return toast('This person is already clocked in');let sid=isOut?active.siteId:(siteId||attendanceDefaultSiteId());modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">SUPERVISOR ENTRY</span><h2>${isOut?'Clock Out':'Clock In'} — ${wm.member.name}</h2><p class="muted">This is a manual management action. It will be labelled <b>Supervisor Entry</b>, not Location Verified.</p>${!isOut?`<div class="field"><label>Site</label><select id="att-super-site" class="select">${activeSites().map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${s.name}</option>`).join('')}</select></div>`:''}<div class="field"><label>Reason</label><textarea id="att-super-reason" class="textarea" placeholder="e.g. Worker has no phone / phone battery flat / supervisor clocking gang in"></textarea></div><button class="btn orange" onclick="saveSupervisorAttendanceClock('${workerId}','${action}')">Save Supervisor Entry</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
async function saveSupervisorAttendanceClock(workerId,action){let wm=attendanceMemberById(workerId),reason=(document.getElementById('att-super-reason')?.value||'').trim();if(!wm||!reason)return toast('Enter a reason for the Supervisor Entry');let stamp=await attendanceServerNow(),active=attendanceActiveShift(workerId),sid=action==='out'?active?.siteId:(document.getElementById('att-super-site')?.value||attendanceDefaultSiteId());if(action==='in'){db.attendanceShifts.push({id:'ats'+Date.now()+Math.random().toString(16).slice(2),workerId,gangId:wm.gang.id,siteId:sid,clockIn:{time:stamp.time,status:'Supervisor Entry',by:currentUser()?.name||'',reason,timeSource:stamp.source},clockOut:null,breakMinutes:Number(wm.member.defaultBreakMinutes||0),source:'Supervisor Entry',corrections:[],createdAt:stamp.time,createdBy:currentUser()?.name||''})}else{active.clockOut={time:stamp.time,status:'Supervisor Entry',by:currentUser()?.name||'',reason,timeSource:stamp.source}}attendanceMarkApprovalNeedsRecheck(workerId,sid,stamp.time);save();audit('attendance','team member',workerId,`${wm.member.name}: supervisor ${action==='in'?'clock in':'clock out'} — ${reason}`,sid);modal=null;savedState('Supervisor Entry saved');render()}
function addAttendanceAbsence(workerId,siteId,date=iso()){if(!hasPerm('manage_attendance'))return toast('Attendance management permission is required');let wm=attendanceMemberById(workerId);if(!wm)return;modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">ABSENCE</span><h2>${wm.member.name}</h2><div class="two"><div class="field"><label>Date</label><input id="att-absence-date" class="input" type="date" value="${date}"></div><div class="field"><label>Type</label><select id="att-absence-type" class="select"><option>Sick</option><option>Holiday</option><option>Absent</option><option>Training</option><option>Other</option></select></div></div><div class="field"><label>Notes</label><textarea id="att-absence-notes" class="textarea"></textarea></div><button class="btn orange" onclick="saveAttendanceAbsence('${workerId}','${siteId}')">Save absence</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveAttendanceAbsence(workerId,siteId){let date=document.getElementById('att-absence-date')?.value||'',type=document.getElementById('att-absence-type')?.value||'Absent',notes=(document.getElementById('att-absence-notes')?.value||'').trim(),wm=attendanceMemberById(workerId);if(!date||!wm)return toast('Choose a date');let old=attendanceAbsence(workerId,date,siteId);if(old){old.type=type;old.notes=notes;old.updatedAt=new Date().toISOString();old.updatedBy=currentUser()?.name||''}else db.attendanceAbsences.push({id:'aba'+Date.now(),workerId,siteId,date,type,notes,createdAt:new Date().toISOString(),by:currentUser()?.name||''});attendanceMarkApprovalNeedsRecheck(workerId,siteId,date+'T12:00:00');save();audit('attendance','absence',workerId,`${wm.member.name}: ${type} recorded for ${date}`,siteId);modal=null;savedState('Absence saved');render()}
function attendanceMarkApprovalNeedsRecheck(workerId,siteId,time){let start=attendanceWeekStartISO(new Date(time||Date.now()));(db.timesheetApprovals||[]).forEach(a=>{if(a.workerId===workerId&&a.siteId===siteId&&a.weekStart===start&&!a.superseded){a.needsReapproval=true}})}
function editAttendanceShift(shiftId){if(!hasPerm('manage_attendance'))return toast('Attendance management permission is required');let s=db.attendanceShifts.find(s=>s.id===shiftId),wm=attendanceMemberById(s?.workerId);if(!s||!wm)return;let approved=attendanceApproval(s.workerId,s.siteId,attendanceWeekStartISO(new Date(s.clockIn.time)));modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">TIMESHEET CORRECTION</span><h2>${wm.member.name}</h2>${approved?'<div class="att-approved-warning">This week has an approval record. Saving a correction will mark it for re-approval.</div>':''}<div class="two"><div class="field"><label>Clock In</label><input id="att-edit-in" class="input" type="datetime-local" value="${attendanceDateTimeLocalValue(s.clockIn?.time)}"></div><div class="field"><label>Clock Out</label><input id="att-edit-out" class="input" type="datetime-local" value="${attendanceDateTimeLocalValue(s.clockOut?.time)}"></div></div><div class="field"><label>Unpaid break minutes</label><input id="att-edit-break" class="input" type="number" min="0" max="240" step="5" value="${Number(s.breakMinutes||0)}"></div><label class="att-checkbox"><input id="att-edit-void" type="checkbox" ${s.voided?'checked':''}> Void this shift from hour/pay totals</label><div class="field"><label>Reason for correction <b>*</b></label><textarea id="att-edit-reason" class="textarea" placeholder="Explain exactly why the original record needs changing"></textarea></div>${(s.corrections||[]).length?`<details class="att-correction-history"><summary>Previous corrections (${s.corrections.length})</summary>${s.corrections.slice().reverse().map(c=>`<div><b>${new Date(c.at).toLocaleString()} · ${c.by}</b><span>${c.reason}</span></div>`).join('')}</details>`:''}<button class="btn orange" onclick="saveAttendanceShiftCorrection('${s.id}')">Save correction</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveAttendanceShiftCorrection(shiftId){let s=db.attendanceShifts.find(s=>s.id===shiftId),wm=attendanceMemberById(s?.workerId),reason=(document.getElementById('att-edit-reason')?.value||'').trim();if(!s||!wm)return;if(!reason)return toast('A correction reason is required');let inTime=attendanceLocalIsoFromInput(document.getElementById('att-edit-in')?.value||''),outTime=attendanceLocalIsoFromInput(document.getElementById('att-edit-out')?.value||'');if(!inTime)return toast('Clock In time is required');if(outTime&&new Date(outTime)<new Date(inTime))return toast('Clock Out cannot be before Clock In');let previous={clockIn:s.clockIn?.time||'',clockOut:s.clockOut?.time||'',breakMinutes:Number(s.breakMinutes||0),voided:!!s.voided};s.clockIn={...(s.clockIn||{}),time:inTime};s.clockOut=outTime?{...(s.clockOut||{}),time:outTime}:null;s.breakMinutes=Math.max(0,Number(document.getElementById('att-edit-break')?.value||0));s.voided=!!document.getElementById('att-edit-void')?.checked;s.corrections=s.corrections||[];let updated={clockIn:inTime,clockOut:outTime,breakMinutes:s.breakMinutes,workItemId:s.workItemId,voided:s.voided};s.corrections.push({id:'cor'+Date.now(),at:new Date().toISOString(),by:currentUser()?.name||'',userId:currentUser()?.id||'',reason,previous,updated});attendanceMarkApprovalNeedsRecheck(s.workerId,s.siteId,inTime);save();audit('attendance','timesheet',s.id,`${wm.member.name}: timesheet corrected — ${reason}`,s.siteId,JSON.stringify({previous,updated}));modal=null;savedState('Correction saved — approval review required');render()}
function addHistoricalAttendance(workerId,siteId){if(!hasPerm('manage_attendance'))return;let wm=attendanceMemberById(workerId),date=attendanceWeekStart||attendanceWeekStartISO(new Date());if(!wm)return;modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">SUPERVISOR TIME ENTRY</span><h2>${wm.member.name}</h2><p class="muted">Use only when a worker could not self-clock. The entry is permanently labelled Supervisor Entry.</p><div class="two"><div class="field"><label>Clock In</label><input id="att-hist-in" class="input" type="datetime-local" value="${date}T07:30"></div><div class="field"><label>Clock Out</label><input id="att-hist-out" class="input" type="datetime-local" value="${date}T16:30"></div></div><div class="two"><div class="field"><label>Break minutes</label><input id="att-hist-break" class="input" type="number" value="${Number(wm.member.defaultBreakMinutes||0)}"></div><div class="field"><label>Work Item</label><select id="att-hist-work" class="select"><option value="">No specific Work Item</option>${db.items.filter(x=>x.site===siteId).map(x=>`<option value="${x.id}">${x.ref}</option>`).join('')}</select></div></div><div class="field"><label>Reason <b>*</b></label><textarea id="att-hist-reason" class="textarea" placeholder="e.g. Worker had no phone available"></textarea></div><button class="btn orange" onclick="saveHistoricalAttendance('${workerId}','${siteId}')">Save Supervisor Entry</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveHistoricalAttendance(workerId,siteId){let wm=attendanceMemberById(workerId),reason=(document.getElementById('att-hist-reason')?.value||'').trim(),tin=attendanceLocalIsoFromInput(document.getElementById('att-hist-in')?.value||''),tout=attendanceLocalIsoFromInput(document.getElementById('att-hist-out')?.value||'');if(!wm||!tin||!tout)return toast('Enter Clock In and Clock Out times');if(!reason)return toast('A reason is required');if(new Date(tout)<=new Date(tin))return toast('Clock Out must be after Clock In');let status={status:'Supervisor Entry',by:currentUser()?.name||'',reason,timeSource:'Manual correction'};db.attendanceShifts.push({id:'ats'+Date.now(),workerId,gangId:wm.gang.id,siteId,workItemId:document.getElementById('att-hist-work')?.value||'',clockIn:{...status,time:tin},clockOut:{...status,time:tout},breakMinutes:Math.max(0,Number(document.getElementById('att-hist-break')?.value||0)),source:'Supervisor Entry',corrections:[],createdAt:new Date().toISOString(),createdBy:currentUser()?.name||''});attendanceMarkApprovalNeedsRecheck(workerId,siteId,tin);save();audit('attendance','timesheet',workerId,`${wm.member.name}: supervisor time entry added — ${reason}`,siteId);modal=null;savedState('Supervisor time entry saved');render()}
function attendanceApproveWorkerWeek(workerId,siteId,start){if(!hasPerm('approve_timesheets'))return toast('Timesheet approval permission is required');let wm=attendanceMemberById(workerId);if(!wm)return;let totals=attendanceWeekTotals(workerId,start,siteId),old=attendanceApproval(workerId,siteId,start);if(old)old.superseded=true;db.timesheetApprovals.push({id:'appr'+Date.now()+Math.random().toString(16).slice(2),workerId,siteId,weekStart:start,approvedBy:currentUser()?.name||'',approvedById:currentUser()?.id||'',approvedAt:new Date().toISOString(),totalHours:Number(totals.totalHours.toFixed(4)),basicHours:Number(totals.basicHours.toFixed(4)),overtimeHours:Number(totals.overtimeHours.toFixed(4)),gross:Number(totals.gross.toFixed(2)),needsReapproval:false,superseded:false});save();audit('attendance','timesheet approval',workerId,`${wm.member.name}: week ${start} approved — ${totals.totalHours.toFixed(2)} hrs`,siteId);savedState(`${wm.member.name} approved`);render()}
function attendanceApproveAllWeek(){if(!hasPerm('approve_timesheets'))return;let sid=attendanceResolvedSite(),members=attendanceVisibleMembers(sid).filter(x=>x.member.employmentType!=='Not tracked'),start=attendanceResolvedWeekStart();if(!members.length)return toast('No attendance workers in this view');if(!confirm(`Approve ${members.length} timesheet${members.length===1?'':'s'} for ${attendanceWeekLabel(start)}?`))return;members.forEach(x=>{let old=attendanceApproval(x.member.id,sid,start);if(old)old.superseded=true;let t=attendanceWeekTotals(x.member.id,start,sid);db.timesheetApprovals.push({id:'appr'+Date.now()+Math.random().toString(16).slice(2),workerId:x.member.id,siteId:sid,weekStart:start,approvedBy:currentUser()?.name||'',approvedById:currentUser()?.id||'',approvedAt:new Date().toISOString(),totalHours:Number(t.totalHours.toFixed(4)),basicHours:Number(t.basicHours.toFixed(4)),overtimeHours:Number(t.overtimeHours.toFixed(4)),gross:Number(t.gross.toFixed(2)),needsReapproval:false,superseded:false})});save();audit('attendance','timesheet approval',start,`${members.length} weekly timesheets approved — ${attendanceWeekLabel(start)}`,sid);savedState('Weekly timesheets approved');render()}
function attendanceResolvedSite(){let allowed=activeSites();if(attendanceSite!=='all'&&allowed.some(s=>s.id===attendanceSite))return attendanceSite;if(site!=='all'&&allowed.some(s=>s.id===site))return site;attendanceSite=allowed[0]?.id||'all';return attendanceSite}
function attendanceResolvedWeekStart(){if(!attendanceWeekStart)attendanceWeekStart=attendanceWeekStartISO(new Date());return attendanceWeekStart}
function attendanceSetWeek(delta){let start=attendanceResolvedWeekStart();attendanceWeekStart=attendanceAddDays(start,delta*7);render()}
function attendanceSetSite(v){attendanceSite=v;attendanceWorker='';render()}
function attendanceTodayCard(){let linked=attendanceLinkedMember();if(!linked||!hasPerm('clock_attendance'))return '';let active=attendanceActiveShift(linked.member.id);return `<div class="panel att-my-clock"><div><span class="att-kicker">MY TIME</span><h2>${active?'You are clocked in':'Ready to start?'}</h2><p>${linked.member.name} · ${linked.gang.name}${active?`<br>${siteName(active.siteId)} · since ${new Date(active.clockIn.time).toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit'})}`:''}</p></div><button class="btn ${active?'red':'green'} attendance-big-clock" onclick="openAttendanceClock('${active?'out':'in'}')">${active?'■ CLOCK OUT':'▶ CLOCK IN'}</button></div>`}
function attendanceTodayView(siteId){let today=iso(),expected=attendanceVisibleMembers(siteId),active=expected.map(x=>({...x,shift:attendanceActiveShift(x.member.id)})).filter(x=>x.shift?.siteId===siteId),cfg=attendanceSiteConfig(siteId),absent=expected.filter(x=>attendanceAbsence(x.member.id,today,siteId)),notIn=expected.filter(x=>!attendanceActiveShift(x.member.id)&&!attendanceAbsence(x.member.id,today,siteId)),warnings=active.filter(x=>attendanceShiftHours(x.shift)>Number(cfg?.maxShiftHours||12)),attempts=(db.attendanceAttempts||[]).filter(a=>a.siteId===siteId&&attendanceDateOnly(a.time)===today).sort((a,b)=>String(b.time).localeCompare(String(a.time)));return `${attendanceTodayCard()}<div class="att-site-config panel"><div><span class="att-kicker">SITE LOCATION</span><h3>${siteName(siteId)}</h3>${attendanceGeoStatusText(siteId)}</div>${hasPerm('manage_attendance')||hasPerm('manage_sites')?`<button class="btn light" onclick="configureAttendanceLocation('${siteId}')">⌖ Configure Location</button>`:''}</div><div class="att-metrics"><div class="card"><span>CLOCKED IN</span><b>${active.length}</b></div><div class="card"><span>LOCATION VERIFIED</span><b>${active.filter(x=>x.shift.clockIn?.status==='Verified').length}</b></div><div class="card"><span>ABSENT / LEAVE</span><b>${absent.length}</b></div><div class="card ${warnings.length?'warn':''}"><span>LONG-SHIFT WARNINGS</span><b>${warnings.length}</b></div></div><div class="panel"><div class="sectionhead"><div><h2>Who is on site now</h2><div class="muted">Live workforce register · ${siteName(siteId)}</div></div><button class="btn light smallbtn" onclick="printAttendanceRegister('${siteId}')">⇩ Emergency Register / Print</button></div><div class="att-live-list">${active.length?active.map(x=>{let h=attendanceShiftHours(x.shift),warn=h>Number(cfg?.maxShiftHours||12);return `<div class="att-live-row ${warn?'warning':''}"><div><b>${x.member.name}</b><span>${x.gang.name}${x.shift.workItemId?' · '+attendanceWorkItemName(x.shift.workItemId):''}</span></div><div><b>${new Date(x.shift.clockIn.time).toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit'})}</b><span>${attendanceFmtHours(h)}${warn?' · ⚠ possible missed clock-out':''}</span></div><div>${attendanceVerificationBadge(x.shift.clockIn)}${x.shift.clockIn?.distanceM!==undefined?`<small>${Math.round(x.shift.clockIn.distanceM)}m from Site · GPS ±${Math.round(x.shift.clockIn.accuracy||0)}m</small>`:''}</div>${hasPerm('manage_attendance')?`<button class="btn light smallbtn" onclick="supervisorAttendanceClock('${x.member.id}','out','${siteId}')">Supervisor Clock Out</button>`:''}</div>`}).join(''):'<div class="empty-help"><b>No one is currently clocked in.</b><div style="margin-top:6px">Verified and Supervisor Entry clock-ins will appear here immediately.</div></div>'}</div></div>${hasPerm('manage_attendance')?`<div class="panel"><div class="sectionhead"><div><h2>Expected workforce</h2><div class="muted">People not currently clocked in</div></div></div><div class="att-notin-grid">${notIn.map(x=>`<div class="att-notin-card"><div><b>${x.member.name}</b><span>${x.gang.name} · ${x.member.role||'Team member'}</span></div><div><button class="btn blue smallbtn" onclick="supervisorAttendanceClock('${x.member.id}','in','${siteId}')">Supervisor Clock In</button><button class="btn light smallbtn" onclick="addAttendanceAbsence('${x.member.id}','${siteId}','${today}')">Mark absence</button></div></div>`).join('')||'<div class="muted">Everyone expected is accounted for.</div>'}</div>${absent.length?`<div class="att-absence-strip">${absent.map(x=>{let a=attendanceAbsence(x.member.id,today,siteId);return `<span><b>${x.member.name}</b> · ${a.type}</span>`}).join('')}</div>`:''}</div>`:''}${attempts.length&&attendanceCanViewAll()?`<div class="panel"><div class="sectionhead"><h3>Rejected clock attempts today</h3><span class="badge b-red">${attempts.length}</span></div><div class="att-attempt-list">${attempts.slice(0,8).map(a=>{let wm=attendanceMemberById(a.workerId);return `<div><span><b>${wm?.member.name||'Worker'}</b> · ${a.action==='clock-in'?'Clock In':'Clock Out'} · ${new Date(a.time).toLocaleTimeString()}</span><strong>${a.reason}</strong><small>${Number.isFinite(Number(a.distanceM))?Math.round(a.distanceM)+'m from Site · ':''}GPS ±${Math.round(a.accuracy||0)}m</small></div>`}).join('')}</div></div>`:''}`}
function attendanceTimesheetView(siteId){let start=attendanceResolvedWeekStart(),dates=attendanceWeekDates(start),members=attendanceVisibleMembers(siteId),financial=hasPerm('view_financials');return `<div class="panel att-week-head"><button class="btn light" onclick="attendanceSetWeek(-1)">← Previous</button><div><span class="att-kicker">WEEK</span><h2>${attendanceWeekLabel(start)}</h2></div><button class="btn light" onclick="attendanceSetWeek(1)">Next →</button></div><div class="att-export-row"><button class="btn light" onclick="downloadAttendanceCSV()">⇩ CSV</button><button class="btn light" onclick="printAttendanceTimesheet()">▣ Print / Save PDF</button>${hasPerm('approve_timesheets')?'<button class="btn orange" onclick="attendanceApproveAllWeek()">✓ Approve visible week</button>':''}</div><div class="panel att-table-wrap"><table class="att-timesheet-table"><thead><tr><th>Person</th>${dates.map(d=>`<th>${new Date(d+'T12:00:00').toLocaleDateString('en-GB',{weekday:'short'})}<small>${d.slice(8)}</small></th>`).join('')}<th>Total</th><th>OT</th>${financial?'<th>Est. Pay</th>':''}<th>Approval</th></tr></thead><tbody>${members.map(x=>{let t=attendanceWeekTotals(x.member.id,start,siteId),approval=attendanceApproval(x.member.id,siteId,start);return `<tr onclick="openAttendanceWorkerWeek('${x.member.id}','${siteId}','${start}')"><td><b>${x.member.name}</b><small>${x.gang.name} · ${x.member.employmentType}</small></td>${dates.map(d=>{let ds=t.shifts.filter(s=>attendanceDateOnly(s.clockIn.time)===d),h=ds.reduce((n,s)=>n+attendanceShiftHours(s),0),a=attendanceAbsence(x.member.id,d,siteId);return `<td>${h?`<b>${h.toFixed(2)}</b>${ds.some(s=>s.clockIn?.status!=='Verified'||s.clockOut&&s.clockOut.status!=='Verified')?'<small>⚠ manual</small>':''}`:a?`<span class="att-absence-cell">${a.type.slice(0,1)}</span>`:'—'}</td>`}).join('')}<td><b>${t.totalHours.toFixed(2)}</b></td><td>${t.overtimeHours.toFixed(2)}</td>${financial?`<td><b>${x.member.employmentType==='Hourly'||x.member.employmentType==='Day Rate'?attendanceMoney(t.gross):'—'}</b></td>`:''}<td>${approval?(approval.needsReapproval?'<span class="att-badge rejected">⚠ Re-approve</span>':`<span class="att-badge verified">✓ Approved</span><small>${approval.approvedBy}<br>${new Date(approval.approvedAt).toLocaleString()}</small>`):'<span class="att-badge neutral">Awaiting</span>'}</td></tr>`}).join('')||'<tr><td colspan="12">No workers in this view.</td></tr>'}</tbody></table></div><div class="att-timesheet-key"><span><b>H</b> Holiday</span><span><b>S</b> Sick</span><span><b>A</b> Absent</span><span>⚠ manual = Supervisor Entry or corrected time</span></div>`}
function openAttendanceWorkerWeek(workerId,siteId,start){if(!v1998CanReviewTimeRecords())return toast('A Director must grant this account shift-review permission');let wm=attendanceMemberById(workerId),t=attendanceWeekTotals(workerId,start,siteId),approval=attendanceApproval(workerId,siteId,start),financial=hasPerm('view_financials');if(!wm)return;modal=`<div class="modal"><div class="modalbox att-worker-week"><span class="att-kicker">WEEKLY TIMESHEET</span><h2>${wm.member.name}</h2><p class="muted">${wm.gang.name} · ${attendanceWeekLabel(start)}</p><div class="attendance-member-mini"><div><span>TOTAL</span><b>${t.totalHours.toFixed(2)} hrs</b></div><div><span>OVERTIME</span><b>${t.overtimeHours.toFixed(2)} hrs</b></div>${financial?`<div><span>EST. GROSS</span><b>${attendanceMoney(t.gross)}</b></div>`:''}</div>${approval?`<div class="att-approval-card ${approval.needsReapproval?'warning':''}"><b>${approval.needsReapproval?'⚠ Correction made after approval':'✓ Approved'}</b><span>${approval.approvedBy} · ${new Date(approval.approvedAt).toLocaleString()}</span></div>`:''}<div class="att-shift-list">${t.shifts.map(s=>`<div class="att-shift-row ${s.voided?'voided':''}"><div><b>${new Date(s.clockIn.time).toLocaleDateString('en-GB',{weekday:'short',day:'numeric',month:'short'})}</b><span>${new Date(s.clockIn.time).toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit'})} → ${s.clockOut?.time?new Date(s.clockOut.time).toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit'}):'Still clocked in'} · ${s.breakMinutes||0}m break</span><small>${attendanceFmtHours(attendanceShiftHours(s))}${s.clockIn?.status==='Verified'?` · ${Math.round(s.clockIn.distanceM||0)}m from Site · GPS ±${Math.round(s.clockIn.accuracy||0)}m · ${s.clockIn.device?.platform||'device'}`:''}</small></div><div>${attendanceVerificationBadge(s.clockIn)}${s.clockOut?attendanceVerificationBadge(s.clockOut):''}</div>${hasPerm('manage_attendance')?`<button class="btn light smallbtn" onclick="editAttendanceShift('${s.id}')">Correction</button>`:''}</div>`).join('')||'<div class="muted">No shifts recorded in this week.</div>'}</div><div class="actions" style="margin-top:14px">${hasPerm('manage_attendance')?`<button class="btn light" onclick="addHistoricalAttendance('${workerId}','${siteId}')">＋ Supervisor Time Entry</button>`:''}${hasPerm('approve_timesheets')?`<button class="btn orange" onclick="modal=null;attendanceApproveWorkerWeek('${workerId}','${siteId}','${start}')">✓ ${approval?'Re-approve':'Approve'} week</button>`:''}<button class="btn light" onclick="modal=null;render()">Close</button></div></div></div>`;render()}
function attendancePaySummaryView(siteId){if(!hasPerm('view_financials'))return `<div class="panel empty-help"><b>Pay Summary is restricted.</b><div style="margin-top:6px">Your account can review hours only. Pay rates and estimated gross amounts require View Financials.</div></div>`;let start=attendanceResolvedWeekStart(),members=attendanceVisibleMembers(siteId),rows=members.map(x=>({...x,t:attendanceWeekTotals(x.member.id,start,siteId)})),totalHours=rows.reduce((n,x)=>n+x.t.totalHours,0),gross=rows.reduce((n,x)=>n+x.t.gross,0),teams=[...new Set(rows.map(x=>x.gang.id))].map(id=>{let a=rows.filter(x=>x.gang.id===id);return {gang:db.gangs.find(g=>g.id===id),hours:a.reduce((n,x)=>n+x.t.totalHours,0),gross:a.reduce((n,x)=>n+x.t.gross,0)}}),workMap=new Map();rows.forEach(x=>x.t.shifts.forEach(s=>{let k=s.workItemId||'unallocated',v=workMap.get(k)||{hours:0,gross:0};let h=attendanceShiftHours(s),rate=x.member.employmentType==='Hourly'?Number(x.member.hourlyRate||0):0;v.hours+=h;v.gross+=h*rate;workMap.set(k,v)}));return `<div class="att-pay-disclaimer"><b>Estimated labour due</b><span>SiteSight multiplies recorded attendance by the saved worker rates. This is a management estimate, not payroll, tax or statutory wage processing.</span></div><div class="att-metrics"><div class="card"><span>RECORDED HOURS</span><b>${totalHours.toFixed(2)}</b></div><div class="card"><span>EST. GROSS DUE</span><b>${attendanceMoney(gross)}</b></div><div class="card"><span>WORKERS</span><b>${rows.length}</b></div><div class="card"><span>WEEK</span><b>${attendanceWeekLabel(start)}</b></div></div><div class="att-pay-grid"><div class="panel"><div class="sectionhead"><h2>People</h2><span>${siteName(siteId)}</span></div><div class="att-pay-list">${rows.map(x=>`<div class="att-pay-row"><div><b>${x.member.name}</b><span>${x.gang.name} · ${x.member.employmentType}</span></div><div><b>${x.t.totalHours.toFixed(2)} hrs</b><span>${x.member.employmentType==='Hourly'?`${attendanceMoney(x.member.hourlyRate)}/hr · OT ${attendanceMoney(x.member.overtimeRate)}/hr`:x.member.employmentType==='Day Rate'?`${attendanceMoney(x.member.dayRate)}/day`:'No automatic pay calc'}</span></div><strong>${x.member.employmentType==='Hourly'||x.member.employmentType==='Day Rate'?attendanceMoney(x.t.gross):'—'}</strong>${hasPerm('manage_attendance')?`<button class="btn light smallbtn" onclick="editAttendanceMemberSettings('${x.gang.id}','${x.member.id}')">Rates</button>`:''}</div>`).join('')}</div></div><div class="panel"><div class="sectionhead"><h2>By Team</h2></div><div class="att-pay-list">${teams.map(t=>`<div class="att-pay-row compact"><div><b>${t.gang?.name||'Team'}</b></div><div><b>${t.hours.toFixed(2)} hrs</b></div><strong>${attendanceMoney(t.gross)}</strong></div>`).join('')}</div></div></div><div class="panel"><div class="sectionhead"><h2>Hours by primary Work Item</h2><span class="muted">Based on the optional Work Item chosen when clocking in or corrected by management.</span></div><div class="att-work-hours">${[...workMap.entries()].sort((a,b)=>b[1].hours-a[1].hours).map(([id,v])=>`<div><span><b>${id==='unallocated'?'Unallocated':attendanceWorkItemName(id)}</b></span><strong>${v.hours.toFixed(2)} hrs</strong><span>${attendanceMoney(v.gross)}</span></div>`).join('')||'<div class="muted">No Work Item allocations for this week.</div>'}</div></div>`}
function attendanceMyTimeOnlyView(siteId,linked){if(!linked)return `<div class="panel empty-help"><b>Your login is not linked to a Team member.</b><div style="margin-top:6px">Ask a SiteSight manager to link your user account to your Team member record before using Clock In / Out.</div></div>`;let start=attendanceWeekStartISO(new Date()),t=attendanceWeekTotals(linked.member.id,start,siteId),active=attendanceActiveShift(linked.member.id);return `${attendanceTodayCard()}<div class="panel"><div class="sectionhead"><div><h2>My week</h2><div class="muted">${attendanceWeekLabel(start)} · ${siteName(siteId)}</div></div><b>${t.totalHours.toFixed(2)} hrs</b></div><div class="att-shift-list">${t.shifts.slice().reverse().map(s=>`<div class="att-shift-row"><div><b>${new Date(s.clockIn.time).toLocaleDateString('en-GB',{weekday:'short',day:'numeric',month:'short'})}</b><span>${new Date(s.clockIn.time).toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit'})} → ${s.clockOut?.time?new Date(s.clockOut.time).toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit'}):'Still clocked in'} · ${s.breakMinutes||0}m unpaid break</span><small>${attendanceFmtHours(attendanceShiftHours(s))}${s.clockIn?.status==='Verified'?` · ${Math.round(s.clockIn.distanceM||0)}m from Site · GPS ±${Math.round(s.clockIn.accuracy||0)}m · ${s.clockIn.device?.platform||'device'}`:''}</small></div><div>${attendanceVerificationBadge(s.clockIn)}${s.clockOut?attendanceVerificationBadge(s.clockOut):''}</div></div>`).join('')||'<div class="muted">No hours recorded this week.</div>'}</div></div><div class="att-privacy-note"><b>Your privacy</b><span>SiteSight checks location only when you deliberately press Clock In or Clock Out. There is no continuous location tracking. Management can see the attendance record, its verification status, server time, distance from Site and GPS accuracy.</span></div>`}
function attendancePage(){if(!hasPerm('view_attendance')&&!hasPerm('clock_attendance'))return `<div class="panel empty-help"><b>You do not have Time & Attendance access.</b></div>`;let sid=attendanceResolvedSite(),full=attendanceCanViewAll(),linked=attendanceLinkedMember();if(!full&&linked){attendanceTab='today';sid=attendanceActiveShift(linked.member.id)?.siteId||attendanceDefaultSiteId();attendanceSite=sid}let tabs=full?`<div class="att-tabs"><button class="${attendanceTab==='today'?'active':''}" onclick="attendanceTab='today';render()">Today</button><button class="${attendanceTab==='timesheets'?'active':''}" onclick="attendanceTab='timesheets';render()">Timesheets</button>${hasPerm('view_financials')?`<button class="${attendanceTab==='pay'?'active':''}" onclick="attendanceTab='pay';render()">Pay Summary</button>`:''}</div>`:'';let body=!full?attendanceMyTimeOnlyView(sid,linked):attendanceTab==='timesheets'?attendanceTimesheetView(sid):attendanceTab==='pay'?attendancePaySummaryView(sid):attendanceTodayView(sid);return `<div class="topbar"><div><h1>${full?'Time & Attendance':'My Time'}</h1><div class="sub">${full?'Verified hours, live workforce and weekly approval':'Clock in and out with Site location confirmation'}</div></div><div class="actions">${full?`<select class="select" onchange="attendanceSetSite(this.value)">${activeSites().map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${s.name}</option>`).join('')}</select><select class="select" onchange="attendanceTeam=this.value;attendanceWorker='';render()"><option value="all">All Teams</option>${db.gangs.map(g=>`<option value="${g.id}" ${attendanceTeam===g.id?'selected':''}>${g.name}</option>`).join('')}</select>${attendanceWorker?`<button class="btn light" onclick="attendanceWorker='';render()">All workers ×</button>`:''}`:''}</div></div>${tabs}${body}`}
function downloadAttendanceCSV(){let sid=attendanceResolvedSite(),start=attendanceResolvedWeekStart(),members=attendanceVisibleMembers(sid),financial=hasPerm('view_financials'),rows=[['SiteSight Time & Attendance'],['Site',siteName(sid)],['Week',attendanceWeekLabel(start)],[],['Person','Team','Employment Type','Date','Clock In','Clock Out','Break Minutes','Hours','Status',...(financial?['Basic Hours','Overtime Hours','Est. Gross']:[])]],esc=v=>'"'+String(v??'').replace(/"/g,'""')+'"';members.forEach(x=>{let totals=attendanceWeekTotals(x.member.id,start,sid);totals.shifts.forEach(s=>rows.push([x.member.name,x.gang.name,x.member.employmentType,attendanceDateOnly(s.clockIn.time),new Date(s.clockIn.time).toLocaleTimeString(),s.clockOut?.time?new Date(s.clockOut.time).toLocaleTimeString():'Still clocked in',s.breakMinutes||0,attendanceShiftHours(s).toFixed(2),s.clockIn?.status||'',...(financial?[totals.basicHours.toFixed(2),totals.overtimeHours.toFixed(2),totals.gross.toFixed(2)]:[])]))});let blob=new Blob([rows.map(r=>r.map(esc).join(',')).join('\\r\\n')],{type:'text/csv;charset=utf-8'}),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`sitesight-timesheet-${start}.csv`;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
function printAttendanceRegister(siteId){let active=attendanceExpectedMembers(siteId).map(x=>({...x,shift:attendanceActiveShift(x.member.id)})).filter(x=>x.shift?.siteId===siteId),html=`<!doctype html><html><head><title>SiteSight Emergency Register</title><style>body{font:14px Arial;padding:28px;color:#17384b}h1{margin-bottom:3px}table{width:100%;border-collapse:collapse;margin-top:20px}th,td{padding:9px;border:1px solid #ccd7dd;text-align:left}th{background:#eef3f6}.ok{font-weight:bold}</style></head><body><h1>SiteSight — Who is on Site</h1><p>${siteName(siteId)} · Generated ${new Date().toLocaleString()}</p><table><tr><th>Person</th><th>Team</th><th>Clock In</th><th>Status</th></tr>${active.map(x=>`<tr><td><b>${x.member.name}</b></td><td>${x.gang.name}</td><td>${new Date(x.shift.clockIn.time).toLocaleString()}</td><td class="ok">${x.shift.clockIn.status}${x.shift.clockIn.distanceM!==undefined?' · '+Math.round(x.shift.clockIn.distanceM)+'m from Site':''}</td></tr>`).join('')}</table><script>window.onload=()=>window.print()<\/script></body></html>`;let w=window.open('','_blank');if(!w)return toast('Allow pop-ups to print the register');w.document.write(html);w.document.close()}
function printAttendanceTimesheet(){let sid=attendanceResolvedSite(),start=attendanceResolvedWeekStart(),financial=hasPerm('view_financials'),rows=attendanceVisibleMembers(sid).map(x=>{let t=attendanceWeekTotals(x.member.id,start,sid),a=attendanceApproval(x.member.id,sid,start);return `<tr><td>${x.member.name}</td><td>${x.gang.name}</td><td>${t.totalHours.toFixed(2)}</td><td>${t.overtimeHours.toFixed(2)}</td>${financial?`<td>${attendanceMoney(t.gross)}</td>`:''}<td>${a?(a.needsReapproval?'RE-APPROVAL REQUIRED':'Approved · '+a.approvedBy+' · '+new Date(a.approvedAt).toLocaleString()):'Awaiting approval'}</td></tr>`}).join(''),html=`<!doctype html><html><head><title>SiteSight Weekly Timesheet</title><style>body{font:14px Arial;padding:28px;color:#17384b}h1{margin-bottom:3px}table{width:100%;border-collapse:collapse;margin-top:20px}th,td{padding:9px;border:1px solid #ccd7dd;text-align:left}th{background:#eef3f6}.note{color:#667b86}</style></head><body><h1>SiteSight — Weekly Timesheet</h1><p>${siteName(sid)} · ${attendanceWeekLabel(start)}</p><table><tr><th>Person</th><th>Team</th><th>Hours</th><th>Overtime</th>${financial?'<th>Estimated Gross</th>':''}<th>Approval</th></tr>${rows}</table><p class="note">Estimated pay is a management calculation, not payroll, tax or statutory wage processing.</p><script>window.onload=()=>window.print()<\/script></body></html>`;let w=window.open('','_blank');if(!w)return toast('Allow pop-ups to print the timesheet');w.document.write(html);w.document.close()}

function signedPage(){if(!roleCanAccessPlots())return `<div class="panel empty">Signed-off work requires Supervisor access or above.</div>`;let a=db.items.filter(x=>x.signed&&canViewItem(x));return `<div class="topbar"><div><h1>Signed Off</h1><div class="sub">Completed work approved by management</div></div></div><div class="cards">${a.map(card).join('')||'<div class="empty-help"><b>No signed-off items yet.</b><div style="margin-top:6px">Signed-off work will appear here automatically.</div></div>'}</div>`}

function requestDeleteSite(id){if(!hasPerm('permanent_delete'))return toast('Permanent Deletion permission is required');let s=db.sites.find(s=>s.id===id);if(!s)return;modal=`<div class="modal"><div class="modalbox"><h2 style="color:var(--red)">Delete Site</h2><div class="delete-site-warning"><b>Are you sure you wish to delete this site</b><br>This will permanently remove <b>${s.name}</b> and all its work items, issues, drawings and payment records from SiteSight.</div><div class="field"><label>Re-enter your password to confirm</label><input id="delete-site-password" class="input" type="password" autocomplete="current-password" onkeydown="if(event.key==='Enter'){confirmDeleteSite('${id}')}"></div><div class="actions"><button class="btn red" onclick="confirmDeleteSite('${id}')">Confirm delete</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()}
function confirmDeleteSite(id){if(!hasPerm('permanent_delete'))return toast('Permanent Deletion permission is required');let pw=document.getElementById('delete-site-password')?.value||'',u=currentUser();if(!u||pw!==u.password)return toast('Password is incorrect');let s=db.sites.find(s=>s.id===id);if(!s)return;db.items=db.items.filter(x=>x.site!==id);db.sites=db.sites.filter(x=>x.id!==id);if(site===id)site='all';save();emitNotification('site','',`Site deleted — ${s.name}`);modal=null;toast('Site deleted');render()}


function adminReportOptions(id){if(!hasPerm('download_reports'))return toast('You do not have permission to download reports');let s=db.sites.find(s=>s.id===id);if(!s)return;modal=`<div class="modal"><div class="modalbox"><h2>Download Site Report</h2><p class="muted">${s.name}</p><div class="card" style="margin-bottom:12px"><h3>From start</h3><p class="muted">Download the full report from the beginning of the site record up to today.</p><button class="btn green" onclick="modal=null;downloadSiteReport('${id}')">⇩ Download from start</button></div><div class="card"><h3>Custom date range</h3><div class="two"><div class="field"><label>FROM</label><input id="adr-from" class="input" type="date"></div><div class="field"><label>TO</label><input id="adr-to" class="input" type="date"></div></div><button class="btn blue" onclick="downloadAdminCustomReport('${id}')">⇩ Download custom range</button></div><div style="margin-top:12px"><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()}
function downloadAdminCustomReport(id){if(!hasPerm('download_reports'))return toast('You do not have permission to download reports');let f=document.getElementById('adr-from')?.value||'',t=document.getElementById('adr-to')?.value||'';if(f&&t&&f>t)return toast('FROM date must be before TO date');modal=null;downloadSiteReport(id,f,t)}


function siteAdvanced(id){if(!hasPerm('permanent_delete'))return toast('Permanent Deletion permission is required');let s=db.sites.find(s=>s.id===id);if(!s)return;modal=`<div class="modal"><div class="modalbox"><h2>Advanced Site Options</h2><p class="muted">${s.name}</p><div class="advanced-delete"><b style="color:var(--red)">Permanent deletion</b><p class="muted">Only use this if the site was created by mistake. Completed sites should normally be archived.</p><button class="btn red danger-btn" onclick="requestDeleteSite('${s.id}')">Delete site permanently</button></div><div style="margin-top:14px"><button class="btn" onclick="modal=null;render()">Close</button></div></div></div>`;render()}
function setAdminTab(t){adminTab=t;adminSearch='';adminArchiveMode=false;adminArchiveSiteId=null;try{localStorage.setItem('sitesight_v173_demo_admin_tab',t)}catch(e){}render()}


function adminSearchChange(v){adminSearch=v;render();requestAnimationFrame(()=>{let e=document.getElementById('admin-search');if(e){e.focus();let n=e.value.length;e.setSelectionRange(n,n)}})}
function clearAdminSearch(){adminSearch='';render();requestAnimationFrame(()=>document.getElementById('admin-search')?.focus())}
function adminSearchBox(placeholder,count,total){let val=String(adminSearch||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');return `<div class="admin-searchbar"><div class="admin-search-input"><span>⌕</span><input id="admin-search" class="input" placeholder="${placeholder}" value="${val}" oninput="adminSearchChange(this.value)">${adminSearch?'<button class="admin-search-clear" onclick="clearAdminSearch()" title="Clear search">×</button>':''}</div><span class="admin-search-count">${count}${count!==total?` of ${total}`:''}</span></div>`}
function adminMatch(parts){let q=String(adminSearch||'').trim().toLowerCase();return !q||parts.filter(Boolean).join(' ').toLowerCase().includes(q)}


function editSite(id){if(!hasPerm('manage_sites'))return;let s=db.sites.find(s=>s.id===id);if(!s)return;modal=`<div class="modal"><div class="modalbox"><h2>Edit Site</h2><div class="field"><label>Site name</label><input id="edit-site-name" class="input" value="${String(s.name||'').replace(/"/g,'&quot;')}"></div><button class="btn orange" onclick="saveSiteName('${id}')">Save changes</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveSiteName(id){let s=db.sites.find(s=>s.id===id),n=(document.getElementById('edit-site-name')?.value||'').trim();if(!s||!n)return toast('Enter a site name.');if(db.sites.some(x=>x.id!==id&&!x.archived&&x.name.trim().toLowerCase()===n.toLowerCase()))return toast('A Site with that name already exists');s.name=n;save();audit('site','site',id,`Site renamed — ${n}`,id);modal=null;savedState('Site saved');render()}
function openSiteArchives(){if(!hasPerm('view_archives'))return toast('You do not have permission to view site archives');adminArchiveMode=true;adminArchiveSiteId=null;adminSearch='';render()}
function closeSiteArchives(){adminArchiveMode=false;adminArchiveSiteId=null;adminSearch='';render()}
function openArchivedSite(id){if(!hasPerm('view_archives'))return toast('You do not have permission to view site archives');let s=db.sites.find(s=>s.id===id&&s.archived&&canAccessSite(s.id));if(!s)return;adminArchiveMode=true;adminArchiveSiteId=id;render()}
function archiveDevelopmentStats(s){let items=db.items.filter(x=>x.site===s.id),issuesAll=items.flatMap(x=>(x.issues||[]).map(i=>({...i,item:x}))),payments=items.flatMap(x=>itemPaymentRows(x)).filter(p=>!p.amended),diary=(db.siteDiary||[]).filter(e=>e.siteId===s.id),notes=(db.calendarNotes||[]).filter(n=>n.siteId===s.id),library=(db.siteDrawings||[]).filter(d=>d.siteId===s.id),plotDrawings=items.flatMap(x=>(x.drawings||[]).map(d=>({...d,item:x}))),original=items.reduce((n,x)=>n+itemSetValue(x),0),variations=items.reduce((n,x)=>n+itemVariationValue(x),0),paid=items.reduce((n,x)=>n+itemTotalPaidIncludingVariations(x),0);return {items,issuesAll,payments,diary,notes,library,plotDrawings,original,variations,paid,total:original+variations,remaining:Math.max(0,original+variations-paid)}}
function siteArchiveDetail(s){let showFin=hasPerm('view_financials'),a=archiveDevelopmentStats(s),completed=a.items.filter(x=>['Complete','Signed Off'].includes(overall(x))).length,teams=[...new Set(a.items.map(x=>x.gang).filter(Boolean))];return `<div class="archive-detail"><div class="archive-toolbar"><button class="btn light" onclick="adminArchiveSiteId=null;render()">← Site archives</button><div class="actions">${hasPerm('download_reports')?`<button class="btn green" onclick="adminReportOptions('${s.id}')">⇩ Download report</button>`:''}${hasPerm('archive_sites')?`<button class="btn light" onclick="unarchiveSite('${s.id}');adminArchiveSiteId=null">Restore site</button>`:''}</div></div><div class="archive-hero"><div><span class="badge b-grey">Archived development</span><h2>${s.name}</h2><div class="muted">${s.archivedAt?`Archived ${s.archivedAt}${s.archivedBy?' by '+s.archivedBy:''}`:'Archived site'}</div></div><div class="archive-hero-count"><b>${a.items.length}</b><span>work items</span></div></div><div class="archive-summary-grid"><div class="card"><span>Completed / signed off</span><b>${completed} / ${a.items.length}</b></div><div class="card"><span>Issues</span><b>${a.issuesAll.length}</b></div><div class="card"><span>Teams used</span><b>${teams.length}</b></div><div class="card"><span>Diary entries</span><b>${a.diary.length}</b></div><div class="card"><span>Drawing Library</span><b>${a.library.length}</b></div><div class="card"><span>Plot drawings</span><b>${a.plotDrawings.length}</b></div></div>${showFin?`<div class="archive-finance"><div class="fbox"><span>Original contract</span><b>${money(a.original)}</b></div><div class="fbox purple"><span>Variations</span><b>${money(a.variations)}</b></div><div class="fbox green"><span>Total authorised</span><b>${money(a.total)}</b></div><div class="fbox"><span>Total paid</span><b>${money(a.paid)}</b></div><div class="fbox"><span>Remaining</span><b>${money(a.remaining)}</b></div></div>`:''}<div class="archive-section"><div class="sectionhead"><h3>Work Items</h3><span class="mini">${a.items.length}</span></div><div class="archive-list">${a.items.map(x=>`<div class="archive-row clickable" onclick="openItem('${x.id}')"><div><b>${x.ref}</b><span>${x.type}${x.houseType?' · '+x.houseType:''}</span></div><div><span>${gangName(x.gang)}</span><span>${statusText(x)}</span></div><div>${showFin?`<span>${money(itemTotalPaidIncludingVariations(x))} paid</span>`:''}<span>${issues(x)} open issue${issues(x)===1?'':'s'}</span></div><b>›</b></div>`).join('')||'<div class="admin-no-results">No work items recorded.</div>'}</div></div><div class="archive-section"><div class="sectionhead"><h3>Issues</h3><span class="mini">${a.issuesAll.length}</span></div><div class="archive-list">${a.issuesAll.map(r=>`<div class="archive-row"><div><b>${r.title}</b><span>${r.item.ref} · ${r.created||'—'}</span></div><div><span>${r.status}</span><span>${r.resolved?'Resolved '+r.resolved:''}</span></div><div class="archive-wide">${r.details||''}${r.notes?` · ${r.notes}`:''}</div></div>`).join('')||'<div class="admin-no-results">No issues recorded.</div>'}</div></div>${showFin?`<div class="archive-section"><div class="sectionhead"><h3>Payments</h3><span class="mini">${a.payments.length}</span></div><div style="overflow:auto"><table class="payment-history-table"><thead><tr><th>Date</th><th>Work item</th><th>Type</th><th>Stage / Variation</th><th>Paid to</th><th>Amount</th></tr></thead><tbody>${a.payments.map(p=>`<tr><td>${p.date||'—'}</td><td>${p.item.ref}</td><td>${p.paymentType||'Original Work'}</td><td>${p.paymentType==='Variation'?(p.variationName||p.stage||'Variation'):(p.stage||'—')}</td><td>${paymentPayeeName(p,p.item.gang||'')}</td><td>${money(p.amount||0)}</td></tr>`).join('')||'<tr><td colspan="6">No payments recorded.</td></tr>'}</tbody></table></div></div>`:''}<div class="archive-section"><div class="sectionhead"><h3>Drawings</h3><span class="mini">${a.library.length+a.plotDrawings.length}</span></div><div class="archive-two"><div><h4>Drawing Library</h4>${a.library.map(d=>`<div class="archive-mini-row"><b>${d.name}</b><span>${d.houseType} · ${d.uploadedDate||'—'}</span></div>`).join('')||'<div class="muted">No library drawings.</div>'}</div><div><h4>Plot Drawings</h4>${a.plotDrawings.map(d=>`<div class="archive-mini-row"><b>${d.name}</b><span>${d.item.ref} · ${d.uploadedDate||'—'}</span></div>`).join('')||'<div class="muted">No plot drawings.</div>'}</div></div></div><div class="archive-section"><div class="sectionhead"><h3>Site Diary</h3><span class="mini">${a.diary.length}</span></div>${a.diary.map(e=>`<div class="archive-diary"><b>${e.date}</b><span>${e.by||''}</span><p><b>Teams:</b> ${e.teams||'—'} · <b>Work:</b> ${e.work||'—'}</p><p><b>Deliveries:</b> ${e.deliveries||'—'} · <b>Problems / delays:</b> ${e.delays||'—'}</p>${e.notes?`<p>${e.notes}</p>`:''}${(e.amendments||[]).length?`<div class="archive-diary-amendments"><b>Corrections / Amendments</b>${e.amendments.map(m=>`<p>${m.createdAt?new Date(m.createdAt).toLocaleString():m.date||''} · ${m.by||''} — ${m.text||''}</p>`).join('')}</div>`:''}</div>`).join('')||'<div class="admin-no-results">No diary entries recorded.</div>'}</div><div class="archive-section"><div class="sectionhead"><h3>Calendar Notes</h3><span class="mini">${a.notes.length}</span></div>${a.notes.map(n=>`<div class="archive-mini-row"><b>${n.date||'—'} · ${n.title||'Calendar note'}</b><span>${n.details||''}</span></div>`).join('')||'<div class="admin-no-results">No calendar notes recorded.</div>'}</div></div>`}
function adminSiteArchivesPage(){if(!hasPerm('view_archives'))return `<div class="admin-no-results"><b>You do not have permission to view site archives.</b></div>`;let archives=db.sites.filter(s=>s.archived&&canAccessSite(s.id));if(adminArchiveSiteId){let s=archives.find(s=>s.id===adminArchiveSiteId);return s?siteArchiveDetail(s):'<div class="admin-no-results">Archived site not found.</div>'}let rows=archives.filter(s=>{let a=db.items.filter(x=>x.site===s.id);return adminMatch([s.name,s.archivedAt,s.archivedBy,a.length+' work items'])});return `<div class="card admin-section"><div class="sectionhead"><div><h2>Site Archives</h2><div class="muted">Completed or inactive developments kept safely for reference</div></div><button class="btn light" onclick="closeSiteArchives()">← Active sites</button></div>${adminSearchBox('Search site archives…',rows.length,archives.length)}<div class="archive-cards">${rows.map(s=>{let a=archiveDevelopmentStats(s);return `<div class="archive-card" onclick="openArchivedSite('${s.id}')"><div class="sectionhead"><div><span class="badge b-grey">Archived</span><h3>${s.name}</h3></div><b>›</b></div><div class="archive-card-meta"><span>${a.items.length} work items</span><span>${a.issuesAll.length} issues</span>${hasPerm('view_financials')?`<span>${money(a.total)} authorised</span>`:''}</div><div class="muted">${s.archivedAt?`Archived ${s.archivedAt}`:'Archive date not recorded'}</div></div>`}).join('')||'<div class="admin-no-results"><b>No archived sites found.</b><span>Archived developments will appear here.</span></div>'}</div></div>`}

function adminPage(){let allowed=[];if(hasPerm('manage_sites')||hasPerm('view_archives'))allowed.push('sites');if(hasPerm('manage_gangs'))allowed.push('teams');if(hasPerm('manage_users')||hasPerm('reset_passwords'))allowed.push('users');if(hasPerm('manage_workitems'))allowed.push('work');if(!allowed.length)return `<div class="panel empty-help"><b>You do not have Admin access.</b></div>`;if(!allowed.includes(adminTab)){adminTab=allowed[0];try{localStorage.setItem('sitesight_v173_demo_admin_tab',adminTab)}catch(e){}}let tabs=`<div class="admin-tabs">${allowed.includes('sites')?`<button class="admin-tab ${adminTab==='sites'?'active':''}" onclick="setAdminTab('sites')">Sites</button>`:''}${allowed.includes('teams')?`<button class="admin-tab ${adminTab==='teams'?'active':''}" onclick="setAdminTab('teams')">Teams</button>`:''}${allowed.includes('users')?`<button class="admin-tab ${adminTab==='users'?'active':''}" onclick="setAdminTab('users')">Users</button>`:''}${allowed.includes('work')?`<button class="admin-tab ${adminTab==='work'?'active':''}" onclick="setAdminTab('work')">Work Items</button>`:''}</div>`;let body='';if(adminTab==='sites'&&(adminArchiveMode||(!hasPerm('manage_sites')&&hasPerm('view_archives')))){body=adminSiteArchivesPage()}else if(adminTab==='sites'){let all=db.sites.filter(s=>!s.archived&&canAccessSite(s.id)),archivedCount=db.sites.filter(s=>s.archived&&canAccessSite(s.id)).length,rows=all.filter(s=>{let a=db.items.filter(x=>x.site===s.id);return adminMatch([s.name,'active',a.length+' work items',a.reduce((n,x)=>n+issues(x),0)+' issues'])});body=`<div class="card admin-section"><div class="sectionhead"><h2>Sites</h2><div class="actions">${hasPerm('view_archives')?`<button class="btn light" onclick="openSiteArchives()">▣ View site archives${archivedCount?` (${archivedCount})`:''}</button>`:''}<button class="btn" onclick="addSite()">＋ Add site</button></div></div>${adminSearchBox('Search sites…',rows.length,all.length)}<div class="cards">${rows.map(s=>{let a=db.items.filter(x=>x.site===s.id),done=a.length&&a.every(x=>['Complete','Signed Off'].includes(overall(x)));return `<div class="card site-card admin-clickable" onclick="editSite('${s.id}')"><div class="sectionhead"><h3>${s.name}</h3>${done?'<span class="badge b-green">Complete</span>':'<span class="badge b-blue">Active</span>'}</div><p>${a.length} work items · ${a.reduce((n,x)=>n+issues(x),0)} open issues</p><div class="mini">${s.reportDownloaded?'✓ Full report downloaded':'Full report not yet downloaded'}</div><div class="site-actions">${hasPerm('download_reports')?`<button class="btn green smallbtn" onclick="event.stopPropagation();adminReportOptions('${s.id}')">⇩ Download report</button>`:''}${hasPerm('archive_sites')?`<button class="btn light smallbtn" onclick="event.stopPropagation();archiveSite('${s.id}')">Archive site</button>`:''}${hasPerm('permanent_delete')?`<button class="btn light smallbtn" onclick="event.stopPropagation();siteAdvanced('${s.id}')">Advanced</button>`:''}</div></div>`}).join('')||'<div class="admin-no-results"><b>No sites found.</b><span>Try another search.</span></div>'}</div>${trainingTip('Archive completed or inactive developments to keep daily views tidy. Archived data remains available under View site archives.')}</div>`}else if(adminTab==='teams'){let all=db.gangs,rows=all.filter(g=>adminMatch([g.name,...(g.members||[]).flatMap(m=>[m.name,m.role]) ]));body=`<div class="card admin-section"><div class="sectionhead"><h2>Teams</h2><button class="btn" onclick="addGang()">＋ Add team</button></div>${adminSearchBox('Search teams or team members…',rows.length,all.length)}${rows.map(g=>`<div class="listrow admin-search-row admin-clickable" onclick="editTeamName('${g.id}')"><span><b>${g.name}</b><br><span class="muted">${(g.members||[]).length} team member(s)</span></span><button class="btn light smallbtn" onclick="event.stopPropagation();editTeamName('${g.id}')">Edit</button></div>`).join('')||'<div class="admin-no-results"><b>No teams found.</b><span>Try a team or member name.</span></div>'}</div>`}else if(adminTab==='users'){let all=db.users,rows=all.filter(u=>adminMatch([u.name,u.username,u.role,u.email,...(u.allowedSites||[]).map(siteName)]));body=`<div class="card admin-section"><div class="sectionhead"><h2>Users</h2>${hasPerm('manage_users')?'<button class="btn orange smallbtn" onclick="addUser()">＋ Add user</button>':''}</div>${adminSearchBox('Search name, username, role or email…',rows.length,all.length)}${rows.map(u=>`<div class="usercard admin-search-row ${u.role!=='Director'&&hasPerm('manage_users')?'admin-clickable':''}" ${u.role!=='Director'&&hasPerm('manage_users')?`onclick="editUserAccess('${u.id}')"`:''}><div class="row"><span><b>${u.name}</b><br><span class="muted">@${u.username} · ${u.role}</span><div class="emailtag">${u.email||'No email registered'}</div></span>${u.role!=='Director'?`<div class="actions">${hasPerm('manage_users')?`<button class="btn light smallbtn" onclick="event.stopPropagation();editUserAccess('${u.id}')">Edit access</button>`:''}${(canDirector()||hasPerm('reset_passwords'))?`<button class="btn light smallbtn" onclick="event.stopPropagation();resetUserPassword('${u.id}')">Reset password</button>`:''}</div>`:'<span class="badge b-green">Full access</span>'}</div></div>`).join('')||'<div class="admin-no-results"><b>No users found.</b><span>Try a name, username, role or email.</span></div>'}</div>`}else{let all=db.items.filter(x=>canAccessSite(x.site)),rows=all.filter(x=>adminMatch([x.ref,x.type,x.houseType,siteName(x.site),gangName(x.gang),overall(x),x.notes]));body=`<div class="card admin-section"><div class="sectionhead"><h2>Work Items</h2><div class="actions"><button class="btn light" onclick="openBulkImport()">⇩ Bulk import CSV</button><button class="btn orange" onclick="newItem()">＋ Create work item</button></div></div><p class="muted">Create individually or bulk-import a plot schedule. Work-item type cannot be changed after creation.</p>${adminSearchBox('Search reference, type, site or team…',rows.length,all.length)}<div class="admin-work-list">${rows.map(x=>`<div class="admin-work-row" onclick="openItem('${x.id}')"><div><b>${x.ref}</b><span>${x.type}${x.houseType?' · '+x.houseType:''}</span></div><div><span>${siteName(x.site)}</span><span>${gangName(x.gang)}</span></div><div>${statusText(x)}</div><b>›</b></div>`).join('')||'<div class="admin-no-results"><b>No work items found.</b><span>Try a reference, type, site or team.</span></div>'}</div>${trainingTip('For normal day-to-day use, work from the Work Items page. Admin is mainly for setup.')}</div>`}return `<div class="topbar"><div><h1>Admin</h1><div class="sub">Simple setup and management</div></div></div>${tabs}${body}`}
function archiveSite(id){if(!hasPerm('archive_sites'))return toast('You do not have permission to archive sites');let s=db.sites.find(s=>s.id===id);if(!s||s.archived)return;let prev={archived:s.archived,archivedAt:s.archivedAt,archivedBy:s.archivedBy},previousSite=site;s.archived=true;s.archivedAt=iso();s.archivedBy=currentUser()?.name||'';save();audit('site','site',id,`Site archived — ${s.name}`,id);if(site===id)site='all';render();showUndo('Site archived',()=>{s.archived=prev.archived;s.archivedAt=prev.archivedAt;s.archivedBy=prev.archivedBy;if(previousSite===id&&canAccessSite(id))site=id;save();audit('site','site',id,`Site archive undone — ${s.name}`,id);render()})}
function unarchiveSite(id){if(!hasPerm('archive_sites'))return toast('You do not have permission to restore sites');let s=db.sites.find(s=>s.id===id);if(!s)return;s.archived=false;s.restoredAt=iso();save();audit('site','site',id,`Site restored — ${s.name}`,id);adminArchiveSiteId=null;toast('Site restored');render()}
function openReportRange(){if(!hasPerm('download_reports'))return toast('You do not have permission to download reports');modal=`<div class="modal"><div class="modalbox"><h2>Download dated site report</h2><p class="muted">Only completed work whose completion date falls inside this range will be included.</p><div class="field"><label>Site</label><select id="rr-site" class="select"><option value="">Select site...</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select></div><div class="two"><div class="field"><label>FROM</label><input id="rr-from" class="input" type="date" value="${fromDate||''}"></div><div class="field"><label>TO</label><input id="rr-to" class="input" type="date" value="${toDate||''}"></div></div><button class="btn green" onclick="downloadSiteReportRange()">⇩ Download report</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function downloadSiteReportRange(){if(!hasPerm('download_reports'))return toast('You do not have permission to download reports');let id=document.getElementById('rr-site')?.value,rf=document.getElementById('rr-from')?.value||'',rt=document.getElementById('rr-to')?.value||'';if(!id)return toast('Select a site');if(rf&&rt&&rf>rt)return toast('FROM date must be before TO date');modal=null;downloadSiteReport(id,rf,rt)}
function downloadSiteReport(id,rangeFrom='',rangeTo=''){if(!hasPerm('download_reports'))return toast('You do not have permission to download reports');id=id||site;if(id==='all'||!id){toast('Select a site first');return}let showFin=hasPerm('view_financials'),showIssues=hasPerm('view_issues'),showDiary=hasPerm('view_site_diary'),s=db.sites.find(s=>s.id===id),allItems=db.items.filter(x=>x.site===id),items=allItems.filter(x=>{if(!rangeFrom&&!rangeTo)return true;let d=completionDate(x);return !!d&&(!rangeFrom||d>=rangeFrom)&&(!rangeTo||d<=rangeTo)}),rows=items.map(x=>{let pays=x.type==='House Plot'?x.lifts.flatMap(l=>(l.payments||[]).map(p=>({...p,stage:l.name}))):(x.payments||[]).map(p=>({...p,stage:x.type})),liftRows=x.type==='House Plot'?x.lifts.map(l=>`<tr><td>${l.name}</td><td>${l.status}</td><td>${l.completedDate||''}</td>${showFin?`<td>${l.pct}%</td><td>£${l.price}</td>`:''}</tr>`).join(''):`<tr><td>${x.type}</td><td>${x.status}</td><td>${x.completedDate||''}</td>${showFin?`<td>${x.pct}%</td><td>£${x.price}</td>`:''}</tr>`;return `<section><h2>${x.ref} ${x.signed?'★':''}</h2><p>${x.type} · ${x.houseType||''} · Team: ${gangName(x.gang)} · ${overall(x)}</p><table><tr><th>Stage</th><th>Status</th><th>Completed</th>${showFin?'<th>Paid</th><th>Price</th>':''}</tr>${liftRows}</table>${showFin?`<h4>Payment records</h4><ul>${pays.map(p=>`<li><b>${p.stage||'Work item'}</b> — ${p.date||''} — ${p.pct}% — £${p.amount||0} — ${p.notes||''}</li>`).join('')||'<li>None recorded</li>'}</ul>`:''}${showIssues?`<h4>Issues</h4><ul>${x.issues.map(i=>`<li>${i.status}: ${i.title} — ${i.details} (${i.photos?.length||0} photos)</li>`).join('')||'<li>None</li>'}</ul>`:''}</section>`}).join('');let rangeText=(rangeFrom||rangeTo)?`${rangeFrom||'Start'} to ${rangeTo||'Now'}`:'Full site history',diaryHtml=showDiary?`<h2>Site Diary</h2>${(db.siteDiary||[]).filter(e=>e.siteId===id&&(!rangeFrom||e.date>=rangeFrom)&&(!rangeTo||e.date<=rangeTo)).map(e=>`<section><b>${e.date}</b><p>Teams: ${e.teams||'—'}</p><p>Work: ${e.work||'—'}</p><p>Deliveries: ${e.deliveries||'—'}</p><p>Problems / delays: ${e.delays||'—'}</p><p>${e.notes||''}</p>${(e.amendments||[]).length?`<h4>Corrections / Amendments</h4><ul>${e.amendments.map(m=>`<li><b>${m.createdAt?new Date(m.createdAt).toLocaleString():m.date||''} · ${m.by||''}</b> — ${m.text||''}</li>`).join('')}</ul>`:''}</section>`).join('')||'<p>No diary entries.</p>'}`:'',report=`<!doctype html><html><head><meta charset="utf-8"><title>${s.name} — SiteSight Site Report</title><style>body{font:14px Arial;color:#172b42;max-width:1000px;margin:30px auto}h1{color:#09233d}section{page-break-inside:avoid;border:1px solid #ddd;padding:18px;margin:15px 0;border-radius:10px}table{width:100%;border-collapse:collapse}td,th{padding:7px;border:1px solid #ddd;text-align:left}th{background:#f2f5f8}.meta{color:#64748b}</style></head><body><h1>SiteSight — ${s.name}</h1><p class="meta">Generated ${iso()} · Range: ${rangeText} · ${items.length} work items</p>${rows||'<p>No completed work found in the selected date range.</p>'}${diaryHtml}</body></html>`;let blob=new Blob([report],{type:'text/html'}),a=document.createElement('a');a.href=URL.createObjectURL(blob);let suffix=(rangeFrom||rangeTo)?`-${rangeFrom||'start'}-to-${rangeTo||'now'}`:'';a.download=(s.name.replace(/[^a-z0-9]+/gi,'-').toLowerCase()+suffix+'-sitesight-report.html');a.click();if(!rangeFrom&&!rangeTo)s.reportDownloaded=true;save();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}function goNav(target){
 if(!confirmLeave())return;dirty=false;settingsOpen=false;selected=null;selectedGang=null;
 if(target==='dashboard'){
   gang='all';type='all';status='all';signed='all';q='';
 }
 if(target==='site-drawings'){
   drawingSite=site||'all';drawingHouseType='all';drawingItem='all';drawingQuery='';
 }
 view=target;render()
}



function viewingSitePill(){let label=site==='all'?'All Sites':siteName(site);return `<span class="site-view-pill">Viewing: <b>${label}</b>${site!=='all'?` <button title="Show all sites" onclick="setSiteContext('all')">×</button>`:''}</span>`}
function toggleWorkMore(){workMoreOpen=!workMoreOpen;render()}
function openPaymentStage(itemId,stageName){let x=db.items.find(x=>x.id===itemId);if(!canViewItem(x))return toast('Plot cards are available to Supervisors and above');selected=x.id;view='work-items';selectedGang=null;if(x.type==='House Plot'){let i=(x.lifts||[]).findIndex(l=>l.name===stageName);openLift=i>=0?i:-1}else openLift=-1;render()}

function paymentPersonDirectory(){
 let rows=[];
 db.users.forEach(u=>rows.push({id:u.id,name:u.name||u.username||'User',kind:'Individual'}));
 db.gangs.forEach(g=>(g.members||[]).forEach(m=>rows.push({id:m.id,name:m.name||'Team member',kind:'Individual',team:g.name})));
 let seen=new Set();
 return rows.filter(r=>r.id&&!seen.has(r.id)&&(seen.add(r.id),true)).sort((a,b)=>a.name.localeCompare(b.name))
}
function paymentPayeeNameFromId(id){
 if(!id)return '';
 let g=db.gangs.find(g=>g.id===id);if(g)return g.name;
 let u=db.users.find(u=>u.id===id);if(u)return u.name||u.username||'User';
 for(let g2 of db.gangs){let m=(g2.members||[]).find(m=>m.id===id);if(m)return m.name||'Team member'}
 return ''
}
function paymentPayeeName(p,fallback=''){
 if(p?.payeeName)return p.payeeName;
 let id=p?.paidTo||fallback||p?.item?.gang||'';
 return paymentPayeeNameFromId(id)||gangName(id)||'To be Confirmed'
}
function paymentPayeeKind(p){
 let id=p?.paidTo||p?.item?.gang||'';
 if(db.gangs.some(g=>g.id===id))return 'Team';
 return 'Individual'
}
function paymentPayeeKey(p){
 if(p?.payeeName)return 'name:'+p.payeeName.trim().toLowerCase();
 let id=p?.paidTo||p?.item?.gang||'';
 return id?'id:'+id:'unassigned'
}
function paymentPayeeOptions(selected='',fallback=''){
 let current=selected||fallback||'';
 return `<option value="">To be Confirmed</option><optgroup label="Teams">${db.gangs.map(g=>`<option value="${g.id}" ${current===g.id?'selected':''}>${g.name}</option>`).join('')}</optgroup><optgroup label="Individuals">${paymentPersonDirectory().map(p=>`<option value="${p.id}" ${current===p.id?'selected':''}>${p.name}${p.team?' · '+p.team:''}</option>`).join('')}</optgroup><option value="__other__">Other individual…</option>`
}
function paymentPayeeChange(selectId,containerId){
 let s=document.getElementById(selectId),e=document.getElementById(containerId);if(!e)return;
 e.style.display=s?.value==='__other__'?'':'none';
 if(s?.value==='__other__')e.querySelector('input')?.focus()
}
function paymentPayeeRecord(selectId,inputId,fallback=''){
 let v=document.getElementById(selectId)?.value||fallback||'';
 if(v==='__other__'){
  let name=(document.getElementById(inputId)?.value||'').trim();
  if(!name)return {error:'Enter the individual name'};
  return {paidTo:'',payeeName:name}
 }
 return {paidTo:v,payeeName:''}
}
function paymentPayeeFilterOptions(){
 let rows=allPaymentRows(),map=new Map();
 rows.forEach(p=>{let key=paymentPayeeKey(p);if(!map.has(key))map.set(key,{key,name:paymentPayeeName(p),kind:paymentPayeeKind(p)})});
 let teams=[...map.values()].filter(x=>x.kind==='Team').sort((a,b)=>a.name.localeCompare(b.name)),
     individuals=[...map.values()].filter(x=>x.kind!=='Team').sort((a,b)=>a.name.localeCompare(b.name));
 return `<option value="all">All teams & individuals</option>${teams.length?`<optgroup label="Teams">${teams.map(x=>`<option value="${x.key}" ${paymentPayeeFilter===x.key?'selected':''}>${x.name}</option>`).join('')}</optgroup>`:''}${individuals.length?`<optgroup label="Individuals">${individuals.map(x=>`<option value="${x.key}" ${paymentPayeeFilter===x.key?'selected':''}>${x.name}</option>`).join('')}</optgroup>`:''}`
}
function paymentPayeeOverview(rows){
 let total=rows.reduce((n,p)=>n+Number(p.amount||0),0),
     original=rows.filter(p=>p.paymentType!=='Variation').reduce((n,p)=>n+Number(p.amount||0),0),
     variations=rows.filter(p=>p.paymentType==='Variation').reduce((n,p)=>n+Number(p.amount||0),0),
     dates=rows.map(p=>p.date).filter(Boolean).sort(),last=dates.length?dates.at(-1):'—',
     label=paymentPayeeFilter==='all'?'All payees':(rows[0]?paymentPayeeName(rows[0]):'Selected payee');
 return `<div class="payee-overview"><div class="payee-overview-head"><span>PAYMENT OVERVIEW</span><b>${label}</b></div><div class="payee-overview-grid"><div><span>Total paid</span><b>${money(total)}</b></div><div><span>Payments</span><b>${rows.length}</b></div><div><span>Original work</span><b>${money(original)}</b></div><div><span>Variations</span><b>${money(variations)}</b></div><div><span>Last payment</span><b>${last}</b></div></div></div>`
}

function filteredPaymentRows(){
 let rows=allPaymentRows();
 if(paymentPayeeFilter!=='all')rows=rows.filter(p=>paymentPayeeKey(p)===paymentPayeeFilter);
 if(paymentFrom)rows=rows.filter(p=>String(p.date||'')>=paymentFrom);
 if(paymentTo)rows=rows.filter(p=>String(p.date||'')<=paymentTo);
 let q=(paymentSearch||'').trim().toLowerCase();
 if(!q)return rows;
 return rows.filter(p=>[siteName(p.item.site),p.item.ref,p.stage,paymentPayeeName(p),p.notes,p.paymentType,p.variationName].join(' ').toLowerCase().includes(q))
}
function stageSetPrice(x,l){return Number(l?.price||0)}
function stagePayments(l){return (l?.payments||[]).filter(p=>!p.amended)}
function stagePaidAmount(l){return stagePayments(l).reduce((n,p)=>n+Number(p.amount||0),0)}
function stagePaidPct(l){return stagePayments(l).reduce((n,p)=>n+Number(p.pct||0),0)}
function stageRemainingAmount(l){return Math.max(0,stageSetPrice(null,l)-stagePaidAmount(l))}
function stageRemainingPct(l){return Math.max(0,100-stagePaidPct(l))}
function simpleSetPrice(x){return Number(x?.price||0)}
function simplePayments(x){return (x?.payments||[]).filter(p=>!p.amended)}
function simplePaidAmount(x){return simplePayments(x).reduce((n,p)=>n+Number(p.amount||0),0)}
function simplePaidPct(x){return simplePayments(x).reduce((n,p)=>n+Number(p.pct||0),0)}
function simpleRemainingAmount(x){return Math.max(0,simpleSetPrice(x)-simplePaidAmount(x))}
function simpleRemainingPct(x){return Math.max(0,100-simplePaidPct(x))}

function variationPaid(v){return (v.payments||[]).filter(p=>!p.amended).reduce((n,p)=>n+Number(p.amount||0),0)}
function variationRemaining(v){return Math.max(0,Number(v.value||0)-variationPaid(v))}
function variationPaidPct(v){let val=Number(v.value||0);return val?Math.min(100,Math.round(variationPaid(v)/val*10000)/100):0}
function itemVariationValue(x){return (x.variations||[]).filter(v=>(v.approvalStatus||'Approved')==='Approved').reduce((n,v)=>n+Number(v.value||0),0)}
function itemVariationPaid(x){return (x.variations||[]).filter(v=>(v.approvalStatus||'Approved')==='Approved').reduce((n,v)=>n+variationPaid(v),0)}
function itemVariationRemaining(x){return Math.max(0,itemVariationValue(x)-itemVariationPaid(x))}
function itemTotalAuthorisedValue(x){return itemSetValue(x)+itemVariationValue(x)}
function itemTotalPaidIncludingVariations(x){return itemPaidValue(x)+itemVariationPaid(x)}
function addVariation(){if(!hasPerm('manage_variations'))return toast('You do not have permission to create variations');let x=db.items.find(x=>x.id===selected);if(!x)return;modal=`<div class="modal"><div class="modalbox"><h2>Add Variation / Extra Work</h2><div class="field"><label>Name / description</label><input id="var-name" class="input" placeholder="e.g. Additional garage brickwork"></div><div class="two"><div class="field"><label>Value</label><input id="var-value" class="input" type="number" min="0.01" step="0.01"></div><div class="field"><label>Date</label><input id="var-date" class="input" type="date" value="${iso()}"></div></div><div class="two"><div class="field"><label>Lift / Stage</label><select id="var-stage" class="select"><option value="">General / whole work item</option>${x.type==='House Plot'?(x.lifts||[]).map(l=>`<option>${l.name}</option>`).join(''):''}</select></div><div class="field"><label>Team</label><select id="var-team" class="select"><option value="">To be Confirmed</option>${db.gangs.map(g=>`<option value="${g.id}" ${x.gang===g.id?'selected':''}>${g.name}</option>`).join('')}</select></div></div><div class="role-template-note">${hasPerm('approve_variations')?'<b>This variation will be approved when saved.</b>':'<b>This variation will be saved as Awaiting Approval.</b> A user with Approve Variations permission must approve it before payments can be recorded.'}</div><button class="btn orange" onclick="saveVariation()">Save variation</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveVariation(){if(!hasPerm('manage_variations'))return toast('You do not have permission to create variations');let x=db.items.find(x=>x.id===selected),name=(document.getElementById('var-name')?.value||'').trim(),value=Number(document.getElementById('var-value')?.value||0);if(!x||!name||value<=0)return toast('Enter a variation name and value.');let approved=hasPerm('approve_variations'),v={id:'v'+Date.now(),name,value,date:document.getElementById('var-date')?.value||iso(),stage:document.getElementById('var-stage')?.value||'',team:document.getElementById('var-team')?.value||x.gang||'',payments:[],approvalStatus:approved?'Approved':'Pending Approval',approvedBy:approved?(currentUser()?.name||''):'',approvedAt:approved?iso():''};x.variations=x.variations||[];x.variations.push(v);touch(x);save();audit('variation','variation',v.id,`${x.ref}: variation added — ${name} — ${v.approvalStatus}`,x.site);savedState('Variation saved');modal=null;render()}

function approveVariation(varId){if(!hasPerm('approve_variations'))return toast('You do not have permission to approve variations');let x=db.items.find(x=>x.id===selected),v=x?.variations?.find(v=>v.id===varId);if(!x||!v)return;if((v.approvalStatus||'Approved')==='Approved')return;v.approvalStatus='Approved';v.approvedBy=currentUser()?.name||'';v.approvedAt=iso();touch(x);save();audit('variation','variation',v.id,`${x.ref}: variation approved — ${v.name}`,x.site);toast('Variation approved');render()}
function deleteVariationPayment(varId,paymentId){
 if(!hasPerm('manage_payments'))return toast('You do not have permission to remove payments');
 let x=db.items.find(x=>x.id===selected),v=x?.variations?.find(v=>v.id===varId);if(!x||!v)return;if(!confirm('Remove this variation payment record?'))return;
 let removed=(v.payments||[]).find(p=>p.id===paymentId);v.payments=(v.payments||[]).filter(p=>p.id!==paymentId);touch(x);save();
 if(removed)audit('payment','variation payment',removed.id,`${x.ref}: variation payment removed — ${v.name} · ${money(removed.amount||0)} · originally entered by ${removed.enteredBy||'Unknown user'}`,x.site);
 savedState('Payment removed');render()
}
function addVariationPayment(varId){if(!hasPerm('record_payments'))return toast('You do not have permission to record payments');let x=db.items.find(x=>x.id===selected),v=x?.variations?.find(v=>v.id===varId);if(!x||!v)return;if((v.approvalStatus||'Approved')!=='Approved')return toast('This variation must be approved before a payment can be recorded.');let remaining=variationRemaining(v);if(remaining<=0)return toast('This variation is already fully paid.');modal=`<div class="modal"><div class="modalbox"><h2>Add Variation Payment</h2><p class="muted">${x.ref} · ${v.name}</p><div class="payment-entry-summary"><div class="card"><span class="muted">Variation value</span><b>${money(v.value)}</b></div><div class="card"><span class="muted">Already paid</span><b>${money(variationPaid(v))}</b></div><div class="card"><span class="muted">Remaining</span><b>${money(remaining)}</b></div></div><div class="field"><label>Payment now</label><input id="vpa" class="input" type="number" min="0.01" max="${remaining}" step="0.01"></div><div class="field"><label>Date</label><input id="vpd" class="input" type="date" value="${iso()}"></div><div class="field"><label>Paid to</label><select id="vpto" class="select" onchange="paymentPayeeChange('vpto','vpto-other')">${paymentPayeeOptions('',v.team||x.gang||'')}</select></div><div class="field" id="vpto-other" style="display:none"><label>Individual name</label><input id="vpto-other-name" class="input" placeholder="Name"></div><div class="field"><label>Notes</label><input id="vpn" class="input"></div><button class="btn green" onclick="saveVariationPayment('${varId}')">Save payment</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveVariationPayment(varId){
 if(!hasPerm('record_payments'))return toast('You do not have permission to record payments');
 let x=db.items.find(x=>x.id===selected),v=x?.variations?.find(v=>v.id===varId);if(!x||!v)return;
 let amt=Number(document.getElementById('vpa')?.value||0),remaining=variationRemaining(v);if(amt<=0)return toast('Enter the amount being paid.');if(amt>remaining+0.005)return toast(`Only ${money(remaining)} remains to be paid on this variation.`);
 let pct=v.value?Math.round((amt/Number(v.value))*10000)/100:0,selectedPayee=document.getElementById('vpto')?.value||v.team||x.gang||'',payeeName='';
 if(selectedPayee==='__other__'){payeeName=(document.getElementById('vpto-other-name')?.value||'').trim();if(!payeeName)return toast('Enter the individual name');selectedPayee=''}
 let rec={id:'vp'+Date.now(),pct,date:document.getElementById('vpd')?.value||iso(),amount:amt,paidTo:selectedPayee,payeeName,notes:document.getElementById('vpn')?.value||'',variation:true,variationId:v.id,variationName:v.name,enteredBy:currentUser()?.name||'',enteredAt:new Date().toISOString()};
 v.payments.push(rec);touch(x);save();emitNotification('payment',x.site,`${x.ref}: variation payment recorded — ${v.name} — ${money(amt)} · entered by ${rec.enteredBy}`,x.id);savedState('Variation payment saved');modal=null;render()
}
function deleteVariation(varId){if(!hasPerm('manage_variations'))return toast('You do not have permission to manage variations');let x=db.items.find(x=>x.id===selected),v=x?.variations?.find(v=>v.id===varId);if(!x||!v)return;if((v.payments||[]).length)return toast('Remove variation payments before deleting this variation.');if(!confirm(`Delete variation "${v.name}"?`))return;x.variations=x.variations.filter(v=>v.id!==varId);touch(x);save();audit('variation','variation',varId,`${x.ref}: variation deleted — ${v.name}`,x.site);render()}
function variationPaymentHistory(v){
 let rows=(v.payments||[]).filter(p=>!p.amended).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||'')));
 return `<div class="variation-payments">${rows.length?`<div style="overflow:auto"><table class="payment-history-table"><thead><tr><th>Date</th><th>Paid to</th><th>%</th><th>Amount</th><th>Entered by</th><th>Notes</th>${hasPerm('manage_payments')?'<th></th>':''}</tr></thead><tbody>${rows.map(p=>`<tr><td>${p.date||'—'}</td><td>${paymentPayeeName(p,v.team||'')}</td><td>${p.pct||0}%</td><td>${money(p.amount||0)}</td><td>${p.enteredBy||'—'}${p.enteredAt?`<small class="payment-entered-time">${new Date(p.enteredAt).toLocaleString('en-GB')}</small>`:''}</td><td>${p.notes||''}</td>${hasPerm('manage_payments')?`<td><button class="btn light smallbtn" onclick="deleteVariationPayment('${v.id}','${p.id}')">Remove</button></td>`:''}</tr>`).join('')}</tbody></table></div>`:'<div class="empty-help"><b>No payments recorded for this variation.</b></div>'}</div>`
}
function financeOverview(x){let original=itemSetValue(x),vars=itemVariationValue(x),auth=original+vars,paid=itemTotalPaidIncludingVariations(x),remain=Math.max(0,auth-paid);return `<div class="finance-overview"><div class="fbox"><span>Original Contract</span><b>${money(original)}</b></div><div class="fbox purple"><span>Variations</span><b>+ ${money(vars)}</b></div><div class="fbox green"><span>Total Authorised</span><b>${money(auth)}</b></div></div><div class="progressrow"><div class="bar"><i style="width:${auth?Math.min(100,paid/auth*100):0}%"></i></div><span class="pct">${money(paid)} paid · ${money(remain)} remaining</span></div>`}

function variationsBlock(x){if(!hasPerm('view_financials'))return '';let approved=(x.variations||[]).filter(v=>(v.approvalStatus||'Approved')==='Approved'),pending=(x.variations||[]).filter(v=>(v.approvalStatus||'Approved')!=='Approved'),total=itemVariationValue(x),paid=itemVariationPaid(x),remaining=itemVariationRemaining(x);return `<div class="card variation-panel"><div class="sectionhead"><div><h3>Variations / Extra Work</h3><div class="muted">Approved variations are kept separate from the original contract value</div></div>${hasPerm('manage_variations')?'<button class="btn orange" onclick="addVariation()">＋ Variation</button>':''}</div>${pending.length?`<div class="variation-pending-note"><b>${pending.length} awaiting approval</b><span>Pending values are not included in Total Authorised until approved.</span></div>`:''}<div class="variation-summary"><div class="card"><span class="muted">Approved variation value</span><b>${money(total)}</b></div><div class="card"><span class="muted">Variation paid</span><b>${money(paid)}</b></div><div class="card"><span class="muted">Variation remaining</span><b>${money(remaining)}</b></div><div class="card"><span class="muted">Total authorised value</span><b>${money(itemTotalAuthorisedValue(x))}</b></div></div>${(x.variations||[]).length?(x.variations||[]).map(v=>{let ok=(v.approvalStatus||'Approved')==='Approved';return `<div class="variation-card ${ok?'':'variation-awaiting'}"><div class="sectionhead"><div><h4>${v.name}</h4><span class="variation-type">Variation</span> <span class="variation-approval ${ok?'approved':'pending'}">${ok?'Approved':'Awaiting Approval'}</span></div><b>${money(v.value)}</b></div><div class="variation-meta"><span>${v.date||'—'}</span><span>${v.stage||'General'}</span><span>${gangName(v.team||x.gang||'')}</span>${ok&&v.approvedBy?`<span>Approved by ${v.approvedBy}</span>`:''}</div>${ok?`<div class="variation-summary"><div class="card"><span class="muted">Paid</span><b>${money(variationPaid(v))}</b></div><div class="card"><span class="muted">Remaining</span><b>${money(variationRemaining(v))}</b></div><div class="card"><span class="muted">Paid</span><b>${variationPaidPct(v)}%</b></div><div class="card"><span class="muted">Status</span><b>${variationRemaining(v)<=0?'Paid':'Open'}</b></div></div>`:'<div class="muted" style="margin:8px 0">Payments are locked until this variation is approved.</div>'}<div class="actions">${!ok&&hasPerm('approve_variations')?`<button class="btn green smallbtn" onclick="approveVariation('${v.id}')">✓ Approve variation</button>`:''}${ok&&hasPerm('record_payments')?`<button class="btn green smallbtn" onclick="addVariationPayment('${v.id}')">＋ Add payment</button>`:''}${hasPerm('manage_variations')&&variationPaid(v)===0?`<button class="btn red smallbtn danger-btn" onclick="deleteVariation('${v.id}')">Delete</button>`:''}</div>${ok?variationPaymentHistory(v):''}</div>`}).join(''):'<div class="empty-help"><b>No variations or extra work recorded.</b><div style="margin-top:6px">Use + Variation when extra work is authorised outside the original set value.</div></div>'}</div>`}

function itemPaymentRows(x){let base=x.type==='House Plot'?(x.lifts||[]).flatMap(l=>(l.payments||[]).map(p=>({...p,stage:l.name,item:x,paymentType:'Original Work'}))):(x.payments||[]).map(p=>({...p,stage:x.type,item:x,paymentType:'Original Work'}));let vars=(x.variations||[]).flatMap(v=>(v.payments||[]).map(p=>({...p,stage:v.stage||v.name,item:x,paymentType:'Variation',variationName:v.name})));return [...base,...vars]}
function itemSetValue(x){return x.type==='House Plot'?(x.lifts||[]).reduce((n,l)=>n+Number(l.price||0),0):Number(x.price||0)}
function itemPaidValue(x){let base=x.type==='House Plot'?(x.lifts||[]).flatMap(l=>l.payments||[]):x.payments||[];return base.filter(p=>!p.amended).reduce((n,p)=>n+Number(p.amount||0),0)}
function itemRemainingValue(x){return Math.max(0,itemSetValue(x)-itemPaidValue(x))}
function itemPaymentHistory(x){
 let rows=itemPaymentRows(x).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))),originalSet=itemSetValue(x),variationSet=itemVariationValue(x),authorised=originalSet+variationSet,originalPaid=itemPaidValue(x),variationPaid=itemVariationPaid(x),totalPaid=originalPaid+variationPaid,remaining=Math.max(0,authorised-totalPaid);
 return `<div class="card work-payment-summary"><div class="sectionhead"><h3>Overall Payment History</h3><span class="muted">Original work + variations</span></div><div class="payment-summary-grid"><div class="payment-summary-card"><span class="muted">Original contract</span><b>${money(originalSet)}</b></div><div class="payment-summary-card"><span class="muted">Variations</span><b>${money(variationSet)}</b></div><div class="payment-summary-card"><span class="muted">Total authorised</span><b>${money(authorised)}</b></div><div class="payment-summary-card"><span class="muted">Total paid</span><b>${money(totalPaid)}</b></div><div class="payment-summary-card"><span class="muted">Original paid</span><b>${money(originalPaid)}</b></div><div class="payment-summary-card"><span class="muted">Variation paid</span><b>${money(variationPaid)}</b></div><div class="payment-summary-card"><span class="muted">Total remaining</span><b>${money(remaining)}</b></div><div class="payment-summary-card"><span class="muted">Overall paid</span><b>${authorised?Math.min(100,Math.round(totalPaid/authorised*100)):0}%</b></div></div><div style="overflow:auto"><table class="payment-history-table"><thead><tr><th>Date</th><th>Type</th><th>Lift / Stage / Variation</th><th>Paid to</th><th>%</th><th>Amount</th><th>Entered by</th><th>Notes</th></tr></thead><tbody>${rows.length?rows.map(p=>`<tr ${p.paymentType==='Original Work'?`class="payment-row-click" onclick="openPaymentStage('${x.id}','${String(p.stage||'').replace(/'/g,"\\'")}')"`:''}><td>${p.date||'—'}</td><td>${p.paymentType==='Variation'?'<span class="variation-type">Variation</span>':'Original Work'}</td><td>${p.paymentType==='Variation'?(p.variationName||p.stage||'Variation'):(p.stage||'—')}</td><td>${paymentPayeeName(p,p.item?.gang||'')}</td><td>${p.pct||0}%</td><td>${money(p.amount||0)}</td><td>${p.enteredBy||'—'}${p.enteredAt?`<small class="payment-entered-time">${new Date(p.enteredAt).toLocaleString('en-GB')}</small>`:''}</td><td>${p.notes||''}</td></tr>`).join(''):'<tr><td colspan="8">No payment records yet.</td></tr>'}<tr class="payment-total-row"><td colspan="5">Total paid</td><td>${money(totalPaid)}</td><td colspan="2">${money(remaining)} remaining</td></tr></tbody></table></div></div>`
}
function allPaymentRows(){return db.items.filter(x=>canAccessSite(x.site)&&(site==='all'||x.site===site)).flatMap(x=>itemPaymentRows(x)).filter(p=>!p.amended).sort((a,b)=>String(b.date||'').localeCompare(String(a.date||'')))}

function setPaymentDatePreset(preset){
 let today=new Date();
 if(preset==='all'){paymentFrom='';paymentTo='';render();return}
 if(preset==='month'){paymentFrom=iso(new Date(today.getFullYear(),today.getMonth(),1));paymentTo=iso(today);render();return}
 let monday=new Date(today),day=(today.getDay()+6)%7;monday.setDate(today.getDate()-day);paymentFrom=iso(monday);paymentTo=iso(today);render()
}
function paymentPresetState(){
 if(!paymentFrom&&!paymentTo)return 'all';
 let today=new Date(),monthStart=iso(new Date(today.getFullYear(),today.getMonth(),1)),todayIso=iso(today),monday=new Date(today),day=(today.getDay()+6)%7;monday.setDate(today.getDate()-day);
 if(paymentFrom===iso(monday)&&paymentTo===todayIso)return 'week';
 if(paymentFrom===monthStart&&paymentTo===todayIso)return 'month';
 return 'custom'
}

function paymentsHistoryPage(){
 if(!hasPerm('view_financials'))return `<div class="panel empty-help"><b>You do not have permission to view financial information.</b></div>`;
 let invalid=paymentFrom&&paymentTo&&paymentFrom>paymentTo,rows=invalid?[]:filteredPaymentRows(),total=rows.reduce((n,p)=>n+Number(p.amount||0),0),preset=paymentPresetState();
 return `<div class="topbar"><div><h1>Payments History</h1><div class="sub">Quick accounting overview by Team, individual and date range</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select></div></div><div class="payment-history-page"><div class="panel payment-filter-panel"><div class="payment-preset-row"><span>Quick range</span><button class="${preset==='week'?'active':''}" onclick="setPaymentDatePreset('week')">This Week</button><button class="${preset==='month'?'active':''}" onclick="setPaymentDatePreset('month')">This Month</button><button class="${preset==='all'?'active':''}" onclick="setPaymentDatePreset('all')">All Time</button></div><div class="payment-filter-grid payment-filter-grid-v158"><div class="field"><label>Paid to</label><select class="select" onchange="paymentPayeeFilter=this.value;render()">${paymentPayeeFilterOptions()}</select></div><div class="field"><label>FROM</label><input class="input" type="date" value="${paymentFrom}" onchange="paymentFrom=this.value;render()"></div><div class="field"><label>TO</label><input class="input" type="date" value="${paymentTo}" onchange="paymentTo=this.value;render()"></div><div class="field payment-search-field"><label>Search payments</label><input class="input" placeholder="Plot, stage, notes or payee…" value="${paymentSearch}" oninput="paymentSearch=this.value;render()"></div><div class="payment-filter-reset"><button class="btn light" onclick="paymentPayeeFilter='all';paymentSearch='';paymentFrom='';paymentTo='';render()">Clear filters</button></div></div>${invalid?'<div class="payment-range-warning">FROM date must be before TO date.</div>':paymentPayeeOverview(rows)}</div><div class="panel"><div class="sectionhead"><div><h2>Payment Records</h2><div class="muted">${invalid?'Correct the date range to continue':`${rows.length} matching payment${rows.length===1?'':'s'}${paymentFrom||paymentTo?` · ${paymentFrom||'Start'} → ${paymentTo||'Now'}`:''}`}</div></div><b>${invalid?'—':money(total)+' total'}</b></div><div style="overflow:auto"><table class="payment-history-table"><thead><tr><th>Date</th><th>Type</th><th>Site</th><th>Work item</th><th>Lift / Stage / Variation</th><th>Paid to</th><th>Payee type</th><th>%</th><th>Amount</th><th>Entered by</th><th>Notes</th></tr></thead><tbody>${!invalid&&rows.length?rows.map(p=>`<tr><td>${p.date||'—'}</td><td>${p.paymentType==='Variation'?'<span class="variation-type">Variation</span>':'Original Work'}</td><td>${siteName(p.item.site)}</td><td>${p.item.ref}</td><td>${p.paymentType==='Variation'?(p.variationName||p.stage||'Variation'):(p.stage||'—')}</td><td><b>${paymentPayeeName(p)}</b></td><td>${paymentPayeeKind(p)}</td><td>${p.pct||0}%</td><td>${money(p.amount||0)}</td><td>${p.enteredBy||'—'}${p.enteredAt?`<small class="payment-entered-time">${new Date(p.enteredAt).toLocaleString('en-GB')}</small>`:''}</td><td>${p.notes||''}</td></tr>`).join(''):`<tr><td colspan="11">${invalid?'Correct the FROM / TO dates.':'No payment records match these filters.'}</td></tr>`}</tbody></table></div></div></div>`
}
function money(n){return '£'+Number(n||0).toLocaleString('en-GB',{maximumFractionDigits:0})}
function itemValue(x){return x.type==='House Plot'?(x.lifts||[]).reduce((n,l)=>n+Number(l.price||0),0):Number(x.price||0)}
function itemPaid(x){return x.type==='House Plot'?(x.lifts||[]).reduce((n,l)=>n+(l.payments||[]).reduce((a,p)=>a+Number(p.amount||0),0),0):(x.payments||[]).reduce((a,p)=>a+Number(p.amount||0),0)}
function itemExtraPaid(x){return itemVariationPaid(x)+(x.type==='House Plot'?(x.lifts||[]).reduce((n,l)=>n+(l.payments||[]).filter(p=>p.authorisedExtra).reduce((a,p)=>a+Number(p.amount||0),0),0):(x.payments||[]).filter(p=>p.authorisedExtra).reduce((a,p)=>a+Number(p.amount||0),0))}
function itemContractPaid(x){return itemPaid(x)-itemExtraPaid(x)}

function weeklyCompletionData(){let out=[];for(let w=5;w>=0;w--){let end=new Date();end.setDate(end.getDate()-w*7),start=new Date(end);start.setDate(end.getDate()-6);let a=filtered().filter(x=>{let d=completionDate(x);return d&&d>=iso(start)&&d<=iso(end)});out.push({label:`W${6-w}`,count:a.length})}return out}


function analyticsTeamRangeReady(){return !!analyticsTeamFrom&&!!analyticsTeamTo&&analyticsTeamFrom<=analyticsTeamTo}
function setAnalyticsTeamDate(which,value){if(which==='from')analyticsTeamFrom=value;else analyticsTeamTo=value;render()}
function clearAnalyticsTeamRange(){analyticsTeamFrom='';analyticsTeamTo='';render()}
function dateWithinRange(d,from,to){return !!d&&(!from||d>=from)&&(!to||d<=to)}
function teamCompletedUnits(items,from,to){return items.reduce((n,x)=>{if(x.type==='House Plot')return n+(x.lifts||[]).filter(l=>l.status==='Complete'&&dateWithinRange(l.completedDate,from,to)).length;return n+(x.status==='Complete'&&dateWithinRange(x.completedDate,from,to)?1:0)},0)}
function teamIssuesInRange(items,from,to){return items.reduce((n,x)=>n+(x.issues||[]).filter(i=>dateWithinRange(i.created,from,to)).length,0)}
function teamPerformanceData(){let base=scopeItems(),ready=analyticsTeamRangeReady();return db.gangs.filter(g=>g.active).map(g=>{let items=base.filter(x=>x.gang===g.id),completed=ready?teamCompletedUnits(items,analyticsTeamFrom,analyticsTeamTo):0,newIssues=ready?teamIssuesInRange(items,analyticsTeamFrom,analyticsTeamTo):0,currentAssigned=items.length,currentProgress=items.filter(x=>overall(x)==='In progress').length,currentComplete=items.filter(x=>['Complete','Signed Off'].includes(overall(x))).length,currentIssues=items.reduce((n,x)=>n+issues(x),0);return {g,items,completed,newIssues,currentAssigned,currentProgress,currentComplete,currentIssues}}).filter(r=>r.items.length||ready).sort((a,b)=>ready?(b.completed-a.completed)||(a.newIssues-b.newIssues)||(b.currentProgress-a.currentProgress)||a.g.name.localeCompare(b.g.name):a.g.name.localeCompare(b.g.name))}
function teamPerformanceTable(){let ready=analyticsTeamRangeReady(),invalid=analyticsTeamFrom&&analyticsTeamTo&&analyticsTeamFrom>analyticsTeamTo,rows=teamPerformanceData();if(invalid)return `<div class="analytics-range-prompt error"><b>Check the date range.</b><span>The FROM date must be before the TO date.</span></div>`;if(!ready)return `<div style="overflow:auto"><table class="analytics-table"><thead><tr><th>Team</th><th>Assigned</th><th>In progress</th><th>Complete</th><th>Open issues</th></tr></thead><tbody>${rows.map(r=>`<tr><td><b>${r.g.name}</b></td><td>${r.currentAssigned}</td><td>${r.currentProgress}</td><td>${r.currentComplete}</td><td>${r.currentIssues}</td></tr>`).join('')||'<tr><td colspan="5">No team data for this selection.</td></tr>'}</tbody></table></div>`;let activeRows=rows.filter(r=>r.currentAssigned||r.completed||r.newIssues),last=activeRows.length-1;return `<div class="team-performance-note"><b>Best → lowest performance</b><span>Ranked by completed stages / work items in the selected range. If teams are tied, fewer new issues ranks higher.</span></div><div style="overflow:auto"><table class="analytics-table team-ranking-table"><thead><tr><th>Rank</th><th>Team</th><th>Completed work</th><th>New issues</th><th>Current assigned</th><th>Current in progress</th></tr></thead><tbody>${activeRows.length?activeRows.map((r,i)=>`<tr class="${i===0?'team-rank-best':i===last&&last>0?'team-rank-lowest':''}"><td><span class="team-rank-number">${i+1}</span></td><td><b>${r.g.name}</b>${i===0?'<span class="team-rank-label best">Best in range</span>':i===last&&last>0?'<span class="team-rank-label lowest">Lowest in range</span>':''}</td><td><b>${r.completed}</b></td><td>${r.newIssues}</td><td>${r.currentAssigned}</td><td>${r.currentProgress}</td></tr>`).join(''):'<tr><td colspan="6">No team activity was recorded in this date range.</td></tr>'}</tbody></table></div>`}

function analyticsPage(){if(!hasPerm('analytics'))return `<div class="panel empty">You do not have permission to view Analytics.</div>`;let showFin=hasPerm('view_financials'),a=filtered(),progress=a.length?Math.round(a.reduce((n,x)=>n+itemProgress(x),0)/a.length):0,totalValue=a.reduce((n,x)=>n+itemValue(x),0),paid=a.reduce((n,x)=>n+itemPaid(x),0),contractPaid=a.reduce((n,x)=>n+itemContractPaid(x),0),overpayments=a.reduce((n,x)=>n+itemExtraPaid(x),0),outstanding=Math.max(0,totalValue-contractPaid),openIssues=a.reduce((n,x)=>n+issues(x),0),signedCount=a.filter(x=>overall(x)==='Signed Off').length,siteRows=activeSites().filter(s=>site==='all'||s.id===site).map(s=>{let items=a.filter(x=>x.site===s.id),sp=items.length?Math.round(items.reduce((n,x)=>n+itemProgress(x),0)/items.length):0,val=items.reduce((n,x)=>n+itemValue(x),0),pd=items.reduce((n,x)=>n+itemContractPaid(x),0),extra=items.reduce((n,x)=>n+itemExtraPaid(x),0);return `<tr><td><b>${s.name}</b></td><td>${items.length}</td><td><div class="analytics-bar"><i style="width:${sp}%"></i></div> ${sp}%</td><td>${items.reduce((n,x)=>n+issues(x),0)}</td>${showFin?`<td>${money(val)}</td><td>${money(pd)}</td><td>${money(extra)}</td>`:''}</tr>`}).join('');return `<div class="topbar"><div><h1>Analytics</h1><div class="sub">${showFin?'Management view of progress, payments, authorised extras, issues and team workload':'Management view of progress, issues and team workload'}</div></div><div class="actions">${site!=='all'?`<span class="site-context">⌖ ${siteName(site)}</span>`:''}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select></div></div><div class="analytics-grid"><div class="card"><div class="muted">Overall progress</div><div class="analytics-value">${progress}%</div></div><div class="card"><div class="muted">Open issues</div><div class="analytics-value">${openIssues}</div></div><div class="card"><div class="muted">Signed off</div><div class="analytics-value">${signedCount}/${a.length}</div></div>${showFin?`<div class="card"><div class="muted">Outstanding contract value</div><div class="analytics-value">${money(outstanding)}</div></div><div class="card"><div class="muted">Original contract / set value</div><div class="analytics-value">${money(totalValue)}</div></div><div class="card"><div class="muted">Contract payments</div><div class="analytics-value">${money(contractPaid)}</div></div><div class="card"><div class="muted">Overpayments / authorised extras</div><div class="analytics-value">${money(overpayments)}</div><div class="muted">Paid outside original contract value</div></div><div class="card"><div class="muted">Total payments recorded</div><div class="analytics-value">${money(paid)}</div></div><div class="card"><div class="muted">Payment coverage</div><div class="analytics-value">${totalValue?Math.min(100,Math.round(contractPaid/totalValue*100)):0}%</div></div>`:''}<div class="card"><div class="muted">Active work</div><div class="analytics-value">${a.filter(x=>overall(x)==='In progress').length}</div></div></div><div class="panel" style="padding:16px;margin-bottom:14px"><div class="sectionhead"><h2>Work completed by week</h2><span class="muted">Last 6 weeks</span></div><div class="analytics-trend">${weeklyCompletionData().map(w=>`<div class="weekbar"><i style="height:${Math.max(5,w.count*16)}px"></i><b>${w.count}</b><span>${w.label}</span></div>`).join('')}</div></div><div class="panel" style="padding:16px;margin-bottom:14px"><div class="sectionhead"><h2>Site performance</h2><span class="muted">${showFin?'Progress, issues, contract payments and extras':'Progress and issues'}</span></div><div style="overflow:auto"><table class="analytics-table"><thead><tr><th>Site</th><th>Items</th><th>Progress</th><th>Open issues</th>${showFin?'<th>Set value</th><th>Contract paid</th><th>Extras paid</th>':''}</tr></thead><tbody>${siteRows||(showFin?'<tr><td colspan="7">No site data.</td></tr>':'<tr><td colspan="4">No site data.</td></tr>')}</tbody></table></div></div><div class="panel team-workload-panel" style="padding:16px"><div class="sectionhead"><div><h2>Team workload</h2><span class="muted">${analyticsTeamRangeReady()?'Performance ranking for selected date range':'Quick view of current capacity and issues'}</span></div></div><div class="team-date-range"><div class="team-date-range-label"><b>Custom date range</b><span>Select both dates to rank teams from best to lowest performance.</span></div><div class="team-date-fields"><label>FROM<input class="input" type="date" value="${analyticsTeamFrom}" onchange="setAnalyticsTeamDate('from',this.value)"></label><label>TO<input class="input" type="date" value="${analyticsTeamTo}" onchange="setAnalyticsTeamDate('to',this.value)"></label>${analyticsTeamFrom||analyticsTeamTo?'<button class="btn light smallbtn" onclick="clearAnalyticsTeamRange()">Clear range</button>':''}</div></div>${teamPerformanceTable()}</div>`}


function diaryAmendmentsHtml(e){
 let a=e.amendments||[];if(!a.length)return '';
 return `<div class="diary-amendments"><div class="diary-amendment-heading"><span>Corrections / Amendments</span><b>${a.length}</b></div>${a.map((m,i)=>`<div class="diary-amendment"><div><b>Amendment ${i+1}</b><span>${m.createdAt?new Date(m.createdAt).toLocaleString():(m.date||'')} · ${m.by||'Unknown user'}</span></div><p>${calendarSafe(m.text||'')}</p></div>`).join('')}</div>`
}
function addDiaryAmendment(id){
 if(!hasPerm('manage_site_diary'))return toast('You do not have permission to manage the Site Diary');
 let e=(db.siteDiary||[]).find(e=>e.id===id);if(!e)return;
 modal=`<div class="modal"><div class="modalbox"><span class="calendar-kicker">PERMANENT RECORD</span><h2>Add Correction / Amendment</h2><p class="muted">${e.date} · ${siteName(e.siteId)} · original entry by ${e.by||'Unknown user'}</p><div class="diary-amendment-warning"><b>The original diary entry will not be changed.</b><span>Your amendment is appended underneath it with your name and exact time.</span></div><div class="field"><label>Correction / amendment</label><textarea id="diary-amendment-text" class="textarea" style="min-height:150px" placeholder="State clearly what needs correcting, clarifying or adding..."></textarea></div><button class="btn orange" onclick="saveDiaryAmendment('${id}')">✓ Add amendment</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function saveDiaryAmendment(id){
 if(!hasPerm('manage_site_diary'))return toast('You do not have permission to manage the Site Diary');
 let e=(db.siteDiary||[]).find(e=>e.id===id),text=(document.getElementById('diary-amendment-text')?.value||'').trim();if(!e||!text)return toast('Enter the correction or amendment');
 e.amendments=e.amendments||[];e.amendments.push({id:'sda'+Date.now(),text,date:iso(),createdAt:new Date().toISOString(),by:currentUser()?.name||'',userId:currentUser()?.id||''});
 save();audit('diary','site diary',e.id,`Site Diary amendment added for ${e.date}`,e.siteId);emitNotification('site',e.siteId,`Site Diary amended for ${e.date}`);modal=null;savedState('Diary amendment added');render()
}

function addDiaryEntry(){if(!hasPerm('manage_site_diary'))return toast('You do not have permission to manage the Site Diary');let defaultSite=site==='all'?(activeSites()[0]?.id||''):site;modal=`<div class="modal"><div class="modalbox"><h2>Add Site Diary Entry</h2><div class="two"><div class="field"><label>Date</label><input id="sd-date" class="input" type="date" value="${iso()}"></div><div class="field"><label>Site</label><select id="sd-site" class="select">${activeSites().map(s=>`<option value="${s.id}" ${s.id===defaultSite?'selected':''}>${s.name}</option>`).join('')}</select></div></div><div class="field"><label>Teams on site</label><input id="sd-teams" class="input" placeholder="e.g. Alpha Team, Mason Crew"></div><div class="field"><label>Work completed</label><textarea id="sd-work" class="textarea"></textarea></div><div class="field"><label>Deliveries</label><textarea id="sd-del" class="textarea"></textarea></div><div class="field"><label>Problems / delays</label><textarea id="sd-delay" class="textarea"></textarea></div><div class="field"><label>General notes</label><textarea id="sd-notes" class="textarea"></textarea></div><div class="field"><label>Photos</label><input id="sd-photos" class="input" type="file" accept="image/*" multiple></div>${cameraPicker('sd-photos','Take site photo')}<button class="btn orange" onclick="saveDiaryEntry()">Save diary</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveDiaryEntry(){
 if(!hasPerm('manage_site_diary'))return toast('You do not have permission to manage the Site Diary');
 let files=uploadFiles('sd-photos'),e={id:'sd'+Date.now(),date:document.getElementById('sd-date')?.value||iso(),siteId:document.getElementById('sd-site')?.value||'',teams:document.getElementById('sd-teams')?.value||'',work:document.getElementById('sd-work')?.value||'',deliveries:document.getElementById('sd-del')?.value||'',delays:document.getElementById('sd-delay')?.value||'',notes:document.getElementById('sd-notes')?.value||'',photos:[],amendments:[],by:currentUser()?.name||'',createdAt:new Date().toISOString()};
 let finish=()=>{clearCameraFiles('sd-photos');db.siteDiary.unshift(e);save();audit('diary','site diary',e.id,`Site Diary entry added for ${e.date}`,e.siteId);savedState('Diary entry saved');emitNotification('site',e.siteId,`Site diary added for ${e.date}`);modal=null;render()};
 if(!files.length)return finish();savePhotosToArray(files,e.photos,finish)
}

function diarySummaryText(e){
 let s=(e.work||e.notes||e.delays||'No summary entered').replace(/\s+/g,' ').trim();return s.length>105?s.slice(0,102)+'…':s
}
function openDiaryEntry(id){
 let e=(db.siteDiary||[]).find(e=>e.id===id);if(!e)return;
 modal=`<div class="modal"><div class="modalbox diary-detail-modal"><div class="sectionhead"><div><span class="calendar-kicker">SITE DIARY RECORD</span><h2>${e.date} · ${siteName(e.siteId)}</h2><div class="muted">Original entry · ${e.by||'Unknown user'}${e.createdAt?' · '+new Date(e.createdAt).toLocaleString('en-GB'):''}</div></div><button class="btn light" onclick="modal=null;render()">✕ Close</button></div><div class="diary-record-grid"><p><b>Teams</b><span>${calendarSafe(e.teams||'—')}</span></p><p><b>Work completed</b><span>${calendarSafe(e.work||'—')}</span></p><p><b>Deliveries</b><span>${calendarSafe(e.deliveries||'—')}</span></p><p><b>Problems / delays</b><span>${calendarSafe(e.delays||'—')}</span></p></div>${e.notes?`<div class="diary-general-note"><b>General notes</b><p>${calendarSafe(e.notes)}</p></div>`:''}${e.photos?.length?`<div class="diary-photo-grid">${e.photos.map(p=>`<img src="${mediaSrc(p.data)}"${pendingMediaAttr(p.data)} alt="${p.name}" onclick="showPhoto(mediaSrc('${p.data}'))">`).join('')}</div>`:''}${diaryAmendmentsHtml(e)}${hasPerm('manage_site_diary')?`<div class="diary-detail-actions"><button class="btn orange" onclick="addDiaryAmendment('${e.id}')">＋ Correction / Amendment</button></div>`:''}</div></div>`;render()
}

function siteDiaryPage(){
 if(!hasPerm('view_site_diary'))return `<div class="panel empty-help"><b>You do not have permission to view the Site Diary.</b></div>`;
 let entries=(db.siteDiary||[]).filter(e=>canAccessSite(e.siteId)&&(site==='all'||e.siteId===site)).sort((a,b)=>String(b.date||'').localeCompare(String(a.date||''))||String(b.createdAt||'').localeCompare(String(a.createdAt||'')));
 return `<div class="topbar"><div><h1>Site Diary</h1><div class="sub">Permanent daily operational record — corrections are appended, not overwritten</div>${trainingTip('Add one factual diary entry per working day. If something is wrong later, add a Correction / Amendment instead of rewriting the original.')}</div><div class="actions">${site!=='all'?`<span class="site-context">⌖ ${siteName(site)}</span>`:''}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select>${hasPerm('manage_site_diary')?'<button class="btn orange primary-action" onclick="addDiaryEntry()">＋ Add Diary Entry</button>':''}</div></div>${entries.length?`<div class="panel diary-history"><div class="diary-history-head"><div><h2>Diary History</h2><span>${entries.length} record${entries.length===1?'':'s'}</span></div><small>Open a day to view the full record, photos and amendments</small></div><div class="diary-history-list">${entries.map(e=>`<button class="diary-history-row" onclick="openDiaryEntry('${e.id}')"><div class="diary-history-date"><b>${new Date(e.date+'T12:00:00').toLocaleDateString('en-GB',{day:'2-digit',month:'short'})}</b><span>${new Date(e.date+'T12:00:00').getFullYear()}</span></div><div class="diary-history-main"><b>${siteName(e.siteId)}${e.teams?' · '+calendarSafe(e.teams):''}</b><span>${calendarSafe(diarySummaryText(e))}</span><small>${e.delays?`⚠ Delays recorded · `:''}${e.photos?.length?`${e.photos.length} photo${e.photos.length===1?'':'s'} · `:''}${(e.amendments||[]).length?`${e.amendments.length} amendment${e.amendments.length===1?'':'s'} · `:''}${e.by||'Unknown user'}</small></div><strong>View ›</strong></button>`).join('')}</div></div>`:smartEmpty('No diary entries yet.','Add the first daily record for the selected site.',hasPerm('manage_site_diary')?'＋ Add Diary Entry':'',hasPerm('manage_site_diary')?'addDiaryEntry()':'','✎')}`
}
function setDrawingSiteFilter(value){
 drawingSite=value||'all';
 site=drawingSite;
 drawingItem='all';
 render()
}
function resetDrawingFilters(){
 drawingSite='all';site='all';drawingHouseType='all';drawingItem='all';drawingQuery='';drawingMoreFilters=false;render()
}
function prepareDrawingLibrary(){
 if(!drawingSite||!['all',...activeSites().map(s=>s.id)].includes(drawingSite))drawingSite=site||'all';
 if(site!=='all'&&drawingSite==='all')drawingSite=site;
 drawingHouseType=drawingHouseType||'all';
 drawingItem=drawingItem||'all';
 if(!roleCanAccessPlots())drawingItem='all';
 drawingQuery=String(drawingQuery||'')
}

function drawingHouseTypes(){return [...new Set((db.siteDrawings||[]).filter(d=>canAccessSite(d.siteId)&&(drawingSite==='all'||d.siteId===drawingSite)).map(d=>d.houseType).filter(Boolean))].sort()}
function drawingItemsForSite(){if(!roleCanAccessPlots())return[];return (db.items||[]).filter(x=>canViewItem(x)&&(drawingSite==='all'||x.site===drawingSite))}
function addSiteDrawing(){if(!hasPerm('manage_drawings'))return toast('You do not have drawing upload access');let defaultSite=drawingSite==='all'?(activeSites()[0]?.id||''):drawingSite;modal=`<div class="modal"><div class="modalbox"><h2>Add Site Drawing</h2><p class="muted">Upload a master drawing for a site and house type, then reuse it on new or existing work items.</p><div class="two"><div class="field"><label>Site</label><select id="sdw-site" class="select">${activeSites().map(s=>`<option value="${s.id}" ${s.id===defaultSite?'selected':''}>${s.name}</option>`).join('')}</select></div><div class="field"><label>House type / drawing group</label><input id="sdw-house" class="input" placeholder="e.g. Type A, Elm, Birch"></div></div><div class="field"><label>Drawing name</label><input id="sdw-name" class="input" placeholder="e.g. Type A Ground Floor Plan"></div><div class="field"><label>Drawing file</label><input id="sdw-file" class="input" type="file" accept=".pdf,image/*"></div>${cameraPicker('sdw-file','Photograph drawing')}<div class="field"><label>Notes</label><textarea id="sdw-notes" class="textarea"></textarea></div><button class="btn green" onclick="saveSiteDrawing()">Upload drawing to library</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveSiteDrawing(){
 let f=uploadFiles('sdw-file')[0],siteId=document.getElementById('sdw-site')?.value||'',houseType=(document.getElementById('sdw-house')?.value||'').trim(),name=(document.getElementById('sdw-name')?.value||'').trim(),notes=(document.getElementById('sdw-notes')?.value||'').trim();if(!f||!siteId||!houseType||!name)return toast('Site, house type, drawing name and file are required');
 let d={id:'sdw'+Date.now(),siteId,houseType,name,notes,fileName:f.name||'Camera drawing.jpg',type:f.type||'image/jpeg',data:'',uploadedDate:iso(),uploadedAt:new Date().toISOString(),uploadedBy:currentUser()?.name||'',lastUsedAt:Date.now(),storage:'browser',takeoffs:[],takeoffCalibration:null};
 storeDrawingInBrowser(f).then(data=>{d.data=data;clearCameraFiles('sdw-file');db.siteDrawings.push(d);save();audit('drawing','drawing library',d.id,`Library drawing saved in browser — ${name}`,siteId);modal=null;savedState('Drawing saved in this browser');render()}).catch(()=>toast('Drawing could not be stored in this browser'))
}

async function openSiteDrawingLegacy(id){
 let d=(db.siteDrawings||[]).find(d=>d.id===id);if(!d)return;d.lastUsedAt=Date.now();save();if(!d.data)return toast('Drawing file is unavailable');
 let src=await resolveMediaRef(d.data);if(!src)return toast('This drawing is stored in a different browser. Open SiteSight on the device where it was uploaded.');
 let isPdf=d.type==='application/pdf'||String(d.fileName||'').toLowerCase().endsWith('.pdf'),tot=takeoffTotals(d);
 modal=`<div class="drawing-fullscreen" onclick="if(event.target===this){modal=null;render()}"><div class="drawing-fullscreen-inner"><div class="drawing-fullscreen-head"><div><b>${d.name}</b><div class="muted">${siteName(d.siteId)} · ${d.houseType} · ${String(d.data).startsWith('browser:')?'Stored in this browser':'Legacy/shared file'}</div>${(d.takeoffs||[]).length?`<div class="viewer-takeoff-line">📐 ${(d.takeoffs||[]).length} take-off item${(d.takeoffs||[]).length===1?'':'s'} · Net ${Math.max(0,tot.net).toFixed(2)} m²${hasPerm('view_financials')?` · ${money(tot.value)}`:''}</div>`:''}</div><div class="actions"><button class="btn blue" onclick="openDrawingTakeoff('library','${d.id}')">📐 Measure Drawing</button><button class="btn light" onclick="modal=null;render()">✕ Close</button></div></div><div class="drawing-fullscreen-body">${isPdf?`<iframe src="${src}" title="${d.name}"></iframe>`:`<img src="${src}" alt="${d.name}">`}</div></div></div>`;render()
}

function deleteSiteDrawing(id){if(!hasPerm('manage_drawings')||!hasPerm('permanent_delete'))return toast('Permanent Deletion permission is required');let d=db.siteDrawings.find(d=>d.id===id);if(!d)return;if(!confirm(`Delete drawing library "${d.name}"?`))return;let ref=d.data;db.siteDrawings=db.siteDrawings.filter(x=>x.id!==id);save();cleanupDrawingRef(ref);audit('drawing','drawing library',id,`Library drawing deleted — ${d.name}`,d.siteId);render()}
function assignSiteDrawing(id){if(!roleCanAccessPlots())return toast('Adding drawings to plot records requires Supervisor access or above');if(!hasPerm('manage_drawings'))return toast('You do not have permission to add drawings to work items');let d=db.siteDrawings.find(d=>d.id===id);if(!d)return;let candidates=db.items.filter(x=>x.site===d.siteId&&canViewItem(x));modal=`<div class="modal"><div class="modalbox"><h2>Transfer Drawing</h2><p class="muted"><b>${d.name}</b> · ${d.houseType}</p><div class="field"><label>Choose work item</label><select id="sdw-target" class="select">${candidates.length?candidates.map(x=>`<option value="${x.id}">${x.ref} · ${x.type}${x.houseType?' · '+x.houseType:''}</option>`).join(''):'<option value="">No work items on this site</option>'}</select></div><button class="btn orange" onclick="transferSiteDrawing('${id}')">Add to Work Item</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function transferSiteDrawing(id,targetOverride=''){if(!hasPerm('manage_drawings'))return toast('You do not have permission to add drawings to work items');let d=db.siteDrawings.find(d=>d.id===id),targetId=targetOverride||document.getElementById('sdw-target')?.value||'',x=db.items.find(x=>x.id===targetId);if(!d||!x)return toast('Choose a work item');x.drawings=x.drawings||[];x.drawings.push({id:'d'+Date.now(),name:d.name,originalName:d.fileName,notes:`Copied from Drawing Library${d.notes?' · '+d.notes:''}`,type:d.type,data:d.data,uploadedDate:iso(),sourceSiteDrawingId:d.id,takeoffs:JSON.parse(JSON.stringify(d.takeoffs||[])),takeoffCalibration:d.takeoffCalibration?JSON.parse(JSON.stringify(d.takeoffCalibration)):null});touch(x);d.lastUsedAt=Date.now();save();audit('drawing','work item',x.id,`${x.ref}: drawing library transferred — ${d.name}`,x.site);modal=null;savedState('Drawing added');toast('Drawing transferred to work item');render()}
function quickAddDrawingToNewWorkItem(id){let d=db.siteDrawings.find(d=>d.id===id);if(!d)return;newItem();setTimeout(()=>{let st=document.getElementById('ns');if(st)st.value=d.siteId;let ht=document.getElementById('nh');if(ht)ht.value=d.houseType;let box=document.querySelector('.modalbox');if(box)box.insertAdjacentHTML('beforeend',`<div class="role-template-note">After creating the work item, return to Drawing Library and transfer <b>${d.name}</b> to it.</div>`)},0)}

function toggleDrawingActionMenu(id){drawingActionMenu=drawingActionMenu===id?'':id;render()}

function siteDrawingsPage(){
 if(!hasPerm('view_drawings'))return `<div class="panel empty-help"><b>You do not have permission to view drawings.</b></div>`;
 prepareDrawingLibrary();
 let drawings=(db.siteDrawings||[]).filter(d=>canAccessSite(d.siteId)&&(drawingSite==='all'||d.siteId===drawingSite)&&(drawingHouseType==='all'||d.houseType===drawingHouseType)),items=drawingItemsForSite();
 if(drawingItem!=='all'){let item=db.items.find(x=>x.id===drawingItem);if(item)drawings=drawings.filter(d=>d.siteId===item.site&&(item.houseType?d.houseType===item.houseType:true))}
 if(drawingQuery.trim()){let q=drawingQuery.trim().toLowerCase();drawings=drawings.filter(d=>[d.name,d.houseType,siteName(d.siteId),d.notes,d.fileName].some(v=>String(v||'').toLowerCase().includes(q)))}
 let recent=(db.siteDrawings||[]).filter(d=>canAccessSite(d.siteId)&&d.lastUsedAt&&(drawingSite==='all'||d.siteId===drawingSite)).sort((a,b)=>Number(b.lastUsedAt||0)-Number(a.lastUsedAt||0)).slice(0,4),showRecent=!drawingQuery.trim()&&drawingHouseType==='all'&&drawingItem==='all'&&recent.length;
 let drawingCard=d=>`<div class="site-drawing-card"><div class="site-drawing-preview" onclick="openSiteDrawing('${d.id}')">${d.data&&d.type!=='application/pdf'&&!String(d.fileName||'').toLowerCase().endsWith('.pdf')?`<img src="${mediaSrc(d.data)}"${pendingMediaAttr(d.data)} alt="${d.name}">`:`<div class="pdf-icon">▤</div>`}</div><div class="site-drawing-body"><h3 onclick="openSiteDrawing('${d.id}')">${d.name}</h3><div class="muted">${siteName(d.siteId)} · ${d.houseType} · ${d.uploadedDate}${String(d.data||'').startsWith('browser:')?' · <span class="browser-drawing-badge">Browser</span>':''}</div>${d.notes?`<div class="muted" style="margin-top:5px">${d.notes}</div>`:''}${drawingTakeoffSummary(d)}<div class="site-drawing-actions"><button class="btn light smallbtn" onclick="openSiteDrawing('${d.id}')">Open Drawing</button><button class="btn blue smallbtn" onclick="openDrawingTakeoff('library','${d.id}')">📐 Measure</button>${hasPerm('manage_drawings')?`<div class="drawing-more-wrap"><button class="btn light smallbtn" onclick="toggleDrawingActionMenu('${d.id}')">More ▾</button>${drawingActionMenu===d.id?`<div class="drawing-more-menu"><button onclick="drawingActionMenu='';assignSiteDrawing('${d.id}')">Add to Work Item</button><button onclick="drawingActionMenu='';quickAddDrawingToNewWorkItem('${d.id}')">Create Work Item</button>${hasPerm('permanent_delete')?`<button class="danger" onclick="drawingActionMenu='';deleteSiteDrawing('${d.id}')">Delete drawing</button>`:''}</div>`:''}</div>`:''}</div></div></div>`;
 return `<div class="topbar"><div><h1>Drawing Library</h1><div class="sub">Reusable master drawings for your sites and house types</div><div class="browser-storage-notice"><b>Trial drawing storage:</b> drawing files are held in this browser; the Drawing Library record still appears in shared SiteSight.</div>${selectedSiteHeading()}${trainingTip("Drawing Library holds reusable master drawings. Plot Drawings belong to one work item.")}${trainingTip('Search by drawing name, house type, site or notes.')}</div><div class="actions">${hasPerm('manage_drawings')?'<button class="btn orange primary-action" onclick="addSiteDrawing()">＋ Upload Drawing</button>':''}</div></div><div class="panel" style="padding:15px"><div class="drawing-filterbar"><div class="drawing-search"><input class="input" placeholder="Search drawings..." value="${drawingQuery}" oninput="drawingQuery=this.value;render()"><button class="btn light" onclick="drawingQuery='';render()">Clear</button></div><select class="select" onchange="setDrawingSiteFilter(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${drawingSite===s.id?'selected':''}>${s.name}</option>`).join('')}</select><button class="simple-filter-toggle" onclick="drawingMoreFilters=!drawingMoreFilters;render()">More filters ${drawingMoreFilters?'▲':'▼'}</button><div class="simple-filter-extra ${drawingMoreFilters?'show':''}"><select class="select" onchange="drawingHouseType=this.value;render()"><option value="all">All house types</option>${drawingHouseTypes().map(t=>`<option value="${t}" ${drawingHouseType===t?'selected':''}>${t}</option>`).join('')}</select><select class="select" onchange="drawingItem=this.value;render()"><option value="all">All work items</option>${items.map(x=>`<option value="${x.id}" ${drawingItem===x.id?'selected':''}>${x.ref} · ${x.type}${x.houseType?' · '+x.houseType:''}</option>`).join('')}</select><button class="btn light" onclick="resetDrawingFilters()">Reset</button></div></div>${showRecent?`<div class="recent-drawings"><div class="sectionhead"><div><h3>Recently Used</h3><div class="muted">Quickly reopen drawings you have used most recently</div></div></div><div class="recent-drawing-grid">${recent.map(drawingCard).join('')}</div></div>`:''}<div class="sectionhead drawing-all-head"><h3>${showRecent?'All Drawings':'Drawings'}</h3><span class="mini">${drawings.length}</span></div><div class="site-drawings-grid">${drawings.length?drawings.map(drawingCard).join(''):smartEmpty('No drawings match this view.','Change the filters, or add a drawing to the Library.',hasPerm('manage_drawings')?'＋ Upload Drawing':'',hasPerm('manage_drawings')?'addSiteDrawing()':'','▤')}</div></div>`
}
function aboutSiteSightPage(){return `<div class="topbar"><div><h1>About SiteSight</h1><div class="sub">Product ownership and legal notice</div></div></div><div class="about-legal-wrap"><div class="panel about-hero"><div class="about-mark">S</div><div><span class="about-kicker">SITESIGHT</span><h2>Construction field management</h2><p>SiteSight is proprietary software created and owned by Bradley James Pearce.</p></div></div><div class="panel about-legal-card"><div class="sectionhead"><div><h2>Ownership</h2><div class="muted">Intellectual property notice</div></div></div><p><b>© ${authYear()} Bradley James Pearce. All rights reserved.</b></p><p>SiteSight, including its software, interface design, workflows, written content and associated materials, is the property of Bradley James Pearce unless otherwise stated.</p><p>Access to SiteSight does not transfer ownership of the software or any associated intellectual property.</p></div><div class="panel about-legal-card"><div class="sectionhead"><div><h2>Access</h2><div class="muted">Use of the application</div></div></div><p>Access to SiteSight is provided at the discretion of the owner and may be suspended or withdrawn where appropriate.</p><p class="about-legal-note">This notice is intended to make ownership clear. Formal licence terms, privacy terms and commercial terms can be added separately if SiteSight is distributed externally or offered as a paid service.</p></div><div class="panel about-legal-card"><div class="sectionhead"><div><h2>Owner</h2><div class="muted">SiteSight intellectual property</div></div></div><div class="about-owner"><span>Owner</span><b>Bradley James Pearce</b><span>Product</span><b>SiteSight</b><span>Status</span><b>Proprietary software</b></div></div></div>`}

function morePage(){let options=[['guide','?','New User Guide','Role-specific help and guided walkthrough'],['about','©','About SiteSight','Ownership and legal notice']],mobile=window.matchMedia&&window.matchMedia('(max-width:720px)').matches;if(mobile&&hasPerm('view_drawings'))options.push(['site-drawings','▤','Drawing Library','Reusable master drawings']);if(mobile&&hasPerm('view_calendar'))options.push(['calendar','▣','Calendar','Completed work and future notes']);if(hasPerm('view_attendance')||hasPerm('clock_attendance'))options.push(['attendance','◷',hasPerm('view_attendance')?'Time & Attendance':'My Time','Clock in/out, weekly hours and attendance']);if(hasPerm('view_site_diary'))options.push(['diary','✎','Site Diary','Daily site record']);if(hasPerm('view_financials'))options.push(['payments-history','£','Payments History','All payments by date, work item and team']);if(hasPerm('analytics'))options.push(['analytics','◒','Analytics','Management overview']);if(hasPerm('audit'))options.push(['audit','≡','Audit Trail','Change history']);if(hasPerm('manage_sites')||hasPerm('view_archives')||hasPerm('manage_gangs')||hasPerm('manage_workitems')||hasPerm('manage_users')||hasPerm('reset_passwords'))options.push(['admin','⚙','Admin','Setup and permissions']);return `<div class="topbar"><div><h1>More</h1><div class="sub">Extra tools kept out of the everyday workflow</div></div></div><div class="more-grid">${options.map(o=>`<div class="card more-card" onclick="goNav('${o[0]}')"><h3>${o[1]} ${o[2]}</h3><p class="muted">${o[3]}</p></div>`).join('')||'<div class="empty-help"><b>No additional tools available.</b></div>'}</div>`}

function guideAccessibleSitesText(){
 let ids=userSiteIds();
 if(canDirector()||!ids.length)return 'All active sites';
 let names=ids.map(siteName).filter(Boolean);
 return names.length?names.join(', '):'Assigned sites'
}
function guideSteps(){
 let steps=[];
 const add=(when,id,icon,title,summary,bullets,target,label)=>{if(when)steps.push({id,icon,title,summary,bullets,target,label})};
 add(hasPerm('dashboard'),'dashboard','⌂','Your Dashboard',
   'Start here for a quick picture of today’s activity and anything that needs attention.',
   ['Use the Site selector to focus the whole app on one development.','Today’s Summary shows work updated today; hover the circular chart for the detail behind each colour.','Needs Attention highlights work that needs action without changing the underlying Work Item.'],
   'dashboard','Open Dashboard');
 add(hasPerm('work_items'),'work','▣','Work Items',
   'Work Items are the main job records in SiteSight.',
   [
    'Search by reference, type, site or team and click a card to open the full job record.',
    hasPerm('update_progress')?'Update lift/stage progress and add Work Photos directly from site.':'You can view progress, dates, notes and the work history available to your role.',
    hasPerm('manage_workitems')?'You can create Work Items and manage their setup.':'Creation and setup controls are hidden because they are not part of your access.',
    hasPerm('sign_off')?'You can sign off completed work when it is ready.':'Final sign-off is handled by an authorised role.'
   ],
   'work-items','Open Work Items');
 add(hasPerm('view_issues'),'issues','⚠','Issues',
   'Use Issues to record problems clearly and keep the resolution visible.',
   [
    hasPerm('raise_issues')?'You can raise a new issue and take photos directly from a phone or tablet.':'You can review issues raised against the sites you can access.',
    hasPerm('manage_issues')?'You can manage and resolve issues when they are dealt with.':'Issue resolution is controlled by another authorised role.',
    'Click an issue to see its details, notes and photos.'
   ],
   'issues','Open Issues');
 add(hasPerm('gangs'),'teams','♟','Teams',
   'Teams show who is carrying the workload across your accessible sites.',
   [
    'Open a team to see its assigned work.',
    hasPerm('manage_gangs')?'You can create and edit teams and maintain team members.':'Team setup is read-only for your role.',
    'Team-member records can include notes and on-site photos.'
   ],
   'gangs','Open Teams');
 add(hasPerm('view_attendance')||hasPerm('clock_attendance'),'attendance','◷','Time & Attendance',
   hasPerm('view_attendance')?'See who is on site now, review weekly hours and manage verified attendance.':'Clock in and out with SiteSight location confirmation.',
   [
    hasPerm('clock_attendance')?'Clock In and Clock Out request your phone location only at the moment you press the button.':'You can review attendance but your account is not linked for self clocking.',
    hasPerm('manage_attendance')?'Supervisor entries and corrections are clearly labelled and permanently audited.':'Only authorised management can correct times.',
    hasPerm('approve_timesheets')?'Weekly timesheets can be approved and corrected records require re-approval.':'Weekly approval is handled by an authorised role.'
   ],
   'attendance','Open Time & Attendance');
 add(hasPerm('view_drawings'),'drawings','▤','Drawings',
   'Drawing Library stores reusable master drawings; Plot Drawings belong to a specific Work Item.',
   [
    'Use search and Site filtering to find drawings quickly.',
    hasPerm('manage_drawings')?'You can upload files, photograph a paper drawing with a phone and attach Library drawings to Work Items.':'You can open the drawings available to your role.',
    hasPerm('permanent_delete')?'You also have permission for permanent drawing deletion.':'Permanent deletion is deliberately restricted.'
   ],
   'site-drawings','Open Drawings');
 add(hasPerm('view_calendar'),'calendar','▣','Calendar',
   'Calendar combines upcoming site notes with completed-work dates.',
   [
    hasPerm('manage_calendar')?'Add deliveries, inspections, meetings and other future site notes.':'You can review calendar information but not change it.',
    'Use FROM and TO dates when you want to view the completion timeline for a period.',
    hasPerm('download_reports')?'You can also download dated site reports where available.':'Report downloads are not included in your role.'
   ],
   'calendar','Open Calendar');
 add(hasPerm('view_site_diary'),'diary','✎','Site Diary',
   'The Site Diary is the day-by-day record of what happened on site.',
   [
    hasPerm('manage_site_diary')?'Record work, teams, deliveries, delays, notes and take photos while on site.':'You can review the diary history for your accessible sites.',
    'Keep entries concise and factual so the history stays useful later.'
   ],
   'diary','Open Site Diary');
 add(hasPerm('view_financials'),'commercial','£','Commercial & Payments',
   'Your role can see financial information attached to the work.',
   [
    'Original contract values and approved Variations are kept separate.',
    hasPerm('record_payments')?'You can record payments against original work and approved Variations.':'Payment recording is controlled by another role.',
    hasPerm('manage_payments')?'You can also remove/correct payment records when required.':'Existing payment records cannot be removed with your current access.',
    hasPerm('manage_variations')?(hasPerm('approve_variations')?'You can create and approve Variations.':'You can create Variations; they remain Awaiting Approval until an authorised user approves them.'):'Variation management is not included in your role.'
   ],
   'payments-history','Open Payments History');
 add(hasPerm('analytics'),'analytics','◒','Analytics',
   'Analytics gives management a read-only view across the information already recorded in SiteSight.',
   [
    'Use the Site filter when you want to compare a single development.',
    'Team workload includes a custom date range for ranking team performance.',
    hasPerm('view_financials')?'Financial analytics are visible because your role includes View Financials.':'Financial values remain hidden from your analytics view.'
   ],
   'analytics','Open Analytics');
 add(hasPerm('audit'),'audit','≡','Audit Trail',
   'Audit Trail shows important changes and who made them.',
   ['Use it when you need to understand the history behind a change.','The Audit Trail is view-only.'],
   'audit','Open Audit Trail');
 let adminAccess=hasPerm('manage_sites')||hasPerm('view_archives')||hasPerm('manage_gangs')||hasPerm('manage_workitems')||hasPerm('manage_users')||hasPerm('reset_passwords');
 add(adminAccess,'admin','⚙','Admin',
   'Admin is for setup and management rather than normal daily site work.',
   [
    hasPerm('manage_sites')?'You can manage active Sites.':'Site setup is not part of your access.',
    hasPerm('view_archives')?(hasPerm('archive_sites')?'You can view, archive and restore developments.':'You can view archived developments but cannot archive/restore them.'):'Site Archives are not included in your role.',
    hasPerm('manage_users')?'You can create users, choose a role template and customise permissions only when necessary.':'User administration is not included in your role.',
    hasPerm('permanent_delete')?'Your role includes Permanent Deletion. Use it only for genuinely destructive actions.':'Permanent deletion is protected from your role.'
   ],
   'admin','Open Admin');
 steps.push({id:'settings',icon:'⚙',title:'Settings & Help',
   summary:'A few useful controls are always close by.',
   bullets:[
    'Use the cog beside your profile for notifications, Training Mode, password changes and logout.',
    'Training Mode adds short hints around the app and can be switched off whenever you are comfortable.',
    'This New User Guide is always available again from More — useful if you forget something or a shared account is being used.'
   ],
   target:'more',label:'Open More'});
 return steps
}
function guidePermissionsSummary(){
 let groups=permissionGroups.map(g=>{
   let allowed=g.items.filter(([k])=>hasPerm(k)).map(([,l])=>l);
   return allowed.length?`<div class="guide-access-group"><b>${g.name}</b><span>${allowed.join(' · ')}</span></div>`:''
 }).join('');
 return groups||'<div class="muted">Your account uses a limited view-only setup.</div>'
}
function openUserGuide(firstRun=false){
 if(!currentUser())return;
 if(view!=='guide')guideReturnView=view||'more';
 guideActive=false;guideStep=0;guideFirstRun=!!firstRun;
 selected=null;selectedGang=null;modal=null;view='guide';render()
}
function markGuideSeen(){
 let u=currentUser();if(!u)return;
 u.onboardingSeen=true;u.onboardingCompletedAt=u.onboardingCompletedAt||new Date().toISOString();save()
}

function closeUserGuide(){
 if(guideFirstRun&&!currentUser()?.onboardingSeen)return skipUserGuide();
 guideActive=false;guideFirstRun=false;guideStep=0;
 view=guideReturnView&&guideReturnView!=='guide'?guideReturnView:'more';
 render()
}

function skipUserGuide(){
 markGuideSeen();guideActive=false;guideFirstRun=false;
 view=hasPerm('dashboard')?'dashboard':hasPerm('work_items')?'work-items':'more';render();
 toast('Guide is always available from More')
}
function startGuidedTour(){
 let steps=guideSteps();if(!steps.length)return skipUserGuide();
 guideActive=true;guideFirstRun=false;guideStep=0;
 selected=null;selectedGang=null;modal=null;
 view=steps[0].target||'more';render()
}
function guideGoTo(i){
 let steps=guideSteps();if(!steps.length)return;
 guideStep=Math.max(0,Math.min(i,steps.length-1));
 selected=null;selectedGang=null;modal=null;
 view=steps[guideStep].target||'more';render()
}
function guideNext(){
 let steps=guideSteps();
 if(guideStep>=steps.length-1)return finishGuidedTour();
 guideGoTo(guideStep+1)
}
function guidePrev(){guideGoTo(guideStep-1)}
function finishGuidedTour(){
 markGuideSeen();guideActive=false;guideFirstRun=false;
 view=hasPerm('dashboard')?'dashboard':hasPerm('work_items')?'work-items':'more';render();
 toast('New User Guide complete')
}
function guideOverlay(){
 if(!guideActive)return '';
 let steps=guideSteps(),s=steps[guideStep];if(!s)return '';
 return `<div class="guide-coach"><div class="guide-coach-progress"><span>NEW USER GUIDE</span><div><b>${guideStep+1} / ${steps.length}</b><button class="guide-coach-close" onclick="closeUserGuide()" title="Close guide">×</button></div></div><div class="guide-coach-title"><i>${s.icon}</i><div><h3>${s.title}</h3><p>${s.summary}</p></div></div><ul>${s.bullets.filter(Boolean).map(b=>`<li>${b}</li>`).join('')}</ul><div class="guide-coach-actions"><button class="btn light smallbtn" onclick="guideActive=false;openUserGuide(false)">Guide overview</button><button class="btn light smallbtn" onclick="closeUserGuide()">Close guide</button>${guideStep?'<button class="btn light smallbtn" onclick="guidePrev()">← Back</button>':'<span></span>'}<button class="btn blue smallbtn" onclick="guideNext()">${guideStep===steps.length-1?'Finish guide':'Next →'}</button></div></div>`
}
function newUserGuidePage(){
 let u=currentUser(),steps=guideSteps(),first=guideFirstRun&&!u?.onboardingSeen;
 return `<div class="topbar guide-topbar"><div><h1>${first?'Welcome to SiteSight':'New User Guide'}</h1><div class="sub">${first?'A quick introduction built around your access':'Your role-specific SiteSight help and quick reference'}</div></div><div class="actions">${!first?'<button class="btn blue" onclick="startGuidedTour()">▶ Start guided tour</button>':''}<button class="btn light" onclick="closeUserGuide()">✕ Close guide</button></div></div><div class="guide-welcome ${first?'first-run':''}"><div class="guide-welcome-main"><span class="guide-kicker">${u?.role||'SiteSight User'}</span><h2>${first?`Hi ${String(u?.name||'').split(' ')[0] || 'there'}, let’s get you started.`:'Your SiteSight workspace'}</h2><p>${first?'This guide only shows the features your account can access. It takes around two minutes and uses the real SiteSight screens — no generic training video to remember.':'Use this page as a quick reference, or restart the guided walkthrough at any time.'}</p><div class="guide-role-facts"><span><b>${steps.length}</b> guide sections</span><span><b>${guideAccessibleSitesText()}</b> site access</span><span><b>${trainingMode?'On':'Off'}</b> Training Mode</span></div><div class="guide-welcome-actions"><button class="btn orange" onclick="startGuidedTour()">Start ${first?'my ':''}guided tour →</button>${first?'<button class="btn light" onclick="skipUserGuide()">Skip — I’ll explore myself</button>':''}</div>${first?'<div class="guide-return-note">You can reopen this guide later from <b>More → New User Guide</b>.</div>':''}</div><div class="guide-welcome-art"><div class="guide-art-ring"><b>SiteSight</b><span>Know what needs doing.<br>Know who is doing it.</span></div></div></div><div class="guide-layout"><div class="panel guide-section-panel"><div class="sectionhead"><div><h2>What you can use</h2><div class="muted">Based on your ${u?.role||'current'} role and individual permissions</div></div></div><div class="guide-access">${guidePermissionsSummary()}</div></div><div class="panel guide-section-panel"><div class="sectionhead"><div><h2>Your guide</h2><div class="muted">Only relevant areas are included</div></div><button class="btn light smallbtn" onclick="startGuidedTour()">Start walkthrough</button></div><div class="guide-step-list">${steps.map((s,i)=>`<button class="guide-step-row" onclick="guideStep=${i};guideActive=true;guideFirstRun=false;view='${s.target||'more'}';render()"><i>${s.icon}</i><span><b>${s.title}</b><small>${s.summary}</small></span><strong>›</strong></button>`).join('')}</div></div><div class="panel guide-section-panel guide-field-tip"><div><span>📱</span><h3>Using SiteSight on site?</h3><p>Where your permissions allow photos, use the <b>Take Photo</b> button to open the phone camera for Issues, Work Photos, Site Diary and drawing updates.</p></div><div><span>💡</span><h3>Need reminders?</h3><p>Keep <b>Training Mode</b> switched on from your profile settings. It adds short practical hints without changing how the app works.</p></div></div></div>`
}


function globalSearchResults(q){
 q=String(q||'').trim().toLowerCase();
 if(q.length<2)return [];
 let out=[];
 if(hasPerm('work_items')){
  db.items.filter(x=>canAccessSite(x.site)&&!db.sites.find(s=>s.id===x.site)?.archived)
   .filter(x=>[x.ref,x.type,x.houseType,siteName(x.site),gangName(x.gang)].some(v=>String(v||'').toLowerCase().includes(q)))
   .slice(0,7).forEach(x=>out.push({kind:'Work Item',icon:'▣',title:x.ref,meta:`${x.type} · ${siteName(x.site)} · ${gangName(x.gang)}`,action:`closeGlobalSearch();openItem('${x.id}')`}))
 }
 if(hasPerm('view_issues')){
  db.items.filter(x=>canAccessSite(x.site)&&!db.sites.find(s=>s.id===x.site)?.archived).forEach(x=>
   (x.issues||[]).filter(i=>[i.title,i.details,i.notes,x.ref,siteName(x.site)].some(v=>String(v||'').toLowerCase().includes(q)))
    .slice(0,5).forEach(i=>out.push({kind:'Issue',icon:'⚠',title:i.title,meta:`${x.ref} · ${siteName(x.site)} · ${i.status}`,action:`closeGlobalSearch();viewIssue('${x.id}','${i.id}')`}))
  )
 }
 if(hasPerm('gangs')){
  db.gangs.filter(g=>[g.name,...(g.members||[]).flatMap(m=>[m.name,m.role])].some(v=>String(v||'').toLowerCase().includes(q)))
   .slice(0,5).forEach(g=>out.push({kind:'Team',icon:'♟',title:g.name,meta:`${(g.members||[]).length} team member(s)`,action:`closeGlobalSearch();openGang('${g.id}')`}))
 }
 if(hasPerm('view_drawings')){
  (db.siteDrawings||[]).filter(d=>canAccessSite(d.siteId)&&[d.name,d.houseType,d.notes,d.fileName,siteName(d.siteId)].some(v=>String(v||'').toLowerCase().includes(q)))
   .slice(0,6).forEach(d=>out.push({kind:'Drawing',icon:'▤',title:d.name,meta:`${siteName(d.siteId)} · ${d.houseType||'Drawing'}`,action:`closeGlobalSearch();openSiteDrawing('${d.id}')`}))
 }
 return out.slice(0,18)
}
function globalSearchResultsHtml(q){
 let rows=globalSearchResults(q);
 if(String(q||'').trim().length<2){
  let recent=recentViewedItems();
  return `<div class="global-search-start"><b>Search across SiteSight</b><p>Find Work Items, Issues, Teams and Drawings without needing to know which screen they are on.</p>${recent.length?`<div class="global-search-recent"><span>RECENT WORK</span>${recent.map(x=>`<button onclick="closeGlobalSearch();openItem('${x.id}')"><b>${x.ref}</b><small>${x.type} · ${siteName(x.site)}</small></button>`).join('')}</div>`:''}</div>`
 }
 if(!rows.length)return smartEmpty('Nothing found','Try a reference, issue title, team name or drawing name.','','','⌕');
 let last='';
 return rows.map(r=>{let head=r.kind!==last?`<div class="global-result-group">${r.kind}s</div>`:'';last=r.kind;return `${head}<button class="global-result-row" onclick="${r.action}"><i>${r.icon}</i><span><b>${r.title}</b><small>${r.meta}</small></span><strong>›</strong></button>`}).join('')
}
function updateGlobalSearch(v){
 globalSearchQuery=v;
 let e=document.getElementById('global-search-results');
 if(e)e.innerHTML=globalSearchResultsHtml(v)
}
function openGlobalSearch(){
 globalSearchQuery='';
 modal=`<div class="modal global-search-modal" onclick="if(event.target===this)closeGlobalSearch()"><div class="global-search-box"><div class="global-search-head"><span>⌕</span><input id="global-search-input" autocomplete="off" placeholder="Search SiteSight…" oninput="updateGlobalSearch(this.value)" onkeydown="if(event.key==='Escape')closeGlobalSearch()"><button onclick="closeGlobalSearch()">×</button></div><div id="global-search-results" class="global-search-results">${globalSearchResultsHtml('')}</div><div class="global-search-foot"><span>Searches only the sites and information your role can access.</span><kbd>Esc</kbd></div></div></div>`;
 render();
 requestAnimationFrame(()=>document.getElementById('global-search-input')?.focus())
}
function closeGlobalSearch(){globalSearchQuery='';modal=null;render()}

function hasMore(){return !!currentUser()}
function nav(){let n=[];if(hasPerm('dashboard'))n.push(['dashboard','⌂','Dashboard']);if(hasPerm('work_items'))n.push(['work-items','▣','Work Items']);if(hasPerm('view_issues'))n.push(['issues','⚠','Issues']);if(hasPerm('gangs'))n.push(['gangs','♟','Teams']);if(hasPerm('view_drawings'))n.push(['site-drawings','▤','Drawings']);if(hasPerm('view_calendar'))n.push(['calendar','▣','Calendar']);let main=n.map(x=>`<button class="${view===x[0]?'active':''}" onclick="goNav('${x[0]}')"><span>${x[1]}</span>${x[2]}${x[0]==='issues'&&db.items.reduce((n,x)=>n+issues(x),0)?'<span class="count">'+db.items.reduce((n,x)=>n+issues(x),0)+'</span>':''}</button>`).join('');return main+(hasMore()?`<button class="${view==='more'||['guide','about','attendance','diary','payments-history','analytics','audit','admin','dayworks','weekly-report'].includes(view)?'active':''}" onclick="goNav('more')"><span>•••</span>More</button>`:'')}
function mobileNav(){let n=[];if(hasPerm('dashboard'))n.push(['dashboard','⌂','Dashboard']);if(hasPerm('work_items'))n.push(['work-items','▣','Work']);if(hasPerm('view_issues'))n.push(['issues','⚠','Issues']);if(hasPerm('gangs'))n.push(['gangs','♟','Teams']);n=n.slice(0,4);if(hasMore())n.push(['more','•••','More']);return n.map(x=>`<button class="${view===x[0]||(x[0]==='dashboard'&&view==='needs-attention')||x[0]==='more'&&['guide','about','site-drawings','calendar','attendance','diary','payments-history','analytics','audit','admin','dayworks','weekly-report'].includes(view)?'active':''}" onclick="goNav('${x[0]}')"><span>${x[1]}</span>${x[2]}</button>`).join('')}
function shell(content){let u=currentUser();role=u?.role||'';document.getElementById('root').innerHTML=`<aside class="sidebar"><div class="brand"><div class="brand-logo-wrap"><img class="sitesight-logo" src="assets/sitesight-logo.png" alt="SiteSight"></div></div><button class="sidebar-global-search" onclick="openGlobalSearch()"><span>⌕</span><b>Search SiteSight</b><kbd>Ctrl K</kbd></button><div class="profile"><div class="userbar"><div class="avatar">${(u?.name||'?')[0]}</div><div style="min-width:0;flex:1"><b>${u?.name||''}</b><div class="muted" style="color:#b7c6d8">${u?.role||''}</div></div><div class="settingswrap"><button class="btn logoutbtn" onclick="toggleSettings(event)" title="Settings">⚙</button>${settingsOpen?`<div class="settingsmenu"><button onclick="notificationSettings()">🔔 Notifications ${unreadNotifications(u).length?`(${unreadNotifications(u).length})`:''}</button><button onclick="toggleTrainingMode(!trainingMode)">💡 Training mode ${trainingMode?'✓ On':'Off'}</button><button onclick="settingsOpen=false;openUserGuide(false)">? New User Guide</button><button onclick="settingsOpen=false;changeOwnPassword()">🔑 Change password</button><button class="logoutitem" onclick="settingsOpen=false;logout()">↪ Log out</button></div>`:''}</div></div></div><div class="nav">${nav()}</div><div class="company"><b>SiteSight</b><div>v1.76.0 · WORKFORCE FLOW DEMO</div>${syncIndicator()}</div></aside><main class="main"><div class="demo-mode-banner"><b>ENTERPRISE DEMO</b><span>4 active sites · 351 plots · full operational dataset</span></div>${content}</main><button class="mobile-global-search" onclick="openGlobalSearch()" aria-label="Search SiteSight">⌕</button><div class="mobilebar">${mobileNav()}</div>${modal||''}${guideOverlay()}`}
function toggleLoginPassword(id,btn){let e=document.getElementById(id);if(!e)return;let show=e.type==='password';e.type=show?'text':'password';if(btn)btn.textContent=show?'Hide':'Show'}
function authYear(){return new Date().getFullYear()}


function firstSetupScreen(){document.getElementById('root').innerHTML=`<div class="first-setup-page"><div class="first-setup-card"><img src="assets/sitesight-login-logo.png" alt="SiteSight"><span class="setup-kicker">FIRST SITE SETUP</span><h1>Set up SiteSight</h1><p>Create the first development and Director account. A short checklist will guide the remaining setup after sign-in.</p><div class="two"><div class="field"><label>Your full name</label><input id="setup-name" class="input" autocomplete="name" placeholder="e.g. Alex Smith"></div><div class="field"><label>Development / Site name</label><input id="setup-site" class="input" placeholder="e.g. Oak View"></div></div><div class="two"><div class="field"><label>Choose a username</label><input id="setup-user" class="input" autocomplete="username" placeholder="e.g. alex.smith"></div><div class="field"><label>Email address</label><input id="setup-email" class="input" type="email" autocomplete="email" placeholder="you@company.co.uk"></div></div><div class="field"><label>Create a password</label><div class="auth-input-wrap"><input id="setup-pass" class="input auth-input auth-password-input" type="password" autocomplete="new-password" placeholder="At least 8 characters"><button class="auth-password-toggle" onclick="toggleLoginPassword('setup-pass',this)" type="button">Show</button></div></div><button class="btn orange setup-button" onclick="completeFirstSetup()">Create SiteSight →</button><div class="setup-network-note"><b>${sharedMode?'Secure SiteSight service detected':'Local browser mode'}</b><span>${sharedMode?'Accounts and shared records will be protected by secure sign-in.':'For shared/mobile use, open the deployed SiteSight address.'}</span></div></div></div>`}
function completeFirstSetup(){let name=(document.getElementById('setup-name')?.value||'').trim(),siteNameValue=(document.getElementById('setup-site')?.value||'').trim(),username=(document.getElementById('setup-user')?.value||'').trim().toLowerCase(),email=(document.getElementById('setup-email')?.value||'').trim().toLowerCase(),password=document.getElementById('setup-pass')?.value||'';if(!name||!siteNameValue||!username||!email||!password)return toast('Complete all setup fields');if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))return toast('Enter a valid email address');if(password.length<8)return toast('Use a password with at least 8 characters');let sid='s'+Date.now(),uid='u'+Date.now();db={...cloneDB(seed),sites:[{id:sid,name:siteNameValue,archived:false,createdAt:new Date().toISOString(),createdBy:name,attendanceConfig:{enabled:true,latitude:null,longitude:null,radiusM:250,maxAccuracyM:75,maxShiftHours:12,expectedStartTime:'07:30',expectedEndTime:'16:30',graceMinutes:5,analyticsManualMinutes:8,analyticsSystemMinutes:1,analyticsAdminHourlyCost:25,locationSetAt:'',locationSetBy:''},timeZone:'Europe/London',stageTemplate:['1st Lift','2nd Lift (Joist)','3rd Lift (Band)','4th Lift (Top/Plate)','5th Lift (Gables)']}],users:[{id:uid,name,username,email,password,role:'Director',permissions:defaultPerms('Director'),allowedSites:[],allSitesAccess:true,notificationsEnabled:false,trainingMode:true,onboardingSeen:false,onboardingCompletedAt:'',loginCount:1,lastLoginAt:new Date().toISOString(),recentWorkItems:[],cameraIntroSeen:false,linkedMemberId:'',accountActive:true,disabledReason:'',disabledAt:'',disabledBy:'',permissionProfileMode:'role-default',permissionTemplateVersion:199,setupChecklistDismissed:false}]};currentUserId=uid;try{localStorage.setItem(CURRENT_USER_KEY,uid)}catch(e){};site=sid;role='Director';trainingMode=true;migrate(false);save();audit('setup','site',sid,`SiteSight created — ${siteNameValue}`,sid);view='dashboard';render()}

function loginScreen(){document.getElementById('root').innerHTML=`<div class="auth-shell"><section class="auth-brand-panel"><div class="auth-brand-content"><img class="auth-brand-logo" src="assets/sitesight-login-logo.png" alt="SiteSight"><div class="auth-brand-copy"><span class="auth-eyebrow">SITESIGHT ENTERPRISE DEMO</span><h1>Four live developments. One business view.</h1><p>351 plots, multi-site teams and realistic operational records — organised in one practical place.</p></div><div class="auth-feature-list"><div><i>✓</i><span><b>Built for site teams</b><small>Fast access to the information that matters on the job.</small></span></div><div><i>✓</i><span><b>Clear project visibility</b><small>See progress, issues and responsibilities without the clutter.</small></span></div><div><i>✓</i><span><b>Ready wherever you work</b><small>Designed for desktop, tablet and mobile use.</small></span></div></div></div><div class="auth-brand-footer">SiteSight · Construction field management</div></section><main class="auth-login-panel"><div class="auth-login-inner"><div class="auth-mobile-brand"><img src="assets/sitesight-login-logo.png" alt="SiteSight"></div><div class="auth-welcome"><span class="auth-eyebrow">WELCOME BACK</span><h2>Sign in to SiteSight</h2><p>Access your sites, teams and project records.</p></div><div class="auth-form"><div class="field"><label>Username</label><div class="auth-input-wrap"><span class="auth-input-icon">♙</span><input id="login-user" class="input auth-input" autocomplete="username" placeholder="Enter your username" autofocus onkeydown="if(event.key==='Enter'){event.preventDefault();document.getElementById('login-pass')?.focus()}"></div></div><div class="field"><div class="auth-label-row"><label>Password</label><button class="auth-text-link" onclick="forgotPassword()">Forgot password?</button></div><div class="auth-input-wrap"><span class="auth-input-icon">●</span><input id="login-pass" class="input auth-input auth-password-input" type="password" autocomplete="current-password" placeholder="Enter your password" onkeydown="if(event.key==='Enter'){event.preventDefault();login()}"><button class="auth-password-toggle" onclick="toggleLoginPassword('login-pass',this)" type="button">Show</button></div></div><button class="btn auth-login-btn" onclick="login()">Sign in <span>→</span></button></div><div class="demo-login-box"><span>ENTERPRISE DEMO</span><b>Four active developments · 351 plots</b><p>Use a demo role to explore SiteSight:</p><div><code>owner.demo</code><small>Director / full access</small></div><div><code>director</code><small>Director</small></div><div><code>site.manager</code><small>Site Manager</small></div><div><code>qs</code><small>Quantity Surveyor</small></div><div><code>worker.liam</code><small>Worker · GPS Clock In / Out</small></div><p class="demo-password">Password for all: <b>Demo2026!</b></p></div><div class="auth-security-note"><span>◈</span><p><b>Secure account access</b><br>Your access is controlled by your SiteSight role and permissions.</p></div><div class="auth-copyright"><b>© ${authYear()} Bradley James Pearce</b><span>SiteSight is proprietary software owned by Bradley James Pearce. All rights reserved.</span></div></div></main></div>`}
function forgotPassword(){document.getElementById('root').innerHTML=`<div class="auth-reset-page"><div class="auth-reset-card"><img class="auth-reset-logo" src="assets/sitesight-login-logo.png" alt="SiteSight"><button class="auth-back-link" onclick="loginScreen()">← Back to sign in</button><div class="auth-welcome"><span class="auth-eyebrow">ACCOUNT RECOVERY</span><h2>Reset your password</h2><p>Enter the email address registered to your SiteSight account.</p></div><div class="field"><label>Email address</label><div class="auth-input-wrap"><span class="auth-input-icon">@</span><input id="fp-email" class="input auth-input" type="email" autocomplete="email" placeholder="name@company.co.uk" autofocus onkeydown="if(event.key==='Enter'){event.preventDefault();requestPasswordReset()}"></div></div><button class="btn auth-login-btn" onclick="requestPasswordReset()">Continue <span>→</span></button><div class="auth-security-note compact"><span>◈</span><p>We'll verify the account before allowing the password to be changed.</p></div></div></div>`}
function requestPasswordReset(){let email=(document.getElementById('fp-email')?.value||'').trim().toLowerCase(),u=db.users.find(u=>(u.email||'').toLowerCase()===email);if(!email)return toast('Enter your registered email address');if(!u){showResetRequested(email,'');return}let code=String(Math.floor(100000+Math.random()*900000));u.resetCode=code;u.resetExpires=Date.now()+15*60*1000;save();showResetRequested(email,code)}
function showResetRequested(email,code){document.getElementById('root').innerHTML=`<div class="auth-reset-page"><div class="auth-reset-card"><img class="auth-reset-logo" src="assets/sitesight-login-logo.png" alt="SiteSight"><button class="auth-back-link" onclick="loginScreen()">← Back to sign in</button><div class="auth-welcome"><span class="auth-eyebrow">PASSWORD RESET</span><h2>Enter your recovery code</h2><p>A recovery request has been created for <b>${email}</b>.</p></div>${code?`<div class="auth-recovery-code"><span>Recovery code</span><b>${code}</b></div>`:''}<div class="field"><label>Recovery code</label><div class="auth-input-wrap"><span class="auth-input-icon">#</span><input id="fp-code" class="input auth-input" inputmode="numeric" maxlength="6" placeholder="6-digit code"></div></div><div class="field"><label>New password</label><div class="auth-input-wrap"><span class="auth-input-icon">●</span><input id="fp-new" class="input auth-input auth-password-input" type="password" autocomplete="new-password" placeholder="New password"><button class="auth-password-toggle" onclick="toggleLoginPassword('fp-new',this)" type="button">Show</button></div></div><div class="field"><label>Confirm new password</label><div class="auth-input-wrap"><span class="auth-input-icon">●</span><input id="fp-confirm" class="input auth-input auth-password-input" type="password" autocomplete="new-password" placeholder="Confirm password" onkeydown="if(event.key==='Enter'){event.preventDefault();completeForgotPassword()}"><button class="auth-password-toggle" onclick="toggleLoginPassword('fp-confirm',this)" type="button">Show</button></div></div><input id="fp-email-hidden" type="hidden" value="${email}"><button class="btn auth-login-btn" onclick="completeForgotPassword()">Reset password <span>→</span></button></div></div>`}
function completeForgotPassword(){let email=(document.getElementById('fp-email-hidden')?.value||'').trim().toLowerCase(),code=(document.getElementById('fp-code')?.value||'').trim(),nw=document.getElementById('fp-new')?.value||'',cf=document.getElementById('fp-confirm')?.value||'',u=db.users.find(u=>(u.email||'').toLowerCase()===email);if(!u||!u.resetCode||Date.now()>u.resetExpires||code!==u.resetCode)return toast('Reset code is invalid or has expired');if(nw.length<8)return toast('Use at least 8 characters');if(nw!==cf)return toast('New passwords do not match');u.password=nw;u.resetCode='';u.resetExpires=0;save();toast('Password changed');setTimeout(()=>loginScreen(),500)}
function login(){let un=(document.getElementById('login-user')?.value||'').trim().toLowerCase(),pw=document.getElementById('login-pass')?.value||'',u=db.users.find(u=>(u.username||'').toLowerCase()===un&&u.password===pw);if(!u)return toast('Incorrect username or password');currentUserId=u.id;try{localStorage.setItem(CURRENT_USER_KEY,u.id)}catch(e){};role=u.role;trainingMode=!!u.trainingMode;u.loginCount=Number(u.loginCount||0)+1;u.lastLoginAt=new Date().toISOString();save();if(site!=='all'&&!canAccessSite(site))site='all';selected=null;selectedGang=null;guideActive=false;guideStep=0;if(!u.onboardingSeen){openUserGuide(true)}else{view=hasPerm('dashboard')?'dashboard':hasPerm('work_items')?'work-items':'more';render()}setTimeout(deliverNotifications,100)}


function eventVisibleToUser(ev,u){if(!u||u.id===ev.actorId)return false;if(ev.siteId&&!((u.role==='Director')||!(u.allowedSites||[]).length||(u.allowedSites||[]).includes(ev.siteId)))return false;let p=u.permissions||{},director=u.role==='Director';if(ev.kind==='issue'&&!p.view_issues&&!director)return false;if((ev.kind==='payment'||ev.kind==='variation')&&!p.view_financials&&!director)return false;if(ev.kind==='drawing'&&!p.view_drawings&&!director)return false;if(ev.kind==='work'&&!p.work_items&&!director)return false;if((ev.kind==='site'||ev.kind==='signoff')&&!p.dashboard&&!director)return false;return true}
function emitNotification(kind,siteId,text,itemId=''){audit(kind,'update',itemId,text,siteId);let u=currentUser();db.notifications=db.notifications||[];let ev={id:'n'+Date.now()+Math.random().toString(16).slice(2),kind,siteId:siteId||'',text,itemId,actorId:u?.id||'',actorName:u?.name||'',createdAt:new Date().toISOString(),readBy:[]};db.notifications.unshift(ev);db.notifications=db.notifications.slice(0,250);save();deliverNotifications()}
function relevantNotifications(u=currentUser()){return (db.notifications||[]).filter(n=>eventVisibleToUser(n,u))}
function unreadNotifications(u=currentUser()){return relevantNotifications(u).filter(n=>!(n.readBy||[]).includes(u.id))}
async function enableNotifications(v){let u=currentUser();if(!u)return;u.notificationsEnabled=!!v;if(v&&'Notification' in window&&Notification.permission==='default'){try{let p=await Notification.requestPermission();if(p!=='granted')toast('Browser notifications were not allowed. In-app notifications will still be available.')}catch(e){}}save();render()}
function deliverNotifications(){let u=currentUser();if(!u?.notificationsEnabled)return;let unread=unreadNotifications(u);if(!unread.length)return;let latest=unread[0];if('Notification' in window&&Notification.permission==='granted'){try{new Notification('SiteSight update',{body:latest.text})}catch(e){}}}
function markNotificationsRead(){let u=currentUser();if(!u)return;relevantNotifications(u).forEach(n=>{n.readBy=n.readBy||[];if(!n.readBy.includes(u.id))n.readBy.push(u.id)});save();render()}
function notificationSettings(){let u=currentUser(),items=relevantNotifications(u).slice(0,30),unread=unreadNotifications(u).length;settingsOpen=false;modal=`<div class="modal"><div class="modalbox"><h2>Notification Settings</h2><div class="notify-row"><div><b>Automatic notifications</b><div class="muted">Notify me about updates I am permitted to view.</div></div><label class="switch"><input type="checkbox" ${u.notificationsEnabled?'checked':''} onchange="enableNotifications(this.checked)"><span></span></label></div><p class="muted" style="margin:10px 0">Notifications are filtered by your site visibility and feature permissions. On the shared trial server, updates are available to both authorised users. Browser notifications still depend on the device and browser settings.</p><div class="sectionhead"><h3>Recent notifications</h3>${unread?`<button class="btn light smallbtn" onclick="markNotificationsRead()">Mark ${unread} read</button>`:''}</div><div class="notification-list">${items.length?items.map(n=>`<div class="notification-item ${(n.readBy||[]).includes(u.id)?'':'unread'}"><b>${n.text}</b><div class="muted">${n.actorName||'SiteSight'} · ${new Date(n.createdAt).toLocaleString()}</div></div>`).join(''):'<div class="empty">No notifications yet.</div>'}</div><div style="margin-top:12px"><button class="btn" onclick="modal=null;render()">Close</button></div></div></div>`;render()}

function toggleSettings(e){if(e)e.stopPropagation();settingsOpen=!settingsOpen;render()}
function changeOwnPassword(){settingsOpen=false;let u=currentUser();if(!u)return;modal=`<div class="modal"><div class="modalbox"><h2>Change Password</h2><p class="muted">Update the password for <b>${u.name}</b>.</p><div class="field"><label>Current password</label><input id="cp-old" class="input" type="password"></div><div class="field"><label>New password</label><input id="cp-new" class="input" type="password"></div><div class="field"><label>Confirm new password</label><input id="cp-confirm" class="input" type="password"></div><button class="btn orange" onclick="saveOwnPassword()">Save new password</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveOwnPassword(){let u=currentUser(),old=document.getElementById('cp-old')?.value||'',nw=document.getElementById('cp-new')?.value||'',cf=document.getElementById('cp-confirm')?.value||'';if(old!==u.password)return toast('Current password is incorrect');if(nw.length<8)return toast('Use at least 8 characters');if(nw!==cf)return toast('New passwords do not match');u.password=nw;save();modal=null;toast('Password changed');render()}
function resetUserPassword(id){if(!(canDirector()||hasPerm('reset_passwords')))return toast('You do not have password reset access');let u=db.users.find(u=>u.id===id);if(!u)return;if(u.role==='Director'&&!canDirector())return toast('Only a Director can reset a Director password');modal=`<div class="modal"><div class="modalbox"><h2>Reset Password</h2><p>Set a new password for <b>${u.name}</b>.</p><div class="field"><label>New password</label><input id="rp-new" class="input" type="password"></div><div class="field"><label>Confirm new password</label><input id="rp-confirm" class="input" type="password"></div><button class="btn orange" onclick="saveResetPassword('${u.id}')">Reset password</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveResetPassword(id){if(!(canDirector()||hasPerm('reset_passwords')))return;let u=db.users.find(u=>u.id===id),nw=document.getElementById('rp-new')?.value||'',cf=document.getElementById('rp-confirm')?.value||'';if(!u)return;if(nw.length<8)return toast('Use at least 8 characters');if(nw!==cf)return toast('Passwords do not match');u.password=nw;save();modal=null;toast('Password reset');render()}
function logout(){currentUserId='';role='';try{localStorage.removeItem(CURRENT_USER_KEY)}catch(e){};selected=null;selectedGang=null;modal=null;loginScreen()}
function render(){if(!db.users?.length){firstSetupScreen();return}if(!currentUser()){loginScreen();return}role=currentUser().role;let c=selected?detail(db.items.find(x=>x.id===selected)):selectedGang?gangDashboard(db.gangs.find(g=>g.id===selectedGang)):view==='dashboard'?dashboard():view==='needs-attention'?needsAttentionPage():view==='work-items'?workItems():view==='guide'?newUserGuidePage():view==='about'?aboutSiteSightPage():view==='more'?morePage():view==='payments-history'?paymentsHistoryPage():view==='site-drawings'?siteDrawingsPage():view==='issues'?issuesPage():view==='calendar'?calendarPage():view==='diary'?siteDiaryPage():view==='gangs'?gangsPage():view==='attendance'?attendancePage():view==='analytics'?analyticsPage():view==='audit'?auditPage():view==='activity'?activityPage():view==='signed-off'?signedPage():adminPage();shell(c);updateSyncUI()}
function openItem(id){let item=db.items.find(row=>row.id===id);if(!canViewItem(item))return toast('Plot cards are available to Supervisors and above');selectedGang=null;selected=id;openLift=-1;rememberRecentWorkItem(id);render()}

function editWorkItemTeam(){let x=db.items.find(x=>x.id===selected);if(!x)return;if(!hasPerm('manage_workitems')&&!hasPerm('manage_gangs'))return toast('You do not have permission to change the active team');modal=`<div class="modal"><div class="modalbox"><h2>Edit Active Team</h2><p class="muted">${x.ref} · ${siteName(x.site)}</p><div class="field"><label>Active team</label><select id="wit-team" class="select"><option value="" ${!x.gang?'selected':''}>To be Confirmed</option>${db.gangs.filter(g=>g.active).map(g=>`<option value="${g.id}" ${x.gang===g.id?'selected':''}>${g.name}</option>`).join('')}</select></div><button class="btn orange" onclick="saveWorkItemTeam()">Save changes</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function saveWorkItemTeam(){
 let x=db.items.find(x=>x.id===selected);if(!x)return;
 let oldTeam=x.gang||'',newTeam=document.getElementById('wit-team')?.value||'';
 x.gang=newTeam;
 if(x.type==='House Plot'){
  (x.lifts||[]).forEach(l=>{if(!l.paidTo||l.paidTo===oldTeam)l.paidTo=newTeam})
 }else{
  if(!x.paidTo||x.paidTo===oldTeam)x.paidTo=newTeam
 }
 touch(x);save();emitNotification('work',x.site,`${x.ref}: active team changed from ${gangName(oldTeam)} to ${gangName(newTeam)}`,x.id);modal=null;savedState('Active team updated');toast('Active team updated');render()
}
function addWorkPhotos(){if(!hasPerm('update_progress')&&!hasPerm('manage_workitems'))return toast('You do not have permission to add work photos');clearCameraFiles('work-photo-files');modal=`<div class="modal"><div class="modalbox"><h2>Add Work Photos</h2><p class="muted">Add progress photos directly to this Work Item.</p><div class="field"><label>Photos from device</label><input id="work-photo-files" class="input" type="file" accept="image/*" multiple></div>${cameraPicker('work-photo-files','Take progress photo')}${annotationButton('work-photo-files')}<div class="field"><label>Photo note</label><input id="work-photo-note" class="input" placeholder="e.g. Lift 2 completed before scaffold move"></div><button class="btn blue" onclick="saveWorkPhotos()">Save work photos</button> <button class="btn light" onclick="clearCameraFiles('work-photo-files');cancelQuickEntry()">Cancel</button></div></div>`;render()}
function saveWorkPhotos(){if(!hasPerm('update_progress')&&!hasPerm('manage_workitems'))return toast('You do not have permission to add work photos');let x=db.items.find(x=>x.id===selected),files=uploadFiles('work-photo-files'),note=(document.getElementById('work-photo-note')?.value||'').trim();if(!x||!files.length)return toast('Choose or take at least one photo');x.workPhotos=x.workPhotos||[];let added=[],left=files.length;files.forEach(async(f,idx)=>{let annotated=photoAnnotations['work-photo-files']?.[idx];try{let data=await storeMediaFile(f,annotated||'');if(data){let p={id:'wp'+Date.now()+Math.random().toString(16).slice(2),name:f.name||'Progress photo',data,note,created:iso(),createdAt:new Date().toISOString(),by:currentUser()?.name||'',annotated:!!annotated};x.workPhotos.push(p);added.push(p)}}catch(e){toast('A work photo could not be stored')}if(!--left){clearCameraFiles('work-photo-files');touch(x);save();audit('photo','work item',x.id,`${x.ref}: ${added.length} work photo${added.length===1?'':'s'} added`,x.site);let wasQuick=quickReturnAfterSave;quickReturnAfterSave=false;modal=null;if(wasQuick)selected=null;savedState('Work photos saved');render()}})}
function removeWorkPhoto(id){if(!hasPerm('update_progress')&&!hasPerm('manage_workitems'))return toast('You do not have permission to remove work photos');let x=db.items.find(x=>x.id===selected);if(!x)return;let idx=(x.workPhotos||[]).findIndex(p=>p.id===id);if(idx<0)return;let removed=x.workPhotos[idx];x.workPhotos.splice(idx,1);touch(x);save();render();showUndo('Work photo removed',()=>{x.workPhotos.splice(Math.min(idx,x.workPhotos.length),0,removed);touch(x);save();render()})}
function workPhotosBlock(x){let ps=x.workPhotos||[];return `<div class="work-section-card"><div class="sectionhead"><div><h3>Work Photos</h3><div class="muted">Progress photos captured from site</div></div>${hasPerm('update_progress')||hasPerm('manage_workitems')?'<button class="btn blue smallbtn" onclick="addWorkPhotos()">📷 Add / Take Photo</button>':''}</div>${ps.length?`<div class="work-photo-grid">${ps.slice().reverse().map(p=>`<div class="work-photo-card"><img src="${mediaSrc(p.data)}"${pendingMediaAttr(p.data)} alt="${p.name||'Work photo'}" onclick="showPhoto(mediaSrc('${p.data}'))"><div class="work-photo-caption"><b>${p.note||'Work update'}</b><span>${p.created||''}${p.by?' · '+p.by:''}</span>${hasPerm('update_progress')||hasPerm('manage_workitems')?`<button class="photo-remove" onclick="removeWorkPhoto('${p.id}')">Remove</button>`:''}</div></div>`).join('')}</div>`:'<div class="empty-help"><b>No work photos yet.</b><div style="margin-top:6px">Use Add / Take Photo for quick visual progress updates from site.</div></div>'}</div>`}

function detail(x){let o=overall(x),fin=hasPerm('view_financials'),draw=hasPerm('view_drawings'),eligible=hasPerm('sign_off')&&!x.signed&&((x.type==='House Plot'&&x.lifts.every(l=>l.status==='Complete'))||(x.type!=='House Plot'&&x.status==='Complete'));return `<div class="detailwrap"><button class="btn light back simple-back" onclick="backAction()">← Back</button><div class="panel" style="padding:18px"><div class="detailhead"><div><div class="sub">${x.type}</div><h1>${x.ref} ${x.signed?'<span style="color:#d69a00">★</span>':''}</h1><div class="detail-context"><span>${x.type==='House Plot'&&x.houseType?x.houseType+' · ':''}${siteName(x.site)}</span><span>Team: ${gangName(x.gang)}</span>${statusText(x)}</div><div class="last-updated">${lastUpdatedText(x)}</div></div><div class="actions">${canRaiseIssues()?'<button class="btn red primary-action" onclick="addIssue()">＋ Add Issue</button>':''}${eligible?'<button class="btn green primary-action" onclick="signOff()">★ Sign Off</button>':''}<div class="more-actions-wrap"><button class="btn light" onclick="toggleWorkMore()">More actions ▾</button>${workMoreOpen?`<div class="more-actions-menu">${(hasPerm('manage_workitems')||hasPerm('manage_gangs'))?'<button onclick="workMoreOpen=false;editWorkItemTeam()">♟ Edit team</button>':''}${hasPerm('manage_drawings')?'<button onclick="workMoreOpen=false;uploadDrawing()">♧ Upload drawing</button>':''}${canSupervisor()||hasPerm('manage_workitems')?'<button onclick="workMoreOpen=false;editNotes()">✎ Edit notes</button>':''}</div>`:''}</div></div></div>${signedOffBanner(x)}<div class="summary"><div class="card"><div class="muted">${x.type==='House Plot'?'Progress':'Completion date'}</div><b>${x.type==='House Plot'?x.lifts.filter(l=>l.status==='Complete').length+'/'+x.lifts.length+' lifts':(x.completedDate||'—')}</b></div>${hasPerm('view_issues')?`<div class="card"><div class="muted">Open issues</div><b>${issues(x)}</b></div>`:''}${fin?`<div class="card"><div class="muted">Paid</div><b>${pct(x)}%</b></div>`:''}${draw?`<div class="card"><div class="muted">Drawings</div><b>${x.drawings.length}</b></div>`:''}</div><div class="work-section-card"><div class="sectionhead"><h3>Main Work</h3>${x.type==='House Plot'&&hasPerm('edit_lifts')?'<button class="btn light smallbtn" onclick="editLifts()">⚙ Edit lift stages</button>':''}</div>${x.type==='House Plot'?x.lifts.map((l,i)=>lift(x,l,i)).join(''):garden(x)}</div>${workPhotosBlock(x)}${hasPerm('view_issues')?`<div class="work-section-card"><div class="sectionhead"><h3>Issues</h3>${canRaiseIssues()?'<button class="btn red smallbtn" onclick="addIssue()">＋ Add issue</button>':''}</div>${x.issues.map(i=>`<div class="card issue ${i.status==='Resolved'?'resolved':''}" onclick="viewIssue('${x.id}','${i.id}')" style="cursor:pointer"><div class="sectionhead"><div><h3>${i.title}</h3><div class="issue-detail-badges">${issuePriorityBadge(i)}${issueDueBadge(i)}<span class="issue-owner">♟ ${issueAssigneeLabel(i.assignedTo)}</span></div></div>${badge(i.status)}</div><p>${i.details}</p><p class="muted">${i.notes}</p><div class="muted">Created ${i.created}${i.resolved?' · Resolved '+i.resolved:''} · ${i.photos?.length||0} photo(s)</div>${i.status==='Open'&&hasPerm('manage_issues')?`<button class="btn light smallbtn" onclick="event.stopPropagation();resolveIssue('${x.id}','${i.id}')">Mark resolved</button>`:''}</div>`).join('')||'<div class="empty-help"><b>No issues recorded.</b><div style="margin-top:6px">New issues will appear here.</div></div>'}</div>`:''}${fin?variationsBlock(x):''}${fin?`<div class="work-section-card"><div class="sectionhead"><div><h3>Payments</h3><div class="muted">Original contract and approved variations</div></div></div>${financeOverview(x)}${itemPaymentHistory(x)}</div>`:''}${draw?`<div class="work-section-card"><div class="sectionhead"><h3>Plot Drawings</h3><div class="drawing-section-actions">${hasPerm('manage_drawings')?'<button class="btn light smallbtn" onclick="uploadDrawing()">＋ Upload New Drawing</button><button class="btn blue smallbtn" onclick="openMatchingSiteDrawings()">▤ Add from Drawing Library</button>':''}</div></div>${x.drawings.map((d,i)=>`<div class="card drawingcard ${d.revisionStatus==='Superseded'?'v170-superseded':''}"><span><b>${d.name}</b><div class="muted">${d.notes||''}${d.uploadedDate?` · Uploaded ${d.uploadedDate}`:''}${String(d.data||'').startsWith('browser:')?' · Browser storage':''}</div>${drawingTakeoffSummary(d)}</span><div class="drawing-actions">${d.data?`<button class="btn light smallbtn" onclick="openDrawing(${i})">Open Drawing</button>${d.revisionStatus==='Superseded'?'<span class="badge b-red">SUPERSEDED</span>':`<button class="btn blue smallbtn" onclick="openDrawingTakeoff('work','${d.id}','${x.id}')">📐 Measure</button>`}`:'<span class="badge b-grey">Recorded</span>'}${hasPerm('manage_drawings')&&hasPerm('permanent_delete')?`<button class="btn red smallbtn danger-btn" onclick="deleteDrawing(${i})">Delete</button>`:''}</div></div>`).join('')||'<div class="empty-help"><b>No drawings yet.</b><div style="margin-top:6px">Drawings added to this work item will appear here.</div></div>'}</div>`:''}<div class="work-section-card"><div class="sectionhead"><h3>Notes</h3>${canSupervisor()||hasPerm('manage_workitems')?'<button class="btn light smallbtn" onclick="editNotes()">Edit notes</button>':''}</div><div class="note-display">${(x.notesHtml||x.notes)?noteHtml(x):'<span class="muted">No notes yet.</span>'}</div></div></div></div>`}
function paymentHistory(entries,ctx){if(!hasPerm('view_financials'))return '';return `<div class="section"><div class="sectionhead"><h4>Payment history</h4>${hasPerm('record_payments')?`<button class="btn orange smallbtn" onclick="addPayment('${ctx}')">＋ Add payment</button>`:''}</div><div class="payment-history">${(entries||[]).map((p,i)=>`<div class="payment-entry"><strong>${p.pct}%</strong><span>${p.date||'No date'}</span><span>£${p.amount||0}</span><span>${p.authorisedExtra?'<span class="extra-badge">Authorised extra</span> ':''}${p.notes||''}</span>${hasPerm('manage_payments')?`<button class="btn light smallbtn" onclick="deletePayment('${ctx}',${i})">Remove</button>`:''}</div>`).join('')||'<div class="empty-help"><b>No payment records yet.</b><div style="margin-top:6px">Use Add payment when a payment needs recording.</div></div>'}</div></div>`}
function lift(x,l,i){let fin=hasPerm('view_financials');return `<div class="lift"><div class="lifthead" onclick="if(confirmLeave()){clearDirty();openLift=${openLift===i?-1:i};render()}"><div><b>${l.name}</b><div class="muted">${badge(l.status)} ${fin?badge(pay(l.pct)):''} ${l.completedDate?'· '+l.completedDate:''}</div></div>${fin?`<b>${money(l.price)} set · ${money(stageRemainingAmount(l))} remaining</b>`:''}</div>${openLift===i?`<div class="liftbody"><div class="progress-zone"><h4>Progress</h4><div class="two"><div class="field"><label>Status</label><select id="lf-status-${i}" class="select" ${canSupervisor()?'':'disabled'} onchange="markDirty()">${['Not started','Started','Complete'].map(s=>`<option ${l.status===s?'selected':''}>${s}</option>`).join('')}</select></div><div class="field"><label>Completion date</label><input id="lf-date-${i}" class="input" type="date" value="${l.completedDate||''}" ${canSupervisor()?'':'disabled'} onchange="markDirty()"></div></div><div class="field"><label>Variation details</label><textarea id="lf-var-${i}" class="textarea" ${canSupervisor()?'':'disabled'} oninput="markDirty()">${l.variation||''}</textarea></div><div class="field"><label>Lift notes</label><textarea id="lf-notes-${i}" class="textarea" ${canSupervisor()?'':'disabled'} oninput="markDirty()">${l.notes||''}</textarea></div></div>${fin?`<div class="finance-zone"><h4>Payment</h4><div class="remaining-pay">Paid ${money(stagePaidAmount(l))} · Remaining ${money(stageRemainingAmount(l))} · ${stageRemainingPct(l)}% left</div><div class="two"><div class="field"><label>Original set price</label><input id="lf-price-${i}" class="input" type="number" value="${l.price}" ${hasPerm('manage_payments')?'':'disabled'} onchange="markDirty()"></div><div class="field"><label>Paid so far</label><select id="lf-pct-${i}" class="select" ${hasPerm('manage_payments')?'':'disabled'} onchange="markDirty()">${[0,25,50,75,100].map(p=>`<option ${l.pct===p?'selected':''}>${p}</option>`).join('')}</select></div></div><div class="two"><div class="field"><label>Paid to</label><select id="lf-paidto-${i}" class="select" ${hasPerm('manage_payments')?'':'disabled'} onchange="markDirty()">${db.gangs.map(g=>`<option value="${g.id}" ${l.paidTo===g.id?'selected':''}>${g.name}</option>`).join('')}</select></div><div class="field"><label>Payment notes</label><input id="lf-paynotes-${i}" class="input" value="${l.paymentNotes||''}" ${hasPerm('manage_payments')?'':'disabled'} oninput="markDirty()"></div></div>${paymentHistory(l.payments,'lift|'+x.id+'|'+i)}</div>`:''}${canSupervisor()?`<div class="lift-savebar"><span class="unsaved-note">Changes are only stored when you press Save lift changes.</span><button class="btn orange" onclick="event.stopPropagation();saveLiftCard(${i})">✓ Save lift changes</button></div>`:''}</div>`:''}</div>`}
function saveLiftCard(i){let x=db.items.find(x=>x.id===selected),l=x.lifts[i];if(!l)return;let oldStatus=l.status;l.status=document.getElementById('lf-status-'+i)?.value||l.status;if(canManager()){l.price=Number(document.getElementById('lf-price-'+i)?.value||0);l.pct=Number(document.getElementById('lf-pct-'+i)?.value||0);l.paidTo=document.getElementById('lf-paidto-'+i)?.value||l.paidTo;l.paymentNotes=document.getElementById('lf-paynotes-'+i)?.value||''}l.completedDate=document.getElementById('lf-date-'+i)?.value||'';l.variation=document.getElementById('lf-var-'+i)?.value||'';l.notes=document.getElementById('lf-notes-'+i)?.value||'';if(l.status==='Complete'&&!l.completedDate)l.completedDate=iso();touch(x);clearDirty();openLift=-1;save();emitNotification('work',x.site,`${x.ref}: lift/stage updated`,x.id);savedState('Lift changes saved');toast('Lift changes saved');render()}
function garden(x){let fin=hasPerm('view_financials'),editable=canSupervisor()||hasPerm('manage_payments');return `<div class="card"><div class="two"><div class="field"><label>Status</label><select id="gd-status" class="select" ${canSupervisor()?'':'disabled'} onchange="markDirty()">${['Not started','Started','Complete'].map(s=>`<option ${x.status===s?'selected':''}>${s}</option>`).join('')}</select></div><div class="field"><label>Completion date</label><input id="gd-date" class="input" type="date" value="${x.completedDate||''}" ${canSupervisor()?'':'disabled'} onchange="markDirty()"></div></div>${fin?`<div class="finance-zone"><h4>Payment</h4><div class="two"><div class="field"><label>Set price</label><input id="gd-price" class="input" type="number" value="${x.price}" ${hasPerm('manage_payments')?'':'disabled'} onchange="markDirty()"></div><div class="field"><label>Paid so far</label><select id="gd-pct" class="select" ${hasPerm('manage_payments')?'':'disabled'} onchange="markDirty()">${[0,25,50,75,100].map(p=>`<option ${x.pct===p?'selected':''}>${p}</option>`).join('')}</select></div></div><div class="two"><div class="field"><label>Paid to</label><select id="gd-paidto" class="select" ${hasPerm('manage_payments')?'':'disabled'} onchange="markDirty()">${db.gangs.map(g=>`<option value="${g.id}" ${x.paidTo===g.id?'selected':''}>${g.name}</option>`).join('')}</select></div><div class="field"><label>Payment notes</label><input id="gd-paynotes" class="input" value="${x.paymentNotes||''}" ${hasPerm('manage_payments')?'':'disabled'} oninput="markDirty()"></div></div>${paymentHistory(x.payments,'garden|'+x.id)}</div>`:''}<div class="field"><label>Notes</label><textarea id="gd-notes" class="textarea" ${canSupervisor()?'':'disabled'} oninput="markDirty()">${x.notes||''}</textarea></div>${editable?`<div class="lift-savebar"><span class="unsaved-note">Changes are only stored when you press Save changes.</span><button class="btn orange" onclick="saveGardenCard()">✓ Save changes</button></div>`:''}</div>`}
function saveGardenCard(){let x=db.items.find(x=>x.id===selected);if(!x)return;if(canSupervisor()){x.status=document.getElementById('gd-status')?.value||x.status;x.completedDate=document.getElementById('gd-date')?.value||'';x.notes=document.getElementById('gd-notes')?.value||'';if(x.status==='Complete'&&!x.completedDate)x.completedDate=iso()}if(hasPerm('manage_payments')){x.price=Number(document.getElementById('gd-price')?.value||0);x.pct=Number(document.getElementById('gd-pct')?.value||0);x.paidTo=document.getElementById('gd-paidto')?.value||x.paidTo;x.paymentNotes=document.getElementById('gd-paynotes')?.value||''}touch(x);clearDirty();save();emitNotification('work',x.site,`${x.ref}: work item updated`,x.id);savedState('Work changes saved');render()}
function addPayment(ctx){if(!hasPerm('record_payments'))return toast('You do not have permission to record payments');let parts=ctx.split('|'),x=db.items.find(x=>x.id===parts[1]),target=parts[0]==='garden'?x:x.lifts[Number(parts[2])];if(!x||!target)return;let remainingPct=parts[0]==='garden'?simpleRemainingPct(x):stageRemainingPct(target),remainingAmount=parts[0]==='garden'?simpleRemainingAmount(x):stageRemainingAmount(target),setValue=parts[0]==='garden'?simpleSetPrice(x):stageSetPrice(x,target),paidValue=parts[0]==='garden'?simplePaidAmount(x):stagePaidAmount(target);if(remainingPct<=0||remainingAmount<=0)return toast('This stage is already fully paid.');modal=`<div class="modal"><div class="modalbox"><h2>Add Payment</h2><p class="muted">${x.ref} · ${parts[0]==='garden'?x.type:target.name}</p><div class="payment-entry-summary"><div class="card"><span class="muted">Original value</span><b>${money(setValue)}</b></div><div class="card"><span class="muted">Already paid</span><b>${money(paidValue)}</b></div><div class="card"><span class="muted">Remaining</span><b>${money(remainingAmount)}</b></div></div><div class="field"><label>Payment now</label><input id="pa" class="input" type="number" min="0.01" max="${remainingAmount}" step="0.01" placeholder="£ amount"></div><div class="field"><label>Date</label><input id="pd" class="input" type="date" value="${iso()}"></div><div class="field"><label>Paid to</label><select id="pto" class="select" onchange="paymentPayeeChange('pto','pto-other')">${paymentPayeeOptions('',target.paidTo||x.gang||'')}</select></div><div class="field" id="pto-other" style="display:none"><label>Individual name</label><input id="pto-other-name" class="input" placeholder="Name"></div><div class="field"><label>Notes</label><input id="pn" class="input"></div><label class="site-access-item"><input id="pextra" type="checkbox"> Authorised extra / variation outside original contract value</label><button class="btn green" onclick="savePayment('${ctx}')">Save payment</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function savePayment(ctx){
 if(!hasPerm('record_payments'))return toast('You do not have permission to record payments');
 let parts=ctx.split('|'),x=db.items.find(x=>x.id===parts[1]),target=parts[0]==='garden'?x:x.lifts[Number(parts[2])];if(!x||!target)return;
 let amt=Number(document.getElementById('pa')?.value||0),remainingAmount=parts[0]==='garden'?simpleRemainingAmount(x):stageRemainingAmount(target),setValue=parts[0]==='garden'?simpleSetPrice(x):stageSetPrice(x,target);
 if(amt<=0)return toast('Enter the amount being paid.');if(amt>remainingAmount+0.005)return toast(`Only ${money(remainingAmount)} remains to be paid on this stage.`);
 let pctVal=setValue?Math.round((amt/setValue)*10000)/100:0,selectedPayee=document.getElementById('pto')?.value||target.paidTo||x.gang||'',payeeName='';
 if(selectedPayee==='__other__'){payeeName=(document.getElementById('pto-other-name')?.value||'').trim();if(!payeeName)return toast('Enter the individual name');selectedPayee=''}
 let rec={id:'p'+Date.now(),pct:pctVal,date:document.getElementById('pd')?.value||iso(),amount:amt,paidTo:selectedPayee,payeeName,notes:document.getElementById('pn')?.value||'',authorisedExtra:document.getElementById('pextra')?.checked||false,enteredBy:currentUser()?.name||'',enteredAt:new Date().toISOString()};
 target.payments=target.payments||[];target.payments.push(rec);target.pct=Math.min(100,(parts[0]==='garden'?simplePaidPct(x):stagePaidPct(target)));if(rec.paidTo&&db.gangs.some(g=>g.id===rec.paidTo))target.paidTo=rec.paidTo;
 touch(x);save();emitNotification('payment',x.site,`${x.ref}: payment recorded — ${money(amt)} · ${paymentPayeeName(rec,x.gang)} · entered by ${rec.enteredBy}`,x.id);savedState('Payment saved');modal=null;render()
}
function deletePayment(ctx,i){
 if(!hasPerm('manage_payments'))return toast('You do not have permission to remove payments');if(!confirm('Remove this payment record?'))return;
 let parts=ctx.split('|'),x=db.items.find(x=>x.id===parts[1]),l=parts[0]==='garden'?x:x.lifts[Number(parts[2])],arr=l.payments||[],removed=arr[i];if(!removed)return;
 arr.splice(i,1);l.pct=arr.length?Math.max(...arr.map(p=>p.pct)):0;touch(x);save();audit('payment','payment',removed.id,`${x.ref}: payment removed — ${money(removed.amount||0)} · originally entered by ${removed.enteredBy||'Unknown user'}`,x.site);savedState('Payment removed');render()
}
function editLifts(){if(!hasPerm('edit_lifts'))return toast('You do not have lift-edit access');let x=db.items.find(x=>x.id===selected);modal=`<div class="modal"><div class="modalbox"><h2>Edit lift stages</h2><p class="muted">Rename stages and change set amounts. You can add or remove stages for different house types.</p><div id="liftEditor">${x.lifts.map((l,i)=>`<div class="two" style="margin-bottom:8px"><input class="input lname" data-i="${i}" value="${l.name}"><input class="input lprice" data-i="${i}" type="number" value="${l.price}"></div>`).join('')}</div><button class="btn light" onclick="addLiftRow()">＋ Add stage</button> <button class="btn orange" onclick="saveLifts()">Save stages</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function addLiftRow(){let el=document.getElementById('liftEditor');let i=el.children.length;el.insertAdjacentHTML('beforeend',`<div class="two" style="margin-bottom:8px"><input class="input lname" data-i="${i}" value="Stage ${i+1}"><input class="input lprice" data-i="${i}" type="number" value="0"></div>`)}
function saveLifts(){let x=db.items.find(x=>x.id===selected),names=[...document.querySelectorAll('.lname')],prices=[...document.querySelectorAll('.lprice')];let old=x.lifts;x.lifts=names.map((n,i)=>{let prev=old[i]||{status:'Not started',pct:0,paidTo:x.gang,date:'',paymentNotes:'',variation:'',notes:'',completedDate:'',payments:[]};return{...prev,name:n.value.trim()||'Stage '+(i+1),price:Number(prices[i].value||0)}});touch(x);save();modal=null;render()}

function signedOffBanner(x){
 if(!x?.signed)return '';
 let by=x.signedOffBy||'Signed off',when=x.signedOffAt?new Date(x.signedOffAt).toLocaleString('en-GB'):'Time not recorded';
 return `<div class="signed-off-record"><div class="signed-off-icon">★</div><div><span>SIGNED OFF</span><b>${calendarSafe(by)}</b><small>${when}</small>${x.signedOffNote?`<p>${calendarSafe(x.signedOffNote)}</p>`:''}</div></div>`
}

function signOff(){
 let x=db.items.find(x=>x.id===selected);if(!x)return;
 if(!hasPerm('sign_off'))return toast('You do not have sign-off access');
 if(x.signed)return toast('This Work Item is already signed off.');
 if((x.type==='House Plot'&&!x.lifts.every(l=>l.status==='Complete'))||(x.type!=='House Plot'&&x.status!=='Complete'))return toast('Work is not eligible for sign-off.');
 modal=`<div class="modal"><div class="modalbox signoff-modal"><span class="signoff-kicker">FINAL RECORD</span><h2>Sign off ${x.ref}</h2><p class="muted">This records who signed the work off and the exact time.</p><div class="signoff-check"><span>✓</span><div><b>Work is complete</b><small>${x.type==='House Plot'?'All lift stages are marked Complete.':'This Work Item is marked Complete.'}</small></div></div><div class="field"><label>Sign-off note <span class="muted">(optional)</span></label><textarea id="signoff-note" class="textarea" placeholder="e.g. Checked on site — work accepted"></textarea></div><button class="btn green" onclick="confirmSignOff()">★ Confirm Sign Off</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function confirmSignOff(){
 let x=db.items.find(x=>x.id===selected);if(!x||x.signed)return;
 if(!hasPerm('sign_off'))return toast('You do not have sign-off access');
 let u=currentUser(),now=new Date().toISOString();
 x.signed=true;x.signedOffBy=u?.name||'';x.signedOffAt=now;x.signedOffNote=(document.getElementById('signoff-note')?.value||'').trim();touch(x);save();
 emitNotification('signoff',x.site,`${x.ref}: signed off by ${x.signedOffBy||'SiteSight user'}`,x.id);
 modal=null;savedState('Work signed off');toast('Signed off');render()
}

function issuePriorityChanged(){
 let p=document.getElementById('ipr')?.value||'Normal',hint=document.getElementById('urgent-issue-hint');if(hint)hint.style.display=p==='Urgent'?'flex':'none'
}

function addIssue(){
 if(!hasPerm('raise_issues'))return toast('You do not have permission to raise issues');
 modal=`<div class="modal"><div class="modalbox"><h2>Add Issue</h2><div class="field"><label>Title</label><input id="it" class="input" placeholder="What needs attention?"></div><div class="two"><div class="field"><label>Assigned to</label><select id="ia" class="select">${issueAssigneeOptions('')}</select></div><div class="field"><label>Priority</label><select id="ipr" class="select" onchange="issuePriorityChanged()"><option>Normal</option><option>High</option><option>Urgent</option></select></div></div><div id="urgent-issue-hint" class="urgent-issue-hint" style="display:none"><span>!</span><div><b>Urgent issue</b><small>For a clear handover, set who owns it and when it is due where possible.</small></div></div><div class="field"><label>Target date</label><input id="itarget" class="input" type="date"></div><div class="field"><label>Details</label><textarea id="id" class="textarea" placeholder="What is wrong, where is it, and what is required?"></textarea></div><div class="field"><label>Notes</label><textarea id="in" class="textarea"></textarea></div><div class="field"><label>Issue photos</label><input id="if" class="input" type="file" accept="image/*" multiple></div>${cameraPicker('if','Take issue photo')}${annotationButton('if')}<button class="btn red" onclick="saveIssue()">Add issue</button> <button class="btn light" onclick="cancelQuickEntry()">Cancel</button></div></div>`;render()
}
function saveIssue(){
 if(!hasPerm('raise_issues'))return toast('You do not have permission to raise issues');
 let x=db.items.find(x=>x.id===selected),files=uploadFiles('if'),title=(document.getElementById('it')?.value||'').trim();if(!title)return toast('Enter an issue title');
 let assignedTo=document.getElementById('ia')?.value||'',priority=document.getElementById('ipr')?.value||'Normal',targetDate=document.getElementById('itarget')?.value||'';
 if(priority==='Urgent'&&(!assignedTo||!targetDate)&&!confirm('This Urgent Issue has no assignee or target date. Save it anyway?'))return;
 let now=new Date().toISOString(),issue={id:'i'+Date.now(),title,details:document.getElementById('id')?.value||'',notes:document.getElementById('in')?.value||'',status:'Open',created:iso(),createdAt:now,createdById:currentUser()?.id||'',createdBy:currentUser()?.name||'',updatedAt:now,updatedBy:currentUser()?.name||'',resolved:'',resolvedBy:'',assignedTo,priority,targetDate,photos:[]};
 x.issues.push(issue);touch(x);
 let finish=()=>{clearCameraFiles('if');save();emitNotification('issue',x.site,`${x.ref}: new ${issue.priority.toLowerCase()} issue — ${issue.title}${issueAssigneeLabel(issue.assignedTo)!=='Unassigned'?' · '+issueAssigneeLabel(issue.assignedTo):''}`,x.id);let wasQuick=quickReturnAfterSave;quickReturnAfterSave=false;modal=null;if(wasQuick)selected=null;render();savedState('Issue saved')};
 if(!files.length)return finish();savePhotosToArray(files,issue.photos,finish,'if')
}
function resolveIssue(xid,iid){if(!hasPerm('manage_issues'))return toast('You do not have permission to resolve issues');let x=db.items.find(x=>x.id===xid),i=x?.issues?.find(i=>i.id===iid);if(!x||!i)return;let prev={status:i.status,resolved:i.resolved,resolvedBy:i.resolvedBy,updatedAt:i.updatedAt,updatedBy:i.updatedBy};i.status='Resolved';i.resolved=iso();i.resolvedBy=currentUser()?.name||'';i.updatedAt=new Date().toISOString();i.updatedBy=currentUser()?.name||'';touch(x);save();emitNotification('issue',x.site,`${x.ref}: issue resolved — ${i.title}`,x.id);modal=null;render();showUndo('Issue resolved',()=>{Object.assign(i,prev);touch(x);save();audit('issue','issue',i.id,`${x.ref}: issue resolution undone`,x.site);render()})}
function escapeNoteText(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[c])).replace(/\n/g,'<br>')}
function noteHtml(x){return x.notesHtml||escapeNoteText(x.notes||'')}
function noteFormat(cmd,val=null){document.execCommand(cmd,false,val);document.getElementById('ne-rich')?.focus()}
function noteToggle(cmd,id){noteFormat(cmd);document.getElementById(id)?.classList.toggle('format-active')}
function noteBold(){noteToggle('bold','fmt-bold')}
function noteItalic(){noteToggle('italic','fmt-italic')}
function noteUnderline(){noteToggle('underline','fmt-underline')}
function noteColour(c,el){noteFormat('foreColor',c);document.querySelectorAll('.colour-swatch').forEach(b=>b.classList.remove('colour-active'));el?.classList.add('colour-active')}
function saveRichNotes(){let x=db.items.find(x=>x.id===selected),ed=document.getElementById('ne-rich');if(!x||!ed)return;x.notesHtml=ed.innerHTML;x.notes=ed.innerText;touch(x);clearDirty();save();emitNotification('work',x.site,`${x.ref}: notes updated`,x.id);modal=null;savedState('Notes saved');toast('Notes saved');render()}

function editNotes(){let x=db.items.find(x=>x.id===selected),cols=['#10243b','#dc2626','#f97316','#ca8a04','#16a34a','#2563eb','#7c3aed','#db2777'];modal=`<div class="modal"><div class="modalbox"><h2>Edit Notes</h2><div class="note-toolbar"><button id="fmt-bold" class="formatbtn" type="button" onmousedown="event.preventDefault()" onclick="noteBold()"><b>B</b></button><button id="fmt-italic" class="formatbtn" type="button" onmousedown="event.preventDefault()" onclick="noteItalic()"><i>I</i></button><button id="fmt-underline" class="formatbtn" type="button" onmousedown="event.preventDefault()" onclick="noteUnderline()"><u>U</u></button><div class="colour-palette">${cols.map(c=>`<button type="button" class="colour-swatch" title="${c}" style="background:${c}" onmousedown="event.preventDefault()" onclick="noteColour('${c}',this)"></button>`).join('')}</div><span class="muted">Selected formatting stays highlighted after you click it.</span></div><div id="ne-rich" class="notes-editor" contenteditable="true" oninput="markDirty()">${noteHtml(x)}</div><div style="margin-top:12px"><button class="btn" onclick="saveRichNotes()">Save changes</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()}

function openMatchingSiteDrawings(){if(!hasPerm('manage_drawings'))return toast('You do not have permission to add drawings');let x=db.items.find(x=>x.id===selected);if(!x)return;let a=db.siteDrawings.filter(d=>d.siteId===x.site&&(!x.houseType||d.houseType===x.houseType));modal=`<div class="modal"><div class="modalbox"><h2>Add from Drawing Library</h2><p class="muted">${x.ref} · ${siteName(x.site)}${x.houseType?' · '+x.houseType:''}</p><div class="quick-manage-list">${a.length?a.map(d=>`<div class="quick-manage-row"><div><b>${d.name}</b><span>${d.houseType} · ${d.uploadedDate}</span></div><button class="btn blue smallbtn" onclick="transferSiteDrawing('${d.id}','${x.id}')">Add to ${x.ref}</button></div>`).join(''):'<div class="empty">No matching drawing librarys found. Upload them from the Drawing Library tab.</div>'}</div><button class="btn" onclick="modal=null;render()">Close</button></div></div>`;render()}

function uploadDrawing(){if(!hasPerm('manage_drawings'))return toast('You do not have drawing upload access');modal=`<div class="modal"><div class="modalbox"><h2>Upload Plot Drawing</h2><p class="muted">Choose a PDF or image, then give the drawing a clear name for the site record.</p><div class="field"><label>Drawing file</label><input id="df" class="input" type="file" accept=".pdf,image/*" onchange="suggestDrawingName()"></div>${cameraPicker('df','Photograph drawing','suggestDrawingName')}<div class="field"><label>Drawing name</label><input id="ddn" class="input" placeholder="e.g. Plot 12 Ground Floor Plan"></div><div class="field"><label>Notes</label><textarea id="dn" class="textarea"></textarea></div><button class="btn green" onclick="saveDrawing()">Upload drawing</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function suggestDrawingName(){let f=uploadFiles('df')[0],n=document.getElementById('ddn');if(f&&n&&!n.value)n.value=(f.name||'Site photo drawing').replace(/\.[^.]+$/,'')}
function saveDrawing(){
 if(!hasPerm('manage_drawings'))return toast('You do not have drawing upload access');let x=db.items.find(x=>x.id===selected),f=uploadFiles('df')[0],notes=document.getElementById('dn')?.value||'',displayName=(document.getElementById('ddn')?.value||'').trim();if(!f)return toast('Choose a drawing file first');
 let d={id:'d'+Date.now(),name:displayName||f.name||'Camera drawing',originalName:f.name||'Camera drawing.jpg',notes,type:f.type||'image/jpeg',data:'',uploadedDate:iso(),uploadedAt:new Date().toISOString(),uploadedBy:currentUser()?.name||'',storage:'browser',takeoffs:[],takeoffCalibration:null};
 storeDrawingInBrowser(f).then(data=>{d.data=data;clearCameraFiles('df');x.drawings.push(d);touch(x);save();audit('drawing','work item',x.id,`${x.ref}: drawing saved in browser — ${d.name}`,x.site);emitNotification('drawing',x.site,`${x.ref}: drawing recorded — ${d.name}`,x.id);modal=null;savedState('Drawing saved in this browser');render()}).catch(()=>toast('Drawing could not be stored in this browser'))
}
async function openDrawing(i){
 let x=db.items.find(x=>x.id===selected),d=x?.drawings?.[i];if(!d)return;if(!d.data)return toast('Drawing file is unavailable');
 let src=await resolveMediaRef(d.data);if(!src)return toast('This drawing is stored in a different browser. Open SiteSight on the device where it was uploaded.');
 let isPdf=d.type==='application/pdf'||String(d.originalName||'').toLowerCase().endsWith('.pdf'),tot=takeoffTotals(d);
 modal=`<div class="work-drawing-fullscreen" onclick="if(event.target===this){modal=null;render()}"><div class="work-drawing-fullscreen-inner"><div class="work-drawing-fullscreen-head"><div><b>${d.name||d.originalName||'Drawing'}</b><div class="muted">${x.ref} · ${siteName(x.site)}${d.uploadedDate?' · Uploaded '+d.uploadedDate:''} · ${String(d.data).startsWith('browser:')?'Stored in this browser':'Legacy/shared file'}</div>${(d.takeoffs||[]).length?`<div class="viewer-takeoff-line">📐 ${(d.takeoffs||[]).length} take-off item${(d.takeoffs||[]).length===1?'':'s'} · Net ${Math.max(0,tot.net).toFixed(2)} m²${hasPerm('view_financials')?` · ${money(tot.value)}`:''}</div>`:''}</div><div class="actions"><button class="btn blue" onclick="openDrawingTakeoff('work','${d.id}','${x.id}')">📐 Measure Drawing</button><button class="btn light" onclick="modal=null;render()">✕ Close</button></div></div><div class="work-drawing-fullscreen-body">${isPdf?`<iframe src="${src}" title="${d.name||'Drawing'}"></iframe>`:`<img src="${src}" alt="${d.name||'Drawing'}">`}</div></div></div>`;render()
}
function deleteDrawing(i){if(!hasPerm('manage_drawings')||!hasPerm('permanent_delete'))return toast('Permanent Deletion permission is required');let x=db.items.find(x=>x.id===selected),d=x?.drawings?.[i];if(!d)return;if(!confirm(`Delete drawing "${d.name}"?`))return;let ref=d.data;x.drawings.splice(i,1);touch(x);save();cleanupDrawingRef(ref);emitNotification('drawing',x.site,`${x.ref}: drawing deleted — ${d.name}`,x.id);savedState('Drawing deleted');toast('Drawing deleted');render()}function toggleCustomType(){let v=document.getElementById('nt')?.value||'',box=document.getElementById('custom-type-box');if(box)box.style.display=v==='__custom__'?'block':'none'}

function bulkCsvEscape(v){let s=String(v??'');return /[",\n]/.test(s)?`"${s.replace(/"/g,'""')}"`:s}
function downloadBulkImportTemplate(){
 let rows=[['Reference','Type','House Type','Team','Set Value','1st Lift Price','2nd Lift Price','3rd Lift Price','4th Lift Price','5th Lift Price'],['Plot 001','House Plot','House Type A','Team Name','','2500','2500','2500','2500','2500'],['Plot 002','House Plot','House Type B','','','','','','',''],['Wall 01','Garden Wall','','Team Name','3500','','','','','']],
 csv=rows.map(r=>r.map(bulkCsvEscape).join(',')).join('\r\n'),blob=new Blob([csv],{type:'text/csv;charset=utf-8'}),a=document.createElement('a');
 a.href=URL.createObjectURL(blob);a.download='SiteSight-bulk-work-items-template.csv';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000)
}
function parseCsv(text){
 let rows=[],row=[],cell='',quoted=false;
 for(let i=0;i<text.length;i++){let c=text[i],n=text[i+1];if(quoted){if(c==='"'&&n==='"'){cell+='"';i++}else if(c==='"')quoted=false;else cell+=c}else if(c==='"')quoted=true;else if(c===','){row.push(cell.trim());cell=''}else if(c==='\n'){row.push(cell.trim());if(row.some(v=>v!==''))rows.push(row);row=[];cell=''}else if(c!=='\r')cell+=c}
 row.push(cell.trim());if(row.some(v=>v!==''))rows.push(row);return rows
}
function bulkHeaderKey(s){return String(s||'').toLowerCase().replace(/[^a-z0-9]+/g,'')}
function bulkValue(o,...keys){for(let k of keys){let v=o[bulkHeaderKey(k)];if(v!==undefined&&v!=='')return String(v).trim()}return ''}
function parseBulkWorkItems(text){
 let raw=parseCsv(text);if(raw.length<2)return [];let headers=raw[0].map(bulkHeaderKey);
 return raw.slice(1).map((r,idx)=>{let o={};headers.forEach((h,i)=>o[h]=r[i]??'');let ref=bulkValue(o,'Reference','Ref','Plot'),type=bulkValue(o,'Type','Work Type')||'House Plot',houseType=bulkValue(o,'House Type','HouseType'),team=bulkValue(o,'Team','Gang'),setValue=Number(String(bulkValue(o,'Set Value','Price','Value')).replace(/[£,\s]/g,''))||0,liftPrices=[1,2,3,4,5].map(n=>Number(String(bulkValue(o,`${n}${n===1?'st':n===2?'nd':n===3?'rd':'th'} Lift Price`,`Lift ${n} Price`,`Lift${n}`)).replace(/[£,\s]/g,''))||0),gang=db.gangs.find(g=>String(g.name||'').trim().toLowerCase()===team.toLowerCase());return {row:idx+2,ref,type,houseType,team,gangId:gang?.id||'',teamMissing:!!team&&!gang,setValue,liftPrices}}).filter(r=>r.ref)
}
function openBulkImport(){
 if(!hasPerm('manage_workitems'))return toast('You do not have work-item creation access');bulkImportRows=[];bulkImportSite=site!=='all'?site:(activeSites()[0]?.id||'');
 modal=`<div class="modal"><div class="modalbox bulk-import-modal"><div class="sectionhead"><div><span class="calendar-kicker">SITE SETUP</span><h2>Bulk Import Work Items</h2><div class="muted">Designed for quickly setting up a large plot schedule.</div></div><button class="btn light" onclick="modal=null;render()">✕ Close</button></div><div class="bulk-import-steps"><div class="bulk-step"><b>1</b><span>Download the CSV template and fill one Work Item per row.</span><button class="btn light smallbtn" onclick="downloadBulkImportTemplate()">⇩ Download template</button></div><div class="bulk-step"><b>2</b><span>Create real Teams first so Team names can be matched automatically.</span></div></div><div class="field"><label>Import into Site</label><select id="bulk-site" class="select" onchange="bulkImportSite=this.value;document.getElementById('bulk-preview').innerHTML=bulkImportPreviewHtml()">${activeSites().map(s=>`<option value="${s.id}" ${bulkImportSite===s.id?'selected':''}>${s.name}</option>`).join('')}</select></div><div class="field"><label>CSV file</label><input id="bulk-file" class="input" type="file" accept=".csv,text/csv" onchange="loadBulkImportFile(this)"></div><div id="bulk-preview">${bulkImportPreviewHtml()}</div></div></div>`;render()
}
function loadBulkImportFile(input){let file=input?.files?.[0];if(!file)return;let reader=new FileReader();reader.onload=()=>{bulkImportRows=parseBulkWorkItems(String(reader.result||''));let e=document.getElementById('bulk-preview');if(e)e.innerHTML=bulkImportPreviewHtml()};reader.readAsText(file)}
function bulkImportPreviewHtml(){
 if(!bulkImportRows.length)return `<div class="bulk-import-empty">No CSV loaded yet.</div>`;
 let targetSite=document.getElementById('bulk-site')?.value||bulkImportSite,existing=new Set(db.items.filter(x=>x.site===targetSite).map(x=>String(x.ref||'').trim().toLowerCase())),dupes=bulkImportRows.filter(r=>existing.has(r.ref.toLowerCase())),missingTeams=bulkImportRows.filter(r=>r.teamMissing),valid=bulkImportRows.length-dupes.length;
 return `<div class="bulk-import-summary"><div><span>Rows found</span><b>${bulkImportRows.length}</b></div><div><span>Ready to import</span><b>${valid}</b></div><div class="${dupes.length?'bulk-warn':''}"><span>Already exists</span><b>${dupes.length}</b></div><div class="${missingTeams.length?'bulk-warn':''}"><span>Unknown Teams</span><b>${missingTeams.length}</b></div></div>${missingTeams.length?`<div class="bulk-import-warning"><b>${missingTeams.length} row${missingTeams.length===1?' has':'s have'} a Team name that does not exactly match SiteSight.</b><span>Those Work Items will import as “To be Confirmed”.</span></div>`:''}<div class="bulk-preview-table"><table><thead><tr><th>Row</th><th>Reference</th><th>Type</th><th>House Type</th><th>Team</th><th>Value / Lift Prices</th><th>Status</th></tr></thead><tbody>${bulkImportRows.slice(0,18).map(r=>{let duplicate=existing.has(r.ref.toLowerCase()),value=r.type==='House Plot'?r.liftPrices.map((v,i)=>v?`L${i+1} ${money(v)}`:'').filter(Boolean).join(' · '):(r.setValue?money(r.setValue):'—');return `<tr class="${duplicate?'bulk-row-skip':''}"><td>${r.row}</td><td><b>${calendarSafe(r.ref)}</b></td><td>${calendarSafe(r.type)}</td><td>${calendarSafe(r.houseType||'—')}</td><td>${calendarSafe(r.team||'To be Confirmed')}${r.teamMissing?' <span class="badge b-amber">Not matched</span>':''}</td><td>${value||'—'}</td><td>${duplicate?'<span class="badge b-red">Skip duplicate</span>':'<span class="badge b-green">Ready</span>'}</td></tr>`}).join('')}</tbody></table>${bulkImportRows.length>18?`<div class="bulk-more">+ ${bulkImportRows.length-18} more rows</div>`:''}</div><div class="bulk-import-actions"><button class="btn orange" onclick="confirmBulkImport()" ${valid?'':'disabled'}>Import ${valid} Work Item${valid===1?'':'s'}</button></div>`
}
function confirmBulkImport(){
 if(!hasPerm('manage_workitems'))return toast('You do not have work-item creation access');let siteId=document.getElementById('bulk-site')?.value||bulkImportSite;if(!siteId)return toast('Choose a Site');bulkImportSite=siteId;
 let existing=new Set(db.items.filter(x=>x.site===siteId).map(x=>String(x.ref||'').trim().toLowerCase())),created=[],skipped=0,now=new Date().toISOString();
	 bulkImportRows.forEach((r,n)=>{if(!r.ref||existing.has(r.ref.toLowerCase())){skipped++;return}let gang=db.gangs.find(g=>String(g.name||'').trim().toLowerCase()===String(r.team||'').trim().toLowerCase()),type=r.type||'House Plot',x={id:'bi'+Date.now()+'-'+n,type,ref:r.ref,houseType:r.houseType||'',site:siteId,gang:gang?.id||'',notes:'',signed:false,drawings:[],workPhotos:[],issues:[],variations:[],lastUpdated:iso(),lastUpdatedAt:now,lastUpdatedBy:currentUser()?.name||'',createdAt:now,createdBy:currentUser()?.name||'',bulkImported:true};if(type==='House Plot'){let names=db.sites.find(row=>row.id===siteId)?.stageTemplate||['1st Lift','2nd Lift (Joist)','3rd Lift (Band)','4th Lift (Top/Plate)','5th Lift (Gables)'];x.lifts=names.map((name,i)=>({name,status:'Not started',price:Number(r.liftPrices[i]||0),pct:0,paidTo:x.gang,date:'',paymentNotes:'',variation:'',notes:'',completedDate:'',payments:[]}))}else Object.assign(x,{status:'Not started',price:Number(r.setValue||0),pct:0,paidTo:x.gang,date:'',paymentNotes:'',completedDate:'',payments:[]});db.items.push(x);created.push(x);existing.add(r.ref.toLowerCase())});
 if(!created.length)return toast('No new Work Items to import');save();audit('create','work items','bulk-'+Date.now(),`Bulk import — ${created.length} Work Items created${skipped?`, ${skipped} duplicates skipped`:''}`,siteId);bulkImportRows=[];modal=null;adminSearch='';savedState(`${created.length} Work Items imported`);toast(`${created.length} Work Items imported${skipped?` · ${skipped} skipped`:''}`);render()
}

function newItem(forced){if(!hasPerm('manage_workitems'))return toast('You do not have work-item creation access');let types=workTypes(),isForcedCustom=!!forced&&!types.includes(forced);modal=`<div class="modal"><div class="modalbox"><h2>Create Work Item</h2><div class="two"><div class="field"><label>Type</label><select id="nt" class="select" onchange="toggleCustomType()">${types.map(t=>`<option value="${t}" ${forced===t?'selected':''}>${t}</option>`).join('')}<option value="__custom__" ${isForcedCustom?'selected':''}>＋ Custom type...</option></select><div id="custom-type-box" class="custom-type-wrap" style="display:${isForcedCustom?'block':'none'}"><label>Custom type</label><input id="nt-custom" class="input" style="width:100%;margin-top:5px" value="${isForcedCustom?forced:''}" placeholder="e.g. Porch, Garage, Boundary Feature"></div></div><div class="field"><label>Reference</label><input id="nr" class="input" placeholder="${forced&&forced!=='House Plot'&&forced!=='Garden Wall'?forced+' 1':'Plot 13'}"></div></div><div class="two"><div class="field"><label>Site</label><select id="ns" class="select">${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select></div><div class="field"><label>Team</label><select id="ng" class="select"><option value="">To be Confirmed</option>${db.gangs.filter(g=>g.active).map(g=>`<option value="${g.id}">${g.name}</option>`).join('')}</select></div></div><div class="field"><label>House type (House Plot only)</label><input id="nh" class="input"></div><button class="btn orange" onclick="createItem()">Create work item</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function createItem(){let selectedType=document.getElementById('nt').value,type=selectedType==='__custom__'?(document.getElementById('nt-custom')?.value||'').trim():selectedType,ref=(document.getElementById('nr')?.value||'').trim(),siteId=document.getElementById('ns')?.value||'',gang=document.getElementById('ng')?.value||'';if(!type)return toast('Enter a work item type');if(!ref)return toast('Enter a Work Item reference');if(!siteId)return toast('Choose a Site');if(db.items.some(x=>x.site===siteId&&String(x.ref||'').trim().toLowerCase()===ref.toLowerCase()))return toast('That Work Item reference already exists on this Site');let now=new Date().toISOString(),x={id:'i'+Date.now(),type,ref,houseType:(document.getElementById('nh')?.value||'').trim(),site:siteId,gang,notes:'',signed:false,drawings:[],workPhotos:[],issues:[],variations:[],lastUpdated:iso(),lastUpdatedAt:now,lastUpdatedBy:currentUser()?.name||'',createdAt:now,createdBy:currentUser()?.name||''};if(type==='House Plot'){let names=db.sites.find(row=>row.id===siteId)?.stageTemplate||['1st Lift','2nd Lift (Joist)','3rd Lift (Band)','4th Lift (Top/Plate)','5th Lift (Gables)'];x.lifts=names.map(n=>({name:n,status:'Not started',price:0,pct:0,paidTo:gang,date:'',paymentNotes:'',variation:'',notes:'',completedDate:'',payments:[]}))}else Object.assign(x,{status:'Not started',price:0,pct:0,paidTo:gang,date:'',paymentNotes:'',completedDate:'',payments:[]});db.items.push(x);save();savedState('Work item created');audit('create','work item',x.id,`Work item created — ${x.ref}`,x.site);modal=null;selected=x.id;selectedGang=null;render()}
function addSite(){
 if(!hasPerm('manage_sites'))return toast('You do not have site admin access');
 modal=`<div class="modal"><div class="modalbox"><h2>Add Site</h2><p class="muted">Create a development so Work Items, Teams and records can be kept in the right place.</p><div class="field"><label>Site / development name</label><input id="new-site-name" class="input" placeholder="e.g. Oak View"></div><button class="btn orange" onclick="saveNewSite()">Create site</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function saveNewSite(){
 if(!hasPerm('manage_sites'))return toast('You do not have site admin access');
 let n=(document.getElementById('new-site-name')?.value||'').trim();if(!n)return toast('Enter the Site name');
 if(db.sites.some(s=>!s.archived&&s.name.trim().toLowerCase()===n.toLowerCase()))return toast('A Site with that name already exists');
 let s={id:'s'+Date.now(),name:n,archived:false,createdAt:new Date().toISOString(),createdBy:currentUser()?.name||''};db.sites.push(s);save();audit('site','site',s.id,`Site created — ${n}`,s.id);modal=null;savedState('Site saved');render()
}
function addGang(){addTeam()}
function sitePermissionInputs(ids=[]){let all=!ids||!ids.length;return `<div class="field"><label>Site visibility</label><label class="site-access-item"><input id="usa-all" type="checkbox" ${all?'checked':''} onchange="toggleSitePermissions()"> View all sites</label><div class="site-access-grid">${db.sites.filter(s=>!s.archived).map(s=>`<label class="site-access-item"><input class="usa-site" type="checkbox" value="${s.id}" ${!all&&ids.includes(s.id)?'checked':''} ${all?'disabled':''}> ${s.name}</label>`).join('')}</div><div class="muted" style="margin-top:6px">Untick “View all sites” to restrict this user to selected sites only.</div></div>`}
function toggleSitePermissions(){let all=document.getElementById('usa-all')?.checked;document.querySelectorAll('.usa-site').forEach(c=>c.disabled=all)}
function collectAllowedSites(){if(document.getElementById('usa-all')?.checked)return [];return [...document.querySelectorAll('.usa-site:checked')].map(c=>c.value)}

const roleOptions=['Director','Contracts Manager','Site Manager','Supervisor','Quantity Surveyor','Accounts','Read Only','Operator'];
function permissionInputs(p){return `<details class="permission-customizer"><summary><span><b>Customise permissions</b><small>Role defaults are already applied</small></span><b>▾</b></summary><div class="permission-groups">${permissionGroups.map(g=>`<section class="permission-group"><h4>${g.name}</h4><div class="permgrid">${g.items.map(([k,l])=>`<label class="permcheck"><input type="checkbox" data-perm="${k}" ${p?.[k]?'checked':''}> <span>${l}</span></label>`).join('')}</div></section>`).join('')}</div></details>`}
function refreshRolePerms(){let r=document.getElementById('ur')?.value,p=defaultPerms(r);document.querySelectorAll('[data-perm]').forEach(c=>c.checked=!!p[c.dataset.perm])}
function collectPerms(){let p={};document.querySelectorAll('[data-perm]').forEach(c=>p[c.dataset.perm]=c.checked);return p}
function addUser(){if(!hasPerm('manage_users'))return toast('You do not have permission to manage users');let p=defaultPerms('Operator');modal=`<div class="modal"><div class="modalbox"><h2>Add User</h2><div class="two"><div class="field"><label>Full name</label><input id="un" class="input" placeholder="Alex Smith"></div><div class="field"><label>Username</label><input id="uu" class="input" placeholder="alex.smith"></div></div><div class="field"><label>Email address</label><input id="ue" class="input" type="email" placeholder="alex@company.co.uk"></div><div class="two"><div class="field"><label>Temporary password</label><input id="up" class="input" type="password" autocomplete="new-password" placeholder="Minimum 8 characters"></div><div class="field"><label>Role</label><select id="ur" class="select" onchange="refreshRolePerms()">${roleOptions.map(r=>`<option ${r==='Operator'?'selected':''}>${r}</option>`).join('')}</select></div></div><div class="role-template-note"><b>Role template:</b> select the closest job role first. SiteSight loads sensible default permissions, then the Director can customise individual access.</div><h3>Access</h3><p class="muted">Recommended permissions are applied automatically from the selected role.</p>${permissionInputs(p)}<h3 style="margin-top:18px">Site access</h3>${sitePermissionInputs([])}<div style="margin-top:14px"><button class="btn orange" onclick="saveNewUser()">Save user</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()}
function saveNewUser(){if(!hasPerm('manage_users'))return toast('You do not have permission to manage users');let name=document.getElementById('un').value.trim(),username=document.getElementById('uu').value.trim().toLowerCase(),email=document.getElementById('ue').value.trim().toLowerCase(),password=document.getElementById('up').value,ur=document.getElementById('ur').value;if(!name||!username||!email||!password)return toast('Name, username, email and password are required');if(password.length<8)return toast('Use a password with at least 8 characters');if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))return toast('Enter a valid email address');if(db.users.some(u=>u.username.toLowerCase()===username))return toast('That username already exists');if(db.users.some(u=>(u.email||'').toLowerCase()===email))return toast('That email address is already registered');db.users.push({id:'u'+Date.now(),name,username,email,password,resetCode:'',resetExpires:0,role:ur,permissions:ur==='Director'?defaultPerms('Director'):collectPerms(),allowedSites:ur==='Director'?[]:collectAllowedSites(),trainingMode:true,onboardingSeen:false,onboardingCompletedAt:'',loginCount:0,lastLoginAt:'',recentWorkItems:[],cameraIntroSeen:false,linkedMemberId:'',accountActive:true,disabledReason:'',disabledAt:'',disabledBy:''});save();modal=null;savedState('User saved');toast('User added');render()}function editUserAccess(id){if(!hasPerm('manage_users'))return toast('You do not have permission to manage users');let u=db.users.find(u=>u.id===id);if(!u||u.role==='Director')return;modal=`<div class="modal"><div class="modalbox"><h2>Edit User Access</h2><p><b>${u.name}</b> · @${u.username}</p><div class="field"><label>Registered email</label><input id="ee" class="input" type="email" value="${u.email||''}"></div><div class="field"><label>Role</label><select id="er" class="select" onchange="document.querySelectorAll('[data-perm]').forEach(c=>c.checked=!!defaultPerms(document.getElementById('er').value)[c.dataset.perm])">${roleOptions.filter(r=>r!=='Director').map(r=>`<option ${u.role===r?'selected':''}>${r}</option>`).join('')}</select></div><h3>Access</h3><p class="muted">Role defaults are applied automatically. Open Customise permissions only when this user needs an exception.</p>${permissionInputs(u.permissions)}<h3 style="margin-top:18px">Site access</h3>${sitePermissionInputs(u.allowedSites||[])}<div style="margin-top:14px"><button class="btn orange" onclick="saveUserAccess('${u.id}')">Save access</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()}
function saveUserAccess(id){if(!hasPerm('manage_users'))return toast('You do not have permission to manage users');let u=db.users.find(u=>u.id===id);if(!u)return;let email=(document.getElementById('ee')?.value||'').trim().toLowerCase();if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))return toast('Enter a valid email address');if(db.users.some(x=>x.id!==id&&(x.email||'').toLowerCase()===email))return toast('That email is already registered');u.email=email;u.role=document.getElementById('er').value;u.permissions=collectPerms();u.allowedSites=collectAllowedSites();save();modal=null;toast('User access updated');render()}function toast(t){let e=document.createElement('div');e.className='toast';e.textContent=t;document.body.appendChild(e);setTimeout(()=>e.remove(),1600)}
document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='k'&&currentUser()){e.preventDefault();openGlobalSearch()}});
window.addEventListener('beforeunload',e=>{if(dirty){e.preventDefault();e.returnValue=''}});

/* ===== v167-contractor.js ===== */
// ============================================================
// SiteSight V1.67 — Brickwork Contractor Operations
// Keeps the everyday UI simple while adding contractor-specific
// people, planning, pay, daywork and drawing controls.
// Clocking remains Clock In / Clock Out only.
// ============================================================

let contractorAttendanceTab='today', contractorLeaveFilter='active', contractorPayWeek='', contractorReportWeek='',
    contractorDayworkSite='all', contractorDayworkSearch='';

function contractorEsc(v){return calendarSafe(v)}
function contractorId(p='v167'){return p+'-'+Date.now()+'-'+Math.random().toString(16).slice(2)}
function contractorWeekStart(v=iso()){return attendanceWeekStartISO(v)}
function contractorToday(){return iso()}
function contractorMember(workerId){return attendanceMemberById(workerId)}
function contractorMemberName(workerId){return contractorMember(workerId)?.member?.name||'Team member'}
function contractorTeamName(workerId){return contractorMember(workerId)?.gang?.name||'Team'}
function contractorSiteForPeople(){return attendanceResolvedSite?attendanceResolvedSite():attendanceDefaultSiteId()}
function contractorCanManagePeople(){return hasPerm('manage_gangs')||hasPerm('manage_attendance')||canDirector()}
function contractorCanHR(){return canDirector()||hasPerm('manage_users')}
function contractorCanApproveLeave(){return hasPerm('manage_attendance')||hasPerm('manage_gangs')||canDirector()}
function contractorCanViewLeave(){return hasPerm('gangs')||hasPerm('view_attendance')||hasPerm('clock_attendance')||canDirector()}
function contractorCurrentLeaveFor(date,siteId='all'){
  return (db.leaveRequests||[]).filter(r=>r.status==='Approved'&&r.from<=date&&r.to>=date&&(siteId==='all'||r.siteId===siteId))
}
function contractorAbsencesFor(date,siteId='all'){
  return (db.attendanceAbsences||[]).filter(a=>a.date===date&&(siteId==='all'||a.siteId===siteId))
}
function contractorPeopleSummary(date=contractorToday(),siteId=contractorSiteForPeople()){
  let expected=attendanceExpectedMembers(siteId), leave=contractorCurrentLeaveFor(date,siteId),
      abs=contractorAbsencesFor(date,siteId),
      holidayIds=new Set(leave.filter(x=>x.type==='Holiday').map(x=>x.workerId)),
      sickIds=new Set(abs.filter(x=>x.type==='Sick').map(x=>x.workerId)),
      trainingIds=new Set(abs.filter(x=>x.type==='Training').map(x=>x.workerId)),
      active=(db.attendanceShifts||[]).filter(s=>s.siteId===siteId&&!s.voided&&attendanceDateOnly(s.clockIn?.time)===date&&!s.clockOut?.time),
      activeIds=new Set(active.map(s=>s.workerId)),
      notIn=expected.filter(x=>!activeIds.has(x.member.id)&&!holidayIds.has(x.member.id)&&!sickIds.has(x.member.id)&&!trainingIds.has(x.member.id));
  return {expected,leave,abs,holidayIds,sickIds,trainingIds,active,notIn}
}
function contractorPeopleTodayStrip(){
  if(!contractorCanViewLeave()||!activeSites().length)return '';
  let sid=contractorSiteForPeople();if(!sid)return '';
  let s=contractorPeopleSummary(contractorToday(),sid);
  return `<div class="people-today-strip">
    <div><span>PEOPLE TODAY</span><b>${siteName(sid)}</b></div>
    <button onclick="goNav('attendance');contractorAttendanceTab='today';render()"><strong>${s.active.length}</strong><small>On site</small></button>
    <button onclick="goNav('attendance');contractorAttendanceTab='leave';render()"><strong>${s.holidayIds.size}</strong><small>Holiday</small></button>
    <button onclick="goNav('attendance');contractorAttendanceTab='leave';render()"><strong>${s.sickIds.size}</strong><small>Sick</small></button>
    <button onclick="goNav('attendance');contractorAttendanceTab='today';render()" class="${s.notIn.length?'warn':''}"><strong>${s.notIn.length}</strong><small>Not clocked in</small></button>
  </div>`
}

// ---- Migration wrapper ------------------------------------------------
const v167BaseMigrate=migrate;
migrate=function(persist=true){
  v167BaseMigrate(false);
  db.leaveRequests=Array.isArray(db.leaveRequests)?db.leaveRequests:[];
  db.dayRateUnits=Array.isArray(db.dayRateUnits)?db.dayRateUnits:[];
  db.dayworks=Array.isArray(db.dayworks)?db.dayworks:[];
  db.hrRecords=Array.isArray(db.hrRecords)?db.hrRecords:[];
  db.sites=(db.sites||[]).map(s=>({...s,
    stageTemplate:Array.isArray(s.stageTemplate)&&s.stageTemplate.length?s.stageTemplate:['1st Lift','2nd Lift (Joist)','3rd Lift (Band)','4th Lift (Top/Plate)','5th Lift (Gables)']
  }));
  db.gangs=(db.gangs||[]).map(g=>({...g,members:(g.members||[]).map(m=>({...m,
    startDate:m.startDate||'',leavingDate:m.leavingDate||'',employmentStatus:m.employmentStatus||'Active',
    usualDays:Array.isArray(m.usualDays)&&m.usualDays.length?m.usualDays:[1,2,3,4,5],
    qualifications:Array.isArray(m.qualifications)?m.qualifications:[],
    profilePhoto:m.profilePhoto||''
  }))}));
  db.items=(db.items||[]).map(x=>({...x,
    plannedStart:x.plannedStart||'',targetComplete:x.targetComplete||'',
    issues:(x.issues||[]).map(i=>({...i,blockingWork:!!i.blockingWork})),
    variations:(x.variations||[]).map(v=>({...v,instructionRef:v.instructionRef||'',instructedBy:v.instructedBy||'',instructionDate:v.instructionDate||''})),
    drawings:(x.drawings||[]).map(d=>({...d,drawingNumber:d.drawingNumber||'',revision:d.revision||'',issuedDate:d.issuedDate||d.uploadedDate||'',revisionStatus:d.revisionStatus||'Current'}))
  }));
  db.siteDrawings=(db.siteDrawings||[]).map(d=>({...d,drawingNumber:d.drawingNumber||'',revision:d.revision||'',issuedDate:d.issuedDate||d.uploadedDate||'',revisionStatus:d.revisionStatus||'Current'}));
  if(persist)save()
}

// ---- No Work Item clock allocation ----------------------------------
attendanceWorkItemName=function(){return ''}

// ---- Holiday / leave -------------------------------------------------
function contractorOpenLeaveRequest(workerId=''){
  if(!contractorCanViewLeave())return toast('You do not have access to People / Leave');
  let linked=attendanceLinkedMember(), defaultWorker=workerId||(linked?.member?.id||''), full=contractorCanApproveLeave();
  if(!full&&linked)defaultWorker=linked.member.id;
  let sid=attendanceDefaultSiteId();
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">PEOPLE</span><h2>Book Holiday / Leave</h2>
    <div class="field"><label>Person</label><select id="lv-worker" class="select" ${!full?'disabled':''}>${attendanceAllMembers().filter(x=>x.member.activeForAttendance!==false).map(x=>`<option value="${x.member.id}" ${x.member.id===defaultWorker?'selected':''}>${contractorEsc(x.member.name)} · ${contractorEsc(x.gang.name)}</option>`).join('')}</select></div>
    <div class="field"><label>Site</label><select id="lv-site" class="select">${activeSites().map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${contractorEsc(s.name)}</option>`).join('')}</select></div>
    <div class="two"><div class="field"><label>From</label><input id="lv-from" class="input" type="date" value="${contractorToday()}"></div><div class="field"><label>To</label><input id="lv-to" class="input" type="date" value="${contractorToday()}"></div></div>
    <div class="two"><div class="field"><label>Type</label><select id="lv-type" class="select"><option>Holiday</option><option>Training</option><option>Other Leave</option></select></div><div class="field"><label>Day</label><select id="lv-day" class="select"><option value="1">Full day</option><option value="0.5">Half day</option></select></div></div>
    <div class="field"><label>Note</label><textarea id="lv-note" class="textarea" placeholder="Optional useful note"></textarea></div>
    <button class="btn orange" onclick="contractorSaveLeave()">Submit booking</button> <button class="btn light" onclick="modal=null;render()">Cancel</button>
  </div></div>`;render()
}
function contractorSaveLeave(){
  let workerId=document.getElementById('lv-worker')?.value||'',siteId=document.getElementById('lv-site')?.value||'',
      from=document.getElementById('lv-from')?.value||'',to=document.getElementById('lv-to')?.value||'',
      type=document.getElementById('lv-type')?.value||'Holiday',dayUnit=Number(document.getElementById('lv-day')?.value||1),
      note=(document.getElementById('lv-note')?.value||'').trim();
  if(!workerId||!siteId||!from||!to)return toast('Person, Site, From and To are required');
  if(from>to)return toast('From date must be before To date');
  let linked=attendanceLinkedMember(), self=linked?.member?.id===workerId, canApprove=contractorCanApproveLeave()&&!self;
  let r={id:contractorId('leave'),workerId,siteId,from,to,type,dayUnit,note,status:canApprove?'Approved':'Pending',
    requestedBy:currentUser()?.name||'',requestedById:currentUser()?.id||'',requestedAt:new Date().toISOString(),
    approvedBy:canApprove?(currentUser()?.name||''):'',approvedById:canApprove?(currentUser()?.id||''):'',approvedAt:canApprove?new Date().toISOString():''};
  db.leaveRequests.unshift(r);save();audit('people','leave',r.id,`${contractorMemberName(workerId)}: ${type} ${from} → ${to} — ${r.status}`,siteId);
  modal=null;savedState(r.status==='Approved'?'Leave booked':'Leave request submitted');render()
}
function contractorApproveLeave(id,approve=true){
  if(!contractorCanApproveLeave())return toast('You do not have permission to approve leave');
  let r=(db.leaveRequests||[]).find(r=>r.id===id);if(!r)return;
  if(r.requestedById===currentUser()?.id&&r.workerId===attendanceLinkedMember()?.member?.id)return toast('You cannot approve your own leave request');
  r.status=approve?'Approved':'Declined';r.approvedBy=currentUser()?.name||'';r.approvedById=currentUser()?.id||'';r.approvedAt=new Date().toISOString();
  save();audit('people','leave',r.id,`${contractorMemberName(r.workerId)}: ${r.type} ${r.status}`,r.siteId);savedState(`Leave ${r.status.toLowerCase()}`);render()
}
function contractorLeavePage(siteId){
  let rows=(db.leaveRequests||[]).filter(r=>canAccessSite(r.siteId)&&(siteId==='all'||r.siteId===siteId));
  if(contractorLeaveFilter==='active')rows=rows.filter(r=>r.status==='Pending'||r.to>=contractorToday());
  if(contractorLeaveFilter==='pending')rows=rows.filter(r=>r.status==='Pending');
  rows.sort((a,b)=>String(a.from).localeCompare(String(b.from)));
  return `<div class="contractor-section">
    <div class="sectionhead"><div><h2>Holiday & Leave</h2><div class="muted">Bookings automatically appear on the Calendar once approved.</div></div><button class="btn orange" onclick="contractorOpenLeaveRequest()">＋ Book leave</button></div>
    <div class="issue-focus contractor-filter"><button class="${contractorLeaveFilter==='active'?'active':''}" onclick="contractorLeaveFilter='active';render()">Current / Upcoming</button><button class="${contractorLeaveFilter==='pending'?'active':''}" onclick="contractorLeaveFilter='pending';render()">Awaiting approval</button><button class="${contractorLeaveFilter==='all'?'active':''}" onclick="contractorLeaveFilter='all';render()">All</button></div>
    <div class="leave-list">${rows.map(r=>`<div class="leave-row ${r.status.toLowerCase().replace(/\s/g,'-')}"><div><b>${contractorEsc(contractorMemberName(r.workerId))}</b><span>${contractorEsc(contractorTeamName(r.workerId))} · ${contractorEsc(siteName(r.siteId))}</span></div><div><b>${contractorEsc(r.type)}</b><span>${r.from}${r.to!==r.from?' → '+r.to:''}${Number(r.dayUnit)===0.5?' · Half day':''}</span></div><span class="leave-status">${r.status}</span>${r.status==='Pending'&&contractorCanApproveLeave()?`<div class="actions"><button class="btn green smallbtn" onclick="contractorApproveLeave('${r.id}',true)">Approve</button><button class="btn light smallbtn" onclick="contractorApproveLeave('${r.id}',false)">Decline</button></div>`:''}</div>`).join('')||smartEmpty('No leave records in this view.','Holiday and leave bookings will appear here.','','','◷')}</div>
  </div>`
}

// ---- Dayworks ---------------------------------------------------------
function contractorDayworksPage(){
  if(!hasPerm('work_items'))return `<div class="panel empty-help"><b>You do not have Work access.</b></div>`;
  let sid=contractorDayworkSite==='all'?(site!=='all'?site:(activeSites()[0]?.id||'')):contractorDayworkSite,
      rows=(db.dayworks||[]).filter(d=>canAccessSite(d.siteId)&&(sid==='all'||!sid||d.siteId===sid)),
      q=contractorDayworkSearch.trim().toLowerCase();
  if(q)rows=rows.filter(d=>[d.area,d.work,d.instructedBy,d.instructionRef,siteName(d.siteId)].join(' ').toLowerCase().includes(q));
  rows.sort((a,b)=>String(b.date).localeCompare(String(a.date)));
  return `<div class="topbar"><div><h1>Dayworks</h1><div class="sub">Simple evidence for instructed extra work</div></div><div class="actions"><select class="select" onchange="contractorDayworkSite=this.value;render()"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${contractorEsc(s.name)}</option>`).join('')}</select>${hasPerm('manage_variations')||hasPerm('update_progress')?'<button class="btn orange" onclick="contractorAddDaywork()">＋ Record Daywork</button>':''}</div></div>
    <div class="panel" style="padding:15px"><div class="searchbox"><span>⌕</span><input value="${contractorEsc(contractorDayworkSearch)}" placeholder="Search dayworks…" oninput="contractorDayworkSearch=this.value;render()"></div>
    <div class="daywork-list">${rows.map(d=>`<div class="daywork-card"><div class="sectionhead"><div><span class="badge b-purple">Daywork</span><h3>${contractorEsc(d.area||'Site work')}</h3><div class="muted">${d.date} · ${contractorEsc(siteName(d.siteId))}</div></div>${hasPerm('view_financials')?`<b>${attendanceMoney(d.value||0)}</b>`:''}</div><p>${contractorEsc(d.work)}</p><div class="daywork-meta"><span>Instructed by <b>${contractorEsc(d.instructedBy||'—')}</b></span><span>Ref <b>${contractorEsc(d.instructionRef||'—')}</b></span><span>${(d.labour||[]).reduce((n,r)=>n+Number(r.hours||0),0).toFixed(1)} labour hrs</span>${d.variationId?'<span>✓ Variation created</span>':''}</div></div>`).join('')||smartEmpty('No Dayworks recorded.','Use Daywork when extra work is instructed outside the original price.','','','＋')}</div></div>`
}
function contractorAddDaywork(){
  let sid=site!=='all'?site:(activeSites()[0]?.id||'');
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">EXTRA WORK</span><h2>Record Daywork</h2>
    <div class="two"><div class="field"><label>Date</label><input id="dw-date" class="input" type="date" value="${contractorToday()}"></div><div class="field"><label>Site</label><select id="dw-site" class="select">${activeSites().map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${contractorEsc(s.name)}</option>`).join('')}</select></div></div>
    <div class="field"><label>Plot / Area</label><input id="dw-area" class="input" placeholder="e.g. Plot 034 / retaining wall"></div>
    <div class="field"><label>Work instructed</label><textarea id="dw-work" class="textarea" placeholder="What extra work was carried out?"></textarea></div>
    <div class="two"><div class="field"><label>Instructed by</label><input id="dw-by" class="input" placeholder="Name / role"></div><div class="field"><label>Instruction reference</label><input id="dw-ref" class="input" placeholder="Email / SI / VO / verbal ref"></div></div>
    <div class="field"><label>Labour hours <span class="muted">(simple total)</span></label><input id="dw-hours" class="input" type="number" min="0" step="0.5" value="8"></div>
    ${hasPerm('view_financials')?'<div class="field"><label>Estimated value</label><input id="dw-value" class="input" type="number" min="0" step="0.01"></div>':''}
    ${hasPerm('manage_variations')?'<label class="permcheck"><input id="dw-variation" type="checkbox"> <span>Create a pending Variation from this Daywork</span></label>':''}
    <div style="margin-top:14px"><button class="btn orange" onclick="contractorSaveDaywork()">Save Daywork</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div>
  </div></div>`;render()
}
function contractorSaveDaywork(){
  let siteId=document.getElementById('dw-site')?.value||'',date=document.getElementById('dw-date')?.value||'',area=(document.getElementById('dw-area')?.value||'').trim(),
      work=(document.getElementById('dw-work')?.value||'').trim(),by=(document.getElementById('dw-by')?.value||'').trim(),ref=(document.getElementById('dw-ref')?.value||'').trim(),
      hours=Number(document.getElementById('dw-hours')?.value||0),value=Number(document.getElementById('dw-value')?.value||0);
  if(!siteId||!date||!work||!by)return toast('Site, date, work and instructed by are required');
  let d={id:contractorId('dw'),siteId,date,area,work,instructedBy:by,instructionRef:ref,labour:[{hours}],value,createdBy:currentUser()?.name||'',createdAt:new Date().toISOString(),variationId:''};
  if(document.getElementById('dw-variation')?.checked){
    let item=db.items.find(x=>x.site===siteId&&(area&&String(area).toLowerCase().includes(String(x.ref||'').toLowerCase())));
    if(item){
      let v={id:contractorId('var'),name:`Daywork — ${area||work.slice(0,40)}`,value,date,stage:'Daywork',team:item.gang||'',payments:[],approvalStatus:'Pending Approval',approvedBy:'',approvedAt:'',instructionRef:ref,instructedBy:by,instructionDate:date};
      item.variations.push(v);d.variationId=v.id
    }
  }
  db.dayworks.unshift(d);save();audit('daywork','daywork',d.id,`Daywork recorded — ${area||siteName(siteId)} · ${work.slice(0,80)}`,siteId);modal=null;savedState('Daywork saved');render()
}

// ---- Planning / work presets / stage templates -----------------------
workTypes=function(){return ['House Plot','Substructure','Garage','Boundary / Garden Wall','Retaining Wall','Remedials / Snagging','Daywork','Other']}
const v167BaseNewItem=newItem;
newItem=function(forced){
  if(!hasPerm('manage_workitems'))return toast('You do not have work-item creation access');
  let types=workTypes(),selectedType=forced||'House Plot';
  modal=`<div class="modal"><div class="modalbox"><h2>Create Work Item</h2>
    <div class="two"><div class="field"><label>Type</label><select id="nt" class="select" onchange="document.getElementById('custom-type-box').style.display=this.value==='Other'?'block':'none'">${types.map(t=>`<option value="${t}" ${selectedType===t?'selected':''}>${t}</option>`).join('')}</select><div id="custom-type-box" style="display:${selectedType==='Other'?'block':'none'};margin-top:7px"><label>Other type</label><input id="nt-custom" class="input" placeholder="Short clear work type"></div></div><div class="field"><label>Reference</label><input id="nr" class="input" placeholder="Plot 013 / Wall 02"></div></div>
    <div class="two"><div class="field"><label>Site</label><select id="ns" class="select">${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${contractorEsc(s.name)}</option>`).join('')}</select></div><div class="field"><label>Team</label><select id="ng" class="select"><option value="">To be Confirmed</option>${db.gangs.filter(g=>g.active).map(g=>`<option value="${g.id}">${contractorEsc(g.name)}</option>`).join('')}</select></div></div>
    <div class="field"><label>House type <span class="muted">(House Plot only)</span></label><input id="nh" class="input"></div>
    <div class="two"><div class="field"><label>Planned start</label><input id="n-start" class="input" type="date"></div><div class="field"><label>Target completion</label><input id="n-target" class="input" type="date"></div></div>
    <button class="btn orange" onclick="contractorCreateItem()">Create work item</button> <button class="btn light" onclick="modal=null;render()">Cancel</button>
  </div></div>`;render()
}
function contractorCreateItem(){
  let selectedType=document.getElementById('nt')?.value||'House Plot',type=selectedType==='Other'?((document.getElementById('nt-custom')?.value||'').trim()||'Other'):selectedType,
      ref=(document.getElementById('nr')?.value||'').trim(),siteId=document.getElementById('ns')?.value||'',gang=document.getElementById('ng')?.value||'';
  if(!ref)return toast('Enter a Work Item reference');if(!siteId)return toast('Choose a Site');
  if(db.items.some(x=>x.site===siteId&&String(x.ref||'').trim().toLowerCase()===ref.toLowerCase()))return toast('That Work Item reference already exists on this Site');
  let now=new Date().toISOString(),x={id:contractorId('wi'),type,ref,houseType:(document.getElementById('nh')?.value||'').trim(),site:siteId,gang,notes:'',signed:false,drawings:[],workPhotos:[],issues:[],variations:[],plannedStart:document.getElementById('n-start')?.value||'',targetComplete:document.getElementById('n-target')?.value||'',lastUpdated:iso(),lastUpdatedAt:now,lastUpdatedBy:currentUser()?.name||'',createdAt:now,createdBy:currentUser()?.name||''};
  if(type==='House Plot'){let names=db.sites.find(s=>s.id===siteId)?.stageTemplate||['1st Lift','2nd Lift (Joist)','3rd Lift (Band)','4th Lift (Top/Plate)','5th Lift (Gables)'];x.lifts=names.map(n=>({name:n,status:'Not started',price:0,pct:0,paidTo:gang,date:'',paymentNotes:'',variation:'',notes:'',completedDate:'',payments:[]}))}
  else Object.assign(x,{status:'Not started',price:0,pct:0,paidTo:gang,date:'',paymentNotes:'',completedDate:'',payments:[]});
  db.items.push(x);save();audit('create','work item',x.id,`Work item created — ${x.ref}`,x.site);modal=null;selected=x.id;render()
}
function contractorProgrammeState(x){
  let today=contractorToday(),st=overall(x);
  if(x.targetComplete&&x.targetComplete<today&&!['Complete','Signed Off'].includes(st))return {cls:'late',label:`Behind programme · target ${x.targetComplete}`};
  if(x.plannedStart&&x.plannedStart>today&&st==='Not started')return {cls:'upcoming',label:`Starts ${x.plannedStart}`};
  return null
}
const v167BaseCard=card;
card=function(x){
  let html=v167BaseCard(x),p=contractorProgrammeState(x);
  if(p)html=html.replace('</div>',`<span class="programme-pill ${p.cls}">${p.label}</span></div>`);
  return html
}
function contractorEditProgramme(){
  let x=db.items.find(x=>x.id===selected);if(!x)return;
  modal=`<div class="modal"><div class="modalbox"><h2>Programme Dates</h2><p class="muted">${contractorEsc(x.ref)} · keep planning simple with just two dates.</p><div class="two"><div class="field"><label>Planned start</label><input id="pg-start" class="input" type="date" value="${x.plannedStart||''}"></div><div class="field"><label>Target completion</label><input id="pg-target" class="input" type="date" value="${x.targetComplete||''}"></div></div><button class="btn orange" onclick="contractorSaveProgramme()">Save dates</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function contractorSaveProgramme(){let x=db.items.find(x=>x.id===selected);if(!x)return;x.plannedStart=document.getElementById('pg-start')?.value||'';x.targetComplete=document.getElementById('pg-target')?.value||'';touch(x);save();audit('programme','work item',x.id,`${x.ref}: programme dates updated`,x.site);modal=null;render()}
const v167BaseDetail=detail;
detail=function(x){
  let html=v167BaseDetail(x),p=contractorProgrammeState(x),
      plan=`<div class="contractor-plan-line"><span><b>Programme</b> · ${x.plannedStart?`Start ${x.plannedStart}`:'No start date'} · ${x.targetComplete?`Target ${x.targetComplete}`:'No target date'} ${p?`<strong class="${p.cls}">${p.label}</strong>`:''}</span>${hasPerm('manage_workitems')?'<button class="btn light smallbtn" onclick="contractorEditProgramme()">Edit dates</button>':''}</div>`;
  return html.replace('<div class="work-layout">',plan+'<div class="work-layout">')
}
function contractorEditStageTemplate(siteId){
  if(!hasPerm('manage_sites'))return toast('Site admin permission is required');let s=db.sites.find(s=>s.id===siteId);if(!s)return;
  modal=`<div class="modal"><div class="modalbox"><h2>House Plot Stage Template</h2><p class="muted">${contractorEsc(s.name)} · this only changes new House Plots. Existing Work Items are left untouched.</p><div id="stage-template-editor">${(s.stageTemplate||[]).map((n,i)=>`<div class="field"><label>Stage ${i+1}</label><input class="input v167-stage-name" value="${contractorEsc(n)}"></div>`).join('')}</div><button class="btn orange" onclick="contractorSaveStageTemplate('${siteId}')">Save template</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function contractorSaveStageTemplate(siteId){let s=db.sites.find(s=>s.id===siteId),names=[...document.querySelectorAll('.v167-stage-name')].map(e=>e.value.trim()).filter(Boolean);if(!s||!names.length)return toast('Keep at least one stage');s.stageTemplate=names;save();audit('site','stage template',siteId,`House Plot stage template updated — ${names.join(' → ')}`,siteId);modal=null;render()}

// ---- Blocking Issues --------------------------------------------------
const v167BaseEditIssueRecord=editIssueRecord;
editIssueRecord=function(xid,iid){
  v167BaseEditIssueRecord(xid,iid);
  let box=document.querySelector('.modalbox');if(!box)return;
  let btn=box.querySelector("button[onclick^=\"saveIssueRecord\"]");
  if(btn&&!document.getElementById('ei-blocking'))btn.insertAdjacentHTML('beforebegin',`<label class="permcheck" style="margin:10px 0"><input id="ei-blocking" type="checkbox" ${db.items.find(x=>x.id===xid)?.issues?.find(i=>i.id===iid)?.blockingWork?'checked':''}> <span><b>Blocking work</b> — this issue is stopping progress</span></label>`)
}
const v167BaseSaveIssue=saveIssueRecord;
saveIssueRecord=function(xid,iid){let x=db.items.find(x=>x.id===xid),i=x?.issues?.find(i=>i.id===iid),el=document.getElementById('ei-blocking');if(i&&el)i.blockingWork=!!el.checked;v167BaseSaveIssue(xid,iid)}
const v167BaseIssuePriority=issuePriorityBadge;
issuePriorityBadge=function(i){return `${i.blockingWork?'<span class="badge b-red">BLOCKING WORK</span>':''}${v167BaseIssuePriority(i)}`}

// ---- Variations: instruction evidence --------------------------------
const v167BaseAddVariation=addVariation;
addVariation=function(){
  v167BaseAddVariation();
  let box=document.querySelector('.modalbox');if(!box)return;
  let saveBtn=[...box.querySelectorAll('button')].find(b=>String(b.getAttribute('onclick')||'').includes('saveVariation'));
  if(saveBtn&&!document.getElementById('var-inst-ref'))saveBtn.insertAdjacentHTML('beforebegin',`<div class="two"><div class="field"><label>Instruction reference</label><input id="var-inst-ref" class="input" placeholder="SI / VO / email ref"></div><div class="field"><label>Instruction date</label><input id="var-inst-date" class="input" type="date" value="${contractorToday()}"></div></div><div class="field"><label>Instructed by</label><input id="var-inst-by" class="input" placeholder="Name / role"></div>`)
}
const v167BaseSaveVariation=saveVariation;
saveVariation=function(){
  let before=new Set((db.items.find(x=>x.id===selected)?.variations||[]).map(v=>v.id)),
      ref=document.getElementById('var-inst-ref')?.value||'',by=document.getElementById('var-inst-by')?.value||'',date=document.getElementById('var-inst-date')?.value||'';
  v167BaseSaveVariation();
  let x=db.items.find(x=>x.id===selected),v=(x?.variations||[]).find(v=>!before.has(v.id));
  if(v){v.instructionRef=ref;v.instructedBy=by;v.instructionDate=date;save()}
}
const v167BaseVariationsBlock=variationsBlock;
variationsBlock=function(x){
  let html=v167BaseVariationsBlock(x);
  (x.variations||[]).forEach(v=>{if(v.instructionRef||v.instructedBy||v.instructionDate){let needle=`<div class="variation-meta"><span>${v.date||'—'}</span>`;let rep=`<div class="variation-instruction">Instruction: <b>${contractorEsc(v.instructionRef||'No ref')}</b>${v.instructedBy?' · '+contractorEsc(v.instructedBy):''}${v.instructionDate?' · '+v.instructionDate:''}</div>`+needle;html=html.replace(needle,rep)}});return html
}

// ---- Drawing revision metadata ---------------------------------------
const v167BaseDrawingSummary=drawingTakeoffSummary;
drawingTakeoffSummary=function(d){return `${d.revisionStatus==='Superseded'?'<span class="badge b-red">SUPERSEDED</span>':d.revision?'<span class="badge b-green">CURRENT</span>':''}${v167BaseDrawingSummary(d)}`}

// ---- People records / qualifications / HR ----------------------------
function contractorQualificationWarning(m){
  let today=new Date(contractorToday()+'T12:00:00'), soon=new Date(today);soon.setDate(soon.getDate()+30);
  let exp=(m.qualifications||[]).filter(q=>q.expiry&&new Date(q.expiry+'T12:00:00')<=soon);
  return exp.length?`<span class="qualification-warning">⚠ ${exp.length} qualification${exp.length===1?'':'s'} due / expired</span>`:''
}
openTeamMember=function(teamId,memberId){
  let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!g||!m)return;
  let week=attendanceWeekTotals(m.id,attendanceWeekStartISO(new Date()),attendanceDefaultSiteId()),active=attendanceActiveShift(m.id),editable=contractorCanManagePeople(),payVisible=hasPerm('view_financials');
  modal=`<div class="modal"><div class="modalbox"><h2>${contractorEsc(m.name)}</h2><p class="muted">${contractorEsc(m.role||'Team member')} · ${contractorEsc(g.name)} ${contractorQualificationWarning(m)}</p>
    ${hasPerm('view_attendance')||hasPerm('manage_attendance')?`<div class="attendance-member-mini"><div><span>THIS WEEK</span><b>${attendanceFmtHours(week.totalHours)}</b></div><div><span>STATUS</span><b>${active?'● Clocked In':'○ Not Clocked In'}</b></div>${payVisible&&['Hourly','Day Rate'].includes(m.employmentType)?`<div><span>EST. GROSS</span><b>${attendanceMoney(week.gross)}</b></div>`:''}</div>`:''}
    <div class="two"><div class="field"><label>Start date</label><input id="tm-start" class="input" type="date" value="${m.startDate||''}" ${editable?'':'disabled'}></div><div class="field"><label>Leaving date</label><input id="tm-leave" class="input" type="date" value="${m.leavingDate||''}" ${editable?'':'disabled'}></div></div>
    <div class="field"><label>Status</label><select id="tm-status" class="select" ${editable?'':'disabled'}>${['Active','Leaver','Inactive'].map(s=>`<option ${m.employmentStatus===s?'selected':''}>${s}</option>`).join('')}</select></div>
    <div class="field"><label>Member notes</label><textarea id="tm-notes" class="textarea" style="min-height:120px" ${editable?'':'disabled'} placeholder="Operational notes only. Keep sensitive HR matters in Confidential HR.">${contractorEsc(m.notes||'')}</textarea></div>
    <div class="contractor-qualifications"><div class="sectionhead"><h3>Qualifications / Expiry</h3>${editable?`<button class="btn light smallbtn" onclick="contractorAddQualification('${teamId}','${memberId}')">＋ Add</button>`:''}</div>${(m.qualifications||[]).map(q=>`<div class="qualification-row"><b>${contractorEsc(q.name)}</b><span>${q.expiry||'No expiry'}</span></div>`).join('')||'<div class="muted">No qualifications recorded.</div>'}</div>
    <div class="attendance-member-actions">${hasPerm('view_attendance')?`<button class="btn light" onclick="modal=null;attendanceWorker='${m.id}';contractorAttendanceTab='timesheets';goNav('attendance')">◷ View timesheet</button>`:''}${hasPerm('manage_attendance')?`<button class="btn light" onclick="editAttendanceMemberSettings('${teamId}','${memberId}')">Pay / attendance settings</button>`:''}${contractorCanHR()?`<button class="btn light" onclick="contractorOpenHR('${teamId}','${memberId}')">🔒 Confidential HR</button>`:''}</div>
    <div style="margin-top:14px">${editable?`<button class="btn blue" onclick="contractorSaveMember('${teamId}','${memberId}')">Save changes</button> `:''}<button class="btn light" onclick="modal=null;render()">Close</button></div>
  </div></div>`;render()
}
function contractorSaveMember(teamId,memberId){let m=db.gangs.find(g=>g.id===teamId)?.members?.find(m=>m.id===memberId);if(!m)return;m.startDate=document.getElementById('tm-start')?.value||'';m.leavingDate=document.getElementById('tm-leave')?.value||'';m.employmentStatus=document.getElementById('tm-status')?.value||'Active';m.notes=document.getElementById('tm-notes')?.value||'';save();audit('people','team member',m.id,`${m.name}: member record updated`,'');modal=null;savedState('Member saved');render()}
function contractorAddQualification(teamId,memberId){let m=db.gangs.find(g=>g.id===teamId)?.members?.find(m=>m.id===memberId);if(!m)return;let name=prompt('Qualification / card name (for example CSCS)');if(!name)return;let expiry=prompt('Expiry date YYYY-MM-DD (leave blank if none)')||'';m.qualifications=m.qualifications||[];m.qualifications.push({id:contractorId('qual'),name,expiry});save();openTeamMember(teamId,memberId)}
function contractorOpenHR(teamId,memberId){
  if(!contractorCanHR())return toast('Confidential HR is restricted');let m=db.gangs.find(g=>g.id===teamId)?.members?.find(m=>m.id===memberId);if(!m)return;
  let rows=(db.hrRecords||[]).filter(r=>r.workerId===memberId).sort((a,b)=>String(b.createdAt).localeCompare(String(a.createdAt)));
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">RESTRICTED</span><h2>Confidential HR — ${contractorEsc(m.name)}</h2><p class="muted">Separate from ordinary Team notes. Only authorised management users should use this area.</p>
    <div class="field"><label>Add confidential note</label><textarea id="hr-note" class="textarea" placeholder="Keep records factual and necessary."></textarea></div><button class="btn orange" onclick="contractorSaveHR('${teamId}','${memberId}')">Add record</button>
    <div class="hr-list">${rows.map(r=>`<div class="hr-record"><b>${new Date(r.createdAt).toLocaleString()} · ${contractorEsc(r.by)}</b><p>${contractorEsc(r.note)}</p></div>`).join('')||'<div class="muted">No confidential HR records.</div>'}</div>
    <button class="btn light" onclick="openTeamMember('${teamId}','${memberId}')">← Member record</button></div></div>`;render()
}
function contractorSaveHR(teamId,memberId){if(!contractorCanHR())return;let note=(document.getElementById('hr-note')?.value||'').trim();if(!note)return toast('Enter the confidential note');let r={id:contractorId('hr'),workerId:memberId,note,by:currentUser()?.name||'',byId:currentUser()?.id||'',createdAt:new Date().toISOString()};db.hrRecords.unshift(r);save();audit('hr','hr record',r.id,`${contractorMemberName(memberId)}: confidential HR record added`,'');contractorOpenHR(teamId,memberId)}

// ---- Weekly report ----------------------------------------------------
function contractorWeeklyReportPage(){
  if(!hasPerm('download_reports'))return `<div class="panel empty-help"><b>You do not have report access.</b></div>`;
  let sid=site!=='all'?site:(activeSites()[0]?.id||''),start=contractorReportWeek||contractorWeekStart(),end=attendanceAddDays(start,6),
      items=db.items.filter(x=>x.site===sid),updated=items.filter(x=>x.lastUpdated>=start&&x.lastUpdated<=end),
      openIssues=items.flatMap(x=>(x.issues||[]).filter(i=>i.status==='Open').map(i=>({x,i}))),
      dayworks=(db.dayworks||[]).filter(d=>d.siteId===sid&&d.date>=start&&d.date<=end),
      leave=(db.leaveRequests||[]).filter(r=>r.siteId===sid&&r.status==='Approved'&&r.to>=start&&r.from<=end),
      late=items.filter(x=>contractorProgrammeState(x)?.cls==='late');
  return `<div class="topbar"><div><h1>Weekly Site Report</h1><div class="sub">Built automatically from records already entered in SiteSight</div></div><div class="actions"><select class="select" onchange="site=this.value;render()">${activeSites().map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${contractorEsc(s.name)}</option>`).join('')}</select><input class="input" type="date" value="${start}" onchange="contractorReportWeek=contractorWeekStart(this.value);render()"><button class="btn green" onclick="contractorPrintWeekly()">Print / Save PDF</button></div></div>
    <div class="weekly-report panel"><div class="weekly-report-hero"><div><span>WEEK ENDING</span><b>${end}</b><small>${contractorEsc(siteName(sid))}</small></div><div><strong>${updated.length}</strong><span>Work Items updated</span></div><div><strong>${openIssues.length}</strong><span>Open Issues</span></div><div><strong>${late.length}</strong><span>Behind programme</span></div><div><strong>${dayworks.length}</strong><span>Dayworks</span></div></div>
    <section><h2>Progress</h2>${updated.map(x=>`<div class="weekly-row"><b>${contractorEsc(x.ref)}</b><span>${overall(x)} · ${contractorEsc(gangName(x.gang))}</span></div>`).join('')||'<p class="muted">No Work Item updates recorded in this week.</p>'}</section>
    <section><h2>People / Leave</h2>${leave.map(r=>`<div class="weekly-row"><b>${contractorEsc(contractorMemberName(r.workerId))}</b><span>${contractorEsc(r.type)} · ${r.from} → ${r.to}</span></div>`).join('')||'<p class="muted">No approved leave overlaps this week.</p>'}</section>
    <section><h2>Issues / Programme</h2>${openIssues.slice(0,12).map(r=>`<div class="weekly-row"><b>${contractorEsc(r.x.ref)} · ${contractorEsc(r.i.title)}</b><span>${r.i.blockingWork?'BLOCKING · ':''}${contractorEsc(r.i.priority||'Normal')} ${r.i.targetDate?'· due '+r.i.targetDate:''}</span></div>`).join('')||'<p class="muted">No open issues.</p>'}</section>
    <section><h2>Dayworks</h2>${dayworks.map(d=>`<div class="weekly-row"><b>${contractorEsc(d.area||'Daywork')}</b><span>${contractorEsc(d.work)} · ${d.date}</span></div>`).join('')||'<p class="muted">No Dayworks recorded.</p>'}</section></div>`
}
function contractorPrintWeekly(){window.print()}

// ---- Attendance page enhanced with People / Leave + Pay Review -------
const v167BaseAttendancePage=attendancePage;
attendancePage=function(){
  if(!hasPerm('view_attendance')&&!hasPerm('clock_attendance'))return `<div class="panel empty-help"><b>You do not have Time & Attendance access.</b></div>`;
  let sid=attendanceDefaultSiteId(),full=attendanceCanViewAll();
  if(!full)return v167BaseAttendancePage();
  let tabs=`<div class="att-tabs"><button class="${contractorAttendanceTab==='today'?'active':''}" onclick="contractorAttendanceTab='today';render()">Today</button><button class="${contractorAttendanceTab==='timesheets'?'active':''}" onclick="contractorAttendanceTab='timesheets';render()">Timesheets</button><button class="${contractorAttendanceTab==='leave'?'active':''}" onclick="contractorAttendanceTab='leave';render()">Holiday / Leave</button>${hasPerm('view_financials')?`<button class="${contractorAttendanceTab==='pay'?'active':''}" onclick="contractorAttendanceTab='pay';render()">Pay Review</button>`:''}</div>`;
  let body=contractorAttendanceTab==='timesheets'?attendanceTimesheetView(sid):contractorAttendanceTab==='leave'?contractorLeavePage(sid):contractorAttendanceTab==='pay'?contractorPayReview(sid):attendanceTodayView(sid);
  return `<div class="topbar"><div><h1>People & Time</h1><div class="sub">Who is here, who is away and what the time records indicate</div></div><div class="actions"><select class="select" onchange="attendanceSetSite(this.value)">${activeSites().map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${contractorEsc(s.name)}</option>`).join('')}</select><select class="select" onchange="attendanceTeam=this.value;attendanceWorker='';render()"><option value="all">All Teams</option>${db.gangs.map(g=>`<option value="${g.id}" ${attendanceTeam===g.id?'selected':''}>${contractorEsc(g.name)}</option>`).join('')}</select></div></div>${tabs}${body}`
}

// ---- Dashboard --------------------------------------------------------
const v167BaseDashboard=dashboard;
dashboard=function(){return contractorPeopleTodayStrip()+v167BaseDashboard()}

// ---- More / render routing -------------------------------------------
const v167BaseMore=morePage;
morePage=function(){
  let html=v167BaseMore(),extra='';
  if(hasPerm('work_items'))extra+=`<div class="card more-card" onclick="goNav('dayworks')"><h3>＋ Dayworks</h3><p class="muted">Instructed extra work and labour evidence</p></div>`;
  if(hasPerm('download_reports'))extra+=`<div class="card more-card" onclick="goNav('weekly-report')"><h3>▤ Weekly Site Report</h3><p class="muted">Automatic weekly management summary</p></div>`;
  let pos=html.lastIndexOf('</div>');return pos>=0?html.slice(0,pos)+extra+html.slice(pos):html+extra
}
const v167BaseRender=render;
render=function(){
  if(!db.users?.length||!currentUser())return v167BaseRender();
  if(selected||selectedGang||!['dayworks','weekly-report'].includes(view))return v167BaseRender();
  role=currentUser().role;
  let c=view==='dayworks'?contractorDayworksPage():contractorWeeklyReportPage();
  shell(c);updateSyncUI()
}

// ---- Site stage template button inside Admin Site cards ---------------
const v167BaseAdmin=adminPage;
adminPage=function(){
  let html=v167BaseAdmin();
  if(adminTab==='sites'&&hasPerm('manage_sites')){
    db.sites.filter(s=>!s.archived&&canAccessSite(s.id)).forEach(s=>{
      let needle=`<button class="btn`;
      let cardNeedle=`onclick="editSite('${s.id}')">`;
      // Add a small stage-template action next to existing site actions.
      let exact=`${hasPerm('archive_sites')?`<button class="btn light smallbtn" onclick="event.stopPropagation();archiveSite('${s.id}')">Archive site</button>`:''}`;
      if(exact)html=html.replace(exact,`<button class="btn light smallbtn" onclick="event.stopPropagation();contractorEditStageTemplate('${s.id}')">Stage template</button>`+exact)
    })
  }
  return html
}

// ---- Team lists: remove photo-gallery emphasis ------------------------
const v167BaseGangs=gangsPage;
gangsPage=function(){return v167BaseGangs().replace(/ · \d+ photo\(s\)/g,'').replace(/ · \d+ photos/g,'')}
const v167BaseGangDash=gangDashboard;
gangDashboard=function(g){return v167BaseGangDash(g).replace(/ · \d+ photo\(s\)/g,'').replace(/ · \d+ photos/g,'')}

// ---- Qualification / planning / blocking attention -------------------
const v167BaseAttention=userAttentionText;
userAttentionText=function(x){
  let base=v167BaseAttention(x),parts=base?[base]:[],p=contractorProgrammeState(x);
  if(p?.cls==='late')parts.push(`Behind programme · target ${x.targetComplete}`);
  let blocking=(x.issues||[]).filter(i=>i.status==='Open'&&i.blockingWork);
  if(blocking.length)parts.push(`${blocking.length} blocking issue${blocking.length===1?'':'s'}`);
  return parts.join(' · ')
}

/* ===== v168-assist.js ===== */
// ============================================================
// SiteSight V1.68 — SiteSight Assist / TaskBot
// Context-aware help + controlled task automation.
// Important actions are prepared first and require explicit confirmation.
// ============================================================
let assistOpen=false,assistBusy=false,assistMessages=[],assistPending=null,assistLastIntent='';

function assistEsc(v){return String(v??'').replace(/[&<>\"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',"'":'&#039;'}[c]))}
function assistPageLabel(){if(selected){let x=db.items.find(x=>x.id===selected);return x?`${x.ref} · Work Item`:'Work Item'}return ({dashboard:'Dashboard','work-items':'Work Items',issues:'Issues',gangs:'Teams',attendance:'People & Time','site-drawings':'Drawings',calendar:'Calendar',diary:'Site Diary','payments-history':'Payments',dayworks:'Dayworks','weekly-report':'Weekly Site Report',analytics:'Analytics',audit:'Audit Trail',admin:'Admin',more:'More',guide:'New User Guide',about:'About SiteSight'})[view]||String(view||'SiteSight')}
function assistContext(){let x=selected?db.items.find(x=>x.id===selected):null,u=currentUser();return {page:assistPageLabel(),view,selectedRef:x?.ref||'',selectedType:x?.type||'',site:site==='all'?'All Sites':siteName(site),role:u?.role||'',permissions:Object.entries(u?.permissions||{}).filter(([,v])=>v).map(([k])=>k).slice(0,40)}}
function assistLog(intent,mode='help'){try{db.assistUsage=Array.isArray(db.assistUsage)?db.assistUsage:[];db.assistUsage.unshift({id:'as-'+Date.now()+'-'+Math.random().toString(16).slice(2),createdAt:new Date().toISOString(),userId:currentUser()?.id||'',role:currentUser()?.role||'',view,selectedRef:selected?db.items.find(x=>x.id===selected)?.ref||'':'',intent,mode});db.assistUsage=db.assistUsage.slice(0,500);save()}catch(e){}}
function assistSay(text,actions=[]){assistMessages.push({who:'bot',text,actions});assistMessages=assistMessages.slice(-12);assistBusy=false;assistRenderPanel()}
function assistUser(text){assistMessages.push({who:'user',text});assistMessages=assistMessages.slice(-12)}
function assistOpenPanel(prefill=''){assistOpen=true;if(!assistMessages.length)assistMessages=[{who:'bot',text:`Hi — I’m SiteSight Assist. Tell me what you’re trying to do on ${assistPageLabel()}. I can explain it, take you there, highlight the right control, or prepare supported tasks for you.`}];assistMount();if(prefill){setTimeout(()=>{let i=document.getElementById('assist-input');if(i){i.value=prefill;i.focus()}},20)}}
function assistClose(){assistOpen=false;assistPending=null;document.getElementById('sitesight-assist-panel')?.remove();document.getElementById('sitesight-assist-scrim')?.remove()}
function assistMount(){if(!currentUser())return;let old=document.getElementById('sitesight-assist-fab');if(!old){let b=document.createElement('button');b.id='sitesight-assist-fab';b.className='assist-fab';b.innerHTML='<span>?</span><b>Need help?</b>';b.onclick=()=>assistOpenPanel();document.body.appendChild(b)}if(assistOpen)assistRenderPanel()}
function assistRenderPanel(){if(!assistOpen)return;let old=document.getElementById('sitesight-assist-panel');if(old)old.remove();let scrim=document.getElementById('sitesight-assist-scrim');if(!scrim){scrim=document.createElement('div');scrim.id='sitesight-assist-scrim';scrim.className='assist-scrim';scrim.onclick=assistClose;document.body.appendChild(scrim)}let panel=document.createElement('section');panel.id='sitesight-assist-panel';panel.className='assist-panel';panel.innerHTML=`<div class="assist-head"><div><span>SITESIGHT ASSIST</span><b>TaskBot</b><small>${assistEsc(assistPageLabel())} · ${assistEsc(currentUser()?.role||'')}</small></div><button onclick="assistClose()">×</button></div><div class="assist-quick"><button onclick="assistAskPreset('How do I use this page?')">How do I use this page?</button><button onclick="assistAskPreset('What needs attention here?')">What needs attention?</button><button onclick="assistAskPreset('Take me to holiday booking')">Book holiday</button></div><div class="assist-chat">${assistMessages.map(m=>`<div class="assist-msg ${m.who}"><div>${assistEsc(m.text).replace(/\n/g,'<br>')}</div>${(m.actions||[]).length?`<div class="assist-actions">${m.actions.map((a,i)=>`<button class="${a.primary?'primary':''}" onclick="assistRunMessageAction(${assistMessages.indexOf(m)},${i})">${assistEsc(a.label)}</button>`).join('')}</div>`:''}</div>`).join('')}${assistBusy?'<div class="assist-msg bot"><div>Working that out…</div></div>':''}</div><div class="assist-input-row"><button class="assist-mic" onclick="assistVoice()" title="Speak">🎤</button><textarea id="assist-input" rows="2" placeholder="Ask SiteSight… e.g. ‘raise urgent issue on Plot 21 rear scaffold incomplete’" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();assistSubmit()}"></textarea><button class="assist-send" onclick="assistSubmit()">Send</button></div><div class="assist-foot">TaskBot never bypasses your SiteSight permissions. Record-changing tasks require confirmation.</div>`;document.body.appendChild(panel);let chat=panel.querySelector('.assist-chat');if(chat)chat.scrollTop=chat.scrollHeight}
function assistAskPreset(text){let i=document.getElementById('assist-input');if(i){i.value=text;assistSubmit()}}
function assistRunMessageAction(mi,ai){let a=assistMessages[mi]?.actions?.[ai];if(!a)return;if(a.kind==='navigate')return assistNavigate(a.target,a.highlight||'');if(a.kind==='highlight')return assistNavigate(a.target,a.highlight||'');if(a.kind==='pending')return assistConfirmPlan(a.plan);if(a.kind==='open-leave')return assistNavigate('attendance','Book leave');if(a.kind==='open-daywork')return assistNavigate('dayworks','Record Daywork')}

function assistFindItem(text){let norm=String(text||'').toLowerCase().replace(/^0+/,'');let m=norm.match(/(?:plot|work\s*item)\s*#?0*(\d{1,4})/i);if(m){let n=Number(m[1]);return db.items.find(x=>Number(String(x.ref||'').match(/\d+/)?.[0])===n&&String(x.ref||'').toLowerCase().includes('plot'))||null}let q=norm.match(/(?:on|for)\s+([a-z]+\s*\d+)/i)?.[1];return q?db.items.find(x=>String(x.ref||'').toLowerCase()===q.toLowerCase()):null}
function assistFindMember(text){let t=String(text||'').toLowerCase(),all=attendanceAllMembers?attendanceAllMembers():[];let exact=all.find(x=>t.includes(String(x.member.name||'').toLowerCase()));if(exact)return exact;let words=t.split(/\s+/).filter(Boolean);return all.find(x=>words.some(w=>w.length>=3&&String(x.member.name||'').toLowerCase().split(/\s+/).includes(w)))||null}
function assistParseDateToken(t){if(!t)return'';t=String(t).trim().toLowerCase();let base=new Date(),d=new Date();if(t==='today')return iso();if(t==='tomorrow'){d.setDate(d.getDate()+1);return iso(d)}let m=t.match(/^(\d{1,2})[\/\-.](\d{1,2})(?:[\/\-.](\d{2,4}))?$/);if(m){let y=m[3]?Number(m[3]):base.getFullYear();if(y<100)y+=2000;return `${y}-${String(Number(m[2])).padStart(2,'0')}-${String(Number(m[1])).padStart(2,'0')}`}let i=t.match(/^\d{4}-\d{2}-\d{2}$/);if(i)return t;let days=['sunday','monday','tuesday','wednesday','thursday','friday','saturday'];let di=days.indexOf(t.replace(/^next\s+/,''));if(di>=0){let add=(di-base.getDay()+7)%7;if(t.startsWith('next ')||add===0)add+=7;d.setDate(base.getDate()+add);return iso(d)}return''}
function assistDates(text){let tokens=String(text||'').match(/(?:today|tomorrow|next\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)|(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)|\d{1,2}[\/\-.]\d{1,2}(?:[\/\-.]\d{2,4})?|\d{4}-\d{2}-\d{2})/gi)||[];return tokens.map(assistParseDateToken).filter(Boolean)}
function assistNormalizeStatus(text){let t=text.toLowerCase();if(/complete|completed|finish|finished/.test(t))return'Complete';if(/start|started|in progress|progressing/.test(t))return'Started';if(/not started|reset/.test(t))return'Not started';return''}
function assistStageIndex(item,text){if(item.type!=='House Plot')return-1;let t=text.toLowerCase(),ord=t.match(/\b([1-5])(?:st|nd|rd|th)?\s*(?:lift|stage)?\b/);if(ord)return Math.min(item.lifts.length-1,Number(ord[1])-1);let named=(item.lifts||[]).findIndex(l=>t.includes(String(l.name||'').toLowerCase()));if(named>=0)return named;if(/current|next\s+stage|next\s+lift/.test(t)){let i=(item.lifts||[]).findIndex(l=>l.status!=='Complete');return i<0?item.lifts.length-1:i}return-1}

function assistIntent(text){let t=text.trim(),l=t.toLowerCase(),ctx=assistContext();
  // Navigation / help categories.
  let routes=[['holiday|leave','attendance','Book leave'],['clock|timesheet|hours','attendance','Clock In'],['pay review|wage|pay people','attendance','Pay Review'],['daywork','dayworks','Record Daywork'],['drawing|revision|take.?off|highlight','site-drawings','Highlight'],['calendar','calendar','Add Calendar Note'],['diary','diary','Site Diary'],['issue','issues','Raise Issue'],['team|gang','gangs','Teams'],['analytics','analytics','Analytics'],['audit','audit','Audit Trail'],['admin|user|permission','admin','Admin'],['payment','payments-history','Payments'],['work item|plot','work-items','Work Items'],['weekly report|site report','weekly-report','Weekly Site Report']];
  if(/^(go|take me|open|show me)\b/.test(l)){let r=routes.find(([p])=>new RegExp(p).test(l));if(r)return {intent:'navigate',reply:`I can take you straight to ${r[2]}.`,actions:[{label:`Open ${r[2]}`,kind:'navigate',target:r[1],highlight:r[2],primary:true}]}}
  // Automatic holiday booking.
  if(/\b(book|add|request)\b.*\b(holiday|leave)\b|\bholiday\b.*\b(book|add|request)\b/.test(l)){let wm=assistFindMember(t),dates=assistDates(t);if(wm&&dates.length){let from=dates[0],to=dates[1]||dates[0],half=/half\s*day/.test(l),plan={type:'leave',workerId:wm.member.id,siteId:attendanceDefaultSiteId(),from,to,leaveType:'Holiday',dayUnit:half?.5:1};return {intent:'book_leave',reply:`I can prepare ${wm.member.name}'s holiday from ${from}${to!==from?' to '+to:''}${half?' as a half day':''}.`,actions:[{label:'Review & confirm',kind:'pending',plan,primary:true},{label:'Open leave screen',kind:'navigate',target:'attendance',highlight:'Book leave'}]}}return {intent:'book_leave',reply:'I can book holiday for you. Include the person and date range, for example: “Book Liam holiday 31/08/2026 to 04/09/2026”.',actions:[{label:'Open holiday booking',kind:'navigate',target:'attendance',highlight:'Book leave',primary:true}]}}
  // Sick / absent / training record.
  if(/\b(mark|record|put)\b.*\b(sick|absent|training)\b/.test(l)){let wm=assistFindMember(t),dates=assistDates(t),kind=/training/.test(l)?'Training':/sick/.test(l)?'Sick':'Absent';if(wm){let plan={type:'absence',workerId:wm.member.id,siteId:attendanceDefaultSiteId(),date:dates[0]||iso(),absenceType:kind};return {intent:'absence',reply:`I can record ${wm.member.name} as ${kind} on ${plan.date}.`,actions:[{label:'Review & confirm',kind:'pending',plan,primary:true},{label:'Open People & Time',kind:'navigate',target:'attendance',highlight:'Today'}]}}}
  // Issue creation.
  if(/\b(raise|create|add|record)\b.*\bissue\b|\bissue\b.*\b(on|for)\b/.test(l)){let item=assistFindItem(t);if(item){let pri=/urgent/.test(l)?'Urgent':/high/.test(l)?'High':'Normal',title=t.replace(/^.*?issue\s*(?:on|for)?\s*/i,'').replace(new RegExp(item.ref.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),'ig'),'').replace(/^(urgent|high|normal)\s*/i,'').trim().replace(/^[-–—:]+/,'').trim();if(!title)title='Site issue';let dates=assistDates(t),plan={type:'issue',itemId:item.id,title,priority:pri,targetDate:dates[0]||''};return {intent:'raise_issue',reply:`I can raise a ${pri.toLowerCase()} issue on ${item.ref}: “${title}”.${pri==='Urgent'&&!plan.targetDate?' No target date was supplied, so I will flag that in the confirmation.':''}`,actions:[{label:'Review & confirm',kind:'pending',plan,primary:true},{label:`Open ${item.ref}`,kind:'navigate-item',target:item.id}]}}return {intent:'raise_issue',reply:'Tell me which plot/work item the Issue belongs to, for example: “Raise urgent issue on Plot 21 rear scaffold incomplete”.',actions:[{label:'Open Issues',kind:'navigate',target:'issues',highlight:'Issues'}]}}
  // Progress update.
  if(/\b(mark|set|update)\b.*\b(plot|work item)\b.*\b(complete|completed|started|in progress|not started)\b/.test(l)){let item=assistFindItem(t),st=assistNormalizeStatus(t);if(item&&st){let idx=assistStageIndex(item,t);if(item.type==='House Plot'&&idx<0)return {intent:'progress',reply:`I found ${item.ref}. Tell me which lift/stage, for example “Mark ${item.ref} 4th lift complete”, or say “current stage”.`,actions:[{label:`Open ${item.ref}`,kind:'navigate-item',target:item.id}]};let plan={type:'progress',itemId:item.id,status:st,stageIndex:idx};return {intent:'progress',reply:`I can update ${item.ref}${idx>=0?' · '+item.lifts[idx].name:''} to ${st}.`,actions:[{label:'Review & confirm',kind:'pending',plan,primary:true},{label:`Open ${item.ref}`,kind:'navigate-item',target:item.id}]}}}
  // Calendar note.
  if(/\b(add|create|record)\b.*\bcalendar\s*(note|event)?\b/.test(l)){let dates=assistDates(t),date=dates[0]||iso(),title=t.replace(/^.*?calendar\s*(?:note|event)?\s*/i,'').replace(/\b(today|tomorrow|next\s+\w+|\d{1,2}[\/\-.]\d{1,2}(?:[\/\-.]\d{2,4})?|\d{4}-\d{2}-\d{2})\b/ig,'').replace(/^[-–—:]+/,'').trim();if(title){let plan={type:'calendar',siteId:attendanceDefaultSiteId(),date,title};return {intent:'calendar_note',reply:`I can add “${title}” to the Calendar on ${date}.`,actions:[{label:'Review & confirm',kind:'pending',plan,primary:true},{label:'Open Calendar',kind:'navigate',target:'calendar',highlight:'Add Calendar Note'}]}}return {intent:'calendar_note',reply:'Tell me the calendar note title and date, for example: “Add calendar note brick delivery tomorrow”.',actions:[{label:'Open Calendar',kind:'navigate',target:'calendar',highlight:'Add Calendar Note',primary:true}]}}
  // Daywork: take user to proper evidence form rather than guessing critical fields.
  if(/daywork/.test(l))return {intent:'daywork',reply:'Dayworks need instruction evidence, labour hours and usually an instructed-by name, so I won’t guess those fields. I can take you straight to Record Daywork.',actions:[{label:'Record Daywork',kind:'navigate',target:'dayworks',highlight:'Record Daywork',primary:true}]};
  // Payments / signoff / financial approvals: navigation only by design.
  if(/\b(pay|payment|variation approval|sign off|sign-off|approve variation|approve timesheet)\b/.test(l)){let r=routes.find(([p])=>new RegExp(p).test(l))||['payment','payments-history','Payments'];return {intent:'controlled_finance',reply:'For payments, approvals and sign-off I can take you to the correct record, but I leave the final financial/sign-off action on the normal SiteSight screen so you can see the amount and evidence before confirming.',actions:[{label:`Open ${r[2]}`,kind:'navigate',target:r[1],highlight:r[2],primary:true}]}}
  // Generic help by keyword.
  let r=routes.find(([p])=>new RegExp(p).test(l));if(r)return {intent:'help_'+r[1],reply:assistGuide(r[1],ctx),actions:[{label:`Take me to ${r[2]}`,kind:'navigate',target:r[1],highlight:r[2],primary:true},{label:'Show me where',kind:'highlight',target:r[1],highlight:r[2]}]};
  if(/how do i use this page|what can i do here|this page/.test(l))return {intent:'page_help',reply:assistPageHelp(ctx),actions:assistPageActions(ctx.view)};
  if(/what needs attention|attention here/.test(l))return {intent:'attention_help',reply:assistAttentionHelp(ctx),actions:[{label:'Open Needs Attention',kind:'navigate',target:'needs-attention',highlight:'Needs Attention',primary:true}]};
  return null
}
function assistGuide(target,ctx){let g={attendance:'Use People & Time for Clock In/Out, live workforce, timesheets, holiday/leave and Pay Review. Holiday approvals automatically feed the Calendar.',issues:'Issues record problems with priority, ownership, due date, evidence and resolution. Mark an Issue Blocking Work when it is stopping progress.',calendar:'Calendar combines notes, Issue deadlines, completions, Diary activity and approved leave. Use Add Calendar Note for deliveries, scaffold moves, inspections or meetings.',dayworks:'Dayworks record instructed extra work with date, area, work, instructed by, instruction reference, labour hours and optional Variation creation.','site-drawings':'Drawings holds reusable master drawings, revisions, Take-off and visual Highlights. Open a current drawing and use Highlight to drag a coloured box over an important area, with an optional short note. Current/Superseded status helps prevent use of old information.',diary:'Site Diary is the dated operational record. SiteSight can prefill activity already recorded so the supervisor only adds what is missing.','payments-history':'Payments shows recorded payment history. Pay Review under People & Time combines set-price amounts due with Hourly/Day Rate indications for authorised users.','work-items':'Work Items are the main plot/work-package records: progress, Team, Issues, Payments, Variations, Drawings, photos and notes.',gangs:'Teams shows gangs and people, attendance/pay settings, qualifications and operational member records.',analytics:'Analytics is view-only management information built from SiteSight records.',audit:'Audit Trail lets authorised users search who changed what and when.',admin:'Admin controls Sites, Teams, Users, permissions and Work Item setup.','weekly-report':'Weekly Site Report automatically brings together progress, people/leave, issues/programme and Dayworks.'};return g[target]||`You are on ${ctx.page}. Ask me what you want to achieve and I’ll give the shortest route.`}
function assistPageHelp(ctx){return ({dashboard:'Dashboard is your management starting point: Director Daily Brief, Needs Attention, Today, People Today and work status. Use it to decide what needs action first.','work-items':'Search a plot/work item, open it, then use Quick Update for the fastest progress/photo/Issue workflow.',issues:'This page prioritises open, overdue, urgent and blocking Issues. Open one to see ownership, target date, evidence and resolution.',attendance:'People & Time shows who is on site, timesheets, holiday/leave and Pay Review. Clocking remains simply Clock In / Clock Out.',calendar:'Calendar shows site notes, issue deadlines, completions, Diary activity and approved holiday/leave.',dayworks:'Record instructed extra work here. Keep instruction reference and instructed-by details with the labour evidence.',admin:'Admin is for setup and security. Most day-to-day users should rarely need it.'})[ctx.view]||`This is ${ctx.page}. Tell me the task you want to complete and I’ll explain it or take you to the correct control.`}
function assistPageActions(v){let map={dashboard:[['Work Items','work-items'],['Issues','issues']], 'work-items':[['Search Work Items','work-items']],issues:[['Issues','issues']],attendance:[['Holiday / Leave','attendance'],['Pay Review','attendance']],calendar:[['Add Calendar Note','calendar']],dayworks:[['Record Daywork','dayworks']],admin:[['Admin','admin']]};return (map[v]||[]).map(([label,target],i)=>({label,kind:'navigate',target,highlight:label,primary:i===0}))}
function assistAttentionHelp(ctx){let count=0;try{count=scopeItems().filter(x=>userAttentionText(x)).length}catch(e){}return `Needs Attention is SiteSight’s prioritised exception list. ${count?`There are currently ${count} Work Items with attention information in this view.`:'Open it to see overdue Issues, sign-off, programme and other exceptions relevant to your permissions.'}`}

async function assistSubmit(){let el=document.getElementById('assist-input'),text=(el?.value||'').trim();if(!text||assistBusy)return;if(el)el.value='';assistUser(text);assistBusy=true;assistRenderPanel();let plan=assistIntent(text);if(plan){assistLastIntent=plan.intent;assistLog(plan.intent,plan.actions?.some(a=>a.kind==='pending')?'task':'help');return assistSay(plan.reply,plan.actions||[])}assistLog('fallback','help');assistSay('I can still help offline. Try asking about Work Items, Issues, holiday, clocking, Dayworks, Drawings, Calendar, Site Diary, Payments, Variations, Teams, Analytics, Audit or Admin. You can also say “Take me to …”.',[{label:'Open New User Guide',kind:'navigate',target:'guide',highlight:'New User Guide',primary:true}])}
async function assistRemote(){return ''}
function assistVoice(){let SR=window.SpeechRecognition||window.webkitSpeechRecognition;if(!SR)return toast('Voice input is not supported by this browser');let r=new SR();r.lang='en-GB';r.interimResults=false;r.maxAlternatives=1;r.onresult=e=>{let t=e.results?.[0]?.[0]?.transcript||'',i=document.getElementById('assist-input');if(i){i.value=t;assistSubmit()}};r.onerror=()=>toast('Voice input could not start');r.start()}

function assistNavigate(target,highlight=''){assistClose();if(target==='attendance'){contractorAttendanceTab=highlight.toLowerCase().includes('pay')?'pay':highlight.toLowerCase().includes('holiday')||highlight.toLowerCase().includes('leave')?'leave':'today'}goNav(target);if(highlight)setTimeout(()=>assistHighlight(highlight),160)}
function assistNavigateItem(id){let item=(db.items||[]).find(row=>row.id===id);if(!canViewItem(item))return toast('This work record is outside your access');assistClose();selected=id;view='work-items';render();setTimeout(()=>assistHighlight('Quick update'),160)}
function assistHighlight(label){let target=[...document.querySelectorAll('button,a,[role="button"]')].find(el=>el.offsetParent!==null&&String(el.textContent||'').trim().toLowerCase().includes(String(label||'').toLowerCase()));if(!target&&label==='Book leave')target=[...document.querySelectorAll('button')].find(el=>/book leave/i.test(el.textContent||''));if(!target)return toast('You are on the right page — look for '+label);target.classList.add('assist-highlight');target.scrollIntoView({behavior:'smooth',block:'center'});setTimeout(()=>target.classList.remove('assist-highlight'),4500)}

function assistConfirmPlan(plan){assistPending=plan;let summary=assistPlanSummary(plan);modal=`<div class="modal"><div class="modalbox assist-confirm"><span class="att-kicker">TASKBOT — REVIEW</span><h2>Confirm task</h2><p>${assistEsc(summary)}</p><div class="assist-confirm-rule">SiteSight will use your current account and permissions, record the change normally and add it to the Audit/operational history where applicable.</div><button class="btn orange" onclick="assistExecutePending()">Confirm & do it</button> <button class="btn light" onclick="modal=null;assistPending=null;render();assistOpenPanel()">Cancel</button></div></div>`;assistClose();render()}
function assistPlanSummary(p){if(p.type==='leave')return `Book ${contractorMemberName(p.workerId)} ${p.leaveType} from ${p.from} to ${p.to}${Number(p.dayUnit)===.5?' (half day)':''}.`;if(p.type==='absence')return `Record ${contractorMemberName(p.workerId)} as ${p.absenceType} on ${p.date}.`;if(p.type==='issue'){let x=db.items.find(x=>x.id===p.itemId);return `Raise ${p.priority} Issue on ${x?.ref||'Work Item'}: ${p.title}${p.targetDate?' · target '+p.targetDate:''}.`}if(p.type==='progress'){let x=db.items.find(x=>x.id===p.itemId);return `Update ${x?.ref||'Work Item'}${p.stageIndex>=0?' · '+x?.lifts?.[p.stageIndex]?.name:''} to ${p.status}.`}if(p.type==='calendar')return `Add Calendar Note “${p.title}” on ${p.date} at ${siteName(p.siteId)}.`;return'Complete this SiteSight task.'}
function assistExecutePending(){let p=assistPending;assistPending=null;if(!p)return;if(p.type==='leave')return assistDoLeave(p);if(p.type==='absence')return assistDoAbsence(p);if(p.type==='issue')return assistDoIssue(p);if(p.type==='progress')return assistDoProgress(p);if(p.type==='calendar')return assistDoCalendar(p)}
function assistDone(text){modal=null;savedState('TaskBot completed task');render();setTimeout(()=>assistOpenPanel(),80);setTimeout(()=>assistSay(text),100)}
function assistDoLeave(p){if(!contractorCanViewLeave())return toast('You do not have leave access');let wm=contractorMember(p.workerId);if(!wm)return toast('Team member not found');let linked=attendanceLinkedMember(),self=linked?.member?.id===p.workerId,canApprove=contractorCanApproveLeave()&&!self,r={id:contractorId('leave'),workerId:p.workerId,siteId:p.siteId,from:p.from,to:p.to,type:p.leaveType||'Holiday',dayUnit:Number(p.dayUnit||1),note:'Created by SiteSight TaskBot',status:canApprove?'Approved':'Pending',requestedBy:currentUser()?.name||'',requestedById:currentUser()?.id||'',requestedAt:new Date().toISOString(),approvedBy:canApprove?(currentUser()?.name||''):'',approvedById:canApprove?(currentUser()?.id||''):'',approvedAt:canApprove?new Date().toISOString():''};db.leaveRequests.unshift(r);save();audit('taskbot','leave',r.id,`${contractorMemberName(p.workerId)}: ${r.type} ${r.from} → ${r.to} — ${r.status} · created by TaskBot`,r.siteId);assistDone(`Done. ${contractorMemberName(p.workerId)}'s holiday has been ${r.status==='Approved'?'booked and approved':'submitted for approval'}. It will appear on the Calendar once approved.`)}
function assistDoAbsence(p){if(!hasPerm('manage_attendance')&&!canDirector())return toast('Attendance management permission is required');let wm=contractorMember(p.workerId);if(!wm)return toast('Team member not found');db.attendanceAbsences=db.attendanceAbsences||[];let r={id:contractorId('abs'),workerId:p.workerId,gangId:wm.gang.id,siteId:p.siteId,date:p.date,type:p.absenceType,note:'Created by SiteSight TaskBot',createdAt:new Date().toISOString(),by:currentUser()?.name||''};db.attendanceAbsences.push(r);save();audit('taskbot','attendance absence',r.id,`${wm.member.name}: ${p.absenceType} recorded for ${p.date} · TaskBot`,p.siteId);assistDone(`Done. ${wm.member.name} is recorded as ${p.absenceType} on ${p.date}.`)}
function assistDoIssue(p){if(!hasPerm('raise_issues'))return toast('You do not have permission to raise Issues');let x=db.items.find(x=>x.id===p.itemId);if(!x)return toast('Work Item not found');let now=new Date().toISOString(),i={id:'i'+Date.now(),title:p.title,details:'Created through SiteSight TaskBot.',notes:'',status:'Open',created:iso(),createdAt:now,createdBy:currentUser()?.name||'',updatedAt:now,updatedBy:currentUser()?.name||'',resolved:'',resolvedBy:'',assignedTo:'',priority:p.priority||'Normal',targetDate:p.targetDate||'',photos:[],blockingWork:/blocking|stopping work|stop work/i.test(p.title)};x.issues.push(i);touch(x);save();emitNotification('issue',x.site,`${x.ref}: new ${i.priority.toLowerCase()} issue — ${i.title}`,x.id);audit('taskbot','issue',i.id,`${x.ref}: Issue created by TaskBot — ${i.title}`,x.site);assistDone(`Done. I raised the ${i.priority.toLowerCase()} Issue on ${x.ref}.`)}
function assistDoProgress(p){if(!hasPerm('update_progress')&&!hasPerm('manage_workitems'))return toast('You do not have permission to update progress');let x=db.items.find(x=>x.id===p.itemId);if(!x)return toast('Work Item not found');let desc=p.status;if(x.type==='House Plot'){let l=x.lifts?.[Number(p.stageIndex)];if(!l)return toast('Stage not found');l.status=p.status;if(p.status==='Complete'&&!l.completedDate)l.completedDate=iso();desc=`${l.name} — ${p.status}`}else{x.status=p.status;if(p.status==='Complete'&&!x.completedDate)x.completedDate=iso()}touch(x);save();audit('taskbot','work item',x.id,`${x.ref}: progress updated by TaskBot — ${desc}`,x.site);emitNotification('work',x.site,`${x.ref}: progress updated — ${desc}`,x.id);assistDone(`Done. ${x.ref} is now ${desc}.`)}
function assistDoCalendar(p){if(!hasPerm('manage_calendar'))return toast('You do not have permission to manage Calendar');let r={id:'cn'+Date.now(),date:p.date,siteId:p.siteId,title:p.title,notes:'Created through SiteSight TaskBot.',createdBy:currentUser()?.id||'',createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()};db.calendarNotes.push(r);calendarMonth=p.date.slice(0,7)+'-01';save();emitNotification('site',p.siteId,`Calendar note added — ${p.title}`);audit('taskbot','calendar note',r.id,`Calendar note created by TaskBot — ${p.title}`,p.siteId);assistDone(`Done. “${p.title}” has been added to the Calendar for ${p.date}.`)}

// Handle item action buttons generated in chat.
const v168BaseAction=assistRunMessageAction;assistRunMessageAction=function(mi,ai){let a=assistMessages[mi]?.actions?.[ai];if(a?.kind==='navigate-item')return assistNavigateItem(a.target);return v168BaseAction(mi,ai)}

// Mount after every normal SiteSight render without adding a navigation module.
const v168BaseRender=render;render=function(){v168BaseRender();setTimeout(assistMount,0)};
setTimeout(assistMount,0);

/* ===== v169-workforce.js ===== */
// ============================================================
// SiteSight V1.69 — Workforce Intelligence & Trial Refinements
// Local TaskBot only. No external AI/API dependency.
// ============================================================

let v169AttendanceRange='30',v169AttendanceFrom='',v169AttendanceTo='',v169ShowSupersededDrawings=false;

function v169Tz(siteId=''){
  let s=(db.sites||[]).find(x=>x.id===siteId);
  return s?.timeZone||'Europe/London'
}
function v169DateKey(value=new Date(),siteId=''){
  let d=value instanceof Date?value:new Date(value);
  if(Number.isNaN(d.getTime()))return String(value||'').slice(0,10);
  let parts=new Intl.DateTimeFormat('en-GB',{timeZone:v169Tz(siteId),year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(d),
      get=t=>parts.find(p=>p.type===t)?.value||'';
  return `${get('year')}-${get('month')}-${get('day')}`
}
function v169TimeMinutes(value,siteId=''){
  let d=value instanceof Date?value:new Date(value);if(Number.isNaN(d.getTime()))return 0;
  let parts=new Intl.DateTimeFormat('en-GB',{timeZone:v169Tz(siteId),hour:'2-digit',minute:'2-digit',hourCycle:'h23'}).formatToParts(d),
      h=Number(parts.find(p=>p.type==='hour')?.value||0),m=Number(parts.find(p=>p.type==='minute')?.value||0);
  return h*60+m
}
function v169TimeLabel(value,siteId=''){
  let d=value instanceof Date?value:new Date(value);if(Number.isNaN(d.getTime()))return '—';
  return new Intl.DateTimeFormat('en-GB',{timeZone:v169Tz(siteId),hour:'2-digit',minute:'2-digit',hourCycle:'h23'}).format(d)
}
function v169ParseTime(v,def){
  let m=String(v||def||'').match(/^(\d{1,2}):(\d{2})$/);return m?Number(m[1])*60+Number(m[2]):0
}
function v169MinutesLabel(v){
  v=Math.round(Number(v||0));if(v<60)return `${v}m`;let h=Math.floor(v/60),m=v%60;return `${h}h${m?` ${m}m`:''}`
}
function v169SiteIdsForAnalytics(){return site==='all'?activeSites().map(s=>s.id):[site].filter(Boolean)}
function v169MemberActiveOnDate(m,date){
  if(!m||m.activeForAttendance===false||m.employmentStatus==='Leaver'||m.employmentStatus==='Inactive')return false;
  if(m.startDate&&date<m.startDate)return false;if(m.leavingDate&&date>m.leavingDate)return false;
  let dow=new Date(date+'T12:00:00').getDay(),days=(m.usualDays&&m.usualDays.length?m.usualDays:[1,2,3,4,5]).map(Number);
  return days.includes(dow)
}
function v169ShiftSchedule(m,siteId){
  let c=attendanceSiteConfig(siteId)||{};
  return {start:v169ParseTime(m.expectedStartTime||c.expectedStartTime||'07:30','07:30'),
          end:v169ParseTime(m.expectedEndTime||c.expectedEndTime||'16:30','16:30'),
          grace:Number(c.graceMinutes??5)}
}
function v169ShiftPunctuality(shift,m){
  if(!shift?.clockIn?.time)return {late:0,early:0,onTime:false};
  let sc=v169ShiftSchedule(m,shift.siteId),start=v169TimeMinutes(shift.clockIn.time,shift.siteId),late=Math.max(0,start-sc.start);
  let early=0;if(shift.clockOut?.time){let out=v169TimeMinutes(shift.clockOut.time,shift.siteId);early=Math.max(0,sc.end-out)}
  let lateFlag=late>sc.grace,earlyFlag=early>sc.grace;
  return {late:lateFlag?late:0,early:earlyFlag?early:0,onTime:!lateFlag&&!earlyFlag,startMinutes:start,endMinutes:shift.clockOut?.time?v169TimeMinutes(shift.clockOut.time,shift.siteId):null}
}

// UK/site business date — exact timestamps remain UTC.
iso=function(d=new Date()){let sid=(typeof site!=='undefined'&&site!=='all')?site:'';return v169DateKey(d,sid)}
attendanceDateOnly=function(v,siteId=''){return v169DateKey(v,siteId)}
contractorToday=function(){return iso()}

// ---- Migration -------------------------------------------------------
const v169BaseMigrate=migrate;
migrate=function(persist=true){
  v169BaseMigrate(false);
  db.dayRateFullOverrides=Array.isArray(db.dayRateFullOverrides)?db.dayRateFullOverrides:[];
  db.sites=(db.sites||[]).map(s=>{
    let c=s.attendanceConfig||{};
    return {...s,timeZone:s.timeZone||'Europe/London',attendanceConfig:{...c,
      expectedStartTime:c.expectedStartTime||'07:30',
      expectedEndTime:c.expectedEndTime||'16:30',
      graceMinutes:Number(c.graceMinutes??5),
      analyticsManualMinutes:Number(c.analyticsManualMinutes??8),
      analyticsSystemMinutes:Number(c.analyticsSystemMinutes??1),
      analyticsAdminHourlyCost:Number(c.analyticsAdminHourlyCost??25)
    }}
  });
  db.gangs=(db.gangs||[]).map(g=>({...g,members:(g.members||[]).map(m=>({...m,
    expectedStartTime:m.expectedStartTime||'',expectedEndTime:m.expectedEndTime||''
  }))}));
  db.calendarNotes=(db.calendarNotes||[]).map(n=>({...n,category:n.category||'General'}));
  db.siteDrawings=(db.siteDrawings||[]).map(d=>({...d,drawingNumber:d.drawingNumber||'',revision:d.revision||'',issuedDate:d.issuedDate||d.uploadedDate||'',revisionStatus:d.revisionStatus||'Current',supersededBy:d.supersededBy||''}));
  db.items=(db.items||[]).map(x=>({...x,drawings:(x.drawings||[]).map(d=>({...d,drawingNumber:d.drawingNumber||'',revision:d.revision||'',issuedDate:d.issuedDate||d.uploadedDate||'',revisionStatus:d.revisionStatus||'Current',supersededBy:d.supersededBy||''}))}));
  if(persist)save()
}

// ---- Local-only SiteSight Assist ------------------------------------
assistSubmit=async function(){
  let el=document.getElementById('assist-input'),text=(el?.value||'').trim();if(!text||assistBusy)return;
  if(el)el.value='';assistUser(text);assistBusy=true;assistRenderPanel();
  let plan=assistIntent(text);
  if(plan){assistLastIntent=plan.intent;assistLog(plan.intent,plan.actions?.some(a=>a.kind==='pending')?'task':'help');return assistSay(plan.reply,plan.actions||[])}
  assistLog('fallback','help');
  assistSay('I can help with SiteSight tasks stored in the built-in guide. Try asking about Work Items, Issues, holiday, clocking, Dayworks, Drawings, Calendar, Site Diary, Payments, Variations, Teams, Analytics, Audit or Admin. You can also say “Take me to …”.',[{label:'Open New User Guide',kind:'navigate',target:'guide',highlight:'New User Guide',primary:true}])
}
assistRemote=async function(){return ''}

// ---- Location: several fresh GPS samples, best accuracy wins --------
attendanceGetPosition=function(){
  return new Promise((resolve,reject)=>{
    if(!attendanceSecureContextOkay())return reject(new Error('Location verification requires the HTTPS SiteSight address (or localhost for desktop testing).'));
    if(!navigator.geolocation)return reject(new Error('This device does not provide browser location services.'));
    let samples=[],done=false,watch=null,timer=null;
    const finish=(err)=>{
      if(done)return;done=true;if(watch!==null)navigator.geolocation.clearWatch(watch);if(timer)clearTimeout(timer);
      if(samples.length){
        samples.sort((a,b)=>a.accuracy-b.accuracy);let best=samples[0];
        return resolve({...best,sampleCount:samples.length})
      }
      reject(err||new Error('Your phone could not establish a reliable GPS position. Move outdoors and try again.'))
    };
    const onErr=e=>{if(e.code===1)finish(new Error('Location permission was denied. Allow precise location for SiteSight and try again.'))};
    watch=navigator.geolocation.watchPosition(p=>{
      samples.push({latitude:p.coords.latitude,longitude:p.coords.longitude,accuracy:p.coords.accuracy,positionTimestamp:p.timestamp||Date.now()});
      if(samples.length>=3||p.coords.accuracy<=12)finish()
    },onErr,{enableHighAccuracy:true,timeout:15000,maximumAge:0});
    timer=setTimeout(()=>finish(new Error('Location timed out. Move somewhere with a clearer GPS signal and try again.')),12000)
  })
}
attendanceDoClock=async function(action){
  let linked=attendanceLinkedMember();if(!linked)return;let active=attendanceActiveShift(linked.member.id),
      sid=action==='clock-out'?active?.siteId:(document.getElementById('att-clock-site')?.value||attendanceDefaultSiteId());
  if(!sid)return toast('Select a Site');if(!attendanceLocationConfigured(sid))return toast('This Site does not have a verified geofence yet. Ask a manager to configure Site Location.');
  if(!sharedMode||!navigator.onLine)return toast('Verified clocking requires an online SiteSight connection for server time and location validation.');
  let btn=document.getElementById('att-clock-action'),prog=document.getElementById('att-clock-progress');
  if(btn){btn.disabled=true;btn.textContent='Checking precise location…'}if(prog)prog.textContent='SiteSight is taking several fresh GPS readings and will use the most accurate one.';
  try{
    if(pendingSave)await syncToServer(true);let pos=await attendanceGetPosition();
    if(prog)prog.textContent=`${pos.sampleCount} GPS reading${pos.sampleCount===1?'':'s'} · best accuracy ±${Math.round(pos.accuracy)}m · validating against SiteSight…`;
    let payload={action,workerId:linked.member.id,siteId:sid,actorId:currentUser()?.id||'',latitude:pos.latitude,longitude:pos.longitude,accuracy:pos.accuracy,
      positionTimestamp:pos.positionTimestamp||Date.now(),sampleCount:pos.sampleCount||1,sessionId:attendanceSessionId(),device:attendanceDeviceInfo()};
    let r=await fetch('/api/attendance/stamp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}),result=await r.json();
    if(result.data){db=result.data;sharedRevision=result.revision||sharedRevision;lastSyncedDB=cloneDB(db);pendingSave=false;syncState='synced';lastSyncAt=new Date().toISOString();migrate(false);persistCache();try{localStorage.setItem(BASE_KEY,JSON.stringify(db));localStorage.removeItem(PENDING_KEY)}catch(e){}}
    modal=null;view='attendance';contractorAttendanceTab='today';attendanceTab='today';render();
    if(result.ok)savedState(action==='clock-in'?'Clocked in — location verified':'Clocked out — location verified');else toast(result.message||'Clock attempt rejected')
  }catch(e){
    if(btn){btn.disabled=false;btn.textContent=action==='clock-in'?'▶ VERIFY LOCATION & CLOCK IN':'■ VERIFY LOCATION & CLOCK OUT'}
    if(prog)prog.textContent=e.message||'Location could not be verified';toast(e.message||'Location could not be verified')
  }
}

// Site location + workday + savings assumptions kept in one compact setup.
configureAttendanceLocation=function(siteId){
  if(!hasPerm('manage_attendance')&&!hasPerm('manage_sites'))return toast('Attendance management permission is required');
  let s=db.sites.find(s=>s.id===siteId),c=attendanceSiteConfig(siteId);if(!s)return;
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">SITE ATTENDANCE RULES</span><h2>${s.name}</h2>
  <p class="muted">Set the geofence while physically on the development. SiteSight checks location only when Clock In / Clock Out is pressed and takes several fresh readings.</p>
  ${c.demoPreset?'<div class="att-demo-warning"><b>Demo preset location</b><span>Replace this with your real Site location before a live mobile test.</span></div>':''}
  <div class="att-config-grid"><div class="field"><label>Allowed radius</label><select id="att-radius" class="select">${[50,75,100,150,200,250,300].map(v=>`<option value="${v}" ${Number(c.radiusM)===v?'selected':''}>${v} metres</option>`).join('')}</select></div>
  <div class="field"><label>Maximum GPS uncertainty</label><select id="att-accuracy" class="select">${[20,30,50,75,100,150].map(v=>`<option value="${v}" ${Number(c.maxAccuracyM)===v?'selected':''}>±${v} metres</option>`).join('')}</select></div>
  <div class="field"><label>Long-shift warning</label><select id="att-maxshift" class="select">${[10,11,12,13,14,16].map(v=>`<option value="${v}" ${Number(c.maxShiftHours)===v?'selected':''}>${v} hours</option>`).join('')}</select></div></div>
  <div class="two"><div class="field"><label>Normal start time</label><input id="att-start-time" class="input" type="time" value="${c.expectedStartTime||'07:30'}"></div><div class="field"><label>Normal finish time</label><input id="att-end-time" class="input" type="time" value="${c.expectedEndTime||'16:30'}"></div></div>
  <div class="field"><label>Late / early grace</label><select id="att-grace" class="select">${[0,3,5,10,15].map(v=>`<option value="${v}" ${Number(c.graceMinutes)===v?'selected':''}>${v} minutes</option>`).join('')}</select></div>
  <details class="v169-savings-settings"><summary>Analytics savings assumptions</summary><div class="three"><div class="field"><label>Manual admin / person / day</label><input id="att-manual-mins" class="input" type="number" min="0" max="60" value="${Number(c.analyticsManualMinutes||8)}"><small>minutes before SiteSight</small></div><div class="field"><label>SiteSight admin / person / day</label><input id="att-system-mins" class="input" type="number" min="0" max="60" value="${Number(c.analyticsSystemMinutes||1)}"><small>minutes now</small></div><div class="field"><label>Admin cost / hour</label><input id="att-admin-cost" class="input" type="number" min="0" step="0.5" value="${Number(c.analyticsAdminHourlyCost||25)}"><small>£ per hour</small></div></div></details>
  <div class="att-coordinate-box">${attendanceLocationConfigured(siteId)?`<b>Current Site Location</b><span>${Number(c.latitude).toFixed(6)}, ${Number(c.longitude).toFixed(6)}</span><small>Set ${c.locationSetAt?new Date(c.locationSetAt).toLocaleString():'—'} by ${c.locationSetBy||'—'}</small>`:'<b>No Site Location stored yet.</b>'}</div>
  <div id="att-config-status" class="att-clock-progress"></div>
  <button class="btn orange" onclick="attendanceSetSiteLocationNow('${siteId}')">⌖ Set Site Location to This Device</button> <button class="btn blue" onclick="attendanceSaveSiteRules('${siteId}')">Save rules</button> <button class="btn light" onclick="modal=null;render()">Close</button>
  <div class="att-privacy-note"><b>Location protection</b><span>Fresh high-accuracy readings · Site geofence · GPS accuracy limit · server timestamp · device/session record · rejected attempts retained. No continuous tracking.</span></div></div></div>`;render()
}
function v169ReadAttendanceRules(siteId){
  let c=attendanceSiteConfig(siteId);
  c.radiusM=Number(document.getElementById('att-radius')?.value||150);c.maxAccuracyM=Number(document.getElementById('att-accuracy')?.value||75);c.maxShiftHours=Number(document.getElementById('att-maxshift')?.value||12);
  c.expectedStartTime=document.getElementById('att-start-time')?.value||'07:30';c.expectedEndTime=document.getElementById('att-end-time')?.value||'16:30';c.graceMinutes=Number(document.getElementById('att-grace')?.value||5);
  c.analyticsManualMinutes=Number(document.getElementById('att-manual-mins')?.value||8);c.analyticsSystemMinutes=Number(document.getElementById('att-system-mins')?.value||1);c.analyticsAdminHourlyCost=Number(document.getElementById('att-admin-cost')?.value||25);
  return c
}
attendanceSetSiteLocationNow=async function(siteId){
  let status=document.getElementById('att-config-status');if(status)status.textContent='Taking several precise GPS readings…';
  try{let pos=await attendanceGetPosition(),c=v169ReadAttendanceRules(siteId);c.latitude=pos.latitude;c.longitude=pos.longitude;c.locationSetAt=new Date().toISOString();c.locationSetBy=currentUser()?.name||'';c.demoPreset=false;save();
    audit('attendance','site',siteId,`Attendance geofence set — ${siteName(siteId)} · ${c.radiusM}m radius · best GPS accuracy ${Math.round(pos.accuracy)}m from ${pos.sampleCount} reading(s)`,siteId);savedState('Site location saved');configureAttendanceLocation(siteId)
  }catch(e){if(status)status.textContent=e.message;toast(e.message)}
}
attendanceSaveSiteRules=function(siteId){let c=v169ReadAttendanceRules(siteId);save();audit('attendance','site',siteId,`Attendance rules updated — ${siteName(siteId)} · ${c.expectedStartTime}–${c.expectedEndTime} · ${c.radiusM}m geofence`,siteId);savedState('Attendance rules saved');configureAttendanceLocation(siteId)}

// ---- People Today ----------------------------------------------------
attendanceExpectedMembers=function(siteId,date=iso()){
  let gangIds=new Set((db.items||[]).filter(x=>x.site===siteId&&!db.sites.find(s=>s.id===x.site)?.archived&&x.gang).map(x=>x.gang));
  return attendanceAllMembers().filter(x=>{
    let activeShift=attendanceActiveShift(x.member.id)?.siteId===siteId;
    return (gangIds.has(x.gang.id)||activeShift)&&v169MemberActiveOnDate(x.member,date)
  })
}
contractorPeopleSummary=function(date=contractorToday(),siteId=contractorSiteForPeople()){
  let expected=attendanceExpectedMembers(siteId,date),leave=contractorCurrentLeaveFor(date,siteId).filter(r=>v169MemberActiveOnDate(contractorMember(r.workerId)?.member,date)),
      abs=contractorAbsencesFor(date,siteId),
      holidayIds=new Set(leave.filter(x=>x.type==='Holiday').map(x=>x.workerId)),
      trainingIds=new Set([...leave.filter(x=>x.type==='Training').map(x=>x.workerId),...abs.filter(x=>x.type==='Training').map(x=>x.workerId)]),
      sickIds=new Set(abs.filter(x=>x.type==='Sick').map(x=>x.workerId)),
      absentIds=new Set(abs.filter(x=>!['Sick','Training'].includes(x.type)).map(x=>x.workerId)),
      active=(db.attendanceShifts||[]).filter(s=>s.siteId===siteId&&!s.voided&&!s.clockOut?.time),
      activeIds=new Set(active.map(s=>s.workerId)),
      notIn=expected.filter(x=>!activeIds.has(x.member.id)&&!holidayIds.has(x.member.id)&&!sickIds.has(x.member.id)&&!trainingIds.has(x.member.id)&&!absentIds.has(x.member.id));
  return {expected,leave,abs,holidayIds,sickIds,trainingIds,absentIds,active,notIn}
}
contractorPeopleTodayStrip=function(){
  if(!contractorCanViewLeave()||!activeSites().length)return '';let sid=contractorSiteForPeople();if(!sid)return '';
  let s=contractorPeopleSummary(contractorToday(),sid);
  return `<div class="people-today-strip v169-people-strip"><div><span>PEOPLE TODAY</span><b>${siteName(sid)}</b><small>${s.expected.length} expected today</small></div>
    <button onclick="goNav('attendance');contractorAttendanceTab='today';render()"><strong>${s.active.length}</strong><small>On site</small></button>
    <button onclick="goNav('attendance');contractorAttendanceTab='leave';render()"><strong>${s.holidayIds.size}</strong><small>Holiday</small></button>
    <button onclick="goNav('attendance');contractorAttendanceTab='leave';render()"><strong>${s.sickIds.size}</strong><small>Sick</small></button>
    <button onclick="goNav('attendance');contractorAttendanceTab='leave';render()"><strong>${s.trainingIds.size}</strong><small>Training</small></button>
    <button onclick="goNav('attendance');contractorAttendanceTab='today';render()" class="${s.notIn.length?'warn':''}"><strong>${s.notIn.length}</strong><small>Expected / missing</small></button></div>`
}
attendanceTodayView=function(siteId){
  let today=iso(),s=contractorPeopleSummary(today,siteId),expected=s.expected,cfg=attendanceSiteConfig(siteId),
      active=s.active.map(sh=>{let wm=attendanceMemberById(sh.workerId);return wm?{...wm,shift:sh}:null}).filter(Boolean),
      warnings=active.filter(x=>attendanceShiftHours(x.shift)>Number(cfg?.maxShiftHours||12)),
      attempts=(db.attendanceAttempts||[]).filter(a=>a.siteId===siteId&&attendanceDateOnly(a.time,a.siteId)===today).sort((a,b)=>String(b.time).localeCompare(String(a.time)));
  let accounted=[...s.holidayIds].length+s.sickIds.size+s.trainingIds.size+s.absentIds.size;
  return `${attendanceTodayCard()}<div class="att-site-config panel"><div><span class="att-kicker">SITE LOCATION</span><h3>${siteName(siteId)}</h3>${attendanceGeoStatusText(siteId)}<small class="v169-shift-rule">Normal day ${cfg.expectedStartTime||'07:30'}–${cfg.expectedEndTime||'16:30'} · ${Number(cfg.graceMinutes||5)}m grace</small></div>${hasPerm('manage_attendance')||hasPerm('manage_sites')?`<button class="btn light" onclick="configureAttendanceLocation('${siteId}')">⌖ Configure Location & Rules</button>`:''}</div>
  <div class="att-metrics v169-people-metrics"><div class="card"><span>ON SITE</span><b>${active.length}</b></div><div class="card"><span>EXPECTED TODAY</span><b>${expected.length}</b></div><div class="card"><span>HOLIDAY</span><b>${s.holidayIds.size}</b></div><div class="card"><span>SICK</span><b>${s.sickIds.size}</b></div><div class="card"><span>TRAINING</span><b>${s.trainingIds.size}</b></div><div class="card ${s.notIn.length?'warn':''}"><span>EXPECTED / MISSING</span><b>${s.notIn.length}</b></div></div>
  <div class="panel"><div class="sectionhead"><div><h2>Who is on site now</h2><div class="muted">Live workforce register · ${siteName(siteId)}</div></div><button class="btn light smallbtn" onclick="printAttendanceRegister('${siteId}')">⇩ Emergency Register / Print</button></div>
  <div class="att-live-list">${active.length?active.map(x=>{let h=attendanceShiftHours(x.shift),warn=h>Number(cfg?.maxShiftHours||12);return `<div class="att-live-row ${warn?'warning':''}"><div><b>${x.member.name}</b><span>${x.gang.name}</span></div><div><b>${v169TimeLabel(x.shift.clockIn.time,siteId)}</b><span>${attendanceFmtHours(h)}${warn?' · ⚠ possible missed clock-out':''}</span></div><div>${attendanceVerificationBadge(x.shift.clockIn)}${x.shift.clockIn?.distanceM!==undefined?`<small>${Math.round(x.shift.clockIn.distanceM)}m from Site · GPS ±${Math.round(x.shift.clockIn.accuracy||0)}m${x.shift.clockIn.sampleCount?` · ${x.shift.clockIn.sampleCount} readings`:''}</small>`:''}</div>${hasPerm('manage_attendance')?`<button class="btn light smallbtn" onclick="supervisorAttendanceClock('${x.member.id}','out','${siteId}')">Supervisor Clock Out</button>`:''}</div>`}).join(''):'<div class="empty-help"><b>No one is currently clocked in.</b><div style="margin-top:6px">Verified and Supervisor Entry clock-ins will appear here immediately.</div></div>'}</div></div>
  ${hasPerm('manage_attendance')?`<div class="panel"><div class="sectionhead"><div><h2>Expected workforce</h2><div class="muted">Only active people normally working today are included.</div></div></div>
  <div class="v169-accounted-grid">${[...s.holidayIds].map(id=>`<span class="holiday"><b>${contractorMemberName(id)}</b> Holiday</span>`).join('')}${[...s.sickIds].map(id=>`<span class="sick"><b>${contractorMemberName(id)}</b> Sick</span>`).join('')}${[...s.trainingIds].map(id=>`<span class="training"><b>${contractorMemberName(id)}</b> Training</span>`).join('')}</div>
  <div class="att-notin-grid">${s.notIn.map(x=>`<div class="att-notin-card"><div><b>${x.member.name}</b><span>${x.gang.name} · ${x.member.role||'Team member'}</span></div><div><button class="btn blue smallbtn" onclick="supervisorAttendanceClock('${x.member.id}','in','${siteId}')">Supervisor Clock In</button><button class="btn light smallbtn" onclick="addAttendanceAbsence('${x.member.id}','${siteId}','${today}')">Mark absence</button></div></div>`).join('')||'<div class="muted">Everyone expected today is accounted for.</div>'}</div></div>`:''}
  ${attempts.length&&attendanceCanViewAll()?`<div class="panel"><div class="sectionhead"><h3>Rejected clock attempts today</h3><span class="badge b-red">${attempts.length}</span></div><div class="att-attempt-list">${attempts.slice(0,8).map(a=>{let wm=attendanceMemberById(a.workerId);return `<div><span><b>${wm?.member.name||'Worker'}</b> · ${a.action==='clock-in'?'Clock In':'Clock Out'} · ${v169TimeLabel(a.time,a.siteId)}</span><strong>${a.reason}</strong><small>${Number.isFinite(Number(a.distanceM))?Math.round(a.distanceM)+'m from Site · ':''}GPS ±${Math.round(a.accuracy||0)}m</small></div>`}).join('')}</div></div>`:''}`
}

// ---- Calendar: people colours + event categories --------------------
function v169CalendarPeopleEvents(date){
  if(!contractorCanViewLeave())return [];let sid=site==='all'?'all':site,out=[];
  contractorCurrentLeaveFor(date,sid).forEach(r=>{
    let m=contractorMember(r.workerId)?.member;if(!v169MemberActiveOnDate(m,date))return;
    let kind=r.type==='Holiday'?'holiday':r.type==='Training'?'training':'leave';
    out.push({kind,title:`${contractorMemberName(r.workerId)} · ${r.type}`,sub:`${contractorTeamName(r.workerId)} · ${siteName(r.siteId)}`,workerId:r.workerId})
  });
  contractorAbsencesFor(date,sid).forEach(a=>{
    let kind=a.type==='Sick'?'sick':a.type==='Training'?'training':'leave';
    out.push({kind,title:`${contractorMemberName(a.workerId)} · ${a.type||'Absent'}`,sub:`${contractorTeamName(a.workerId)} · ${siteName(a.siteId)}`,workerId:a.workerId})
  });return out
}
calendarNoteEvents=function(date){return calendarNotesOnDate(date).map(n=>({kind:n.category&&n.category!=='General'?'siteevent':'note',title:n.title,sub:`${n.category&&n.category!=='General'?n.category+' · ':''}${siteName(n.siteId)}${n.notes?' · '+n.notes:''}`,noteId:n.id,notes:n.notes||''}))}
calendarDayEvents=function(date){return [...calendarIssueDueEvents(date),...calendarNoteEvents(date),...calendarCompletedEvents(date),...calendarDiaryEvents(date),...v169CalendarPeopleEvents(date)]}
calendarEventSummary=function(counts){
  let chips=[];if(counts.issue)chips.push(`<span class="month-chip chip-issue">${counts.issue} issue${counts.issue===1?'':'s'} due</span>`);
  if(counts.note)chips.push(`<span class="month-chip chip-note">${counts.note} note${counts.note===1?'':'s'}</span>`);
  if(counts.siteevent)chips.push(`<span class="month-chip chip-siteevent">${counts.siteevent} site event${counts.siteevent===1?'':'s'}</span>`);
  if(counts.holiday)chips.push(`<span class="month-chip chip-holiday">${counts.holiday} holiday</span>`);
  if(counts.training)chips.push(`<span class="month-chip chip-training">${counts.training} training</span>`);
  if(counts.sick)chips.push(`<span class="month-chip chip-sick">${counts.sick} sick</span>`);
  if(counts.leave)chips.push(`<span class="month-chip chip-leave">${counts.leave} away</span>`);
  if(counts.complete)chips.push(`<span class="month-chip chip-complete">${counts.complete} complete</span>`);
  if(counts.diary)chips.push(`<span class="month-chip chip-diary">${counts.diary} diary</span>`);return chips.join('')
}
fullCalendarMonth=function(){
  let current=calendarMonthDate(),year=current.getFullYear(),month=current.getMonth(),first=new Date(year,month,1),offset=(first.getDay()+6)%7,start=new Date(year,month,1-offset),today=iso(),cells=[];
  for(let i=0;i<42;i++){let d=new Date(start);d.setDate(start.getDate()+i);let ds=iso(d),info=calendarDayCounts(ds),notes=calendarNotesOnDate(ds),inMonth=d.getMonth()===month,
    nonNote={...info.counts,note:0};cells.push(`<button class="month-day ${inMonth?'':'outside'} ${ds===today?'today':''} ${info.events.length?'has-events':''} ${notes.length?'has-calendar-note':''}" onclick="openCalendarDay('${ds}')"><span class="month-day-number">${d.getDate()}</span>${calendarNoteCellPreview(ds)}<div class="month-day-events month-day-other-events">${calendarEventSummary(nonNote)}</div>${info.events.length?`<span class="month-event-count">${info.events.length}</span>`:''}${calendarHoverNotes(ds)}</button>`)}
  return `<div class="panel full-calendar-panel"><div class="full-calendar-head"><div><span class="calendar-kicker">VISUAL CALENDAR</span><h2>${current.toLocaleDateString('en-GB',{month:'long',year:'numeric'})}</h2><div class="muted">Holiday, training, sickness and site events are visible without opening a separate people system.</div></div><div class="calendar-month-actions"><button class="btn light" onclick="shiftCalendarMonth(-1)">← Previous</button><button class="btn light" onclick="goCalendarToday()">Today</button><button class="btn light" onclick="shiftCalendarMonth(1)">Next →</button></div></div>
  <div class="calendar-legend v169-calendar-legend"><span><i class="legend-holiday"></i>Holiday</span><span><i class="legend-training"></i>Training</span><span><i class="legend-sick"></i>Sickness</span><span><i class="legend-siteevent"></i>Site event</span><span><i class="legend-issue"></i>Issue due</span><span><i class="legend-complete"></i>Work completed</span>${hasPerm('view_site_diary')?'<span><i class="legend-diary"></i>Site Diary</span>':''}</div>
  <div class="month-weekdays">${['Mon','Tue','Wed','Thu','Fri','Sat','Sun'].map(d=>`<b>${d}</b>`).join('')}</div><div class="month-grid">${cells.join('')}</div></div>`
}
openCalendarDay=function(date){
  let {events}=calendarDayCounts(date),label=new Date(date+'T12:00:00').toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'}),
      notes=events.filter(e=>['note','siteevent'].includes(e.kind)),people=events.filter(e=>['holiday','training','sick','leave'].includes(e.kind)),other=events.filter(e=>!['note','siteevent','holiday','training','sick','leave'].includes(e.kind));
  let icon=e=>e.kind==='issue'?'⚠':e.kind==='complete'?'✓':e.kind==='holiday'?'◷':e.kind==='training'?'◆':e.kind==='sick'?'＋':e.kind==='siteevent'?'▣':'✎',
      eventRow=e=>`<button class="calendar-day-event event-${e.kind}" onclick="${e.issueId?`modal=null;viewIssue('${e.itemId}','${e.issueId}')`:e.itemId?`modal=null;openItem('${e.itemId}')`:['holiday','training','sick','leave'].includes(e.kind)?`modal=null;contractorAttendanceTab='leave';goNav('attendance')`:`modal=null;goNav('diary')`}"><i>${icon(e)}</i><span><b>${calendarSafe(e.title)}</b><small>${calendarSafe(e.sub||'')}</small></span><strong>›</strong></button>`;
  modal=`<div class="modal"><div class="modalbox calendar-day-modal"><div class="sectionhead"><div><span class="calendar-kicker">DAY VIEW</span><h2>${label}</h2><div class="muted">${events.length} recorded / scheduled event${events.length===1?'':'s'}</div></div><div class="actions">${hasPerm('manage_calendar')?`<button class="btn orange smallbtn" onclick="modal=null;render();setTimeout(()=>addCalendarNoteForDate('${date}'),0)">＋ Add Calendar Note</button>`:''}<button class="btn light" onclick="modal=null;render()">✕ Close</button></div></div>
  ${people.length?`<div class="calendar-day-group"><div class="calendar-day-group-head"><div><b>People</b><span>${people.length}</span></div></div><div class="calendar-day-list">${people.map(eventRow).join('')}</div></div>`:''}
  ${notes.length?`<div class="calendar-day-group calendar-day-note-group"><div class="calendar-day-group-head"><div><b>Site Notes / Events</b><span>${notes.length}</span></div></div><div class="calendar-day-list">${notes.map(e=>`<div class="calendar-day-note-record"><button class="calendar-day-event event-${e.kind}" onclick="openCalendarNote('${e.noteId}')"><i>${icon(e)}</i><span><b>${calendarSafe(e.title)}</b><small>${calendarSafe(e.sub||'')}</small></span><strong>›</strong></button>${hasPerm('manage_calendar')?`<button class="calendar-note-remove" onclick="deleteCalendarNote('${e.noteId}')" title="Remove this calendar note">×</button>`:''}</div>`).join('')}</div></div>`:''}
  ${other.length?`<div class="calendar-day-group"><div class="calendar-day-group-head"><div><b>Site Activity</b><span>${other.length}</span></div></div><div class="calendar-day-list">${other.map(eventRow).join('')}</div></div>`:''}
  ${!events.length?smartEmpty('Nothing recorded for this date.','Add a Calendar Note if something happened or is due to happen.','','','▣'):''}</div></div>`;render()
}
addCalendarNoteForDate=function(date=iso()){
  if(!hasPerm('manage_calendar'))return toast('You do not have permission to manage the Calendar');let defaultSite=site==='all'?(activeSites()[0]?.id||''):site;
  modal=`<div class="modal"><div class="modalbox"><h2>Add Calendar Note</h2><p class="muted">Use a simple category so deliveries, scaffold, inspections and meetings are easy to spot.</p><div class="two"><div class="field"><label>Date</label><input id="cn-date" class="input" type="date" value="${date||iso()}"></div><div class="field"><label>Site</label><select id="cn-site" class="select">${activeSites().map(s=>`<option value="${s.id}" ${s.id===defaultSite?'selected':''}>${s.name}</option>`).join('')}</select></div></div>
  <div class="field"><label>Category</label><select id="cn-category" class="select">${['General','Delivery','Scaffold','Inspection','Meeting','Other Site Event'].map(v=>`<option>${v}</option>`).join('')}</select></div>
  <div class="field"><label>Title</label><input id="cn-title" class="input" placeholder="e.g. Facing brick delivery"></div><div class="field"><label>Notes</label><textarea id="cn-notes" class="textarea" style="min-height:120px" placeholder="Add the useful detail you may need to refer back to later..."></textarea></div>
  <button class="btn orange" onclick="saveCalendarNote()">Save note</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
saveCalendarNote=function(){
  if(!hasPerm('manage_calendar'))return toast('You do not have permission to manage the Calendar');
  let d=document.getElementById('cn-date')?.value||'',siteId=document.getElementById('cn-site')?.value||'',title=(document.getElementById('cn-title')?.value||'').trim(),notes=(document.getElementById('cn-notes')?.value||'').trim(),category=document.getElementById('cn-category')?.value||'General';
  if(!d||!siteId||!title)return toast('Date, site and title are required');db.calendarNotes.push({id:'cn'+Date.now(),date:d,siteId,title,notes,category,createdBy:currentUser()?.id||'',createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()});calendarMonth=d.slice(0,7)+'-01';save();emitNotification('site',siteId,`${category}: ${title}`);modal=null;savedState('Calendar note saved');render()
}
editCalendarNote=function(id){
  if(!hasPerm('manage_calendar'))return toast('You do not have permission to manage the Calendar');let n=(db.calendarNotes||[]).find(n=>n.id===id);if(!n)return;
  modal=`<div class="modal"><div class="modalbox"><h2>Edit Calendar Note</h2><div class="two"><div class="field"><label>Date</label><input id="ecn-date" class="input" type="date" value="${n.date}"></div><div class="field"><label>Site</label><select id="ecn-site" class="select">${activeSites().map(s=>`<option value="${s.id}" ${s.id===n.siteId?'selected':''}>${s.name}</option>`).join('')}</select></div></div>
  <div class="field"><label>Category</label><select id="ecn-category" class="select">${['General','Delivery','Scaffold','Inspection','Meeting','Other Site Event'].map(v=>`<option ${n.category===v?'selected':''}>${v}</option>`).join('')}</select></div>
  <div class="field"><label>Title</label><input id="ecn-title" class="input" value="${String(n.title||'').replace(/"/g,'&quot;')}"></div><div class="field"><label>Notes</label><textarea id="ecn-notes" class="textarea" style="min-height:120px">${n.notes||''}</textarea></div><button class="btn orange" onclick="saveEditedCalendarNote('${n.id}')">Save changes</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
saveEditedCalendarNote=function(id){
  if(!hasPerm('manage_calendar'))return;let n=(db.calendarNotes||[]).find(n=>n.id===id);if(!n)return;
  n.date=document.getElementById('ecn-date')?.value||n.date;n.siteId=document.getElementById('ecn-site')?.value||n.siteId;n.category=document.getElementById('ecn-category')?.value||'General';n.title=(document.getElementById('ecn-title')?.value||n.title).trim();n.notes=(document.getElementById('ecn-notes')?.value||'').trim();n.updatedAt=new Date().toISOString();n.updatedBy=currentUser()?.id||'';calendarMonth=n.date.slice(0,7)+'-01';save();emitNotification('site',n.siteId,`${n.category}: ${n.title}`);modal=null;savedState('Calendar note updated');render()
}

// ---- Day Rate: actual hourly base + optional full-day override -------
function v169FullDayOverride(workerId,date,siteId){return (db.dayRateFullOverrides||[]).find(r=>r.workerId===workerId&&r.date===date&&r.siteId===siteId&&r.active!==false)||null}
attendanceWeekTotals=function(workerId,start,siteId='all'){
  let wm=attendanceMemberById(workerId),m=wm?.member||{},shifts=attendanceShiftsForWorkerWeek(workerId,start,siteId),totalHours=shifts.reduce((n,s)=>n+attendanceShiftHours(s),0),standard=Number(m.standardWeekHours||40),
      basicHours=Math.min(totalHours,standard),overtimeHours=Math.max(0,totalHours-standard),hourly=Number(m.hourlyRate||0),ot=Number(m.overtimeRate||hourly),gross=0,fullDayTopUp=0,days=new Set(shifts.map(s=>attendanceDateOnly(s.clockIn?.time,s.siteId))).size;
  if(m.employmentType==='Hourly'||m.employmentType==='Day Rate')gross=totalHours*hourly+overtimeHours*Math.max(0,ot-hourly);
  if(m.employmentType==='Day Rate'){
    let byDate={};shifts.forEach(s=>{let d=attendanceDateOnly(s.clockIn?.time,s.siteId);byDate[d]=(byDate[d]||0)+attendanceShiftHours(s)});
    Object.entries(byDate).forEach(([d,h])=>{let sid=siteId==='all'?(shifts.find(s=>attendanceDateOnly(s.clockIn?.time,s.siteId)===d)?.siteId||''):siteId;if(v169FullDayOverride(workerId,d,sid)){let top=Math.max(0,Number(m.dayRate||0)-(Number(h)*hourly));fullDayTopUp+=top;gross+=top}})
  }
  return {shifts,totalHours,basicHours,overtimeHours,gross,days,fullDayTopUp}
}
function v169ToggleFullDay(workerId,date,siteId){
  if(!hasPerm('manage_attendance')&&!canDirector())return toast('Attendance management permission is required');let wm=attendanceMemberById(workerId);if(!wm)return;
  let row=v169FullDayOverride(workerId,date,siteId);
  if(row){if(!confirm(`Remove the full day rate override for ${wm.member.name} on ${date}?`))return;row.active=false;row.removedBy=currentUser()?.name||'';row.removedAt=new Date().toISOString();audit('attendance','day rate override',row.id,`${wm.member.name}: full day rate override removed for ${date}`,siteId)}
  else{let reason=prompt(`Reason for paying ${wm.member.name} a full day rate on ${date}:`);if(!reason)return;let r={id:contractorId('full-day'),workerId,date,siteId,reason,active:true,by:currentUser()?.name||'',byId:currentUser()?.id||'',at:new Date().toISOString()};db.dayRateFullOverrides.unshift(r);audit('attendance','day rate override',r.id,`${wm.member.name}: full day rate approved for ${date} — ${reason}`,siteId)}
  attendanceMarkApprovalNeedsRecheck(workerId,siteId,date+'T12:00:00');save();render()
}
contractorDayRateControls=function(m,start,siteId){
  if(m.employmentType!=='Day Rate')return '';let shifts=attendanceShiftsForWorkerWeek(m.id,start,siteId),dates=[...new Set(shifts.map(s=>attendanceDateOnly(s.clockIn.time,s.siteId)))];
  if(!dates.length)return '';
  return `<div class="dayrate-week v169-dayrate"><b>Day Rate exceptions</b><small>Normal pay uses actual clocked hours × hourly base. Use Full Day only when you deliberately want to honour the full day rate after an early finish.</small><div>${dates.map(d=>{let h=shifts.filter(s=>attendanceDateOnly(s.clockIn.time,s.siteId)===d).reduce((n,s)=>n+attendanceShiftHours(s),0),ov=v169FullDayOverride(m.id,d,siteId),actual=h*Number(m.hourlyRate||0);return `<label class="${ov?'approved':''}"><span>${new Date(d+'T12:00:00').toLocaleDateString('en-GB',{weekday:'short',day:'numeric'})} · ${h.toFixed(2)}h · ${attendanceMoney(actual)}</span>${ov?`<button class="btn green smallbtn" onclick="v169ToggleFullDay('${m.id}','${d}','${siteId}')">✓ Full day ${attendanceMoney(m.dayRate)}</button>`:`<button class="btn light smallbtn" onclick="v169ToggleFullDay('${m.id}','${d}','${siteId}')">Pay full day ${attendanceMoney(m.dayRate)}</button>`}</label>`}).join('')}</div></div>`
}
editAttendanceMemberSettings=function(teamId,memberId){
  if(!hasPerm('manage_attendance'))return toast('Attendance management permission is required');let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!m)return;let financial=hasPerm('view_financials'),availableUsers=db.users.filter(u=>!u.linkedMemberId||u.linkedMemberId===m.id);
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">WORKER SETTINGS</span><h2>${m.name}</h2><p class="muted">${g.name} · ${m.role||'Team member'}</p>
  <div class="two"><div class="field"><label>Time / pay type</label><select id="att-emp-type" class="select">${['Hourly','Day Rate','Subcontract','Salary','Not tracked'].map(v=>`<option ${m.employmentType===v?'selected':''}>${v}</option>`).join('')}</select></div><div class="field"><label>Default unpaid break</label><input id="att-break" class="input" type="number" min="0" max="180" step="5" value="${Number(m.defaultBreakMinutes||0)}"><small class="muted">minutes per shift</small></div></div>
  <div class="two"><div class="field"><label>Expected start <span class="muted">(optional override)</span></label><input id="att-worker-start" class="input" type="time" value="${m.expectedStartTime||''}"></div><div class="field"><label>Expected finish <span class="muted">(optional override)</span></label><input id="att-worker-end" class="input" type="time" value="${m.expectedEndTime||''}"></div></div>
  ${financial?`<div class="two"><div class="field"><label>Hourly base rate</label><input id="att-rate" class="input" type="number" min="0" step="0.01" value="${Number(m.hourlyRate||0)}"></div><div class="field"><label>Overtime rate</label><input id="att-ot-rate" class="input" type="number" min="0" step="0.01" value="${Number(m.overtimeRate||0)}"></div></div>
  <div class="two"><div class="field"><label>Full day rate <span class="muted">(Day Rate only)</span></label><input id="att-day-rate" class="input" type="number" min="0" step="0.01" value="${Number(m.dayRate||0)}"><small class="muted">Only used when management approves a Full Day exception.</small></div><div class="field"><label>Standard weekly hours</label><input id="att-week-hours" class="input" type="number" min="1" max="80" step="0.5" value="${Number(m.standardWeekHours||40)}"></div></div>
  <div class="field"><label>Standard day hours</label><input id="att-day-hours" class="input" type="number" min="1" max="16" step="0.5" value="${Number(m.standardDayHours||8)}"></div>`:'<div class="att-pay-hidden">Pay rates are hidden because your account does not include View Financials.</div>'}
  <div class="field"><label>Linked SiteSight user</label><select id="att-linked-user" class="select"><option value="">No self-clocking login</option>${availableUsers.map(u=>`<option value="${u.id}" ${m.linkedUserId===u.id||u.linkedMemberId===m.id?'selected':''}>${u.name} · @${u.username}</option>`).join('')}</select></div>
  <label class="att-checkbox"><input id="att-active-worker" type="checkbox" ${m.activeForAttendance!==false?'checked':''}> Include this person in Time & Attendance</label><div style="margin-top:14px"><button class="btn orange" onclick="saveAttendanceMemberSettings('${teamId}','${memberId}')">Save settings</button> <button class="btn light" onclick="openTeamMember('${teamId}','${memberId}')">Cancel</button></div></div></div>`;render()
}
saveAttendanceMemberSettings=function(teamId,memberId){
  let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!m||!hasPerm('manage_attendance'))return;
  m.employmentType=document.getElementById('att-emp-type')?.value||'Not tracked';m.defaultBreakMinutes=Math.max(0,Number(document.getElementById('att-break')?.value||0));m.activeForAttendance=!!document.getElementById('att-active-worker')?.checked;m.expectedStartTime=document.getElementById('att-worker-start')?.value||'';m.expectedEndTime=document.getElementById('att-worker-end')?.value||'';
  if(hasPerm('view_financials')){m.hourlyRate=Math.max(0,Number(document.getElementById('att-rate')?.value||0));m.overtimeRate=Math.max(0,Number(document.getElementById('att-ot-rate')?.value||0));m.dayRate=Math.max(0,Number(document.getElementById('att-day-rate')?.value||0));m.standardWeekHours=Math.max(1,Number(document.getElementById('att-week-hours')?.value||40));m.standardDayHours=Math.max(1,Number(document.getElementById('att-day-hours')?.value||8))}
  let uid=document.getElementById('att-linked-user')?.value||'';db.users.forEach(u=>{if(u.linkedMemberId===m.id)u.linkedMemberId=''});attendanceAllMembers().forEach(x=>{if(x.member.linkedUserId===uid&&x.member.id!==m.id)x.member.linkedUserId=''});m.linkedUserId=uid;if(uid){let u=db.users.find(u=>u.id===uid);if(u)u.linkedMemberId=m.id}
  save();audit('attendance','team member',m.id,`${m.name}: Time & Attendance settings updated`,attendanceDefaultSiteId());savedState('Attendance settings saved');openTeamMember(teamId,memberId)
}

// ---- Pay Review ------------------------------------------------------
function v169SetPriceRows(siteId){
  let rows=[];(db.items||[]).filter(x=>x.site===siteId).forEach(x=>{
    if(x.type==='House Plot')(x.lifts||[]).forEach(l=>{let paid=stagePaidAmount(l),value=Number(l.price||0),due=Math.max(0,value-paid);if(l.status==='Complete'&&value>0)rows.push({ref:x.ref,stage:l.name,team:paymentPayeeName({paidTo:l.paidTo},x.gang),value,paid,due,status:due<=0?'Paid':paid>0?'Part Paid':'Ready for Pay',signed:x.signed})});
    else if(x.status==='Complete'){let paid=stagePaidAmount(x),value=Number(x.price||0),due=Math.max(0,value-paid);if(value>0)rows.push({ref:x.ref,stage:x.type,team:paymentPayeeName({paidTo:x.paidTo},x.gang),value,paid,due,status:due<=0?'Paid':paid>0?'Part Paid':'Ready for Pay',signed:x.signed})}
  });return rows
}
function v169VariationRows(siteId){
  let rows=[];(db.items||[]).filter(x=>x.site===siteId).forEach(x=>(x.variations||[]).forEach(v=>{if(v.approvalStatus!=='Approved')return;let paid=(v.payments||[]).reduce((n,p)=>n+Number(p.amount||0),0),value=Number(v.value||0),due=Math.max(0,value-paid);rows.push({ref:x.ref,name:v.name,value,paid,due,status:due<=0?'Paid':paid>0?'Part Paid':'Ready for Pay'})}));return rows
}
contractorPayReview=function(siteId){
  if(!hasPerm('view_financials'))return `<div class="panel empty-help"><b>You do not have permission to view pay information.</b></div>`;
  let start=contractorPayWeek||contractorWeekStart(),members=attendanceExpectedMembers(siteId,start).filter(x=>['Hourly','Day Rate'].includes(x.member.employmentType)),
      timeRows=members.map(x=>({...x,tot:attendanceWeekTotals(x.member.id,start,siteId)})),setRows=v169SetPriceRows(siteId),varRows=v169VariationRows(siteId),
      timeTotal=timeRows.reduce((n,x)=>n+x.tot.gross,0),setDue=setRows.reduce((n,x)=>n+x.due,0),varDue=varRows.reduce((n,x)=>n+x.due,0),awaitingSignoff=db.items.filter(x=>x.site===siteId&&!x.signed&&overall(x)==='Complete').length;
  return `<div class="contractor-section"><div class="sectionhead"><div><h2>Weekly Pay Review</h2><div class="muted">Set Price, Hourly, Day Rate and approved Variations in one management view.</div></div><div class="actions"><input class="input" type="date" value="${start}" onchange="contractorPayWeek=contractorWeekStart(this.value);render()"><button class="btn light" onclick="downloadAttendanceCSV()">⇩ Attendance CSV</button></div></div>
  <div class="pay-review-total"><span>SITE RECORDS INDICATE</span><b>${attendanceMoney(timeTotal+setDue+varDue)}</b><small>Operational pay review only — not PAYE, CIS, pensions, tax or bank payment processing.</small></div>
  <div class="v169-pay-status"><span><b>${setRows.filter(r=>r.status==='Ready for Pay').length}</b> Ready for Pay</span><span><b>${setRows.filter(r=>r.status==='Part Paid').length}</b> Part Paid</span><span><b>${setRows.filter(r=>r.status==='Paid').length}</b> Paid</span><span><b>${awaitingSignoff}</b> Complete Work Items awaiting final sign-off</span></div>
  <div class="pay-review-grid"><section><h3>Set Price — completed work</h3>${setRows.map(r=>`<div class="pay-review-row"><div><b>${contractorEsc(r.ref)}</b><span>${contractorEsc(r.stage)} · ${contractorEsc(r.team)} · <em class="pay-state ${r.status.toLowerCase().replace(/\s/g,'-')}">${r.status}</em></span></div><strong>${r.due>0?attendanceMoney(r.due):'Paid'}</strong></div>`).join('')||'<div class="muted">No completed set-price work in this view.</div>'}</section>
  <section><h3>Hourly / Day Rate — ${attendanceWeekLabel(start)}</h3>${timeRows.map(x=>`<div class="pay-review-row"><div><b>${contractorEsc(x.member.name)}</b><span>${contractorEsc(x.gang.name)} · ${x.member.employmentType} · ${attendanceFmtHours(x.tot.totalHours)}${x.member.employmentType==='Day Rate'?' · actual hours × hourly base':''}</span></div><strong>${attendanceMoney(x.tot.gross)}</strong></div>${contractorDayRateControls(x.member,start,siteId)}`).join('')||'<div class="muted">No Hourly or Day Rate workers configured for this Site.</div>'}</section></div>
  <section class="v169-variation-pay"><div class="sectionhead"><h3>Approved Variations</h3><span>${varRows.length}</span></div>${varRows.map(r=>`<div class="pay-review-row"><div><b>${contractorEsc(r.ref)} · ${contractorEsc(r.name)}</b><span>${r.status} · Approved value ${attendanceMoney(r.value)}</span></div><strong>${r.due>0?attendanceMoney(r.due):'Paid'}</strong></div>`).join('')||'<div class="muted">No approved Variations in this Site.</div>'}</section></div>`
}

// ---- Workforce analytics / measurable savings -----------------------
function v169AnalyticsDates(){
  let today=iso(),from,to=today;
  if(v169AttendanceRange==='week')from=attendanceWeekStartISO(new Date());
  else if(v169AttendanceRange==='month')from=today.slice(0,8)+'01';
  else if(v169AttendanceRange==='custom'){from=v169AttendanceFrom||attendanceAddDays(today,-29);to=v169AttendanceTo||today}
  else from=attendanceAddDays(today,-29);
  return {from,to}
}
function v169AttendanceAnalytics(){
  let {from,to}=v169AnalyticsDates(),siteIds=v169SiteIdsForAnalytics(),rows=new Map(),shifts=(db.attendanceShifts||[]).filter(s=>!s.voided&&siteIds.includes(s.siteId)&&s.clockOut?.time&&attendanceDateOnly(s.clockIn.time,s.siteId)>=from&&attendanceDateOnly(s.clockIn.time,s.siteId)<=to);
  let adminHoursSaved=0,adminCostSaved=0,lateMins=0,earlyMins=0,wageExposure=0;
  shifts.forEach(s=>{
    let wm=attendanceMemberById(s.workerId);if(!wm)return;let m=wm.member,p=v169ShiftPunctuality(s,m),c=attendanceSiteConfig(s.siteId)||{},manual=Number(c.analyticsManualMinutes??8),sys=Number(c.analyticsSystemMinutes??1),saved=Math.max(0,manual-sys);
    adminHoursSaved+=saved/60;adminCostSaved+=(saved/60)*Number(c.analyticsAdminHourlyCost??25);lateMins+=p.late;earlyMins+=p.early;
    wageExposure+=((p.late+p.early)/60)*Number(m.hourlyRate||0);
    let r=rows.get(m.id)||{member:m,gang:wm.gang,shifts:0,lateCount:0,lateMinutes:0,earlyCount:0,earlyMinutes:0,onTime:0,hours:0,verified:0};
    r.shifts++;r.hours+=attendanceShiftHours(s);if(p.late){r.lateCount++;r.lateMinutes+=p.late}if(p.early){r.earlyCount++;r.earlyMinutes+=p.early}if(p.onTime)r.onTime++;if(s.clockIn?.status==='Verified'&&s.clockOut?.status==='Verified')r.verified++;rows.set(m.id,r)
  });
  let people=[...rows.values()].map(r=>({...r,punctuality:r.shifts?Math.round(r.onTime/r.shifts*100):0})),
      late=people.filter(r=>r.lateCount).sort((a,b)=>b.lateMinutes-a.lateMinutes),
      early=people.filter(r=>r.earlyCount).sort((a,b)=>b.earlyMinutes-a.earlyMinutes),
      best=people.filter(r=>r.shifts>=3&&r.lateCount===0&&r.earlyCount===0).sort((a,b)=>b.shifts-a.shifts||b.hours-a.hours);
  return {from,to,shifts,people,late,early,best,adminHoursSaved,adminCostSaved,lateMins,earlyMins,wageExposure}
}
function v169AttendanceAnalyticsPanel(){
  if(!hasPerm('view_attendance')&&!hasPerm('manage_attendance'))return '';let a=v169AttendanceAnalytics(),totalUnworked=a.lateMins+a.earlyMins;
  return `<div class="panel v169-attendance-analytics"><div class="sectionhead"><div><h2>Clocking In / Out — Savings & Attendance</h2><div class="muted">Measure administration saved and identify repeated late arrivals / early finishes.</div></div>${site!=='all'&&(hasPerm('manage_attendance')||hasPerm('manage_sites'))?`<button class="btn light smallbtn" onclick="configureAttendanceLocation('${site}')">⚙ Assumptions & workday</button>`:''}</div>
  <div class="v169-rangebar"><button class="${v169AttendanceRange==='week'?'active':''}" onclick="v169AttendanceRange='week';render()">This Week</button><button class="${v169AttendanceRange==='month'?'active':''}" onclick="v169AttendanceRange='month';render()">This Month</button><button class="${v169AttendanceRange==='30'?'active':''}" onclick="v169AttendanceRange='30';render()">Last 30 Days</button><button class="${v169AttendanceRange==='custom'?'active':''}" onclick="v169AttendanceRange='custom';render()">Custom</button>${v169AttendanceRange==='custom'?`<input class="input" type="date" value="${a.from}" onchange="v169AttendanceFrom=this.value;render()"><span>→</span><input class="input" type="date" value="${a.to}" onchange="v169AttendanceTo=this.value;render()">`:''}<span class="muted">${a.from} → ${a.to}</span></div>
  <div class="analytics-grid v169-saving-cards"><div class="card"><div class="muted">Timesheet admin time saved</div><div class="analytics-value">${a.adminHoursSaved.toFixed(1)} hrs</div><small>Based on your saved manual-vs-SiteSight assumptions.</small></div><div class="card"><div class="muted">Estimated admin cost saved</div><div class="analytics-value">${attendanceMoney(a.adminCostSaved)}</div></div><div class="card"><div class="muted">Unworked scheduled time identified</div><div class="analytics-value">${v169MinutesLabel(totalUnworked)}</div><small>${v169MinutesLabel(a.lateMins)} late · ${v169MinutesLabel(a.earlyMins)} early</small></div>${hasPerm('view_financials')?`<div class="card"><div class="muted">Potential wage exposure identified</div><div class="analytics-value">${attendanceMoney(a.wageExposure)}</div><small>If this time would previously have been paid as a full scheduled day.</small></div>`:''}<div class="card"><div class="muted">Verified completed shifts</div><div class="analytics-value">${a.shifts.filter(s=>s.clockIn?.status==='Verified'&&s.clockOut?.status==='Verified').length}/${a.shifts.length}</div></div></div>
  <div class="v169-performance-note">Attendance analytics show punctuality and recorded time only — they do not judge workmanship, productivity or overall employee performance.</div>
  <div class="v169-attendance-rankings"><section><div class="sectionhead"><h3>Most Late</h3><span>Total minutes late</span></div>${a.late.slice(0,8).map((r,i)=>`<div class="v169-rank-row"><b>${i+1}</b><span><strong>${r.member.name}</strong><small>${r.gang.name} · ${r.lateCount} late arrival${r.lateCount===1?'':'s'}</small></span><em>${v169MinutesLabel(r.lateMinutes)}</em></div>`).join('')||'<div class="muted">No late arrivals in this range.</div>'}</section>
  <section><div class="sectionhead"><h3>Most Early Finishes</h3><span>Total minutes early</span></div>${a.early.slice(0,8).map((r,i)=>`<div class="v169-rank-row"><b>${i+1}</b><span><strong>${r.member.name}</strong><small>${r.gang.name} · ${r.earlyCount} early finish${r.earlyCount===1?'':'es'}</small></span><em>${v169MinutesLabel(r.earlyMinutes)}</em></div>`).join('')||'<div class="muted">No early finishes in this range.</div>'}</section>
  <section><div class="sectionhead"><h3>Best Attendance</h3><span>Never late or early in this range</span></div>${a.best.slice(0,8).map((r,i)=>`<div class="v169-rank-row best"><b>✓</b><span><strong>${r.member.name}</strong><small>${r.gang.name} · ${r.shifts} completed shifts · ${r.hours.toFixed(1)} hrs</small></span><em>100%</em></div>`).join('')||'<div class="muted">Nobody with at least 3 shifts has a perfect punctuality record in this range.</div>'}</section></div>
  <details class="v169-breakdown"><summary>Full operative breakdown (${a.people.length})</summary><div style="overflow:auto"><table class="analytics-table"><thead><tr><th>Operative</th><th>Team</th><th>Shifts</th><th>Punctual days</th><th>Late</th><th>Late time</th><th>Early</th><th>Early time</th><th>Hours</th></tr></thead><tbody>${a.people.sort((x,y)=>y.shifts-x.shifts).map(r=>`<tr><td><b>${r.member.name}</b></td><td>${r.gang.name}</td><td>${r.shifts}</td><td>${r.onTime} (${r.punctuality}%)</td><td>${r.lateCount}</td><td>${v169MinutesLabel(r.lateMinutes)}</td><td>${r.earlyCount}</td><td>${v169MinutesLabel(r.earlyMinutes)}</td><td>${r.hours.toFixed(1)}</td></tr>`).join('')}</tbody></table></div></details></div>`
}
const v169BaseAnalyticsPage=analyticsPage;
analyticsPage=function(){if(!hasPerm('analytics'))return v169BaseAnalyticsPage();return v169BaseAnalyticsPage()+v169AttendanceAnalyticsPanel()}

// ---- Drawing revision workflow ---------------------------------------
function v169DrawingMeta(d){
  return `<div class="v169-drawing-meta">${d.drawingNumber?`<b>${contractorEsc(d.drawingNumber)}</b>`:'<b>No drawing number</b>'}${d.revision?`<span>Rev ${contractorEsc(d.revision)}</span>`:''}${d.issuedDate?`<span>Issued ${d.issuedDate}</span>`:''}<span class="${d.revisionStatus==='Superseded'?'superseded':'current'}">${d.revisionStatus||'Current'}</span></div>`
}
const v169BaseDrawingTakeoffSummary=drawingTakeoffSummary;
drawingTakeoffSummary=function(d){return v169DrawingMeta(d)+v169BaseDrawingTakeoffSummary(d)}
addSiteDrawing=function(){
  if(!hasPerm('manage_drawings'))return toast('You do not have drawing upload access');let defaultSite=drawingSite==='all'?(activeSites()[0]?.id||''):drawingSite;
  modal=`<div class="modal"><div class="modalbox"><h2>Add Site Drawing</h2><p class="muted">Drawing number + revision makes the current construction information obvious.</p><div class="two"><div class="field"><label>Site</label><select id="sdw-site" class="select">${activeSites().map(s=>`<option value="${s.id}" ${s.id===defaultSite?'selected':''}>${s.name}</option>`).join('')}</select></div><div class="field"><label>House type / drawing group</label><input id="sdw-house" class="input" placeholder="e.g. Type A, Elm, Birch"></div></div>
  <div class="three"><div class="field"><label>Drawing number</label><input id="sdw-number" class="input" placeholder="e.g. MV-A-104"></div><div class="field"><label>Revision</label><input id="sdw-revision" class="input" placeholder="e.g. C"></div><div class="field"><label>Issued date</label><input id="sdw-issued" class="input" type="date" value="${iso()}"></div></div>
  <div class="field"><label>Drawing name</label><input id="sdw-name" class="input" placeholder="e.g. Type A Ground Floor Plan"></div><div class="field"><label>Drawing file</label><input id="sdw-file" class="input" type="file" accept=".pdf,image/*"></div>${cameraPicker('sdw-file','Photograph drawing')}<div class="field"><label>Notes</label><textarea id="sdw-notes" class="textarea"></textarea></div><button class="btn green" onclick="saveSiteDrawing()">Upload current drawing</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
saveSiteDrawing=function(){
  let f=uploadFiles('sdw-file')[0],siteId=document.getElementById('sdw-site')?.value||'',houseType=(document.getElementById('sdw-house')?.value||'').trim(),name=(document.getElementById('sdw-name')?.value||'').trim(),notes=(document.getElementById('sdw-notes')?.value||'').trim(),
      drawingNumber=(document.getElementById('sdw-number')?.value||'').trim(),revision=(document.getElementById('sdw-revision')?.value||'').trim(),issuedDate=document.getElementById('sdw-issued')?.value||iso();
  if(!f||!siteId||!houseType||!name||!drawingNumber||!revision)return toast('Site, house type, drawing number, revision, drawing name and file are required');
  let previous=(db.siteDrawings||[]).filter(d=>d.siteId===siteId&&String(d.drawingNumber||'').toLowerCase()===drawingNumber.toLowerCase()&&d.revisionStatus!=='Superseded');
  if(previous.length&&!confirm(`A current ${drawingNumber} drawing already exists. Upload Revision ${revision} and mark the previous revision SUPERSEDED?`))return;
  let d={id:'sdw'+Date.now(),siteId,houseType,name,notes,drawingNumber,revision,issuedDate,revisionStatus:'Current',supersededBy:'',fileName:f.name||'Drawing',type:f.type||'image/jpeg',data:'',uploadedDate:iso(),uploadedAt:new Date().toISOString(),uploadedBy:currentUser()?.name||'',lastUsedAt:Date.now(),storage:'browser',takeoffs:[],takeoffCalibration:null};
  storeDrawingInBrowser(f).then(data=>{d.data=data;previous.forEach(old=>{old.revisionStatus='Superseded';old.supersededBy=d.id;old.supersededAt=new Date().toISOString();old.supersededByUser=currentUser()?.name||''});clearCameraFiles('sdw-file');db.siteDrawings.push(d);save();audit('drawing','drawing library',d.id,`${drawingNumber}: Revision ${revision} uploaded CURRENT${previous.length?` · ${previous.length} previous revision superseded`:''}`,siteId);modal=null;savedState('Current drawing saved');render()}).catch(()=>toast('Drawing could not be stored in this browser'))
}
function v169NewDrawingRevision(id){
  if(!hasPerm('manage_drawings'))return;let d=db.siteDrawings.find(x=>x.id===id);if(!d)return;
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">DRAWING REVISION</span><h2>${contractorEsc(d.drawingNumber||d.name)}</h2><p class="muted">Current: Revision ${contractorEsc(d.revision||'—')}. Saving the new revision will automatically mark this one SUPERSEDED.</p>
  <div class="two"><div class="field"><label>New revision</label><input id="rev-new" class="input" placeholder="e.g. D"></div><div class="field"><label>Issued date</label><input id="rev-issued" class="input" type="date" value="${iso()}"></div></div><div class="field"><label>New drawing file</label><input id="rev-file" class="input" type="file" accept=".pdf,image/*"></div><div class="field"><label>Revision note</label><textarea id="rev-note" class="textarea" placeholder="What changed?"></textarea></div><button class="btn orange" onclick="v169SaveDrawingRevision('${id}')">Save new CURRENT revision</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function v169SaveDrawingRevision(id){
  let old=db.siteDrawings.find(x=>x.id===id),f=uploadFiles('rev-file')[0],revision=(document.getElementById('rev-new')?.value||'').trim(),issuedDate=document.getElementById('rev-issued')?.value||iso(),note=(document.getElementById('rev-note')?.value||'').trim();if(!old||!f||!revision)return toast('New revision and drawing file are required');
  let d={...old,id:'sdw'+Date.now(),revision,issuedDate,revisionStatus:'Current',supersededBy:'',supersededAt:'',supersededByUser:'',notes:note||old.notes,fileName:f.name||old.fileName,type:f.type||old.type,data:'',uploadedDate:iso(),uploadedAt:new Date().toISOString(),uploadedBy:currentUser()?.name||'',lastUsedAt:Date.now(),takeoffs:[],takeoffCalibration:null};
  storeDrawingInBrowser(f).then(data=>{d.data=data;db.siteDrawings.filter(x=>x.siteId===old.siteId&&x.drawingNumber===old.drawingNumber&&x.revisionStatus!=='Superseded').forEach(x=>{x.revisionStatus='Superseded';x.supersededBy=d.id;x.supersededAt=new Date().toISOString();x.supersededByUser=currentUser()?.name||''});db.siteDrawings.push(d);save();audit('drawing','drawing library',d.id,`${d.drawingNumber}: Revision ${revision} uploaded · previous revision superseded`,d.siteId);modal=null;savedState('New drawing revision saved');render()})
}
async function openSiteDrawing(id){
  let d=(db.siteDrawings||[]).find(d=>d.id===id);if(!d)return;d.lastUsedAt=Date.now();save();if(!d.data)return toast('Drawing file is unavailable');let src=await resolveMediaRef(d.data);if(!src)return toast('This drawing is stored in a different browser. Open SiteSight on the device where it was uploaded.');
  let isPdf=d.type==='application/pdf'||String(d.fileName||'').toLowerCase().endsWith('.pdf'),tot=takeoffTotals(d),current=d.revisionStatus==='Superseded'?(db.siteDrawings||[]).find(x=>x.id===d.supersededBy)||(db.siteDrawings||[]).find(x=>x.siteId===d.siteId&&x.drawingNumber===d.drawingNumber&&x.revisionStatus==='Current'):null;
  modal=`<div class="drawing-fullscreen" onclick="if(event.target===this){modal=null;render()}"><div class="drawing-fullscreen-inner">${d.revisionStatus==='Superseded'?`<div class="v169-superseded-banner"><b>SUPERSEDED — DO NOT BUILD FROM THIS DRAWING</b><span>${current?`Current revision is ${contractorEsc(current.revision||'newer')}.`:'A newer revision has been issued.'}</span>${current?`<button class="btn red smallbtn" onclick="openSiteDrawing('${current.id}')">Open CURRENT revision</button>`:''}</div>`:''}<div class="drawing-fullscreen-head"><div><b>${contractorEsc(d.drawingNumber||'')} ${d.drawingNumber?'· ':''}${contractorEsc(d.name)}</b>${v169DrawingMeta(d)}<div class="muted">${siteName(d.siteId)} · ${d.houseType}</div>${(d.takeoffs||[]).length?`<div class="viewer-takeoff-line">📐 ${(d.takeoffs||[]).length} take-off item${(d.takeoffs||[]).length===1?'':'s'} · Net ${Math.max(0,tot.net).toFixed(2)} m²${hasPerm('view_financials')?` · ${money(tot.value)}`:''}</div>`:''}</div><div class="actions">${d.revisionStatus!=='Superseded'?`<button class="btn blue" onclick="openDrawingTakeoff('library','${d.id}')">📐 Measure Drawing</button>`:''}<button class="btn light" onclick="modal=null;render()">✕ Close</button></div></div><div class="drawing-fullscreen-body">${isPdf?`<iframe src="${src}" title="${contractorEsc(d.name)}"></iframe>`:`<img src="${src}" alt="${contractorEsc(d.name)}">`}</div></div></div>`;render()
}
siteDrawingsPage=function(){
  if(!hasPerm('view_drawings'))return `<div class="panel empty-help"><b>You do not have permission to view drawings.</b></div>`;prepareDrawingLibrary();
  let drawings=(db.siteDrawings||[]).filter(d=>canAccessSite(d.siteId)&&(drawingSite==='all'||d.siteId===drawingSite)&&(drawingHouseType==='all'||d.houseType===drawingHouseType)&&(v169ShowSupersededDrawings||d.revisionStatus!=='Superseded')),items=drawingItemsForSite();
  if(drawingItem!=='all'){let item=db.items.find(x=>x.id===drawingItem);if(item)drawings=drawings.filter(d=>d.siteId===item.site&&(item.houseType?d.houseType===item.houseType:true))}
  if(drawingQuery.trim()){let q=drawingQuery.trim().toLowerCase();drawings=drawings.filter(d=>[d.name,d.houseType,d.drawingNumber,d.revision,siteName(d.siteId),d.notes,d.fileName].some(v=>String(v||'').toLowerCase().includes(q)))}
  drawings.sort((a,b)=>(a.revisionStatus==='Superseded')-(b.revisionStatus==='Superseded')||String(a.drawingNumber||a.name).localeCompare(String(b.drawingNumber||b.name)));
  let drawingCard=d=>`<div class="site-drawing-card ${d.revisionStatus==='Superseded'?'v169-superseded-card':''}"><div class="site-drawing-preview" onclick="openSiteDrawing('${d.id}')">${d.data&&d.type!=='application/pdf'&&!String(d.fileName||'').toLowerCase().endsWith('.pdf')?`<img src="${mediaSrc(d.data)}"${pendingMediaAttr(d.data)} alt="${contractorEsc(d.name)}">`:`<div class="pdf-icon">▤</div>`}</div><div class="site-drawing-body"><h3 onclick="openSiteDrawing('${d.id}')">${contractorEsc(d.name)}</h3>${v169DrawingMeta(d)}<div class="muted">${siteName(d.siteId)} · ${contractorEsc(d.houseType)} · uploaded ${d.uploadedDate}</div>${d.notes?`<div class="muted" style="margin-top:5px">${contractorEsc(d.notes)}</div>`:''}<div class="site-drawing-actions"><button class="btn light smallbtn" onclick="openSiteDrawing('${d.id}')">Open Drawing</button>${d.revisionStatus!=='Superseded'?`<button class="btn blue smallbtn" onclick="openDrawingTakeoff('library','${d.id}')">📐 Measure</button>`:''}${hasPerm('manage_drawings')?`<div class="drawing-more-wrap"><button class="btn light smallbtn" onclick="toggleDrawingActionMenu('${d.id}')">More ▾</button>${drawingActionMenu===d.id?`<div class="drawing-more-menu">${d.revisionStatus!=='Superseded'?`<button onclick="drawingActionMenu='';v169NewDrawingRevision('${d.id}')">Upload new revision</button><button onclick="drawingActionMenu='';assignSiteDrawing('${d.id}')">Add CURRENT to Work Item</button><button onclick="drawingActionMenu='';quickAddDrawingToNewWorkItem('${d.id}')">Create Work Item</button>`:''}${hasPerm('permanent_delete')?`<button class="danger" onclick="drawingActionMenu='';deleteSiteDrawing('${d.id}')">Delete drawing</button>`:''}</div>`:''}</div>`:''}</div></div></div>`;
  return `<div class="topbar"><div><h1>Drawing Library</h1><div class="sub">Current drawing revisions first — superseded drawings remain available for history</div></div><div class="actions">${hasPerm('manage_drawings')?'<button class="btn orange primary-action" onclick="addSiteDrawing()">＋ Upload Drawing</button>':''}</div></div><div class="panel" style="padding:15px"><div class="drawing-filterbar"><div class="drawing-search"><input class="input" placeholder="Search name, number, revision…" value="${drawingQuery}" oninput="drawingQuery=this.value;render()"><button class="btn light" onclick="drawingQuery='';render()">Clear</button></div><select class="select" onchange="setDrawingSiteFilter(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${drawingSite===s.id?'selected':''}>${s.name}</option>`).join('')}</select><button class="simple-filter-toggle" onclick="drawingMoreFilters=!drawingMoreFilters;render()">More filters ${drawingMoreFilters?'▲':'▼'}</button><div class="simple-filter-extra ${drawingMoreFilters?'show':''}"><select class="select" onchange="drawingHouseType=this.value;render()"><option value="all">All house types</option>${drawingHouseTypes().map(t=>`<option value="${t}" ${drawingHouseType===t?'selected':''}>${t}</option>`).join('')}</select><select class="select" onchange="drawingItem=this.value;render()"><option value="all">All work items</option>${items.map(x=>`<option value="${x.id}" ${drawingItem===x.id?'selected':''}>${x.ref} · ${x.type}</option>`).join('')}</select><label class="v169-show-old"><input type="checkbox" ${v169ShowSupersededDrawings?'checked':''} onchange="v169ShowSupersededDrawings=this.checked;render()"> Show superseded</label></div></div><div class="sectionhead drawing-all-head"><h3>Drawings</h3><span class="mini">${drawings.length}</span></div><div class="site-drawings-grid">${drawings.length?drawings.map(drawingCard).join(''):smartEmpty('No drawings match this view.','Change the filters, or upload a current drawing.',hasPerm('manage_drawings')?'＋ Upload Drawing':'',hasPerm('manage_drawings')?'addSiteDrawing()':'','▤')}</div></div>`
}
transferSiteDrawing=function(id,targetOverride=''){
  if(!hasPerm('manage_drawings'))return toast('You do not have permission to add drawings to work items');let d=db.siteDrawings.find(d=>d.id===id),targetId=targetOverride||document.getElementById('sdw-target')?.value||'',x=db.items.find(x=>x.id===targetId);if(!d||!x)return toast('Choose a work item');if(d.revisionStatus==='Superseded')return toast('Only the CURRENT drawing can be added to a Work Item');
  x.drawings=x.drawings||[];x.drawings.filter(old=>d.drawingNumber&&old.drawingNumber===d.drawingNumber&&old.revisionStatus!=='Superseded').forEach(old=>{old.revisionStatus='Superseded';old.supersededBy=d.id});
  x.drawings.push({id:'d'+Date.now(),name:d.name,originalName:d.fileName,notes:`Copied from Drawing Library${d.notes?' · '+d.notes:''}`,type:d.type,data:d.data,uploadedDate:iso(),sourceSiteDrawingId:d.id,drawingNumber:d.drawingNumber,revision:d.revision,issuedDate:d.issuedDate,revisionStatus:'Current',takeoffs:JSON.parse(JSON.stringify(d.takeoffs||[])),takeoffCalibration:d.takeoffCalibration?JSON.parse(JSON.stringify(d.takeoffCalibration)):null});touch(x);d.lastUsedAt=Date.now();save();audit('drawing','work item',x.id,`${x.ref}: CURRENT drawing added — ${d.drawingNumber} Rev ${d.revision}`,x.site);modal=null;savedState('Current drawing added');render()
}
openMatchingSiteDrawings=function(){
  if(!hasPerm('manage_drawings'))return toast('You do not have permission to add drawings');let x=db.items.find(x=>x.id===selected);if(!x)return;let a=db.siteDrawings.filter(d=>d.revisionStatus!=='Superseded'&&d.siteId===x.site&&(!x.houseType||d.houseType===x.houseType));
  modal=`<div class="modal"><div class="modalbox"><h2>Add CURRENT Drawing</h2><p class="muted">${x.ref} · ${siteName(x.site)}</p><div class="quick-manage-list">${a.length?a.map(d=>`<div class="quick-manage-row"><div><b>${contractorEsc(d.drawingNumber||d.name)} · Rev ${contractorEsc(d.revision||'—')}</b><span>${contractorEsc(d.name)} · ${contractorEsc(d.houseType)}</span></div><button class="btn blue smallbtn" onclick="transferSiteDrawing('${d.id}','${x.id}')">Add CURRENT</button></div>`).join(''):'<div class="empty">No current matching drawings found.</div>'}</div><button class="btn" onclick="modal=null;render()">Close</button></div></div>`;render()
}
uploadDrawing=function(){
  if(!hasPerm('manage_drawings'))return toast('You do not have drawing upload access');
  modal=`<div class="modal"><div class="modalbox"><h2>Upload Plot Drawing</h2><p class="muted">If the drawing number already exists on this Work Item, the previous revision will be marked SUPERSEDED automatically.</p><div class="field"><label>Drawing file</label><input id="df" class="input" type="file" accept=".pdf,image/*" onchange="suggestDrawingName()"></div>${cameraPicker('df','Photograph drawing','suggestDrawingName')}<div class="three"><div class="field"><label>Drawing number</label><input id="pd-number" class="input"></div><div class="field"><label>Revision</label><input id="pd-revision" class="input"></div><div class="field"><label>Issued date</label><input id="pd-issued" class="input" type="date" value="${iso()}"></div></div><div class="field"><label>Drawing name</label><input id="ddn" class="input" placeholder="e.g. Plot 12 Ground Floor Plan"></div><div class="field"><label>Notes</label><textarea id="dn" class="textarea"></textarea></div><button class="btn green" onclick="saveDrawing()">Upload CURRENT drawing</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
saveDrawing=function(){
  if(!hasPerm('manage_drawings'))return;let x=db.items.find(x=>x.id===selected),f=uploadFiles('df')[0],notes=document.getElementById('dn')?.value||'',displayName=(document.getElementById('ddn')?.value||'').trim(),
      drawingNumber=(document.getElementById('pd-number')?.value||'').trim(),revision=(document.getElementById('pd-revision')?.value||'').trim(),issuedDate=document.getElementById('pd-issued')?.value||iso();
  if(!f||!drawingNumber||!revision)return toast('Choose a file and enter drawing number and revision');let previous=(x.drawings||[]).filter(d=>d.drawingNumber===drawingNumber&&d.revisionStatus!=='Superseded');
  let d={id:'d'+Date.now(),name:displayName||f.name||'Drawing',originalName:f.name||'Drawing',notes,type:f.type||'image/jpeg',data:'',drawingNumber,revision,issuedDate,revisionStatus:'Current',uploadedDate:iso(),uploadedAt:new Date().toISOString(),uploadedBy:currentUser()?.name||'',storage:'browser',takeoffs:[],takeoffCalibration:null};
  storeDrawingInBrowser(f).then(data=>{d.data=data;previous.forEach(old=>{old.revisionStatus='Superseded';old.supersededBy=d.id});clearCameraFiles('df');x.drawings.push(d);touch(x);save();audit('drawing','work item',x.id,`${x.ref}: ${drawingNumber} Rev ${revision} uploaded CURRENT${previous.length?' · old revision superseded':''}`,x.site);modal=null;savedState('Current drawing saved');render()})
}
openDrawing=async function(i){
  let x=db.items.find(x=>x.id===selected),d=x?.drawings?.[i];if(!d||!d.data)return toast('Drawing file is unavailable');let src=await resolveMediaRef(d.data);if(!src)return toast('This drawing is stored in a different browser. Open SiteSight on the device where it was uploaded.');
  let isPdf=d.type==='application/pdf'||String(d.originalName||'').toLowerCase().endsWith('.pdf'),tot=takeoffTotals(d),current=d.revisionStatus==='Superseded'?(x.drawings||[]).find(y=>y.drawingNumber===d.drawingNumber&&y.revisionStatus==='Current'):null;
  modal=`<div class="work-drawing-fullscreen" onclick="if(event.target===this){modal=null;render()}"><div class="work-drawing-fullscreen-inner">${d.revisionStatus==='Superseded'?`<div class="v169-superseded-banner"><b>SUPERSEDED — DO NOT BUILD FROM THIS DRAWING</b><span>${current?`Current revision is ${contractorEsc(current.revision||'newer')}.`:''}</span>${current?`<button class="btn red smallbtn" onclick="openDrawing(${x.drawings.indexOf(current)})">Open CURRENT revision</button>`:''}</div>`:''}<div class="work-drawing-fullscreen-head"><div><b>${contractorEsc(d.drawingNumber||'')} ${d.drawingNumber?'· ':''}${contractorEsc(d.name||d.originalName||'Drawing')}</b>${v169DrawingMeta(d)}<div class="muted">${x.ref} · ${siteName(x.site)}</div>${(d.takeoffs||[]).length?`<div class="viewer-takeoff-line">📐 ${(d.takeoffs||[]).length} take-off item${(d.takeoffs||[]).length===1?'':'s'} · Net ${Math.max(0,tot.net).toFixed(2)} m²${hasPerm('view_financials')?` · ${money(tot.value)}`:''}</div>`:''}</div><div class="actions">${d.revisionStatus!=='Superseded'?`<button class="btn blue" onclick="openDrawingTakeoff('work','${d.id}','${x.id}')">📐 Measure Drawing</button>`:''}<button class="btn light" onclick="modal=null;render()">✕ Close</button></div></div><div class="work-drawing-fullscreen-body">${isPdf?`<iframe src="${src}" title="${contractorEsc(d.name||'Drawing')}"></iframe>`:`<img src="${src}" alt="${contractorEsc(d.name||'Drawing')}">`}</div></div></div>`;render()
}

// ---- Self-constructed Site Diary ------------------------------------
function v169DiaryPrefill(siteId,date=iso()){
  let shifts=(db.attendanceShifts||[]).filter(s=>s.siteId===siteId&&!s.voided&&attendanceDateOnly(s.clockIn?.time,s.siteId)===date),people=[...new Set(shifts.map(s=>s.workerId))],
      teamCounts=new Map();people.forEach(id=>{let wm=attendanceMemberById(id);if(wm)teamCounts.set(wm.gang.name,(teamCounts.get(wm.gang.name)||0)+1)});
  let items=(db.items||[]).filter(x=>x.site===siteId&&x.lastUpdated===date),progress=items.map(x=>`${x.ref} — ${overall(x)}`),
      raised=(db.items||[]).filter(x=>x.site===siteId).flatMap(x=>(x.issues||[]).filter(i=>i.created===date).map(i=>`${x.ref} — ${i.title}`)),
      resolved=(db.items||[]).filter(x=>x.site===siteId).flatMap(x=>(x.issues||[]).filter(i=>i.resolved===date).map(i=>`${x.ref} — resolved: ${i.title}`)),
      dayworks=(db.dayworks||[]).filter(d=>d.siteId===siteId&&d.date===date).map(d=>`${d.area||'Daywork'} — ${d.work}`),
      notes=(db.calendarNotes||[]).filter(n=>n.siteId===siteId&&n.date===date),
      deliveries=notes.filter(n=>['Delivery','Scaffold'].includes(n.category)).map(n=>`${n.category}: ${n.title}${n.notes?' — '+n.notes:''}`),
      inspections=notes.filter(n=>['Inspection','Meeting','Other Site Event'].includes(n.category)).map(n=>`${n.category}: ${n.title}`),
      peopleSummary=contractorPeopleSummary(date,siteId);
  return {teams:[...teamCounts.entries()].map(([n,c])=>`${n} (${c})`).join(', ')||`${people.length} people recorded`,
    work:[...progress,...dayworks].join('\n'),deliveries:deliveries.join('\n'),
    delays:raised.map(x=>`Issue: ${x}`).join('\n'),notes:[...resolved,...inspections,peopleSummary.holidayIds.size?`${peopleSummary.holidayIds.size} holiday`:'' ,peopleSummary.sickIds.size?`${peopleSummary.sickIds.size} sick`:'' ].filter(Boolean).join('\n')}
}
addDiaryEntry=function(){
  if(!hasPerm('manage_site_diary'))return toast('You do not have permission to manage the Site Diary');let defaultSite=site==='all'?(activeSites()[0]?.id||''):site,date=iso(),p=v169DiaryPrefill(defaultSite,date);
  modal=`<div class="modal"><div class="modalbox"><h2>Add Site Diary Entry</h2><div class="v169-autofill-note"><b>SiteSight has prepared the base diary from today's records.</b><span>Review it, add anything missing, then save. You remain in control of the final diary.</span></div><div class="two"><div class="field"><label>Date</label><input id="sd-date" class="input" type="date" value="${date}" onchange="v169RefreshDiaryPrefill()"></div><div class="field"><label>Site</label><select id="sd-site" class="select" onchange="v169RefreshDiaryPrefill()">${activeSites().map(s=>`<option value="${s.id}" ${s.id===defaultSite?'selected':''}>${s.name}</option>`).join('')}</select></div></div>
  <div class="field"><label>Teams on site</label><input id="sd-teams" class="input" value="${contractorEsc(p.teams)}"></div><div class="field"><label>Work completed / progressed</label><textarea id="sd-work" class="textarea">${contractorEsc(p.work)}</textarea></div><div class="field"><label>Deliveries / scaffold</label><textarea id="sd-del" class="textarea">${contractorEsc(p.deliveries)}</textarea></div><div class="field"><label>Problems / delays</label><textarea id="sd-delay" class="textarea">${contractorEsc(p.delays)}</textarea></div><div class="field"><label>General notes</label><textarea id="sd-notes" class="textarea">${contractorEsc(p.notes)}</textarea></div><div class="field"><label>Photos</label><input id="sd-photos" class="input" type="file" accept="image/*" multiple></div>${cameraPicker('sd-photos','Take site photo')}<button class="btn orange" onclick="saveDiaryEntry()">Save diary</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function v169RefreshDiaryPrefill(){
  let sid=document.getElementById('sd-site')?.value||'',date=document.getElementById('sd-date')?.value||iso(),p=v169DiaryPrefill(sid,date);
  [['sd-teams',p.teams],['sd-work',p.work],['sd-del',p.deliveries],['sd-delay',p.delays],['sd-notes',p.notes]].forEach(([id,v])=>{let e=document.getElementById(id);if(e)e.value=v})
}

/* ===== v171-drawing-highlights.js ===== */
// ============================================================
// SiteSight V1.71 — Drawing Highlights
// Simple visual markup on drawings: draw a translucent box,
// optionally add a short note, and retain who/when in Audit.
// ============================================================

let v171HighlightContext=null,
    v171HighlightEditing=false,
    v171HighlightColor='yellow',
    v171HighlightStart=null,
    v171HighlightDraft=null,
    v171HighlightsVisible=true,
    v171HighlightPanelOpen=false,
    v171ResizeObserver=null;

function v171Clamp(v,min=0,max=1){v=Number(v);return Math.max(min,Math.min(max,Number.isFinite(v)?v:0))}
function v171NormaliseHighlightRect(x1,y1,x2,y2){
  let x=Math.min(x1,x2),y=Math.min(y1,y2),w=Math.abs(x2-x1),h=Math.abs(y2-y1);
  return {x:v171Clamp(x),y:v171Clamp(y),w:v171Clamp(w,0,1-v171Clamp(x)),h:v171Clamp(h,0,1-v171Clamp(y))}
}
function v171HighlightColourName(c){return c==='red'?'Attention':c==='blue'?'Information':'Relevant'}
function v171DrawingByContext(){
  if(!v171HighlightContext)return null;
  if(v171HighlightContext.scope==='library')return (db.siteDrawings||[]).find(d=>d.id===v171HighlightContext.drawingId)||null;
  let x=(db.items||[]).find(x=>x.id===v171HighlightContext.itemId);
  return (x?.drawings||[]).find(d=>d.id===v171HighlightContext.drawingId)||null
}
function v171ContextSiteId(){
  if(!v171HighlightContext)return '';
  if(v171HighlightContext.scope==='library')return v171DrawingByContext()?.siteId||'';
  return (db.items||[]).find(x=>x.id===v171HighlightContext.itemId)?.site||''
}
function v171ContextLabel(){
  let d=v171DrawingByContext();if(!d)return 'Drawing';
  return `${d.drawingNumber?d.drawingNumber+' · ':''}${d.name||d.originalName||'Drawing'}${d.revision?' · Rev '+d.revision:''}`
}
function v171DrawingHighlights(d){return Array.isArray(d?.highlights)?d.highlights:[]}
function v171HighlightStyle(h){
  let c=h.color||'yellow';
  return c==='red'?'background:rgba(235,72,72,.20);border-color:#ce3f3f':
         c==='blue'?'background:rgba(61,130,235,.20);border-color:#3171c8':
         'background:rgba(255,215,0,.28);border-color:#d8ad00'
}
function v171HighlightBoxes(d){
  if(!v171HighlightsVisible)return '';
  return v171DrawingHighlights(d).map((h,i)=>`<div class="v171-highlight-box ${h.color||'yellow'}" style="left:${Number(h.x||0)*100}%;top:${Number(h.y||0)*100}%;width:${Number(h.w||0)*100}%;height:${Number(h.h||0)*100}%;${v171HighlightStyle(h)}" title="${contractorEsc(h.note||v171HighlightColourName(h.color))}"><span>${i+1}</span></div>`).join('')
}
function v171HighlightList(d){
  let hs=v171DrawingHighlights(d);
  if(!hs.length)return `<div class="v171-highlight-empty">No visual highlights on this drawing.</div>`;
  return hs.map((h,i)=>`<div class="v171-highlight-list-row"><i class="${h.color||'yellow'}"></i><div><b>${i+1}. ${contractorEsc(h.note||v171HighlightColourName(h.color))}</b><span>${contractorEsc(h.by||'')} ${h.createdAt?'· '+new Date(h.createdAt).toLocaleString():''}</span></div>${hasPerm('manage_drawings')&&d.revisionStatus!=='Superseded'?`<button onclick="v171RemoveHighlight('${h.id}')" title="Remove highlight">×</button>`:''}</div>`).join('')
}
function v171ViewerTools(d,isPdf=false){
  let count=v171DrawingHighlights(d).length,manage=hasPerm('manage_drawings')&&d.revisionStatus!=='Superseded';
  return `<div class="v171-viewer-tools">${manage?`<button class="btn orange" onclick="v171BeginHighlight()">🖍 Highlight</button><button class="btn light" onclick="v171OpenDrawingNote()">✎ Drawing Note</button>`:''}${count?`<button class="btn light" onclick="v171ToggleHighlightPanel()">Highlights (${count})</button><button class="btn light" onclick="v171ToggleHighlightVisibility()">${v171HighlightsVisible?'Hide':'Show'} Highlights</button>`:''}${isPdf&&manage?`<span class="v171-pdf-caution">PDF highlights attach to the visible PDF view. Keep page and zoom fixed while marking it.</span>`:''}</div>`
}
function v171ViewerOverlay(d){
  return `<div id="v171-highlight-layer" class="v171-highlight-layer ${v171HighlightEditing?'editing':''}" onpointerdown="v171HighlightPointerDown(event)" onpointermove="v171HighlightPointerMove(event)" onpointerup="v171HighlightPointerUp(event)" onpointercancel="v171CancelDraft()">${v171HighlightBoxes(d)}<div id="v171-highlight-draft" class="v171-highlight-draft" style="display:none"></div></div>
  <div id="v171-highlight-toolbar" class="v171-highlight-toolbar" style="display:${v171HighlightEditing?'flex':'none'}"><b>Drag a box over the relevant area</b><button class="${v171HighlightColor==='yellow'?'active':''}" onclick="v171SetHighlightColor('yellow')"><i class="yellow"></i>Relevant</button><button class="${v171HighlightColor==='red'?'active':''}" onclick="v171SetHighlightColor('red')"><i class="red"></i>Attention</button><button class="${v171HighlightColor==='blue'?'active':''}" onclick="v171SetHighlightColor('blue')"><i class="blue"></i>Info</button><button class="done" onclick="v171EndHighlight()">Done</button></div>
  <aside id="v171-highlight-panel" class="v171-highlight-panel ${v171HighlightPanelOpen?'open':''}"><div class="v171-highlight-panel-head"><div><span>DRAWING MARKUP</span><b>Highlights</b></div><button onclick="v171ToggleHighlightPanel()">×</button></div><div class="v171-highlight-panel-body">${v171HighlightList(d)}</div></aside>
  <div id="v171-inline-editor" class="v171-inline-editor"></div>`
}
function v171PositionLayer(){
  let media=document.getElementById('v171-drawing-media'),layer=document.getElementById('v171-highlight-layer'),host=media?.parentElement;
  if(!media||!layer||!host)return;
  layer.style.left=media.offsetLeft+'px';layer.style.top=media.offsetTop+'px';layer.style.width=media.offsetWidth+'px';layer.style.height=media.offsetHeight+'px';
}
function v171BindLayer(){
  setTimeout(()=>{v171PositionLayer();let media=document.getElementById('v171-drawing-media');if(v171ResizeObserver){try{v171ResizeObserver.disconnect()}catch(e){}}
    if(window.ResizeObserver&&media){v171ResizeObserver=new ResizeObserver(()=>v171PositionLayer());v171ResizeObserver.observe(media)}},30)
}
function v171SetHighlightContext(scope,drawingId,itemId=''){
  v171HighlightContext={scope,drawingId,itemId};v171HighlightEditing=false;v171HighlightStart=null;v171HighlightDraft=null;v171HighlightPanelOpen=false
}
function v171BeginHighlight(){
  let d=v171DrawingByContext();if(!d||!hasPerm('manage_drawings')||d.revisionStatus==='Superseded')return toast('Only current drawings can be highlighted');
  v171HighlightEditing=true;let layer=document.getElementById('v171-highlight-layer'),bar=document.getElementById('v171-highlight-toolbar');
  layer?.classList.add('editing');if(bar)bar.style.display='flex';v171PositionLayer()
}
function v171EndHighlight(){
  v171HighlightEditing=false;v171HighlightStart=null;v171CancelDraft();document.getElementById('v171-highlight-layer')?.classList.remove('editing');let bar=document.getElementById('v171-highlight-toolbar');if(bar)bar.style.display='none'
}
function v171SetHighlightColor(c){
  v171HighlightColor=['yellow','red','blue'].includes(c)?c:'yellow';
  document.querySelectorAll('#v171-highlight-toolbar button').forEach(b=>b.classList.remove('active'));
  let buttons=[...document.querySelectorAll('#v171-highlight-toolbar button')],match=buttons.find(b=>String(b.getAttribute('onclick')||'').includes(`'${v171HighlightColor}'`));match?.classList.add('active')
}
function v171Point(ev){
  let layer=document.getElementById('v171-highlight-layer'),r=layer?.getBoundingClientRect();if(!r||!r.width||!r.height)return {x:0,y:0};
  return {x:v171Clamp((ev.clientX-r.left)/r.width),y:v171Clamp((ev.clientY-r.top)/r.height)}
}
function v171HighlightPointerDown(ev){
  if(!v171HighlightEditing||v171HighlightDraft)return;ev.preventDefault();let layer=ev.currentTarget;
  try{layer.setPointerCapture(ev.pointerId)}catch(e){}
  v171HighlightStart=v171Point(ev);let draft=document.getElementById('v171-highlight-draft');if(draft){draft.style.display='block';draft.className=`v171-highlight-draft ${v171HighlightColor}`;draft.style.left=(v171HighlightStart.x*100)+'%';draft.style.top=(v171HighlightStart.y*100)+'%';draft.style.width='0';draft.style.height='0'}
}
function v171HighlightPointerMove(ev){
  if(!v171HighlightEditing||!v171HighlightStart||v171HighlightDraft)return;ev.preventDefault();let p=v171Point(ev),rect=v171NormaliseHighlightRect(v171HighlightStart.x,v171HighlightStart.y,p.x,p.y),draft=document.getElementById('v171-highlight-draft');
  if(draft){draft.style.left=(rect.x*100)+'%';draft.style.top=(rect.y*100)+'%';draft.style.width=(rect.w*100)+'%';draft.style.height=(rect.h*100)+'%'}
}
function v171HighlightPointerUp(ev){
  if(!v171HighlightEditing||!v171HighlightStart||v171HighlightDraft)return;ev.preventDefault();let p=v171Point(ev),rect=v171NormaliseHighlightRect(v171HighlightStart.x,v171HighlightStart.y,p.x,p.y);v171HighlightStart=null;
  if(rect.w<.012||rect.h<.012){v171CancelDraft();return toast('Drag over a slightly larger area to create the highlight')}
  v171HighlightDraft={...rect,color:v171HighlightColor};v171OpenHighlightEditor()
}
function v171CancelDraft(){
  v171HighlightStart=null;v171HighlightDraft=null;let draft=document.getElementById('v171-highlight-draft');if(draft)draft.style.display='none';let ed=document.getElementById('v171-inline-editor');if(ed){ed.innerHTML='';ed.classList.remove('open')}
}
function v171OpenHighlightEditor(){
  let ed=document.getElementById('v171-inline-editor');if(!ed||!v171HighlightDraft)return;
  ed.innerHTML=`<div class="v171-editor-card"><span class="att-kicker">NEW HIGHLIGHT</span><h3>${v171HighlightColourName(v171HighlightDraft.color)}</h3><div class="field"><label>Short note <span class="muted">(optional)</span></label><input id="v171-highlight-note" class="input" maxlength="120" placeholder="e.g. Revised opening / check pier / important detail"></div><div class="actions"><button class="btn orange" onclick="v171SaveHighlight()">Save highlight</button><button class="btn light" onclick="v171CancelDraft()">Cancel</button></div></div>`;ed.classList.add('open');setTimeout(()=>document.getElementById('v171-highlight-note')?.focus(),30)
}
function v171SaveHighlight(){
  let d=v171DrawingByContext();if(!d||!v171HighlightDraft)return;d.highlights=Array.isArray(d.highlights)?d.highlights:[];
  let note=(document.getElementById('v171-highlight-note')?.value||'').trim(),h={id:'hl-'+Date.now()+'-'+Math.random().toString(16).slice(2),...v171HighlightDraft,note,by:currentUser()?.name||'',userId:currentUser()?.id||'',createdAt:new Date().toISOString()};
  d.highlights.push(h);let siteId=v171ContextSiteId();save();audit('drawing','drawing highlight',h.id,`${v171ContextLabel()}: ${v171HighlightColourName(h.color)} highlight added${note?' — '+note:''}`,siteId);
  v171HighlightDraft=null;savedState('Drawing highlight saved');v171ReopenViewer(true)
}
function v171RemoveHighlight(id){
  let d=v171DrawingByContext();if(!d||!hasPerm('manage_drawings')||d.revisionStatus==='Superseded')return;
  let h=v171DrawingHighlights(d).find(x=>x.id===id);if(!h)return;if(!confirm(`Remove highlight${h.note?' “'+h.note+'”':''}?`))return;
  d.highlights=d.highlights.filter(x=>x.id!==id);save();audit('drawing','drawing highlight',id,`${v171ContextLabel()}: highlight removed${h.note?' — '+h.note:''}`,v171ContextSiteId());savedState('Highlight removed');v171ReopenViewer(true)
}
function v171OpenDrawingNote(){
  let d=v171DrawingByContext();if(!d||!hasPerm('manage_drawings')||d.revisionStatus==='Superseded')return;
  let ed=document.getElementById('v171-inline-editor');if(!ed)return;
  ed.innerHTML=`<div class="v171-editor-card"><span class="att-kicker">DRAWING NOTE</span><h3>${v171ContextLabel()}</h3><div class="field"><label>Drawing note</label><textarea id="v171-drawing-note" class="textarea" maxlength="1000" placeholder="Useful note about this drawing...">${contractorEsc(d.notes||'')}</textarea></div><div class="actions"><button class="btn blue" onclick="v171SaveDrawingNote()">Save note</button><button class="btn light" onclick="v171CloseInlineEditor()">Cancel</button></div></div>`;ed.classList.add('open');setTimeout(()=>document.getElementById('v171-drawing-note')?.focus(),30)
}
function v171SaveDrawingNote(){
  let d=v171DrawingByContext();if(!d)return;let old=d.notes||'',note=(document.getElementById('v171-drawing-note')?.value||'').trim();d.notes=note;save();audit('drawing','drawing note',d.id,`${v171ContextLabel()}: drawing note ${old?'updated':'added'}${note?' — '+note.slice(0,100):' — cleared'}`,v171ContextSiteId());savedState('Drawing note saved');v171ReopenViewer(v171HighlightPanelOpen)
}
function v171CloseInlineEditor(){let ed=document.getElementById('v171-inline-editor');if(ed){ed.innerHTML='';ed.classList.remove('open')}}
function v171ToggleHighlightPanel(){
  v171HighlightPanelOpen=!v171HighlightPanelOpen;document.getElementById('v171-highlight-panel')?.classList.toggle('open',v171HighlightPanelOpen)
}
function v171ToggleHighlightVisibility(){
  v171HighlightsVisible=!v171HighlightsVisible;let layer=document.getElementById('v171-highlight-layer');if(layer)layer.style.visibility=v171HighlightsVisible?'visible':'hidden';
  let buttons=[...document.querySelectorAll('.v171-viewer-tools button')],b=buttons.find(x=>/Highlights$/.test(x.textContent||''));if(b)b.textContent=v171HighlightsVisible?'Hide Highlights':'Show Highlights'
}
function v171ReopenViewer(openPanel=false){
  let ctx=v171HighlightContext;if(!ctx)return;v171HighlightEditing=false;v171HighlightPanelOpen=!!openPanel;
  if(ctx.scope==='library')openSiteDrawing(ctx.drawingId);
  else{let x=db.items.find(x=>x.id===ctx.itemId),i=(x?.drawings||[]).findIndex(d=>d.id===ctx.drawingId);if(i>=0){selected=ctx.itemId;openDrawing(i)}}
}
function v171ViewerBody(mediaHtml,d){
  return `<div class="v171-drawing-stage">${mediaHtml}${v171ViewerOverlay(d)}</div>`
}

// Ensure old data gets a highlight collection without disturbing anything else.
const v171BaseMigrate=migrate;
migrate=function(persist=true){
  v171BaseMigrate(false);
  db.siteDrawings=(db.siteDrawings||[]).map(d=>({...d,highlights:Array.isArray(d.highlights)?d.highlights:[]}));
  db.items=(db.items||[]).map(x=>({...x,drawings:(x.drawings||[]).map(d=>({...d,highlights:Array.isArray(d.highlights)?d.highlights:[]}))}));
  if(persist)save()
}

// Show highlight count wherever drawing metadata is already used.
const v171BaseDrawingMeta=v169DrawingMeta;
v169DrawingMeta=function(d){return v171BaseDrawingMeta(d)+(v171DrawingHighlights(d).length?`<span class="v171-highlight-count">🖍 ${v171DrawingHighlights(d).length} highlight${v171DrawingHighlights(d).length===1?'':'s'}</span>`:'')}

// New library revisions start clean; annotations remain with the old revision.
v169SaveDrawingRevision=function(id){
  let old=db.siteDrawings.find(x=>x.id===id),f=uploadFiles('rev-file')[0],revision=(document.getElementById('rev-new')?.value||'').trim(),issuedDate=document.getElementById('rev-issued')?.value||iso(),note=(document.getElementById('rev-note')?.value||'').trim();if(!old||!f||!revision)return toast('New revision and drawing file are required');
  let d={...old,id:'sdw'+Date.now(),revision,issuedDate,revisionStatus:'Current',supersededBy:'',supersededAt:'',supersededByUser:'',notes:note||old.notes,fileName:f.name||old.fileName,type:f.type||old.type,data:'',uploadedDate:iso(),uploadedAt:new Date().toISOString(),uploadedBy:currentUser()?.name||'',lastUsedAt:Date.now(),takeoffs:[],takeoffCalibration:null,highlights:[]};
  storeDrawingInBrowser(f).then(data=>{d.data=data;db.siteDrawings.filter(x=>x.siteId===old.siteId&&x.drawingNumber===old.drawingNumber&&x.revisionStatus!=='Superseded').forEach(x=>{x.revisionStatus='Superseded';x.supersededBy=d.id;x.supersededAt=new Date().toISOString();x.supersededByUser=currentUser()?.name||''});db.siteDrawings.push(d);save();audit('drawing','drawing library',d.id,`${d.drawingNumber}: Revision ${revision} uploaded · previous revision superseded`,d.siteId);modal=null;savedState('New drawing revision saved');render()})
}

// Copy useful visual markup when a current master drawing is allocated to a Work Item.
transferSiteDrawing=function(id,targetOverride=''){
  if(!hasPerm('manage_drawings'))return toast('You do not have permission to add drawings to work items');let d=db.siteDrawings.find(d=>d.id===id),targetId=targetOverride||document.getElementById('sdw-target')?.value||'',x=db.items.find(x=>x.id===targetId);if(!d||!x)return toast('Choose a work item');if(d.revisionStatus==='Superseded')return toast('Only the CURRENT drawing can be added to a Work Item');
  x.drawings=x.drawings||[];x.drawings.filter(old=>d.drawingNumber&&old.drawingNumber===d.drawingNumber&&old.revisionStatus!=='Superseded').forEach(old=>{old.revisionStatus='Superseded';old.supersededBy=d.id});
  x.drawings.push({id:'d'+Date.now(),name:d.name,originalName:d.fileName,notes:`Copied from Drawing Library${d.notes?' · '+d.notes:''}`,type:d.type,data:d.data,uploadedDate:iso(),sourceSiteDrawingId:d.id,drawingNumber:d.drawingNumber,revision:d.revision,issuedDate:d.issuedDate,revisionStatus:'Current',takeoffs:JSON.parse(JSON.stringify(d.takeoffs||[])),takeoffCalibration:d.takeoffCalibration?JSON.parse(JSON.stringify(d.takeoffCalibration)):null,highlights:JSON.parse(JSON.stringify(d.highlights||[]))});
  touch(x);d.lastUsedAt=Date.now();save();audit('drawing','work item',x.id,`${x.ref}: CURRENT drawing added — ${d.drawingNumber} Rev ${d.revision}`,x.site);modal=null;savedState('Current drawing added');render()
}

// Fullscreen library viewer with live highlight overlay.
openSiteDrawing=async function(id){
  let d=(db.siteDrawings||[]).find(d=>d.id===id);if(!d)return;v171SetHighlightContext('library',d.id,'');d.lastUsedAt=Date.now();save();if(!d.data)return toast('Drawing file is unavailable');let src=await resolveMediaRef(d.data);if(!src)return toast('This drawing is stored in a different browser. Open SiteSight on the device where it was uploaded.');
  let isPdf=d.type==='application/pdf'||String(d.fileName||'').toLowerCase().endsWith('.pdf'),tot=takeoffTotals(d),current=d.revisionStatus==='Superseded'?(db.siteDrawings||[]).find(x=>x.id===d.supersededBy)||(db.siteDrawings||[]).find(x=>x.siteId===d.siteId&&x.drawingNumber===d.drawingNumber&&x.revisionStatus==='Current'):null,
      media=isPdf?`<iframe id="v171-drawing-media" src="${src}" title="${contractorEsc(d.name)}" onload="v171PositionLayer()"></iframe>`:`<img id="v171-drawing-media" src="${src}" alt="${contractorEsc(d.name)}" onload="v171PositionLayer()">`;
  modal=`<div class="drawing-fullscreen" onclick="if(event.target===this){modal=null;render()}"><div class="drawing-fullscreen-inner v171-viewer">${d.revisionStatus==='Superseded'?`<div class="v169-superseded-banner"><b>SUPERSEDED — DO NOT BUILD FROM THIS DRAWING</b><span>${current?`Current revision is ${contractorEsc(current.revision||'newer')}.`:'A newer revision has been issued.'}</span>${current?`<button class="btn red smallbtn" onclick="openSiteDrawing('${current.id}')">Open CURRENT revision</button>`:''}</div>`:''}
  <div class="drawing-fullscreen-head"><div><b>${contractorEsc(d.drawingNumber||'')} ${d.drawingNumber?'· ':''}${contractorEsc(d.name)}</b>${v169DrawingMeta(d)}<div class="muted">${siteName(d.siteId)} · ${d.houseType}</div>${d.notes?`<div class="v171-drawing-note-preview">✎ ${contractorEsc(d.notes)}</div>`:''}${(d.takeoffs||[]).length?`<div class="viewer-takeoff-line">📐 ${(d.takeoffs||[]).length} take-off item${(d.takeoffs||[]).length===1?'':'s'} · Net ${Math.max(0,tot.net).toFixed(2)} m²${hasPerm('view_financials')?` · ${money(tot.value)}`:''}</div>`:''}${v171ViewerTools(d,isPdf)}</div><div class="actions">${d.revisionStatus!=='Superseded'?`<button class="btn blue" onclick="openDrawingTakeoff('library','${d.id}')">📐 Measure Drawing</button>`:''}<button class="btn light" onclick="modal=null;render()">✕ Close</button></div></div>
  <div class="drawing-fullscreen-body v171-drawing-host">${v171ViewerBody(media,d)}</div></div></div>`;render();v171BindLayer()
}

// Fullscreen Work Item drawing viewer with the same markup workflow.
openDrawing=async function(i){
  let x=db.items.find(x=>x.id===selected),d=x?.drawings?.[i];if(!d||!d.data)return toast('Drawing file is unavailable');v171SetHighlightContext('work',d.id,x.id);let src=await resolveMediaRef(d.data);if(!src)return toast('This drawing is stored in a different browser. Open SiteSight on the device where it was uploaded.');
  let isPdf=d.type==='application/pdf'||String(d.originalName||'').toLowerCase().endsWith('.pdf'),tot=takeoffTotals(d),current=d.revisionStatus==='Superseded'?(x.drawings||[]).find(y=>y.drawingNumber===d.drawingNumber&&y.revisionStatus==='Current'):null,
      media=isPdf?`<iframe id="v171-drawing-media" src="${src}" title="${contractorEsc(d.name||'Drawing')}" onload="v171PositionLayer()"></iframe>`:`<img id="v171-drawing-media" src="${src}" alt="${contractorEsc(d.name||'Drawing')}" onload="v171PositionLayer()">`;
  modal=`<div class="work-drawing-fullscreen" onclick="if(event.target===this){modal=null;render()}"><div class="work-drawing-fullscreen-inner v171-viewer">${d.revisionStatus==='Superseded'?`<div class="v169-superseded-banner"><b>SUPERSEDED — DO NOT BUILD FROM THIS DRAWING</b><span>${current?`Current revision is ${contractorEsc(current.revision||'newer')}.`:''}</span>${current?`<button class="btn red smallbtn" onclick="openDrawing(${x.drawings.indexOf(current)})">Open CURRENT revision</button>`:''}</div>`:''}
  <div class="work-drawing-fullscreen-head"><div><b>${contractorEsc(d.drawingNumber||'')} ${d.drawingNumber?'· ':''}${contractorEsc(d.name||d.originalName||'Drawing')}</b>${v169DrawingMeta(d)}<div class="muted">${x.ref} · ${siteName(x.site)}</div>${d.notes?`<div class="v171-drawing-note-preview">✎ ${contractorEsc(d.notes)}</div>`:''}${(d.takeoffs||[]).length?`<div class="viewer-takeoff-line">📐 ${(d.takeoffs||[]).length} take-off item${(d.takeoffs||[]).length===1?'':'s'} · Net ${Math.max(0,tot.net).toFixed(2)} m²${hasPerm('view_financials')?` · ${money(tot.value)}`:''}</div>`:''}${v171ViewerTools(d,isPdf)}</div><div class="actions">${d.revisionStatus!=='Superseded'?`<button class="btn blue" onclick="openDrawingTakeoff('work','${d.id}','${x.id}')">📐 Measure Drawing</button>`:''}<button class="btn light" onclick="modal=null;render()">✕ Close</button></div></div>
  <div class="work-drawing-fullscreen-body v171-drawing-host">${v171ViewerBody(media,d)}</div></div></div>`;render();v171BindLayer()
}

/* ===== v172-operative-access.js ===== */
// ============================================================
// SiteSight V1.72 — Operative Access Control
// Prevents former / suspended / moved operatives from self-clocking
// on Sites where they are no longer authorised.
// ============================================================

function v172LinkedUserForMember(m){
  if(!m)return null;
  return (db.users||[]).find(u=>u.id===m.linkedUserId||(u.linkedMemberId||'')===m.id)||null
}
function v172AssignedSitesForGang(gangId){
  return [...new Set((db.items||[]).filter(x=>x.gang===gangId&&!db.sites.find(s=>s.id===x.site)?.archived).map(x=>x.site).filter(Boolean))]
}
function v172DefaultClockSites(g,m){
  let u=v172LinkedUserForMember(m), userSites=(u?.allowedSites||[]).filter(id=>db.sites.some(s=>s.id===id&&!s.archived)),
      assigned=v172AssignedSitesForGang(g.id);
  if(userSites.length)return userSites;
  if(assigned.length)return assigned;
  return []
}
function v172MemberClockStatus(m){
  if(!m)return 'suspended';
  if(m.clockAccessStatus)return m.clockAccessStatus;
  if(m.employmentStatus==='Leaver')return 'left';
  if(m.activeForAttendance===false)return 'suspended';
  return 'active'
}
function v172ClockSiteIds(m,g){
  return Array.isArray(m?.clockSiteIds)?m.clockSiteIds.filter(id=>db.sites.some(s=>s.id===id&&!s.archived)):v172DefaultClockSites(g,m)
}
function v172CanClockAt(member,gang,siteId){
  if(!member||v172MemberClockStatus(member)!=='active'||member.activeForAttendance===false||member.employmentStatus==='Leaver'||member.employmentStatus==='Inactive')return false;
  let ids=v172ClockSiteIds(member,gang);return ids.includes(siteId)
}
function v172ClockAccessSummary(m,g){
  let status=v172MemberClockStatus(m),sites=v172ClockSiteIds(m,g).map(siteName).filter(Boolean),u=v172LinkedUserForMember(m);
  return {status,sites,user:u,loginActive:u?u.accountActive!==false:false}
}
function v172AccessStatusBadge(status){
  if(status==='left')return '<span class="badge b-red">ACCESS REVOKED</span>';
  if(status==='suspended')return '<span class="badge b-amber">CLOCKING SUSPENDED</span>';
  return '<span class="badge b-green">CLOCKING ACTIVE</span>'
}

// ---- Migration -------------------------------------------------------
const v172BaseMigrate=migrate;
migrate=function(persist=true){
  v172BaseMigrate(false);
  db.users=(db.users||[]).map(u=>({...u,
    accountActive:u.accountActive!==false,
    disabledReason:u.disabledReason||'',
    disabledAt:u.disabledAt||'',
    disabledBy:u.disabledBy||''
  }));
  db.gangs=(db.gangs||[]).map(g=>({...g,members:(g.members||[]).map(m=>{
    let status=m.clockAccessStatus||(m.employmentStatus==='Leaver'?'left':m.activeForAttendance===false?'suspended':'active'),
        ids=Array.isArray(m.clockSiteIds)?m.clockSiteIds:v172DefaultClockSites(g,m);
    return {...m,clockAccessStatus:status,clockSiteIds:ids,clockAccessUpdatedAt:m.clockAccessUpdatedAt||'',clockAccessUpdatedBy:m.clockAccessUpdatedBy||'',clockAccessReason:m.clockAccessReason||''}
  })}));
  if(persist)save()
}

// Historical dates remain visible for leavers so their final pay history is not lost.
v169MemberActiveOnDate=function(m,date){
  if(!m||m.activeForAttendance===false&&v172MemberClockStatus(m)!=='left')return false;
  if(m.startDate&&date<m.startDate)return false;
  if(m.leavingDate&&date>m.leavingDate)return false;
  if(m.employmentStatus==='Leaver'&&!m.leavingDate)return false;
  if(m.employmentStatus==='Inactive'&&v172MemberClockStatus(m)!=='active')return false;
  let dow=new Date(date+'T12:00:00').getDay(),days=(m.usualDays&&m.usualDays.length?m.usualDays:[1,2,3,4,5]).map(Number);
  return days.includes(dow)
}

// ---- Disabled account login / active-session guard -------------------
login=function(){
  let un=(document.getElementById('login-user')?.value||'').trim().toLowerCase(),pw=document.getElementById('login-pass')?.value||'',
      u=db.users.find(u=>(u.username||'').toLowerCase()===un&&u.password===pw);
  if(!u)return toast('Incorrect username or password');
  if(u.accountActive===false)return toast('This SiteSight account has been disabled. Contact a manager.');
  currentUserId=u.id;try{localStorage.setItem(CURRENT_USER_KEY,u.id)}catch(e){};role=u.role;trainingMode=!!u.trainingMode;u.loginCount=Number(u.loginCount||0)+1;u.lastLoginAt=new Date().toISOString();save();
  if(site!=='all'&&!canAccessSite(site))site='all';selected=null;selectedGang=null;guideActive=false;guideStep=0;
  if(!u.onboardingSeen){openUserGuide(true)}else{view=hasPerm('dashboard')?'dashboard':hasPerm('work_items')?'work-items':'more';render()}setTimeout(deliverNotifications,100)
}
const v172BaseRender=render;
render=function(){
  let u=db.users?.find(x=>x.id===currentUserId);
  if(u&&u.accountActive===false){
    currentUserId='';role='';selected=null;selectedGang=null;modal=null;
    try{localStorage.removeItem(CURRENT_USER_KEY)}catch(e){}
  }
  return v172BaseRender()
}

// ---- Self-clocking UI is restricted to explicit operative Sites -----
function v172SelfClockSites(linked){
  if(!linked)return [];
  let permitted=new Set(v172ClockSiteIds(linked.member,linked.gang));
  let user=currentUser(),userSites=(user?.allowedSites||[]);
  return activeSites().filter(s=>permitted.has(s.id)&&(!userSites.length||userSites.includes(s.id)))
}
attendanceTodayCard=function(){
  let linked=attendanceLinkedMember();if(!linked||!hasPerm('clock_attendance'))return '';
  let active=attendanceActiveShift(linked.member.id),summary=v172ClockAccessSummary(linked.member,linked.gang);
  if(summary.status!=='active'){
    return `<div class="panel att-my-clock v172-clock-blocked"><div><span class="att-kicker">MY TIME</span><h2>${summary.status==='left'?'Clocking access removed':'Clocking suspended'}</h2><p>${linked.member.name} · ${linked.gang.name}<br>${summary.status==='left'?'You are no longer authorised to clock in for this company.':'A manager has temporarily removed your clocking access.'}</p></div>${v172AccessStatusBadge(summary.status)}</div>`
  }
  if(active)return `<div class="panel att-my-clock"><div><span class="att-kicker">MY TIME</span><h2>You are clocked in</h2><p>${linked.member.name} · ${linked.gang.name}<br>${siteName(active.siteId)} · since ${v169TimeLabel(active.clockIn.time,active.siteId)}</p></div><button class="btn red attendance-big-clock" onclick="openAttendanceClock('out')">■ CLOCK OUT</button></div>`;
  let sites=v172SelfClockSites(linked);
  if(!sites.length)return `<div class="panel att-my-clock v172-clock-blocked"><div><span class="att-kicker">MY TIME</span><h2>No Site access</h2><p>${linked.member.name} · ${linked.gang.name}<br>You are not currently authorised to clock in on any Site.</p></div><span class="badge b-red">NO CLOCKING SITE</span></div>`;
  return `<div class="panel att-my-clock"><div><span class="att-kicker">MY TIME</span><h2>Ready to start?</h2><p>${linked.member.name} · ${linked.gang.name}<br>${sites.length===1?'Clocking Site: '+sites[0].name:sites.length+' permitted Sites'}</p></div><button class="btn green attendance-big-clock" onclick="openAttendanceClock('in')">▶ CLOCK IN</button></div>`
}
openAttendanceClock=function(action){
  let linked=attendanceLinkedMember();if(!linked)return toast('Your SiteSight account is not linked to a Team member. Ask a manager to link it first.');
  if(!hasPerm('clock_attendance'))return toast('You do not have Clock In / Out permission');
  if(currentUser()?.accountActive===false)return toast('This SiteSight account has been disabled');
  let status=v172MemberClockStatus(linked.member),active=attendanceActiveShift(linked.member.id),isOut=action==='out';
  if(status!=='active'&&!isOut)return toast(status==='left'?'Your clocking access has been removed':'Your clocking access is suspended');
  if(isOut&&!active)return toast('You are not currently clocked in');if(!isOut&&active)return toast('You are already clocked in');
  let sites=v172SelfClockSites(linked),sid=isOut?active.siteId:(sites[0]?.id||'');
  if(!isOut&&!sites.length)return toast('You are not authorised to clock in on any Site');
  modal=`<div class="modal"><div class="modalbox attendance-clock-modal"><span class="att-kicker">TIME & ATTENDANCE</span><h2>${isOut?'Clock Out':'Clock In'}</h2><p><b>${linked.member.name}</b> · ${linked.gang.name}</p>
  ${!isOut?`<div class="field"><label>Authorised Site</label><select id="att-clock-site" class="select" onchange="attendanceRefreshClockLocationHint()">${sites.map(s=>`<option value="${s.id}">${s.name}</option>`).join('')}</select></div>`:`<div class="att-clock-current"><span>Clocked in</span><b>${v169TimeLabel(active.clockIn.time,active.siteId)}</b><small>${siteName(active.siteId)}</small></div>`}
  <div id="att-clock-location-hint" class="att-location-hint">${attendanceGeoStatusText(sid)}</div><div class="att-privacy-note"><b>Location confirmation</b><span>SiteSight checks your location only when you press Clock In or Clock Out. You can only Clock In on Sites specifically assigned to you by management.</span></div><div id="att-clock-progress" class="att-clock-progress"></div>
  <button id="att-clock-action" class="btn ${isOut?'red':'green'} attendance-big-clock" onclick="attendanceDoClock('${isOut?'clock-out':'clock-in'}')">${isOut?'■ VERIFY LOCATION & CLOCK OUT':'▶ VERIFY LOCATION & CLOCK IN'}</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
const v172BaseAttendanceDoClock=attendanceDoClock;
attendanceDoClock=async function(action){
  let linked=attendanceLinkedMember();if(!linked)return;
  if(currentUser()?.accountActive===false)return toast('This SiteSight account has been disabled');
  if(v172MemberClockStatus(linked.member)!=='active'&&action==='clock-in')return toast('Your clocking access is not active');
  let sid=action==='clock-out'?attendanceActiveShift(linked.member.id)?.siteId:(document.getElementById('att-clock-site')?.value||'');
  if(action==='clock-in'&&!v172CanClockAt(linked.member,linked.gang,sid))return toast('You are not authorised to clock in on this Site');
  return v172BaseAttendanceDoClock(action)
}

// ---- Manager access control ------------------------------------------
const v172BaseOpenTeamMember=openTeamMember;
openTeamMember=function(teamId,memberId){
  v172BaseOpenTeamMember(teamId,memberId);
  let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId),box=document.querySelector('.modalbox');if(!g||!m||!box)return;
  let s=v172ClockAccessSummary(m,g),active=attendanceActiveShift(m.id),editable=hasPerm('manage_attendance')||hasPerm('manage_gangs')||canDirector(),
      html=`<div class="v172-operative-access-card"><div class="sectionhead"><div><span class="att-kicker">OPERATIVE ACCESS</span><h3>Clocking & Site Access</h3></div>${v172AccessStatusBadge(s.status)}</div>
      <div class="v172-access-facts"><span><b>Clocking Sites</b>${s.sites.length?s.sites.map(contractorEsc).join(', '):'None'}</span><span><b>Linked Login</b>${s.user?`${contractorEsc(s.user.name)} · ${s.loginActive?'Active':'Disabled'}`:'No linked SiteSight login'}</span><span><b>Current Shift</b>${active?`${siteName(active.siteId)} · since ${v169TimeLabel(active.clockIn.time,active.siteId)}`:'Not clocked in'}</span></div>
      ${editable?`<button class="btn ${s.status==='left'?'blue':'orange'}" onclick="v172OpenOperativeAccess('${teamId}','${memberId}')">${s.status==='left'?'Review / Restore Access':'Manage Operative Access'}</button>`:''}</div>`;
  let actions=box.querySelector('.attendance-member-actions');if(actions)actions.insertAdjacentHTML('beforebegin',html);else box.insertAdjacentHTML('beforeend',html)
}
function v172OpenOperativeAccess(teamId,memberId){
  if(!(hasPerm('manage_attendance')||hasPerm('manage_gangs')||canDirector()))return toast('You do not have permission to manage operative access');
  let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!g||!m)return;let status=v172MemberClockStatus(m),sites=v172ClockSiteIds(m,g),u=v172LinkedUserForMember(m),active=attendanceActiveShift(m.id);
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">OPERATIVE ACCESS</span><h2>${contractorEsc(m.name)}</h2><p class="muted">Use this when somebody moves Site, is temporarily suspended, or leaves the company. Historical hours and pay records are never deleted.</p>
  ${active?`<div class="v172-live-shift-warning"><b>Currently clocked in</b><span>${siteName(active.siteId)} · since ${v169TimeLabel(active.clockIn.time,active.siteId)}. If access is removed from this Site, SiteSight will end this shift at the exact access-change time.</span></div>`:''}
  <div class="field"><label>Access status</label><select id="v172-access-status" class="select" onchange="v172RefreshAccessForm()"><option value="active" ${status==='active'?'selected':''}>Active — can Clock In on selected Sites</option><option value="suspended" ${status==='suspended'?'selected':''}>Suspend clocking — temporary</option><option value="left" ${status==='left'?'selected':''}>Left company — revoke all access</option></select></div>
  <div id="v172-site-access-block"><label class="field-label">Authorised clocking Sites</label><div class="v172-site-grid">${db.sites.filter(s=>!s.archived).map(s=>`<label class="site-access-item"><input class="v172-clock-site" type="checkbox" value="${s.id}" ${sites.includes(s.id)?'checked':''}> ${contractorEsc(s.name)}</label>`).join('')||'<div class="muted">No active Sites exist.</div>'}<div class="muted v172-site-note">For a Site move, untick the old Site and tick the new Site. A linked operative login is restricted to the same Sites.</div></div></div>
  <div class="field"><label>Reason / note <b>*</b></label><textarea id="v172-access-reason" class="textarea" placeholder="e.g. Moved to Oak View / employment ended / temporarily suspended"></textarea></div>
  ${u?`<div class="v172-linked-account"><b>Linked login: ${contractorEsc(u.name)} (@${contractorEsc(u.username)})</b><span>${status==='left'?'This login is currently disabled.':'If Left company is selected, the login will be disabled immediately.'}</span></div>`:'<div class="v172-linked-account"><b>No linked login</b><span>Clocking is still controlled on the Team member record. Link a user under Pay / attendance settings if self-clocking is required.</span></div>'}
  <div class="v172-access-impact"><b>What SiteSight will do</b><ul><li>Block Clock In on every Site not selected above.</li><li>Keep all previous timesheets, pay and Audit history.</li><li>End a live shift if that Site is removed from the operative's access.</li><li>Disable the linked login completely if they have left the company.</li></ul></div>
  <button class="btn ${status==='left'?'blue':'orange'}" onclick="v172SaveOperativeAccess('${teamId}','${memberId}')">Save Access</button> <button class="btn light" onclick="openTeamMember('${teamId}','${memberId}')">Cancel</button></div></div>`;render();v172RefreshAccessForm()
}
function v172RefreshAccessForm(){
  let status=document.getElementById('v172-access-status')?.value||'active',block=document.getElementById('v172-site-access-block');
  if(block){block.style.opacity=status==='active'?'1':'.5';block.querySelectorAll('input').forEach(i=>i.disabled=status!=='active')}
}
function v172SelectedClockSites(){return [...document.querySelectorAll('.v172-clock-site:checked')].map(x=>x.value)}
function v172ApplyAccessLocal(teamId,memberId,status,siteIds,reason,at=new Date().toISOString()){
  let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!g||!m)return false;let u=v172LinkedUserForMember(m),active=attendanceActiveShift(m.id),oldSites=v172ClockSiteIds(m,g);
  m.clockAccessStatus=status;m.clockSiteIds=status==='active'?siteIds:[];m.clockAccessReason=reason;m.clockAccessUpdatedAt=at;m.clockAccessUpdatedBy=currentUser()?.name||'';
  if(status==='active'){m.activeForAttendance=true;m.employmentStatus='Active';if(m.leavingDate&&m.leavingDate<=iso())m.leavingDate='';if(u){u.accountActive=true;u.allowedSites=[...siteIds];u.allSitesAccess=false;u.disabledReason='';u.disabledAt='';u.disabledBy=''}}
  if(status==='suspended'){m.activeForAttendance=false;if(u&&u.accountActive===false)u.accountActive=true}
  if(status==='left'){m.activeForAttendance=false;m.employmentStatus='Leaver';m.leavingDate=iso();if(u){u.accountActive=false;u.disabledReason=reason;u.disabledAt=at;u.disabledBy=currentUser()?.name||'';u.allowedSites=[]}}
  let mustClose=!!active&&(status!=='active'||!siteIds.includes(active.siteId));
  if(mustClose){active.clockOut={time:at,status:'Access Changed',by:currentUser()?.name||'',reason:`Operative access changed — ${reason}`,timeSource:'SiteSight access control'};attendanceMarkApprovalNeedsRecheck(m.id,active.siteId,at)}
  return {mustClose,oldSites}
}
async function v172SaveOperativeAccess(teamId,memberId){
  let status=document.getElementById('v172-access-status')?.value||'active',reason=(document.getElementById('v172-access-reason')?.value||'').trim(),siteIds=status==='active'?v172SelectedClockSites():[];
  if(!reason)return toast('Enter a reason for the access change');if(status==='active'&&!siteIds.length)return toast('Select at least one authorised Site');
  let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!g||!m)return;
  if(status==='left'&&!confirm(`Revoke all access for ${m.name}? Their linked SiteSight login will be disabled and any live shift will be ended now.`))return;
  let payload={actorId:currentUser()?.id||'',workerId:memberId,status,siteIds,reason};
  if(sharedMode&&navigator.onLine){
    try{
      if(pendingSave)await syncToServer(true);
      let r=await fetch('/api/attendance/access',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}),result=await r.json();
      if(!r.ok||!result.ok)return toast(result.message||'Access change could not be saved');
      if(result.data){db=result.data;sharedRevision=result.revision||sharedRevision;lastSyncedDB=cloneDB(db);pendingSave=false;syncState='synced';lastSyncAt=new Date().toISOString();migrate(false);persistCache();try{localStorage.setItem(BASE_KEY,JSON.stringify(db));localStorage.removeItem(PENDING_KEY)}catch(e){}}
      savedState(status==='left'?'Operative access revoked':status==='suspended'?'Clocking suspended':'Operative Site access updated');openTeamMember(teamId,memberId);return
    }catch(e){return toast(e.message||'Access change could not be saved')}
  }
  let result=v172ApplyAccessLocal(teamId,memberId,status,siteIds,reason);if(!result)return;save();audit('attendance','operative access',memberId,`${m.name}: access changed to ${status}${siteIds.length?' · '+siteIds.map(siteName).join(', '):''} — ${reason}`,'');savedState('Operative access updated');openTeamMember(teamId,memberId)
}

// If a user is linked for self-clocking for the first time, initialise safe explicit Site access.
const v172BaseSaveAttendanceMemberSettings=saveAttendanceMemberSettings;
saveAttendanceMemberSettings=function(teamId,memberId){
  let before=db.gangs.find(g=>g.id===teamId)?.members?.find(m=>m.id===memberId),oldLinked=before?.linkedUserId||'';
  v172BaseSaveAttendanceMemberSettings(teamId,memberId);
  let g=db.gangs.find(g=>g.id===teamId),m=g?.members?.find(m=>m.id===memberId);if(!g||!m)return;
  if(m.linkedUserId&&!oldLinked&&!Array.isArray(m.clockSiteIds)){m.clockSiteIds=v172DefaultClockSites(g,m)}
  if(m.linkedUserId&&(!m.clockSiteIds||!m.clockSiteIds.length)){let sid=attendanceDefaultSiteId();if(sid)m.clockSiteIds=[sid]}
  if(m.linkedUserId&&m.clockSiteIds?.length){let u=v172LinkedUserForMember(m);if(u&&u.role!=='Director'){u.allowedSites=[...m.clockSiteIds];u.allSitesAccess=false}}
  save()
}

// ---- User Admin can explicitly disable / enable any non-Director login ----
editUserAccess=function(id){
  if(!hasPerm('manage_users'))return toast('You do not have permission to manage users');let u=db.users.find(u=>u.id===id);if(!u||u.role==='Director')return;
  modal=`<div class="modal"><div class="modalbox"><h2>Edit User Access</h2><p><b>${u.name}</b> · @${u.username}</p>
  <div class="v172-user-status ${u.accountActive===false?'disabled':'active'}"><b>${u.accountActive===false?'ACCOUNT DISABLED':'ACCOUNT ACTIVE'}</b><span>${u.accountActive===false?(u.disabledReason||'Access disabled'):'User can sign in with their current permissions.'}</span></div>
  <label class="permcheck v172-account-toggle"><input id="v172-account-active" type="checkbox" ${u.accountActive!==false?'checked':''}> <span><b>Allow this user to sign in</b><small>Untick to disable their SiteSight account immediately.</small></span></label>
  <div class="field"><label>Registered email</label><input id="ee" class="input" type="email" value="${u.email||''}"></div><div class="field"><label>Role</label><select id="er" class="select" onchange="v173ApplyRoleTemplate('er')">${roleOptions.filter(r=>r!=='Director').map(r=>`<option ${u.role===r?'selected':''}>${r}</option>`).join('')}</select></div>
  <h3>Access</h3><p class="muted">Role defaults are applied automatically. Open Customise permissions only when this user needs an exception.</p>${permissionInputs(u.permissions)}<h3 style="margin-top:18px">Site access</h3>${sitePermissionInputs(u.allowedSites||[])}
  <div style="margin-top:14px"><button class="btn orange" onclick="saveUserAccess('${u.id}')">Save access</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()
}
saveUserAccess=function(id){
  if(!hasPerm('manage_users'))return toast('You do not have permission to manage users');let u=db.users.find(u=>u.id===id);if(!u)return;let email=(document.getElementById('ee')?.value||'').trim().toLowerCase();
  if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))return toast('Enter a valid email address');if(db.users.some(x=>x.id!==id&&(x.email||'').toLowerCase()===email))return toast('That email is already registered');
  let wasActive=u.accountActive!==false,nowActive=!!document.getElementById('v172-account-active')?.checked;
  u.email=email;u.role=document.getElementById('er').value;u.permissions=collectPerms();u.allowedSites=collectAllowedSites();u.accountActive=nowActive;
  if(wasActive&&!nowActive){u.disabledReason='Disabled by administrator';u.disabledAt=new Date().toISOString();u.disabledBy=currentUser()?.name||''}else if(nowActive){u.disabledReason='';u.disabledAt='';u.disabledBy=''}
  save();audit('user','user access',u.id,`${u.name}: account ${nowActive?'enabled':'disabled'} · Site access updated`,'');modal=null;toast('User access updated');render()
}

// Add status to Admin user cards without rebuilding the Admin page.
const v172BaseAdminPage=adminPage;
adminPage=function(){
  let html=v172BaseAdminPage();
  if(adminTab==='users'){
    (db.users||[]).forEach(u=>{
      let needle=`<b>${u.name}</b><br><span class="muted">@${u.username} · ${u.role}</span>`;
      let badge=u.accountActive===false?'<span class="v172-inline-disabled">ACCOUNT DISABLED</span>':'';
      html=html.replace(needle,`${needle}${badge}`)
    })
  }
  return html
}

/* ===== v173-role-workspaces.js ===== */
'use strict';

// SiteSight V1.73 — role-first workspaces and least-privilege defaults.
// V1.72 remains the underlying operational baseline; this module changes
// where each role starts, the order of its everyday tools, and role templates.

const V173_ROLE_PROFILES={
  'Director':{
    home:'dashboard',purpose:'Full business oversight, security and final control.',
    main:['dashboard','attendance','work-items','issues','analytics','gangs','diary','site-drawings','calendar'],
    extra:['weekly-report','payments-history','dayworks','audit','admin','guide','about'],permissions:'all'
  },
  'Contracts Manager':{
    home:'dashboard',purpose:'Programme, delivery, commercial oversight and Site leadership.',
    main:['dashboard','work-items','issues','attendance','analytics','gangs','site-drawings','calendar','diary'],
    extra:['weekly-report','payments-history','dayworks','audit','admin','guide','about'],
    permissions:['dashboard','work_items','update_progress','view_issues','raise_issues','manage_issues','sign_off','edit_lifts','manage_workitems','view_financials','manage_variations','approve_variations','view_drawings','manage_drawings','view_calendar','manage_calendar','view_site_diary','manage_site_diary','download_reports','view_attendance','approve_timesheets','gangs','analytics','audit','manage_gangs','manage_sites','view_archives','archive_sites']
  },
  'Site Manager':{
    home:'dashboard',purpose:'Run today’s Site, people, progress, issues and records.',
    main:['dashboard','attendance','work-items','issues','diary','gangs','site-drawings','calendar'],
    extra:['weekly-report','dayworks','analytics','admin','guide','about'],
    permissions:['dashboard','work_items','update_progress','view_issues','raise_issues','manage_issues','sign_off','manage_workitems','view_drawings','manage_drawings','view_calendar','manage_calendar','view_site_diary','manage_site_diary','download_reports','view_attendance','manage_attendance','approve_timesheets','gangs','analytics','manage_gangs']
  },
  'Supervisor':{
    home:'work-items',purpose:'Coordinate live work, progress, issues, people and the Site diary.',
    main:['work-items','issues','attendance','diary','gangs','site-drawings','calendar'],
    extra:['dayworks','guide','about'],
    permissions:['work_items','update_progress','view_issues','raise_issues','manage_issues','view_drawings','view_calendar','view_site_diary','manage_site_diary','view_attendance','clock_attendance','manage_attendance','gangs']
  },
  'Quantity Surveyor':{
    home:'payments-history',purpose:'Commercial records, payments, variations and reporting.',
    main:['payments-history','analytics','site-drawings'],
    extra:['weekly-report','guide','about'],
    permissions:['view_financials','record_payments','manage_payments','manage_variations','approve_variations','view_drawings','download_reports','analytics','view_archives']
  },
  'Accounts':{
    home:'payments-history',purpose:'Payment records, approved hours and financial reporting.',
    main:['payments-history','analytics'],
    extra:['weekly-report','guide','about'],
    permissions:['view_financials','record_payments','manage_payments','download_reports','analytics','view_archives']
  },
  'Read Only':{
    home:'dashboard',purpose:'View operational information without changing records.',
    main:['dashboard','site-drawings','calendar','diary'],
    extra:['guide','about'],
    permissions:['dashboard','view_drawings','view_calendar','view_site_diary']
  },
  'Operator':{
    home:'attendance',purpose:'Clock in and out quickly with Site location confirmation.',
    main:['attendance'],extra:['guide','about'],permissions:['clock_attendance']
  }
};

const v173PreviousDefaultPerms=defaultPerms;
function v173RoleDefaultPerms(role){
  let p=emptyPerms(),profile=V173_ROLE_PROFILES[role]||V173_ROLE_PROFILES.Operator;
  if(profile.permissions==='all'){permissionDefs.forEach(([key])=>p[key]=true);return p}
  (profile.permissions||[]).forEach(key=>p[key]=true);return p
}
defaultPerms=v173RoleDefaultPerms;

function v173UserHas(user,key){if(!user||plotScopedPermissions.has(key)&&!roleCanAccessPlots(user))return false;return user.role==='Director'||!!user.permissions?.[key]}
function v173UserHasAny(user,keys){return (keys||[]).some(key=>v173UserHas(user,key))}
function v173IsOperativeAccount(user){
  if(!user)return false;if(user.role==='Operator')return true;
  return !!user.linkedMemberId&&v173UserHas(user,'clock_attendance')&&!permissionDefs.some(([key])=>key!=='clock_attendance'&&v173UserHas(user,key))
}
function v173CanOpenView(target,user=currentUser()){
  if(!user)return false;
  if(['more','guide','about'].includes(target))return true;
  if(['dashboard','needs-attention','work-items','my-work','issues','dayworks','activity','signed-off','weekly-report','analytics','gangs','admin'].includes(target)&&!roleCanAccessPlots(user))return false;
  const access={
    dashboard:['dashboard'],'needs-attention':['dashboard'],'work-items':['work_items'],issues:['view_issues'],gangs:['gangs'],
    'site-drawings':['view_drawings'],calendar:['view_calendar'],diary:['view_site_diary'],
    attendance:['view_attendance','clock_attendance','manage_attendance','approve_timesheets'],
    'payments-history':['view_financials'],analytics:['analytics'],audit:['audit'],dayworks:['work_items'],
    'weekly-report':['download_reports'],activity:['dashboard','work_items'],'signed-off':['dashboard','work_items'],
    admin:['manage_sites','view_archives','manage_gangs','manage_workitems','manage_users','reset_passwords']
  };
  return v173UserHasAny(user,access[target]||[])
}
function v173HomeView(user=currentUser()){
  if(!user)return 'more';let profile=V173_ROLE_PROFILES[user.role]||V173_ROLE_PROFILES.Operator;
  let candidates=[profile.home,...profile.main,'attendance','dashboard','work-items','payments-history','more'];
  return candidates.find((target,index)=>candidates.indexOf(target)===index&&v173CanOpenView(target,user))||'more'
}

const V173_MAIN_NAV={
  dashboard:{icon:'⌂',label:'Dashboard'},
  'work-items':{icon:'▣',label:'Work Items'},
  issues:{icon:'⚠',label:'Issues'},
  attendance:{icon:'◷',label:user=>v173UserHas(user,'view_attendance')?'People & Time':'My Time'},
  'payments-history':{icon:'£',label:'Payments'},
  analytics:{icon:'◒',label:'Analytics'},
  gangs:{icon:'♟',label:'Teams'},
  diary:{icon:'✎',label:'Site Diary'},
  'site-drawings':{icon:'▤',label:'Drawings'},
  calendar:{icon:'▣',label:'Calendar'}
};
function v173RoleNavItems(user=currentUser()){
  if(!user)return [];let profile=V173_ROLE_PROFILES[user.role]||V173_ROLE_PROFILES.Operator;
  let general=['dashboard','work-items','issues','attendance','payments-history','analytics','gangs','diary','site-drawings','calendar'],order=[...profile.main,...general];
  return order.filter((id,index)=>order.indexOf(id)===index&&V173_MAIN_NAV[id]&&v173CanOpenView(id,user)).map(id=>({id,...V173_MAIN_NAV[id]}))
}
function v173NavActive(id){return view===id||(id==='dashboard'&&view==='needs-attention')}
function v173NavButton(item,user=currentUser(),mobile=false){
  let label=typeof item.label==='function'?item.label(user):item.label,issueCount=item.id==='issues'?scopeItems().reduce((n,x)=>n+issues(x),0):0;
  if(mobile&&item.id==='work-items')label='Work';
  return `<button class="${v173NavActive(item.id)?'active':''}" onclick="goNav('${item.id}')"><span>${item.icon}</span>${label}${issueCount?`<span class="count">${issueCount}</span>`:''}</button>`
}
nav=function(){
  let user=currentUser(),items=v173RoleNavItems(user),main=items.map(item=>v173NavButton(item,user)).join(''),primary=new Set(items.map(x=>x.id)),moreActive=view==='more'||!primary.has(view);
  return main+(hasMore()?`<button class="${moreActive?'active':''}" onclick="goNav('more')"><span>•••</span>More</button>`:'')
};
mobileNav=function(){
  let user=currentUser(),items=v173RoleNavItems(user).slice(0,4),primary=new Set(items.map(x=>x.id)),main=items.map(item=>v173NavButton(item,user,true)).join(''),moreActive=view==='more'||!primary.has(view);
  return main+(hasMore()?`<button class="${moreActive?'active':''}" onclick="goNav('more')"><span>•••</span>More</button>`:'')
};

const V173_MORE_TOOLS={
  dashboard:['⌂','Dashboard','Business and Site overview'],
  'work-items':['▣','Work Items','Assigned work, progress and evidence'],issues:['⚠','Issues','Open, urgent and blocking issues'],
  attendance:['◷','People & Time','Clocking, weekly hours and attendance'],
  'payments-history':['£','Payments History','Payments by date, work item and team'],analytics:['◒','Analytics','Management and commercial overview'],
  gangs:['♟','Teams','Teams, members and current workload'],diary:['✎','Site Diary','Daily Site record'],
  'site-drawings':['▤','Drawing Library','Reusable master drawings'],calendar:['▣','Calendar','Programme dates, notes and people events'],
  dayworks:['＋','Dayworks','Instructed extra work and labour evidence'],'weekly-report':['▤','Weekly Site Report','Automatic weekly management summary'],
  audit:['≡','Audit Trail','Permanent change history'],admin:['⚙','Admin','Sites, teams, work setup and user access'],
  guide:['?','New User Guide','Help tailored to this account'],about:['©','About SiteSight','Ownership and legal notice']
};
function v173MoreToolIds(user=currentUser()){
  if(!user)return [];let profile=V173_ROLE_PROFILES[user.role]||V173_ROLE_PROFILES.Operator,mobile=window.matchMedia&&window.matchMedia('(max-width:720px)').matches,
      visible=new Set(v173RoleNavItems(user).slice(0,mobile?4:999).map(x=>x.id)),all=[...profile.extra,...profile.main,...Object.keys(V173_MORE_TOOLS)];
  return all.filter((id,index)=>all.indexOf(id)===index&&!visible.has(id)&&V173_MORE_TOOLS[id]&&v173CanOpenView(id,user))
}
morePage=function(){
  let options=v173MoreToolIds();return `<div class="topbar"><div><h1>More</h1><div class="sub">Additional tools available to your role and individual permissions</div></div></div><div class="more-grid">${options.map(id=>{let o=V173_MORE_TOOLS[id];return `<div class="card more-card" onclick="goNav('${id}')"><h3>${o[0]} ${o[1]}</h3><p class="muted">${o[2]}</p></div>`}).join('')||'<div class="empty-help"><b>No additional tools available.</b></div>'}</div>`
};

// ---- Existing-user migration ----------------------------------------
function v173SamePermissionSet(actual,expected){return permissionDefs.every(([key])=>!!actual?.[key]===!!expected?.[key])}
function v173MigrateRoleProfiles(){
  let changed=false;(db.users||[]).forEach(user=>{
    let oldDefault=v173PreviousDefaultPerms(user.role),newDefault=v173RoleDefaultPerms(user.role),version=Number(user.permissionTemplateVersion||0);
    if(user.role==='Director'){
      if(!v173SamePermissionSet(user.permissions,newDefault)){user.permissions=newDefault;changed=true}
      if(user.permissionProfileMode!=='role-default'){user.permissionProfileMode='role-default';changed=true}
    }else if(version<173){
      if(!user.permissions||v173SamePermissionSet(user.permissions,oldDefault)){user.permissions=newDefault;user.permissionProfileMode='role-default'}
      else user.permissionProfileMode='custom';
      changed=true
    }
    if(version<173){user.permissionTemplateVersion=173;changed=true}
  });return changed
}
const v173BaseMigrate=migrate;
migrate=function(persist=true){v173BaseMigrate(false);let changed=v173MigrateRoleProfiles();if(persist&&(changed||persist))save()};
if(v173MigrateRoleProfiles())persistCache();

// ---- Login, landing and route guards --------------------------------
login=function(){
  let un=(document.getElementById('login-user')?.value||'').trim().toLowerCase(),pw=document.getElementById('login-pass')?.value||'',
      user=db.users.find(item=>(item.username||'').toLowerCase()===un&&item.password===pw);
  if(!user)return toast('Incorrect username or password');
  if(user.accountActive===false)return toast('This SiteSight account has been disabled. Contact a manager.');
  currentUserId=user.id;try{localStorage.setItem(CURRENT_USER_KEY,user.id)}catch(e){};role=user.role;trainingMode=!!user.trainingMode;
  user.loginCount=Number(user.loginCount||0)+1;user.lastLoginAt=new Date().toISOString();save();
  if(site!=='all'&&!canAccessSite(site))site='all';selected=null;selectedGang=null;guideActive=false;guideStep=0;view=v173HomeView(user);
  if(!user.onboardingSeen&&!v173IsOperativeAccount(user))openUserGuide(true);else render();setTimeout(deliverNotifications,100)
};
skipUserGuide=function(){markGuideSeen();guideActive=false;guideFirstRun=false;view=v173HomeView();render();toast('Guide is always available from More')};
finishGuidedTour=function(){markGuideSeen();guideActive=false;guideFirstRun=false;view=v173HomeView();render();toast('New User Guide complete')};
const v173BaseGoNav=goNav;
goNav=function(target){if(!v173CanOpenView(target))return toast('This tool is not included in your permissions');return v173BaseGoNav(target)};
const v173BaseRender=render;
render=function(){
  let user=currentUser();if(user){
    if(selected&&!canViewItem(db.items.find(item=>item.id===selected),user))selected=null;
    if(selectedGang&&(!roleCanAccessPlots(user)||!v173UserHas(user,'gangs')))selectedGang=null;
    if(!selected&&!selectedGang&&!v173CanOpenView(view,user))view=v173HomeView(user)
  }
  return v173BaseRender()
};

// ---- Admin role-template guidance -----------------------------------
function v173RoleTemplateCard(role){
  let profile=V173_ROLE_PROFILES[role]||V173_ROLE_PROFILES.Operator,p=v173RoleDefaultPerms(role),labels=permissionDefs.filter(([key])=>p[key]).map(([,label])=>label),home=V173_MORE_TOOLS[profile.home]?.[1]||profile.home;
  return `<div class="v173-role-template-card"><span>ROLE WORKSPACE</span><b>${role} · starts on ${home}</b><p>${profile.purpose}</p><small>Essential access: ${labels.slice(0,7).join(' · ')}${labels.length>7?` · +${labels.length-7} more`:''}</small></div>`
}
const v173BasePermissionInputs=permissionInputs;
permissionInputs=function(permissions){
  let matched=roleOptions.find(roleName=>v173SamePermissionSet(permissions,v173RoleDefaultPerms(roleName))),summary=matched?v173RoleTemplateCard(matched):'<div class="v173-role-template-card custom"><span>CUSTOM WORKSPACE</span><b>Individual permissions</b><p>This account differs from the standard role template. Only the permissions ticked below are available.</p></div>';
  return `<div id="v173-role-template-summary">${summary}</div>${v173BasePermissionInputs(permissions)}`
};
function v173ApplyRoleTemplate(selectId){
  let roleName=document.getElementById(selectId)?.value||'Operator',p=v173RoleDefaultPerms(roleName);
  document.querySelectorAll('[data-perm]').forEach(box=>box.checked=!!p[box.dataset.perm]);let summary=document.getElementById('v173-role-template-summary');if(summary)summary.innerHTML=v173RoleTemplateCard(roleName)
}
refreshRolePerms=function(){v173ApplyRoleTemplate('ur')};
function v173PermissionMode(roleName,permissions){return v173SamePermissionSet(permissions,v173RoleDefaultPerms(roleName))?'role-default':'custom'}
const v173BaseSaveNewUser=saveNewUser;
saveNewUser=function(){
  let before=new Set((db.users||[]).map(user=>user.id)),roleName=document.getElementById('ur')?.value||'Operator',planned=collectPerms(),mode=v173PermissionMode(roleName,planned);
  v173BaseSaveNewUser();let user=(db.users||[]).find(item=>!before.has(item.id));if(!user)return;user.permissionTemplateVersion=173;user.permissionProfileMode=mode;save()
};
const v173BaseSaveUserAccess=saveUserAccess;
saveUserAccess=function(id){
  let roleName=document.getElementById('er')?.value||'Operator',email=(document.getElementById('ee')?.value||'').trim().toLowerCase(),planned=collectPerms(),hadModal=!!modal;
  v173BaseSaveUserAccess(id);let user=db.users.find(item=>item.id===id);if(!user||hadModal&&modal!==null||user.email!==email||user.role!==roleName||!v173SamePermissionSet(user.permissions,planned))return;
  user.permissionTemplateVersion=173;user.permissionProfileMode=v173PermissionMode(roleName,planned);save()
};

setTimeout(()=>{let user=currentUser();if(user){if(!v173CanOpenView(view,user))view=v173HomeView(user);render()}},0);

/* ===== v174-pay-controls.js ===== */
'use strict';

// SiteSight V1.74 — working hours, authorised overtime, flexible pay periods,
// individual pay review and complete dated-record filtering.

const V174_PERMISSIONS=[
  ['manage_working_hours','Set People Working Hours'],
  ['approve_overtime','Authorise Overtime']
];
const v174AttendancePermissionGroup=permissionGroups.find(group=>group.name==='Time & Attendance');
V174_PERMISSIONS.forEach(def=>{
  if(v174AttendancePermissionGroup&&!v174AttendancePermissionGroup.items.some(([key])=>key===def[0]))v174AttendancePermissionGroup.items.push(def);
  if(!permissionDefs.some(([key])=>key===def[0]))permissionDefs.push(def)
});

let v174PayPreset='week',v174PayFrom='',v174PayTo='',v174Payee='all',
    v174TimesheetPreset='week',v174TimesheetFrom='',v174TimesheetTo='',
    v174CalendarAgendaMode='upcoming',v174CalendarFrom='',v174CalendarTo='',
    v174RecordRanges={work:{from:'',to:''},activity:{from:'',to:''},signed:{from:'',to:''},drawings:{from:'',to:''},issues:{from:'',to:''},diary:{from:'',to:''},dayworks:{from:'',to:''},audit:{from:'',to:''},leave:{from:'',to:''}};

function v174Date(value=iso()){if(value instanceof Date&&!Number.isNaN(value.getTime()))return value.toISOString().slice(0,10);return String(value||'').slice(0,10)}
function v174DateAdd(value,days){return attendanceAddDays(v174Date(value),days)}
function v174MonthEnd(value=iso()){let d=new Date(v174Date(value)+'T12:00:00');return v174Date(new Date(d.getFullYear(),d.getMonth()+1,0))}
function v174RangeValid(from,to){return !!from&&!!to&&from<=to}
function v174DateInRange(date,from,to){let d=v174Date(date);return !!d&&(!from||d>=from)&&(!to||d<=to)}
function v174RangesOverlap(from,to,rangeFrom,rangeTo){return (!rangeFrom||String(to||from)>=rangeFrom)&&(!rangeTo||String(from||to)<=rangeTo)}
function v174RangeDays(from,to){if(!v174RangeValid(from,to))return 0;return Math.floor((new Date(to+'T12:00:00')-new Date(from+'T12:00:00'))/86400000)+1}
function v174RangeLabel(from,to){return v174RangeValid(from,to)?`${new Date(from+'T12:00:00').toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'})} – ${new Date(to+'T12:00:00').toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'})}`:'Choose From and To'}

function v174Period(preset){
  let today=iso(),start=attendanceWeekStartISO(new Date());
  if(preset==='fortnight')return {from:start,to:v174DateAdd(start,13)};
  if(preset==='month')return {from:today.slice(0,8)+'01',to:v174MonthEnd(today)};
  return {from:start,to:v174DateAdd(start,6)}
}
function v174SetPayPeriod(preset){v174PayPreset=preset;if(preset!=='custom'){let r=v174Period(preset);v174PayFrom=r.from;v174PayTo=r.to}render()}
function v174SetPayDate(which,value){v174PayPreset='custom';if(which==='from')v174PayFrom=value;else v174PayTo=value;render()}
function v174EnsurePayPeriod(){if(!v174PayFrom||!v174PayTo){let r=v174Period(v174PayPreset);v174PayFrom=r.from;v174PayTo=r.to}}
function v174SetTimesheetPeriod(preset){v174TimesheetPreset=preset;if(preset!=='custom'){let r=v174Period(preset);v174TimesheetFrom=r.from;v174TimesheetTo=r.to}render()}
function v174SetTimesheetDate(which,value){v174TimesheetPreset='custom';if(which==='from')v174TimesheetFrom=value;else v174TimesheetTo=value;render()}
function v174EnsureTimesheetPeriod(){if(!v174TimesheetFrom||!v174TimesheetTo){let r=v174Period(v174TimesheetPreset);v174TimesheetFrom=r.from;v174TimesheetTo=r.to}}

function v174MemberScheduleForDate(member,siteId,date){
  let cfg=attendanceSiteConfig(siteId)||{},dow=new Date(v174Date(date)+'T12:00:00').getDay(),saved=member?.workingSchedule?.[dow],usual=(member?.usualDays?.length?member.usualDays:[1,2,3,4,5]).map(Number);
  return {active:saved?saved.active!==false:usual.includes(dow),start:saved?.start||member?.expectedStartTime||cfg.expectedStartTime||'07:30',end:saved?.end||member?.expectedEndTime||cfg.expectedEndTime||'16:30'}
}
function v174ShiftScheduleText(member,shift){let date=attendanceDateOnly(shift?.clockIn?.time,shift?.siteId),s=v174MemberScheduleForDate(member,shift?.siteId,date);return s.active?`${s.start}–${s.end}`:'Non-working day'}
function v174ShiftPayBreakdown(shift,member){
  let recordedHours=attendanceShiftHours(shift),date=attendanceDateOnly(shift?.clockIn?.time,shift?.siteId),schedule=v174MemberScheduleForDate(member,shift?.siteId,date),lateMinutes=0;
  if(recordedHours>0&&shift?.clockOut?.time){
    if(!schedule.active)lateMinutes=Math.round(recordedHours*60);
    else{let out=v169TimeMinutes(shift.clockOut.time,shift.siteId),finish=v169ParseTime(schedule.end,'16:30');lateMinutes=Math.max(0,out-finish)}
  }
  let approval=shift?.overtimeApproval||{},approvedMinutes=approval.status==='Approved'?Math.min(lateMinutes,Math.max(0,Number(approval.minutes||0))):0,
      unauthorisedMinutes=Math.max(0,lateMinutes-approvedMinutes),payableHours=Math.max(0,recordedHours-unauthorisedMinutes/60);
  return {recordedHours,payableHours,lateMinutes,approvedMinutes,unauthorisedMinutes,schedule,approval}
}
function v174ShiftsForRange(workerId,from,to,siteId='all'){
  return (db.attendanceShifts||[]).filter(shift=>shift.workerId===workerId&&!shift.voided&&(siteId==='all'||shift.siteId===siteId)&&v174DateInRange(attendanceDateOnly(shift.clockIn?.time,shift.siteId),from,to)).sort((a,b)=>String(a.clockIn?.time||'').localeCompare(String(b.clockIn?.time||'')))
}
function v174ExpectedStandardHours(member,siteId,from,to){
  if(!v174RangeValid(from,to))return Number(member?.standardWeekHours||40);let working=0;
  for(let d=from;d<=to;d=v174DateAdd(d,1))if(v174MemberScheduleForDate(member,siteId,d).active)working++;
  let usual=Math.max(1,(member?.usualDays?.length?member.usualDays:[1,2,3,4,5]).length);return Number(member?.standardWeekHours||40)*(working/usual)
}
function v174AttendanceTotalsForRange(workerId,from,to,siteId='all'){
  let wm=attendanceMemberById(workerId),member=wm?.member||{},shifts=v174ShiftsForRange(workerId,from,to,siteId),details=shifts.map(shift=>({shift,...v174ShiftPayBreakdown(shift,member)})),
      totalHours=details.reduce((n,row)=>n+row.recordedHours,0),payableHours=details.reduce((n,row)=>n+row.payableHours,0),authorisedOvertimeHours=details.reduce((n,row)=>n+row.approvedMinutes/60,0),
      unauthorisedOvertimeHours=details.reduce((n,row)=>n+row.unauthorisedMinutes/60,0),standard=v174ExpectedStandardHours(member,siteId==='all'?(shifts[0]?.siteId||attendanceDefaultSiteId()):siteId,from,to),
      thresholdOvertime=Math.max(0,(payableHours-authorisedOvertimeHours)-standard),overtimeHours=Math.min(payableHours,authorisedOvertimeHours+thresholdOvertime),basicHours=Math.max(0,payableHours-overtimeHours),
      hourly=Number(member.hourlyRate||0),overtimeRate=Number(member.overtimeRate||hourly),gross=0,fullDayTopUp=0,days=new Set(shifts.map(s=>attendanceDateOnly(s.clockIn?.time,s.siteId))).size;
  if(member.employmentType==='Hourly'||member.employmentType==='Day Rate')gross=basicHours*hourly+overtimeHours*overtimeRate;
  if(member.employmentType==='Day Rate'){
    let byDate={};details.forEach(row=>{let d=attendanceDateOnly(row.shift.clockIn?.time,row.shift.siteId);byDate[d]=(byDate[d]||0)+row.payableHours});
    Object.entries(byDate).forEach(([date,hours])=>{let sid=siteId==='all'?(shifts.find(s=>attendanceDateOnly(s.clockIn?.time,s.siteId)===date)?.siteId||''):siteId;if(v169FullDayOverride(workerId,date,sid)){let top=Math.max(0,Number(member.dayRate||0)-Number(hours)*hourly);fullDayTopUp+=top;gross+=top}})
  }
  return {shifts,details,totalHours,payableHours,basicHours,overtimeHours,authorisedOvertimeHours,unauthorisedOvertimeHours,gross,days,fullDayTopUp,standardHours:standard}
}

// All existing weekly views inherit the safer payment calculation.
attendanceWeekTotals=function(workerId,start,siteId='all'){return v174AttendanceTotalsForRange(workerId,start,v174DateAdd(start,6),siteId)};
v169MemberActiveOnDate=function(member,date){
  if(!member||member.activeForAttendance===false||member.employmentStatus==='Leaver'||member.employmentStatus==='Inactive')return false;
  if(member.startDate&&date<member.startDate)return false;if(member.leavingDate&&date>member.leavingDate)return false;
  return v174MemberScheduleForDate(member,attendanceDefaultSiteId(),date).active
};
attendanceExpectedMembers=function(siteId,date=iso()){
  let gangIds=new Set((db.items||[]).filter(x=>x.site===siteId&&!db.sites.find(s=>s.id===x.site)?.archived&&x.gang).map(x=>x.gang));
  return attendanceAllMembers().filter(x=>{
    let activeShift=attendanceActiveShift(x.member.id)?.siteId===siteId,member=x.member;
    if(!member||member.activeForAttendance===false||member.employmentStatus==='Leaver'||member.employmentStatus==='Inactive')return false;
    if(member.startDate&&date<member.startDate)return false;if(member.leavingDate&&date>member.leavingDate)return false;
    return (gangIds.has(x.gang.id)||activeShift)&&v174MemberScheduleForDate(member,siteId,date).active
  })
};
v169ShiftPunctuality=function(shift,member){
  if(!shift?.clockIn?.time)return {late:0,early:0,onTime:false};let date=attendanceDateOnly(shift.clockIn.time,shift.siteId),schedule=v174MemberScheduleForDate(member,shift.siteId,date),cfg=attendanceSiteConfig(shift.siteId)||{},start=v169TimeMinutes(shift.clockIn.time,shift.siteId),finish=shift.clockOut?.time?v169TimeMinutes(shift.clockOut.time,shift.siteId):null,
      plannedStart=v169ParseTime(schedule.start,'07:30'),plannedEnd=v169ParseTime(schedule.end,'16:30'),grace=Number(cfg.graceMinutes??5),late=schedule.active?Math.max(0,start-plannedStart):0,early=schedule.active&&finish!==null?Math.max(0,plannedEnd-finish):0,lateFlag=late>grace,earlyFlag=early>grace;
  return {late:lateFlag?late:0,early:earlyFlag?early:0,onTime:schedule.active&&!lateFlag&&!earlyFlag,startMinutes:start,endMinutes:finish}
};

function v174OvertimeStatusHtml(shift,member){
  let b=v174ShiftPayBreakdown(shift,member);if(!b.lateMinutes)return '<span class="v174-ot none">Within set hours</span>';
  if(b.approval.status==='Approved')return `<span class="v174-ot approved">✓ ${v169MinutesLabel(b.approvedMinutes)} authorised OT</span>${b.unauthorisedMinutes?`<small>${v169MinutesLabel(b.unauthorisedMinutes)} not payable</small>`:''}`;
  return `<span class="v174-ot pending">⚠ ${v169MinutesLabel(b.lateMinutes)} after finish — not payable</span>`
}
function v174OpenOvertimeApproval(shiftId){
  if(!hasPerm('approve_overtime'))return toast('Overtime authorisation permission is required');let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId),wm=attendanceMemberById(shift?.workerId);if(!shift||!wm)return;
  let b=v174ShiftPayBreakdown(shift,wm.member);if(!b.lateMinutes)return toast('This shift did not run beyond the set finishing hours');let current=b.approval.status==='Approved'?b.approval.minutes:b.lateMinutes;
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">OVERTIME AUTHORISATION</span><h2>${contractorEsc(wm.member.name)}</h2><p class="muted">${attendanceDateOnly(shift.clockIn.time,shift.siteId)} · set hours ${v174ShiftScheduleText(wm.member,shift)} · clocked out ${v169TimeLabel(shift.clockOut.time,shift.siteId)}</p><div class="v174-overtime-warning"><b>${v169MinutesLabel(b.lateMinutes)} recorded after the set finish</b><span>None of this time is included in pay until an authorised user approves it.</span></div><div class="field"><label>Authorised overtime minutes</label><input id="v174-ot-minutes" class="input" type="number" min="0" max="${b.lateMinutes}" step="1" value="${Math.min(current,b.lateMinutes)}"></div><div class="field"><label>Reason / instruction</label><textarea id="v174-ot-reason" class="textarea" placeholder="Who instructed the overtime and why?">${contractorEsc(b.approval.reason||'')}</textarea></div><div class="actions"><button class="btn green" onclick="v174SaveOvertimeApproval('${shift.id}')">✓ Authorise overtime</button>${b.approval.status==='Approved'?`<button class="btn light" onclick="v174DeclineOvertime('${shift.id}')">Remove authorisation</button>`:''}<button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()
}
function v174SaveOvertimeApproval(shiftId){
  if(!hasPerm('approve_overtime'))return;let shift=db.attendanceShifts.find(row=>row.id===shiftId),wm=attendanceMemberById(shift?.workerId),minutes=Math.max(0,Number(document.getElementById('v174-ot-minutes')?.value||0)),reason=(document.getElementById('v174-ot-reason')?.value||'').trim();if(!shift||!wm)return;
  let max=v174ShiftPayBreakdown(shift,wm.member).lateMinutes;if(!minutes||minutes>max)return toast(`Enter between 1 and ${max} minutes`);if(!reason)return toast('A reason / instruction is required');
  shift.overtimeApproval={status:'Approved',minutes,reason,approvedBy:currentUser()?.name||'',approvedById:currentUser()?.id||'',approvedAt:new Date().toISOString()};attendanceMarkApprovalNeedsRecheck(shift.workerId,shift.siteId,shift.clockIn.time);save();audit('attendance','overtime approval',shift.id,`${wm.member.name}: ${minutes} minutes overtime authorised — ${reason}`,shift.siteId);modal=null;savedState('Overtime authorised');render()
}
function v174DeclineOvertime(shiftId){
  if(!hasPerm('approve_overtime'))return;let shift=db.attendanceShifts.find(row=>row.id===shiftId),wm=attendanceMemberById(shift?.workerId);if(!shift||!wm)return;let previous=shift.overtimeApproval||{};
  shift.overtimeApproval={status:'Not Authorised',minutes:0,reason:'Authorisation removed',approvedBy:currentUser()?.name||'',approvedById:currentUser()?.id||'',approvedAt:new Date().toISOString()};attendanceMarkApprovalNeedsRecheck(shift.workerId,shift.siteId,shift.clockIn.time);save();audit('attendance','overtime approval',shift.id,`${wm.member.name}: overtime authorisation removed${previous.minutes?` (${previous.minutes} minutes)`:''}`,shift.siteId);modal=null;savedState('Overtime authorisation removed');render()
}

function v174WorkingScheduleRows(member,siteId){
  return ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'].map((label,dow)=>{let schedule=v174MemberScheduleForDate(member,siteId,v174DateAdd('2026-08-23',dow));return `<div class="v174-workday-row"><label class="v174-day-toggle"><input id="v174-day-${dow}" type="checkbox" ${schedule.active?'checked':''} onchange="v174ToggleWorkday(${dow},this.checked)"><span>${label}</span></label><div id="v174-day-times-${dow}" class="v174-day-times ${schedule.active?'':'disabled'}"><label>Start<input id="v174-start-${dow}" class="input" type="time" value="${schedule.start}" ${schedule.active?'':'disabled'}></label><label>Finish<input id="v174-end-${dow}" class="input" type="time" value="${schedule.end}" ${schedule.active?'':'disabled'}></label></div></div>`}).join('')
}
function v174ToggleWorkday(day,active){let box=document.getElementById(`v174-day-times-${day}`);if(box)box.classList.toggle('disabled',!active);[`v174-start-${day}`,`v174-end-${day}`].forEach(id=>{let input=document.getElementById(id);if(input)input.disabled=!active})}
function v174OpenWorkingHours(workerId,siteId=attendanceDefaultSiteId()){
  if(!hasPerm('manage_working_hours'))return toast('Working-hours permission is required');let wm=attendanceMemberById(workerId);if(!wm)return;
  modal=`<div class="modal"><div class="modalbox v174-hours-modal"><span class="att-kicker">WORKING HOURS</span><h2>${contractorEsc(wm.member.name)}</h2><p class="muted">${contractorEsc(wm.gang.name)} · choose the days and normal start / finish. These hours drive late, early and payable overtime calculations.</p><div class="v174-workweek">${v174WorkingScheduleRows(wm.member,siteId)}</div><div class="actions"><button class="btn orange" onclick="v174SaveWorkingHours('${workerId}','${siteId}')">Save working hours</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()
}
function v174SaveWorkingHours(workerId,siteId){
  if(!hasPerm('manage_working_hours'))return;let wm=attendanceMemberById(workerId);if(!wm)return;let schedule={},usual=[];
  for(let dow=0;dow<7;dow++){let active=!!document.getElementById(`v174-day-${dow}`)?.checked,start=document.getElementById(`v174-start-${dow}`)?.value||'07:30',end=document.getElementById(`v174-end-${dow}`)?.value||'16:30';if(active&&v169ParseTime(end)<=v169ParseTime(start))return toast('Each finishing time must be after its starting time');schedule[dow]={active,start,end};if(active)usual.push(dow)}
  if(!usual.length)return toast('Select at least one normal working day');wm.member.workingSchedule=schedule;wm.member.usualDays=usual;(db.timesheetApprovals||[]).forEach(approval=>{if(approval.workerId===workerId&&!approval.superseded)approval.needsReapproval=true});save();audit('attendance','working hours',wm.member.id,`${wm.member.name}: working hours updated — ${usual.map(d=>['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d]).join(', ')}`,siteId);modal=null;savedState('Working hours saved');render()
}
function v174WorkingHoursManager(siteId){
  if(!hasPerm('manage_working_hours'))return '';let rows=attendanceAllMembers().filter(row=>row.member.activeForAttendance!==false);
  return `<div class="panel v174-hours-manager"><div class="sectionhead"><div><span class="att-kicker">PEOPLE SETTINGS</span><h2>Working hours</h2><div class="muted">Individual schedules override the Site’s default day.</div></div></div><div class="v174-hours-list">${rows.map(row=>{let today=v174MemberScheduleForDate(row.member,siteId,iso());return `<button onclick="v174OpenWorkingHours('${row.member.id}','${siteId}')"><span><b>${contractorEsc(row.member.name)}</b><small>${contractorEsc(row.gang.name)}</small></span><strong>${today.active?today.start+'–'+today.end:'Not working today'} ›</strong></button>`}).join('')||'<div class="muted">No active people.</div>'}</div></div>`
}
function v174PendingOvertimePanel(siteId){
  if(!hasPerm('approve_overtime'))return '';let rows=(db.attendanceShifts||[]).filter(shift=>shift.siteId===siteId&&!shift.voided&&shift.clockOut?.time).map(shift=>{let wm=attendanceMemberById(shift.workerId),b=wm?v174ShiftPayBreakdown(shift,wm.member):null;return wm&&b?.lateMinutes&&b.approval.status!=='Approved'?{shift,wm,b}:null}).filter(Boolean).sort((a,b)=>String(b.shift.clockOut.time).localeCompare(String(a.shift.clockOut.time))).slice(0,20);
  return `<div class="panel v174-pending-overtime"><div class="sectionhead"><div><h2>Overtime awaiting decision</h2><div class="muted">Late clock-outs are excluded from pay until authorised.</div></div><span class="badge ${rows.length?'b-amber':'b-green'}">${rows.length}</span></div>${rows.map(row=>`<div class="v174-overtime-row"><span><b>${contractorEsc(row.wm.member.name)}</b><small>${attendanceDateOnly(row.shift.clockIn.time,row.shift.siteId)} · finish ${row.b.schedule.end} · out ${v169TimeLabel(row.shift.clockOut.time,row.shift.siteId)}</small></span><strong>${v169MinutesLabel(row.b.lateMinutes)}</strong><button class="btn orange smallbtn" onclick="v174OpenOvertimeApproval('${row.shift.id}')">Review</button></div>`).join('')||'<div class="muted">No overtime is awaiting a decision.</div>'}</div>`
}

const v174BaseAttendanceTodayView=attendanceTodayView;
attendanceTodayView=function(siteId){return `${v174WorkingHoursManager(siteId)}${v174PendingOvertimePanel(siteId)}${v174BaseAttendanceTodayView(siteId)}`};
const v174BaseAttendanceCanViewAll=attendanceCanViewAll;
attendanceCanViewAll=function(){return v174BaseAttendanceCanViewAll()||hasPerm('manage_working_hours')||hasPerm('approve_overtime')};
const v174BaseCanOpenView=v173CanOpenView;
v173CanOpenView=function(target,user=currentUser()){if(target==='attendance'&&user&&(v173UserHas(user,'manage_working_hours')||v173UserHas(user,'approve_overtime')))return true;return v174BaseCanOpenView(target,user)};

function v174PeriodBar(kind,preset,from,to){
  let set=kind==='pay'?'v174SetPayPeriod':'v174SetTimesheetPeriod',change=kind==='pay'?'v174SetPayDate':'v174SetTimesheetDate';
  return `<div class="v174-period-bar"><div class="v174-period-presets"><span>Pay period</span>${[['week','Weekly'],['fortnight','Fortnightly'],['month','Monthly'],['custom','Custom']].map(([key,label])=>`<button class="${preset===key?'active':''}" onclick="${set}('${key}')">${label}</button>`).join('')}</div><div class="v174-period-dates"><label>From<input class="input" type="date" value="${from}" onchange="${change}('from',this.value)"></label><span>→</span><label>To<input class="input" type="date" value="${to}" onchange="${change}('to',this.value)"></label><b>${v174RangeLabel(from,to)}</b></div>${from&&to&&from>to?'<div class="payment-range-warning">From date must be before To date.</div>':''}</div>`
}

function v174PayMembers(siteId,from,to){
  let gangIds=new Set((db.items||[]).filter(item=>item.site===siteId&&item.gang).map(item=>item.gang));
  return attendanceAllMembers().filter(row=>gangIds.has(row.gang.id)||v174ShiftsForRange(row.member.id,from,to,siteId).length)
}
function v174PayFilterOptions(){
  let members=attendanceAllMembers().filter(row=>row.member.activeForAttendance!==false).sort((a,b)=>a.member.name.localeCompare(b.member.name));
  return `<option value="all" ${v174Payee==='all'?'selected':''}>All pay records</option><option value="individuals" ${v174Payee==='individuals'?'selected':''}>Individuals only</option><option value="teams" ${v174Payee==='teams'?'selected':''}>Teams / unallocated price work</option><optgroup label="Choose individual">${members.map(row=>`<option value="id:${row.member.id}" ${v174Payee==='id:'+row.member.id?'selected':''}>${contractorEsc(row.member.name)} · ${contractorEsc(row.gang.name)}</option>`).join('')}</optgroup>`
}
function v174PayRowMatches(row){if(v174Payee==='all')return true;if(v174Payee==='individuals')return row.payeeKind==='Individual';if(v174Payee==='teams')return row.payeeKind!=='Individual';return row.payeeKey===v174Payee}
function v174SetPriceRows(siteId,from,to){
  let rows=[];(db.items||[]).filter(item=>item.site===siteId).forEach(item=>{
    let add=(stage,date,target)=>{if(!v174DateInRange(date,from,to))return;let paid=stagePaidAmount(target),value=Number(target.price||0),due=Math.max(0,value-paid),payeeId=target.paidTo||item.gang||'',payeeKind=db.gangs.some(g=>g.id===payeeId)?'Team':'Individual';if(value>0)rows.push({kind:'Set Price',ref:item.ref,stage,date,payee:paymentPayeeName({paidTo:payeeId},item.gang),payeeKey:'id:'+payeeId,payeeKind,value,paid,due,status:due<=0?'Paid':paid>0?'Part Paid':'Ready for Pay'})};
    if(item.type==='House Plot')(item.lifts||[]).forEach(lift=>{if(lift.status==='Complete')add(lift.name,lift.completedDate,lift)});else if(item.status==='Complete')add(item.type,item.completedDate,item)
  });return rows
}
function v174VariationRows(siteId,from,to){
  let rows=[];(db.items||[]).filter(item=>item.site===siteId).forEach(item=>(item.variations||[]).forEach(v=>{if(v.approvalStatus!=='Approved'||!v174DateInRange(v.date,from,to))return;let paid=(v.payments||[]).filter(p=>!p.amended).reduce((n,p)=>n+Number(p.amount||0),0),value=Number(v.value||0),due=Math.max(0,value-paid),payeeId=v.team||item.gang||'',payeeKind=db.gangs.some(g=>g.id===payeeId)?'Team':'Individual';rows.push({kind:'Variation',ref:item.ref,stage:v.name,date:v.date,payee:paymentPayeeName({paidTo:payeeId},item.gang),payeeKey:'id:'+payeeId,payeeKind,value,paid,due,status:due<=0?'Paid':paid>0?'Part Paid':'Ready for Pay'})}));return rows
}
function v174RecordedPaymentsPanel(siteId,from,to){
  let rows=allPaymentRows().filter(row=>row.item?.site===siteId&&v174DateInRange(row.date,from,to)).map(row=>({...row,payeeKey:paymentPayeeKey(row),payeeKind:paymentPayeeKind(row)})).filter(v174PayRowMatches),total=rows.reduce((n,row)=>n+Number(row.amount||0),0);
  return `<section class="v174-recorded-payments"><div class="sectionhead"><div><h3>Payments recorded in this period</h3><div class="muted">Every payment can be isolated by its individual or Team payee.</div></div><b>${attendanceMoney(total)}</b></div>${rows.map(row=>`<div class="v174-pay-person"><span><b>${contractorEsc(paymentPayeeName(row))}</b><small>${contractorEsc(row.item.ref)} · ${contractorEsc(row.paymentType==='Variation'?(row.variationName||row.stage||'Variation'):(row.stage||'Original Work'))} · ${row.date}</small></span><span>${paymentPayeeKind(row)}</span><strong>${attendanceMoney(row.amount||0)}</strong></div>`).join('')||'<div class="muted">No recorded payments match this person and period.</div>'}</section>`
}
contractorPayReview=function(siteId){
  if(!hasPerm('view_financials'))return `<div class="panel empty-help"><b>You do not have permission to view pay information.</b></div>`;v174EnsurePayPeriod();let valid=v174RangeValid(v174PayFrom,v174PayTo),members=valid?v174PayMembers(siteId,v174PayFrom,v174PayTo).filter(row=>['Hourly','Day Rate'].includes(row.member.employmentType)):[],
      timeRows=members.map(row=>({...row,kind:'Time',payee:row.member.name,payeeKey:'id:'+row.member.id,payeeKind:'Individual',tot:v174AttendanceTotalsForRange(row.member.id,v174PayFrom,v174PayTo,siteId)})),setRows=valid?v174SetPriceRows(siteId,v174PayFrom,v174PayTo):[],variationRows=valid?v174VariationRows(siteId,v174PayFrom,v174PayTo):[],
      visibleTime=timeRows.filter(v174PayRowMatches),visibleSet=setRows.filter(v174PayRowMatches),visibleVariations=variationRows.filter(v174PayRowMatches),timeTotal=visibleTime.reduce((n,row)=>n+row.tot.gross,0),setDue=visibleSet.reduce((n,row)=>n+row.due,0),variationDue=visibleVariations.reduce((n,row)=>n+row.due,0),unpaidLate=visibleTime.reduce((n,row)=>n+row.tot.unauthorisedOvertimeHours,0);
  let breakdown=[...visibleTime.map(row=>({name:row.member.name,type:'Individual · '+row.member.employmentType,hours:row.tot.payableHours,due:row.tot.gross})),...visibleSet.map(row=>({name:row.payee,type:`${row.payeeKind} · Set Price`,hours:0,due:row.due})),...visibleVariations.map(row=>({name:row.payee,type:`${row.payeeKind} · Variation`,hours:0,due:row.due}))];
  return `<div class="contractor-section v174-pay-review"><div class="sectionhead"><div><h2>Pay Review</h2><div class="muted">Weekly, fortnightly, monthly or any custom period — with every person isolated when needed.</div></div><div class="actions"><select class="select" onchange="v174Payee=this.value;render()">${v174PayFilterOptions()}</select><button class="btn light" onclick="downloadAttendanceCSV()">⇩ Attendance CSV</button></div></div>${v174PeriodBar('pay',v174PayPreset,v174PayFrom,v174PayTo)}
  ${!valid?'<div class="calendar-prompt"><b>Choose a valid From and To date.</b></div>':`<div class="pay-review-total"><span>RECORDS INDICATE FOR THIS PERIOD</span><b>${attendanceMoney(timeTotal+setDue+variationDue)}</b><small>Operational pay review only — not PAYE, CIS, pensions, tax or bank payment processing.</small></div><div class="v174-pay-metrics"><div><span>Recorded hours</span><b>${visibleTime.reduce((n,row)=>n+row.tot.totalHours,0).toFixed(2)}</b></div><div><span>Payable hours</span><b>${visibleTime.reduce((n,row)=>n+row.tot.payableHours,0).toFixed(2)}</b></div><div class="${unpaidLate?'warn':''}"><span>Late time excluded</span><b>${unpaidLate.toFixed(2)} hrs</b></div><div><span>Authorised OT</span><b>${visibleTime.reduce((n,row)=>n+row.tot.authorisedOvertimeHours,0).toFixed(2)} hrs</b></div></div>
  <div class="v174-individual-breakdown"><div class="sectionhead"><div><h3>Individual / payee breakdown</h3><div class="muted">Use the filter above to isolate one person without changing Team records.</div></div><span>${breakdown.length}</span></div>${breakdown.map(row=>`<div class="v174-pay-person"><span><b>${contractorEsc(row.name)}</b><small>${contractorEsc(row.type)}</small></span><span>${row.hours?row.hours.toFixed(2)+' payable hrs':'Price record'}</span><strong>${attendanceMoney(row.due)}</strong></div>`).join('')||'<div class="muted">No pay records match this person and period.</div>'}</div>
  <div class="pay-review-grid"><section><h3>Set Price — completed in period</h3>${visibleSet.map(row=>`<div class="pay-review-row"><div><b>${contractorEsc(row.ref)}</b><span>${contractorEsc(row.stage)} · ${contractorEsc(row.payee)} · ${row.date} · <em class="pay-state ${row.status.toLowerCase().replace(/\s/g,'-')}">${row.status}</em></span></div><strong>${row.due>0?attendanceMoney(row.due):'Paid'}</strong></div>`).join('')||'<div class="muted">No completed set-price work in this period.</div>'}</section>
  <section><h3>Hourly / Day Rate</h3>${visibleTime.map(row=>`<div class="pay-review-row"><div><b>${contractorEsc(row.member.name)}</b><span>${contractorEsc(row.gang.name)} · ${row.member.employmentType} · ${row.tot.totalHours.toFixed(2)} recorded · ${row.tot.payableHours.toFixed(2)} payable${row.tot.unauthorisedOvertimeHours?` · ${row.tot.unauthorisedOvertimeHours.toFixed(2)} late hrs excluded`:''}</span></div><strong>${attendanceMoney(row.tot.gross)}</strong></div>`).join('')||'<div class="muted">No Hourly or Day Rate workers match this view.</div>'}</section></div>
  <section class="v169-variation-pay"><div class="sectionhead"><h3>Approved Variations dated in period</h3><span>${visibleVariations.length}</span></div>${visibleVariations.map(row=>`<div class="pay-review-row"><div><b>${contractorEsc(row.ref)} · ${contractorEsc(row.stage)}</b><span>${contractorEsc(row.payee)} · ${row.date} · ${row.status}</span></div><strong>${row.due>0?attendanceMoney(row.due):'Paid'}</strong></div>`).join('')||'<div class="muted">No approved Variations match this period.</div>'}</section>${v174RecordedPaymentsPanel(siteId,v174PayFrom,v174PayTo)}`}</div>`
}

attendanceTimesheetView=function(siteId){
  v174EnsureTimesheetPeriod();let valid=v174RangeValid(v174TimesheetFrom,v174TimesheetTo),members=valid?v174PayMembers(siteId,v174TimesheetFrom,v174TimesheetTo):[],days=v174RangeDays(v174TimesheetFrom,v174TimesheetTo),weeklyApproval=days===7&&v174TimesheetFrom===attendanceWeekStartISO(v174TimesheetFrom),financial=hasPerm('view_financials');
  return `${v174PeriodBar('timesheet',v174TimesheetPreset,v174TimesheetFrom,v174TimesheetTo)}<div class="att-export-row"><button class="btn light" onclick="downloadAttendanceCSV()">⇩ CSV</button><button class="btn light" onclick="printAttendanceTimesheet()">▣ Print / Save PDF</button>${weeklyApproval&&hasPerm('approve_timesheets')?'<button class="btn orange" onclick="attendanceWeek=v174TimesheetFrom;attendanceApproveAllWeek()">✓ Approve visible week</button>':'<span class="muted">Timesheet approval remains weekly; viewing can use any range.</span>'}</div><div class="panel att-table-wrap"><table class="att-timesheet-table v174-timesheet-table"><thead><tr><th>Person</th><th>Recorded</th><th>Payable</th><th>Authorised OT</th><th>Late time excluded</th>${financial?'<th>Est. pay</th>':''}<th>Open</th></tr></thead><tbody>${valid&&members.length?members.map(row=>{let t=v174AttendanceTotalsForRange(row.member.id,v174TimesheetFrom,v174TimesheetTo,siteId);return `<tr><td><b>${contractorEsc(row.member.name)}</b><small>${contractorEsc(row.gang.name)} · ${row.member.employmentType}</small></td><td><b>${t.totalHours.toFixed(2)}</b></td><td><b>${t.payableHours.toFixed(2)}</b></td><td>${t.authorisedOvertimeHours.toFixed(2)}</td><td class="${t.unauthorisedOvertimeHours?'v174-unpaid':''}">${t.unauthorisedOvertimeHours.toFixed(2)}</td>${financial?`<td><b>${['Hourly','Day Rate'].includes(row.member.employmentType)?attendanceMoney(t.gross):'—'}</b></td>`:''}<td><button class="btn light smallbtn" onclick="v174OpenWorkerRange('${row.member.id}','${siteId}')">View shifts</button></td></tr>`}).join(''):`<tr><td colspan="7">${valid?'No people or shifts in this range.':'Choose a valid date range.'}</td></tr>`}</tbody></table></div><div class="att-timesheet-key"><span><b>Recorded</b> clock-to-clock less break</span><span><b>Payable</b> excludes unapproved time after the set finish</span></div>`
}
function v174OpenWorkerRange(workerId,siteId){
  if(!v1998CanReviewTimeRecords())return toast('A Director must grant this account shift-review permission');let wm=attendanceMemberById(workerId),t=v174AttendanceTotalsForRange(workerId,v174TimesheetFrom,v174TimesheetTo,siteId);if(!wm)return;
  modal=`<div class="modal"><div class="modalbox att-worker-week"><span class="att-kicker">TIMESHEET DETAIL</span><h2>${contractorEsc(wm.member.name)}</h2><p class="muted">${contractorEsc(wm.gang.name)} · ${v174RangeLabel(v174TimesheetFrom,v174TimesheetTo)}</p><div class="attendance-member-mini"><div><span>RECORDED</span><b>${t.totalHours.toFixed(2)} hrs</b></div><div><span>PAYABLE</span><b>${t.payableHours.toFixed(2)} hrs</b></div><div><span>AUTHORISED OT</span><b>${t.authorisedOvertimeHours.toFixed(2)} hrs</b></div></div><div class="att-shift-list">${t.details.map(row=>`<div class="att-shift-row"><div><b>${new Date(row.shift.clockIn.time).toLocaleDateString('en-GB',{weekday:'short',day:'numeric',month:'short'})}</b><span>${v169TimeLabel(row.shift.clockIn.time,row.shift.siteId)} → ${row.shift.clockOut?.time?v169TimeLabel(row.shift.clockOut.time,row.shift.siteId):'Still clocked in'} · set ${v174ShiftScheduleText(wm.member,row.shift)}</span><small>${row.recordedHours.toFixed(2)} recorded · ${row.payableHours.toFixed(2)} payable · ${row.shift.breakMinutes||0}m break</small></div><div>${v174OvertimeStatusHtml(row.shift,wm.member)}</div><div class="actions">${hasPerm('approve_overtime')&&row.lateMinutes?`<button class="btn orange smallbtn" onclick="v174OpenOvertimeApproval('${row.shift.id}')">Overtime</button>`:''}${hasPerm('manage_attendance')?`<button class="btn light smallbtn" onclick="editAttendanceShift('${row.shift.id}')">Correction</button>`:''}</div></div>`).join('')||'<div class="muted">No shifts in this range.</div>'}</div><button class="btn light" onclick="modal=null;render()">Close</button></div></div>`;render()
}

attendanceMyTimeOnlyView=function(siteId,linked){
  if(!linked)return `<div class="panel empty-help"><b>Your login is not linked to a Team member.</b><div style="margin-top:6px">Ask a SiteSight manager to link your user account to your Team member record before using Clock In / Out.</div></div>`;let start=attendanceWeekStartISO(new Date()),to=v174DateAdd(start,6),t=v174AttendanceTotalsForRange(linked.member.id,start,to,siteId);
  return `${attendanceTodayCard()}<div class="panel"><div class="sectionhead"><div><h2>My week</h2><div class="muted">${attendanceWeekLabel(start)} · ${siteName(siteId)}</div></div><div class="v174-my-hours"><span><b>${t.totalHours.toFixed(2)}</b> recorded</span><span><b>${t.payableHours.toFixed(2)}</b> payable</span></div></div>${t.unauthorisedOvertimeHours?`<div class="v174-worker-notice"><b>${t.unauthorisedOvertimeHours.toFixed(2)} hours after set finishing times are awaiting authorisation.</b><span>This time is visible in your record but is not included in estimated pay unless an authorised manager approves it.</span></div>`:''}<div class="att-shift-list">${t.details.slice().reverse().map(row=>`<div class="att-shift-row"><div><b>${new Date(row.shift.clockIn.time).toLocaleDateString('en-GB',{weekday:'short',day:'numeric',month:'short'})}</b><span>${v169TimeLabel(row.shift.clockIn.time,row.shift.siteId)} → ${row.shift.clockOut?.time?v169TimeLabel(row.shift.clockOut.time,row.shift.siteId):'Still clocked in'} · ${row.shift.breakMinutes||0}m unpaid break</span><small>${row.recordedHours.toFixed(2)} recorded · ${row.payableHours.toFixed(2)} payable · set hours ${v174ShiftScheduleText(linked.member,row.shift)}</small></div><div>${v174OvertimeStatusHtml(row.shift,linked.member)}</div></div>`).join('')||'<div class="muted">No hours recorded this week.</div>'}</div></div><div class="att-privacy-note"><b>Your privacy</b><span>SiteSight checks location only when you deliberately press Clock In or Clock Out. There is no continuous location tracking.</span></div>`
};

function v174RecordRangeBar(scope,label='Date range'){
  let r=v174RecordRanges[scope]||{from:'',to:''};return `<div class="panel v174-record-range"><div><b>${label}</b><span>Use either date or both.</span></div><label>From<input class="input" type="date" value="${r.from}" onchange="v174SetRecordRange('${scope}','from',this.value)"></label><span>→</span><label>To<input class="input" type="date" value="${r.to}" onchange="v174SetRecordRange('${scope}','to',this.value)"></label>${r.from||r.to?`<button class="btn light smallbtn" onclick="v174ClearRecordRange('${scope}')">Clear</button>`:''}${r.from&&r.to&&r.from>r.to?'<b class="v174-range-error">Check dates</b>':''}</div>`
}
function v174SetRecordRange(scope,which,value){v174RecordRanges[scope][which]=value;render()}
function v174ClearRecordRange(scope){v174RecordRanges[scope]={from:'',to:''};render()}
function v174InsertAfterTopbar(html,bar){let start=html.indexOf('<div class="topbar">'),panel=html.indexOf('<div class="panel"',start);return start>=0&&panel>=0?html.slice(0,panel)+bar+html.slice(panel):bar+html}
function v174FilterArrayDuring(key,rows,fn){let old=db[key];db[key]=rows;try{return fn()}finally{db[key]=old}}

const v174BaseDiaryPage=siteDiaryPage;
siteDiaryPage=function(){let r=v174RecordRanges.diary,rows=(db.siteDiary||[]).filter(entry=>v174DateInRange(entry.date,r.from,r.to)),html=v174FilterArrayDuring('siteDiary',rows,()=>v174BaseDiaryPage());return v174InsertAfterTopbar(html,v174RecordRangeBar('diary','Diary date range'))};
const v174BaseDayworksPage=contractorDayworksPage;
contractorDayworksPage=function(){let r=v174RecordRanges.dayworks,rows=(db.dayworks||[]).filter(entry=>v174DateInRange(entry.date,r.from,r.to)),html=v174FilterArrayDuring('dayworks',rows,()=>v174BaseDayworksPage());return v174InsertAfterTopbar(html,v174RecordRangeBar('dayworks','Daywork date range'))};
const v174BaseLeavePage=contractorLeavePage;
contractorLeavePage=function(siteId){let r=v174RecordRanges.leave,rows=(db.leaveRequests||[]).filter(entry=>v174RangesOverlap(entry.from,entry.to,r.from,r.to)),html=v174FilterArrayDuring('leaveRequests',rows,()=>v174BaseLeavePage(siteId));return v174RecordRangeBar('leave','Leave overlap range')+html};
const v174BaseAuditPage=auditPage;
auditPage=function(){let r=v174RecordRanges.audit,rows=(db.auditLog||[]).filter(entry=>v174DateInRange(entry.createdAt,r.from,r.to)),html=v174FilterArrayDuring('auditLog',rows,()=>v174BaseAuditPage());return v174InsertAfterTopbar(html,v174RecordRangeBar('audit','Audit date range'))};
const v174BaseIssuesPage=issuesPage;
issuesPage=function(){let r=v174RecordRanges.issues,original=(db.items||[]).map(item=>[item,item.issues]);(db.items||[]).forEach(item=>item.issues=(item.issues||[]).filter(issue=>!r.from&&!r.to||[issue.created,issue.targetDate,issue.resolved].some(date=>v174DateInRange(date,r.from,r.to))));let html;try{html=v174BaseIssuesPage()}finally{original.forEach(([item,issues])=>item.issues=issues)}return v174InsertAfterTopbar(html,v174RecordRangeBar('issues','Created, due or resolved in range'))};

function v174WorkItemDates(item){return [...itemActivityDates(item),item.createdAt,item.plannedStart,item.targetComplete,item.signedOffAt].filter(Boolean)}
const v174BaseWorkItems=workItems;
workItems=function(){let r=v174RecordRanges.work,rows=(db.items||[]).filter(item=>!r.from&&!r.to||v174WorkItemDates(item).some(date=>v174DateInRange(date,r.from,r.to))),html=v174FilterArrayDuring('items',rows,()=>v174BaseWorkItems());return v174InsertAfterTopbar(html,v174RecordRangeBar('work','Work record date range'))};
const v174BaseActivityPage=activityPage;
activityPage=function(){let r=v174RecordRanges.activity;if(!r.from&&!r.to)return v174InsertAfterTopbar(v174BaseActivityPage(),v174RecordRangeBar('activity','Custom activity range'));let items=itemsUpdatedBetween(r.from,r.to);return `<div class="topbar"><div><h1>Activity Summary</h1><div class="sub">Items with a dated update in the selected range</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${s.name}</option>`).join('')}</select></div></div>${v174RecordRangeBar('activity','Custom activity range')}<div class="panel"><div class="activityrow" style="background:#f7f9fc;font-weight:900"><span>Reference</span><span>Type</span><span class="activity-hide">Site / Team</span><span>Status</span><span></span></div>${items.length?items.map(x=>`<div class="activityrow" onclick="openItem('${x.id}')"><b>${x.ref}</b><span>${x.type}${x.houseType?' · '+x.houseType:''}</span><span class="activity-hide">${siteName(x.site)} · ${gangName(x.gang)}</span><span>${badge(overall(x))}</span><b>›</b></div>`).join(''):'<div class="empty">No items updated in this period.</div>'}</div>`};
const v174BaseSignedPage=signedPage;
signedPage=function(){let r=v174RecordRanges.signed,rows=(db.items||[]).filter(item=>!r.from&&!r.to||v174DateInRange(item.signedOffAt||item.lastUpdatedAt||item.completedDate,r.from,r.to)),html=v174FilterArrayDuring('items',rows,()=>v174BaseSignedPage());return v174InsertAfterTopbar(html,v174RecordRangeBar('signed','Sign-off date range'))};
const v174BaseSiteDrawingsPage=siteDrawingsPage;
siteDrawingsPage=function(){let r=v174RecordRanges.drawings,rows=(db.siteDrawings||[]).filter(drawing=>!r.from&&!r.to||[drawing.uploadedDate,drawing.uploadedAt,drawing.revisionDate,drawing.supersededAt].some(date=>v174DateInRange(date,r.from,r.to))),html=v174FilterArrayDuring('siteDrawings',rows,()=>v174BaseSiteDrawingsPage());return v174InsertAfterTopbar(html,v174RecordRangeBar('drawings','Drawing record date range'))};

function v174CalendarAllRows(){
  let rows=[],push=(date,kind,title,sub,action,end='')=>{if(date)rows.push({date:v174Date(date),end:v174Date(end),kind,title,sub,action})};
  (db.calendarNotes||[]).filter(n=>canAccessSite(n.siteId)&&(site==='all'||n.siteId===site)).forEach(n=>push(n.date,n.category&&n.category!=='General'?'siteevent':'note',n.title,`${siteName(n.siteId)}${n.notes?' · '+n.notes:''}`,`openCalendarNote('${n.id}')`));
  (db.items||[]).filter(item=>canViewItem(item)&&(site==='all'||item.site===site)).forEach(item=>{
    if(item.plannedStart)push(item.plannedStart,'programme',`${item.ref} · planned start`,`${item.type} · ${gangName(item.gang)}`,`openItem('${item.id}')`);if(item.targetComplete)push(item.targetComplete,'programme',`${item.ref} · target completion`,`${item.type} · ${gangName(item.gang)}`,`openItem('${item.id}')`);
    if(hasPerm('view_issues'))(item.issues||[]).filter(issue=>issue.targetDate).forEach(issue=>push(issue.targetDate,'issue',`${item.ref} · ${issue.title}`,`${issue.status} · ${issueAssigneeLabel(issue.assignedTo)}`,`viewIssue('${item.id}','${issue.id}')`));
    if(item.type==='House Plot')(item.lifts||[]).filter(lift=>lift.status==='Complete'&&lift.completedDate).forEach(lift=>push(lift.completedDate,'complete',`${item.ref} · ${lift.name}`,`Completed · ${gangName(item.gang)}`,`openItem('${item.id}')`));else if(item.status==='Complete'&&item.completedDate)push(item.completedDate,'complete',`${item.ref} · ${item.type}`,`Completed · ${gangName(item.gang)}`,`openItem('${item.id}')`)
  });
  if(hasPerm('view_site_diary'))(db.siteDiary||[]).filter(entry=>canAccessSite(entry.siteId)&&(site==='all'||entry.siteId===site)).forEach(entry=>push(entry.date,'diary','Site Diary recorded',`${siteName(entry.siteId)} · ${entry.by||'Site record'}`,`goNav('diary')`));
  if(contractorCanViewLeave())(db.leaveRequests||[]).filter(r=>r.status==='Approved'&&canAccessSite(r.siteId)&&(site==='all'||r.siteId===site)).forEach(r=>push(r.from,r.type==='Holiday'?'holiday':r.type==='Training'?'training':'leave',`${contractorMemberName(r.workerId)} · ${r.type}`,`${contractorTeamName(r.workerId)} · ${siteName(r.siteId)}`,`contractorAttendanceTab='leave';goNav('attendance')`,r.to));
  if(contractorCanViewLeave())(db.attendanceAbsences||[]).filter(a=>canAccessSite(a.siteId)&&(site==='all'||a.siteId===site)).forEach(a=>push(a.date,a.type==='Sick'?'sick':a.type==='Training'?'training':'leave',`${contractorMemberName(a.workerId)} · ${a.type}`,`${contractorTeamName(a.workerId)} · ${siteName(a.siteId)}`,`contractorAttendanceTab='leave';goNav('attendance')`));
  if(hasPerm('work_items'))(db.dayworks||[]).filter(d=>canAccessSite(d.siteId)&&(site==='all'||d.siteId===site)).forEach(d=>push(d.date,'daywork',d.area||'Daywork',`${siteName(d.siteId)} · ${d.work||''}`,`goNav('dayworks')`));
  return rows
}
function v174SetCalendarAgenda(mode){v174CalendarAgendaMode=mode;if(mode==='custom'&&(!v174CalendarFrom||!v174CalendarTo)){v174CalendarFrom=v174DateAdd(iso(),-30);v174CalendarTo=v174DateAdd(iso(),30)}render()}
function v174CalendarAgendaPanel(){
  let today=iso(),all=v174CalendarAllRows(),rows=all.filter(row=>v174CalendarAgendaMode==='upcoming'?row.date>=today:v174CalendarAgendaMode==='past'?row.date<today:v174DateInRange(row.date,v174CalendarFrom,v174CalendarTo));rows.sort((a,b)=>v174CalendarAgendaMode==='past'?b.date.localeCompare(a.date):a.date.localeCompare(b.date));
  return `<div class="panel v174-calendar-agenda"><div class="sectionhead"><div><span class="calendar-kicker">EVENT AGENDA</span><h2>Upcoming & past events</h2><div class="muted">Calendar notes, programme dates, people events, issues, completions, Diary and Dayworks.</div></div>${hasPerm('manage_calendar')?'<button class="btn orange smallbtn" onclick="addCalendarNote()">＋ Add event</button>':''}</div><div class="v174-agenda-tabs"><button class="${v174CalendarAgendaMode==='upcoming'?'active':''}" onclick="v174SetCalendarAgenda('upcoming')">Upcoming <b>${all.filter(row=>row.date>=today).length}</b></button><button class="${v174CalendarAgendaMode==='past'?'active':''}" onclick="v174SetCalendarAgenda('past')">Past <b>${all.filter(row=>row.date<today).length}</b></button><button class="${v174CalendarAgendaMode==='custom'?'active':''}" onclick="v174SetCalendarAgenda('custom')">Custom range</button></div>${v174CalendarAgendaMode==='custom'?`<div class="v174-agenda-dates"><label>From<input class="input" type="date" value="${v174CalendarFrom}" onchange="v174CalendarFrom=this.value;render()"></label><span>→</span><label>To<input class="input" type="date" value="${v174CalendarTo}" onchange="v174CalendarTo=this.value;render()"></label></div>`:''}<div class="v174-agenda-list">${rows.slice(0,80).map(row=>`<button class="v174-agenda-row event-${row.kind}" onclick="${row.action}"><time><b>${new Date(row.date+'T12:00:00').toLocaleDateString('en-GB',{day:'2-digit',month:'short'})}</b><span>${row.date.slice(0,4)}</span></time><i>${row.kind==='issue'?'⚠':row.kind==='complete'?'✓':row.kind==='holiday'?'◷':row.kind==='sick'?'＋':row.kind==='programme'?'◆':row.kind==='daywork'?'£':'▣'}</i><span><b>${calendarSafe(row.title)}</b><small>${calendarSafe(row.sub)}${row.end&&row.end!==row.date?' · to '+row.end:''}</small></span><strong>›</strong></button>`).join('')||'<div class="empty-help"><b>No events in this view.</b><div style="margin-top:6px">Try another tab or custom date range.</div></div>'}</div>${rows.length>80?`<div class="muted">Showing the first 80 of ${rows.length} events. Narrow the custom range to focus the list.</div>`:''}</div>`
}
calendarNotesPanel=function(){return v174CalendarAgendaPanel()};
const v174BaseCalendarDayEvents=calendarDayEvents;
calendarDayEvents=function(date){let base=v174BaseCalendarDayEvents(date),extra=v174CalendarAllRows().filter(row=>row.date===date&&row.kind==='programme').map(row=>({kind:row.kind,title:row.title,sub:row.sub,itemId:(String(row.action).match(/openItem\('([^']+)'/)||[])[1]||''}));return [...base,...extra]};
const v174BaseCalendarSummary=calendarEventSummary;
calendarEventSummary=function(counts){return v174BaseCalendarSummary(counts)+(counts.programme?`<span class="month-chip chip-programme">${counts.programme} programme</span>`:'')+(counts.daywork?`<span class="month-chip chip-daywork">${counts.daywork} daywork</span>`:'')};

// Migrate safely from the latest V1.73 data without changing prior packages.
const v174BaseMigrate=migrate;
migrate=function(persist=true){
  v174BaseMigrate(false);let changed=false;
  (db.users||[]).forEach(user=>{V174_PERMISSIONS.forEach(([key])=>{let wanted=user.role==='Director';if(user.permissions?.[key]===undefined){user.permissions=user.permissions||{};user.permissions[key]=wanted;changed=true}});if(Number(user.permissionTemplateVersion||0)<174){user.permissionTemplateVersion=174;changed=true}});
  (db.attendanceShifts||[]).forEach(shift=>{if(!shift.overtimeApproval){shift.overtimeApproval={status:'Not Authorised',minutes:0,reason:'',approvedBy:'',approvedById:'',approvedAt:''};changed=true}});
  (db.gangs||[]).forEach(gang=>(gang.members||[]).forEach(member=>{if(member.workingSchedule===undefined){member.workingSchedule=null;changed=true}}));
  if(persist&&(changed||persist))save()
};
if((db.users||[]).length)migrate(false);

// Keep permission-only working-hours/overtime accounts usable without exposing other pages.
const v174BaseAttendancePage=attendancePage;
attendancePage=function(){
  if((hasPerm('manage_working_hours')||hasPerm('approve_overtime'))&&!hasPerm('view_attendance')&&!hasPerm('clock_attendance')&&!hasPerm('manage_attendance')&&!hasPerm('approve_timesheets')){let sid=attendanceDefaultSiteId();return `<div class="topbar"><div><h1>People & Time Controls</h1><div class="sub">Only the permissions granted to this account are shown.</div></div><div class="actions"><select class="select" onchange="attendanceSetSite(this.value)">${activeSites().map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${s.name}</option>`).join('')}</select></div></div>${v174WorkingHoursManager(sid)}${v174PendingOvertimePanel(sid)}`}
  return v174BaseAttendancePage()
};

/* ===== v175-daily-control.js ===== */
'use strict';

// SiteSight V1.75 — immediate clocking, finish reminders, payroll control,
// reusable working patterns, exceptions-first management and guided setup.

// ---- Permission and role defaults -----------------------------------
const V175_PERMISSIONS=[['lock_payroll','Lock & Export Pay Periods']];
const V175_TIME_PERMISSION=['grant_time_controls','Grant Time-control Permissions'];
const v175CommercialPermissionGroup=permissionGroups.find(group=>group.name==='Commercial');
V175_PERMISSIONS.forEach(def=>{
  if(v175CommercialPermissionGroup&&!v175CommercialPermissionGroup.items.some(([key])=>key===def[0]))v175CommercialPermissionGroup.items.push(def);
  if(!permissionDefs.some(([key])=>key===def[0]))permissionDefs.push(def)
});
const v175AttendancePermissionGroup=permissionGroups.find(group=>group.name==='Time & Attendance');
if(v175AttendancePermissionGroup&&!v175AttendancePermissionGroup.items.some(([key])=>key===V175_TIME_PERMISSION[0]))v175AttendancePermissionGroup.items.push(V175_TIME_PERMISSION);
if(!permissionDefs.some(([key])=>key===V175_TIME_PERMISSION[0]))permissionDefs.push(V175_TIME_PERMISSION);
['Contracts Manager','Accounts'].forEach(roleName=>{
  let profile=V173_ROLE_PROFILES[roleName];
  if(profile&&Array.isArray(profile.permissions)&&!profile.permissions.includes('lock_payroll'))profile.permissions.push('lock_payroll')
});
if(!V173_ROLE_PROFILES['Contracts Manager'].permissions.includes('grant_time_controls'))V173_ROLE_PROFILES['Contracts Manager'].permissions.push('grant_time_controls');

let v175EditingPatternId='',v175ReminderTimer=null;

function v175Clone(value){return JSON.parse(JSON.stringify(value))}
function v175Id(prefix){return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`}
function v175Hash(value){let text=String(value||''),hash=2166136261;for(let i=0;i<text.length;i++){hash^=text.charCodeAt(i);hash=Math.imul(hash,16777619)}return (hash>>>0).toString(16).padStart(8,'0')}
function v175DefaultPatternSchedule(start='07:30',end='16:30'){let schedule={};for(let day=0;day<7;day++)schedule[day]={active:day>=1&&day<=5,start,end};return schedule}

// ---- Delegated management time controls ----------------------------
const V175_TIME_MANAGER_ROLES=['Director','Contracts Manager','Site Manager','Supervisor'];
function v175TimeManagerRole(user=currentUser()){return !!user&&V175_TIME_MANAGER_ROLES.includes(user.role)}
function v175CanCorrectTime(user=currentUser()){return v175TimeManagerRole(user)&&(user.role==='Director'||!!user.permissions?.manage_attendance)}
function v175CanApproveOvertime(user=currentUser()){return v175TimeManagerRole(user)&&(user.role==='Director'||!!user.permissions?.approve_overtime)}
function v175TimeGrantTargets(){
  let grantor=currentUser();if(!grantor||!['Director','Contracts Manager'].includes(grantor.role)||(grantor.role!=='Director'&&!grantor.permissions?.grant_time_controls))return [];
  let roles=grantor.role==='Director'?['Contracts Manager','Site Manager','Supervisor']:['Site Manager','Supervisor'];
  return (db.users||[]).filter(user=>user.id!==grantor.id&&user.accountActive!==false&&roles.includes(user.role))
}
function v175TimePermissionPanel(){
  if(!hasPerm('grant_time_controls')||!['Director','Contracts Manager'].includes(currentUser()?.role||''))return '';let people=v175TimeGrantTargets();
  return `<div class="panel v175-time-access"><div class="sectionhead"><div><span class="att-kicker">DELEGATED CONTROL</span><h2>Who can manage time?</h2><div class="muted">Directors and Contracts Managers can grant these limited controls only to management roles. Every change is audited.</div></div></div><div class="v175-time-access-list">${people.map(user=>`<div class="v175-time-access-row"><span><b>${contractorEsc(user.name)}</b><small>${contractorEsc(user.role)}</small></span><label><input id="v175-grant-hours-${user.id}" type="checkbox" ${user.permissions?.manage_working_hours?'checked':''}> Set hours</label><label><input id="v175-grant-ot-${user.id}" type="checkbox" ${user.permissions?.approve_overtime?'checked':''}> Approve overtime</label><label><input id="v175-grant-edit-${user.id}" type="checkbox" ${user.permissions?.manage_attendance?'checked':''}> Correct time</label><button class="btn light smallbtn" onclick="v175SaveTimePermissions('${user.id}')">Save</button></div>`).join('')||'<div class="empty-help"><b>No eligible management users to show.</b><div style="margin-top:6px">Add a Site Manager or Supervisor account first.</div></div>'}</div></div>`
}
function v175SaveTimePermissions(userId){
  if(!hasPerm('grant_time_controls')||!['Director','Contracts Manager'].includes(currentUser()?.role||''))return toast('Time-control delegation permission is required');let target=v175TimeGrantTargets().find(user=>user.id===userId);if(!target)return toast('This account is outside your delegation level');
  let previous={manage_working_hours:!!target.permissions?.manage_working_hours,approve_overtime:!!target.permissions?.approve_overtime,manage_attendance:!!target.permissions?.manage_attendance};target.permissions=target.permissions||{};target.permissions.manage_working_hours=!!document.getElementById(`v175-grant-hours-${userId}`)?.checked;target.permissions.approve_overtime=!!document.getElementById(`v175-grant-ot-${userId}`)?.checked;target.permissions.manage_attendance=!!document.getElementById(`v175-grant-edit-${userId}`)?.checked;target.permissionProfileMode='custom';save();audit('attendance','user permissions',target.id,`Time controls updated for ${target.name}`, '',JSON.stringify({previous,updated:{manage_working_hours:target.permissions.manage_working_hours,approve_overtime:target.permissions.approve_overtime,manage_attendance:target.permissions.manage_attendance}}));savedState('Time-control permissions saved');render()
}

const v175BaseSupervisorAttendanceClock=supervisorAttendanceClock,v175BaseSaveSupervisorAttendanceClock=saveSupervisorAttendanceClock,v175BaseEditAttendanceShift=editAttendanceShift,v175BaseSaveAttendanceShiftCorrection=saveAttendanceShiftCorrection,v175BaseAddHistoricalAttendance=addHistoricalAttendance,v175BaseSaveHistoricalAttendance=saveHistoricalAttendance;
supervisorAttendanceClock=function(...args){if(!v175CanCorrectTime())return toast('Only an authorised Supervisor or Manager can edit time');return v175BaseSupervisorAttendanceClock(...args)};
saveSupervisorAttendanceClock=async function(...args){if(!v175CanCorrectTime())return toast('Only an authorised Supervisor or Manager can edit time');return v175BaseSaveSupervisorAttendanceClock(...args)};
editAttendanceShift=function(...args){if(!v175CanCorrectTime())return toast('Only an authorised Supervisor or Manager can edit time');return v175BaseEditAttendanceShift(...args)};
saveAttendanceShiftCorrection=function(...args){if(!v175CanCorrectTime())return toast('Only an authorised Supervisor or Manager can edit time');return v175BaseSaveAttendanceShiftCorrection(...args)};
addHistoricalAttendance=function(...args){if(!v175CanCorrectTime())return toast('Only an authorised Supervisor or Manager can edit time');return v175BaseAddHistoricalAttendance(...args)};
saveHistoricalAttendance=function(...args){if(!v175CanCorrectTime())return toast('Only an authorised Supervisor or Manager can edit time');return v175BaseSaveHistoricalAttendance(...args)};
const v175BaseOpenOvertimeApproval=v174OpenOvertimeApproval,v175BaseSaveOvertimeApproval=v174SaveOvertimeApproval,v175BaseDeclineOvertime=v174DeclineOvertime,v175BasePendingOvertimePanel=v174PendingOvertimePanel;
v174OpenOvertimeApproval=function(...args){if(!v175CanApproveOvertime())return toast('Only an authorised Supervisor or Manager can approve overtime');return v175BaseOpenOvertimeApproval(...args)};
v174SaveOvertimeApproval=function(...args){if(!v175CanApproveOvertime())return toast('Only an authorised Supervisor or Manager can approve overtime');return v175BaseSaveOvertimeApproval(...args)};
v174DeclineOvertime=function(...args){if(!v175CanApproveOvertime())return toast('Only an authorised Supervisor or Manager can decide overtime');return v175BaseDeclineOvertime(...args)};
v174PendingOvertimePanel=function(...args){return v175CanApproveOvertime()?v175BasePendingOvertimePanel(...args):''};

// ---- Standalone Clock In / Out -------------------------------------
V173_MAIN_NAV.clock={icon:'◷',label:'Clock In / Out'};
V173_MORE_TOOLS.clock=['◷','Clock In / Out','Immediate access to verified clocking'];
const v175BaseCanOpenView=v173CanOpenView;
v173CanOpenView=function(target,user=currentUser()){
  if(target==='clock')return !!user&&v173UserHas(user,'clock_attendance')&&!!user.linkedMemberId;
  return v175BaseCanOpenView(target,user)
};
const v175BaseHomeView=v173HomeView;
v173HomeView=function(user=currentUser()){
  if(user&&v173CanOpenView('clock',user)&&(user.role==='Operator'||v173IsOperativeAccount(user)))return 'clock';
  return v175BaseHomeView(user)
};
const v175BaseRoleNavItems=v173RoleNavItems;
v173RoleNavItems=function(user=currentUser()){
  let items=v175BaseRoleNavItems(user);
  if(user&&!v173UserHas(user,'view_attendance')&&!v173UserHas(user,'manage_attendance')&&!v173UserHas(user,'approve_timesheets'))items=items.filter(item=>item.id!=='attendance');
  if(v173CanOpenView('clock',user))items=[{id:'clock',...V173_MAIN_NAV.clock},...items.filter(item=>item.id!=='clock')];
  return items
};
const v175BaseNavButton=v173NavButton;
v173NavButton=function(item,user=currentUser(),mobile=false){
  if(item.id!=='clock')return v175BaseNavButton(item,user,mobile);
  let active=attendanceActiveShift(user?.linkedMemberId),label=active?'Clock Out':'Clock In';
  return `<button class="v175-clock-nav ${v173NavActive('clock')?'active':''} ${active?'clocked-in':''}" onclick="goNav('clock')"><span>◷</span><b>${label}</b><small>${active?'Currently clocked in':'Ready when you are'}</small></button>`
};

function v175ClockPage(){
  let linked=attendanceLinkedMember();
  if(!linked)return `<div class="topbar"><div><h1>Clock In / Out</h1><div class="sub">Immediate access to your time record</div></div></div><div class="panel empty-help"><b>Your login is not linked to a person.</b><div style="margin-top:6px">Ask a manager to link your user account to your Team member record.</div></div>`;
  let active=attendanceActiveShift(linked.member.id),siteId=active?.siteId||attendanceDefaultSiteId(),today=iso(),schedule=v174MemberScheduleForDate(linked.member,siteId,today),start=attendanceWeekStartISO(new Date()),tot=v174AttendanceTotalsForRange(linked.member.id,start,v174DateAdd(start,6),siteId),state=v175FinishReminderState();
  return `<div class="topbar v175-clock-topbar"><div><span class="att-kicker">MY WORKING DAY</span><h1>Clock In / Out</h1><div class="sub">No menus or timesheets — your clock button is ready immediately.</div></div><span class="v175-clock-site">⌖ ${siteName(siteId)}</span></div>
  <div class="v175-clock-hero ${active?'active':''}"><div><span>${active?'YOU ARE CLOCKED IN':'READY TO START'}</span><h2>${contractorEsc(linked.member.name)}</h2><p>${contractorEsc(linked.gang.name)} · scheduled ${schedule.active?schedule.start+'–'+schedule.end:'non-working day'}</p>${active?`<small>Started ${v169TimeLabel(active.clockIn.time,active.siteId)} · ${siteName(active.siteId)}</small>`:''}</div><button class="${active?'out':'in'}" onclick="openAttendanceClock('${active?'out':'in'}')"><i>${active?'■':'▶'}</i><b>${active?'CLOCK OUT':'CLOCK IN'}</b><span>${active?'Finish my shift':'Start my shift'}</span></button></div>
  ${active?`<div class="v175-finish-reminder ${state.due?'due':''}"><span>🔔</span><div><b>${state.due?'Your scheduled finish time has passed':'Clock-out reminder is set'}</b><p>${state.due?'Please clock out when you have finished. Any late time remains unpaid unless authorised.':`SiteSight will remind you at ${schedule.end} while this shift is active.`}</p></div></div>`:''}
  <div class="v175-clock-week"><div><span>THIS WEEK</span><b>${tot.totalHours.toFixed(2)} recorded</b></div><div><span>PAYABLE</span><b>${tot.payableHours.toFixed(2)} hours</b></div><div><span>AWAITING OT DECISION</span><b>${tot.unauthorisedOvertimeHours.toFixed(2)} hours</b></div></div>
  <div class="panel v175-clock-recent"><div class="sectionhead"><div><h2>Recent shifts</h2><div class="muted">Your real clock times and payable hours stay visible.</div></div></div>${tot.details.slice().reverse().slice(0,5).map(row=>`<div><span><b>${attendanceDateOnly(row.shift.clockIn.time,row.shift.siteId)}</b><small>${v169TimeLabel(row.shift.clockIn.time,row.shift.siteId)} → ${row.shift.clockOut?.time?v169TimeLabel(row.shift.clockOut.time,row.shift.siteId):'Still clocked in'}</small></span><span>${row.recordedHours.toFixed(2)} recorded</span><strong>${row.payableHours.toFixed(2)} payable</strong></div>`).join('')||'<div class="muted">No shifts recorded this week.</div>'}</div>`
}

// ---- Scheduled finish reminder ------------------------------------
const v175BaseEventVisibleToUser=eventVisibleToUser;
eventVisibleToUser=function(event,user){if(event?.targetUserId)return event.targetUserId===user?.id;return v175BaseEventVisibleToUser(event,user)};
function v175FinishReminderState(now=new Date()){
  let user=currentUser(),linked=attendanceLinkedMember(),active=linked?attendanceActiveShift(linked.member.id):null;
  if(!user||!linked||!active)return {due:false,active:null};
  let date=attendanceDateOnly(now.toISOString(),active.siteId),shiftDate=attendanceDateOnly(active.clockIn?.time,active.siteId),schedule=v174MemberScheduleForDate(linked.member,active.siteId,date),minutes=v169TimeMinutes(now.toISOString(),active.siteId),finish=v169ParseTime(schedule.end,'16:30');
  return {user,linked,active,date,schedule,due:date===shiftDate&&schedule.active&&minutes>=finish,sent:active.finishReminderDate===date}
}
function v175FinishReminderTick(now=new Date()){
  let state=v175FinishReminderState(now);if(!state.due||state.sent)return false;
  state.active.finishReminderDate=state.date;state.active.finishReminderSentAt=now.toISOString();
  db.notifications=db.notifications||[];let message=`Scheduled finish ${state.schedule.end}: please Clock Out when you have finished.`;
  db.notifications.unshift({id:v175Id('finish-reminder'),kind:'clock-reminder',siteId:state.active.siteId,text:message,itemId:state.active.id,targetUserId:state.user.id,actorId:'system',actorName:'SiteSight',createdAt:now.toISOString(),readBy:[]});db.notifications=db.notifications.slice(0,250);
  audit('attendance','clock reminder',state.active.id,`${state.linked.member.name}: scheduled finish reminder sent at ${state.schedule.end}`,state.active.siteId);toast(message);deliverNotifications();if(view==='clock')render();return true
}
function v175StartReminderTimer(){if(v175ReminderTimer)clearInterval(v175ReminderTimer);v175ReminderTimer=setInterval(()=>v175FinishReminderTick(),30000);setTimeout(()=>v175FinishReminderTick(),250)}

// ---- Reusable working-pattern templates -----------------------------
function v175PatternById(id){return (db.workingPatternTemplates||[]).find(pattern=>pattern.id===id)}
function v175PatternSummary(pattern){let active=Object.entries(pattern.schedule||{}).filter(([,day])=>day.active);if(!active.length)return 'No working days';let times=[...new Set(active.map(([,day])=>`${day.start}–${day.end}`))];return `${active.length} days · ${times.join(' / ')}`}
function v175WorkingPatternPanel(siteId){
  if(!hasPerm('manage_working_hours'))return '';
  let patterns=db.workingPatternTemplates||[];
  return `<div class="panel v175-pattern-panel"><div class="sectionhead"><div><span class="att-kicker">QUICK SETUP</span><h2>Saved working patterns</h2><div class="muted">Set common hours once, then apply them to several people.</div></div><button class="btn orange smallbtn" onclick="v175OpenPatternEditor()">＋ New pattern</button></div><div class="v175-pattern-grid">${patterns.map(pattern=>`<div><span><b>${contractorEsc(pattern.name)}</b><small>${v175PatternSummary(pattern)}</small></span><button class="btn blue smallbtn" onclick="v175OpenPatternApply('${pattern.id}','${siteId}')">Apply to people</button><button class="btn light smallbtn" onclick="v175OpenPatternEditor('${pattern.id}')">Edit</button>${pattern.builtIn?'':`<button class="btn light smallbtn" onclick="v175DeletePattern('${pattern.id}')">Delete</button>`}</div>`).join('')}</div></div>`
}
function v175PatternScheduleRows(pattern){
  let schedule=pattern?.schedule||v175DefaultPatternSchedule();
  return ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'].map((label,day)=>{let row=schedule[day]||{active:false,start:'07:30',end:'16:30'};return `<div class="v174-workday-row"><label class="v174-day-toggle"><input id="v175-pattern-active-${day}" type="checkbox" ${row.active?'checked':''} onchange="v175TogglePatternDay(${day},this.checked)"><span>${label}</span></label><div id="v175-pattern-times-${day}" class="v174-day-times ${row.active?'':'disabled'}"><label>Start<input id="v175-pattern-start-${day}" class="input" type="time" value="${row.start||'07:30'}" ${row.active?'':'disabled'}></label><label>Finish<input id="v175-pattern-end-${day}" class="input" type="time" value="${row.end||'16:30'}" ${row.active?'':'disabled'}></label></div></div>`}).join('')
}
function v175TogglePatternDay(day,active){let box=document.getElementById(`v175-pattern-times-${day}`);if(box)box.classList.toggle('disabled',!active);[`v175-pattern-start-${day}`,`v175-pattern-end-${day}`].forEach(id=>{let input=document.getElementById(id);if(input)input.disabled=!active})}
function v175OpenPatternEditor(id=''){
  if(!hasPerm('manage_working_hours'))return;let pattern=v175PatternById(id)||{name:'',schedule:v175DefaultPatternSchedule()};v175EditingPatternId=id;
  modal=`<div class="modal"><div class="modalbox v174-hours-modal"><span class="att-kicker">WORKING PATTERN</span><h2>${id?'Edit pattern':'Create a reusable pattern'}</h2><div class="field"><label>Pattern name</label><input id="v175-pattern-name" class="input" value="${contractorEsc(pattern.name)}" placeholder="e.g. Standard site week"></div><div class="v174-workweek">${v175PatternScheduleRows(pattern)}</div><div class="actions"><button class="btn orange" onclick="v175SavePattern()">Save pattern</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()
}
function v175ReadPatternSchedule(){let schedule={},usual=[];for(let day=0;day<7;day++){let active=!!document.getElementById(`v175-pattern-active-${day}`)?.checked,start=document.getElementById(`v175-pattern-start-${day}`)?.value||'07:30',end=document.getElementById(`v175-pattern-end-${day}`)?.value||'16:30';if(active&&v169ParseTime(end)<=v169ParseTime(start))return null;schedule[day]={active,start,end};if(active)usual.push(day)}return usual.length?{schedule,usual}:null}
function v175SavePattern(){
  if(!hasPerm('manage_working_hours'))return;let name=(document.getElementById('v175-pattern-name')?.value||'').trim(),read=v175ReadPatternSchedule();if(!name)return toast('Enter a pattern name');if(!read)return toast('Select at least one day and check that every finish is after its start');
  let pattern=v175PatternById(v175EditingPatternId);if(pattern){pattern.name=name;pattern.schedule=read.schedule;pattern.updatedAt=new Date().toISOString();pattern.updatedBy=currentUser()?.name||''}else{pattern={id:v175Id('pattern'),name,schedule:read.schedule,builtIn:false,createdAt:new Date().toISOString(),createdBy:currentUser()?.name||''};db.workingPatternTemplates.push(pattern)}
  save();audit('attendance','working pattern',pattern.id,`Working pattern saved — ${name}`,'');modal=null;savedState('Working pattern saved');render()
}
function v175OpenPatternApply(patternId,siteId){
  if(!hasPerm('manage_working_hours'))return;let pattern=v175PatternById(patternId),people=attendanceAllMembers().filter(row=>row.member.activeForAttendance!==false);if(!pattern)return;
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">BULK APPLY</span><h2>${contractorEsc(pattern.name)}</h2><p class="muted">Choose the people who should use ${v175PatternSummary(pattern)}. Existing individual hours for those people will be replaced.</p><div class="v175-person-picker">${people.map(row=>`<label><input type="checkbox" data-v175-pattern-person="${row.member.id}"><span><b>${contractorEsc(row.member.name)}</b><small>${contractorEsc(row.gang.name)} · ${row.member.employmentType}</small></span></label>`).join('')}</div><div class="actions"><button class="btn orange" onclick="v175ApplyPattern('${patternId}','${siteId}')">Apply to selected people</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()
}
function v175ApplyPattern(patternId,siteId){
  if(!hasPerm('manage_working_hours'))return;let pattern=v175PatternById(patternId),ids=[...document.querySelectorAll('[data-v175-pattern-person]:checked')].map(box=>box.dataset.v175PatternPerson);if(!pattern||!ids.length)return toast('Select at least one person');
  ids.forEach(id=>{let wm=attendanceMemberById(id);if(!wm)return;wm.member.workingSchedule=v175Clone(pattern.schedule);wm.member.usualDays=Object.entries(pattern.schedule).filter(([,day])=>day.active).map(([day])=>Number(day));(db.timesheetApprovals||[]).forEach(approval=>{if(approval.workerId===id&&!approval.superseded)approval.needsReapproval=true})});
  save();audit('attendance','working pattern',pattern.id,`${pattern.name} applied to ${ids.length} people`,siteId);modal=null;savedState(`Pattern applied to ${ids.length} people`);render()
}
function v175DeletePattern(id){if(!hasPerm('manage_working_hours'))return;let pattern=v175PatternById(id);if(!pattern||pattern.builtIn)return;if(!confirm(`Delete working pattern “${pattern.name}”?`))return;db.workingPatternTemplates=db.workingPatternTemplates.filter(row=>row.id!==id);save();audit('attendance','working pattern',id,`Working pattern deleted — ${pattern.name}`,'');render()}
const v175BaseWorkingHoursManager=v174WorkingHoursManager;
v174WorkingHoursManager=function(siteId){return v175WorkingPatternPanel(siteId)+v175BaseWorkingHoursManager(siteId)};
const v175BaseAttendancePage=attendancePage;
attendancePage=function(){let html=v175BaseAttendancePage();return hasPerm('grant_time_controls')?v174InsertAfterTopbar(html,v175TimePermissionPanel()):html};

// ---- Payroll Review → Approve → Lock → Export ----------------------
function v175PayrollApprovalState(siteId,from,to){
  let missing=[],recheck=[],members=v174PayMembers(siteId,from,to).filter(row=>['Hourly','Day Rate'].includes(row.member.employmentType));
  for(let week=attendanceWeekStartISO(from);week<=to;week=v174DateAdd(week,7))members.forEach(row=>{if(!v174ShiftsForRange(row.member.id,week,v174DateAdd(week,6),siteId).length)return;let approval=attendanceApproval(row.member.id,siteId,week);if(!approval)missing.push({workerId:row.member.id,week});else if(approval.needsReapproval)recheck.push({workerId:row.member.id,week})});
  return {missing,recheck,ready:!missing.length&&!recheck.length}
}
function v175PayrollSnapshot(siteId,from,to){
  let members=v174PayMembers(siteId,from,to).filter(row=>['Hourly','Day Rate'].includes(row.member.employmentType)),people=members.map(row=>{let total=v174AttendanceTotalsForRange(row.member.id,from,to,siteId);return {id:row.member.id,name:row.member.name,type:row.member.employmentType,recorded:Number(total.totalHours.toFixed(4)),payable:Number(total.payableHours.toFixed(4)),authorisedOT:Number(total.authorisedOvertimeHours.toFixed(4)),excluded:Number(total.unauthorisedOvertimeHours.toFixed(4)),gross:Number(total.gross.toFixed(2))}}),setPrice=v174SetPriceRows(siteId,from,to),variations=v174VariationRows(siteId,from,to),payments=allPaymentRows().filter(row=>row.item?.site===siteId&&v174DateInRange(row.date,from,to)).map(row=>({id:row.id||'',date:row.date,amount:Number(row.amount||0),payee:paymentPayeeKey(row)})),shiftState=(db.attendanceShifts||[]).filter(shift=>shift.siteId===siteId&&v174DateInRange(attendanceDateOnly(shift.clockIn?.time,shift.siteId),from,to)).map(shift=>({id:shift.id,in:shift.clockIn?.time||'',out:shift.clockOut?.time||'',break:Number(shift.breakMinutes||0),voided:!!shift.voided,ot:shift.overtimeApproval||null,corrections:(shift.corrections||[]).length})).sort((a,b)=>a.id.localeCompare(b.id));
  let totals={recorded:people.reduce((n,row)=>n+row.recorded,0),payable:people.reduce((n,row)=>n+row.payable,0),gross:people.reduce((n,row)=>n+row.gross,0),excluded:people.reduce((n,row)=>n+row.excluded,0),setDue:setPrice.reduce((n,row)=>n+row.due,0),variationDue:variations.reduce((n,row)=>n+row.due,0)},payload={siteId,from,to,people,setPrice,variations,payments,shiftState,totals};return {...payload,hash:v175Hash(JSON.stringify(payload))}
}
function v175CurrentPayrollLock(siteId,from,to){return (db.payrollLocks||[]).filter(lock=>lock.siteId===siteId&&lock.from===from&&lock.to===to&&!lock.superseded&&!lock.unlockedAt).sort((a,b)=>String(b.lockedAt).localeCompare(String(a.lockedAt)))[0]||null}
function v175LockPayPeriod(siteId){
  if(!hasPerm('lock_payroll'))return toast('Pay-period lock permission is required');v174EnsurePayPeriod();if(!v174RangeValid(v174PayFrom,v174PayTo))return toast('Choose a valid pay period');let approvals=v175PayrollApprovalState(siteId,v174PayFrom,v174PayTo);if(!approvals.ready)return toast(`Approve the outstanding weekly timesheets first (${approvals.missing.length} awaiting, ${approvals.recheck.length} to re-approve)`);let snapshot=v175PayrollSnapshot(siteId,v174PayFrom,v174PayTo),old=v175CurrentPayrollLock(siteId,v174PayFrom,v174PayTo);if(!confirm(`Lock ${v174RangeLabel(v174PayFrom,v174PayTo)} at ${attendanceMoney(snapshot.totals.gross+snapshot.totals.setDue+snapshot.totals.variationDue)}? Changes after locking will be flagged.`))return;if(old)old.superseded=true;let lock={id:v175Id('pay-lock'),siteId,from:v174PayFrom,to:v174PayTo,status:'Locked',snapshotHash:snapshot.hash,snapshot:snapshot.totals,lockedAt:new Date().toISOString(),lockedBy:currentUser()?.name||'',lockedById:currentUser()?.id||'',superseded:false,unlockedAt:'',unlockedBy:'',unlockReason:''};db.payrollLocks.push(lock);save();audit('payments','pay period',lock.id,`Pay period locked — ${v174PayFrom} to ${v174PayTo} · ${attendanceMoney(snapshot.totals.gross+snapshot.totals.setDue+snapshot.totals.variationDue)}`,siteId);savedState('Pay period locked');render()
}
function v175UnlockPayPeriod(id){if(!hasPerm('lock_payroll'))return;let lock=(db.payrollLocks||[]).find(row=>row.id===id);if(!lock)return;let reason=prompt('Reason for unlocking this pay period');if(!String(reason||'').trim())return toast('An unlock reason is required');lock.unlockedAt=new Date().toISOString();lock.unlockedBy=currentUser()?.name||'';lock.unlockReason=String(reason).trim();save();audit('payments','pay period',lock.id,`Pay period unlocked — ${lock.unlockReason}`,lock.siteId);savedState('Pay period unlocked');render()}
function v175PayrollExportRows(siteId,from,to){
  let snapshot=v175PayrollSnapshot(siteId,from,to),lock=v175CurrentPayrollLock(siteId,from,to),rows=[['SiteSight Pay Period Export'],['Site',siteName(siteId)],['From',from],['To',to],['Lock status',lock?(lock.snapshotHash===snapshot.hash?'Locked':'Changed since lock'):'Not locked'],[],['Payee','Record type','Recorded hours','Payable hours','Authorised OT','Late time excluded','Gross / Due']];
  snapshot.people.forEach(row=>rows.push([row.name,`Individual · ${row.type}`,row.recorded.toFixed(2),row.payable.toFixed(2),row.authorisedOT.toFixed(2),row.excluded.toFixed(2),row.gross.toFixed(2)]));snapshot.setPrice.forEach(row=>rows.push([row.payee,`Set Price · ${row.ref} · ${row.stage}`,'','','','',row.due.toFixed(2)]));snapshot.variations.forEach(row=>rows.push([row.payee,`Variation · ${row.ref} · ${row.stage}`,'','','','',row.due.toFixed(2)]));return rows
}
function v175ExportPayroll(siteId){
  if(!hasPerm('view_financials'))return;v174EnsurePayPeriod();if(!v174RangeValid(v174PayFrom,v174PayTo))return toast('Choose a valid pay period');let rows=v175PayrollExportRows(siteId,v174PayFrom,v174PayTo),escape=value=>'"'+String(value??'').replace(/"/g,'""')+'"',blob=new Blob([rows.map(row=>row.map(escape).join(',')).join('\r\n')],{type:'text/csv;charset=utf-8'}),link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download=`sitesight-pay-period-${v174PayFrom}-to-${v174PayTo}.csv`;link.click();setTimeout(()=>URL.revokeObjectURL(link.href),1000);audit('payments','pay export',v174PayFrom,`Pay period exported — ${v174PayFrom} to ${v174PayTo}`,siteId)
}
function v175PayrollWorkflow(siteId){
  v174EnsurePayPeriod();if(!v174RangeValid(v174PayFrom,v174PayTo))return '';let approvals=v175PayrollApprovalState(siteId,v174PayFrom,v174PayTo),snapshot=v175PayrollSnapshot(siteId,v174PayFrom,v174PayTo),lock=v175CurrentPayrollLock(siteId,v174PayFrom,v174PayTo),changed=!!lock&&lock.snapshotHash!==snapshot.hash,step=lock&&!changed?4:approvals.ready?3:2;
  return `<div class="panel v175-payroll-workflow"><div class="sectionhead"><div><span class="att-kicker">PAY PERIOD CONTROL</span><h2>Review → Approve → Lock → Export</h2><div class="muted">A plain, auditable hand-off for Accounts. SiteSight still does not process PAYE, CIS, tax or bank payments.</div></div><span class="v175-lock-state ${changed?'changed':lock?'locked':'open'}">${changed?'⚠ Changed since lock':lock?'✓ Locked':'Open for review'}</span></div><div class="v175-payroll-steps">${[['Review','Check people, price work and exclusions'],['Approve',approvals.ready?'Weekly timesheets approved':`${approvals.missing.length} awaiting · ${approvals.recheck.length} re-approve`],['Lock',lock&&!changed?`${lock.lockedBy} · ${new Date(lock.lockedAt).toLocaleString()}`:'Protect this reviewed snapshot'],['Export','Create payroll-ready CSV']].map(([name,detail],index)=>{let number=index+1,done=number<step;return `<div class="${done?'done':number===step?'current':''}"><i>${done?'✓':number}</i><span><b>${name}</b><small>${detail}</small></span></div>`}).join('')}</div>${changed?'<div class="v175-payroll-change"><b>Records changed after this period was locked.</b><span>Review the changed hours/pay records, re-approve affected timesheets and create a new lock before exporting.</span></div>':''}<div class="v175-payroll-actions"><button class="btn light" onclick="contractorAttendanceTab='timesheets';render()">Review approvals</button>${hasPerm('lock_payroll')?`<button class="btn orange" onclick="v175LockPayPeriod('${siteId}')">${lock?'Re-lock reviewed period':'Lock pay period'}</button>${lock?`<button class="btn light" onclick="v175UnlockPayPeriod('${lock.id}')">Unlock</button>`:''}`:''}<button class="btn green" onclick="v175ExportPayroll('${siteId}')">⇩ Export pay-period CSV</button></div></div>`
}
const v175BaseContractorPayReview=contractorPayReview;
contractorPayReview=function(siteId){let html=v175BaseContractorPayReview(siteId);return hasPerm('view_financials')?v175PayrollWorkflow(siteId)+html:html};

// ---- Exceptions-first daily queue ----------------------------------
function v175ExceptionRows(){
  let rows=[],siteId=site==='all'?'all':site,today=iso(),siteMatches=id=>siteId==='all'||id===siteId;
  if(hasPerm('approve_overtime')){let count=(db.attendanceShifts||[]).filter(shift=>siteMatches(shift.siteId)&&!shift.voided&&shift.clockOut?.time).filter(shift=>{let wm=attendanceMemberById(shift.workerId),breakdown=wm?v174ShiftPayBreakdown(shift,wm.member):null;return breakdown?.lateMinutes&&breakdown.approval.status!=='Approved'}).length;if(count)rows.push({kind:'overtime',count,title:'Overtime decisions',detail:`${count} late clock-out${count===1?'':'s'} excluded from pay`,action:"contractorAttendanceTab='today';goNav('attendance')"})}
  if(hasPerm('manage_attendance')){let count=(db.attendanceShifts||[]).filter(shift=>siteMatches(shift.siteId)&&!shift.voided&&!shift.clockOut?.time&&attendanceDateOnly(shift.clockIn?.time,shift.siteId)<today).length;if(count)rows.push({kind:'clock',count,title:'Missing Clock Outs',detail:`${count} earlier shift${count===1?' is':'s are'} still open`,action:"contractorAttendanceTab='today';goNav('attendance')"})}
  if(hasPerm('approve_timesheets')){let count=(db.timesheetApprovals||[]).filter(approval=>!approval.superseded&&approval.needsReapproval&&siteMatches(approval.siteId)).length;if(count)rows.push({kind:'timesheet',count,title:'Timesheets changed',detail:`${count} approval${count===1?' needs':'s need'} checking again`,action:"contractorAttendanceTab='timesheets';goNav('attendance')"})}
  if(hasPerm('view_issues')){let issuesDue=[];(db.items||[]).filter(item=>siteMatches(item.site)&&canAccessSite(item.site)).forEach(item=>(item.issues||[]).filter(issue=>issue.status==='Open'&&issue.targetDate&&issue.targetDate<today).forEach(issue=>issuesDue.push({item,issue})));if(issuesDue.length)rows.push({kind:'issue',count:issuesDue.length,title:'Overdue Issues',detail:'Ownership or target date needs attention',action:"issueFocusFilter='overdue';goNav('issues')"})}
  if(hasPerm('manage_gangs')||hasPerm('gangs')){let soon=v174DateAdd(today,30),count=attendanceAllMembers().reduce((n,row)=>n+(row.member.qualifications||[]).filter(q=>q.expiry&&q.expiry>=today&&q.expiry<=soon).length,0);if(count)rows.push({kind:'qualification',count,title:'Cards expiring soon',detail:`${count} qualification${count===1?'':'s'} expire within 30 days`,action:"goNav('gangs')"})}
  if(hasPerm('lock_payroll')){let changed=(db.payrollLocks||[]).filter(lock=>!lock.superseded&&!lock.unlockedAt&&siteMatches(lock.siteId)).filter(lock=>v175PayrollSnapshot(lock.siteId,lock.from,lock.to).hash!==lock.snapshotHash).length;if(changed)rows.push({kind:'payroll',count:changed,title:'Locked pay changed',detail:'Review and re-lock before payroll export',action:"contractorAttendanceTab='pay';goNav('attendance')"})}
  return rows
}
function v175CanSeeExceptions(){return ['Director','Contracts Manager','Site Manager','Supervisor','Quantity Surveyor','Accounts'].includes(currentUser()?.role||'')}
function v175ExceptionsPanel(){
  if(!v175CanSeeExceptions())return '';let rows=v175ExceptionRows(),total=rows.reduce((n,row)=>n+row.count,0);
  return `<div class="panel v175-exceptions"><div class="sectionhead"><div><span class="att-kicker">TODAY'S ACTIONS</span><h2>Exceptions to check</h2><div class="muted">Only items outside the normal flow are shown.</div></div><span class="v175-exception-total ${total?'attention':'clear'}">${total||'✓'} ${total?'to check':'All clear'}</span></div><div class="v175-exception-grid">${rows.map(row=>`<button class="${row.kind}" onclick="${row.action}"><i>${row.count}</i><span><b>${row.title}</b><small>${row.detail}</small></span><strong>Review ›</strong></button>`).join('')||'<div class="v175-all-clear"><b>No exceptions need your attention.</b><span>Normal work stays out of this queue so it remains easy to scan.</span></div>'}</div></div>`
}

// ---- Guided first-site setup ---------------------------------------
function v175SetupSteps(){
  let people=attendanceAllMembers(),siteConfigured=(db.sites||[]).some(s=>!s.archived&&attendanceLocationConfigured(s.id));
  return [
    {id:'site',label:'Confirm Site location',detail:'Set the GPS point and attendance radius.',done:siteConfigured},
    {id:'teams',label:'Add Teams and people',detail:'Create the gangs and named workforce.',done:(db.gangs||[]).length>0&&people.length>0},
    {id:'hours',label:'Set working patterns',detail:'Apply normal start and finish times.',done:people.length>0&&people.every(row=>!!row.member.workingSchedule)},
    {id:'pay',label:'Choose pay types and rates',detail:'Set Price, Hourly, Day Rate or untracked.',done:people.some(row=>row.member.employmentType&&row.member.employmentType!=='Not tracked')},
    {id:'users',label:'Create user access',detail:'Give each role only the tools it needs.',done:(db.users||[]).length>1},
    {id:'work',label:'Add the first Work Items',detail:'Create or import the plot schedule.',done:(db.items||[]).length>0}
  ]
}
function v175SetupGo(step){if(step==='site'||step==='hours'){contractorAttendanceTab='today';goNav('attendance')}else if(step==='teams'||step==='users'){adminTab=step==='teams'?'teams':'users';goNav('admin')}else if(step==='pay')goNav('gangs');else goNav('work-items')}
function v175DismissSetup(){let user=currentUser();if(!user)return;user.setupChecklistDismissed=true;save();render()}
function v175SetupPanel(){
  let user=currentUser();if(!user||user.role!=='Director'||user.setupChecklistDismissed)return '';let steps=v175SetupSteps(),done=steps.filter(step=>step.done).length;if(done===steps.length&&user.setupChecklistCompletedAt)return '';
  if(done===steps.length&&!user.setupChecklistCompletedAt){user.setupChecklistCompletedAt=new Date().toISOString();save()}
  return `<div class="panel v175-setup"><div class="v175-setup-head"><div><span>FIRST SITE SETUP</span><h2>${done===steps.length?'Setup complete':'Finish setting up SiteSight'}</h2><p>${done} of ${steps.length} essentials complete. Work through one clear step at a time.</p></div><div class="v175-setup-progress"><b>${Math.round(done/steps.length*100)}%</b><i><span style="width:${done/steps.length*100}%"></span></i></div></div><div class="v175-setup-steps">${steps.map((step,index)=>`<button class="${step.done?'done':''}" onclick="${step.done?'':'v175SetupGo(\''+step.id+'\')'}"><i>${step.done?'✓':index+1}</i><span><b>${step.label}</b><small>${step.done?'Complete':step.detail}</small></span><strong>${step.done?'Done':'Start ›'}</strong></button>`).join('')}</div><button class="v175-setup-dismiss" onclick="v175DismissSetup()">${done===steps.length?'Close checklist':'Hide for now'}</button></div>`
}

const v175BaseDashboard=dashboard;
dashboard=function(){return v174InsertAfterTopbar(v175BaseDashboard(),v175SetupPanel()+v175ExceptionsPanel())};
const v175BaseWorkItems=workItems;
workItems=function(){let html=v175BaseWorkItems();return currentUser()?.role==='Supervisor'?v174InsertAfterTopbar(html,v175ExceptionsPanel()):html};

// ---- Migration and render lifecycle --------------------------------
const v175BaseMigrate=migrate;
migrate=function(persist=true){
  v175BaseMigrate(false);let changed=false,matureSetup=(db.items||[]).length>0&&(db.gangs||[]).length>0&&(db.users||[]).length>1;db.payrollLocks=Array.isArray(db.payrollLocks)?db.payrollLocks:[];db.workingPatternTemplates=Array.isArray(db.workingPatternTemplates)?db.workingPatternTemplates:[];
  if(!db.workingPatternTemplates.length){db.workingPatternTemplates.push({id:'v175-standard-site-week',name:'Standard Site Week',schedule:v175DefaultPatternSchedule(),builtIn:true,createdAt:new Date().toISOString(),createdBy:'SiteSight'});changed=true}
  (db.users||[]).forEach(user=>{let wanted=['Director','Contracts Manager','Accounts'].includes(user.role),canGrant=['Director','Contracts Manager'].includes(user.role);if(user.permissions?.lock_payroll===undefined){user.permissions=user.permissions||{};user.permissions.lock_payroll=wanted;changed=true}if(user.permissions?.grant_time_controls===undefined){user.permissions=user.permissions||{};user.permissions.grant_time_controls=canGrant;changed=true}else if(!canGrant&&user.permissions.grant_time_controls){user.permissions.grant_time_controls=false;changed=true}if(!V175_TIME_MANAGER_ROLES.includes(user.role)){if(user.permissions?.approve_overtime){user.permissions.approve_overtime=false;changed=true}if(user.permissions?.manage_attendance){user.permissions.manage_attendance=false;changed=true}if(user.permissions?.manage_working_hours){user.permissions.manage_working_hours=false;changed=true}}if(user.setupChecklistDismissed===undefined){user.setupChecklistDismissed=matureSetup;changed=true}if(Number(user.permissionTemplateVersion||0)<175){user.permissionTemplateVersion=175;changed=true}});
  if(persist&&(changed||persist))save()
};
if((db.users||[]).length)migrate(false);
const v175BaseCompleteFirstSetup=completeFirstSetup;
completeFirstSetup=function(){v175BaseCompleteFirstSetup();if((db.users||[]).length){migrate(false);save()}};
const v175BaseRender=render;
render=function(){let user=currentUser();if(user&&view==='clock'){if(!v173CanOpenView('clock',user)){view=v173HomeView(user);return v175BaseRender()}shell(v175ClockPage());updateSyncUI();return}return v175BaseRender()};

v175StartReminderTimer();

/* ===== v176-workforce-flow.js ===== */
'use strict';

// SiteSight V1.76 — workforce flow, management clocking, accessible controls,
// drawing/calender fixes, people administration and accounting export profiles.

const V176_INDIVIDUALS_ID='v176-individuals';
const V176_MANAGEMENT_ROLES=['Director','Contracts Manager','Site Manager','Supervisor'];
let v176AssistListening=false,v176AssistRecognition=null,v176ExportProfileId='';

function v176Id(prefix){return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`}
function v176Esc(value){return contractorEsc(String(value??''))}
function v176ActiveTeams(){return (db.gangs||[]).filter(team=>team.id!==V176_INDIVIDUALS_ID&&team.active!==false)}
function v176Individuals(create=false){let group=(db.gangs||[]).find(team=>team.id===V176_INDIVIDUALS_ID);if(!group&&create){group={id:V176_INDIVIDUALS_ID,name:'Individuals / Unassigned',active:true,isIndividuals:true,contact:'',members:[]};db.gangs.push(group)}return group||null}
function v176ActivePeople(){return attendanceAllMembers().filter(row=>row.member.activeForAttendance!==false&&row.member.employmentStatus!=='Leaver')}
function v176ManagementClockUser(user=currentUser()){return !!user&&V176_MANAGEMENT_ROLES.includes(user.role)}
function v176FindArchivedPerson(id){return (db.peopleArchive||[]).find(row=>row.member?.id===id)||null}

// ---- Migration ------------------------------------------------------
const v176BaseMigrate=migrate;
migrate=function(persist=true){
  v176BaseMigrate(false);let changed=false;
  ['peopleArchive','teamArchive','payrollExportProfiles'].forEach(key=>{if(!Array.isArray(db[key])){db[key]=[];changed=true}});
  if(!db.payrollExportProfiles.length){
    db.payrollExportProfiles.push({id:'v176-generic-payroll',name:'Generic payroll CSV',system:'Generic CSV',costCode:'',columns:['employeeName','employeeReference','recordType','payableHours','authorisedOvertime','gross','costCode'],builtIn:true,createdAt:new Date().toISOString(),createdBy:'SiteSight'});changed=true
  }
  ['Contracts Manager','Site Manager','Supervisor'].forEach(roleName=>{let profile=V173_ROLE_PROFILES[roleName];if(profile&&Array.isArray(profile.permissions)&&!profile.permissions.includes('clock_attendance'))profile.permissions.push('clock_attendance')});
  (db.users||[]).forEach(user=>{
    if(['Contracts Manager','Site Manager','Supervisor'].includes(user.role)&&user.linkedMemberId&&!user.permissions?.clock_attendance){user.permissions=user.permissions||{};user.permissions.clock_attendance=true;changed=true}
    if(Number(user.permissionTemplateVersion||0)<176){user.permissionTemplateVersion=176;changed=true}
  });
  if(persist&&(changed||persist))save()
};
if((db.users||[]).length)migrate(false);
const v176BaseCompleteFirstSetup=completeFirstSetup;
completeFirstSetup=function(){v176BaseCompleteFirstSetup();if((db.users||[]).length){migrate(false);save()}};

// ---- Management clocking: explicit action, no GPS ------------------
const v176BaseOpenAttendanceClock=openAttendanceClock;
openAttendanceClock=function(action){
  if(!v176ManagementClockUser())return v176BaseOpenAttendanceClock(action);
  let linked=attendanceLinkedMember();if(!linked)return toast('Your SiteSight account is not linked to a person. Ask a Director to link it first.');if(!hasPerm('clock_attendance'))return toast('You do not have Clock In / Out permission');
  let active=attendanceActiveShift(linked.member.id),isOut=action==='out';if(isOut&&!active)return toast('You are not currently clocked in');if(!isOut&&active)return toast('You are already clocked in');let sid=isOut?active.siteId:attendanceDefaultSiteId(),sites=activeSites();if(!sid&&sites.length)sid=sites[0].id;
  modal=`<div class="modal"><div class="modalbox attendance-clock-modal v176-management-clock"><span class="att-kicker">MANAGEMENT CLOCK</span><h2>${isOut?'Clock Out':'Clock In'}</h2><p><b>${v176Esc(linked.member.name)}</b> · ${v176Esc(linked.gang.name)}</p>${!isOut?`<div class="field"><label>Site</label><select id="att-clock-site" class="select">${sites.map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${v176Esc(s.name)}</option>`).join('')}</select></div>`:`<div class="att-clock-current"><span>Clocked in</span><b>${new Date(active.clockIn.time).toLocaleString('en-GB')}</b><small>${v176Esc(siteName(active.siteId))}</small></div>`}<div class="v176-no-gps"><b>✓ No location check</b><span>Supervisors and higher may clock from outside Site. SiteSight records server time only when you press the button. Signing in or using the system outside working hours never records hours or location.</span></div><div id="att-clock-progress" class="att-clock-progress"></div><button id="att-clock-action" class="btn ${isOut?'red':'green'} attendance-big-clock" onclick="attendanceDoClock('${isOut?'clock-out':'clock-in'}')">${isOut?'■ CLOCK OUT':'▶ CLOCK IN'}</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
};

const v176BaseAttendanceDoClock=attendanceDoClock;
attendanceDoClock=async function(action){
  if(!v176ManagementClockUser())return v176BaseAttendanceDoClock(action);
  let linked=attendanceLinkedMember();if(!linked)return;let active=attendanceActiveShift(linked.member.id),sid=action==='clock-out'?active?.siteId:(document.getElementById('att-clock-site')?.value||attendanceDefaultSiteId());if(!sid)return toast('Select a Site');if(!sharedMode||!navigator.onLine)return toast('Management clocking needs an online connection so SiteSight can use trusted server time. No GPS is requested.');
  let btn=document.getElementById('att-clock-action'),prog=document.getElementById('att-clock-progress');if(btn){btn.disabled=true;btn.textContent='Saving server time…'}if(prog)prog.textContent='No location or device position is being requested.';
  try{if(pendingSave)await syncToServer(true);let payload={action,workerId:linked.member.id,siteId:sid,actorId:currentUser()?.id||''},r=await fetch('/api/attendance/management-stamp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}),result=await r.json();if(result.data){db=result.data;sharedRevision=result.revision||sharedRevision;lastSyncedDB=cloneDB(db);pendingSave=false;syncState='synced';lastSyncAt=new Date().toISOString();migrate(false);persistCache();try{localStorage.setItem(BASE_KEY,JSON.stringify(db));localStorage.removeItem(PENDING_KEY)}catch(e){}}modal=null;view='clock';render();if(result.ok)savedState(action==='clock-in'?'Clocked in — server time recorded, no location':'Clocked out — server time recorded, no location');else toast(result.message||'Clock attempt rejected')}catch(e){if(btn){btn.disabled=false;btn.textContent=action==='clock-in'?'▶ CLOCK IN':'■ CLOCK OUT'}if(prog)prog.textContent=e.message||'Clocking could not be completed';toast(e.message||'Clocking could not be completed')}
};

const v176BaseAttendanceVerificationBadge=attendanceVerificationBadge;
attendanceVerificationBadge=function(point){if(point?.status==='Management Clock')return '<span class="att-badge v176-management-badge">◷ Management · no GPS</span>';return v176BaseAttendanceVerificationBadge(point)};

const v176BaseClockPage=v175ClockPage;
v175ClockPage=function(){let html=v176BaseClockPage();if(!v176ManagementClockUser())return html;return html.replace('No menus or timesheets — your clock button is ready immediately.','Clock only when recording paid attendance. Normal SiteSight use never starts a shift or requests location.').replace(/⌖ /g,'Site · ')};

// ---- Scrollable time choices ---------------------------------------
function v176TimeOptions(value='07:30'){let out=[];for(let minutes=240;minutes<=1430;minutes+=15){let h=String(Math.floor(minutes/60)).padStart(2,'0'),m=String(minutes%60).padStart(2,'0'),v=`${h}:${m}`;out.push(`<option value="${v}" ${v===value?'selected':''}>${v}</option>`)}if(!out.some(option=>option.includes(`value="${value}"`)))out.unshift(`<option value="${value}" selected>${value}</option>`);return out.join('')}
v174WorkingScheduleRows=function(member,siteId){return ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'].map((label,dow)=>{let schedule=v174MemberScheduleForDay(member,siteId,dow);return `<div class="v174-workday-row"><label class="v174-day-toggle"><input id="v174-day-${dow}" type="checkbox" ${schedule.active?'checked':''} onchange="v174ToggleWorkday(${dow},this.checked)"><span>${label}</span></label><div id="v174-day-times-${dow}" class="v174-day-times ${schedule.active?'':'disabled'}"><label>Start<select id="v174-start-${dow}" class="select v176-time-select" ${schedule.active?'':'disabled'}>${v176TimeOptions(schedule.start)}</select></label><label>Finish<select id="v174-end-${dow}" class="select v176-time-select" ${schedule.active?'':'disabled'}>${v176TimeOptions(schedule.end)}</select></label></div></div>`}).join('')};
v175PatternScheduleRows=function(pattern){let schedule=pattern?.schedule||v175DefaultPatternSchedule();return ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'].map((label,day)=>{let row=schedule[day]||{active:false,start:'07:30',end:'16:30'};return `<div class="v174-workday-row"><label class="v174-day-toggle"><input id="v175-pattern-active-${day}" type="checkbox" ${row.active?'checked':''} onchange="v175TogglePatternDay(${day},this.checked)"><span>${label}</span></label><div id="v175-pattern-times-${day}" class="v174-day-times ${row.active?'':'disabled'}"><label>Start<select id="v175-pattern-start-${day}" class="select v176-time-select" ${row.active?'':'disabled'}>${v176TimeOptions(row.start||'07:30')}</select></label><label>Finish<select id="v175-pattern-end-${day}" class="select v176-time-select" ${row.active?'':'disabled'}>${v176TimeOptions(row.end||'16:30')}</select></label></div></div>`}).join('')};

// ---- Individuals, Team moves, linking and safe deletion ------------
const v176BaseAttendanceMemberById=attendanceMemberById;
attendanceMemberById=function(id){let active=v176BaseAttendanceMemberById(id);if(active)return active;let archived=v176FindArchivedPerson(id);return archived?{member:archived.member,gang:{id:archived.formerGangId||'',name:archived.formerGangName||'Former workforce',active:false,members:[]}}:null};
const v176BasePaymentPayeeNameFromId=paymentPayeeNameFromId;
paymentPayeeNameFromId=function(id){let name=v176BasePaymentPayeeNameFromId(id);if(name)return name;let person=v176FindArchivedPerson(id);if(person)return person.member.name||'Former person';let team=(db.teamArchive||[]).find(row=>row.id===id);return team?.name||''};

function v176NewPerson(teamId=V176_INDIVIDUALS_ID){
  if(!hasPerm('manage_gangs'))return toast('You do not have people administration access');
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">PEOPLE</span><h2>${teamId===V176_INDIVIDUALS_ID?'Add an individual':'Add Team member'}</h2><p class="muted">Create the person once. You can move them between a Team and Individuals later without losing their records.</p><div class="two"><div class="field"><label>Full name</label><input id="v176-person-name" class="input" placeholder="Full name"></div><div class="field"><label>Trade / role</label><input id="v176-person-role" class="input" placeholder="e.g. Bricklayer, Labourer"></div></div><div class="field"><label>Place initially</label><select id="v176-person-team" class="select"><option value="${V176_INDIVIDUALS_ID}" ${teamId===V176_INDIVIDUALS_ID?'selected':''}>Individual / no Team</option>${v176ActiveTeams().map(team=>`<option value="${team.id}" ${team.id===teamId?'selected':''}>${v176Esc(team.name)}</option>`).join('')}</select></div><button class="btn orange" onclick="v176SaveNewPerson()">Create person</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function v176SaveNewPerson(){
  if(!hasPerm('manage_gangs'))return;let name=(document.getElementById('v176-person-name')?.value||'').trim(),memberRole=(document.getElementById('v176-person-role')?.value||'').trim(),teamId=document.getElementById('v176-person-team')?.value||V176_INDIVIDUALS_ID;if(!name)return toast('Enter the person’s full name');let team=teamId===V176_INDIVIDUALS_ID?v176Individuals(true):db.gangs.find(row=>row.id===teamId);if(!team)return toast('Choose a Team or Individuals');let member={id:v176Id('tm'),name,role:memberRole,notes:'',photos:[],employmentType:'Not tracked',hourlyRate:0,overtimeRate:0,dayRate:0,basePay:0,standardDayHours:8,standardWeekHours:40,defaultBreakMinutes:30,linkedUserId:'',activeForAttendance:true,employmentStatus:'Active',payRateHistory:[]};team.members=team.members||[];team.members.push(member);save();audit('people','person',member.id,`${name} added — ${team.name}`,'');modal=null;savedState('Person created');openTeamMember(team.id,member.id)
}
function v176MovePerson(memberId,targetTeamId){
  if(!hasPerm('manage_gangs'))return toast('You do not have people administration access');let current=v176BaseAttendanceMemberById(memberId);if(!current)return toast('Person not found');let target=targetTeamId===V176_INDIVIDUALS_ID?v176Individuals(true):db.gangs.find(team=>team.id===targetTeamId&&team.id!==V176_INDIVIDUALS_ID);if(!target||target.id===current.gang.id)return;current.gang.members=current.gang.members.filter(member=>member.id!==memberId);target.members=target.members||[];target.members.push(current.member);(db.attendanceShifts||[]).filter(shift=>shift.workerId===memberId&&!shift.voided&&!shift.clockOut?.time).forEach(shift=>shift.gangId=target.id);save();audit('people','team membership',memberId,`${current.member.name}: moved from ${current.gang.name} to ${target.name}`,'');modal=null;savedState(target.id===V176_INDIVIDUALS_ID?'Person is now an individual':'Person added to Team');openTeamMember(target.id,memberId)
}
function v176DeletePerson(memberId){
  if(!hasPerm('manage_gangs'))return toast('You do not have people administration access');let current=v176BaseAttendanceMemberById(memberId);if(!current)return;if(attendanceActiveShift(memberId))return toast('Clock this person out before removing them');if(!confirm(`Remove ${current.member.name} from the active workforce? Historic hours, payments and Audit records will remain.`))return;let member=current.member,user=(db.users||[]).find(u=>u.id===member.linkedUserId||u.linkedMemberId===member.id);current.gang.members=current.gang.members.filter(row=>row.id!==member.id);member.activeForAttendance=false;member.employmentStatus='Leaver';member.clockAccessStatus='left';member.deletedAt=new Date().toISOString();member.deletedBy=currentUser()?.name||'';db.peopleArchive.push({member,formerGangId:current.gang.id,formerGangName:current.gang.name,deletedAt:member.deletedAt,deletedBy:member.deletedBy});if(user){user.accountActive=false;user.linkedMemberId='';user.disabledReason='Person removed from active workforce';user.disabledAt=member.deletedAt;member.linkedUserId=''}save();audit('people','person',member.id,`${member.name} removed from active workforce; history retained`,'');modal=null;savedState('Person removed; history retained');render()
}
function v176DeleteTeam(teamId){
  if(!hasPerm('manage_gangs'))return toast('You do not have Team administration access');let team=db.gangs.find(row=>row.id===teamId&&row.id!==V176_INDIVIDUALS_ID);if(!team)return;if(!confirm(`Delete ${team.name}? Its ${team.members?.length||0} member(s) will become Individuals. Historic payment names will remain.`))return;let individuals=v176Individuals(true);(team.members||[]).forEach(member=>individuals.members.push(member));db.teamArchive.push({id:team.id,name:team.name,deletedAt:new Date().toISOString(),deletedBy:currentUser()?.name||''});(db.items||[]).forEach(item=>{if(item.gang===team.id)item.gang='';if(item.paidTo===team.id)item.paidTo='';(item.lifts||[]).forEach(lift=>{if(lift.paidTo===team.id)lift.paidTo=''});(item.variations||[]).forEach(v=>{if(v.team===team.id)v.team=''})});(db.dayworks||[]).forEach(daywork=>{if(daywork.assignedType==='Team'&&daywork.assignedId===team.id){daywork.formerAssigneeName=team.name;daywork.assignedId='';daywork.assignedType=''}});db.gangs=db.gangs.filter(row=>row.id!==team.id);if(gang===team.id)gang='all';save();audit('people','team',team.id,`${team.name} deleted; members moved to Individuals; history retained`,'');savedState('Team deleted; members retained as Individuals');render()
}

function v176TeamChoices(selected=''){return `<option value="${V176_INDIVIDUALS_ID}" ${selected===V176_INDIVIDUALS_ID?'selected':''}>Individual / no Team</option>${v176ActiveTeams().map(team=>`<option value="${team.id}" ${team.id===selected?'selected':''}>${v176Esc(team.name)}</option>`).join('')}`}
function v176LinkChoices(member){let linked=(db.users||[]).find(user=>user.id===member.linkedUserId||user.linkedMemberId===member.id),available=(db.users||[]).filter(user=>user.accountActive!==false&&(!user.linkedMemberId||user.linkedMemberId===member.id));return `<option value="">No linked login</option>${available.map(user=>`<option value="${user.id}" ${linked?.id===user.id?'selected':''}>${v176Esc(user.name)} · @${v176Esc(user.username)} · ${v176Esc(user.role)}</option>`).join('')}`}
function v176SaveUserLink(teamId,memberId){
  if(!hasPerm('manage_users'))return toast('User administration permission is required');let wm=v176BaseAttendanceMemberById(memberId),userId=document.getElementById('v176-link-user')?.value||'';if(!wm)return;let old=(db.users||[]).find(user=>user.linkedMemberId===memberId||user.id===wm.member.linkedUserId);if(old&&old.id!==userId)old.linkedMemberId='';wm.member.linkedUserId='';if(userId){let user=db.users.find(row=>row.id===userId);if(!user)return;let previous=v176BaseAttendanceMemberById(user.linkedMemberId||'');if(previous)previous.member.linkedUserId='';user.linkedMemberId=memberId;wm.member.linkedUserId=user.id;if(user.role!=='Director'){let visible=activeSites().filter(row=>canAccessSite(row.id)),fallback=(site!=='all'&&canAccessSite(site)?site:visible[0]?.id)||'',ids=wm.member.clockSiteIds?.filter(id=>visible.some(row=>row.id===id))||[];if(!ids.length&&fallback)ids=[fallback];user.allSitesAccess=false;user.allowedSites=[...ids];if(!V176_MANAGEMENT_ROLES.includes(user.role))wm.member.clockSiteIds=[...ids]}}save();audit('people','user link',memberId,`${wm.member.name}: ${userId?'login linked':'login unlinked'}`,'');savedState(userId?'Login linked':'Login unlinked');openTeamMember(teamId,memberId)
}
function v176CreateUserForMember(teamId,memberId){
  if(!hasPerm('manage_users'))return toast('User administration permission is required');let wm=v176BaseAttendanceMemberById(memberId);if(!wm)return;let slug=String(wm.member.name||'user').toLowerCase().replace(/[^a-z0-9]+/g,'.').replace(/^\.|\.$/g,'');modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">LINKED LOGIN</span><h2>Create login for ${v176Esc(wm.member.name)}</h2><div class="two"><div class="field"><label>Username</label><input id="v176-new-username" class="input" value="${v176Esc(slug)}"></div><div class="field"><label>Email</label><input id="v176-new-email" class="input" type="email" placeholder="name@company.co.uk"></div></div><div class="two"><div class="field"><label>Temporary password</label><input id="v176-new-password" class="input" type="password" autocomplete="new-password" placeholder="Minimum 8 characters"></div><div class="field"><label>User role</label><select id="v176-new-role" class="select">${roleOptions.filter(role=>role!=='Director').map(role=>`<option ${role==='Operator'?'selected':''}>${role}</option>`).join('')}</select></div></div><button class="btn orange" onclick="v176SaveUserForMember('${teamId}','${memberId}')">Create & link login</button> <button class="btn light" onclick="openTeamMember('${teamId}','${memberId}')">Cancel</button></div></div>`;render()
}
function v176SaveUserForMember(teamId,memberId){
  let wm=v176BaseAttendanceMemberById(memberId),username=(document.getElementById('v176-new-username')?.value||'').trim().toLowerCase(),email=(document.getElementById('v176-new-email')?.value||'').trim().toLowerCase(),password=document.getElementById('v176-new-password')?.value||'',userRole=document.getElementById('v176-new-role')?.value||'Operator';if(!wm||!username||!email||!password)return toast('Username, email and password are required');if(password.length<8)return toast('Use a password with at least 8 characters');if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))return toast('Enter a valid email address');if(db.users.some(user=>user.username.toLowerCase()===username))return toast('That username already exists');if(db.users.some(user=>(user.email||'').toLowerCase()===email))return toast('That email is already registered');let visible=activeSites().filter(row=>canAccessSite(row.id)),fallback=(site!=='all'&&canAccessSite(site)?site:visible[0]?.id)||'',ids=(wm.member.clockSiteIds||[]).filter(id=>visible.some(row=>row.id===id));if(!ids.length&&fallback)ids=[fallback];if(!ids.length)return toast('Create or select a Site before adding this login');let user={id:v176Id('u'),name:wm.member.name,username,email,password,resetCode:'',resetExpires:0,role:userRole,permissions:defaultPerms(userRole),allowedSites:ids,allSitesAccess:false,trainingMode:true,onboardingSeen:false,onboardingCompletedAt:'',loginCount:0,lastLoginAt:'',recentWorkItems:[],cameraIntroSeen:false,linkedMemberId:memberId,accountActive:true,disabledReason:'',disabledAt:'',disabledBy:'',permissionProfileMode:'role-default',permissionTemplateVersion:199};db.users.push(user);wm.member.linkedUserId=user.id;if(!V176_MANAGEMENT_ROLES.includes(userRole))wm.member.clockSiteIds=[...ids];save();audit('people','user link',memberId,`${wm.member.name}: new ${userRole} login created and linked`,'');savedState('Login created and linked');openTeamMember(teamId,memberId)
}

function v176OpenWorkforceEditor(teamId,memberId){
  let wm=v176BaseAttendanceMemberById(memberId);if(!wm||!(hasPerm('manage_working_hours')||hasPerm('manage_attendance')))return toast('Working-hours permission is required');let m=wm.member;modal=`<div class="modal"><div class="modalbox v174-hours-modal"><span class="att-kicker">WORK & PAY SETTINGS</span><h2>${v176Esc(m.name)}</h2><p class="muted">Update current hours or pay when employment terms change. Previous pay values are retained in the person’s history.</p><div class="two"><div class="field"><label>Pay type</label><select id="v176-pay-type" class="select"><option ${m.employmentType==='Not tracked'?'selected':''}>Not tracked</option><option ${m.employmentType==='Hourly'?'selected':''}>Hourly</option><option ${m.employmentType==='Day Rate'?'selected':''}>Day Rate</option><option ${m.employmentType==='Price Work'?'selected':''}>Price Work</option></select></div><div class="field"><label>Base / hourly rate</label><input id="v176-base-pay" class="input" type="number" min="0" step="0.01" value="${Number(m.basePay||m.hourlyRate||0)}"></div></div><div class="two"><div class="field"><label>Day rate</label><input id="v176-day-rate" class="input" type="number" min="0" step="0.01" value="${Number(m.dayRate||0)}"></div><div class="field"><label>Overtime rate</label><input id="v176-ot-rate" class="input" type="number" min="0" step="0.01" value="${Number(m.overtimeRate||0)}"></div></div><div class="field"><label>Change effective from</label><input id="v176-pay-effective" class="input" type="date" value="${iso()}"></div><div class="v174-workweek">${v174WorkingScheduleRows(m,attendanceDefaultSiteId())}</div><button class="btn orange" onclick="v176SaveWorkforceEditor('${teamId}','${memberId}')">Save hours & pay</button> <button class="btn light" onclick="openTeamMember('${teamId}','${memberId}')">Cancel</button></div></div>`;render()
}
function v176SaveWorkforceEditor(teamId,memberId){
  let wm=v176BaseAttendanceMemberById(memberId);if(!wm)return;let m=wm.member,schedule={},usual=[];for(let dow=0;dow<7;dow++){let active=!!document.getElementById(`v174-day-${dow}`)?.checked,start=document.getElementById(`v174-start-${dow}`)?.value||'07:30',end=document.getElementById(`v174-end-${dow}`)?.value||'16:30';if(active&&v169ParseTime(end)<=v169ParseTime(start))return toast('Each finishing time must be after its starting time');schedule[dow]={active,start,end};if(active)usual.push(dow)}if(!usual.length)return toast('Select at least one normal working day');let previous={employmentType:m.employmentType,basePay:Number(m.basePay||m.hourlyRate||0),dayRate:Number(m.dayRate||0),overtimeRate:Number(m.overtimeRate||0)},next={employmentType:document.getElementById('v176-pay-type')?.value||'Not tracked',basePay:Number(document.getElementById('v176-base-pay')?.value||0),dayRate:Number(document.getElementById('v176-day-rate')?.value||0),overtimeRate:Number(document.getElementById('v176-ot-rate')?.value||0)};m.payRateHistory=Array.isArray(m.payRateHistory)?m.payRateHistory:[];if(JSON.stringify(previous)!==JSON.stringify(next))m.payRateHistory.unshift({id:v176Id('pay-rate'),effectiveFrom:document.getElementById('v176-pay-effective')?.value||iso(),previous,next,changedAt:new Date().toISOString(),changedBy:currentUser()?.name||''});m.employmentType=next.employmentType;m.basePay=next.basePay;m.hourlyRate=next.basePay;m.dayRate=next.dayRate;m.overtimeRate=next.overtimeRate;m.workingSchedule=schedule;m.usualDays=usual;(db.timesheetApprovals||[]).filter(approval=>approval.workerId===memberId&&!approval.superseded).forEach(approval=>approval.needsReapproval=true);save();audit('people','work and pay',memberId,`${m.name}: working hours/pay updated; prior rates retained`,'');savedState('Working hours and pay saved');openTeamMember(teamId,memberId)
}

function v176WorkforceCard(teamId,member){
  let linked=(db.users||[]).find(user=>user.id===member.linkedUserId||user.linkedMemberId===member.id),wm=v176BaseAttendanceMemberById(member.id);return `<div class="v176-workforce-card"><h3>Workforce controls</h3><div class="v176-link-row"><div class="field"><label>Linked SiteSight user</label><select id="v176-link-user" class="select">${v176LinkChoices(member)}</select></div>${hasPerm('manage_users')?'<button class="btn blue" onclick="v176SaveUserLink(\''+teamId+'\',\''+member.id+'\')">Save link</button>':''}</div>${hasPerm('manage_users')&&!linked?`<button class="btn light" onclick="v176CreateUserForMember('${teamId}','${member.id}')">＋ Create a new linked login</button>`:''}<div class="actions" style="margin-top:10px"><button class="btn light" onclick="v176OpenWorkforceEditor('${teamId}','${member.id}')">Edit working hours & base pay</button><select class="select" aria-label="Move person" onchange="if(this.value)v176MovePerson('${member.id}',this.value)"><option value="">Move / assign…</option>${v176TeamChoices(teamId)}</select><button class="btn light v176-team-danger" onclick="v176DeletePerson('${member.id}')">Remove person</button></div><div class="v176-pay-history">${(member.payRateHistory||[]).length?`${member.payRateHistory.length} previous pay change${member.payRateHistory.length===1?'':'s'} retained`:'No previous pay changes recorded'} · Current: ${v176Esc(member.employmentType||'Not tracked')}${wm?` · ${v176Esc(wm.gang.name)}`:''}</div></div>`
}

const v176BaseOpenTeamMember=openTeamMember;
openTeamMember=function(teamId,memberId){v176BaseOpenTeamMember(teamId,memberId);let wm=v176BaseAttendanceMemberById(memberId),box=document.querySelector('.modalbox');if(!wm||!box)return;let actions=box.querySelector('.attendance-member-actions');(actions||box).insertAdjacentHTML(actions?'beforebegin':'beforeend',v176WorkforceCard(wm.gang.id,wm.member))};

manageTeamMembers=function(id){
  if(!hasPerm('manage_gangs'))return toast('You do not have Team administration access');let team=db.gangs.find(row=>row.id===id);if(!team)return;modal=`<div class="modal"><div class="modalbox"><div class="sectionhead"><div><span class="att-kicker">TEAM MEMBERSHIP</span><h2>${v176Esc(team.name)}</h2><p class="muted">Move a person out of this Team without deleting their record, or add an existing Individual.</p></div></div><div class="team-members">${(team.members||[]).map(member=>`<div class="team-member"><div class="clickable" style="flex:1" onclick="openTeamMember('${team.id}','${member.id}')"><b>${v176Esc(member.name)}</b><span>${v176Esc(member.role||'Team member')}</span></div><button class="btn light smallbtn" onclick="v176MovePerson('${member.id}','${V176_INDIVIDUALS_ID}')">Make individual</button><button class="btn light smallbtn v176-team-danger" onclick="v176DeletePerson('${member.id}')">Remove</button></div>`).join('')||'<div class="empty-help"><b>No members in this Team.</b></div>'}</div><div class="field"><label>Add an existing Individual</label><select id="v176-add-existing" class="select"><option value="">Choose a person…</option>${(v176Individuals()?.members||[]).map(member=>`<option value="${member.id}">${v176Esc(member.name)} · ${v176Esc(member.role||'Person')}</option>`).join('')}</select></div><div class="actions"><button class="btn blue" onclick="let id=document.getElementById('v176-add-existing').value;if(id)v176MovePerson(id,'${team.id}');else toast('Choose a person')">Add selected person</button><button class="btn orange" onclick="v176NewPerson('${team.id}')">＋ Create new member</button><button class="btn light" onclick="modal=null;render()">Done</button></div></div></div>`;render()
};

gangsPage=function(){
  let individuals=v176Individuals(),people=individuals?.members||[],teams=v176ActiveTeams();return `<div class="topbar"><div><h1>Teams & Individuals</h1><div class="sub">One clear place to create people, link logins and move membership without losing records</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${v176Esc(s.name)}</option>`).join('')}</select>${hasPerm('manage_gangs')?`<button class="btn blue" onclick="v176NewPerson()">＋ Add Individual</button><button class="btn orange" onclick="addTeam()">＋ Add Team</button>`:''}</div></div><div class="panel v176-people-panel"><div class="sectionhead"><div><span class="att-kicker">NOT IN A TEAM</span><h2>Individuals</h2><div class="muted">People can be paid, assigned Daywork and linked to a login without belonging to a Team.</div></div><span class="badge b-blue">${people.length}</span></div><div class="v176-people-grid">${people.map(member=>`<div class="v176-person-card"><div onclick="openTeamMember('${V176_INDIVIDUALS_ID}','${member.id}')" class="clickable"><b>${v176Esc(member.name)}</b><span>${v176Esc(member.role||'Individual')} · ${member.linkedUserId?'Login linked':'No login'}</span></div><div class="v176-person-actions"><button class="btn light smallbtn" onclick="openTeamMember('${V176_INDIVIDUALS_ID}','${member.id}')">Open</button><select class="select" aria-label="Add to Team" onchange="if(this.value)v176MovePerson('${member.id}',this.value)"><option value="">Add to Team…</option>${v176ActiveTeams().map(team=>`<option value="${team.id}">${v176Esc(team.name)}</option>`).join('')}</select></div></div>`).join('')||'<div class="empty-help"><b>No unassigned Individuals.</b><div style="margin-top:6px">Use Add Individual when somebody works independently of a Team.</div></div>'}</div></div><div class="cards">${teams.map(team=>{let items=db.items.filter(item=>canViewItem(item)&&item.gang===team.id&&!db.sites.find(s=>s.id===item.site)?.archived&&(site==='all'||item.site===site)),members=team.members||[];if(site!=='all'&&!items.length&&!members.length)return'';return `<div class="card gangcard"><div class="sectionhead"><h3 onclick="openGang('${team.id}')" class="clickable">${v176Esc(team.name)}</h3>${team.active?badge('Active'):badge('Not started')}</div><p>Assigned <b>${items.length}</b> · In progress <b>${items.filter(item=>overall(item)==='In progress').length}</b></p><p>Members <b>${members.length}</b> · Issues <b>${items.reduce((n,item)=>n+issues(item),0)}</b></p><div class="team-members">${members.slice(0,4).map(member=>`<div class="team-member clickable" onclick="openTeamMember('${team.id}','${member.id}')"><div><b>${v176Esc(member.name)}</b><span>${v176Esc(member.role||'Team member')}</span></div><b>›</b></div>`).join('')||'<div class="muted team-no-members">No Team members.</div>'}</div><div class="team-actions"><button class="btn light smallbtn" onclick="openGang('${team.id}')">Open Team</button>${hasPerm('manage_gangs')?`<button class="btn blue smallbtn" onclick="manageTeamMembers('${team.id}')">Manage members</button><button class="btn light smallbtn" onclick="editTeamName('${team.id}')">Edit name</button><button class="btn light smallbtn v176-team-danger" onclick="v176DeleteTeam('${team.id}')">Delete Team</button>`:''}</div></div>`}).join('')||smartEmpty('No Teams are showing.','Create a Team or add people as Individuals.',hasPerm('manage_gangs')?'＋ Add Team':'',hasPerm('manage_gangs')?'addTeam()':'','♟')}</div>`
};

// ---- Drawing measurement and drawing menu fixes --------------------
takeoffWorkspaceHtml=function(){
  let d=takeoffDrawing();if(!d||!takeoffContext)return'';let isPdf=takeoffContext.isPdf,canEdit=hasPerm('manage_drawings'),cal=!!d.takeoffCalibration,finishReady=takeoffTool==='Length'?takeoffDraft.length>=2:(takeoffTool==='Area'||takeoffTool==='Deduct Area'?takeoffDraft.length>=3:(takeoffTool==='Count'?takeoffDraft.length>=1:false)),finishCall=takeoffTool==='Calibration'?'finishTakeoffCalibration()':'finishTakeoffGeometry()';
  return `<div class="takeoff-workspace"><div class="takeoff-head"><div><span class="takeoff-kicker">DRAWING TAKE-OFF</span><h2>${calendarSafe(d.name||d.originalName||'Drawing')}</h2><div class="muted">${takeoffContext.scope==='work'?`${db.items.find(x=>x.id===takeoffContext.itemId)?.ref||''} · `:''}${siteName(takeoffSiteId())}${isPdf?' · PDF':''}</div></div><div class="actions"><button class="btn light" onclick="closeDrawingTakeoff()">✕ Close take-off</button></div></div>${takeoffGuideHtml()}<div class="takeoff-body"><div class="takeoff-canvas-column"><div class="takeoff-toolbar">${canEdit?`<button class="${takeoffTool==='Calibration'?'active':''}" onclick="chooseTakeoffTool('Calibration')">↔ Calibrate</button><button class="${takeoffTool==='Length'?'active':''}" onclick="chooseTakeoffTool('Length')" ${!cal?'disabled':''}>╱ Length</button><button class="${takeoffTool==='Area'?'active':''}" onclick="chooseTakeoffTool('Area')" ${!cal?'disabled':''}>▱ Area</button><button class="${takeoffTool==='Deduct Area'?'active':''}" onclick="chooseTakeoffTool('Deduct Area')" ${!cal?'disabled':''}>⊟ Deduct Area</button><button class="${takeoffTool==='Count'?'active':''}" onclick="chooseTakeoffTool('Count')">● Count</button>${takeoffDraft.length?`<button class="finish" onclick="${finishCall}" ${takeoffTool!=='Calibration'&&!finishReady?'disabled':''}>✓ Finish</button><button onclick="cancelTakeoffDraft()">Clear points</button>`:''}`:''}<span id="takeoff-draft-status">${takeoffDraft.length?`${takeoffDraft.length} point${takeoffDraft.length===1?'':'s'} selected`:(canEdit?'Choose a tool, then click the drawing':'View-only take-off')}</span></div><div class="takeoff-drawing-stage ${isPdf?'takeoff-pdf':''}">${isPdf?`<iframe src="${takeoffContext.src}#toolbar=1&navpanes=0&view=FitH" title="${calendarSafe(d.name||'Drawing')}"></iframe><svg id="takeoff-svg" onclick="takeoffCanvasClick(event)" onmousemove="takeoffCanvasMove(event)" onmouseleave="takeoffCanvasLeave()"></svg>`:`<div class="v176-takeoff-sheet"><img src="${takeoffContext.src}" alt="${calendarSafe(d.name||'Drawing')}"><svg id="takeoff-svg" onclick="takeoffCanvasClick(event)" onmousemove="takeoffCanvasMove(event)" onmouseleave="takeoffCanvasLeave()"></svg></div>`}</div>${takeoffDraft.length?`<button class="v176-finish-measurement" onclick="${finishCall}" ${takeoffTool!=='Calibration'&&!finishReady?'disabled':''}>✓ Finish ${takeoffTool==='Calibration'?'calibration':takeoffTool.toLowerCase()} — ${takeoffDraft.length} point${takeoffDraft.length===1?'':'s'}</button>`:''}${isPdf?'<div class="takeoff-pdf-warning"><b>PDF measurement:</b> use the PDF scroll controls to reach the full sheet. Calibrate again after changing page or zoom.</div>':''}</div><aside class="takeoff-sidebar"><div class="takeoff-scale ${cal?'calibrated':''}"><div><span>DRAWING SCALE</span><b>${cal?'✓ Calibrated':'Not calibrated'}</b></div><p>${cal?calendarSafe(takeoffCalibrationText(d)):'Click Calibrate, select two known points and enter their real distance.'}</p>${canEdit&&cal?'<button class="btn light smallbtn" onclick="chooseTakeoffTool(\'Calibration\')">Recalibrate</button> <button class="btn light smallbtn" onclick="clearDrawingCalibration()">Remove scale</button>':''}</div>${takeoffPendingHtml()}${takeoffTotalsHtml(d)}<div class="takeoff-list-head"><div><h3>Saved Take-off</h3><span>${(d.takeoffs||[]).length} item${(d.takeoffs||[]).length===1?'':'s'}</span></div></div>${takeoffListHtml(d)}</aside></div></div>`
};

// ---- Calendar month chooser ----------------------------------------
const v176BaseFullCalendarMonth=fullCalendarMonth;
function v176CalendarMonthOptions(){let current=calendarMonthDate(),years=new Set([current.getFullYear()]);for(let n=-5;n<=5;n++)years.add(new Date().getFullYear()+n);['calendarNotes','siteDiary','dayworks'].forEach(key=>(db[key]||[]).forEach(row=>{let y=Number(String(row.date||'').slice(0,4));if(y)years.add(y)}));return [...years].sort((a,b)=>a-b).flatMap(year=>Array.from({length:12},(_,month)=>{let value=`${year}-${String(month+1).padStart(2,'0')}-01`,label=new Date(year,month,1).toLocaleDateString('en-GB',{month:'long',year:'numeric'});return `<option value="${value}" ${calendarMonth===value?'selected':''}>${label}</option>`})).join('')}
function v176SetCalendarMonth(value){if(/^\d{4}-\d{2}-01$/.test(value)){calendarMonth=value;render()}}
fullCalendarMonth=function(){let html=v176BaseFullCalendarMonth(),select=`<label class="sr-only" for="v176-calendar-month">Choose month</label><select id="v176-calendar-month" class="select v176-month-select" onchange="v176SetCalendarMonth(this.value)">${v176CalendarMonthOptions()}</select>`;return html.replace('<div class="calendar-month-actions">',`<div class="calendar-month-actions">${select}`)};

// ---- Flexible plot-number search -----------------------------------
function v176ReferenceMatches(value,term){let text=String(value||'').toLowerCase(),query=String(term||'').trim().toLowerCase();if(!query)return true;if(text.includes(query))return true;if(/^0*\d+$/.test(query)){let wanted=Number(query),numbers=text.match(/\d+/g)||[];return numbers.some(number=>Number(number)===wanted)}return false}
filtered=function(){return db.items.filter(x=>canViewItem(x)&&(site==='all'||x.site===site)&&(gang==='all'||x.gang===gang)&&(type==='all'||x.type===type)&&(status==='all'||overall(x)===status)&&(signed==='all'||String(x.signed)===signed)&&(!q||v176ReferenceMatches(x.ref,q)))};
searchSuggestions=function(term){let t=(term||'').trim().toLowerCase();if(!t)return[];return db.items.filter(x=>canViewItem(x)&&(v176ReferenceMatches(x.ref,t)||[x.type,x.houseType,siteName(x.site),gangName(x.gang)].join(' ').toLowerCase().includes(t))).slice(0,8)};
const v176BaseGlobalSearchResults=globalSearchResults;
globalSearchResults=function(query){let out=v176BaseGlobalSearchResults(query),t=String(query||'').trim();if(!/^0*\d+$/.test(t)||!hasPerm('work_items'))return out;db.items.filter(x=>canAccessSite(x.site)&&v176ReferenceMatches(x.ref,t)&&!out.some(row=>row.kind==='Work Item'&&row.title===x.ref)).slice(0,7).forEach(x=>out.unshift({kind:'Work Item',icon:'▣',title:x.ref,meta:`${x.type} · ${siteName(x.site)} · ${gangName(x.gang)}`,action:`closeGlobalSearch();openItem('${x.id}')`}));return out.slice(0,18)};

// ---- Daywork assignment to Team or Individual ----------------------
function v176AssigneeOptions(selectedType='',selectedId=''){return `<option value="">Choose Team or person…</option><optgroup label="Teams">${v176ActiveTeams().map(team=>`<option value="team:${team.id}" ${selectedType==='Team'&&selectedId===team.id?'selected':''}>${v176Esc(team.name)}</option>`).join('')}</optgroup><optgroup label="Individuals / people">${v176ActivePeople().sort((a,b)=>a.member.name.localeCompare(b.member.name)).map(row=>`<option value="person:${row.member.id}" ${selectedType==='Individual'&&selectedId===row.member.id?'selected':''}>${v176Esc(row.member.name)} · ${v176Esc(row.gang.name)}</option>`).join('')}</optgroup>`}
function v176DayworkAssignee(daywork){if(daywork.assignedType==='Team')return paymentPayeeNameFromId(daywork.assignedId)||daywork.formerAssigneeName||'Former Team';if(daywork.assignedType==='Individual')return paymentPayeeNameFromId(daywork.assignedId)||'Former person';return daywork.formerAssigneeName||'Not assigned'}
contractorAddDaywork=function(){if(!(hasPerm('manage_variations')||hasPerm('update_progress')))return toast('You do not have permission to record Daywork');let sid=site!=='all'?site:(activeSites()[0]?.id||'');modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">EXTRA WORK</span><h2>Record Daywork</h2><div class="two"><div class="field"><label>Date</label><input id="dw-date" class="input" type="date" value="${contractorToday()}"></div><div class="field"><label>Site</label><select id="dw-site" class="select">${activeSites().map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${v176Esc(s.name)}</option>`).join('')}</select></div></div><div class="field"><label>Assign Daywork to</label><select id="dw-assignee" class="select">${v176AssigneeOptions()}</select></div><div class="field"><label>Plot / Area</label><input id="dw-area" class="input" placeholder="e.g. Plot 034 / retaining wall"></div><div class="field"><label>Work instructed</label><textarea id="dw-work" class="textarea" placeholder="What extra work was carried out?"></textarea></div><div class="two"><div class="field"><label>Instructed by</label><input id="dw-by" class="input" placeholder="Name / role"></div><div class="field"><label>Instruction reference</label><input id="dw-ref" class="input" placeholder="Email / SI / VO / verbal ref"></div></div><div class="field"><label>Labour hours</label><input id="dw-hours" class="input" type="number" min="0" step="0.5" value="8"></div>${hasPerm('view_financials')?'<div class="field"><label>Estimated value</label><input id="dw-value" class="input" type="number" min="0" step="0.01"></div>':''}${hasPerm('manage_variations')?'<label class="permcheck"><input id="dw-variation" type="checkbox"> <span>Create a pending Variation from this Daywork</span></label>':''}<div class="actions" style="margin-top:14px"><button class="btn orange" onclick="contractorSaveDaywork()">Save Daywork</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()};
contractorSaveDaywork=function(){let siteId=document.getElementById('dw-site')?.value||'',date=document.getElementById('dw-date')?.value||'',area=(document.getElementById('dw-area')?.value||'').trim(),work=(document.getElementById('dw-work')?.value||'').trim(),by=(document.getElementById('dw-by')?.value||'').trim(),ref=(document.getElementById('dw-ref')?.value||'').trim(),hours=Number(document.getElementById('dw-hours')?.value||0),value=Number(document.getElementById('dw-value')?.value||0),assignee=document.getElementById('dw-assignee')?.value||'';if(!siteId||!date||!work||!by||!assignee)return toast('Site, date, assignee, work and instructed by are required');let [kind,assignedId]=assignee.split(':'),assignedType=kind==='team'?'Team':'Individual',d={id:contractorId('dw'),siteId,date,area,work,instructedBy:by,instructionRef:ref,assignedType,assignedId,labour:[{workerId:assignedType==='Individual'?assignedId:'',teamId:assignedType==='Team'?assignedId:'',hours}],value,createdBy:currentUser()?.name||'',createdAt:new Date().toISOString(),variationId:''};if(document.getElementById('dw-variation')?.checked){let item=db.items.find(x=>x.site===siteId&&area&&v176ReferenceMatches(x.ref,(area.match(/\d+/)||[])[0]||area));if(item){let v={id:contractorId('var'),name:`Daywork — ${area||work.slice(0,40)}`,value,date,stage:'Daywork',team:assignedId,payeeKind:assignedType,payments:[],approvalStatus:'Pending Approval',approvedBy:'',approvedAt:'',instructionRef:ref,instructedBy:by,instructionDate:date};item.variations=item.variations||[];item.variations.push(v);d.variationId=v.id}}db.dayworks.unshift(d);save();audit('daywork','daywork',d.id,`Daywork recorded — ${area||siteName(siteId)} · assigned to ${v176DayworkAssignee(d)}`,siteId);modal=null;savedState('Daywork saved and assigned');render()};
contractorDayworksPage=function(){if(!hasPerm('work_items'))return'<div class="panel empty-help"><b>You do not have Work access.</b></div>';let sid=contractorDayworkSite==='all'?(site!=='all'?site:'all'):contractorDayworkSite,r=v174RecordRanges.dayworks,rows=(db.dayworks||[]).filter(d=>canAccessSite(d.siteId)&&(sid==='all'||d.siteId===sid)&&v174DateInRange(d.date,r.from,r.to)),search=contractorDayworkSearch.trim().toLowerCase();if(search)rows=rows.filter(d=>[d.area,d.work,d.instructedBy,d.instructionRef,siteName(d.siteId),v176DayworkAssignee(d)].join(' ').toLowerCase().includes(search));rows.sort((a,b)=>String(b.date).localeCompare(String(a.date)));return `<div class="topbar"><div><h1>Dayworks</h1><div class="sub">Instructed extra work assigned to a Team or individual</div></div><div class="actions"><select class="select" onchange="contractorDayworkSite=this.value;render()"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${v176Esc(s.name)}</option>`).join('')}</select>${hasPerm('manage_variations')||hasPerm('update_progress')?'<button class="btn orange" onclick="contractorAddDaywork()">＋ Record Daywork</button>':''}</div></div>${v174RecordRangeBar('dayworks','Daywork date range')}<div class="panel" style="padding:15px"><div class="searchbox"><span>⌕</span><input value="${v176Esc(contractorDayworkSearch)}" placeholder="Search Dayworks…" oninput="contractorDayworkSearch=this.value;render()"></div><div class="daywork-list">${rows.map(d=>`<div class="daywork-card"><div class="sectionhead"><div><span class="badge b-purple">Daywork</span><h3>${v176Esc(d.area||'Site work')}</h3><div class="muted">${d.date} · ${v176Esc(siteName(d.siteId))}</div></div>${hasPerm('view_financials')?`<b>${attendanceMoney(d.value||0)}</b>`:''}</div><p>${v176Esc(d.work)}</p><div class="daywork-meta"><span class="v176-daywork-assignee">Assigned to <b>${v176Esc(v176DayworkAssignee(d))}</b></span><span>Instructed by <b>${v176Esc(d.instructedBy||'—')}</b></span><span>Ref <b>${v176Esc(d.instructionRef||'—')}</b></span><span>${(d.labour||[]).reduce((n,row)=>n+Number(row.hours||0),0).toFixed(1)} labour hrs</span>${d.variationId?'<span>✓ Variation created</span>':''}</div></div>`).join('')||smartEmpty('No Dayworks recorded.','Use Daywork when extra work is instructed outside the original price.','','','＋')}</div></div>`};

// ---- Site report current lift --------------------------------------
function v176PlotProgressText(item){if(item.type!=='House Plot')return overall(item);let lifts=item.lifts||[];if(item.signed)return'Signed Off';if(lifts.length&&lifts.every(lift=>lift.status==='Complete'))return'All lifts complete — awaiting sign-off';let index=lifts.findIndex(lift=>lift.status!=='Complete');if(index<0)return'Complete';let lift=lifts[index],previous=index>0?lifts[index-1]:null;if(lift.status==='Started')return`${lift.name} started — incomplete`;if(previous?.status==='Complete')return`${lift.name} not started — ${previous.name} complete`;return`${lift.name} not started`}
downloadSiteReport=function(id,rangeFrom='',rangeTo=''){if(!roleCanAccessPlots())return toast('Plot progress reports are available to Supervisors and above');if(!hasPerm('download_reports'))return toast('You do not have permission to download reports');id=id||site;if(id==='all'||!id)return toast('Select a Site first');let showFin=hasPerm('view_financials'),showIssues=hasPerm('view_issues'),showDiary=hasPerm('view_site_diary'),s=db.sites.find(row=>row.id===id),items=db.items.filter(item=>item.site===id&&canViewItem(item)).filter(item=>{if(!rangeFrom&&!rangeTo)return true;let d=completionDate(item)||String(item.lastUpdatedAt||item.lastUpdated||'').slice(0,10);return d&&(!rangeFrom||d>=rangeFrom)&&(!rangeTo||d<=rangeTo)}),rows=items.map(item=>{let liftRows=item.type==='House Plot'?(item.lifts||[]).map(lift=>`<tr><td>${calendarSafe(lift.name)}</td><td>${calendarSafe(lift.status)}</td><td>${lift.completedDate||''}</td>${showFin?`<td>${lift.pct}%</td><td>${money(lift.price)}</td>`:''}</tr>`).join(''):`<tr><td>${calendarSafe(item.type)}</td><td>${calendarSafe(item.status)}</td><td>${item.completedDate||''}</td>${showFin?`<td>${item.pct}%</td><td>${money(item.price)}</td>`:''}</tr>`;return `<section><h2>${calendarSafe(item.ref)} ${item.signed?'★':''}</h2><p>${calendarSafe(item.type)} · Team: ${calendarSafe(gangName(item.gang))} · <b>${calendarSafe(overall(item))}</b></p><p class="current"><b>Current progress:</b> ${calendarSafe(v176PlotProgressText(item))}</p><table><tr><th>Stage</th><th>Status</th><th>Completed</th>${showFin?'<th>Paid</th><th>Price</th>':''}</tr>${liftRows}</table>${showIssues?`<h4>Issues</h4><ul>${(item.issues||[]).map(issue=>`<li>${calendarSafe(issue.status)}: ${calendarSafe(issue.title)}</li>`).join('')||'<li>None</li>'}</ul>`:''}</section>`}).join(''),diaryHtml=showDiary?`<h2>Site Diary</h2>${(db.siteDiary||[]).filter(e=>e.siteId===id&&(!rangeFrom||e.date>=rangeFrom)&&(!rangeTo||e.date<=rangeTo)).map(e=>`<section><b>${e.date}</b><p>Work: ${calendarSafe(e.work||'—')}</p><p>Problems / delays: ${calendarSafe(e.delays||'—')}</p></section>`).join('')||'<p>No Diary entries.</p>'}`:'',report=`<!doctype html><html><head><meta charset="utf-8"><title>${calendarSafe(s?.name||'Site')} — SiteSight Site Report</title><style>body{font:14px Arial;color:#172b42;max-width:1000px;margin:30px auto}h1{color:#09233d}section{page-break-inside:avoid;border:1px solid #ddd;padding:18px;margin:15px 0;border-radius:10px}.current{padding:9px;background:#fff2d9;border-left:4px solid #e29a25}table{width:100%;border-collapse:collapse}td,th{padding:7px;border:1px solid #ddd;text-align:left}th{background:#f2f5f8}.meta{color:#64748b}</style></head><body><h1>SiteSight — ${calendarSafe(s?.name||'Site')}</h1><p class="meta">Generated ${iso()} · Range: ${rangeFrom||'Start'} to ${rangeTo||'Now'} · ${items.length} work items</p>${rows||'<p>No work found in the selected range.</p>'}${diaryHtml}</body></html>`,blob=new Blob([report],{type:'text/html'}),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`${String(s?.name||'site').replace(/[^a-z0-9]+/gi,'-').toLowerCase()}-sitesight-report.html`;a.click();save();setTimeout(()=>URL.revokeObjectURL(a.href),1000)};

// ---- HelpBot listening state and working confirmation --------------
const v176BaseAssistRenderPanel=assistRenderPanel;
assistRenderPanel=function(){v176BaseAssistRenderPanel();let button=document.querySelector('#sitesight-assist-panel .assist-mic'),row=document.querySelector('#sitesight-assist-panel .assist-input-row');if(button){button.classList.toggle('listening',v176AssistListening);button.innerHTML=v176AssistListening?'●<span class="sr-only"> Listening</span>':'🎤';button.title=v176AssistListening?'Listening — click to stop':'Speak';button.setAttribute('aria-pressed',String(v176AssistListening));button.setAttribute('aria-label',v176AssistListening?'Listening. Click to stop.':'Start voice input')}if(row&&v176AssistListening&&!document.getElementById('v176-assist-listening'))row.insertAdjacentHTML('beforebegin','<div id="v176-assist-listening" class="assist-listening-status" role="status" aria-live="polite">● Listening… speak now</div>')};
function v176AssistSetListening(value){v176AssistListening=!!value;assistRenderPanel()}
assistVoice=function(){if(v176AssistRecognition&&v176AssistListening){v176AssistRecognition.stop();return}let SR=window.SpeechRecognition||window.webkitSpeechRecognition;if(!SR)return toast('Voice input is not supported by this browser');let recognition=new SR();v176AssistRecognition=recognition;recognition.lang='en-GB';recognition.interimResults=true;recognition.maxAlternatives=1;recognition.onstart=()=>v176AssistSetListening(true);recognition.onresult=event=>{let transcript=[...event.results].map(result=>result[0]?.transcript||'').join(''),input=document.getElementById('assist-input');if(input)input.value=transcript;if(event.results[event.results.length-1]?.isFinal){v176AssistSetListening(false);setTimeout(()=>assistSubmit(),80)}};recognition.onerror=event=>{v176AssistSetListening(false);toast(event.error==='not-allowed'?'Microphone permission was not allowed':'Voice input could not start')};recognition.onend=()=>{v176AssistRecognition=null;if(v176AssistListening)v176AssistSetListening(false)};try{recognition.start()}catch(e){v176AssistRecognition=null;v176AssistSetListening(false);toast('Voice input could not start')}};
assistConfirmPlan=function(plan){assistClose();assistPending=plan;let summary=assistPlanSummary(plan);modal=`<div class="modal"><div class="modalbox assist-confirm"><span class="att-kicker">TASKBOT — REVIEW</span><h2>Confirm task</h2><p>${assistEsc(summary)}</p><div class="assist-confirm-rule">SiteSight will use your current account and permissions, record the change normally and add it to the Audit/operational history where applicable.</div><button class="btn orange" onclick="this.disabled=true;this.textContent='Applying…';assistExecutePending()">Confirm & do it</button> <button class="btn light" onclick="modal=null;assistPending=null;render();assistOpenPanel()">Cancel</button></div></div>`;render()};

// ---- Payment history Team-member breakdown -------------------------
function v176ActualPayment(paymentId){for(let item of db.items||[]){for(let lift of item.lifts||[]){let payment=(lift.payments||[]).find(row=>row.id===paymentId);if(payment)return{payment,item,stage:lift}}let payment=(item.payments||[]).find(row=>row.id===paymentId);if(payment)return{payment,item};for(let variation of item.variations||[]){payment=(variation.payments||[]).find(row=>row.id===paymentId);if(payment)return{payment,item,variation}}}return null}
function v176PaymentTeam(payment){let id=payment?.paidTo||payment?.item?.gang||'';return (db.gangs||[]).find(team=>team.id===id&&team.id!==V176_INDIVIDUALS_ID)||null}
function v176PaymentSplit(payment){let team=v176PaymentTeam(payment),members=team?.members||[];if(!team||!members.length)return[];if(Array.isArray(payment.memberBreakdown)&&payment.memberBreakdown.length)return payment.memberBreakdown.map(row=>({workerId:row.workerId,name:paymentPayeeNameFromId(row.workerId)||row.name||'Former member',amount:Number(row.amount||0),explicit:true}));let pennies=Math.round(Number(payment.amount||0)*100),base=Math.floor(pennies/members.length),remainder=pennies-base*members.length;return members.map((member,index)=>({workerId:member.id,name:member.name,amount:(base+(index<remainder?1:0))/100,explicit:false}))}
function v176OpenPaymentSplit(paymentId){if(!hasPerm('manage_payments'))return toast('Payment management permission is required');let found=v176ActualPayment(paymentId);if(!found)return toast('Payment record not found');let shadow={...found.payment,item:found.item},team=v176PaymentTeam(shadow),split=v176PaymentSplit(shadow);if(!team||!split.length)return toast('This payment is not assigned to an active Team with members');modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">TEAM PAYMENT</span><h2>${v176Esc(team.name)} breakdown</h2><p class="muted">Allocate the exact ${attendanceMoney(found.payment.amount)} paid across the Team. The total must match the payment record.</p><div class="v176-payment-allocation">${split.map(row=>`<div class="two"><div class="field"><label>${v176Esc(row.name)}</label><input class="input" disabled value="${v176Esc(row.workerId)}"></div><div class="field"><label>Amount</label><input class="input v176-split-input" data-worker-id="${row.workerId}" type="number" min="0" step="0.01" value="${row.amount.toFixed(2)}"></div></div>`).join('')}</div><button class="btn orange" onclick="v176SavePaymentSplit('${paymentId}')">Save member breakdown</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function v176SavePaymentSplit(paymentId){let found=v176ActualPayment(paymentId);if(!found)return;let rows=[...document.querySelectorAll('.v176-split-input')].map(input=>({workerId:input.dataset.workerId,amount:Number(input.value||0)})),total=rows.reduce((n,row)=>n+row.amount,0);if(Math.abs(total-Number(found.payment.amount||0))>.005)return toast(`Member amounts must total ${attendanceMoney(found.payment.amount)}`);found.payment.memberBreakdown=rows;found.payment.memberBreakdownUpdatedAt=new Date().toISOString();found.payment.memberBreakdownUpdatedBy=currentUser()?.name||'';save();audit('payments','team breakdown',paymentId,`Team payment breakdown saved — ${rows.length} people · ${attendanceMoney(total)}`,found.item.site);modal=null;savedState('Team payment breakdown saved');render()}
function v176PaymentBreakdownPanel(rows){let teamRows=rows.filter(row=>v176PaymentTeam(row));if(!teamRows.length)return'';return `<div class="panel v176-payment-splits"><div class="sectionhead"><div><span class="att-kicker">TEAM PAYMENT DETAIL</span><h2>Who received each Team payment?</h2><div class="muted">Exact saved allocations are shown. Until edited, SiteSight clearly labels an equal-split estimate.</div></div></div>${teamRows.map(payment=>{let split=v176PaymentSplit(payment),explicit=split.some(row=>row.explicit);return `<div class="v176-split-row"><span><b>${v176Esc(paymentPayeeName(payment))}</b><small>${v176Esc(payment.item?.ref||'')} · ${payment.date||'—'} · ${v176Esc(payment.stage||payment.variationName||'Payment')}</small></span><strong>${attendanceMoney(payment.amount||0)}</strong>${hasPerm('manage_payments')?`<button class="btn light smallbtn" onclick="v176OpenPaymentSplit('${payment.id}')">${explicit?'Edit':'Set'} split</button>`:''}<div class="v176-split-members">${split.map(row=>`<span>${v176Esc(row.name)} <b>${attendanceMoney(row.amount)}</b></span>`).join('')}<span><b>${explicit?'Saved allocation':'Equal-split estimate'}</b></span></div></div>`}).join('')}</div>`}
const v176BasePaymentsHistoryPage=paymentsHistoryPage;
paymentsHistoryPage=function(){let html=v176BasePaymentsHistoryPage();if(!hasPerm('view_financials'))return html;let invalid=paymentFrom&&paymentTo&&paymentFrom>paymentTo,rows=invalid?[]:filteredPaymentRows();return html+v176PaymentBreakdownPanel(rows)};

// ---- Configurable payroll/accounting export profiles ---------------
const V176_EXPORT_COLUMNS={employeeName:'Employee / payee name',employeeReference:'Employee reference',recordType:'Record type',site:'Site',from:'Period from',to:'Period to',recordedHours:'Recorded hours',payableHours:'Payable hours',authorisedOvertime:'Authorised overtime',lateExcluded:'Late time excluded',gross:'Gross / amount due',costCode:'Cost code'};
function v176ExportProfile(){return (db.payrollExportProfiles||[]).find(profile=>profile.id===v176ExportProfileId)||(db.payrollExportProfiles||[])[0]}
function v176CanManageExportProfiles(){return hasPerm('lock_payroll')}
function v176OpenExportProfile(id=''){if(!v176CanManageExportProfiles())return toast('Payroll lock/export permission is required to change export profiles');let profile=(db.payrollExportProfiles||[]).find(row=>row.id===id)||{id:'',name:'',system:'Generic CSV',costCode:'',columns:['employeeName','employeeReference','recordType','payableHours','gross','costCode']};modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">ACCOUNTING EXPORT</span><h2>${id?'Edit export profile':'New export profile'}</h2><p class="muted">Save the column order and cost code expected by your payroll or accounting package. SiteSight creates a CSV; it does not submit PAYE, CIS or bank payments.</p><div class="two"><div class="field"><label>Profile name</label><input id="v176-profile-name" class="input" value="${v176Esc(profile.name)}" placeholder="e.g. Sage weekly payroll"></div><div class="field"><label>Downstream system</label><select id="v176-profile-system" class="select">${['Generic CSV','Sage','Xero','QuickBooks','Custom'].map(name=>`<option ${profile.system===name?'selected':''}>${name}</option>`).join('')}</select></div></div><div class="field"><label>Default cost code</label><input id="v176-profile-cost" class="input" value="${v176Esc(profile.costCode||'')}" placeholder="e.g. BRICK-001"></div><h3>Columns and order</h3><p class="muted">Tick the columns to include. They export in the numbered order shown below.</p><div class="v176-column-grid">${Object.entries(V176_EXPORT_COLUMNS).map(([key,label],index)=>`<label><input type="checkbox" data-v176-export-column="${key}" ${profile.columns.includes(key)?'checked':''}><span><b>${index+1}. ${label}</b></span></label>`).join('')}</div><button class="btn orange" onclick="v176SaveExportProfile('${id}')">Save profile</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function v176SaveExportProfile(id){if(!v176CanManageExportProfiles())return toast('Payroll lock/export permission is required');let name=(document.getElementById('v176-profile-name')?.value||'').trim(),columns=[...document.querySelectorAll('[data-v176-export-column]:checked')].map(input=>input.dataset.v176ExportColumn);if(!name||!columns.length)return toast('Enter a profile name and choose at least one column');let profile=(db.payrollExportProfiles||[]).find(row=>row.id===id);if(!profile){profile={id:v176Id('export-profile'),createdAt:new Date().toISOString(),createdBy:currentUser()?.name||'',builtIn:false};db.payrollExportProfiles.push(profile)}profile.name=name;profile.system=document.getElementById('v176-profile-system')?.value||'Generic CSV';profile.costCode=(document.getElementById('v176-profile-cost')?.value||'').trim();profile.columns=columns;profile.updatedAt=new Date().toISOString();profile.updatedBy=currentUser()?.name||'';v176ExportProfileId=profile.id;save();audit('payments','export profile',profile.id,`Payroll export profile saved — ${name}`,'');modal=null;savedState('Export profile saved');render()}
function v176DeleteExportProfile(id){if(!v176CanManageExportProfiles())return;let profile=(db.payrollExportProfiles||[]).find(row=>row.id===id);if(!profile||profile.builtIn)return;if(!confirm(`Delete export profile “${profile.name}”?`))return;db.payrollExportProfiles=db.payrollExportProfiles.filter(row=>row.id!==id);v176ExportProfileId='';save();audit('payments','export profile',id,`Payroll export profile deleted — ${profile.name}`,'');render()}
function v176ProfileExportRows(siteId,from,to,profile){let snapshot=v175PayrollSnapshot(siteId,from,to),records=[];snapshot.people.forEach(row=>records.push({employeeName:row.name,employeeReference:row.id,recordType:`Individual · ${row.type}`,site:siteName(siteId),from,to,recordedHours:row.recorded.toFixed(2),payableHours:row.payable.toFixed(2),authorisedOvertime:row.authorisedOT.toFixed(2),lateExcluded:row.excluded.toFixed(2),gross:row.gross.toFixed(2),costCode:profile.costCode||''}));snapshot.setPrice.forEach(row=>records.push({employeeName:row.payee,employeeReference:row.payeeKey||'',recordType:`Set Price · ${row.ref} · ${row.stage}`,site:siteName(siteId),from,to,recordedHours:'',payableHours:'',authorisedOvertime:'',lateExcluded:'',gross:row.due.toFixed(2),costCode:profile.costCode||''}));snapshot.variations.forEach(row=>records.push({employeeName:row.payee,employeeReference:row.payeeKey||'',recordType:`Variation · ${row.ref} · ${row.stage}`,site:siteName(siteId),from,to,recordedHours:'',payableHours:'',authorisedOvertime:'',lateExcluded:'',gross:row.due.toFixed(2),costCode:profile.costCode||''}));return [[...profile.columns.map(key=>V176_EXPORT_COLUMNS[key]||key)],...records.map(record=>profile.columns.map(key=>record[key]??''))]}
function v176ExportPayrollProfile(siteId){let profile=v176ExportProfile();if(!profile)return toast('Create an export profile first');v174EnsurePayPeriod();if(!v174RangeValid(v174PayFrom,v174PayTo))return toast('Choose a valid pay period');let rows=v176ProfileExportRows(siteId,v174PayFrom,v174PayTo,profile),escape=value=>'"'+String(value??'').replace(/"/g,'""')+'"',blob=new Blob([rows.map(row=>row.map(escape).join(',')).join('\r\n')],{type:'text/csv;charset=utf-8'}),link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download=`${profile.name.replace(/[^a-z0-9]+/gi,'-').toLowerCase()}-${v174PayFrom}-to-${v174PayTo}.csv`;link.click();setTimeout(()=>URL.revokeObjectURL(link.href),1000);audit('payments','pay export',profile.id,`${profile.name} exported — ${v174PayFrom} to ${v174PayTo}`,siteId)}
function v176ExportProfilePanel(siteId){if(!hasPerm('view_financials'))return'';let profiles=db.payrollExportProfiles||[],profile=v176ExportProfile(),manage=v176CanManageExportProfiles();return `<div class="v176-export-panel"><div class="sectionhead"><div><span class="att-kicker">PAYROLL / ACCOUNTING PROFILE</span><h2>Export in the format you need</h2><div class="muted">Saved column mappings and cost codes for Accounts. CSV only — no statutory submission.</div></div></div><div class="v176-export-actions"><select class="select" onchange="v176ExportProfileId=this.value;render()">${profiles.map(row=>`<option value="${row.id}" ${profile?.id===row.id?'selected':''}>${v176Esc(row.name)} · ${v176Esc(row.system)}</option>`).join('')}</select><button class="btn green" onclick="v176ExportPayrollProfile('${siteId}')">⇩ Export with profile</button>${manage?`<button class="btn light" onclick="v176OpenExportProfile('${profile?.id||''}')">Edit profile</button><button class="btn light" onclick="v176OpenExportProfile()">＋ New profile</button>${profile&&!profile.builtIn?`<button class="btn light v176-team-danger" onclick="v176DeleteExportProfile('${profile.id}')">Delete</button>`:''}`:''}</div>${profile?`<div class="muted" style="margin-top:8px">${profile.columns.map(key=>V176_EXPORT_COLUMNS[key]).join(' → ')}${profile.costCode?' · Cost code '+v176Esc(profile.costCode):''}</div>`:''}</div>`}
const v176BaseContractorPayReview=contractorPayReview;
contractorPayReview=function(siteId){return v176ExportProfilePanel(siteId)+v176BaseContractorPayReview(siteId)};

// Keep pay values restricted to commercial roles while allowing delegated
// working-hours changes without exposing financial information.
function v176CanEditHours(){return hasPerm('manage_working_hours')}
function v176CanEditPay(){return hasPerm('view_financials')&&(hasPerm('manage_payments')||canDirector())}
v176OpenWorkforceEditor=function(teamId,memberId){let wm=v176BaseAttendanceMemberById(memberId),canHours=v176CanEditHours(),canPay=v176CanEditPay();if(!wm||(!canHours&&!canPay))return toast('Working-hours or pay management permission is required');let m=wm.member,payHtml=canPay?`<div class="two"><div class="field"><label>Pay type</label><select id="v176-pay-type" class="select"><option ${m.employmentType==='Not tracked'?'selected':''}>Not tracked</option><option ${m.employmentType==='Hourly'?'selected':''}>Hourly</option><option ${m.employmentType==='Day Rate'?'selected':''}>Day Rate</option><option ${m.employmentType==='Price Work'?'selected':''}>Price Work</option></select></div><div class="field"><label>Base / hourly rate</label><input id="v176-base-pay" class="input" type="number" min="0" step="0.01" value="${Number(m.basePay||m.hourlyRate||0)}"></div></div><div class="two"><div class="field"><label>Day rate</label><input id="v176-day-rate" class="input" type="number" min="0" step="0.01" value="${Number(m.dayRate||0)}"></div><div class="field"><label>Overtime rate</label><input id="v176-ot-rate" class="input" type="number" min="0" step="0.01" value="${Number(m.overtimeRate||0)}"></div></div><div class="field"><label>Pay change effective from</label><input id="v176-pay-effective" class="input" type="date" value="${iso()}"></div>`:'<div class="role-template-note"><b>Pay is hidden.</b> Your account can change working hours but does not have commercial access.</div>',hoursHtml=canHours?`<h3>Normal working hours</h3><div class="v174-workweek">${v174WorkingScheduleRows(m,attendanceDefaultSiteId())}</div>`:'<div class="role-template-note"><b>Working hours are read-only for this account.</b></div>';modal=`<div class="modal"><div class="modalbox v174-hours-modal"><span class="att-kicker">WORK & PAY SETTINGS</span><h2>${v176Esc(m.name)}</h2><p class="muted">Only the areas permitted for your role are shown. Previous pay values are retained in history.</p>${payHtml}${hoursHtml}<button class="btn orange" onclick="v176SaveWorkforceEditor('${teamId}','${memberId}')">Save changes</button> <button class="btn light" onclick="openTeamMember('${teamId}','${memberId}')">Cancel</button></div></div>`;render()};
v176SaveWorkforceEditor=function(teamId,memberId){let wm=v176BaseAttendanceMemberById(memberId),canHours=v176CanEditHours(),canPay=v176CanEditPay();if(!wm||(!canHours&&!canPay))return;let m=wm.member;if(canHours){let schedule={},usual=[];for(let dow=0;dow<7;dow++){let active=!!document.getElementById(`v174-day-${dow}`)?.checked,start=document.getElementById(`v174-start-${dow}`)?.value||'07:30',end=document.getElementById(`v174-end-${dow}`)?.value||'16:30';if(active&&v169ParseTime(end)<=v169ParseTime(start))return toast('Each finishing time must be after its starting time');schedule[dow]={active,start,end};if(active)usual.push(dow)}if(!usual.length)return toast('Select at least one normal working day');m.workingSchedule=schedule;m.usualDays=usual;(db.timesheetApprovals||[]).filter(approval=>approval.workerId===memberId&&!approval.superseded).forEach(approval=>approval.needsReapproval=true)}if(canPay){let previous={employmentType:m.employmentType,basePay:Number(m.basePay||m.hourlyRate||0),dayRate:Number(m.dayRate||0),overtimeRate:Number(m.overtimeRate||0)},next={employmentType:document.getElementById('v176-pay-type')?.value||'Not tracked',basePay:Number(document.getElementById('v176-base-pay')?.value||0),dayRate:Number(document.getElementById('v176-day-rate')?.value||0),overtimeRate:Number(document.getElementById('v176-ot-rate')?.value||0)};m.payRateHistory=Array.isArray(m.payRateHistory)?m.payRateHistory:[];if(JSON.stringify(previous)!==JSON.stringify(next))m.payRateHistory.unshift({id:v176Id('pay-rate'),effectiveFrom:document.getElementById('v176-pay-effective')?.value||iso(),previous,next,changedAt:new Date().toISOString(),changedBy:currentUser()?.name||''});m.employmentType=next.employmentType;m.basePay=next.basePay;m.hourlyRate=next.basePay;m.dayRate=next.dayRate;m.overtimeRate=next.overtimeRate}save();audit('people','work and pay',memberId,`${m.name}: ${canHours?'working hours ':''}${canHours&&canPay?'and ':''}${canPay?'pay ':''}updated`,'');savedState('Workforce settings saved');openTeamMember(teamId,memberId)};
v176WorkforceCard=function(teamId,member){let linked=(db.users||[]).find(user=>user.id===member.linkedUserId||user.linkedMemberId===member.id),wm=v176BaseAttendanceMemberById(member.id),canHours=v176CanEditHours(),canPay=v176CanEditPay(),canPeople=hasPerm('manage_gangs'),canUsers=hasPerm('manage_users'),editLabel=canHours&&canPay?'Edit working hours & base pay':canHours?'Edit working hours':canPay?'Edit base pay':'';if(!canHours&&!canPay&&!canPeople&&!canUsers)return'';let linkHtml=canUsers?`<div class="v176-link-row"><div class="field"><label>Linked SiteSight user</label><select id="v176-link-user" class="select">${v176LinkChoices(member)}</select></div><button class="btn blue" onclick="v176SaveUserLink('${teamId}','${member.id}')">Save link</button></div>${!linked?`<button class="btn light" onclick="v176CreateUserForMember('${teamId}','${member.id}')">＋ Create a new linked login</button>`:''}`:linked?`<div class="muted">Linked login: ${v176Esc(linked.name)} · ${v176Esc(linked.role)}</div>`:'',membershipHtml=canPeople?`<select class="select" aria-label="Move person" onchange="if(this.value)v176MovePerson('${member.id}',this.value)"><option value="">Move / assign…</option>${v176TeamChoices(teamId)}</select><button class="btn light v176-team-danger" onclick="v176DeletePerson('${member.id}')">Remove person</button>`:'';return `<div class="v176-workforce-card"><h3>Workforce controls</h3>${linkHtml}<div class="actions" style="margin-top:10px">${editLabel?`<button class="btn light" onclick="v176OpenWorkforceEditor('${teamId}','${member.id}')">${editLabel}</button>`:''}${membershipHtml}</div>${canPay?`<div class="v176-pay-history">${(member.payRateHistory||[]).length?`${member.payRateHistory.length} previous pay change${member.payRateHistory.length===1?'':'s'} retained`:'No previous pay changes recorded'} · Current: ${v176Esc(member.employmentType||'Not tracked')} · ${v176Esc(wm?.gang.name||'')}</div>`:''}</div>`};

// Re-run migration after all V1.76 role/profile extensions are loaded.
if((db.users||[]).length)migrate(false);

/* ===== v177-connected-workforce.js ===== */
'use strict';

// SiteSight V1.77 — connected workforce, resilient search, workforce analytics,
// offline clock requests, archive restore, Web Push setup and CIS reconciliation.

let v177PeopleQuery='',v177ArchiveQuery='',v177ArchiveFrom='',v177ArchiveTo='',
    v177ReportFrom='',v177ReportTo='',v177AnalyticsKind='',v177AnalyticsId='',
    v177AnalyticsFrom='',v177AnalyticsTo='',v177PushState='unknown';

function v177Esc(value){return v176Esc(value)}
function v177Js(value){return String(value??'').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/[\r\n]/g,' ')}
function v177Date(value){return String(value||'').slice(0,10)}
function v177Today(){return iso()}
function v177MonthStart(){return v177Today().slice(0,8)+'01'}
function v177InRange(value,from,to){let date=v177Date(value);return !!date&&(!from||date>=from)&&(!to||date<=to)}
function v177SearchValue(key,value){
  if(key==='drawings')drawingQuery=value;
  else if(key==='issues')issueSearch=value;
  else if(key==='audit')auditSearch=value;
  else if(key==='payments')paymentSearch=value;
  else if(key==='dayworks')contractorDayworkSearch=value;
  else if(key==='people')v177PeopleQuery=value;
  else if(key==='archive')v177ArchiveQuery=value;
}
function v177LiveSearch(input,key){
  let value=input?.value||'',start=Number.isFinite(input?.selectionStart)?input.selectionStart:value.length,end=Number.isFinite(input?.selectionEnd)?input.selectionEnd:start;
  v177SearchValue(key,value);render();requestAnimationFrame(()=>{let next=document.querySelector(`[data-v177-search="${key}"]`);if(!next)return;next.focus();try{next.setSelectionRange(start,end)}catch(e){}})
}
function v177FixSearchMarkup(html){
  const replacements=[
    ['oninput="drawingQuery=this.value;render()"','data-v177-search="drawings" oninput="v177LiveSearch(this,\'drawings\')"'],
    ['oninput="issueSearch=this.value;render()"','data-v177-search="issues" oninput="v177LiveSearch(this,\'issues\')"'],
    ['oninput="auditSearch=this.value;render()"','data-v177-search="audit" oninput="v177LiveSearch(this,\'audit\')"'],
    ['oninput="paymentSearch=this.value;render()"','data-v177-search="payments" oninput="v177LiveSearch(this,\'payments\')"'],
    ['oninput="contractorDayworkSearch=this.value;render()"','data-v177-search="dayworks" oninput="v177LiveSearch(this,\'dayworks\')"']
  ];
  return replacements.reduce((out,[before,after])=>out.split(before).join(after),String(html||''))
}

const v177BaseSiteDrawingsPage=siteDrawingsPage;
siteDrawingsPage=function(){return v177FixSearchMarkup(v177BaseSiteDrawingsPage())};
const v177BaseIssuesPage=issuesPage;
issuesPage=function(){return v177FixSearchMarkup(v177BaseIssuesPage())};
const v177BaseAuditPage=auditPage;
auditPage=function(){return v177FixSearchMarkup(v177BaseAuditPage())};
const v177BaseDayworksPage=contractorDayworksPage;
contractorDayworksPage=function(){return v177FixSearchMarkup(v177BaseDayworksPage())};

// Site Diary is always a primary left-hand tab immediately after Calendar.
const v177BaseRoleNavItems=v173RoleNavItems;
v173RoleNavItems=function(user=currentUser()){
  let items=v177BaseRoleNavItems(user),diary=items.find(item=>item.id==='diary');
  if(!diary&&v173CanOpenView('diary',user))diary={id:'diary',...V173_MAIN_NAV.diary};
  if(!diary)return items;
  items=items.filter(item=>item.id!=='diary');let calendarIndex=items.findIndex(item=>item.id==='calendar');
  items.splice(calendarIndex>=0?calendarIndex+1:items.length,0,diary);return items
};

// Early arrivals remain recorded but are not payable before the person's set start.
const v177BaseShiftPayBreakdown=v174ShiftPayBreakdown;
v174ShiftPayBreakdown=function(shift,member){
  let row=v177BaseShiftPayBreakdown(shift,member),date=attendanceDateOnly(shift?.clockIn?.time,shift?.siteId),schedule=v174MemberScheduleForDate(member,shift?.siteId,date),earlyMinutes=0;
  if(row.recordedHours>0&&schedule.active){let actual=v169TimeMinutes(shift.clockIn.time,shift.siteId),planned=v169ParseTime(schedule.start,'07:30');earlyMinutes=Math.max(0,planned-actual)}
  row.clockToClockHours=row.recordedHours;row.earlyMinutes=earlyMinutes;row.recordedHours=Math.max(0,row.recordedHours-earlyMinutes/60);row.payableHours=Math.max(0,row.payableHours-earlyMinutes/60);return row
};

// ---- Teams / Individuals search and permission-gated analytics -----
function v177PersonMatches(member,teamName,query){let q=String(query||'').trim().toLowerCase();return !q||[member.name,member.role,teamName].join(' ').toLowerCase().includes(q)}
function v177AnalyticsButton(kind,id){return hasPerm('analytics')&&roleCanAccessPlots()?`<button class="btn blue smallbtn v177-analytics-link" onclick="v177OpenWorkforceAnalytics('${kind}','${id}')">◒ Analytics</button>`:''}
function v177PeopleSearchPanel(total){return `<div class="panel v177-people-search"><div class="searchbox"><span>⌕</span><input data-v177-search="people" value="${v177Esc(v177PeopleQuery)}" placeholder="Find an individual by name, trade or Team…" oninput="v177LiveSearch(this,'people')"></div><span><b>${total}</b> person${total===1?'':'s'} shown</span>${v177PeopleQuery?'<button class="btn light smallbtn" onclick="v177PeopleQuery=\'\';render()">Clear</button>':''}</div>`}

gangsPage=function(){
  if(!hasPerm('gangs'))return '<div class="panel empty-help"><b>You do not have permission to view Teams & Individuals.</b></div>';
  let individuals=v176Individuals(),allIndividuals=individuals?.members||[],allTeams=v176ActiveTeams(),query=v177PeopleQuery.trim(),people=allIndividuals.filter(member=>v177PersonMatches(member,'Individual',query)),
      teams=allTeams.map(team=>({...team,members:(team.members||[]).filter(member=>v177PersonMatches(member,team.name,query))})).filter(team=>!query||team.members.length||team.name.toLowerCase().includes(query.toLowerCase())),
      shownPeople=people.length+teams.reduce((n,team)=>n+team.members.length,0);
  return `<div class="topbar"><div><h1>Teams & Individuals</h1><div class="sub">Find people quickly, review performance and manage Team membership without losing records</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(s=>`<option value="${s.id}" ${site===s.id?'selected':''}>${v177Esc(s.name)}</option>`).join('')}</select>${hasPerm('manage_gangs')?`<button class="btn light" onclick="goNav('workforce-archive')">Former people</button><button class="btn blue" onclick="v176NewPerson()">＋ Add Individual</button><button class="btn orange" onclick="addTeam()">＋ Add Team</button>`:''}</div></div>
  ${v177PeopleSearchPanel(shownPeople)}
  <div class="panel v176-people-panel"><div class="sectionhead"><div><span class="att-kicker">NOT IN A TEAM</span><h2>Individuals</h2><div class="muted">Independent people can still be assigned work, paid and linked to a login.</div></div><span class="badge b-blue">${people.length}</span></div><div class="v176-people-grid">${people.map(member=>`<div class="v176-person-card"><div onclick="openTeamMember('${V176_INDIVIDUALS_ID}','${member.id}')" class="clickable"><b>${v177Esc(member.name)}</b><span>${v177Esc(member.role||'Individual')} · ${member.linkedUserId?'Login linked':'No login'}</span></div><div class="v176-person-actions"><button class="btn light smallbtn" onclick="openTeamMember('${V176_INDIVIDUALS_ID}','${member.id}')">Open</button>${v177AnalyticsButton('person',member.id)}${hasPerm('manage_gangs')?`<select class="select" aria-label="Add to Team" onchange="if(this.value)v176MovePerson('${member.id}',this.value)"><option value="">Add to Team…</option>${allTeams.map(team=>`<option value="${team.id}">${v177Esc(team.name)}</option>`).join('')}</select>`:''}</div></div>`).join('')||'<div class="empty-help"><b>No Individuals match this search.</b></div>'}</div></div>
  <div class="cards">${teams.map(team=>{let items=db.items.filter(item=>canViewItem(item)&&item.gang===team.id&&!db.sites.find(s=>s.id===item.site)?.archived&&(site==='all'||item.site===site)),members=team.members||[];return `<div class="card gangcard"><div class="sectionhead"><h3 onclick="openGang('${team.id}')" class="clickable">${v177Esc(team.name)}</h3>${team.active?badge('Active'):badge('Not started')}</div><p>Assigned <b>${items.length}</b> · In progress <b>${items.filter(item=>overall(item)==='In progress').length}</b></p><p>Members shown <b>${members.length}</b> · Issues <b>${items.reduce((n,item)=>n+issues(item),0)}</b></p><div class="team-members">${members.map(member=>`<div class="team-member"><div class="clickable" onclick="openTeamMember('${team.id}','${member.id}')"><b>${v177Esc(member.name)}</b><span>${v177Esc(member.role||'Team member')}</span></div>${v177AnalyticsButton('person',member.id)}</div>`).join('')||'<div class="muted team-no-members">No matching Team members.</div>'}</div><div class="team-actions"><button class="btn light smallbtn" onclick="openGang('${team.id}')">Open Team</button>${v177AnalyticsButton('team',team.id)}${hasPerm('manage_gangs')?`<button class="btn blue smallbtn" onclick="manageTeamMembers('${team.id}')">Manage members</button><button class="btn light smallbtn" onclick="editTeamName('${team.id}')">Edit name</button><button class="btn light smallbtn v176-team-danger" onclick="v177DeleteTeam('${team.id}')">Delete Team</button>`:''}</div></div>`}).join('')||smartEmpty('No Teams match this search.','Clear the search or create a Team.',hasPerm('manage_gangs')?'＋ Add Team':'',hasPerm('manage_gangs')?'addTeam()':'','♟')}</div>`
}

function v177WorkforceAnalyticsRows(kind,id,from,to){
  if(!roleCanAccessPlots())return {member:null,team:null,items:[],shifts:[],dayworks:[],completed:0,issuesRaised:0,hours:0,payable:0,payments:0};
  let member=kind==='person'?attendanceMemberById(id)?.member:null,team=kind==='team'?(db.gangs||[]).find(row=>row.id===id):attendanceMemberById(id)?.gang,
      memberIds=kind==='team'?(team?.members||[]).map(row=>row.id):[id],items=(db.items||[]).filter(item=>canViewItem(item)&&(site==='all'||item.site===site)&&item.gang===team?.id),
      shifts=(db.attendanceShifts||[]).filter(shift=>memberIds.includes(shift.workerId)&&v177InRange(attendanceDateOnly(shift.clockIn?.time,shift.siteId),from,to)),
      dayworks=(db.dayworks||[]).filter(row=>v177InRange(row.date,from,to)&&(kind==='team'?row.assignedType==='Team'&&row.assignedId===id:row.assignedType==='Individual'&&row.assignedId===id)),
      completed=items.reduce((n,item)=>n+(item.type==='House Plot'?(item.lifts||[]).filter(lift=>lift.status==='Complete'&&v177InRange(lift.completedDate,from,to)).length:(item.status==='Complete'&&v177InRange(item.completedDate,from,to)?1:0)),0),
      issuesRaised=items.reduce((n,item)=>n+(item.issues||[]).filter(issue=>v177InRange(issue.created,from,to)).length,0),hours=shifts.reduce((n,shift)=>{let wm=attendanceMemberById(shift.workerId);return n+(wm?v174ShiftPayBreakdown(shift,wm.member).recordedHours:0)},0),payable=shifts.reduce((n,shift)=>{let wm=attendanceMemberById(shift.workerId);return n+(wm?v174ShiftPayBreakdown(shift,wm.member).payableHours:0)},0),payments=allPaymentRows().filter(row=>v177InRange(row.date,from,to)).reduce((n,row)=>{if(kind==='team')return n+(v176PaymentTeam(row)?.id===id?Number(row.amount||0):0);if((row.paidTo||'')===id)return n+Number(row.amount||0);let split=v176PaymentSplit(row).find(part=>part.workerId===id);return n+Number(split?.amount||0)},0);
  return {member,team,items,shifts,dayworks,completed,issuesRaised,hours,payable,payments}
}
function v177OpenWorkforceAnalytics(kind,id){if(!hasPerm('analytics')||!roleCanAccessPlots())return toast('Workforce progress analytics require Supervisor access or above');v177AnalyticsKind=kind;v177AnalyticsId=id;if(!v177AnalyticsFrom||!v177AnalyticsTo){v177AnalyticsFrom=v177MonthStart();v177AnalyticsTo=v177Today()}v177RenderWorkforceAnalytics()}
function v177SetAnalyticsDate(which,value){if(which==='from')v177AnalyticsFrom=value;else v177AnalyticsTo=value;v177RenderWorkforceAnalytics()}
function v177RenderWorkforceAnalytics(){
  if(!hasPerm('analytics')||!roleCanAccessPlots())return;let invalid=v177AnalyticsFrom&&v177AnalyticsTo&&v177AnalyticsFrom>v177AnalyticsTo,data=invalid?null:v177WorkforceAnalyticsRows(v177AnalyticsKind,v177AnalyticsId,v177AnalyticsFrom,v177AnalyticsTo),title=v177AnalyticsKind==='team'?data?.team?.name:data?.member?.name;
  modal=`<div class="modal"><div class="modalbox v177-analytics-modal"><span class="att-kicker">PERFORMANCE OVERVIEW</span><h2>${v177Esc(title||'Workforce analytics')}</h2><p class="muted">A quick operational view. Use any From / To dates needed for the review.</p><div class="v177-range"><label>From<input class="input" type="date" value="${v177AnalyticsFrom}" onchange="v177SetAnalyticsDate('from',this.value)"></label><span>→</span><label>To<input class="input" type="date" value="${v177AnalyticsTo}" onchange="v177SetAnalyticsDate('to',this.value)"></label></div>${invalid?'<div class="payment-range-warning">From must be before To.</div>':`<div class="v177-metric-grid"><div><span>Completed work</span><b>${data.completed}</b></div><div><span>Assigned now</span><b>${data.items.length}</b></div><div><span>Issues raised</span><b>${data.issuesRaised}</b></div><div><span>Recorded hours</span><b>${data.hours.toFixed(2)}</b></div><div><span>Payable hours</span><b>${data.payable.toFixed(2)}</b></div><div><span>Dayworks</span><b>${data.dayworks.length}</b></div>${hasPerm('view_financials')?`<div><span>Payments allocated</span><b>${attendanceMoney(data.payments)}</b></div>`:''}</div><div class="v177-analytics-note"><b>${v177AnalyticsKind==='team'?'Team view':'Individual view'}</b><span>${v177AnalyticsKind==='team'?'Includes current Team work, members’ attendance, assigned Dayworks and Team payments.':'Includes the person’s attendance, direct Dayworks and direct or saved Team-payment allocations.'}</span></div>`}<button class="btn light" onclick="modal=null;render()">Close</button></div></div>`;render()
}

// ---- Offline clock request queue -----------------------------------
function v177PendingOfflineRequests(){return (db.offlineClockRequests||[]).filter(row=>row.status==='Awaiting Verification')}
const v177BaseOpenAttendanceClock=openAttendanceClock;
openAttendanceClock=function(action){let management=v176ManagementClockUser(),result=v177BaseOpenAttendanceClock(action);if(management||!modal||!modal.includes('attendance-clock-modal'))return result;let button=`<button class="btn light v177-offline-request" onclick="v177OpenOfflineClockRequest('${action}')">Poor signal? Save a clock request</button>`;modal=modal.replace('<button class="btn light" onclick="modal=null;render()">Cancel</button>',button+' <button class="btn light" onclick="modal=null;render()">Cancel</button>');render();return result};
function v177OpenOfflineClockRequest(action){
  let linked=attendanceLinkedMember(),active=linked?attendanceActiveShift(linked.member.id):null,sid=action==='out'?active?.siteId:(document.getElementById('att-clock-site')?.value||v172SelfClockSites(linked)[0]?.id||'');if(!linked||!sid)return toast('Choose an authorised Site first');
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">POOR SIGNAL</span><h2>Save a Clock ${action==='out'?'Out':'In'} request</h2><div class="v177-unverified-warning"><b>This does not create paid hours.</b><span>The request records your device time without GPS and stays unverified until an authorised manager accepts or rejects it.</span></div><p><b>${v177Esc(linked.member.name)}</b> · ${v177Esc(siteName(sid))}</p><div class="field"><label>Why could you not clock normally?</label><textarea id="v177-offline-reason" class="textarea" placeholder="e.g. No mobile signal at the plot"></textarea></div><button class="btn orange" onclick="v177SaveOfflineClockRequest('${action}','${sid}')">Save request</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function v177SaveOfflineClockRequest(action,siteId){
  let linked=attendanceLinkedMember(),reason=(document.getElementById('v177-offline-reason')?.value||'').trim();if(!linked||!reason)return toast('Enter the reason for the request');db.offlineClockRequests=db.offlineClockRequests||[];let row={id:v176Id('offline-clock'),workerId:linked.member.id,userId:currentUser()?.id||'',siteId,action:action==='out'?'clock-out':'clock-in',requestedAt:new Date().toISOString(),deviceTimeZone:Intl.DateTimeFormat().resolvedOptions().timeZone||'',reason,status:'Awaiting Verification',createdOffline:!navigator.onLine,reviewedAt:'',reviewedBy:'',reviewReason:'',shiftId:''};db.offlineClockRequests.unshift(row);save();audit('attendance','offline clock request',row.id,`${linked.member.name}: ${row.action} request saved — unverified and unpaid`,siteId);modal=null;savedState('Clock request saved for manager review');render()
}
function v177OfflineRequestsPanel(){
  if(!v175CanCorrectTime())return'';let rows=v177PendingOfflineRequests().filter(row=>site==='all'||row.siteId===site);return `<div class="panel v177-offline-panel"><div class="sectionhead"><div><span class="att-kicker">POOR-SIGNAL QUEUE</span><h2>Unverified clock requests</h2><div class="muted">No request creates hours until an authorised manager reviews it.</div></div><span class="badge ${rows.length?'b-amber':'b-green'}">${rows.length}</span></div>${rows.map(row=>{let wm=attendanceMemberById(row.workerId);return `<div class="v177-request-row"><span><b>${v177Esc(wm?.member.name||'Former person')}</b><small>${row.action==='clock-in'?'Clock In':'Clock Out'} · ${new Date(row.requestedAt).toLocaleString('en-GB')} · ${v177Esc(siteName(row.siteId))}</small></span><p>${v177Esc(row.reason)}</p><button class="btn orange smallbtn" onclick="v177ReviewOfflineRequest('${row.id}')">Review</button></div>`}).join('')||'<div class="muted">No unverified clock requests.</div>'}</div>`
}
function v177ReviewOfflineRequest(id){
  if(!v175CanCorrectTime())return;let row=(db.offlineClockRequests||[]).find(item=>item.id===id),wm=attendanceMemberById(row?.workerId);if(!row||!wm)return;
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">MANAGER VERIFICATION</span><h2>${v177Esc(wm.member.name)}</h2><p class="muted">Requested ${row.action==='clock-in'?'Clock In':'Clock Out'} · ${new Date(row.requestedAt).toLocaleString('en-GB')} · ${v177Esc(siteName(row.siteId))}</p><div class="v177-unverified-warning"><b>Check this against the Site record.</b><span>Acceptance creates a manager-approved attendance point without GPS. Pay and overtime still follow the normal approval rules.</span></div><div class="field"><label>Accepted date and time</label><input id="v177-request-time" class="input" type="datetime-local" value="${attendanceDateTimeLocalValue(row.requestedAt)}"></div><div class="field"><label>Manager reason / evidence</label><textarea id="v177-request-review" class="textarea" placeholder="Who confirmed the time and why?"></textarea></div><button class="btn green" onclick="v177DecideOfflineRequest('${id}','Approved')">✓ Accept request</button> <button class="btn red" onclick="v177DecideOfflineRequest('${id}','Rejected')">Reject</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function v177DecideOfflineRequest(id,decision){
  if(!v175CanCorrectTime())return toast('Only an authorised manager can decide clock requests');let row=(db.offlineClockRequests||[]).find(item=>item.id===id),wm=attendanceMemberById(row?.workerId),reason=(document.getElementById('v177-request-review')?.value||'').trim(),local=document.getElementById('v177-request-time')?.value||'';if(!row||!wm||!reason)return toast('Enter the manager reason / evidence');if(decision==='Approved'&&!local)return toast('Enter the accepted time');
  if(decision==='Approved'){
    let time=new Date(local).toISOString(),point={time,status:'Offline Request Approved',serverTime:false,locationRequested:false,actorId:currentUser()?.id||'',actorName:currentUser()?.name||'',reason};
    if(row.action==='clock-in'){if(attendanceActiveShift(row.workerId))return toast('This person already has an active shift');let shift={id:v176Id('ats'),workerId:row.workerId,gangId:wm.gang.id,siteId:row.siteId,clockIn:point,clockOut:null,breakMinutes:Number(wm.member.defaultBreakMinutes??30),source:'Approved Offline Request',corrections:[],createdAt:new Date().toISOString(),createdBy:currentUser()?.name||'',overtimeApproval:{status:'Not Authorised',minutes:0,reason:'',approvedBy:'',approvedById:'',approvedAt:''}};db.attendanceShifts.push(shift);row.shiftId=shift.id}
    else{let active=attendanceActiveShift(row.workerId);if(!active)return toast('No active shift is available to Clock Out');active.clockOut=point;row.shiftId=active.id;attendanceMarkApprovalNeedsRecheck(active.workerId,active.siteId,active.clockIn.time)}
  }
  row.status=decision;row.reviewedAt=new Date().toISOString();row.reviewedBy=currentUser()?.name||'';row.reviewedById=currentUser()?.id||'';row.reviewReason=reason;save();audit('attendance','offline clock request',row.id,`${wm.member.name}: ${row.action} request ${decision.toLowerCase()} — ${reason}`,row.siteId);modal=null;savedState(`Clock request ${decision.toLowerCase()}`);render()
}
const v177BaseAttendancePage=attendancePage;
attendancePage=function(){let html=v177BaseAttendancePage();return v175CanCorrectTime()?v174InsertAfterTopbar(html,v177OfflineRequestsPanel()):html};

// ---- Former People / Deleted Teams restore workspace --------------
function v177DeleteTeam(teamId){
  if(!hasPerm('manage_gangs'))return;let team=(db.gangs||[]).find(row=>row.id===teamId&&row.id!==V176_INDIVIDUALS_ID);if(!team)return;if(!confirm(`Delete ${team.name}? Its members will become Individuals and the Team can be restored later.`))return;let individuals=v176Individuals(true),deletedAt=new Date().toISOString();(team.members||[]).forEach(member=>individuals.members.push(member));db.teamArchive=db.teamArchive||[];db.teamArchive.push({id:team.id,name:team.name,contact:team.contact||'',active:team.active!==false,deletedAt,deletedBy:currentUser()?.name||''});(db.items||[]).forEach(item=>{if(item.gang===team.id)item.gang='';if(item.paidTo===team.id)item.paidTo='';(item.lifts||[]).forEach(lift=>{if(lift.paidTo===team.id)lift.paidTo=''});(item.variations||[]).forEach(v=>{if(v.team===team.id)v.team=''})});(db.dayworks||[]).forEach(daywork=>{if(daywork.assignedType==='Team'&&daywork.assignedId===team.id){daywork.formerAssigneeName=team.name;daywork.assignedId='';daywork.assignedType=''}});db.gangs=db.gangs.filter(row=>row.id!==team.id);save();audit('people','team',team.id,`${team.name} deleted; restorable archive created`,'');savedState('Team deleted; members retained as Individuals');render()
}
function v177RestoreTeam(id){if(!hasPerm('manage_gangs'))return;let row=(db.teamArchive||[]).find(item=>item.id===id);if(!row)return;if((db.gangs||[]).some(team=>team.id===id))return toast('A Team with this record already exists');db.gangs.push({id:row.id,name:row.name,contact:row.contact||'',active:true,members:[]});db.teamArchive=db.teamArchive.filter(item=>item!==row);save();audit('people','team',id,`${row.name} restored as an empty active Team`,'');savedState('Team restored; add people from Individuals');render()}
function v177RestorePerson(id){if(!hasPerm('manage_gangs'))return;let row=(db.peopleArchive||[]).find(item=>item.member?.id===id);if(!row)return;let member=row.member,individuals=v176Individuals(true);member.activeForAttendance=false;member.employmentStatus='Active';member.clockAccessStatus='suspended';member.clockAccessReason='Restored person — Site and login access must be reviewed';member.deletedAt='';member.deletedBy='';individuals.members.push(member);db.peopleArchive=db.peopleArchive.filter(item=>item!==row);save();audit('people','person',id,`${member.name} restored to Individuals with clocking suspended`,'');savedState('Person restored to Individuals; review access before clocking');render()}
function v177ArchiveWorkspace(){
  if(!hasPerm('manage_gangs'))return '<div class="panel empty-help"><b>People administration permission is required.</b></div>';let query=v177ArchiveQuery.toLowerCase(),inRange=row=>v177InRange(row.deletedAt,v177ArchiveFrom,v177ArchiveTo),people=(db.peopleArchive||[]).filter(row=>inRange(row)&&[row.member?.name,row.member?.role,row.formerGangName].join(' ').toLowerCase().includes(query)),teams=(db.teamArchive||[]).filter(row=>inRange(row)&&[row.name,row.contact].join(' ').toLowerCase().includes(query));
  return `<div class="topbar"><div><h1>Former People & Deleted Teams</h1><div class="sub">Restore records safely without losing historic hours, payments or Audit links</div></div><button class="btn light" onclick="goNav('gangs')">← Teams & Individuals</button></div><div class="panel v177-archive-filters"><div class="searchbox"><span>⌕</span><input data-v177-search="archive" value="${v177Esc(v177ArchiveQuery)}" placeholder="Search former people or Teams…" oninput="v177LiveSearch(this,'archive')"></div><label>From<input class="input" type="date" value="${v177ArchiveFrom}" onchange="v177ArchiveFrom=this.value;render()"></label><label>To<input class="input" type="date" value="${v177ArchiveTo}" onchange="v177ArchiveTo=this.value;render()"></label><button class="btn light smallbtn" onclick="v177ArchiveQuery='';v177ArchiveFrom='';v177ArchiveTo='';render()">Clear</button></div><div class="two v177-archive-columns"><div class="panel"><div class="sectionhead"><h2>Former People</h2><span class="badge b-grey">${people.length}</span></div>${people.map(row=>`<div class="v177-archive-row"><span><b>${v177Esc(row.member?.name)}</b><small>${v177Esc(row.member?.role||'Person')} · formerly ${v177Esc(row.formerGangName||'Individual')} · ${new Date(row.deletedAt).toLocaleDateString('en-GB')}</small></span><button class="btn blue smallbtn" onclick="v177RestorePerson('${row.member.id}')">Restore to Individuals</button></div>`).join('')||'<div class="muted">No former people match this range.</div>'}</div><div class="panel"><div class="sectionhead"><h2>Deleted Teams</h2><span class="badge b-grey">${teams.length}</span></div>${teams.map(row=>`<div class="v177-archive-row"><span><b>${v177Esc(row.name)}</b><small>Deleted ${new Date(row.deletedAt).toLocaleDateString('en-GB')} by ${v177Esc(row.deletedBy||'Unknown')}</small></span><button class="btn blue smallbtn" onclick="v177RestoreTeam('${row.id}')">Restore Team</button></div>`).join('')||'<div class="muted">No deleted Teams match this range.</div>'}</div></div>`
}
const v177BaseCanOpenView=v173CanOpenView;
v173CanOpenView=function(target,user=currentUser()){if(target==='workforce-archive')return !!user&&v173UserHas(user,'manage_gangs');return v177BaseCanOpenView(target,user)};
const v177BaseRender=render;
render=function(){let user=currentUser();if(user&&view==='workforce-archive'){if(!v173CanOpenView(view,user)){view=v173HomeView(user);return v177BaseRender()}shell(v177ArchiveWorkspace());updateSyncUI();return}return v177BaseRender()};

// ---- Custom date range Site Report ---------------------------------
contractorWeeklyReportPage=function(){
  if(!hasPerm('download_reports'))return '<div class="panel empty-help"><b>You do not have report access.</b></div>';let sid=site!=='all'?site:(activeSites()[0]?.id||'');if(!v177ReportFrom||!v177ReportTo){v177ReportFrom=contractorWeekStart();v177ReportTo=attendanceAddDays(v177ReportFrom,6)}let start=v177ReportFrom,end=v177ReportTo,invalid=start>end,items=invalid?[]:db.items.filter(x=>x.site===sid),updated=items.filter(x=>v177InRange(x.lastUpdatedAt||x.lastUpdated,start,end)),openIssues=items.flatMap(x=>(x.issues||[]).filter(i=>i.status==='Open'&&(!start&&!end||[i.created,i.targetDate,i.resolved].some(date=>v177InRange(date,start,end)))).map(i=>({x,i}))),dayworks=(db.dayworks||[]).filter(d=>d.siteId===sid&&v177InRange(d.date,start,end)),leave=(db.leaveRequests||[]).filter(r=>r.siteId===sid&&r.status==='Approved'&&v174RangesOverlap(r.from,r.to,start,end)),late=items.filter(x=>contractorProgrammeState(x)?.cls==='late');
  return `<div class="topbar"><div><h1>Site Report</h1><div class="sub">Built automatically for any custom From / To date range</div></div><div class="actions"><select class="select" onchange="site=this.value;render()">${activeSites().map(s=>`<option value="${s.id}" ${s.id===sid?'selected':''}>${v177Esc(s.name)}</option>`).join('')}</select><button class="btn green" onclick="contractorPrintWeekly()">Print / Save PDF</button></div></div><div class="panel v177-report-range"><label>From<input class="input" type="date" value="${start}" onchange="v177ReportFrom=this.value;render()"></label><span>→</span><label>To<input class="input" type="date" value="${end}" onchange="v177ReportTo=this.value;render()"></label>${invalid?'<b>Check the date range</b>':`<strong>${v174RangeLabel(start,end)}</strong>`}</div><div class="weekly-report panel"><div class="weekly-report-hero"><div><span>REPORT PERIOD</span><b>${start} → ${end}</b><small>${v177Esc(siteName(sid))}</small></div><div><strong>${updated.length}</strong><span>Work Items updated</span></div><div><strong>${openIssues.length}</strong><span>Issues in range</span></div><div><strong>${late.length}</strong><span>Behind programme</span></div><div><strong>${dayworks.length}</strong><span>Dayworks</span></div></div><section><h2>Progress</h2>${updated.map(x=>`<div class="weekly-row"><b>${v177Esc(x.ref)}</b><span>${overall(x)} · ${v177Esc(gangName(x.gang))}</span></div>`).join('')||'<p class="muted">No Work Item updates in this range.</p>'}</section><section><h2>People / Leave</h2>${leave.map(r=>`<div class="weekly-row"><b>${v177Esc(contractorMemberName(r.workerId))}</b><span>${v177Esc(r.type)} · ${r.from} → ${r.to}</span></div>`).join('')||'<p class="muted">No approved leave overlaps this range.</p>'}</section><section><h2>Issues / Programme</h2>${openIssues.slice(0,20).map(r=>`<div class="weekly-row"><b>${v177Esc(r.x.ref)} · ${v177Esc(r.i.title)}</b><span>${r.i.blockingWork?'BLOCKING · ':''}${v177Esc(r.i.priority||'Normal')} ${r.i.targetDate?'· due '+r.i.targetDate:''}</span></div>`).join('')||'<p class="muted">No issues in this range.</p>'}</section><section><h2>Dayworks</h2>${dayworks.map(d=>`<div class="weekly-row"><b>${v177Esc(d.area||'Daywork')}</b><span>${v177Esc(d.work)} · ${d.date}</span></div>`).join('')||'<p class="muted">No Dayworks in this range.</p>'}</section></div>`
};

// ---- CIS profiles and payment reconciliation ----------------------
Object.assign(V176_EXPORT_COLUMNS,{utr:'UTR',verificationNumber:'CIS verification number',cisRate:'CIS rate',cisMaterials:'Direct materials',cisLiable:'Amount liable to CIS',cisDeduction:'CIS deduction',cisNet:'Net after CIS',reconciliationStatus:'Reconciliation status'});
function v177CisProfile(key){return (db.cisProfiles||[]).find(row=>row.payeeKey===key)||null}
function v177CisRate(profile){return [0,20,30].includes(Number(profile?.deductionRate))?Number(profile.deductionRate):0}
function v177CisCalculation(gross,materials,rate){gross=Math.max(0,Number(gross||0));materials=Math.max(0,Math.min(gross,Number(materials||0)));let liable=Math.max(0,gross-materials),deduction=Math.round(liable*Number(rate||0))/100,net=gross-deduction;return {gross,materials,liable,deduction,net}}
function v177OpenCisProfile(key,name){
  if(!v176CanManageExportProfiles())return toast('Payroll lock/export permission is required');let profile=v177CisProfile(key)||{payeeKey:key,name:name||paymentPayeeNameFromId(String(key).replace(/^id:/,'')),utr:'',verificationNumber:'',deductionRate:20,notes:''};
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">CIS PAYEE</span><h2>${v177Esc(profile.name||name||'Subcontractor')}</h2><div class="v177-cis-warning"><b>Accountant review required</b><span>Use the rate HMRC gave you when the subcontractor was verified. SiteSight prepares records and CSVs; it does not verify or submit CIS.</span></div><div class="two"><div class="field"><label>UTR</label><input id="v177-cis-utr" class="input" value="${v177Esc(profile.utr)}"></div><div class="field"><label>Verification number</label><input id="v177-cis-verification" class="input" value="${v177Esc(profile.verificationNumber)}"></div></div><div class="field"><label>HMRC deduction status</label><select id="v177-cis-rate" class="select"><option value="0" ${v177CisRate(profile)===0?'selected':''}>0% · Gross payment status</option><option value="20" ${v177CisRate(profile)===20?'selected':''}>20% · Registered subcontractor</option><option value="30" ${v177CisRate(profile)===30?'selected':''}>30% · Unregistered / cannot verify</option></select></div><div class="field"><label>Accountant notes</label><textarea id="v177-cis-notes" class="textarea">${v177Esc(profile.notes)}</textarea></div><button class="btn orange" onclick="v177SaveCisProfile('${v177Js(key)}','${v177Js(profile.name||name||'Subcontractor')}')">Save CIS profile</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function v177SaveCisProfile(key,name){if(!v176CanManageExportProfiles())return;let row=v177CisProfile(key);if(!row){row={id:v176Id('cis-profile'),payeeKey:key,createdAt:new Date().toISOString(),createdBy:currentUser()?.name||''};db.cisProfiles.push(row)}row.name=name;row.utr=(document.getElementById('v177-cis-utr')?.value||'').trim();row.verificationNumber=(document.getElementById('v177-cis-verification')?.value||'').trim();row.deductionRate=Number(document.getElementById('v177-cis-rate')?.value||0);row.notes=(document.getElementById('v177-cis-notes')?.value||'').trim();row.updatedAt=new Date().toISOString();row.updatedBy=currentUser()?.name||'';save();audit('payments','CIS profile',row.id,`CIS profile saved — ${name} · ${row.deductionRate}%`,'');modal=null;savedState('CIS profile saved');render()}
function v177OpenPaymentReconciliation(paymentId){
  if(!(hasPerm('manage_payments')||hasPerm('lock_payroll')))return toast('Payment management permission is required');let found=v176ActualPayment(paymentId);if(!found)return;let p=found.payment,key=paymentPayeeKey({...p,item:found.item}),name=paymentPayeeName({...p,item:found.item}),profile=v177CisProfile(key),rate=Number(p.cisDeductionRate??v177CisRate(profile)),calc=v177CisCalculation(p.amount,p.cisMaterials,rate);
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">PAYMENT RECONCILIATION</span><h2>${v177Esc(name)}</h2><p class="muted">${v177Esc(found.item.ref)} · ${p.date||'No date'} · gross ${attendanceMoney(p.amount)}</p><div class="two"><div class="field"><label>Direct materials evidenced</label><input id="v177-recon-materials" class="input" type="number" min="0" max="${Number(p.amount||0)}" step="0.01" value="${calc.materials.toFixed(2)}"></div><div class="field"><label>CIS rate used</label><select id="v177-recon-rate" class="select"><option value="0" ${rate===0?'selected':''}>0%</option><option value="20" ${rate===20?'selected':''}>20%</option><option value="30" ${rate===30?'selected':''}>30%</option></select></div></div><div class="field"><label>Reconciliation status</label><select id="v177-recon-status" class="select">${['Unreconciled','Matched','Checked','Query'].map(status=>`<option ${p.reconciliationStatus===status?'selected':''}>${status}</option>`).join('')}</select></div><div class="field"><label>Reference / accountant note</label><textarea id="v177-recon-note" class="textarea">${v177Esc(p.reconciliationNote||'')}</textarea></div><div class="v177-cis-calc"><span>Gross <b>${attendanceMoney(calc.gross)}</b></span><span>Liable before save <b>${attendanceMoney(calc.liable)}</b></span><span>Indicative deduction <b>${attendanceMoney(calc.deduction)}</b></span><span>Indicative net <b>${attendanceMoney(calc.net)}</b></span></div><button class="btn orange" onclick="v177SavePaymentReconciliation('${paymentId}')">Save reconciliation</button>${profile?` <button class="btn light" onclick="v177OpenCisProfile('${v177Js(key)}','${v177Js(name)}')">Edit CIS profile</button>`:` <button class="btn light" onclick="v177OpenCisProfile('${v177Js(key)}','${v177Js(name)}')">Set up CIS payee</button>`} <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function v177SavePaymentReconciliation(paymentId){let found=v176ActualPayment(paymentId);if(!found)return;let p=found.payment,materials=Number(document.getElementById('v177-recon-materials')?.value||0),rate=Number(document.getElementById('v177-recon-rate')?.value||0),status=document.getElementById('v177-recon-status')?.value||'Unreconciled',note=(document.getElementById('v177-recon-note')?.value||'').trim(),calc=v177CisCalculation(p.amount,materials,rate);if(materials<0||materials>Number(p.amount||0))return toast('Materials must be between £0 and the gross payment');p.cisMaterials=calc.materials;p.cisDeductionRate=rate;p.cisLiable=calc.liable;p.cisDeduction=calc.deduction;p.cisNet=calc.net;p.reconciliationStatus=status;p.reconciliationNote=note;p.reconciledAt=new Date().toISOString();p.reconciledBy=currentUser()?.name||'';save();audit('payments','CIS reconciliation',paymentId,`${found.item.ref}: payment ${status.toLowerCase()} · CIS ${rate}% · deduction ${attendanceMoney(calc.deduction)}`,found.item.site);modal=null;savedState('Payment reconciliation saved');render()}
function v177CisReconciliationPanel(rows){
  if(!hasPerm('view_financials'))return'';let manage=hasPerm('manage_payments')||hasPerm('lock_payroll');return `<div class="panel v177-reconciliation"><div class="sectionhead"><div><span class="att-kicker">CIS / ACCOUNTS</span><h2>Payment reconciliation</h2><div class="muted">Record direct materials, the HMRC-given rate and checking status for each actual payment.</div></div><button class="btn green smallbtn" onclick="v177ExportCisReconciliation()">⇩ CIS reconciliation CSV</button></div>${rows.slice(0,80).map(row=>{let key=paymentPayeeKey(row),profile=v177CisProfile(key),status=row.reconciliationStatus||'Unreconciled';return `<div class="v177-recon-row"><span><b>${v177Esc(paymentPayeeName(row))}</b><small>${v177Esc(row.item?.ref||'')} · ${row.date||'—'} · ${attendanceMoney(row.amount)}${profile?' · CIS '+v177CisRate(profile)+'%':' · No CIS profile'}</small></span><em class="status-${status.toLowerCase()}">${v177Esc(status)}</em>${manage?`<button class="btn light smallbtn" onclick="v177OpenPaymentReconciliation('${row.id}')">Reconcile</button>`:''}</div>`}).join('')||'<div class="muted">No payments match the selected date range.</div>'}<div class="v177-cis-footnote">CIS figures are working records only. Confirm verification status, materials evidence and return treatment with your accountant or HMRC before submission.</div></div>`
}
function v177ExportCisReconciliation(){if(!hasPerm('view_financials'))return;let rows=filteredPaymentRows(),data=[['Date','Site','Work item','Payee','UTR','Verification number','Gross payment','Direct materials','Amount liable to CIS','CIS rate','CIS deduction','Net after CIS','Reconciliation status','Reconciliation note']];rows.forEach(row=>{let profile=v177CisProfile(paymentPayeeKey(row)),calc=v177CisCalculation(row.amount,row.cisMaterials,row.cisDeductionRate??v177CisRate(profile));data.push([row.date,siteName(row.item.site),row.item.ref,paymentPayeeName(row),profile?.utr||'',profile?.verificationNumber||'',calc.gross.toFixed(2),calc.materials.toFixed(2),calc.liable.toFixed(2),`${Number(row.cisDeductionRate??v177CisRate(profile))}%`,calc.deduction.toFixed(2),calc.net.toFixed(2),row.reconciliationStatus||'Unreconciled',row.reconciliationNote||''])});let esc=value=>'"'+String(value??'').replace(/"/g,'""')+'"',blob=new Blob([data.map(row=>row.map(esc).join(',')).join('\r\n')],{type:'text/csv;charset=utf-8'}),link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download=`sitesight-cis-reconciliation-${paymentFrom||'start'}-to-${paymentTo||'now'}.csv`;link.click();setTimeout(()=>URL.revokeObjectURL(link.href),1000);audit('payments','CIS export','cis-reconciliation',`CIS reconciliation exported · ${rows.length} payments`,'')}
const v177BasePaymentsHistoryPage=paymentsHistoryPage;
paymentsHistoryPage=function(){let html=v177FixSearchMarkup(v177BasePaymentsHistoryPage());if(!hasPerm('view_financials'))return html;let invalid=paymentFrom&&paymentTo&&paymentFrom>paymentTo,rows=invalid?[]:filteredPaymentRows();return html+v177CisReconciliationPanel(rows)};
v176ProfileExportRows=function(siteId,from,to,profile){
  let snapshot=v175PayrollSnapshot(siteId,from,to),records=[],add=(row,key)=>{let cis=v177CisProfile(key),enriched={...row,utr:cis?.utr||'',verificationNumber:cis?.verificationNumber||'',cisRate:cis?`${v177CisRate(cis)}%`:'',cisMaterials:'',cisLiable:'',cisDeduction:'',cisNet:'',reconciliationStatus:cis?'Review actual payment':'No CIS profile'};records.push(enriched)};
  snapshot.people.forEach(row=>add({employeeName:row.name,employeeReference:row.id,recordType:`Individual · ${row.type}`,site:siteName(siteId),from,to,recordedHours:row.recorded.toFixed(2),payableHours:row.payable.toFixed(2),authorisedOvertime:row.authorisedOT.toFixed(2),lateExcluded:row.excluded.toFixed(2),gross:row.gross.toFixed(2),costCode:profile.costCode||''},'id:'+row.id));
  snapshot.setPrice.forEach(row=>add({employeeName:row.payee,employeeReference:row.payeeKey||'',recordType:`Set Price · ${row.ref} · ${row.stage}`,site:siteName(siteId),from,to,recordedHours:'',payableHours:'',authorisedOvertime:'',lateExcluded:'',gross:row.due.toFixed(2),costCode:profile.costCode||''},row.payeeKey||'name:'+String(row.payee||'').toLowerCase()));
  snapshot.variations.forEach(row=>add({employeeName:row.payee,employeeReference:row.payeeKey||'',recordType:`Variation · ${row.ref} · ${row.stage}`,site:siteName(siteId),from,to,recordedHours:'',payableHours:'',authorisedOvertime:'',lateExcluded:'',gross:row.due.toFixed(2),costCode:profile.costCode||''},row.payeeKey||'name:'+String(row.payee||'').toLowerCase()));
  return [profile.columns.map(key=>V176_EXPORT_COLUMNS[key]||key),...records.map(record=>profile.columns.map(key=>record[key]??''))]
};

// ---- True Web Push subscription UI --------------------------------
function v177UrlBase64ToUint8(value){let padding='='.repeat((4-value.length%4)%4),base64=(value+padding).replace(/-/g,'+').replace(/_/g,'/'),raw=atob(base64);return Uint8Array.from([...raw].map(char=>char.charCodeAt(0)))}
async function v177EnablePushReminders(){
  let user=currentUser();if(!user)return;if(!('serviceWorker' in navigator)||!('PushManager' in window)||!('Notification' in window))return toast('Closed-app reminders are not supported by this browser');if(location.protocol!=='https:'&&location.hostname!=='localhost')return toast('Closed-app reminders need HTTPS');
  try{v177PushState='working';render();let config=await fetch('/api/push/config').then(response=>response.json());if(!config.enabled)throw new Error('Closed-app reminders need the server Push keys to be configured');let permission=await Notification.requestPermission();if(permission!=='granted')throw new Error('Notifications were not allowed on this device');let registration=await navigator.serviceWorker.ready,subscription=await registration.pushManager.subscribe({userVisibleOnly:true,applicationServerKey:v177UrlBase64ToUint8(config.publicKey)}),response=await fetch('/api/push/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({actorId:user.id,subscription:subscription.toJSON()})}),result=await response.json();if(!result.ok)throw new Error(result.message||'Subscription could not be saved');if(result.data){db=result.data;sharedRevision=result.revision||sharedRevision;persistCache()}let savedUser=currentUser();if(savedUser)savedUser.notificationsEnabled=true;v177PushState='enabled';save();savedState('Closed-app clock-out reminders enabled');render()
  }catch(error){v177PushState='error';toast(error.message||'Closed-app reminders could not be enabled');render()}
}
function v177PushReminderCard(){let supported='serviceWorker' in navigator&&'PushManager' in window,permission=typeof Notification!=='undefined'?Notification.permission:'unsupported',state=v177PushState==='enabled'?'Enabled':v177PushState==='working'?'Setting up…':permission==='granted'?'Device allowed':'Not enabled';return `<div class="panel v177-push-card"><span>🔔</span><div><b>Closed-app Clock Out reminder</b><p>Opt in once to receive your scheduled finish reminder even when SiteSight is closed.</p><small>${supported?state:'Not supported by this browser'}</small></div><button class="btn blue" onclick="v177EnablePushReminders()" ${!supported||v177PushState==='working'?'disabled':''}>${v177PushState==='enabled'?'Enabled':'Enable reminder'}</button></div>`}
const v177BaseClockPage=v175ClockPage;
v175ClockPage=function(){return v177BaseClockPage()+v177PushReminderCard()};

// ---- Migration ------------------------------------------------------
const v177BaseMigrate=migrate;
migrate=function(persist=true){
  v177BaseMigrate(false);let changed=false;['offlineClockRequests','cisProfiles','pushSubscriptions'].forEach(key=>{if(!Array.isArray(db[key])){db[key]=[];changed=true}});(db.users||[]).forEach(user=>{if(Number(user.permissionTemplateVersion||0)<177){user.permissionTemplateVersion=177;changed=true}});if(persist&&(changed||persist))save()
};
if((db.users||[]).length)migrate(false);
const v177BaseCompleteFirstSetup=completeFirstSetup;
completeFirstSetup=function(){v177BaseCompleteFirstSetup();if((db.users||[]).length){migrate(false);save()}};

/* ===== v178-site-flow.js ===== */
'use strict';

// SiteSight V1.78 — practical Site flow, 15-minute overtime threshold,
// strict commercial visibility, reliable clocking and clearer decisions.

const V178_OVERTIME_THRESHOLD_MINUTES=15;
const V178_ATTENDANCE_RADIUS_METRES=250;
const V178_FINANCIAL_ROLES=['Director','Contracts Manager','Quantity Surveyor'];
const V178_FINANCIAL_PERMISSIONS=['view_financials','record_payments','manage_payments','manage_variations','approve_variations','lock_payroll'];

function v178FinancialRole(user=currentUser()){return !!user&&V178_FINANCIAL_ROLES.includes(user.role)}
function v178OptionalReasonText(reason){return String(reason||'').trim()||'No reason supplied'}
function v178OvertimeIsDecided(status){return status==='Approved'||status==='Rejected'}

const v178BaseShell=shell;
shell=function(content){v178BaseShell(content);let root=document.getElementById('root');if(root)root.innerHTML=root.innerHTML.replace(/v1\.76\.0 · WORKFORCE FLOW(?: DEMO)?/g,`v1.78.0 · SITE FLOW${String(root.innerHTML).includes('demo-mode-banner')?' DEMO':''}`)};

// ---- Strict commercial visibility ---------------------------------
// Commercial flags are removed from every other role even if an older
// template or stale custom grant still contains them.
['Site Manager','Supervisor','Accounts','Read Only','Operator'].forEach(roleName=>{
  let profile=V173_ROLE_PROFILES[roleName];
  if(profile&&Array.isArray(profile.permissions))profile.permissions=profile.permissions.filter(key=>!V178_FINANCIAL_PERMISSIONS.includes(key))
});
if(V173_ROLE_PROFILES['Quantity Surveyor'])V173_ROLE_PROFILES['Quantity Surveyor'].home='work-items';
if(V173_ROLE_PROFILES.Accounts){V173_ROLE_PROFILES.Accounts.home='payments-history';V173_ROLE_PROFILES.Accounts.purpose='Compact payment reconciliation, pay records and financial reporting.'}

function v178SanitiseFinancialPermissions(){
  let changed=false;
  (db.users||[]).forEach(user=>{
    if(v178FinancialRole(user))return;
    user.permissions=user.permissions||{};
    V178_FINANCIAL_PERMISSIONS.forEach(key=>{if(user.permissions[key]){user.permissions[key]=false;changed=true}})
  });
  return changed
}

const v178BaseCanOpenView=v173CanOpenView;
v173CanOpenView=function(target,user=currentUser()){
  if(target==='payments-history'&&!v178FinancialRole(user))return false;
  return v178BaseCanOpenView(target,user)
};

// ---- Practical primary navigation ---------------------------------
V173_MAIN_NAV.gangs.label='Individuals/Teams';
V173_MORE_TOOLS.gangs=['♟','Individuals/Teams','Find people, manage Teams and open workforce analytics'];
V173_MAIN_NAV.dayworks={icon:'＋',label:'Dayworks'};

const v178BaseRoleNavItems=v173RoleNavItems;
v173RoleNavItems=function(user=currentUser()){
  let items=v178BaseRoleNavItems(user).filter(item=>!['analytics','payments-history','dayworks'].includes(item.id));
  if(v173CanOpenView('dayworks',user)){
    let dayworks={id:'dayworks',...V173_MAIN_NAV.dayworks},workIndex=items.findIndex(item=>item.id==='work-items');
    items.splice(workIndex>=0?workIndex+1:items.length,0,dayworks)
  }
  return items
};

// ---- 15-minute overtime threshold and explicit rejection -----------
const v178BaseShiftPayBreakdown=v174ShiftPayBreakdown;
v174ShiftPayBreakdown=function(shift,member){
  let row=v178BaseShiftPayBreakdown(shift,member),rawLateMinutes=Number(row.lateMinutes||0);
  row.rawLateMinutes=rawLateMinutes;
  row.overtimeThresholdMinutes=V178_OVERTIME_THRESHOLD_MINUTES;
  row.graceExcludedMinutes=0;
  if(rawLateMinutes<=V178_OVERTIME_THRESHOLD_MINUTES){
    row.graceExcludedMinutes=rawLateMinutes;
    row.lateMinutes=0;
    row.approvedMinutes=0;
    row.unauthorisedMinutes=0
  }
  return row
};

v174OvertimeStatusHtml=function(shift,member){
  let b=v174ShiftPayBreakdown(shift,member);
  if(b.graceExcludedMinutes)return `<span class="v174-ot none">Within 15-minute grace</span><small>${v169MinutesLabel(b.graceExcludedMinutes)} after finish not payable</small>`;
  if(!b.lateMinutes)return '<span class="v174-ot none">Within set hours</span>';
  if(b.approval.status==='Approved')return `<span class="v174-ot approved">✓ ${v169MinutesLabel(b.approvedMinutes)} authorised OT</span>${b.unauthorisedMinutes?`<small>${v169MinutesLabel(b.unauthorisedMinutes)} not payable</small>`:''}`;
  if(b.approval.status==='Rejected')return `<span class="v174-ot rejected">✕ Overtime rejected</span><small>${v169MinutesLabel(b.lateMinutes)} not payable</small>`;
  return `<span class="v174-ot pending">⚠ ${v169MinutesLabel(b.lateMinutes)} after finish — not payable</span>`
};

v174OpenOvertimeApproval=function(shiftId){
  if(!v175CanApproveOvertime())return toast('Only an authorised Supervisor or Manager can decide overtime');
  let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId),wm=attendanceMemberById(shift?.workerId);if(!shift||!wm)return;
  let b=v174ShiftPayBreakdown(shift,wm.member);if(!b.lateMinutes)return toast(`Overtime is registered only after ${V178_OVERTIME_THRESHOLD_MINUTES} minutes past the set finish`);
  let current=b.approval.status==='Approved'?b.approval.minutes:b.lateMinutes,status=v178OvertimeIsDecided(b.approval.status)?b.approval.status:'Awaiting decision';
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">OVERTIME DECISION</span><h2>${contractorEsc(wm.member.name)}</h2><p class="muted">${attendanceDateOnly(shift.clockIn.time,shift.siteId)} · set hours ${v174ShiftScheduleText(wm.member,shift)} · clocked out ${v169TimeLabel(shift.clockOut.time,shift.siteId)}</p><div class="v174-overtime-warning"><b>${v169MinutesLabel(b.lateMinutes)} registered after the 15-minute threshold</b><span>Status: ${contractorEsc(status)}. This time is not payable unless it is authorised.</span></div><div class="field"><label>Authorised overtime minutes</label><input id="v174-ot-minutes" class="input" type="number" min="1" max="${b.lateMinutes}" step="1" value="${Math.min(current,b.lateMinutes)}"></div><div class="field"><label>Reason / instruction <span class="muted">(optional)</span></label><textarea id="v174-ot-reason" class="textarea" placeholder="Optional: who instructed the overtime and why?">${contractorEsc(b.approval.reason||'')}</textarea></div><div class="actions"><button class="btn green" onclick="v174SaveOvertimeApproval('${shift.id}')">✓ Authorise overtime</button><button class="btn red" onclick="v178RejectOvertime('${shift.id}')">✕ Reject overtime</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()
};

v174SaveOvertimeApproval=function(shiftId){
  if(!v175CanApproveOvertime())return toast('Only an authorised Supervisor or Manager can approve overtime');
  let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId),wm=attendanceMemberById(shift?.workerId),minutes=Math.max(0,Number(document.getElementById('v174-ot-minutes')?.value||0)),reason=(document.getElementById('v174-ot-reason')?.value||'').trim();if(!shift||!wm)return;
  let max=v174ShiftPayBreakdown(shift,wm.member).lateMinutes;if(!minutes||minutes>max)return toast(`Enter between 1 and ${max} minutes`);
  shift.overtimeApproval={status:'Approved',minutes,reason,approvedBy:currentUser()?.name||'',approvedById:currentUser()?.id||'',approvedAt:new Date().toISOString()};attendanceMarkApprovalNeedsRecheck(shift.workerId,shift.siteId,shift.clockIn.time);save();audit('attendance','overtime approval',shift.id,`${wm.member.name}: ${minutes} minutes overtime authorised — ${v178OptionalReasonText(reason)}`,shift.siteId);modal=null;savedState('Overtime authorised');render()
};

function v178RejectOvertime(shiftId){
  if(!v175CanApproveOvertime())return toast('Only an authorised Supervisor or Manager can decide overtime');
  let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId),wm=attendanceMemberById(shift?.workerId),reason=(document.getElementById('v174-ot-reason')?.value||'').trim();if(!shift||!wm)return;
  shift.overtimeApproval={status:'Rejected',minutes:0,reason,approvedBy:currentUser()?.name||'',approvedById:currentUser()?.id||'',approvedAt:new Date().toISOString()};attendanceMarkApprovalNeedsRecheck(shift.workerId,shift.siteId,shift.clockIn.time);save();audit('attendance','overtime approval',shift.id,`${wm.member.name}: overtime rejected — ${v178OptionalReasonText(reason)}`,shift.siteId);modal=null;savedState('Overtime rejected');render()
}
v174DeclineOvertime=v178RejectOvertime;

v174PendingOvertimePanel=function(siteId){
  if(!v175CanApproveOvertime())return '';
  let rows=(db.attendanceShifts||[]).filter(shift=>shift.siteId===siteId&&!shift.voided&&shift.clockOut?.time).map(shift=>{let wm=attendanceMemberById(shift.workerId),b=wm?v174ShiftPayBreakdown(shift,wm.member):null;return wm&&b?.lateMinutes&&!v178OvertimeIsDecided(b.approval.status)?{shift,wm,b}:null}).filter(Boolean).sort((a,b)=>String(b.shift.clockOut.time).localeCompare(String(a.shift.clockOut.time))).slice(0,20);
  return `<div class="panel v174-pending-overtime"><div class="sectionhead"><div><h2>Overtime awaiting decision</h2><div class="muted">Only clock-outs more than 15 minutes late appear here. Time stays unpaid until authorised.</div></div><span class="badge ${rows.length?'b-amber':'b-green'}">${rows.length}</span></div>${rows.map(row=>`<div class="v174-overtime-row"><span><b>${contractorEsc(row.wm.member.name)}</b><small>${attendanceDateOnly(row.shift.clockIn.time,row.shift.siteId)} · finish ${row.b.schedule.end} · out ${v169TimeLabel(row.shift.clockOut.time,row.shift.siteId)}</small></span><strong>${v169MinutesLabel(row.b.lateMinutes)}</strong><button class="btn orange smallbtn" onclick="v174OpenOvertimeApproval('${row.shift.id}')">Review</button></div>`).join('')||'<div class="muted">No overtime is awaiting a decision.</div>'}</div>`
};

// Rebuild the two exception types whose action/count semantics changed.
const v178BaseExceptionRows=v175ExceptionRows;
v175ExceptionRows=function(){
  let rows=v178BaseExceptionRows().filter(row=>!['overtime','qualification'].includes(row.kind)),siteId=site==='all'?'all':site,today=iso(),siteMatches=id=>siteId==='all'||id===siteId;
  if(hasPerm('approve_overtime')){
    let count=(db.attendanceShifts||[]).filter(shift=>siteMatches(shift.siteId)&&!shift.voided&&shift.clockOut?.time).filter(shift=>{let wm=attendanceMemberById(shift.workerId),b=wm?v174ShiftPayBreakdown(shift,wm.member):null;return b?.lateMinutes&&!v178OvertimeIsDecided(b.approval.status)}).length;
    if(count)rows.unshift({kind:'overtime',count,title:'Overtime decisions',detail:`${count} clock-out${count===1?'':'s'} more than 15 minutes late`,action:"contractorAttendanceTab='today';goNav('attendance')"})
  }
  if(hasPerm('manage_gangs')||hasPerm('gangs')){
    let soon=v174DateAdd(today,30);
    attendanceAllMembers().forEach(({member})=>{let qualifications=(member.qualifications||[]).filter(row=>row.expiry&&row.expiry>=today&&row.expiry<=soon).sort((a,b)=>String(a.expiry).localeCompare(String(b.expiry)));if(!qualifications.length)return;rows.push({kind:'qualification',count:qualifications.length,title:`${member.name}: qualification expiring`,detail:`${qualifications.map(row=>row.name||row.type||'Qualification').join(', ')} · ${qualifications[0].expiry}`,action:`v178OpenQualificationPerson('${member.id}')`})})
  }
  return rows
};

function v178OpenQualificationPerson(memberId){
  let wm=attendanceMemberById(memberId);if(!wm)return toast('This person record could not be found');
  view='gangs';selected=null;selectedGang=null;openTeamMember(wm.gang.id,memberId)
}

// ---- Worker-facing clock clarity ----------------------------------
function v178WorkerAccount(user=currentUser()){return !!user&&user.role==='Operator'}
const v178BaseClockPage=v175ClockPage;
v175ClockPage=function(){
  let html=v178BaseClockPage();if(!v178WorkerAccount())return html;
  return html.replace(/<div><span>AWAITING OT DECISION<\/span><b>[\s\S]*?<\/b><\/div>/,'')
};
const v178BaseMyTimeOnlyView=attendanceMyTimeOnlyView;
attendanceMyTimeOnlyView=function(siteId,linked){
  let html=v178BaseMyTimeOnlyView(siteId,linked);if(!v178WorkerAccount())return html;
  return html.replace(/<div class="v174-worker-notice">[\s\S]*?<\/div>/,'')
};

// ---- Clear drawing measurement completion -------------------------
const v178BaseTakeoffWorkspaceHtml=takeoffWorkspaceHtml;
takeoffWorkspaceHtml=function(){
  let html=v178BaseTakeoffWorkspaceHtml();if(!takeoffDraft.length||!hasPerm('manage_drawings'))return html;
  let finishReady=takeoffTool==='Length'?takeoffDraft.length>=2:(takeoffTool==='Area'||takeoffTool==='Deduct Area'?takeoffDraft.length>=3:(takeoffTool==='Count'?takeoffDraft.length>=1:false)),finishCall=takeoffTool==='Calibration'?'finishTakeoffCalibration()':'finishTakeoffGeometry()',disabled=takeoffTool!=='Calibration'&&!finishReady?'disabled':'',label=takeoffTool==='Calibration'?'Finish calibration':`Finish ${takeoffTool.toLowerCase()}`;
  let button=`<button class="v178-finish-measurement" onclick="${finishCall}" ${disabled}><span>✓</span><b>${label}</b><small>Confirm ${takeoffDraft.length} selected point${takeoffDraft.length===1?'':'s'}</small></button>`;
  return html.replace('<aside class="takeoff-sidebar">','<aside class="takeoff-sidebar">'+button)
};

// ---- Fixed 250 m worker geofence ----------------------------------
const v178BaseConfigureAttendanceLocation=configureAttendanceLocation;
configureAttendanceLocation=function(siteId){
  let result=v178BaseConfigureAttendanceLocation(siteId);
  if(modal){modal=modal.replace('<label>Allowed radius</label>','<label>Allowed radius (fixed)</label>').replace(/<select id="att-radius"[\s\S]*?<\/select>/,`<select id="att-radius" class="select" disabled><option value="${V178_ATTENDANCE_RADIUS_METRES}" selected>${V178_ATTENDANCE_RADIUS_METRES} metres</option></select>`);render()}
  return result
};
attendanceSaveSiteRules=function(siteId){let c=attendanceSiteConfig(siteId);c.radiusM=V178_ATTENDANCE_RADIUS_METRES;c.maxAccuracyM=Number(document.getElementById('att-accuracy')?.value||75);c.maxShiftHours=Number(document.getElementById('att-maxshift')?.value||12);save();audit('attendance','site',siteId,`Attendance rules updated — ${siteName(siteId)} · ${c.radiusM}m radius`,siteId);savedState('Attendance rules saved');configureAttendanceLocation(siteId)};
attendanceSetSiteLocationNow=async function(siteId){let status=document.getElementById('att-config-status');if(status)status.textContent='Requesting a precise GPS position…';try{let pos=await attendanceGetPosition(),c=attendanceSiteConfig(siteId);c.latitude=pos.latitude;c.longitude=pos.longitude;c.radiusM=V178_ATTENDANCE_RADIUS_METRES;c.maxAccuracyM=Number(document.getElementById('att-accuracy')?.value||75);c.maxShiftHours=Number(document.getElementById('att-maxshift')?.value||12);c.locationSetAt=new Date().toISOString();c.locationSetBy=currentUser()?.name||'';c.demoPreset=false;save();audit('attendance','site',siteId,`Attendance geofence set — ${siteName(siteId)} · ${c.radiusM}m radius · max GPS accuracy ${c.maxAccuracyM}m`,siteId);savedState('Site location saved');configureAttendanceLocation(siteId)}catch(error){if(status)status.textContent=error.message;toast(error.message)}};

// V1.76 introduced scrollable time selects but referenced a missing helper.
// Define the day-specific lookup so working-hour editors always open.
function v174MemberScheduleForDay(member,siteId,day){
  let cfg=attendanceSiteConfig(siteId)||{},saved=member?.workingSchedule?.[day],usual=(member?.usualDays?.length?member.usualDays:[1,2,3,4,5]).map(Number);
  return {active:saved?saved.active!==false:usual.includes(Number(day)),start:saved?.start||member?.expectedStartTime||cfg.expectedStartTime||'07:30',end:saved?.end||member?.expectedEndTime||cfg.expectedEndTime||'16:30'}
}

// ---- Optional notes on accept / reject decisions ------------------
v177DecideOfflineRequest=function(id,decision){
  if(!v175CanCorrectTime())return toast('Only an authorised manager can decide clock requests');
  let row=(db.offlineClockRequests||[]).find(item=>item.id===id),wm=attendanceMemberById(row?.workerId),reason=(document.getElementById('v177-request-review')?.value||'').trim(),local=document.getElementById('v177-request-time')?.value||'';if(!row||!wm)return;if(decision==='Approved'&&!local)return toast('Enter the accepted time');
  if(decision==='Approved'){
    let time=new Date(local).toISOString(),point={time,status:'Offline Request Approved',serverTime:false,locationRequested:false,actorId:currentUser()?.id||'',actorName:currentUser()?.name||'',reason};
    if(row.action==='clock-in'){if(attendanceActiveShift(row.workerId))return toast('This person already has an active shift');let shift={id:v176Id('ats'),workerId:row.workerId,gangId:wm.gang.id,siteId:row.siteId,clockIn:point,clockOut:null,breakMinutes:Number(wm.member.defaultBreakMinutes??30),source:'Approved Offline Request',corrections:[],createdAt:new Date().toISOString(),createdBy:currentUser()?.name||'',overtimeApproval:{status:'Not Authorised',minutes:0,reason:'',approvedBy:'',approvedById:'',approvedAt:''}};db.attendanceShifts.push(shift);row.shiftId=shift.id}
    else{let active=attendanceActiveShift(row.workerId);if(!active)return toast('No active shift is available to Clock Out');active.clockOut=point;row.shiftId=active.id;attendanceMarkApprovalNeedsRecheck(active.workerId,active.siteId,active.clockIn.time)}
  }
  row.status=decision;row.reviewedAt=new Date().toISOString();row.reviewedBy=currentUser()?.name||'';row.reviewedById=currentUser()?.id||'';row.reviewReason=reason;save();audit('attendance','offline clock request',row.id,`${wm.member.name}: ${row.action} request ${decision.toLowerCase()} — ${v178OptionalReasonText(reason)}`,row.siteId);modal=null;savedState(`Clock request ${decision.toLowerCase()}`);render()
};

// ---- Migration and user-save enforcement --------------------------
const v178BaseMigrate=migrate;
migrate=function(persist=true){
  v178BaseMigrate(false);let changed=v178SanitiseFinancialPermissions();
  (db.sites||[]).forEach(row=>{row.attendanceConfig=row.attendanceConfig||{};if(Number(row.attendanceConfig.radiusM)!==V178_ATTENDANCE_RADIUS_METRES){row.attendanceConfig.radiusM=V178_ATTENDANCE_RADIUS_METRES;changed=true}});
  (db.users||[]).forEach(user=>{if(Number(user.permissionTemplateVersion||0)<178){user.permissionTemplateVersion=178;changed=true}});
  if(persist&&(changed||persist))save()
};
if((db.users||[]).length)migrate(false);

const v178BaseSaveNewUser=saveNewUser;
saveNewUser=function(...args){let result=v178BaseSaveNewUser(...args);if(v178SanitiseFinancialPermissions()){save();render()}return result};
const v178BaseSaveUserAccess=saveUserAccess;
saveUserAccess=function(...args){let result=v178BaseSaveUserAccess(...args);if(v178SanitiseFinancialPermissions()){save();render()}return result};
const v178BaseCompleteFirstSetup=completeFirstSetup;
completeFirstSetup=function(){v178BaseCompleteFirstSetup();if((db.users||[]).length){migrate(false);save()}};

/* ===== v179-cis-pay.js ===== */
'use strict';

// SiteSight V1.79 — current CIS pay-run controls and plain task permissions.

const V179_CIS_READY_STATUSES=['Matched','Checked'];
const V179_PERMISSION_COPY={
  dashboard:['Can this person see the live Site dashboard?','Daily progress, alerts and the Site overview.'],
  work_items:['Can this person see work items?','Plots, stages, assignments and work details.'],
  update_progress:['Can this person update progress?','Start, progress and complete assigned work.'],
  view_issues:['Can this person see Site issues?','View open, resolved and blocking issues.'],
  raise_issues:['Can this person report a new issue?','Add an issue and supporting evidence.'],
  manage_issues:['Can this person manage and resolve issues?','Change status, assign actions and close issues.'],
  sign_off:['Can this person sign off completed work?','Give final operational sign-off.'],
  edit_lifts:['Can this person change lift stages?','Edit the stage structure used on plots.'],
  manage_workitems:['Can this person create and manage work items?','Add plots or work and change their setup.'],
  view_financials:['Can this person see prices, pay and CIS figures?','Restricted to Director, Contracts Manager and Quantity Surveyor roles.'],
  record_payments:['Can this person record a payment?','Add actual payments against work and variations.'],
  manage_payments:['Can this person correct payment and CIS records?','Edit allocations, reconcile CIS and remove incorrect payments.'],
  manage_variations:['Can this person create and manage variations?','Record instructed work outside the original value.'],
  approve_variations:['Can this person approve variations?','Authorise variation values before payment.'],
  lock_payroll:['Can this person lock and export a reviewed pay period?','Freeze a reviewed snapshot and create accounting files.'],
  view_drawings:['Can this person open drawings?','View the Drawing Library and plot drawings.'],
  manage_drawings:['Can this person add, measure and manage drawings?','Upload files, use take-off tools and maintain the library.'],
  view_calendar:['Can this person see the Calendar?','View programme dates, reminders and people events.'],
  manage_calendar:['Can this person add and change Calendar events?','Create, edit and remove programme notes.'],
  view_site_diary:['Can this person read the Site Diary?','View daily Site records.'],
  manage_site_diary:['Can this person write and correct the Site Diary?','Add daily entries and audited amendments.'],
  download_reports:['Can this person download reports?','Create dated operational and commercial reports.'],
  view_attendance:['Can this person see other people\'s hours?','View attendance, pay-review hours and exceptions.'],
  clock_attendance:['Can this person Clock In and Clock Out?','Use the immediate clock page for their linked person record.'],
  manage_attendance:['Can this person correct attendance records?','Correct missed or wrong clock records with an audit trail.'],
  approve_timesheets:['Can this person approve weekly timesheets?','Confirm reviewed hours for the pay process.'],
  manage_working_hours:['Can this person set people\'s working hours?','Change start, finish, breaks and working patterns.'],
  approve_overtime:['Can this person decide overtime?','Authorise or reject time beyond the set finish.'],
  grant_time_controls:['Can this person grant time-control access?','Restricted to authorised Directors and Contracts Managers.'],
  gangs:['Can this person open Individuals/Teams dashboards?','View people, Teams and their operational detail.'],
  analytics:['Can this person see performance analytics?','View operational metrics; financial figures still need financial access.'],
  audit:['Can this person see the Audit Trail?','Review who changed important records and when.'],
  manage_gangs:['Can this person manage people and Teams?','Add, edit, move, remove and link people.'],
  manage_sites:['Can this person set up Sites?','Create Sites and change Site configuration.'],
  view_archives:['Can this person open archived Sites?','Read completed-development records.'],
  archive_sites:['Can this person archive or restore a Site?','Move completed Sites out of daily views and restore them.'],
  manage_users:['Can this person add users and change access?','Create logins, choose roles and grant individual tasks.'],
  reset_passwords:['Can this person reset another user\'s password?','Issue a temporary password for another account.'],
  permanent_delete:['Can this person permanently delete records?','High-risk access for records that cannot be recovered in the app.']
};
const V179_GROUP_COPY={
  Work:['Daily work','What can this person see and change while work is being delivered?'],
  Commercial:['Money, pay and CIS','Financial access is restricted to Director, Contracts Manager and Quantity Surveyor roles.'],
  'Site Information':['Site records','Drawings, Calendar, Diary and dated reports.'],
  'Time & Attendance':['Hours and attendance','Clocking, corrections, approvals, working hours and overtime.'],
  Management:['People, Sites and performance','Team dashboards, analytics and Site administration.'],
  'Users & Security':['Accounts and security','User access, password resets and high-risk deletion.']
};

let v179CisView='all';

function v179Esc(value){return v177Esc(String(value??''))}
function v179Js(value){return v177Js(String(value??''))}
function v179Money(value){return Number(value||0).toLocaleString('en-GB',{style:'currency',currency:'GBP',minimumFractionDigits:2,maximumFractionDigits:2})}
function v179IsoDate(value){let date=new Date(value);return Number.isNaN(date.getTime())?'':date.toISOString().slice(0,10)}
function v179AddMonths(date,months){let next=new Date(Date.UTC(date.getUTCFullYear(),date.getUTCMonth()+months,date.getUTCDate()));return next}
function v179TaxMonthRange(anchor=iso()){
  let date=new Date(`${anchor||iso()}T12:00:00Z`);if(Number.isNaN(date.getTime()))date=new Date();
  let start=date.getUTCDate()>=6?new Date(Date.UTC(date.getUTCFullYear(),date.getUTCMonth(),6)):new Date(Date.UTC(date.getUTCFullYear(),date.getUTCMonth()-1,6));
  let end=new Date(Date.UTC(start.getUTCFullYear(),start.getUTCMonth()+1,5));
  return {from:v179IsoDate(start),to:v179IsoDate(end)}
}
function v179SetCurrentTaxMonth(){let range=v179TaxMonthRange();paymentFrom=range.from;paymentTo=range.to;paymentPayeeFilter='all';paymentSearch='';v179CisView='all';render()}
function v179IsTaxMonth(from=paymentFrom,to=paymentTo){if(!from||!to)return false;let start=new Date(`${from}T12:00:00Z`);if(Number.isNaN(start.getTime())||start.getUTCDate()!==6)return false;let end=new Date(Date.UTC(start.getUTCFullYear(),start.getUTCMonth()+1,5));return to===v179IsoDate(end)}
function v179PeriodLabel(){if(!paymentFrom&&!paymentTo)return 'All payment dates';if(v179IsTaxMonth())return `CIS tax month · ${paymentFrom} to ${paymentTo}`;return `Custom range · ${paymentFrom||'Start'} to ${paymentTo||'Now'}`}

function v179CanManageCis(user=currentUser()){return v178FinancialRole(user)&&(user.role==='Director'||!!user.permissions?.manage_payments||!!user.permissions?.lock_payroll)}
function v179CisProfileIssues(profile){
  if(!profile)return ['Set up the CIS payee'];let issues=[],utr=String(profile.utr||'').replace(/\D/g,'');
  if(utr.length!==10)issues.push('Check the 10-digit UTR');
  if(!String(profile.businessType||''))issues.push('Choose the business type');
  if(!String(profile.verificationNumber||'').trim())issues.push('Add the HMRC verification number');
  if(!profile.verifiedOn)issues.push('Add the last verification date');
  if(![0,20,30].includes(Number(profile.deductionRate)))issues.push('Choose the HMRC deduction rate');
  if(profile.actualPayeeConfirmed!==true)issues.push('Confirm this is the actual subcontractor/payee');
  if(profile.subcontractorStatusConfirmed!==true)issues.push('Confirm subcontractor status was checked');
  return issues
}
function v179PaymentIssues(payment){
  let issues=[];
  if(payment.cisMaterialsReviewed!==true)issues.push('Review direct materials, including a confirmed £0');
  if(!V179_CIS_READY_STATUSES.includes(payment.reconciliationStatus))issues.push(payment.reconciliationStatus==='Query'?'Resolve the payment query':'Check the payment');
  return issues
}
function v179PaymentCalculation(payment,profile=v177CisProfile(paymentPayeeKey(payment))){
  let invoiceTotal=Math.max(0,Number(payment.amount||0)),gross=Math.max(0,Math.min(invoiceTotal,Number(payment.cisGrossExVat??invoiceTotal))),vat=Math.max(0,invoiceTotal-gross),rate=profile?Number(payment.cisDeductionRate??v177CisRate(profile)):null,base=v177CisCalculation(gross,payment.cisMaterials,rate??0);
  return {...base,invoiceTotal,vat,rate,net:Math.max(0,invoiceTotal-base.deduction)}
}
function v179CisSummaryRows(rows=filteredPaymentRows()){
  let groups=new Map();
  rows.forEach(payment=>{
    let key=paymentPayeeKey(payment),profile=v177CisProfile(key),calc=v179PaymentCalculation(payment,profile),group=groups.get(key);
    if(!group){group={key,name:paymentPayeeName(payment),kind:paymentPayeeKind(payment),profile,payments:[],invoiceTotal:0,gross:0,vat:0,materials:0,liable:0,deduction:0,net:0,issues:[]};groups.set(key,group)}
    group.payments.push(payment);group.invoiceTotal+=calc.invoiceTotal;group.gross+=calc.gross;group.vat+=calc.vat;group.materials+=calc.materials;group.liable+=calc.liable;group.deduction+=calc.deduction;group.net+=calc.net
  });
  return [...groups.values()].map(group=>{
    let issues=[...v179CisProfileIssues(group.profile)];group.payments.forEach(payment=>v179PaymentIssues(payment).forEach(issue=>{if(!issues.includes(issue))issues.push(issue)}));
    group.issues=issues;group.ready=!issues.length;group.rate=group.profile?v177CisRate(group.profile):null;return group
  }).sort((a,b)=>Number(a.ready)-Number(b.ready)||a.name.localeCompare(b.name))
}
function v179CisTotals(groups){return groups.reduce((total,row)=>{['invoiceTotal','gross','vat','materials','liable','deduction','net'].forEach(key=>total[key]+=row[key]);return total},{invoiceTotal:0,gross:0,vat:0,materials:0,liable:0,deduction:0,net:0})}
function v179CisFilteredGroups(groups){if(v179CisView==='ready')return groups.filter(row=>row.ready);if(v179CisView==='action')return groups.filter(row=>!row.ready);return groups}
function v179CisContractor(){db.cisContractor=db.cisContractor||{contractorName:'',contractorUTR:'',employerTaxReference:'',accountsOfficeReference:''};return db.cisContractor}
function v179ContractorIssues(){let row=v179CisContractor(),issues=[];if(!String(row.contractorName||'').trim())issues.push('Add contractor name');if(!String(row.employerTaxReference||'').trim())issues.push('Add employer tax reference');return issues}

function v179OpenCisContractor(){
  if(!v179CanManageCis())return toast('Payment and CIS management permission is required');let row=v179CisContractor();
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">CIS CONTRACTOR</span><h2>Contractor details for exports</h2><p class="muted">These details appear on statement drafts and the CIS supporting schedules. Check them against the business tax account.</p><div class="field"><label>Contractor business name</label><input id="v179-contractor-name" class="input" value="${v179Esc(row.contractorName)}"></div><div class="two"><div class="field"><label>Employer tax reference</label><input id="v179-employer-reference" class="input" value="${v179Esc(row.employerTaxReference)}" placeholder="e.g. 123/AB456"></div><div class="field"><label>Contractor UTR</label><input id="v179-contractor-utr" class="input" inputmode="numeric" value="${v179Esc(row.contractorUTR)}"></div></div><div class="field"><label>Accounts Office reference</label><input id="v179-accounts-office" class="input" value="${v179Esc(row.accountsOfficeReference)}"></div><div class="v179-guidance"><b>Why these are kept together</b><span>HMRC verification uses the contractor UTR, Accounts Office reference and employer reference. Payment statement drafts show the contractor name and employer tax reference.</span></div><button class="btn orange" onclick="v179SaveCisContractor()">Save contractor details</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function v179SaveCisContractor(){
  if(!v179CanManageCis())return;let row=v179CisContractor();row.contractorName=(document.getElementById('v179-contractor-name')?.value||'').trim();row.employerTaxReference=(document.getElementById('v179-employer-reference')?.value||'').trim();row.contractorUTR=(document.getElementById('v179-contractor-utr')?.value||'').replace(/\s/g,'');row.accountsOfficeReference=(document.getElementById('v179-accounts-office')?.value||'').trim();row.updatedAt=new Date().toISOString();row.updatedBy=currentUser()?.name||'';save();audit('payments','CIS contractor settings','cis-contractor',`CIS contractor export details updated by ${row.updatedBy}`,'');modal=null;savedState('CIS contractor details saved');render()
}

v177OpenCisProfile=function(key,name){
  if(!v179CanManageCis())return toast('Payment and CIS management permission is required');let profile=v177CisProfile(key)||{payeeKey:key,name:name||paymentPayeeNameFromId(String(key).replace(/^id:/,'')),utr:'',businessType:'',verificationNumber:'',verifiedOn:'',deductionRate:20,actualPayeeConfirmed:false,subcontractorStatusConfirmed:false,notes:''},issues=v179CisProfileIssues(profile);
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">CIS PAYEE</span><h2>${v179Esc(profile.name||name||'Subcontractor')}</h2><div class="v177-cis-warning"><b>${issues.length?`${issues.length} item${issues.length===1?'':'s'} still to check`:'Ready for payment review'}</b><span>Use the deduction rate HMRC returned when this subcontractor was verified. SiteSight does not contact HMRC.</span></div><div class="two"><div class="field"><label>Business type</label><select id="v179-cis-type" class="select"><option value="">Select type…</option>${['Sole trader','Limited company','Partnership / trust'].map(type=>`<option ${profile.businessType===type?'selected':''}>${type}</option>`).join('')}</select></div><div class="field"><label>10-digit UTR</label><input id="v177-cis-utr" class="input" inputmode="numeric" maxlength="13" value="${v179Esc(profile.utr)}"></div></div><div class="two"><div class="field"><label>HMRC verification number</label><input id="v177-cis-verification" class="input" value="${v179Esc(profile.verificationNumber)}"></div><div class="field"><label>Last verified with HMRC</label><input id="v179-cis-verified" class="input" type="date" value="${v179Esc(profile.verifiedOn)}"></div></div><div class="field"><label>Rate HMRC told you to use</label><select id="v177-cis-rate" class="select"><option value="0" ${v177CisRate(profile)===0?'selected':''}>0% · Gross payment status</option><option value="20" ${v177CisRate(profile)===20?'selected':''}>20% · Registered subcontractor</option><option value="30" ${v177CisRate(profile)===30?'selected':''}>30% · Unregistered / cannot verify</option></select></div><label class="v179-confirm"><input id="v179-cis-payee" type="checkbox" ${profile.actualPayeeConfirmed===true?'checked':''}><span><b>This business is the actual subcontractor and payee shown on the invoice.</b><small>A Team allocation is not a separate CIS payee unless that person or business was actually paid under its own subcontract.</small></span></label><label class="v179-confirm"><input id="v179-cis-status" type="checkbox" ${profile.subcontractorStatusConfirmed===true?'checked':''}><span><b>I have checked this payee is being treated as a subcontractor, not an employee.</b><small>The CIS monthly return includes this declaration; use HMRC employment-status guidance if unsure.</small></span></label><div class="field"><label>Review notes <span class="muted">(optional)</span></label><textarea id="v177-cis-notes" class="textarea">${v179Esc(profile.notes)}</textarea></div><button class="btn orange" onclick="v177SaveCisProfile('${v179Js(key)}','${v179Js(profile.name||name||'Subcontractor')}')">Save CIS payee</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
};
v177SaveCisProfile=function(key,name){
  if(!v179CanManageCis())return;db.cisProfiles=db.cisProfiles||[];let row=v177CisProfile(key);if(!row){row={id:v176Id('cis-profile'),payeeKey:key,createdAt:new Date().toISOString(),createdBy:currentUser()?.name||''};db.cisProfiles.push(row)}
  row.name=name;row.businessType=document.getElementById('v179-cis-type')?.value||'';row.utr=(document.getElementById('v177-cis-utr')?.value||'').replace(/\s/g,'');row.verificationNumber=(document.getElementById('v177-cis-verification')?.value||'').trim();row.verifiedOn=document.getElementById('v179-cis-verified')?.value||'';row.deductionRate=Number(document.getElementById('v177-cis-rate')?.value||0);row.actualPayeeConfirmed=!!document.getElementById('v179-cis-payee')?.checked;row.subcontractorStatusConfirmed=!!document.getElementById('v179-cis-status')?.checked;row.notes=(document.getElementById('v177-cis-notes')?.value||'').trim();row.updatedAt=new Date().toISOString();row.updatedBy=currentUser()?.name||'';save();audit('payments','CIS profile',row.id,`CIS payee saved — ${name} · ${row.deductionRate}% · ${v179CisProfileIssues(row).length?'needs review':'ready'}`,'');modal=null;savedState(v179CisProfileIssues(row).length?'CIS payee saved — checks still needed':'CIS payee ready');render()
};

function v179UpdateCisPreview(){let total=Number(document.getElementById('v179-recon-total')?.dataset.total||0),gross=Math.max(0,Math.min(total,Number(document.getElementById('v179-recon-gross')?.value||0))),materials=Number(document.getElementById('v177-recon-materials')?.value||0),rate=Number(document.getElementById('v177-recon-rate')?.value||0),base=v177CisCalculation(gross,materials,rate),calc={...base,vat:Math.max(0,total-gross),net:Math.max(0,total-base.deduction)};[['vat','vat'],['materials','materials'],['liable','liable'],['deduction','deduction'],['net','net']].forEach(([id,key])=>{let el=document.getElementById(`v179-preview-${id}`);if(el)el.textContent=v179Money(calc[key])})}
v177OpenPaymentReconciliation=function(paymentId){
  if(!v179CanManageCis())return toast('Payment and CIS management permission is required');let found=v176ActualPayment(paymentId);if(!found)return;let p=found.payment,key=paymentPayeeKey({...p,item:found.item}),name=paymentPayeeName({...p,item:found.item}),profile=v177CisProfile(key),rate=Number(p.cisDeductionRate??(profile?v177CisRate(profile):20)),calc=v179PaymentCalculation({...p,item:found.item},profile);
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">CIS PAYMENT CHECK</span><h2>${v179Esc(name)}</h2><p class="muted">${v179Esc(found.item.ref)} · ${p.date||'No date'} · recorded invoice/payment total ${v179Money(p.amount)}</p><div id="v179-recon-total" data-total="${Number(p.amount||0)}"></div><div class="v179-guidance"><b>Separate VAT before calculating CIS</b><span>Enter the gross payment excluding VAT. If the recorded amount already excludes VAT or no VAT is charged, leave it equal to the recorded total.</span></div><div class="two"><div class="field"><label>Gross payment excluding VAT</label><input id="v179-recon-gross" class="input" type="number" min="0" max="${Number(p.amount||0)}" step="0.01" value="${calc.gross.toFixed(2)}" oninput="v179UpdateCisPreview()"></div><div class="field"><label>VAT excluded from CIS</label><input class="input" disabled value="${calc.vat.toFixed(2)}" aria-label="VAT excluded from CIS"><small>Updates in the calculation below.</small></div></div><div class="v179-guidance"><b>Only qualifying direct costs reduce the CIS deduction</b><span>Enter materials, consumable stores, qualifying fuel, job-specific plant hire or manufacturing/prefabrication costs paid directly by this subcontractor. Keep evidence. Do not enter VAT here.</span></div><div class="two"><div class="field"><label>Qualifying direct costs excluding VAT</label><input id="v177-recon-materials" class="input" type="number" min="0" max="${calc.gross}" step="0.01" value="${calc.materials.toFixed(2)}" oninput="v179UpdateCisPreview()"></div><div class="field"><label>Rate used for this payment</label><select id="v177-recon-rate" class="select" onchange="v179UpdateCisPreview()"><option value="0" ${rate===0?'selected':''}>0%</option><option value="20" ${rate===20?'selected':''}>20%</option><option value="30" ${rate===30?'selected':''}>30%</option></select></div></div><label class="v179-confirm"><input id="v179-materials-reviewed" type="checkbox" ${p.cisMaterialsReviewed===true?'checked':''}><span><b>I have checked the invoice/evidence and these VAT/direct-cost figures are correct.</b><small>Tick this even when the correct VAT or direct-cost figure is £0.00.</small></span></label><div class="field"><label>Payment check</label><select id="v177-recon-status" class="select">${['Unreconciled','Matched','Checked','Query'].map(status=>`<option ${p.reconciliationStatus===status?'selected':''}>${status}</option>`).join('')}</select></div><div class="field"><label>Invoice / review note <span class="muted">(optional)</span></label><textarea id="v177-recon-note" class="textarea">${v179Esc(p.reconciliationNote||'')}</textarea></div><div class="v177-cis-calc"><span>Invoice/payment total <b>${v179Money(calc.invoiceTotal)}</b></span><span>VAT excluded <b id="v179-preview-vat">${v179Money(calc.vat)}</b></span><span>Gross excluding VAT <b>${v179Money(calc.gross)}</b></span><span>Direct costs <b id="v179-preview-materials">${v179Money(calc.materials)}</b></span><span>Amount liable <b id="v179-preview-liable">${v179Money(calc.liable)}</b></span><span>CIS to retain <b id="v179-preview-deduction">${v179Money(calc.deduction)}</b></span><span>Net payment after CIS <b id="v179-preview-net">${v179Money(calc.net)}</b></span></div><button class="btn orange" onclick="v177SavePaymentReconciliation('${paymentId}')">Save payment check</button> <button class="btn light" onclick="v177OpenCisProfile('${v179Js(key)}','${v179Js(name)}')">${profile?'Edit':'Set up'} CIS payee</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
};
v177SavePaymentReconciliation=function(paymentId){
  if(!v179CanManageCis())return;let found=v176ActualPayment(paymentId);if(!found)return;let p=found.payment,gross=Number(document.getElementById('v179-recon-gross')?.value||0),materials=Number(document.getElementById('v177-recon-materials')?.value||0),rate=Number(document.getElementById('v177-recon-rate')?.value||0),status=document.getElementById('v177-recon-status')?.value||'Unreconciled',reviewed=!!document.getElementById('v179-materials-reviewed')?.checked,note=(document.getElementById('v177-recon-note')?.value||'').trim(),total=Number(p.amount||0),base=v177CisCalculation(gross,materials,rate),calc={...base,invoiceTotal:total,vat:Math.max(0,total-gross),net:Math.max(0,total-base.deduction)};
  if(gross<0||gross>total)return toast('Gross excluding VAT must be between £0 and the recorded total');if(materials<0||materials>gross)return toast('Direct costs must be between £0 and the gross excluding VAT');if(![0,20,30].includes(rate))return toast('Choose the HMRC rate used');if(V179_CIS_READY_STATUSES.includes(status)&&!reviewed)return toast('Confirm the VAT and direct-cost figures before marking this payment checked');
  p.cisGrossExVat=calc.gross;p.cisVat=calc.vat;p.cisMaterials=calc.materials;p.cisMaterialsReviewed=reviewed;p.cisDeductionRate=rate;p.cisLiable=calc.liable;p.cisDeduction=calc.deduction;p.cisNet=calc.net;p.reconciliationStatus=status;p.reconciliationNote=note;p.reconciledAt=new Date().toISOString();p.reconciledBy=currentUser()?.name||'';save();audit('payments','CIS reconciliation',paymentId,`${found.item.ref}: ${status.toLowerCase()} · VAT ${v179Money(calc.vat)} · direct costs ${v179Money(calc.materials)} · CIS retained ${v179Money(calc.deduction)}`,found.item.site);modal=null;savedState('CIS payment check saved');render()
};

function v179CsvDownload(filename,rows){let esc=value=>'"'+String(value??'').replace(/"/g,'""')+'"',blob=new Blob([rows.map(row=>row.map(esc).join(',')).join('\r\n')],{type:'text/csv;charset=utf-8'}),link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download=filename;link.click();setTimeout(()=>URL.revokeObjectURL(link.href),1000)}
function v179ExportCisSummary(){
  if(!hasPerm('view_financials'))return;let groups=v179CisSummaryRows(),rows=[['Period from','Period to','Payee','Payee type','Business type','UTR','Verification number','Last verified','CIS rate','Invoice/payment total','VAT excluded from CIS','Gross payments excluding VAT','Qualifying direct costs excluding VAT','Amount liable to CIS','CIS deduction to retain','Net payment after CIS','Payment count','Ready for review','Action needed']];
  groups.forEach(group=>rows.push([paymentFrom,paymentTo,group.name,group.kind,group.profile?.businessType||'',group.profile?.utr||'',group.profile?.verificationNumber||'',group.profile?.verifiedOn||'',group.rate===null?'':`${group.rate}%`,group.invoiceTotal.toFixed(2),group.vat.toFixed(2),group.gross.toFixed(2),group.materials.toFixed(2),group.liable.toFixed(2),group.deduction.toFixed(2),group.net.toFixed(2),group.payments.length,group.ready?'Yes':'No',group.issues.join('; ')]));
  v179CsvDownload(`sitesight-cis-pay-run-${paymentFrom||'start'}-to-${paymentTo||'now'}.csv`,rows);audit('payments','CIS export','cis-pay-run',`CIS pay-run summary exported · ${groups.length} payees · ${v179Money(v179CisTotals(groups).deduction)} deduction`,'')
}
v177ExportCisReconciliation=function(){
  if(!hasPerm('view_financials'))return;let rows=[['Date','Site','Work item','Payment type','Stage / variation','Payee','Payee type','Business type','UTR','Verification number','Last verified','Invoice/payment total','VAT excluded from CIS','Gross payment excluding VAT','Qualifying direct costs excluding VAT','VAT and direct costs reviewed','Amount liable to CIS','CIS rate','CIS deduction to retain','Net payment after CIS','Payment check','Ready','Action needed','Review note']];
  filteredPaymentRows().forEach(payment=>{let profile=v177CisProfile(paymentPayeeKey(payment)),calc=v179PaymentCalculation(payment,profile),issues=[...v179CisProfileIssues(profile),...v179PaymentIssues(payment)];rows.push([payment.date,siteName(payment.item.site),payment.item.ref,payment.paymentType||'Original Work',payment.variationName||payment.stage||'',paymentPayeeName(payment),paymentPayeeKind(payment),profile?.businessType||'',profile?.utr||'',profile?.verificationNumber||'',profile?.verifiedOn||'',calc.invoiceTotal.toFixed(2),calc.vat.toFixed(2),calc.gross.toFixed(2),calc.materials.toFixed(2),payment.cisMaterialsReviewed===true?'Yes':'No',calc.liable.toFixed(2),calc.rate===null?'':`${calc.rate}%`,calc.deduction.toFixed(2),calc.net.toFixed(2),payment.reconciliationStatus||'Unreconciled',issues.length?'No':'Yes',issues.join('; '),payment.reconciliationNote||''])});
  v179CsvDownload(`sitesight-cis-payment-detail-${paymentFrom||'start'}-to-${paymentTo||'now'}.csv`,rows);audit('payments','CIS export','cis-payment-detail',`Detailed CIS payment schedule exported · ${rows.length-1} payments`,'')
};
function v179Html(value){return v179Esc(value)}
function v179ExportDeductionStatements(){
  if(!hasPerm('view_financials'))return;let groups=v179CisSummaryRows(),contractor=v179CisContractor(),period=v179PeriodLabel(),generated=new Date().toLocaleString('en-GB'),pages=groups.filter(group=>group.rate>0&&group.deduction>0).map(group=>`<section><header><div><h1>Construction Industry Scheme</h1><h2>Payment and deduction statement draft</h2></div><strong>${v179Html(period)}</strong></header><div class="notice">Review this draft before giving it to the subcontractor. SiteSight has not filed an HMRC return.</div><table><tr><th>Contractor</th><td>${v179Html(contractor.contractorName||'NOT SET')}</td></tr><tr><th>Employer tax reference</th><td>${v179Html(contractor.employerTaxReference||'NOT SET')}</td></tr><tr><th>Subcontractor</th><td>${v179Html(group.name)}</td></tr><tr><th>Subcontractor UTR</th><td>${v179Html(group.profile?.utr||'NOT SET')}</td></tr><tr><th>Verification number</th><td>${v179Html(group.profile?.verificationNumber||'NOT SET')}</td></tr><tr><th>Payments in period</th><td>${group.payments.length}</td></tr><tr><th>Invoice/payment total</th><td>${v179Money(group.invoiceTotal)}</td></tr><tr><th>VAT excluded from CIS</th><td>${v179Money(group.vat)}</td></tr><tr><th>Gross payments excluding VAT</th><td>${v179Money(group.gross)}</td></tr><tr><th>Qualifying direct costs excluding VAT</th><td>${v179Money(group.materials)}</td></tr><tr><th>Amount liable to CIS</th><td>${v179Money(group.liable)}</td></tr><tr><th>Deduction rate</th><td>${group.rate}%</td></tr><tr class="total"><th>CIS deduction retained</th><td>${v179Money(group.deduction)}</td></tr><tr><th>Net payment after CIS</th><td>${v179Money(group.net)}</td></tr></table>${group.issues.length?`<div class="warning"><b>Complete before issue:</b> ${v179Html(group.issues.join('; '))}</div>`:''}<footer>Prepared ${v179Html(generated)} by SiteSight · Supporting statement draft only</footer></section>`).join('');
  if(!pages)return toast('No payments with CIS deductions match this range');let html=`<!doctype html><html><head><meta charset="utf-8"><title>CIS payment and deduction statement drafts</title><style>body{font:14px Arial,sans-serif;color:#172b42;margin:0;background:#e9eef3}section{box-sizing:border-box;width:210mm;min-height:285mm;margin:10mm auto;padding:16mm;background:#fff;page-break-after:always}header{display:flex;justify-content:space-between;gap:20px;border-bottom:4px solid #e28a20;padding-bottom:12px}h1{margin:0;color:#0c273a}h2{margin:5px 0 0}.notice,.warning{margin:18px 0;padding:12px;border-left:5px solid #e28a20;background:#fff4df}.warning{border-color:#b42318;background:#fff1f0}table{width:100%;border-collapse:collapse;margin-top:22px}th,td{border:1px solid #ccd5dd;padding:11px;text-align:left}th{width:45%;background:#f3f6f8}.total th,.total td{font-size:17px;background:#e8f5ee}footer{margin-top:28px;color:#64748b}@media print{body{background:#fff}section{margin:0;min-height:280mm}}</style></head><body>${pages}</body></html>`,blob=new Blob([html],{type:'text/html;charset=utf-8'}),link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download=`sitesight-cis-deduction-statements-${paymentFrom||'start'}-to-${paymentTo||'now'}.html`;link.click();setTimeout(()=>URL.revokeObjectURL(link.href),1000);audit('payments','CIS export','cis-deduction-statements',`CIS deduction statement drafts exported · ${groups.filter(group=>group.rate>0&&group.deduction>0).length} payees`,'')
}

function v179CisPayRunPanel(rows){
  if(!hasPerm('view_financials'))return'';let groups=v179CisSummaryRows(rows),visible=v179CisFilteredGroups(groups),totals=v179CisTotals(groups),action=groups.filter(row=>!row.ready).length,contractorIssues=v179ContractorIssues(),manage=v179CanManageCis(),taxMonth=v179IsTaxMonth();
  return `<div class="panel v179-cis-run"><div class="sectionhead"><div><span class="att-kicker">CIS PAY RUN</span><h2>What to retain and what to pay</h2><div class="muted">${v179Esc(v179PeriodLabel())} · grouped by the actual subcontractor/payee</div></div><span class="v179-run-state ${action||contractorIssues.length?'action':'ready'}">${action||contractorIssues.length?`${action+contractorIssues.length} check${action+contractorIssues.length===1?'':'s'} needed`:'Ready for final review'}</span></div>${!taxMonth?'<div class="v179-period-warning"><b>This is not a complete CIS tax month.</b><span>Monthly CIS normally runs from the 6th to the 5th. Use the tax-month shortcut or keep this custom range for an internal payment review.</span></div>':''}<div class="v179-cis-metrics"><div><span>Invoice/payment total</span><b>${v179Money(totals.invoiceTotal)}</b></div><div><span>VAT excluded</span><b>${v179Money(totals.vat)}</b></div><div><span>Gross excluding VAT</span><b>${v179Money(totals.gross)}</b></div><div><span>Direct costs</span><b>${v179Money(totals.materials)}</b></div><div><span>Amount liable</span><b>${v179Money(totals.liable)}</b></div><div class="retain"><span>CIS to retain</span><b>${v179Money(totals.deduction)}</b></div><div class="net"><span>Net payment after CIS</span><b>${v179Money(totals.net)}</b></div></div><div class="v179-cis-actions"><button class="btn light" onclick="v179SetCurrentTaxMonth()">Use current CIS tax month</button>${manage?`<button class="btn light" onclick="v179OpenCisContractor()">${contractorIssues.length?'Set':'Edit'} contractor details</button>`:''}<button class="btn green" onclick="v179ExportCisSummary()">⇩ Pay-run summary CSV</button><button class="btn light" onclick="v177ExportCisReconciliation()">⇩ Payment detail CSV</button><button class="btn light" onclick="v179ExportDeductionStatements()">⇩ Deduction statement drafts</button></div><div class="v179-cis-filter"><button class="${v179CisView==='all'?'active':''}" onclick="v179CisView='all';render()">All payees <b>${groups.length}</b></button><button class="${v179CisView==='action'?'active':''}" onclick="v179CisView='action';render()">Needs action <b>${action}</b></button><button class="${v179CisView==='ready'?'active':''}" onclick="v179CisView='ready';render()">Ready <b>${groups.length-action}</b></button></div><div class="v179-payee-list">${visible.map(group=>`<article class="v179-payee ${group.ready?'ready':'action'}"><div class="v179-payee-title"><span><b>${v179Esc(group.name)}</b><small>${v179Esc(group.kind)} · ${group.payments.length} payment${group.payments.length===1?'':'s'}${group.rate===null?' · No CIS rate':` · ${group.rate}% CIS`}</small></span><em>${group.ready?'✓ Ready':'Check'}</em></div><div class="v179-payee-money"><span>Gross ex VAT <b>${v179Money(group.gross)}</b></span><span>Direct costs <b>${v179Money(group.materials)}</b></span><span>Retain <b>${v179Money(group.deduction)}</b></span><span>Net payment <b>${v179Money(group.net)}</b></span></div>${group.issues.length?`<div class="v179-issues">${group.issues.map(issue=>`<span>${v179Esc(issue)}</span>`).join('')}</div>`:'<div class="v179-complete">Profile and matching payments are ready for final review.</div>'}<div class="v179-payee-actions">${manage?`<button class="btn light smallbtn" onclick="v177OpenCisProfile('${v179Js(group.key)}','${v179Js(group.name)}')">${group.profile?'Edit':'Set up'} CIS payee</button>`:''}<button class="btn light smallbtn" onclick="paymentPayeeFilter='${v179Js(group.key)}';v179CisView='all';render()">Show only this payee</button></div></article>`).join('')||'<div class="empty-help"><b>No payees in this view.</b><div>Change the filter or selected payment dates.</div></div>'}</div><div class="v179-cis-note"><b>Before filing:</b> SiteSight prepares supporting records and statement drafts only. Verify subcontractors with HMRC, confirm employment status, review VAT/direct-cost evidence and submit the monthly return through HMRC or recognised CIS software. Team member allocations do not create separate CIS payees unless those people were actually paid as separate subcontractors.</div></div>`
}

v177CisReconciliationPanel=function(rows){
  if(!hasPerm('view_financials'))return'';let manage=v179CanManageCis();return `${v179CisPayRunPanel(rows)}<div class="panel v177-reconciliation v179-payment-checks"><div class="sectionhead"><div><span class="att-kicker">PAYMENT DETAIL</span><h2>Check each payment</h2><div class="muted">Confirm the invoice direct costs, rate and reconciliation state. A confirmed £0 is different from a missing review.</div></div></div>${rows.slice(0,120).map(row=>{let profile=v177CisProfile(paymentPayeeKey(row)),issues=[...v179CisProfileIssues(profile),...v179PaymentIssues(row)],status=row.reconciliationStatus||'Unreconciled';return `<div class="v177-recon-row"><span><b>${v179Esc(paymentPayeeName(row))}</b><small>${v179Esc(row.item?.ref||'')} · ${row.date||'—'} · ${v179Money(row.amount)}${profile?' · CIS '+v177CisRate(profile)+'%':' · No CIS payee'}</small></span><em class="status-${status.toLowerCase()}">${issues.length?`${issues.length} check${issues.length===1?'':'s'}`:'Ready'}</em>${manage?`<button class="btn light smallbtn" onclick="v177OpenPaymentReconciliation('${row.id}')">${row.cisMaterialsReviewed===true?'Edit':'Check'}</button>`:''}</div>`}).join('')||'<div class="muted">No payments match the selected date range.</div>'}</div>`
};

const v179BasePaymentsHistoryPage=paymentsHistoryPage;
paymentsHistoryPage=function(){
  let html=v179BasePaymentsHistoryPage();if(!hasPerm('view_financials'))return html;let range=v179TaxMonthRange(),active=paymentFrom===range.from&&paymentTo===range.to;
  return html.replace(/(<button class="[^"]*" onclick="setPaymentDatePreset\('all'\)">All Time<\/button>)/,`$1<button class="${active?'active':''}" onclick="v179SetCurrentTaxMonth()">Current CIS Tax Month</button>`)
};

permissionInputs=function(permissions){
  let matched=roleOptions.find(roleName=>v173SamePermissionSet(permissions,v173RoleDefaultPerms(roleName))),summary=matched?v173RoleTemplateCard(matched):'<div class="v173-role-template-card custom"><span>CUSTOM WORKSPACE</span><b>Access tailored to this person</b><p>Only the task questions ticked below will be available.</p></div>';
  let groups=permissionGroups.map(group=>{let copy=V179_GROUP_COPY[group.name]||[group.name,''];return `<section class="permission-group v179-permission-group"><h4>${v179Esc(copy[0])}</h4><p>${v179Esc(copy[1])}</p><div class="v179-question-grid">${group.items.map(([key,label])=>{let text=V179_PERMISSION_COPY[key]||[`Can this person ${String(label||key).toLowerCase()}?`,''];return `<label class="v179-permission-question"><input type="checkbox" data-perm="${key}" ${permissions?.[key]?'checked':''}><span><b>${v179Esc(text[0])}</b>${text[1]?`<small>${v179Esc(text[1])}</small>`:''}</span></label>`}).join('')}</div></section>`}).join('');
  return `<div id="v173-role-template-summary">${summary}</div><details class="permission-customizer v179-permission-customizer"><summary><span><b>Does this person need anything different?</b><small>The selected job role already provides the recommended tasks</small></span><b>▾</b></summary><div class="v179-permission-intro"><b>Choose what this person can do</b><span>Read each question and tick only the extra tasks they need. Untick a task to remove it.</span></div><div class="permission-groups">${groups}</div></details>`
};

const v179BaseShell=shell;
shell=function(content){v179BaseShell(content);let root=document.getElementById('root');if(root)root.innerHTML=root.innerHTML.replace(/v1\.78\.0 · SITE FLOW(?: DEMO)?/g,`v1.79.0 · CIS PAY${String(root.innerHTML).includes('demo-mode-banner')?' DEMO':''}`)};

function v179Migrate(){
  let changed=false;db.cisProfiles=Array.isArray(db.cisProfiles)?db.cisProfiles:[];
  if(!db.cisContractor){db.cisContractor={contractorName:'',contractorUTR:'',employerTaxReference:'',accountsOfficeReference:''};changed=true}
  allPaymentRows().forEach(payment=>{if(payment.cisMaterialsReviewed===undefined&&(payment.cisMaterials!==undefined||V179_CIS_READY_STATUSES.includes(payment.reconciliationStatus))){payment.cisMaterialsReviewed=true;changed=true}});
  (db.users||[]).forEach(user=>{if(Number(user.permissionTemplateVersion||0)<179){user.permissionTemplateVersion=179;changed=true}});return changed
}
const v179BaseMigrate=migrate;
migrate=function(persist=true){v179BaseMigrate(false);let changed=v179Migrate();if(persist&&(changed||persist))save()};
if(v179Migrate())persistCache();

/* ===== v180-usability-audit.js ===== */
'use strict';

// SiteSight V1.80 — secure sign-in, role journey audit and low-friction starts.

let v180AuthBusy=false;
function v180SafeClone(value){return JSON.parse(JSON.stringify(value||{}))}
function v180PublicCache(value=db){let copy=v180SafeClone(value);(copy.users||[]).forEach(user=>{delete user.password;delete user.passwordHash;delete user.resetCode;delete user.resetExpires});return copy}

persistCache=function(){
  try{localStorage.setItem(CACHE_KEY,JSON.stringify(sharedMode?v180PublicCache():v180SafeClone(db)));return true}
  catch(error){syncState='error';syncError='This device could not store the local safety copy.';setTimeout(()=>toast('Could not save the local safety copy'),0);return false}
};

function v180InstallRemote(payload){
  db=payload.data||v180SafeClone(seed);sharedRevision=Number(payload.revision||0);lastSyncedDB=v180SafeClone(db);pendingSave=false;syncReady=true;syncState='synced';syncError='';lastSyncAt=new Date().toISOString();
  migrate(false);persistCache();try{localStorage.setItem(BASE_KEY,JSON.stringify(v180PublicCache(db)));localStorage.removeItem(PENDING_KEY)}catch(error){}
}

const v180LocalLogin=login;
login=async function(){
  if(!sharedMode)return v180LocalLogin();
  if(v180AuthBusy)return;let username=(document.getElementById('login-user')?.value||'').trim().toLowerCase(),password=document.getElementById('login-pass')?.value||'',button=document.querySelector?.('.auth-login-btn');
  if(!username||!password)return v180LoginMessage('Enter your username and password.');
  v180AuthBusy=true;if(button){button.disabled=true;button.innerHTML='Signing in…'}v180LoginMessage('Checking your account…','working');
  try{
    let response=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username,password})}),result=await response.json().catch(()=>({}));
    if(!response.ok)throw new Error(result.message||'Username or password is incorrect.');
    v180InstallRemote(result);currentUserId=result.userId;try{localStorage.setItem(CURRENT_USER_KEY,currentUserId)}catch(error){}let user=currentUser();if(!user)throw new Error('Your account could not be loaded.');
    role=user.role;trainingMode=!!user.trainingMode;if(site!=='all'&&!canAccessSite(site))site='all';selected=null;selectedGang=null;guideActive=false;guideStep=0;view=v173HomeView(user);startSyncPolling();render();setTimeout(deliverNotifications,100)
  }catch(error){v180LoginMessage(error.message||'Sign-in failed. Check the connection and try again.','error');let pass=document.getElementById('login-pass');if(pass){pass.value='';pass.focus()}}
  finally{v180AuthBusy=false;button=document.querySelector?.('.auth-login-btn');if(button){button.disabled=false;button.innerHTML='Sign in <span>→</span>'}}
};

function v180LoginMessage(message='',kind='error'){
  let form=document.querySelector?.('.auth-form');if(!form)return;let row=document.getElementById('v180-login-message');if(!row){row=document.createElement('div');row.id='v180-login-message';form.insertBefore(row,form.firstChild)}row.className=`v180-login-message ${kind}`;row.textContent=message
}

const v180BaseLoginScreen=loginScreen;
loginScreen=function(){v180BaseLoginScreen();let form=document.querySelector?.('.auth-form');if(form&&!document.getElementById('v180-login-help'))form.insertAdjacentHTML('beforeend','<div id="v180-login-help" class="v180-login-help"><b>First time here?</b> Use the username and temporary password supplied by your manager. You can change the password after signing in.</div>')};

forgotPassword=function(){document.getElementById('root').innerHTML=`<div class="auth-reset-page"><div class="auth-reset-card"><img class="auth-reset-logo" src="assets/sitesight-login-logo.png" alt="SiteSight"><button class="auth-back-link" onclick="loginScreen()">← Back to sign in</button><div class="auth-welcome"><span class="auth-eyebrow">ACCOUNT HELP</span><h2>Cannot sign in?</h2><p>For security, password changes are completed by an authorised SiteSight manager.</p></div><div class="v180-recovery-steps"><div><i>1</i><span><b>Contact your Director or authorised manager</b><small>Tell them your SiteSight username. Never send your old password.</small></span></div><div><i>2</i><span><b>Ask for a temporary password</b><small>They can reset it from Admin → Users.</small></span></div><div><i>3</i><span><b>Sign in and change it</b><small>Open Settings → Change password.</small></span></div></div><button class="btn auth-login-btn" onclick="loginScreen()">Return to sign in <span>→</span></button></div></div>`};

const v180LocalSaveOwnPassword=saveOwnPassword,v180LocalSaveResetPassword=saveResetPassword,v180LocalConfirmDeleteSite=confirmDeleteSite;
saveOwnPassword=async function(){
  if(!sharedMode)return v180LocalSaveOwnPassword();let current=document.getElementById('cp-old')?.value||'',next=document.getElementById('cp-new')?.value||'',confirmValue=document.getElementById('cp-confirm')?.value||'';if(next.length<8)return toast('Use at least 8 characters');if(next!==confirmValue)return toast('New passwords do not match');
  try{let response=await fetch('/api/auth/change-password',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({currentPassword:current,newPassword:next})}),result=await response.json().catch(()=>({}));if(!response.ok)throw new Error(result.message||'Password could not be changed');if(result.data)v180InstallRemote(result);modal=null;savedState('Password changed');render()}catch(error){toast(error.message||'Password could not be changed')}
};
saveResetPassword=async function(id){
  if(!sharedMode)return v180LocalSaveResetPassword(id);if(!(canDirector()||hasPerm('reset_passwords')))return;let next=document.getElementById('rp-new')?.value||'',confirmValue=document.getElementById('rp-confirm')?.value||'';if(next.length<8)return toast('Use at least 8 characters');if(next!==confirmValue)return toast('Passwords do not match');
  try{let response=await fetch('/api/auth/admin-reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:id,newPassword:next})}),result=await response.json().catch(()=>({}));if(!response.ok)throw new Error(result.message||'Password could not be reset');if(result.data)v180InstallRemote(result);modal=null;savedState('Password reset');render()}catch(error){toast(error.message||'Password could not be reset')}
};
confirmDeleteSite=async function(id){
  if(!sharedMode)return v180LocalConfirmDeleteSite(id);if(!hasPerm('permanent_delete'))return toast('Permanent Deletion permission is required');let password=document.getElementById('delete-site-password')?.value||'',row=db.sites.find(siteRow=>siteRow.id===id);if(!row)return;
  try{let response=await fetch('/api/auth/verify-password',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password})}),result=await response.json().catch(()=>({}));if(!response.ok)throw new Error(result.message||'Password is incorrect');db.items=db.items.filter(item=>item.site!==id);db.sites=db.sites.filter(siteRow=>siteRow.id!==id);if(site===id)site='all';audit('delete','site',id,`Site permanently deleted — ${row.name}`,id);save();emitNotification('site','',`Site deleted — ${row.name}`);modal=null;savedState('Site deleted');render()}catch(error){toast(error.message||'Password is incorrect')}
};

const v180LocalCompleteFirstSetup=completeFirstSetup;
completeFirstSetup=async function(){
  if(!sharedMode)return v180LocalCompleteFirstSetup();if(v180AuthBusy)return;
  let name=(document.getElementById('setup-name')?.value||'').trim(),siteNameValue=(document.getElementById('setup-site')?.value||'').trim(),username=(document.getElementById('setup-user')?.value||'').trim().toLowerCase(),email=(document.getElementById('setup-email')?.value||'').trim().toLowerCase(),password=document.getElementById('setup-pass')?.value||'',button=document.querySelector?.('.setup-button');
  if(!name||!siteNameValue||!username||!email||!password)return toast('Complete every setup field');if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))return toast('Enter a valid email address');if(password.length<8)return toast('Use a password with at least 8 characters');
  v180AuthBusy=true;if(button){button.disabled=true;button.textContent='Creating SiteSight…'}
  try{let response=await fetch('/api/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,siteName:siteNameValue,username,email,password})}),result=await response.json().catch(()=>({}));if(!response.ok)throw new Error(result.message||'Setup could not be completed');v180InstallRemote(result);currentUserId=result.userId;try{localStorage.setItem(CURRENT_USER_KEY,currentUserId)}catch(error){}site=db.sites[0]?.id||'all';role='Director';trainingMode=true;view='dashboard';startSyncPolling();render();savedState('SiteSight is ready')}
  catch(error){toast(error.message||'Setup could not be completed');if(button){button.disabled=false;button.textContent='Create SiteSight →'}}finally{v180AuthBusy=false}
};

const v180BaseLogout=logout;
logout=function(){if(sharedMode)fetch('/api/auth/logout',{method:'POST'}).catch(()=>{});return v180BaseLogout()};

bootSiteSight=async function(){
  if(!sharedMode){migrate(false);syncReady=true;syncState='local';if(!db.users.length)firstSetupScreen();else render();return}
  document.getElementById('root').innerHTML=`<div class="boot-screen"><img src="assets/sitesight-login-logo.png" alt="SiteSight"><div class="boot-spinner"></div><b>Opening SiteSight securely…</b><span>Checking your sign-in and loading current records</span></div>`;
  try{
    let response=await fetch('/api/auth/session',{cache:'no-store'}),result=await response.json().catch(()=>({}));
    if(response.status===401){syncReady=true;syncState='connecting';currentUserId='';try{localStorage.removeItem(CURRENT_USER_KEY)}catch(error){}if(result.setupRequired)firstSetupScreen();else loginScreen();return}
    if(!response.ok)throw new Error('SiteSight service is unavailable');v180InstallRemote(result);currentUserId=result.userId;try{localStorage.setItem(CURRENT_USER_KEY,currentUserId)}catch(error){}let user=currentUser();if(!user){loginScreen();return}role=user.role;trainingMode=!!user.trainingMode;view=v173HomeView(user);startSyncPolling();render()
  }catch(error){migrate(false);syncReady=true;syncState='offline';syncError='The shared service is unavailable. Previously signed-in users can use the cached safety copy on this device.';if(currentUser())render();else{loginScreen();v180LoginMessage('SiteSight cannot reach the shared service. Check the connection and try again.','error')}}
};

pollSharedData=async function(){
  if(!sharedMode||!syncReady||!navigator.onLine||!currentUserId)return;
  try{let response=await fetch(`/api/data?revision=${sharedRevision}`,{cache:'no-store'});if(response.status===401){syncState='error';syncError='Your secure session ended. Sign in again.';currentUserId='';try{localStorage.removeItem(CURRENT_USER_KEY)}catch(error){}loginScreen();v180LoginMessage('Your session ended. Please sign in again.','error');return}if(!response.ok)return;let incoming=await response.json();if(incoming.unchanged||incoming.revision===sharedRevision)return;if(pendingSave){db=threeWayMerge(lastSyncedDB||seed,db,incoming.data||seed);sharedRevision=incoming.revision;lastSyncedDB=v180SafeClone(incoming.data||seed);persistCache();scheduleSync(50)}else{v180InstallRemote(incoming);render()}}
  catch(error){if(pendingSave){syncState='offline';updateSyncUI()}}
};

const V180_ROLE_STARTS={
  'Director':{title:'Business overview',text:'Review exceptions first, then move into people, work or commercial records.',actions:[['People & Time','attendance','◷'],['Work Items','work-items','▣'],['Payments & CIS','payments-history','£']]},
  'Contracts Manager':{title:'Delivery overview',text:'Start with anything needing a decision, then check live work, time or payments.',actions:[['Live Work','work-items','▣'],['People & Time','attendance','◷'],['Payments & CIS','payments-history','£']]},
  'Site Manager':{title:'Run today’s site',text:'See who is working, update live plots and record the daily site position.',actions:[['Who is on site?','attendance','◷'],['Update Work','work-items','▣'],['Site Diary','diary','✎']]},
  'Supervisor':{title:'Keep work moving',text:'Open the current plot, record progress or Daywork, and raise issues as they happen.',actions:[['Current Work','work-items','▣'],['Dayworks','dayworks','＋'],['Issues','issues','⚠']]},
  'Quantity Surveyor':{title:'Commercial work queue',text:'Check progress evidence before reviewing Dayworks, pay records and CIS deductions.',actions:[['Work & Progress','work-items','▣'],['Dayworks','dayworks','＋'],['Payments & CIS','payments-history','£']]},
  'Accounts':{title:'Payment reconciliation',text:'Review compact pay and commercial records. Shift approval is unavailable unless a Director grants it.',actions:[['Payment records','payments-history','£'],['Weekly Report','weekly-report','▤'],['More Tools','more','•••']]},
  'Read Only':{title:'View site information',text:'Open the latest work position, dates or drawings without changing records.',actions:[['Work Progress','work-items','▣'],['Calendar','calendar','▣'],['Drawings','site-drawings','▤']]},
  'Operator':{title:'Clock In / Out',text:'Use the large button below. SiteSight only asks for location when you press Clock In or Clock Out.',actions:[['My Time','clock','◷']]}
};
function v180CanOpen(target){return target==='clock'?v173CanOpenView('clock')||v173CanOpenView('attendance'):v173CanOpenView(target)}
function v180Go(target){goNav(target==='clock'&&!v173CanOpenView('clock')?'attendance':target)}
function v180StartPanel(){
  let user=currentUser(),row=V180_ROLE_STARTS[user?.role]||V180_ROLE_STARTS.Operator,actions=row.actions.filter(action=>v180CanOpen(action[1]));if(!user)return'';let first=actions[0],newUser=!user.onboardingSeen;
  return `<section class="v180-start ${newUser?'new-user':''}"><div class="v180-start-copy"><span>${newUser?'WELCOME TO SITESIGHT':'START HERE'} · ${v180Esc(user.role)}</span><h2>${v180Greeting()}, ${v180Esc((user.name||'').split(' ')[0]||'there')}</h2><p><b>${v180Esc(row.title)}.</b> ${v180Esc(row.text)}</p>${newUser?'<div class="v180-welcome-actions"><button class="btn light smallbtn" onclick="v180StartTour()">Show me around</button><button class="v180-text-button" onclick="v180DismissWelcome()">I’m ready — hide this welcome</button></div>':''}</div><div class="v180-start-actions">${actions.map((action,index)=>`<button class="${index===0?'primary':''}" onclick="v180Go('${action[1]}')"><i>${action[2]}</i><span><b>${v180Esc(action[0])}</b><small>${index===0?'Recommended first step':'Open'}</small></span><strong>›</strong></button>`).join('')}</div></section>`
}
function v180Esc(value){return String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]))}
function v180Greeting(){let hour=new Date().getHours();return hour<12?'Good morning':hour<18?'Good afternoon':'Good evening'}
function v180DismissWelcome(){let user=currentUser();if(!user)return;user.onboardingSeen=true;user.onboardingCompletedAt=user.onboardingCompletedAt||new Date().toISOString();save();render()}
function v180StartTour(){let user=currentUser();if(user)user.onboardingSeen=true;save();openUserGuide(true)}
function v180InsertStart(html){return v174InsertAfterTopbar(html,v180StartPanel())}

const v180BaseDashboard=dashboard;
dashboard=function(){let html=v180BaseDashboard(),user=currentUser();return user&&v173HomeView(user)==='dashboard'?v180InsertStart(html):html};
const v180BaseWorkItems=workItems;
workItems=function(){let html=v180BaseWorkItems(),user=currentUser();return user&&v173HomeView(user)==='work-items'?v180InsertStart(html):html};
const v180BaseAttendancePage=attendancePage;
attendancePage=function(){let html=v180BaseAttendancePage(),user=currentUser();return user&&v173HomeView(user)==='attendance'?v180InsertStart(html):html};
const v180BaseClockPage=v175ClockPage;
v175ClockPage=function(){let html=v180BaseClockPage(),user=currentUser();return user&&v173HomeView(user)==='clock'?v180InsertStart(html):html};

dashboardRoleIntro=function(){let user=currentUser(),row=V180_ROLE_STARTS[user?.role]||V180_ROLE_STARTS.Operator;return `<div class="dashboard-role-note"><span>${v180Esc(user?.role||'Your role')}</span><p>${v180Esc(row.text)}</p></div>`};

const v180BaseMigrate=migrate;
migrate=function(persist=true){v180BaseMigrate(false);let changed=false;(db.sites||[]).forEach(row=>{row.attendanceConfig=row.attendanceConfig||{};if(Number(row.attendanceConfig.radiusM)!==250){row.attendanceConfig.radiusM=250;changed=true}});(db.users||[]).forEach(user=>{if(Number(user.permissionTemplateVersion||0)<180){user.permissionTemplateVersion=180;changed=true}delete user.passwordHash;delete user.resetCode;delete user.resetExpires});if(persist&&(changed||persist))save()};
if((db.users||[]).length)migrate(false);

const v180BaseShell=shell;
shell=function(content){v180BaseShell(content);let root=document.getElementById('root');if(root)root.innerHTML=root.innerHTML.replace(/v1\.79\.0 · CIS PAY(?: DEMO)?/g,`v1.80.0 · EASY START${String(root.innerHTML).includes('demo-mode-banner')?' DEMO':''}`)};

document.addEventListener('keydown',event=>{let input=document.getElementById('login-pass');if(!input||document.activeElement!==input)return;let message=document.getElementById('v180-caps-lock');if(event.getModifierState?.('CapsLock')){if(!message){message=document.createElement('div');message.id='v180-caps-lock';message.className='v180-caps-lock';message.textContent='Caps Lock is on';input.closest?.('.field')?.appendChild(message)}}else message?.remove()});

/* ===== v181-enterprise-demo.js ===== */
'use strict';

// SiteSight V1.81 — multi-site enterprise portfolio view.

function v181Esc(value){return String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]))}
function v181AccessibleSites(){return (db.sites||[]).filter(row=>!row.archived&&canAccessSite(row.id))}
function v181SiteTeamIds(siteId){return new Set((db.items||[]).filter(item=>item.site===siteId&&item.gang).map(item=>item.gang))}
function v181SitePeople(siteId){let ids=v181SiteTeamIds(siteId);return (db.gangs||[]).filter(team=>ids.has(team.id)).reduce((count,team)=>count+(team.members||[]).filter(member=>member.activeForAttendance!==false&&member.employmentStatus!=='Leaver').length,0)}
function v181SiteRow(siteRow){
  let items=(db.items||[]).filter(item=>item.site===siteRow.id),openIssues=items.reduce((count,item)=>count+(item.issues||[]).filter(issue=>issue.status==='Open').length,0),inProgress=items.filter(item=>overall(item)==='In progress').length,signed=items.filter(item=>overall(item)==='Signed Off').length,complete=items.filter(item=>overall(item)==='Complete').length;
  return {site:siteRow,items,openIssues,inProgress,signed,complete,people:v181SitePeople(siteRow.id),progress:siteProgress(siteRow.id)}
}
function v181PortfolioPanel(){
  let rows=v181AccessibleSites().map(v181SiteRow),plots=rows.reduce((sum,row)=>sum+row.items.length,0),live=rows.reduce((sum,row)=>sum+row.inProgress,0),issues=rows.reduce((sum,row)=>sum+row.openIssues,0),people=attendanceAllMembers().filter(row=>row.member.activeForAttendance!==false&&row.member.employmentStatus!=='Leaver').length;
  return `<section class="v181-portfolio panel"><div class="v181-portfolio-head"><div><span>ENTERPRISE PORTFOLIO</span><h2>All developments, one operational view</h2><p>Choose a site to move from company-wide exceptions into its plots, people and records.</p></div><button class="btn light smallbtn" onclick="goNav('weekly-report')">Open Site Reports</button></div><div class="v181-portfolio-metrics"><div><b>${rows.length}</b><span>Active sites</span></div><div><b>${plots}</b><span>Live plots</span></div><div><b>${live}</b><span>In progress</span></div><div class="${issues?'attention':''}"><b>${issues}</b><span>Open issues</span></div><div><b>${people}</b><span>Active people</span></div></div><div class="v181-site-grid">${rows.map(row=>`<button class="v181-site-card" onclick="setSiteContext('${row.site.id}')"><span class="v181-site-code">${v181Esc(row.site.name.split('—')[0].trim())}</span><b>${v181Esc(row.site.name)}</b><small>${row.items.length} plots · ${row.people} people · ${row.inProgress} in progress</small><i><em style="width:${row.progress}%"></em></i><span class="v181-site-foot"><strong>${row.progress}% complete</strong><strong class="${row.openIssues?'warn':''}">${row.openIssues} open issue${row.openIssues===1?'':'s'}</strong></span></button>`).join('')}</div></section>`
}

const v181BaseDashboard=dashboard;
dashboard=function(){
  let html=v181BaseDashboard(),user=currentUser();
  if(!user||site!=='all'||!['Director','Contracts Manager','Read Only'].includes(user.role)||v181AccessibleSites().length<2)return html;
  return v174InsertAfterTopbar(html,v181PortfolioPanel())
};

const v181BaseLoginScreen=loginScreen;
loginScreen=function(){
  v181BaseLoginScreen();
};

const v181BaseMigrate=migrate;
migrate=function(persist=true){
  v181BaseMigrate(false);let changed=false;
  (db.users||[]).forEach(user=>{if(Number(user.permissionTemplateVersion||0)<181){user.permissionTemplateVersion=181;changed=true}});
  (db.sites||[]).forEach(siteRow=>{siteRow.attendanceConfig=siteRow.attendanceConfig||{};if(Number(siteRow.attendanceConfig.radiusM)!==250){siteRow.attendanceConfig.radiusM=250;changed=true}});
  if(persist&&(changed||persist))save()
};
if((db.users||[]).length)migrate(false);

const v181BaseShell=shell;
shell=function(content){
  v181BaseShell(content);let root=document.getElementById('root');if(!root)return;
  root.innerHTML=root.innerHTML.replace(/v1\.80\.0 · EASY START DEMO/g,'v1.81.0 · ENTERPRISE')
};

/* ===== v182-operational-polish.js ===== */
'use strict';

// SiteSight V1.82 — operational polish, progressive disclosure and unified Daywork.

const V182_LIST_LIMIT=10,V182_TODAY_LIMIT=8,V182_INDIVIDUALS_ID='v176-individuals';
function v182Esc(value){return String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]))}
function v182Unique(values){return [...new Set(values.filter(Boolean))]}

// ---- Role access ----------------------------------------------------
// Everyone except an Operator keeps a visible Dashboard. Accounts receive
// payment/payroll access without user security, HR or Director Admin access.
if(!V178_FINANCIAL_ROLES.includes('Accounts'))V178_FINANCIAL_ROLES.push('Accounts');
Object.entries(V173_ROLE_PROFILES).forEach(([roleName,profile])=>{
  if(roleName!=='Operator'&&Array.isArray(profile.permissions)&&!profile.permissions.includes('dashboard'))profile.permissions.unshift('dashboard')
});
['view_financials','record_payments','manage_payments','lock_payroll'].forEach(key=>{
  let profile=V173_ROLE_PROFILES.Accounts;if(profile&&!profile.permissions.includes(key))profile.permissions.push(key)
});
['manage_users','reset_passwords','manage_gangs','manage_sites','audit','view_archives'].forEach(key=>{let profile=V173_ROLE_PROFILES.Accounts;if(profile)profile.permissions=profile.permissions.filter(value=>value!==key)});
['view_attendance'].forEach(key=>{let profile=V173_ROLE_PROFILES['Quantity Surveyor'];if(profile&&!profile.permissions.includes(key))profile.permissions.push(key)});
V173_ROLE_PROFILES.Accounts.purpose='Working hours, approved pay records, payments due and payroll hand-off without private HR, user security or Director administration.';
V173_ROLE_PROFILES['Quantity Surveyor'].purpose='Commercial records, payments due, CIS, variations and working-hours evidence.';

const v182BaseRoleNavItems=v173RoleNavItems;
v173RoleNavItems=function(user=currentUser()){
  let items=v182BaseRoleNavItems(user).filter(item=>item.id!=='dayworks');
  if(user&&user.role!=='Operator'){
    let dashboard=items.find(item=>item.id==='dashboard')||{id:'dashboard',...V173_MAIN_NAV.dashboard};
    items=[dashboard,...items.filter(item=>item.id!=='dashboard')]
  }
  return items
};
const v182BaseMoreToolIds=v173MoreToolIds;
v173MoreToolIds=function(user=currentUser()){return v182BaseMoreToolIds(user).filter(id=>id!=='dayworks')};
delete V173_MORE_TOOLS.dayworks;
if(typeof V180_ROLE_STARTS==='object')Object.values(V180_ROLE_STARTS).forEach(row=>{(row.actions||[]).forEach(action=>{if(action[1]==='dayworks'){action[0]='Daywork Work Items';action[1]='dayworks'}})});

const v182BaseAssistPageLabel=assistPageLabel;
assistPageLabel=function(){let label=v182BaseAssistPageLabel();return label==='Teams'?'Individuals/Teams':label==='Dayworks'?'Daywork Work Items':label};
const v182BaseAssistGuide=assistGuide;
assistGuide=function(target,context){if(target==='dayworks')return 'Daywork is a purple Work Item. Open Work Items, choose Daywork when adding a record, then assign it to a Team or individual. It stays in the normal Dashboard, Work Items and reporting flow.';if(target==='gangs')return 'Individuals/Teams shows Site-specific people and Teams, working details and Team analytics where your permissions allow it.';return v182BaseAssistGuide(target,context)};

const v182BaseGoNav=goNav;
goNav=function(target){
  if(target==='dayworks'){type='Daywork';status='all';signed='all';q='';selected=null;selectedGang=null;return v182BaseGoNav('work-items')}
  return v182BaseGoNav(target)
};

// ---- Clear expandable lists ----------------------------------------
function v182ListRows(rows,renderer,limit=V182_LIST_LIMIT,noun='items',holderClass='v182-expanded-list'){
  let first=rows.slice(0,limit).map(renderer).join(''),rest=rows.slice(limit);
  if(!rest.length)return first;
  return `${first}<details class="v182-expander"><summary><span>View all ${rows.length} ${noun}</span><b>${rest.length} more ▼</b></summary><div class="${holderClass}">${rest.map(renderer).join('')}</div></details>`
}
function v182ActivityRow(x){return `<div class="activityrow" onclick="openItem('${x.id}')"><b>${v182Esc(x.ref)}</b><span>${v182Esc(x.type)}${x.houseType?' · '+v182Esc(x.houseType):''}</span><span class="activity-hide">${v182Esc(siteName(x.site))} · ${v182Esc(gangName(x.gang))}</span><span>${badge(overall(x))}</span><b>›</b></div>`}

todayItemsList=function(){
  let rows=todaysItems(),renderRow=x=>`<div class="todayitem" onclick="openItem('${x.id}')"><div class="todayitemmain"><b>${v182Esc(x.ref)}</b><span>${v182Esc(x.type)}${x.houseType?' · '+v182Esc(x.houseType):''} · ${v182Esc(siteName(x.site))}</span></div><span class="todaystatus">${overall(x)}</span></div>`;
  return `<div class="todayitems"><h4>Today's items</h4>${rows.length?v182ListRows(rows,renderRow,V182_TODAY_LIMIT,'items'):'<div class="muted v182-empty-note">No items updated today.</div>'}</div>`
};

dashboardNeedsAttentionBlock=function(){
  let rows=needsAttentionItems(),renderRow=x=>`<button class="dashboard-attention-row" onclick="openItem('${x.id}')"><span><b>${v182Esc(x.ref)} · ${v182Esc(x.type)}</b><small>${v182Esc(userAttentionText(x))}</small></span><strong>›</strong></button>`;
  return `<div class="panel dashboard-attention"><div class="sectionhead"><div><h3>Needs Attention</h3><div class="muted">The items most likely to need action now</div></div><button class="viewall" onclick="openNeedsAttention()">Open full view</button></div>${rows.length?`<div class="dashboard-attention-grid">${v182ListRows(rows,renderRow,V182_TODAY_LIMIT,'items')}</div>`:smartEmpty('Nothing currently needs attention.','Open issues, unassigned Teams, sign-off and commercial exceptions will appear here.','','','✓')}</div>`
};

needsAttentionPage=function(){
  let rows=needsAttentionItems(),renderRow=x=>`<div class="needs-attention-full-card" onclick="openItem('${x.id}')"><div class="needs-attention-card-main"><div><b>${v182Esc(x.ref)}</b><span>${v182Esc(x.type)} · ${v182Esc(siteName(x.site))}</span></div><span class="status-text status-issue">⚠ Needs attention</span></div><div class="needs-attention-reason">${v182Esc(userAttentionText(x))}</div><div class="needs-attention-card-meta"><span>Team: ${v182Esc(gangName(x.gang))}</span><span>${v182Esc(lastUpdatedText(x))}</span><b>Open ›</b></div></div>`;
  return `<div class="topbar"><div><h1>Needs Attention</h1><div class="sub">Prioritised exceptions that currently need action</div>${selectedSiteHeading()}</div><div class="actions"><button class="btn light" onclick="goNav('dashboard')">← Dashboard</button><select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(row=>`<option value="${row.id}" ${site===row.id?'selected':''}>${v182Esc(row.name)}</option>`).join('')}</select></div></div><div class="panel needs-attention-page"><div class="sectionhead"><div><h2>Needs Attention</h2><div class="muted">${rows.length} work item${rows.length===1?'':'s'} require attention</div></div></div><div class="needs-attention-grid">${rows.length?v182ListRows(rows,renderRow,V182_LIST_LIMIT,'items','v182-expanded-attention'):smartEmpty('Nothing currently needs attention.','Items appear here automatically when an authorised user needs to act.','','','✓')}</div></div>`
};

activityPage=function(){
  let range=v174RecordRanges.activity,custom=!!(range.from||range.to),weekly=activityRange==='week',items=custom?itemsUpdatedBetween(range.from,range.to):(weekly?weeklyItems():todaysItems()),title=custom?'Activity Summary':weekly?'Weekly Summary':'Today’s Summary',sub=custom?'Items with a dated update in the selected range':weekly?'Items updated during the last 7 days':'Items updated today';
  return `<div class="topbar"><div><h1>${title}</h1><div class="sub">${sub}</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(row=>`<option value="${row.id}" ${site===row.id?'selected':''}>${v182Esc(row.name)}</option>`).join('')}</select></div></div>${v174RecordRangeBar('activity','Custom activity range')}<div class="panel"><div class="activityrow v182-table-head"><span>Reference</span><span>Type</span><span class="activity-hide">Site / Team</span><span>Status</span><span></span></div>${items.length?v182ListRows(items,v182ActivityRow,V182_LIST_LIMIT,'records'):'<div class="empty">No items updated in this period.</div>'}</div>`
};

workItems=function(){
  let range=v174RecordRanges.work,original=db.items,dated=original.filter(item=>!range.from&&!range.to||v174WorkItemDates(item).some(date=>v174DateInRange(date,range.from,range.to))),rows,filterMarkup,summaryMarkup;
  db.items=dated;try{rows=filtered();filterMarkup=filters();summaryMarkup=filterSummary()}finally{db.items=original}
  let hasFilters=site!=='all'||gang!=='all'||type!=='all'||status!=='all'||signed!=='all'||!!q||!!range.from||!!range.to,first=rows.slice(0,12),rest=rows.slice(12),cards=first.map(card).join('');
  if(rest.length)cards+=`<details class="v182-expander v182-card-expander"><summary><span>View all ${rows.length} matching Work Items</span><b>${rest.length} more ▼</b></summary><div class="cards v182-expanded-cards">${rest.map(card).join('')}</div></details>`;
  let html=`<div class="topbar"><div><h1>Work Items</h1><div class="sub">Search and manage plots, Daywork and other work packages</div>${selectedSiteHeading()}${trainingTip('Start with the Site filter, then search only if you need to.')}</div>${hasPerm('manage_workitems')?'<button class="btn orange primary-action" onclick="newItem()">＋ Add work item</button>':''}</div>${v174RecordRangeBar('work','Work record date range')}<div class="panel">${filterMarkup}<div class="v182-work-list">${summaryMarkup}<div class="cards">${cards||(hasFilters?smartEmpty('No work items match these filters.','Clear the filters or try a different search.','Clear filters','resetWorkFilters()','⌕'):smartEmpty('No work items yet.','Create the first Work Item for this site to get started.',hasPerm('manage_workitems')?'＋ Create Work Item':'',hasPerm('manage_workitems')?'newItem()':'','▣'))}</div></div></div>`;
  let user=currentUser();return user&&v173HomeView(user)==='work-items'?v180InsertStart(html):html
};

// ---- Daywork is now a normal Work Item -----------------------------
const v182BaseWorkTypes=workTypes;
workTypes=function(){return v182Unique(['House Plot','Garden Wall','Daywork',...v182BaseWorkTypes()])};
function v182AssigneeOptions(){
  let teams=v176ActiveTeams(),people=attendanceAllMembers().filter(row=>row.member.activeForAttendance!==false&&row.member.employmentStatus!=='Leaver');
  return `<option value="">To be Confirmed</option><optgroup label="Teams">${teams.map(row=>`<option value="team:${row.id}">${v182Esc(row.name)}</option>`).join('')}</optgroup><optgroup label="Individuals">${people.map(row=>`<option value="person:${row.member.id}">${v182Esc(row.member.name)} · ${v182Esc(row.gang.name)}</option>`).join('')}</optgroup>`
}
function v182ToggleNewItemFields(){
  let selectedType=document.getElementById('nt')?.value||'',daywork=selectedType==='Daywork',house=selectedType==='House Plot';
  let dayworkFields=document.getElementById('v182-daywork-fields'),houseField=document.getElementById('v182-house-field'),assigneeLabel=document.getElementById('v182-assignee-label'),assignee=document.getElementById('ng');
  if(dayworkFields)dayworkFields.hidden=!daywork;if(houseField)houseField.hidden=!house;if(assigneeLabel)assigneeLabel.textContent=daywork?'Assign to Team or individual':'Team';
  if(assignee){
    let wasDaywork=[...assignee.options].some(option=>String(option.value).startsWith('person:')||String(option.value).startsWith('team:'));
    if(daywork&&!wasDaywork)assignee.innerHTML=v182AssigneeOptions();
    if(!daywork&&wasDaywork)assignee.innerHTML=`<option value="">To be Confirmed</option>${v176ActiveTeams().map(row=>`<option value="${row.id}">${v182Esc(row.name)}</option>`).join('')}`
  }
  if(typeof v182BaseToggleCustomType==='function')v182BaseToggleCustomType()
}
const v182BaseToggleCustomType=toggleCustomType;
toggleCustomType=function(){v182BaseToggleCustomType();v182ToggleNewItemFields()};
newItem=function(forced=''){
  if(!hasPerm('manage_workitems'))return toast('You do not have work-item creation access');let types=workTypes(),chosen=forced&&types.includes(forced)?forced:types[0],isCustom=!!forced&&!types.includes(forced),daywork=chosen==='Daywork';
  modal=`<div class="modal"><div class="modalbox v182-new-work"><span class="att-kicker">ONE WORK LIST</span><h2>Create Work Item</h2><p class="muted">Daywork is recorded here and appears in the same Dashboard and Work Items list as every other job.</p><div class="two"><div class="field"><label>Type</label><select id="nt" class="select" onchange="toggleCustomType()">${types.map(value=>`<option value="${v182Esc(value)}" ${chosen===value?'selected':''}>${v182Esc(value)}</option>`).join('')}<option value="__custom__" ${isCustom?'selected':''}>＋ Custom type...</option></select><div id="custom-type-box" class="custom-type-wrap" style="display:${isCustom?'block':'none'}"><label>Custom type</label><input id="nt-custom" class="input" value="${isCustom?v182Esc(forced):''}" placeholder="e.g. Porch, Garage, Boundary Feature"></div></div><div class="field"><label>Reference</label><input id="nr" class="input" placeholder="${daywork?'e.g. DW-042 · Plot 118 retaining wall':'e.g. Plot 118'}"></div></div><div class="two"><div class="field"><label>Site</label><select id="ns" class="select">${activeSites().map(row=>`<option value="${row.id}" ${site===row.id?'selected':''}>${v182Esc(row.name)}</option>`).join('')}</select></div><div class="field"><label id="v182-assignee-label">${daywork?'Assign to Team or individual':'Team'}</label><select id="ng" class="select">${daywork?v182AssigneeOptions():`<option value="">To be Confirmed</option>${v176ActiveTeams().map(row=>`<option value="${row.id}">${v182Esc(row.name)}</option>`).join('')}`}</select></div></div><div id="v182-house-field" class="field" ${chosen==='House Plot'?'':'hidden'}><label>House type</label><input id="nh" class="input"></div><div id="v182-daywork-fields" ${daywork?'':'hidden'}><div class="field"><label>Work instructed</label><textarea id="v182-dw-work" class="textarea" placeholder="Describe the extra work and why it was needed"></textarea></div><div class="three"><div class="field"><label>Work date</label><input id="v182-dw-date" class="input" type="date" value="${iso()}"></div><div class="field"><label>Instructed by</label><input id="v182-dw-by" class="input" placeholder="Name / role"></div><div class="field"><label>Instruction reference</label><input id="v182-dw-ref" class="input" placeholder="SI / VO / email / verbal"></div></div><div class="two"><div class="field"><label>Labour hours</label><input id="v182-dw-hours" class="input" type="number" min="0" step="0.5" value="8"></div>${hasPerm('view_financials')?'<div class="field"><label>Estimated value</label><input id="v182-dw-value" class="input" type="number" min="0" step="0.01"></div>':''}</div></div><button class="btn orange" onclick="createItem()">Create work item</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
};

const v182BaseCreateItem=createItem;
createItem=function(){
  let selectedType=document.getElementById('nt')?.value||'';if(selectedType!=='Daywork')return v182BaseCreateItem();
  let ref=(document.getElementById('nr')?.value||'').trim(),siteId=document.getElementById('ns')?.value||'',assignee=document.getElementById('ng')?.value||'',work=(document.getElementById('v182-dw-work')?.value||'').trim();
  if(!ref||!siteId||!work)return toast('Reference, Site and work instructed are required');if(db.items.some(item=>item.site===siteId&&String(item.ref||'').trim().toLowerCase()===ref.toLowerCase()))return toast('That Work Item reference already exists on this Site');
  let [assignedTypeRaw,assignedIdRaw]=assignee.includes(':')?assignee.split(':'):['Team',assignee],assignedType=assignedTypeRaw==='person'?'Individual':'Team',assignedId=assignedIdRaw||'',person=assignedType==='Individual'?attendanceMemberById(assignedId):null,gangId=assignedType==='Team'?assignedId:(person?.gang.id||''),now=new Date().toISOString(),date=document.getElementById('v182-dw-date')?.value||iso(),hours=Number(document.getElementById('v182-dw-hours')?.value||0),value=Number(document.getElementById('v182-dw-value')?.value||0);
  let item={id:'dw-item-'+Date.now(),type:'Daywork',ref,houseType:'',site:siteId,gang:gangId,assignedType,assignedId,assignedName:assignedType==='Individual'?(person?.member.name||'Individual'):(db.gangs.find(row=>row.id===assignedId)?.name||'To be Confirmed'),notes:work,daywork:{date,work,instructedBy:(document.getElementById('v182-dw-by')?.value||'').trim(),instructionRef:(document.getElementById('v182-dw-ref')?.value||'').trim(),labourHours:hours},status:'Started',price:value,pct:0,paidTo:assignedId,date:'',paymentNotes:'',completedDate:'',payments:[],signed:false,drawings:[],workPhotos:[],issues:[],variations:[],lastUpdated:date,lastUpdatedAt:now,lastUpdatedBy:currentUser()?.name||'',createdAt:now,createdBy:currentUser()?.name||''};
  db.items.push(item);save();audit('create','work item',item.id,`Daywork Work Item created — ${item.ref} · assigned to ${item.assignedName}`,item.site);modal=null;selected=item.id;selectedGang=null;savedState('Daywork added to Work Items');render()
};

const v182BaseCard=card;
card=function(item){
  let html=v182BaseCard(item);if(item?.type!=='Daywork')return html;
  html=html.replace('class="workcard ','class="workcard v182-daywork ');
  html=html.replace('<div class="type">Daywork</div>',`<div class="type"><span class="badge b-purple">DAYWORK</span>${item.daywork?.date?' · '+v182Esc(item.daywork.date):''}</div>`);
  if(item.assignedName)html=html.replace(`♟ ${gangName(item.gang)}`,`♙ ${v182Esc(item.assignedName)}`);
  return html
};
const v182BaseDetail=detail;
detail=function(item){
  let html=v182BaseDetail(item);if(item?.type!=='Daywork')return html;let meta=item.daywork||{};
  let block=`<div class="v182-daywork-context"><span class="badge b-purple">DAYWORK</span><div><b>${v182Esc(meta.work||item.notes||'Instructed extra work')}</b><small>${v182Esc(meta.date||item.lastUpdated||'')} · assigned to ${v182Esc(item.assignedName||gangName(item.gang))}</small></div><dl><div><dt>Instructed by</dt><dd>${v182Esc(meta.instructedBy||'Not recorded')}</dd></div><div><dt>Reference</dt><dd>${v182Esc(meta.instructionRef||'Not recorded')}</dd></div><div><dt>Labour</dt><dd>${Number(meta.labourHours||0).toFixed(1)} hrs</dd></div></dl></div>`;
  return html.replace('<div class="work-section-card">',block+'<div class="work-section-card">')
};

function v182DayworkItem(row,index=0){
  let assignedType=row.assignedType||'Team',assignedId=row.assignedId||row.gangId||'',wm=assignedType==='Individual'?attendanceMemberById(assignedId):null,gangId=assignedType==='Team'?assignedId:(wm?.gang.id||''),stamp=row.createdAt||`${row.date||iso()}T12:00:00.000Z`;
  return {id:`daywork-item-${row.id||index}`,type:'Daywork',ref:row.reference||row.instructionRef||`DW-${String(index+1).padStart(3,'0')}`,houseType:'',site:row.siteId||'',gang:gangId,assignedType,assignedId,assignedName:v176DayworkAssignee(row),notes:row.work||'',daywork:{date:row.date||'',work:row.work||'',instructedBy:row.instructedBy||'',instructionRef:row.instructionRef||'',labourHours:(row.labour||[]).reduce((sum,entry)=>sum+Number(entry.hours||0),0)},status:row.status==='Complete'?'Complete':'Started',price:Number(row.value||0),pct:row.status==='Complete'?100:0,paidTo:assignedId,date:'',paymentNotes:'',completedDate:row.status==='Complete'?(row.date||''):'',payments:[],signed:false,drawings:[],workPhotos:row.photos||[],issues:[],variations:[],lastUpdated:row.date||'',lastUpdatedAt:stamp,lastUpdatedBy:row.createdBy||row.instructedBy||'',createdAt:stamp,createdBy:row.createdBy||''}
}

// ---- Individuals / Teams -------------------------------------------
function v182MemberSites(member,team){
  let ids=[...(member.clockSiteIds||[])],user=(db.users||[]).find(row=>row.id===member.linkedUserId||row.linkedMemberId===member.id);
  if(user){if((user.allowedSites||[]).length)ids.push(...user.allowedSites);else if(['Director','Contracts Manager'].includes(user.role))ids.push(...activeSites().map(row=>row.id))}
  (db.attendanceShifts||[]).filter(row=>row.workerId===member.id).forEach(row=>ids.push(row.siteId));
  (db.items||[]).filter(row=>row.gang===team?.id).forEach(row=>ids.push(row.site));return v182Unique(ids)
}
function v182MemberVisible(member,team){return site==='all'||v182MemberSites(member,team).includes(site)}
gangsPage=function(){
  if(!hasPerm('gangs'))return '<div class="panel empty-help"><b>You do not have permission to view Individuals/Teams.</b></div>';
  let individuals=v176Individuals(),query=v177PeopleQuery.trim(),allTeams=v176ActiveTeams(),people=(individuals?.members||[]).filter(member=>v182MemberVisible(member,individuals)&&v177PersonMatches(member,'Individual',query)),teams=allTeams.map(team=>({...team,members:(team.members||[]).filter(member=>v182MemberVisible(member,team)&&v177PersonMatches(member,team.name,query))})).filter(team=>{let assigned=(db.items||[]).some(item=>item.gang===team.id&&(site==='all'||item.site===site));return (site==='all'||assigned||team.members.length)&&(!query||team.members.length||team.name.toLowerCase().includes(query.toLowerCase()))}),shown=people.length+teams.reduce((sum,team)=>sum+team.members.length,0);
  return `<div class="topbar"><div><h1>Individuals/Teams</h1><div class="sub">Find people quickly and manage Site-specific Team membership</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(row=>`<option value="${row.id}" ${site===row.id?'selected':''}>${v182Esc(row.name)}</option>`).join('')}</select>${hasPerm('manage_gangs')?'<button class="btn light" onclick="goNav(\'workforce-archive\')">Former people</button><button class="btn blue" onclick="v176NewPerson()">＋ Add Individual</button><button class="btn orange" onclick="addTeam()">＋ Add Team</button>':''}</div></div>${v177PeopleSearchPanel(shown)}<div class="panel v176-people-panel"><div class="sectionhead"><div><span class="att-kicker">NOT IN A TEAM</span><h2>Individuals</h2><div class="muted">People currently shown for ${site==='all'?'all accessible sites':v182Esc(siteName(site))}.</div></div><span class="badge b-blue">${people.length}</span></div><div class="v176-people-grid">${people.map(member=>`<div class="v176-person-card v182-person-card"><div onclick="openTeamMember('${V182_INDIVIDUALS_ID}','${member.id}')" class="clickable v182-person-copy"><b>${v182Esc(member.name)}</b><span>${v182Esc(member.role||'Individual')} · ${member.linkedUserId?'Login linked':'No login'}</span></div><div class="v176-person-actions v182-person-actions"><button class="btn light smallbtn" onclick="openTeamMember('${V182_INDIVIDUALS_ID}','${member.id}')">Open</button>${hasPerm('manage_gangs')?`<select class="select" aria-label="Add ${v182Esc(member.name)} to Team" onchange="if(this.value)v176MovePerson('${member.id}',this.value)"><option value="">Add to Team…</option>${allTeams.map(team=>`<option value="${team.id}">${v182Esc(team.name)}</option>`).join('')}</select>`:''}</div></div>`).join('')||'<div class="empty-help"><b>No Individuals match this Site and search.</b></div>'}</div></div><div class="cards v182-team-grid">${teams.map(team=>{let items=(db.items||[]).filter(item=>canViewItem(item)&&item.gang===team.id&&!db.sites.find(row=>row.id===item.site)?.archived&&(site==='all'||item.site===site));return `<div class="card gangcard"><div class="sectionhead"><h3 onclick="openGang('${team.id}')" class="clickable">${v182Esc(team.name)}</h3>${team.active?badge('Active'):badge('Not started')}</div><p>Assigned <b>${items.length}</b> · In progress <b>${items.filter(item=>overall(item)==='In progress').length}</b></p><p>People shown <b>${team.members.length}</b> · Issues <b>${items.reduce((sum,item)=>sum+issues(item),0)}</b></p><div class="team-members">${team.members.map(member=>`<div class="team-member clickable" onclick="openTeamMember('${team.id}','${member.id}')"><div><b>${v182Esc(member.name)}</b><span>${v182Esc(member.role||'Team member')}</span></div><b>›</b></div>`).join('')||'<div class="muted team-no-members">No Team members match this view.</div>'}</div><div class="team-actions"><button class="btn light smallbtn" onclick="openGang('${team.id}')">Open Team</button>${v177AnalyticsButton('team',team.id)}${hasPerm('manage_gangs')?`<button class="btn blue smallbtn" onclick="manageTeamMembers('${team.id}')">Manage members</button><button class="btn light smallbtn" onclick="editTeamName('${team.id}')">Edit name</button><button class="btn light smallbtn v176-team-danger" onclick="v177DeleteTeam('${team.id}')">Delete Team</button>`:''}</div></div>`}).join('')||smartEmpty('No Teams match this Site and search.','Clear the search or choose another Site.',hasPerm('manage_gangs')?'＋ Add Team':'',hasPerm('manage_gangs')?'addTeam()':'','♟')}</div>`
};

const v182BaseAdminPage=adminPage;
function v182AdminPeopleBody(){
  let query=adminSearch.trim().toLowerCase(),individuals=v176Individuals(),teams=v176ActiveTeams(),teamRows=teams.map(team=>({...team,members:(team.members||[]).filter(member=>v182MemberVisible(member,team)&&(!query||[member.name,member.role,team.name].join(' ').toLowerCase().includes(query)))})).filter(team=>{let assigned=(db.items||[]).some(item=>item.gang===team.id&&(site==='all'||item.site===site));return (site==='all'||assigned||team.members.length)&&(!query||team.name.toLowerCase().includes(query)||team.members.length)}),people=(individuals?.members||[]).filter(member=>v182MemberVisible(member,individuals)&&(!query||[member.name,member.role].join(' ').toLowerCase().includes(query))),total=people.length+teamRows.reduce((sum,team)=>sum+team.members.length,0);
  return `<div class="card admin-section v182-admin-people"><div class="sectionhead"><div><h2>Teams/Individuals</h2><div class="muted">Every person visible to this account · ${total} shown</div></div><div class="actions"><button class="btn blue" onclick="v176NewPerson()">＋ Add Individual</button><button class="btn orange" onclick="addGang()">＋ Add Team</button></div></div>${adminSearchBox('Search every person, trade or Team…',total,total)}<section><div class="v182-admin-group-head"><h3>Individuals</h3><span>${people.length}</span></div>${people.map(member=>`<button class="v182-admin-person" onclick="openTeamMember('${V182_INDIVIDUALS_ID}','${member.id}')"><span><b>${v182Esc(member.name)}</b><small>${v182Esc(member.role||'Individual')} · Not in a Team</small></span><strong>Open ›</strong></button>`).join('')||'<div class="admin-no-results">No Individuals match this view.</div>'}</section>${teamRows.map(team=>`<section><div class="v182-admin-group-head"><h3>${v182Esc(team.name)}</h3><div><span>${team.members.length}</span><button class="btn light smallbtn" onclick="editTeamName('${team.id}')">Edit Team</button></div></div>${team.members.map(member=>`<button class="v182-admin-person" onclick="openTeamMember('${team.id}','${member.id}')"><span><b>${v182Esc(member.name)}</b><small>${v182Esc(member.role||'Team member')}</small></span><strong>Open ›</strong></button>`).join('')||'<div class="admin-no-results">No members match this view.</div>'}</section>`).join('')}</div>`
}
adminPage=function(){
  let html=v182BaseAdminPage().replace(/>Teams<\/button>/g,'>Teams/Individuals</button>');if(adminTab!=='teams')return html;
  let marker='<div class="card admin-section">',index=html.indexOf(marker);return index>=0?html.slice(0,index)+v182AdminPeopleBody():html
};

// ---- Better attendance context ------------------------------------
function v182AttendanceSnapshot(siteId){
  let today=iso(),shifts=(db.attendanceShifts||[]).filter(row=>row.siteId===siteId&&!row.voided&&attendanceDateOnly(row.clockIn?.time,row.siteId)===today),active=shifts.filter(row=>!row.clockOut?.time),people=v182Unique(shifts.map(row=>row.workerId)),approved=shifts.reduce((sum,row)=>sum+Number(row.overtimeApproval?.status==='Approved'?row.overtimeApproval.minutes||0:0),0),pending=shifts.filter(row=>row.clockOut?.time&&row.overtimeApproval?.status==='Not Authorised').length,leave=(db.leaveRequests||[]).filter(row=>row.siteId===siteId&&row.status==='Approved'&&row.from<=today&&row.to>=today).length;
  return {today,shifts,active,people,approved,pending,leave}
}
const v182BaseAttendanceTodayView=attendanceTodayView;
attendanceTodayView=function(siteId){let row=v182AttendanceSnapshot(siteId),panel=`<div class="panel v182-live-context"><div><span>LIVE SITE SNAPSHOT</span><b>${row.active.length} currently on Site</b><small>${row.people.length} people recorded today · ${row.leave} on approved leave</small></div><div><span>SHIFTS TODAY</span><b>${row.shifts.length}</b><small>Clocked and management entries</small></div><div><span>AUTHORISED OT</span><b>${(row.approved/60).toFixed(2)} hrs</b><small>Explicitly approved only</small></div><div class="${row.pending?'warn':''}"><span>OT TO REVIEW</span><b>${row.pending}</b><small>Unpaid until decided</small></div></div>`;return panel+v182BaseAttendanceTodayView(siteId)};

const v182BaseTimesheetView=attendanceTimesheetView;
attendanceTimesheetView=function(siteId){
  v174EnsureTimesheetPeriod();let members=v174RangeValid(v174TimesheetFrom,v174TimesheetTo)?v174PayMembers(siteId,v174TimesheetFrom,v174TimesheetTo):[],totals=members.map(row=>v174AttendanceTotalsForRange(row.member.id,v174TimesheetFrom,v174TimesheetTo,siteId)),recorded=totals.reduce((sum,row)=>sum+row.totalHours,0),payable=totals.reduce((sum,row)=>sum+row.payableHours,0),ot=totals.reduce((sum,row)=>sum+row.authorisedOvertimeHours,0),excluded=totals.reduce((sum,row)=>sum+row.unauthorisedOvertimeHours,0);
  return `<div class="panel v182-period-summary"><div><span>PEOPLE</span><b>${members.length}</b></div><div><span>RECORDED</span><b>${recorded.toFixed(2)} hrs</b></div><div><span>PAYABLE</span><b>${payable.toFixed(2)} hrs</b></div><div><span>AUTHORISED OT</span><b>${ot.toFixed(2)} hrs</b></div><div class="${excluded?'warn':''}"><span>EXCLUDED LATE TIME</span><b>${excluded.toFixed(2)} hrs</b></div></div>${v182BaseTimesheetView(siteId)}`
};

const v182BaseLeavePage=contractorLeavePage;
contractorLeavePage=function(siteId){
  let rows=(db.leaveRequests||[]).filter(row=>row.siteId===siteId),today=iso(),approved=rows.filter(row=>row.status==='Approved'),pending=rows.filter(row=>row.status==='Pending'),upcoming=approved.filter(row=>row.to>=today),days=upcoming.reduce((sum,row)=>{let from=new Date(row.from+'T12:00:00'),to=new Date(row.to+'T12:00:00');return sum+((to-from)/864e5+1)*Number(row.dayUnit||1)},0);
  return `<div class="panel v182-period-summary v182-leave-summary"><div><span>APPROVED</span><b>${approved.length}</b></div><div class="${pending.length?'warn':''}"><span>AWAITING DECISION</span><b>${pending.length}</b></div><div><span>UPCOMING</span><b>${upcoming.length}</b></div><div><span>PLANNED DAYS</span><b>${days.toFixed(1)}</b></div></div>${v182BaseLeavePage(siteId)}`
};

const v182BaseContractorPayReview=contractorPayReview;
contractorPayReview=function(siteId){
  let html=v182BaseContractorPayReview(siteId);if(!hasPerm('view_financials'))return html;v174EnsurePayPeriod();let members=v174PayMembers(siteId,v174PayFrom,v174PayTo),totals=members.map(row=>v174AttendanceTotalsForRange(row.member.id,v174PayFrom,v174PayTo,siteId)),hours=totals.reduce((sum,row)=>sum+row.payableHours,0),ot=totals.reduce((sum,row)=>sum+row.authorisedOvertimeHours,0),approvals=(db.timesheetApprovals||[]).filter(row=>row.siteId===siteId&&!row.superseded&&row.weekStart>=v174PayFrom&&row.weekStart<=v174PayTo).length;
  let panel=`<div class="panel v182-pay-context"><div><span>PERIOD WORKFORCE</span><b>${members.length} people</b></div><div><span>PAYABLE HOURS</span><b>${hours.toFixed(2)}</b></div><div><span>AUTHORISED OT</span><b>${ot.toFixed(2)}</b></div><div><span>APPROVED TIMESHEETS</span><b>${approvals}</b></div></div>${currentUser()?.role==='Accounts'?'<div class="panel v182-accounts-boundary"><b>Accounts workspace</b><span>Payment, hours, CIS and payroll records are available. Private HR, user security, Director settings and confidential personnel records are not included.</span></div>':''}`;
  return panel+html
};

// Workforce analytics now counts unified Daywork Work Items.
const v182BaseWorkforceAnalyticsRows=v177WorkforceAnalyticsRows;
v177WorkforceAnalyticsRows=function(kind,id,from,to){let data=v182BaseWorkforceAnalyticsRows(kind,id,from,to),dayworkItems=(db.items||[]).filter(item=>item.type==='Daywork'&&v174DateInRange(item.daywork?.date||item.lastUpdated,from,to)&&(kind==='team'?item.assignedType==='Team'&&item.assignedId===id:item.assignedType==='Individual'&&item.assignedId===id));data.dayworks=dayworkItems;return data};

// ---- DOM progressive disclosure for remaining large record lists --
function v182CollapseContainer(selector,childSelector,limit=V182_LIST_LIMIT,grid=false){
  document.querySelectorAll(selector).forEach(parent=>{if(parent.closest('.v182-expander'))return;let children=[...parent.children].filter(child=>child.matches(childSelector));if(children.length<=limit)return;let details=document.createElement('details');details.className='v182-expander v182-dom-expander';let summary=document.createElement('summary');summary.innerHTML=`<span>View all ${children.length} records</span><b>${children.length-limit} more ▼</b>`;let holder=document.createElement('div');holder.className=grid?'v182-dom-grid':'v182-expanded-list';children.slice(limit).forEach(child=>holder.appendChild(child));details.append(summary,holder);parent.appendChild(details)})
}
function v182CollapseTables(){
  document.querySelectorAll('.payment-history-table tbody,.att-timesheet-table tbody').forEach(body=>{let rows=[...body.children].filter(row=>row.tagName==='TR'&&!row.classList.contains('payment-total-row'));if(rows.length<=V182_LIST_LIMIT)return;rows.slice(V182_LIST_LIMIT).forEach(row=>row.hidden=true);let wrap=body.closest('table')?.parentElement;if(!wrap||wrap.nextElementSibling?.classList.contains('v182-table-toggle'))return;let button=document.createElement('button');button.className='btn light v182-table-toggle';button.textContent=`View all ${rows.length} records ▼`;button.setAttribute('aria-expanded','false');button.onclick=()=>{let open=button.getAttribute('aria-expanded')==='true';rows.slice(V182_LIST_LIMIT).forEach(row=>row.hidden=open);button.setAttribute('aria-expanded',String(!open));button.textContent=open?`View all ${rows.length} records ▼`:'Show fewer records ▲'};wrap.after(button)})
}
function v182ProgressiveDisclosure(){
  v182CollapseContainer('.site-drawings-grid','.site-drawing-card',V182_LIST_LIMIT,true);v182CollapseContainer('.issue-overview','.issue-overview-row');v182CollapseContainer('.diary-history-list','.diary-history-row');v182CollapseContainer('.audit-list','.audit-row');v182CollapseContainer('.v174-agenda-list','.v174-agenda-row');v182CollapseContainer('.v179-payee-list','.v179-payee',V182_LIST_LIMIT,true);v182CollapseContainer('.notification-list','.notification-item');v182CollapseContainer('.v182-team-grid','.gangcard',V182_TODAY_LIMIT,true);v182CollapseContainer('.v182-admin-people','section',V182_TODAY_LIMIT);v182CollapseContainer('.admin-section','.usercard');v182CollapseContainer('.admin-work-list','.admin-work-row');v182CollapseContainer('.archive-list','.archive-row');v182CollapseTables()
}

// ---- Portfolio, migration and current shell ------------------------
v181SiteRow=function(siteRow){
  let all=(db.items||[]).filter(item=>item.site===siteRow.id),plots=all.filter(item=>item.type==='House Plot'),openIssues=all.reduce((sum,item)=>sum+(item.issues||[]).filter(issue=>issue.status==='Open').length,0),inProgress=all.filter(item=>overall(item)==='In progress').length,signed=plots.filter(item=>overall(item)==='Signed Off').length,complete=plots.filter(item=>overall(item)==='Complete').length,progress=plots.length?Math.round(plots.reduce((sum,item)=>sum+itemProgress(item),0)/plots.length):0;
  return {site:siteRow,items:plots,workItems:all,openIssues,inProgress,signed,complete,people:v181SitePeople(siteRow.id),progress}
};

function v182EnsureUserAccess(user){
  let changed=false;user.permissions=user.permissions||{};
  if(user.role!=='Operator'&&!user.permissions.dashboard){user.permissions.dashboard=true;changed=true}
  if(user.role==='Accounts')for(let key of ['view_financials','record_payments','manage_payments','lock_payroll'])if(!user.permissions[key]){user.permissions[key]=true;changed=true}
  if(user.role==='Accounts')for(let key of ['manage_users','reset_passwords','manage_gangs','manage_sites','audit','view_archives'])if(user.permissions[key]){user.permissions[key]=false;changed=true}
  if(user.role==='Quantity Surveyor'&&!user.permissions.view_attendance){user.permissions.view_attendance=true;changed=true}
  if(Number(user.permissionTemplateVersion||0)<182){user.permissionTemplateVersion=182;changed=true}return changed
}
const v182BaseMigrate=migrate;
migrate=function(persist=true){
  v182BaseMigrate(false);let changed=false,existing=new Set((db.items||[]).map(item=>item.id));
  (db.dayworks||[]).forEach((row,index)=>{let item=v182DayworkItem(row,index);if(!existing.has(item.id)){db.items.push(item);existing.add(item.id);changed=true}});
  if((db.dayworks||[]).length){db.dayworks=[];changed=true}(db.users||[]).forEach(user=>{if(v182EnsureUserAccess(user))changed=true});
  if(persist&&(changed||persist))save()
};
if((db.users||[]).length)migrate(false);

const v182BaseSaveNewUser=saveNewUser;
saveNewUser=function(...args){let before=new Set((db.users||[]).map(user=>user.id)),result=v182BaseSaveNewUser(...args),user=(db.users||[]).find(row=>!before.has(row.id));if(user&&v182EnsureUserAccess(user))save();return result};
const v182BaseSaveUserAccess=saveUserAccess;
saveUserAccess=function(id,...args){let result=v182BaseSaveUserAccess(id,...args),user=(db.users||[]).find(row=>row.id===id);if(user&&v182EnsureUserAccess(user))save();return result};

const v182BaseLoginScreen=loginScreen;
loginScreen=function(){v182BaseLoginScreen();let copy=document.querySelector?.('.auth-brand-copy');if(copy)copy.innerHTML='<span class="auth-eyebrow">SITESIGHT</span><h1>Every development, Team and commercial record in one clear place.</h1><p>Progress, attendance, Daywork, drawings, issues, payments and reporting designed for UK brickwork contractors.</p>';document.querySelector?.('.demo-login-box')?.remove()};

const v182BaseShell=shell;
shell=function(content){v182BaseShell(content);let root=document.getElementById('root');if(root)root.innerHTML=root.innerHTML.replace(/v1\.81\.0 · ENTERPRISE/g,'v1.82.0 · OPERATIONAL POLISH')};

const v182BaseRender=render;
render=function(){let result=v182BaseRender();setTimeout(v182ProgressiveDisclosure,0);return result};

/* ===== v183-individual-workforce.js ===== */
'use strict';

// SiteSight V1.83 — individual workforce, calmer dashboards and reliable take-off.

const V183_INDIVIDUALS_ID='v176-individuals';
let v183PaymentRole='all';

function v183Esc(value){return String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]))}
function v183Members(){return (v176Individuals()?.members||[])}
function v183Member(id){return v183Members().find(member=>member.id===id)||null}
function v183MemberUser(member){return member&&(db.users||[]).find(user=>user.id===member.linkedUserId||user.linkedMemberId===member.id)||null}
function v183PersonName(id,fallback='To be Confirmed'){return v183Member(id)?.name||paymentPayeeNameFromId(id)||fallback}
function v183AssigneeName(item){return item?.assignedName||v183PersonName(item?.assignedId||item?.paidTo||'','To be Confirmed')}
function v183ActiveMembers(){return v183Members().filter(member=>member.activeForAttendance!==false&&member.employmentStatus!=='Leaver')}
function v183PersonOptions(selected='',prefix=''){
  return `<option value="">To be Confirmed</option>${v183ActiveMembers().slice().sort((a,b)=>a.name.localeCompare(b.name)).map(member=>`<option value="${prefix}${member.id}" ${selected===member.id||selected===prefix+member.id?'selected':''}>${v183Esc(member.name)}${member.role?' · '+v183Esc(member.role):''}</option>`).join('')}`
}

// ---- Individual-only data migration --------------------------------
function v183Hash(value){let hash=0;for(let char of String(value||''))hash=(Math.imul(hash,31)+char.charCodeAt(0))>>>0;return hash}
function v183PickMember(groupMap,groupId,key=''){
  let members=groupMap.get(groupId)||[];return members.length?members[v183Hash(key)%members.length]:null
}
function v183AllocateMoney(total,index,count){let pennies=Math.round(Number(total||0)*100),base=Math.floor(pennies/count),remainder=pennies-base*count;return (base+(index<remainder?1:0))/100}
function v183SplitPayment(payment,members){
  if(!payment||!members.length)return payment?[payment]:[];
  return members.map((member,index)=>{
    let row={...payment,id:`${payment.id||'payment'}-individual-${member.id}`,paidTo:member.id,payeeName:'',memberBreakdown:undefined,formerGroupPaymentId:payment.id||''},ratio=Number(payment.amount||0)?v183AllocateMoney(payment.amount,index,members.length)/Number(payment.amount):1/members.length;
    row.amount=v183AllocateMoney(payment.amount,index,members.length);row.pct=Math.round(Number(payment.pct||0)/members.length*100)/100;
    ['cisGrossExVat','cisVat','cisMaterials','cisLiable','cisDeduction','cisNet'].forEach(key=>{if(payment[key]!==undefined)row[key]=Math.round(Number(payment[key]||0)*ratio*100)/100});
    return row
  })
}
function v183MigratePayments(rows,groupMap,fallbackMember){
  return (rows||[]).flatMap(payment=>{
    if(v183Member(payment.paidTo)||payment.payeeName)return [payment];
    let members=groupMap.get(payment.paidTo)||[];if(!members.length&&fallbackMember)members=[fallbackMember];return members.length?v183SplitPayment(payment,members):[payment]
  })
}
function v183DemoUtr(member){return String(1200000000+(v183Hash(member.id)%799999999)).slice(0,10)}
function v183IndividualMigration(){
  if(db.v183IndividualWorkforceMigrated)return false;
  let groups=(db.gangs||[]).slice(),groupMap=new Map(groups.map(group=>[group.id,(group.members||[]).slice()])),all=[],seen=new Set();
  groups.forEach(group=>(group.members||[]).forEach(member=>{if(!seen.has(member.id)){seen.add(member.id);all.push(member)}}));
  let individuals=groups.find(group=>group.id===V183_INDIVIDUALS_ID)||{id:V183_INDIVIDUALS_ID,name:'Individuals',active:true,isIndividuals:true,contact:'',members:[]};individuals.name='Individuals';individuals.isIndividuals=true;individuals.active=true;individuals.members=all;db.gangs=[individuals];
  (db.items||[]).forEach(item=>{
    let member=v183Member(item.assignedId)||v183PickMember(groupMap,item.gang,item.id);item.gang=V183_INDIVIDUALS_ID;item.assignedType='Individual';item.assignedId=member?.id||'';item.assignedName=member?.name||'To be Confirmed';
    let migrateTarget=target=>{if(!target)return;let paid=v183Member(target.paidTo)||v183PickMember(groupMap,target.paidTo,item.id)||member;target.paidTo=paid?.id||'';target.payments=v183MigratePayments(target.payments,groupMap,paid)};
    if(item.type==='House Plot')(item.lifts||[]).forEach(migrateTarget);else migrateTarget(item);
    (item.variations||[]).forEach(variation=>{let paid=v183Member(variation.team)||v183PickMember(groupMap,variation.team,item.id)||member;variation.team=paid?.id||'';variation.payments=v183MigratePayments(variation.payments,groupMap,paid)});
    (item.issues||[]).forEach(issue=>{if(String(issue.assignedTo||'').startsWith('gang:')){let person=v183PickMember(groupMap,String(issue.assignedTo).slice(5),issue.id);issue.assignedTo=person?'person:'+person.id:''}})
  });
  (db.attendanceShifts||[]).forEach(row=>row.gangId=V183_INDIVIDUALS_ID);(db.attendanceAttempts||[]).forEach(row=>{if(row.gangId)row.gangId=V183_INDIVIDUALS_ID});
  (db.peopleArchive||[]).forEach(row=>{row.formerGangId=V183_INDIVIDUALS_ID;row.formerGangName='Individuals';delete row.formerTeamId;delete row.formerTeamName});db.teamArchive=[];
  (db.siteDiary||[]).forEach(entry=>{let people=String(entry.people||entry.teams||'');groups.filter(group=>group.id!==V183_INDIVIDUALS_ID).forEach(group=>{if(people.includes(group.name))people=people.replaceAll(group.name,(group.members||[]).map(member=>member.name).join(', '))});entry.people=people;entry.teams=people});
  let oldProfiles=(db.cisProfiles||[]),individualProfiles=oldProfiles.filter(profile=>v183Member(String(profile.payeeKey||'').replace(/^id:/,'')));
  oldProfiles.filter(profile=>groupMap.has(String(profile.payeeKey||'').replace(/^id:/,''))&&String(profile.payeeKey||'').replace(/^id:/,'')!==V183_INDIVIDUALS_ID).forEach(profile=>{
    let oldId=String(profile.payeeKey||'').replace(/^id:/,''),demo=String(profile.notes||'').toLowerCase().includes('fictional');(groupMap.get(oldId)||[]).forEach(member=>{if(individualProfiles.some(row=>row.payeeKey==='id:'+member.id))return;individualProfiles.push({...profile,id:`cis-${member.id}`,payeeKey:'id:'+member.id,name:member.name,businessType:member.role||'Individual',utr:demo?v183DemoUtr(member):'',verificationNumber:demo?`V-DEMO-${String(v183Hash(member.id)).slice(0,7)}`:'',actualPayeeConfirmed:demo,subcontractorStatusConfirmed:demo,notes:demo?'Fictional individual CIS demonstration profile.':'Migrated from a former group record. Verify this person before CIS use.'})})
  });db.cisProfiles=individualProfiles;
  (db.users||[]).forEach(user=>{if(Number(user.permissionTemplateVersion||0)<183)user.permissionTemplateVersion=183});db.v183IndividualWorkforceMigrated=true;return true
}

const v183BaseMigrate=migrate;
migrate=function(persist=true){v183BaseMigrate(false);let changed=v183IndividualMigration();if(persist&&(changed||persist))save()};
if((db.users||[]).length)migrate(false);

// ---- Navigation and low-friction page order -------------------------
V173_MAIN_NAV.gangs={icon:'♙',label:'Individuals'};
V173_MORE_TOOLS.gangs=['♙','Individuals','People records, working details and linked logins'];
V173_MORE_TOOLS.attendance=['◷','People & Time','Clocking, hours, leave and timesheets'];
V173_MORE_TOOLS.diary=['✎','Site Diary','Daily Site record'];
V173_MORE_TOOLS['weekly-report']=['▤','Site Report','Weekly by default, with any custom date range'];
const v183BaseRoleNavItems=v173RoleNavItems;
v173RoleNavItems=function(user=currentUser()){return v183BaseRoleNavItems(user).filter(item=>!['attendance','diary'].includes(item.id))};
const v183BaseMoreToolIds=v173MoreToolIds;
v173MoreToolIds=function(user=currentUser()){
  let ids=v183BaseMoreToolIds(user),extra=['attendance','diary'].filter(id=>v173CanOpenView(id,user));return [...new Set([...extra,...ids])]
};
v180StartPanel=function(){return''};v180InsertStart=function(html){return html};dashboardRoleIntro=function(){return''};

// ---- Dashboard presentation ----------------------------------------
const v183BaseSection=section;
section=function(title,arr,cls){
  if(!['In Progress','Complete','Signed Off'].includes(title))return v183BaseSection(title,arr,cls);
  let filter=title==='In Progress'?'inprogress':title==='Complete'?'complete':'signed',open=title==='In Progress'?' open':'';
  return `<details class="section v183-status-section ${cls||''}"${open}><summary><span>${icon(title)} ${title} <b class="pill">${arr.length}</b></span><button class="viewall" onclick="event.preventDefault();event.stopPropagation();openDashboardGroup('${filter}')">View all ›</button></summary><div class="v183-status-content">${arr.length?`<div class="cards">${arr.slice(0,10).map(card).join('')}</div>`:smartEmpty(`No ${title.toLowerCase()} work in this view.`,'This section updates automatically as work progresses.','','','✓')}</div></details>`
};

const v183BaseCard=card;
card=function(item){let html=v183BaseCard(item),legacy=gangName(item.gang),name=v183AssigneeName(item);html=html.replace(`<span>♟ ${legacy}</span>`,`<span>♙ ${v183Esc(name)}</span>`);html=html.replace(/<span class="programme-pill late">[\s\S]*?<\/span>/g,'');return html};

side=function(){
  let scoped=scopeItems(),recent=hasPerm('view_issues')?scoped.flatMap(item=>(item.issues||[]).map(issue=>({...issue,ref:item.ref,site:item.site,itemId:item.id}))).filter(issue=>issue.status==='Open').slice(0,4):[],counts=new Map();scoped.forEach(item=>{if(item.assignedId)counts.set(item.assignedId,(counts.get(item.assignedId)||0)+1)});let people=[...counts].map(([id,count])=>({member:v183Member(id),count})).filter(row=>row.member).sort((a,b)=>b.count-a.count||a.member.name.localeCompare(b.member.name)).slice(0,8);
  return `<div class="sidecol">${hasPerm('view_issues')?`<div class="panel sidepanel"><div class="sectionhead"><h3>Recent Issues</h3><button class="viewall" onclick="view='issues';selected=null;render()">View all</button></div><div class="list">${recent.map(issue=>`<div class="listrow clickable-listrow" onclick="openItem('${issue.itemId}')"><span><b>${v183Esc(issue.title)}</b><br><span class="muted">${v183Esc(issue.ref)} · ${v183Esc(siteName(issue.site))}</span></span><span class="badge b-red">1</span></div>`).join('')||'<div class="empty-help"><b>No open issues.</b></div>'}</div></div>`:''}<div class="panel sidepanel"><div class="sectionhead"><h3>Assigned Individuals</h3><button class="viewall" onclick="view='gangs';selected=null;render()">View all</button></div><div class="list">${people.map(row=>`<div class="listrow" onclick="openTeamMember('${V183_INDIVIDUALS_ID}','${row.member.id}')"><span><b>${v183Esc(row.member.name)}</b><small>${v183Esc(row.member.role||'Individual')}</small></span><span class="badge b-blue">${row.count}</span></div>`).join('')||'<div class="empty-help"><b>No assigned individuals.</b></div>'}</div></div></div>`
};

// ---- Work Items belong to named individuals ------------------------
filtered=function(){return db.items.filter(item=>canViewItem(item)&&(site==='all'||item.site===site)&&(gang==='all'||item.assignedId===gang)&&(type==='all'||item.type===type)&&(status==='all'||overall(item)===status)&&(signed==='all'||String(item.signed)===signed)&&(!q||String(item.ref||'').toLowerCase().includes(q.toLowerCase())))};
filters=function(showSite=true){
  let advanced=(gang!=='all'?1:0)+(type!=='all'?1:0)+(status!=='all'?1:0)+(signed!=='all'?1:0),expanded=workFiltersExpanded||advanced>0;
  return `<div class="filterbar simple-work-filterbar"><div class="searchwrap"><input class="input search" placeholder="Search plot / work item…" value="${v183Esc(q)}" oninput="q=this.value;searchSuggestOpen=!!q;searchReference(this)">${searchSuggestOpen&&q?`<div class="search-suggestions">${searchSuggestions(q).map(row=>`<div class="search-suggestion" onclick="selectSearchSuggestion('${row.id}')"><b>${v183Esc(row.ref)} · ${v183Esc(row.type)}</b><div class="muted">${v183Esc(siteName(row.site))} · ${v183Esc(v183AssigneeName(row))}</div></div>`).join('')||'<div class="search-suggestion muted">No matches</div>'}</div>`:''}</div>${showSite?`<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(row=>`<option value="${row.id}" ${site===row.id?'selected':''}>${v183Esc(row.name)}</option>`).join('')}</select>`:''}<button class="btn light more-filter-button" onclick="workFiltersExpanded=!workFiltersExpanded;render()">More filters${advanced?` (${advanced})`:''} ${expanded?'▲':'▼'}</button><div class="work-filter-extra ${expanded?'show':''}"><select class="select" onchange="gang=this.value;render()"><option value="all">All individuals</option>${v183ActiveMembers().map(member=>`<option value="${member.id}" ${gang===member.id?'selected':''}>${v183Esc(member.name)}</option>`).join('')}</select><select class="select" onchange="type=this.value;render()"><option value="all">All types</option>${workTypes().map(value=>`<option ${type===value?'selected':''}>${v183Esc(value)}</option>`).join('')}</select><select class="select" onchange="status=this.value;render()"><option value="all">All status</option>${['Not started','In progress','Complete','Signed Off'].map(value=>`<option ${status===value?'selected':''}>${value}</option>`).join('')}</select><button class="btn light" onclick="resetWorkFilters()">Clear filters</button></div></div>`
};
filterSummary=function(){let bits=[];if(site!=='all')bits.push(`Site: ${siteName(site)}`);if(gang!=='all')bits.push(`Individual: ${v183PersonName(gang)}`);if(type!=='all')bits.push(`Type: ${type}`);if(status!=='all')bits.push(`Status: ${status}`);if(signed!=='all')bits.push(`Signed: ${signed}`);if(q)bits.push(`Search: ${q}`);return bits.length?`<div class="filter-summary">${bits.map(value=>`<span class="filter-chip">${v183Esc(value)}</span>`).join('')}<button class="show-everything" onclick="showEverything()">Show Everything</button></div>`:''};
searchSuggestions=function(term){let value=String(term||'').trim().toLowerCase();if(!value)return[];return db.items.filter(item=>canViewItem(item)&&[item.ref,item.type,item.houseType,siteName(item.site),v183AssigneeName(item)].join(' ').toLowerCase().includes(value)).slice(0,8)};

function v183ToggleNewItemFields(){let selectedType=document.getElementById('nt')?.value||'',daywork=selectedType==='Daywork',house=selectedType==='House Plot',dayworkFields=document.getElementById('v182-daywork-fields'),houseField=document.getElementById('v182-house-field'),custom=document.getElementById('custom-type-box');if(dayworkFields)dayworkFields.hidden=!daywork;if(houseField)houseField.hidden=!house;if(custom)custom.style.display=selectedType==='__custom__'?'block':'none'}
newItem=function(forced=''){
  if(!hasPerm('manage_workitems'))return toast('You do not have work-item creation access');let types=workTypes(),chosen=forced&&types.includes(forced)?forced:types[0],custom=!!forced&&!types.includes(forced),daywork=chosen==='Daywork';
  modal=`<div class="modal"><div class="modalbox v182-new-work"><span class="att-kicker">ONE WORK LIST</span><h2>Create Work Item</h2><p class="muted">Assign every item directly to the person responsible. Daywork stays in this same list.</p><div class="two"><div class="field"><label>Type</label><select id="nt" class="select" onchange="v183ToggleNewItemFields()">${types.map(value=>`<option value="${v183Esc(value)}" ${chosen===value?'selected':''}>${v183Esc(value)}</option>`).join('')}<option value="__custom__" ${custom?'selected':''}>＋ Custom type...</option></select><div id="custom-type-box" class="custom-type-wrap" style="display:${custom?'block':'none'}"><label>Custom type</label><input id="nt-custom" class="input" value="${custom?v183Esc(forced):''}"></div></div><div class="field"><label>Reference</label><input id="nr" class="input" placeholder="${daywork?'e.g. DW-042 · Plot 118':'e.g. Plot 118'}"></div></div><div class="two"><div class="field"><label>Site</label><select id="ns" class="select">${activeSites().map(row=>`<option value="${row.id}" ${site===row.id?'selected':''}>${v183Esc(row.name)}</option>`).join('')}</select></div><div class="field"><label>Assign to individual</label><select id="ng" class="select">${v183PersonOptions()}</select></div></div><div id="v182-house-field" class="field" ${chosen==='House Plot'?'':'hidden'}><label>House type</label><input id="nh" class="input"></div><div id="v182-daywork-fields" ${daywork?'':'hidden'}><div class="field"><label>Work instructed</label><textarea id="v182-dw-work" class="textarea"></textarea></div><div class="three"><div class="field"><label>Work date</label><input id="v182-dw-date" class="input" type="date" value="${iso()}"></div><div class="field"><label>Instructed by</label><input id="v182-dw-by" class="input"></div><div class="field"><label>Instruction reference</label><input id="v182-dw-ref" class="input"></div></div><div class="two"><div class="field"><label>Labour hours</label><input id="v182-dw-hours" class="input" type="number" min="0" step="0.5" value="8"></div>${hasPerm('view_financials')?'<div class="field"><label>Estimated value</label><input id="v182-dw-value" class="input" type="number" min="0" step="0.01"></div>':''}</div></div><button class="btn orange" onclick="createItem()">Create work item</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
};
createItem=function(){
  let selectedType=document.getElementById('nt')?.value||'',workType=selectedType==='__custom__'?(document.getElementById('nt-custom')?.value||'').trim():selectedType,ref=(document.getElementById('nr')?.value||'').trim(),siteId=document.getElementById('ns')?.value||'',personId=document.getElementById('ng')?.value||'',person=v183Member(personId),now=new Date().toISOString();
  if(!workType||!ref||!siteId)return toast('Type, reference and Site are required');if(db.items.some(item=>item.site===siteId&&String(item.ref||'').trim().toLowerCase()===ref.toLowerCase()))return toast('That Work Item reference already exists on this Site');if(workType==='Daywork'&&!(document.getElementById('v182-dw-work')?.value||'').trim())return toast('Describe the instructed Daywork');
  let item={id:(workType==='Daywork'?'dw-item-':'i')+Date.now(),type:workType,ref,houseType:(document.getElementById('nh')?.value||'').trim(),site:siteId,gang:V183_INDIVIDUALS_ID,assignedType:'Individual',assignedId:personId,assignedName:person?.name||'To be Confirmed',notes:'',signed:false,drawings:[],workPhotos:[],issues:[],variations:[],lastUpdated:iso(),lastUpdatedAt:now,lastUpdatedBy:currentUser()?.name||'',createdAt:now,createdBy:currentUser()?.name||''};
  if(workType==='House Plot')item.lifts=(db.sites.find(row=>row.id===siteId)?.stageTemplate||['1st Lift','2nd Lift (Joist)','3rd Lift (Band)','4th Lift (Top/Plate)','5th Lift (Gables)']).map(name=>({name,status:'Not started',price:0,pct:0,paidTo:personId,date:'',paymentNotes:'',variation:'',notes:'',completedDate:'',payments:[]}));else Object.assign(item,{status:workType==='Daywork'?'Started':'Not started',price:Number(document.getElementById('v182-dw-value')?.value||0),pct:0,paidTo:personId,date:'',paymentNotes:'',completedDate:'',payments:[]});
  if(workType==='Daywork')item.daywork={date:document.getElementById('v182-dw-date')?.value||iso(),work:(document.getElementById('v182-dw-work')?.value||'').trim(),instructedBy:(document.getElementById('v182-dw-by')?.value||'').trim(),instructionRef:(document.getElementById('v182-dw-ref')?.value||'').trim(),labourHours:Number(document.getElementById('v182-dw-hours')?.value||0)};
  db.items.push(item);save();audit('create','work item',item.id,`${item.ref} created · assigned to ${item.assignedName}`,item.site);modal=null;selected=item.id;selectedGang=null;savedState('Work item created');render()
};

const v183BaseDetail=detail;
detail=function(item){let html=v183BaseDetail(item),legacy=gangName(item.gang),name=v183Esc(v183AssigneeName(item));return html.replace(`Team: ${legacy}`,`Individual: ${name}`).replace('♟ Edit team','♙ Edit individual')};
editWorkItemTeam=function(){let item=db.items.find(row=>row.id===selected);if(!item)return;if(!hasPerm('manage_workitems')&&!hasPerm('manage_gangs'))return toast('You do not have permission to change the assigned individual');modal=`<div class="modal"><div class="modalbox"><h2>Edit Assigned Individual</h2><p class="muted">${v183Esc(item.ref)} · ${v183Esc(siteName(item.site))}</p><div class="field"><label>Assigned individual</label><select id="wit-team" class="select">${v183PersonOptions(item.assignedId)}</select></div><button class="btn orange" onclick="saveWorkItemTeam()">Save changes</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()};
saveWorkItemTeam=function(){let item=db.items.find(row=>row.id===selected);if(!item)return;let old=item.assignedId||'',next=document.getElementById('wit-team')?.value||'',person=v183Member(next);item.gang=V183_INDIVIDUALS_ID;item.assignedType='Individual';item.assignedId=next;item.assignedName=person?.name||'To be Confirmed';if(item.type==='House Plot')(item.lifts||[]).forEach(lift=>{if(!lift.paidTo||lift.paidTo===old)lift.paidTo=next});else if(!item.paidTo||item.paidTo===old)item.paidTo=next;touch(item);save();emitNotification('work',item.site,`${item.ref}: assigned individual changed to ${item.assignedName}`,item.id);modal=null;savedState('Assigned individual updated');render()};

const v183BaseAddVariation=addVariation;
addVariation=function(){v183BaseAddVariation();if(!modal)return;let item=db.items.find(row=>row.id===selected);modal=modal.replace('<label>Team</label>','<label>Individual</label>').replace(/<select id="var-team"[\s\S]*?<\/select>/,`<select id="var-team" class="select">${v183PersonOptions(item?.assignedId||'')}</select>`);render()};
issueAssigneeOptions=function(value=''){let options=[['','Unassigned'],...v183ActiveMembers().map(member=>['person:'+member.id,'Individual · '+member.name]),...(db.users||[]).map(user=>['user:'+user.id,'User · '+user.name])];return options.map(([key,label])=>`<option value="${v183Esc(key)}" ${value===key?'selected':''}>${v183Esc(label)}</option>`).join('')};
const v183BaseIssueAssigneeLabel=issueAssigneeLabel;
issueAssigneeLabel=function(value){let [kind,id]=String(value||'').split(':');return kind==='person'?v183PersonName(id):v183BaseIssueAssigneeLabel(value)};

// ---- Compact Individuals and user linking --------------------------
v176ActiveTeams=function(){return[]};
v176NewPerson=function(){if(!hasPerm('manage_gangs'))return toast('You do not have people administration access');modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">PEOPLE</span><h2>Add an Individual</h2><p class="muted">Create the person once, then link a SiteSight login and set their hours or pay when permitted.</p><div class="two"><div class="field"><label>Full name</label><input id="v176-person-name" class="input" placeholder="Full name"></div><div class="field"><label>Trade / role</label><input id="v176-person-role" class="input" placeholder="e.g. Bricklayer, Labourer"></div></div><button class="btn orange" onclick="v176SaveNewPerson()">Create person</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()};
v176SaveNewPerson=function(){if(!hasPerm('manage_gangs'))return;let name=(document.getElementById('v176-person-name')?.value||'').trim(),memberRole=(document.getElementById('v176-person-role')?.value||'').trim();if(!name)return toast('Enter the person’s full name');let group=v176Individuals(true),member={id:v176Id('tm'),name,role:memberRole,notes:'',photos:[],employmentType:'Not tracked',hourlyRate:0,overtimeRate:0,dayRate:0,basePay:0,standardDayHours:8,standardWeekHours:40,defaultBreakMinutes:30,linkedUserId:'',activeForAttendance:true,employmentStatus:'Active',payRateHistory:[]};group.name='Individuals';group.members=group.members||[];group.members.push(member);save();audit('people','person',member.id,`${name} added`,'');modal=null;savedState('Individual created');openTeamMember(group.id,member.id)};
v176WorkforceCard=function(groupId,member){let linked=v183MemberUser(member),canHours=v176CanEditHours(),canPay=v176CanEditPay(),canPeople=hasPerm('manage_gangs'),canUsers=hasPerm('manage_users'),editLabel=canHours&&canPay?'Edit working hours & base pay':canHours?'Edit working hours':canPay?'Edit base pay':'';if(!canHours&&!canPay&&!canPeople&&!canUsers)return'';let link=canUsers?`<div class="v176-link-row"><div class="field"><label>Linked SiteSight user</label><select id="v176-link-user" class="select">${v176LinkChoices(member)}</select></div><button class="btn blue" onclick="v176SaveUserLink('${V183_INDIVIDUALS_ID}','${member.id}')">Save link</button></div>${!linked?`<button class="btn light" onclick="v176CreateUserForMember('${V183_INDIVIDUALS_ID}','${member.id}')">＋ Create a new linked login</button>`:''}`:linked?`<div class="muted">Linked login: ${v183Esc(linked.name)} · ${v183Esc(linked.role)}</div>`:'';return `<div class="v176-workforce-card"><h3>Individual controls</h3>${link}<div class="actions" style="margin-top:10px">${editLabel?`<button class="btn light" onclick="v176OpenWorkforceEditor('${V183_INDIVIDUALS_ID}','${member.id}')">${editLabel}</button>`:''}${canPeople?`<button class="btn light v176-team-danger" onclick="v176DeletePerson('${member.id}')">Remove individual</button>`:''}</div>${canPay?`<div class="v176-pay-history">${(member.payRateHistory||[]).length?`${member.payRateHistory.length} previous pay change${member.payRateHistory.length===1?'':'s'} retained`:'No previous pay changes recorded'} · Current: ${v183Esc(member.employmentType||'Not tracked')}</div>`:''}</div>`};

gangsPage=function(){
  if(!hasPerm('gangs'))return '<div class="panel empty-help"><b>You do not have permission to view Individuals.</b></div>';let query=String(v177PeopleQuery||'').trim().toLowerCase(),people=v183ActiveMembers().filter(member=>v182MemberVisible(member,v176Individuals())&&(!query||[member.name,member.role,v183MemberUser(member)?.role].join(' ').toLowerCase().includes(query)));
  return `<div class="topbar"><div><h1>Individuals</h1><div class="sub">Find a person, open their record or link their SiteSight login</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(row=>`<option value="${row.id}" ${site===row.id?'selected':''}>${v183Esc(row.name)}</option>`).join('')}</select>${hasPerm('manage_gangs')?'<button class="btn light" onclick="goNav(\'workforce-archive\')">Former people</button><button class="btn orange" onclick="v176NewPerson()">＋ Add Individual</button>':''}</div></div>${v177PeopleSearchPanel(people.length)}<div class="panel v183-people-panel"><div class="sectionhead"><div><h2>People</h2><div class="muted">${people.length} individual${people.length===1?'':'s'} shown</div></div></div><div class="v183-people-grid">${people.map(member=>{let user=v183MemberUser(member);return `<button class="v183-person-card" onclick="openTeamMember('${V183_INDIVIDUALS_ID}','${member.id}')"><span class="v183-avatar">${v183Esc(member.name.split(/\s+/).slice(0,2).map(part=>part[0]).join('').toUpperCase())}</span><span><b>${v183Esc(member.name)}</b><small>${v183Esc(member.role||'Individual')} · ${user?`${v183Esc(user.role)} login`:'No login linked'}</small></span><strong>Open ›</strong></button>`}).join('')||'<div class="empty-help"><b>No individuals match this search.</b></div>'}</div></div>`
};

function v183AdminIndividuals(){let query=adminSearch.trim().toLowerCase(),people=v183ActiveMembers().filter(member=>!query||[member.name,member.role,v183MemberUser(member)?.name,v183MemberUser(member)?.role].join(' ').toLowerCase().includes(query));return `<div class="card admin-section v183-admin-individuals"><div class="sectionhead"><div><h2>Individuals</h2><div class="muted">Every active person · ${people.length} shown</div></div><button class="btn orange" onclick="v176NewPerson()">＋ Add Individual</button></div>${adminSearchBox('Search name, trade or linked role…',people.length,v183ActiveMembers().length)}<div class="v183-admin-people">${people.map(member=>`<button class="v182-admin-person" onclick="openTeamMember('${V183_INDIVIDUALS_ID}','${member.id}')"><span><b>${v183Esc(member.name)}</b><small>${v183Esc(member.role||'Individual')} · ${v183MemberUser(member)?'Login linked':'No login linked'}</small></span><strong>Open ›</strong></button>`).join('')||'<div class="admin-no-results">No individuals match this view.</div>'}</div></div>`}
const v183BaseAdminPage=adminPage;
adminPage=function(){let html=v183BaseAdminPage().replace(/>Teams\/Individuals<\/button>|>Teams<\/button>/g,'>Individuals</button>');if(adminTab!=='teams')return html;let marker='<div class="card admin-section',index=html.indexOf(marker);return index>=0?html.slice(0,index)+v183AdminIndividuals():html};

v182MemberVisible=function(member){if(site==='all')return true;let user=v183MemberUser(member);return (member.clockSiteIds||[]).includes(site)||(user?.allowedSites||[]).includes(site)||(db.attendanceShifts||[]).some(row=>row.workerId===member.id&&row.siteId===site)||(db.items||[]).some(item=>item.assignedId===member.id&&item.site===site)};
v181SitePeople=function(siteId){let ids=new Set((db.items||[]).filter(item=>item.site===siteId&&item.assignedId).map(item=>item.assignedId));(db.attendanceShifts||[]).filter(row=>row.siteId===siteId).forEach(row=>ids.add(row.workerId));return [...ids].filter(id=>{let member=v183Member(id);return member&&member.activeForAttendance!==false&&member.employmentStatus!=='Leaver'}).length};

// ---- Delegated controls move into Admin after first save ------------
function v183EligibleTimeGrantTargets(){let grantor=currentUser();if(!grantor||!['Director','Contracts Manager'].includes(grantor.role)||(grantor.role!=='Director'&&!grantor.permissions?.grant_time_controls))return[];let roles=grantor.role==='Director'?['Contracts Manager','Site Manager','Supervisor']:['Site Manager','Supervisor'];return (db.users||[]).filter(user=>user.id!==grantor.id&&user.accountActive!==false&&roles.includes(user.role))}
v175TimeGrantTargets=function(){return v183EligibleTimeGrantTargets().filter(user=>!user.timeControlDelegated&&!user.permissions?.manage_working_hours&&!user.permissions?.approve_overtime&&!user.permissions?.manage_attendance)};
v175TimePermissionPanel=function(){if(!hasPerm('grant_time_controls')||!['Director','Contracts Manager'].includes(currentUser()?.role||''))return'';let people=v175TimeGrantTargets();return `<div class="panel v175-time-access"><div class="sectionhead"><div><span class="att-kicker">DELEGATED CONTROL</span><h2>Grant time controls</h2><div class="muted">Choose an eligible person once. After saving, changes are made only in Admin → Users → Edit access.</div></div></div><div class="v175-time-access-list">${people.map(user=>`<div class="v175-time-access-row"><span><b>${v183Esc(user.name)}</b><small>${v183Esc(user.role)}</small></span><label><input id="v175-grant-hours-${user.id}" type="checkbox"> Set hours</label><label><input id="v175-grant-ot-${user.id}" type="checkbox"> Approve overtime</label><label><input id="v175-grant-edit-${user.id}" type="checkbox"> Correct time</label><button class="btn light smallbtn" onclick="v175SaveTimePermissions('${user.id}')">Save</button></div>`).join('')||'<div class="empty-help"><b>No unassigned time-control permissions.</b><div>Open Admin → Users to edit anybody already configured.</div></div>'}</div></div>`};
v175SaveTimePermissions=function(userId){if(!hasPerm('grant_time_controls')||!['Director','Contracts Manager'].includes(currentUser()?.role||''))return toast('Time-control delegation permission is required');let target=v183EligibleTimeGrantTargets().find(user=>user.id===userId);if(!target)return toast('This account is outside your delegation level');target.permissions=target.permissions||{};target.permissions.manage_working_hours=!!document.getElementById(`v175-grant-hours-${userId}`)?.checked;target.permissions.approve_overtime=!!document.getElementById(`v175-grant-ot-${userId}`)?.checked;target.permissions.manage_attendance=!!document.getElementById(`v175-grant-edit-${userId}`)?.checked;target.permissionProfileMode='custom';target.timeControlDelegated=true;save();audit('attendance','user permissions',target.id,`Time controls delegated to ${target.name}; future edits in Admin`, '',JSON.stringify({manage_working_hours:target.permissions.manage_working_hours,approve_overtime:target.permissions.approve_overtime,manage_attendance:target.permissions.manage_attendance}));savedState('Permissions saved — future edits are in Admin');render()};

// ---- Reliable drawing calibration and measurement -----------------
takeoffCanvasClick=function(event){
  if(!takeoffContext||takeoffTool==='select')return;let point=takeoffNormalPoint(event);if(!point)return;
  if(takeoffTool!=='Count'&&takeoffTool!=='Calibration'&&!takeoffDrawing()?.takeoffCalibration)return toast('Calibrate the drawing first.');
  if(takeoffTool==='Calibration'&&takeoffDraft.length>=2)return toast('Two calibration points are ready. Press Finish calibration.');
  takeoffDraft.push(point);takeoffPointer=null;renderTakeoffWorkspace()
};
const v183BaseTakeoffWorkspaceHtml=takeoffWorkspaceHtml;
takeoffWorkspaceHtml=function(){
  let html=v183BaseTakeoffWorkspaceHtml();if(!takeoffDraft.length||!hasPerm('manage_drawings'))return html;
  html=html.replace(/<button class="v178-finish-measurement"[\s\S]*?<\/button>/,'');
  let ready=takeoffTool==='Calibration'?takeoffDraft.length===2:takeoffTool==='Length'?takeoffDraft.length>=2:(takeoffTool==='Area'||takeoffTool==='Deduct Area'?takeoffDraft.length>=3:takeoffTool==='Count'&&takeoffDraft.length>=1),call=takeoffTool==='Calibration'?'finishTakeoffCalibration()':'finishTakeoffGeometry()',label=takeoffTool==='Calibration'?'Finish calibration':`Finish ${String(takeoffTool).toLowerCase()}`;
  let button=`<button class="v183-finish-measurement" onclick="${call}" ${ready?'':'disabled'}><span>✓</span><b>${label}</b><small>${ready?'Ready to confirm':`Add more points · ${takeoffDraft.length} selected`}</small></button>`;return html.replace('<div class="takeoff-scale ',button+'<div class="takeoff-scale ')
};

// ---- Individual payment choices, role filter and fortnight ---------
paymentPayeeOptions=function(selected='',fallback=''){let current=selected||fallback||'';if(current===V183_INDIVIDUALS_ID)current='';return `${v183PersonOptions(current)}<option value="__other__">Other individual…</option>`};
paymentPayeeKind=function(){return'Individual'};
paymentPayeeFilterOptions=function(){let map=new Map();allPaymentRows().forEach(row=>{let key=paymentPayeeKey(row);if(!map.has(key))map.set(key,paymentPayeeName(row))});return `<option value="all">All individuals</option>${[...map].sort((a,b)=>a[1].localeCompare(b[1])).map(([key,name])=>`<option value="${v183Esc(key)}" ${paymentPayeeFilter===key?'selected':''}>${v183Esc(name)}</option>`).join('')}`};
function v183PaymentRoleFor(row){let id=row?.paidTo||'',member=v183Member(id);if(!member&&row?.payeeName)member=v183Members().find(person=>person.name.toLowerCase()===String(row.payeeName).toLowerCase());let user=v183MemberUser(member);return user?.role||member?.role||'No linked role'}
function v183PaymentRoleOptions(){let roles=[...new Set(allPaymentRows().map(v183PaymentRoleFor))].sort();return `<option value="all">All user roles</option>${roles.map(roleName=>`<option value="${v183Esc(roleName)}" ${v183PaymentRole===roleName?'selected':''}>${v183Esc(roleName)}</option>`).join('')}`}
const v183BaseFilteredPaymentRows=filteredPaymentRows;
filteredPaymentRows=function(){let rows=v183BaseFilteredPaymentRows();return v183PaymentRole==='all'?rows:rows.filter(row=>v183PaymentRoleFor(row)===v183PaymentRole)};
const v183BasePaymentPreset=setPaymentDatePreset;
setPaymentDatePreset=function(preset){if(preset!=='fortnight')return v183BasePaymentPreset(preset);let today=new Date(),start=new Date(today),day=(today.getDay()+6)%7;start.setDate(today.getDate()-day-7);paymentFrom=iso(start);paymentTo=iso(today);render()};
const v183BasePaymentPresetState=paymentPresetState;
paymentPresetState=function(){if(paymentFrom&&paymentTo){let today=new Date(),start=new Date(today),day=(today.getDay()+6)%7;start.setDate(today.getDate()-day-7);if(paymentFrom===iso(start)&&paymentTo===iso(today))return'fortnight'}return v183BasePaymentPresetState()};
const v183BasePaymentsHistoryPage=paymentsHistoryPage;
paymentsHistoryPage=function(){let html=v183BasePaymentsHistoryPage(),active=paymentPresetState()==='fortnight';html=html.replace(/(<button class="[^"]*" onclick="setPaymentDatePreset\('week'\)">This Week<\/button>)/,`$1<button class="${active?'active':''}" onclick="setPaymentDatePreset('fortnight')">This Fortnight</button>`);html=html.replace(/(<div class="field"><label>FROM<\/label>)/,`<div class="field"><label>User role</label><select class="select" onchange="v183PaymentRole=this.value;render()">${v183PaymentRoleOptions()}</select></div>$1`);html=html.replace(/paymentPayeeFilter='all';paymentSearch='';paymentFrom='';paymentTo='';render\(\)/g,"paymentPayeeFilter='all';v183PaymentRole='all';paymentSearch='';paymentFrom='';paymentTo='';render()");return html};

const v183BaseLift=lift;
lift=function(item,row,index){let html=v183BaseLift(item,row,index);return html.replace(new RegExp(`<select id="lf-paidto-${index}"[\\s\\S]*?<\\/select>`),`<select id="lf-paidto-${index}" class="select" ${hasPerm('manage_payments')?'':'disabled'} onchange="markDirty()">${paymentPayeeOptions(row.paidTo,item.assignedId)}</select>`)};
const v183BaseGarden=garden;
garden=function(item){let html=v183BaseGarden(item);return html.replace(/<select id="gd-paidto"[\s\S]*?<\/select>/,`<select id="gd-paidto" class="select" ${hasPerm('manage_payments')?'':'disabled'} onchange="markDirty()">${paymentPayeeOptions(item.paidTo,item.assignedId)}</select>`)};
const v183BaseSavePayment=savePayment;
savePayment=function(context){let parts=context.split('|'),item=db.items.find(row=>row.id===parts[1]),target=parts[0]==='garden'?item:item?.lifts?.[Number(parts[2])],before=(target?.payments||[]).length,result=v183BaseSavePayment(context),payment=(target?.payments||[])[before];if(payment?.paidTo){target.paidTo=payment.paidTo;save()}return result};

// ---- Analytics: actual hours and actual individual wages -----------
teamPerformanceData=function(){let base=scopeItems(),ready=analyticsTeamRangeReady();return v183ActiveMembers().map(member=>{let items=base.filter(item=>item.assignedId===member.id),completed=ready?teamCompletedUnits(items,analyticsTeamFrom,analyticsTeamTo):0,newIssues=ready?teamIssuesInRange(items,analyticsTeamFrom,analyticsTeamTo):0;return {member,items,completed,newIssues,currentAssigned:items.length,currentProgress:items.filter(item=>overall(item)==='In progress').length,currentComplete:items.filter(item=>['Complete','Signed Off'].includes(overall(item))).length,currentIssues:items.reduce((sum,item)=>sum+issues(item),0)}}).filter(row=>row.items.length||ready).sort((a,b)=>ready?(b.completed-a.completed)||(a.newIssues-b.newIssues)||a.member.name.localeCompare(b.member.name):a.member.name.localeCompare(b.member.name))};
teamPerformanceTable=function(){let ready=analyticsTeamRangeReady(),invalid=analyticsTeamFrom&&analyticsTeamTo&&analyticsTeamFrom>analyticsTeamTo,rows=teamPerformanceData();if(invalid)return '<div class="analytics-range-prompt error"><b>Check the date range.</b><span>The FROM date must be before the TO date.</span></div>';return `<div style="overflow:auto"><table class="analytics-table"><thead><tr>${ready?'<th>Rank</th>':''}<th>Individual</th><th>${ready?'Completed work':'Assigned'}</th><th>In progress</th><th>Complete</th><th>Open issues</th></tr></thead><tbody>${rows.map((row,index)=>`<tr>${ready?`<td>${index+1}</td>`:''}<td><b>${v183Esc(row.member.name)}</b><small>${v183Esc(row.member.role||'Individual')}</small></td><td>${ready?row.completed:row.currentAssigned}</td><td>${row.currentProgress}</td><td>${row.currentComplete}</td><td>${row.currentIssues}</td></tr>`).join('')||`<tr><td colspan="${ready?6:5}">No individual activity in this selection.</td></tr>`}</tbody></table></div>`};
function v183AnalyticsWages(from,to,siteIds){return allPaymentRows().filter(row=>siteIds.includes(row.item.site)&&(!from||String(row.date||'')>=from)&&(!to||String(row.date||'')<=to)&&!!v183Member(row.paidTo)).reduce((sum,row)=>sum+Number(row.amount||0),0)}
v169AttendanceAnalyticsPanel=function(){if(!hasPerm('view_attendance')&&!hasPerm('manage_attendance'))return'';let data=v169AttendanceAnalytics(),siteIds=v169SiteIdsForAnalytics(),hours=data.people.reduce((sum,row)=>sum+row.hours,0),wages=v183AnalyticsWages(data.from,data.to,siteIds),approvedOt=data.shifts.reduce((sum,shift)=>sum+Number(shift.overtimeApproval?.status==='Approved'?shift.overtimeApproval.minutes||0:0),0)/60;return `<div class="panel v169-attendance-analytics v183-workforce-analytics"><div class="sectionhead"><div><h2>Individual Hours & Wages</h2><div class="muted">Recorded attendance and actual individual payment records for the selected period.</div></div></div><div class="v169-rangebar"><button class="${v169AttendanceRange==='week'?'active':''}" onclick="v169AttendanceRange='week';render()">This Week</button><button class="${v169AttendanceRange==='month'?'active':''}" onclick="v169AttendanceRange='month';render()">This Month</button><button class="${v169AttendanceRange==='30'?'active':''}" onclick="v169AttendanceRange='30';render()">Last 30 Days</button><button class="${v169AttendanceRange==='custom'?'active':''}" onclick="v169AttendanceRange='custom';render()">Custom</button>${v169AttendanceRange==='custom'?`<input class="input" type="date" value="${data.from}" onchange="v169AttendanceFrom=this.value;render()"><span>→</span><input class="input" type="date" value="${data.to}" onchange="v169AttendanceTo=this.value;render()">`:''}<span class="muted">${data.from} → ${data.to}</span></div><div class="analytics-grid v183-total-cards"><div class="card"><div class="muted">Total hours worked</div><div class="analytics-value">${hours.toFixed(2)} hrs</div><small>All completed recorded shifts.</small></div>${hasPerm('view_financials')?`<div class="card"><div class="muted">Total wages paid</div><div class="analytics-value">${attendanceMoney(wages)}</div><small>Actual payments recorded to individuals.</small></div>`:''}<div class="card"><div class="muted">Authorised overtime</div><div class="analytics-value">${approvedOt.toFixed(2)} hrs</div></div><div class="card"><div class="muted">Completed shifts</div><div class="analytics-value">${data.shifts.length}</div></div></div><details class="v169-breakdown"><summary>Individual attendance breakdown (${data.people.length})</summary><div style="overflow:auto"><table class="analytics-table"><thead><tr><th>Individual</th><th>Shifts</th><th>Punctual days</th><th>Late</th><th>Early</th><th>Total hours</th></tr></thead><tbody>${data.people.sort((a,b)=>b.hours-a.hours).map(row=>`<tr><td><b>${v183Esc(row.member.name)}</b></td><td>${row.shifts}</td><td>${row.onTime} (${row.punctuality}%)</td><td>${v169MinutesLabel(row.lateMinutes)}</td><td>${v169MinutesLabel(row.earlyMinutes)}</td><td>${row.hours.toFixed(2)}</td></tr>`).join('')}</tbody></table></div></details></div>`};

// ---- Diary/report wording and user-facing language -----------------
const v183BaseAddDiaryEntry=addDiaryEntry;
addDiaryEntry=function(){v183BaseAddDiaryEntry();if(modal){modal=modal.replace(/Teams on site/g,'People on site').replace(/e\.g\. Alpha Team, Mason Crew/g,'e.g. Alex Smith, Sam Jones');render()}};
if(typeof editDiaryEntry==='function'){const v183BaseEditDiaryEntry=editDiaryEntry;editDiaryEntry=function(...args){let result=v183BaseEditDiaryEntry(...args);if(modal){modal=modal.replace(/Teams on site/g,'People on site');render()}return result}}
const v183BaseAssistGuide=assistGuide;
assistGuide=function(target,context){if(target==='gangs')return 'Individuals contains every active person, their linked login, hours, pay settings and records where your permissions allow them.';if(target==='dayworks')return 'Create a purple Daywork Work Item and assign it directly to an individual.';return v183BaseAssistGuide(target,context)};
const v183BaseAssistPageLabel=assistPageLabel;
assistPageLabel=function(){let label=v183BaseAssistPageLabel();return label==='Individuals/Teams'||label==='Teams'?'Individuals':label};

function v183ReplaceWorkforceLanguage(root){
  if(!root||typeof document.createTreeWalker!=='function'||typeof NodeFilter==='undefined')return;let walker=document.createTreeWalker(root,NodeFilter.SHOW_TEXT),node;while((node=walker.nextNode()))node.nodeValue=node.nodeValue.replace(/Individuals\s*[\/&]\s*Teams|Teams\s*[\/&]\s*Individuals/gi,'Individuals').replace(/\bteams\b/gi,match=>match[0]===match[0].toUpperCase()?'INDIVIDUALS':match[0]==='Teams'?'Individuals':'individuals').replace(/\bteam\b/gi,match=>match[0]===match[0].toUpperCase()?'INDIVIDUAL':match[0]==='Team'?'Individual':'individual');root.querySelectorAll?.('[placeholder],[title],[aria-label]').forEach(element=>['placeholder','title','aria-label'].forEach(name=>{let value=element.getAttribute?.(name);if(value&&/team/i.test(value))element.setAttribute(name,value.replace(/\bteams\b/gi,'individuals').replace(/\bteam\b/gi,'individual'))}))
}
const v183BaseShell=shell;
shell=function(content){v183BaseShell(content);let root=document.getElementById('root');if(root){let demo=root.innerHTML.includes('demo-mode-banner')||root.innerHTML.includes('OPERATIONAL POLISH DEMO');root.innerHTML=root.innerHTML.replace(/v1\.82\.0 · OPERATIONAL POLISH(?: DEMO)?/g,`v1.83.0 · INDIVIDUAL WORKFORCE${demo?' DEMO':''}`);v183ReplaceWorkforceLanguage(root)}};

const v183BaseLoginScreen=loginScreen;
loginScreen=function(){v183BaseLoginScreen();let copy=document.querySelector?.('.auth-brand-copy');if(copy)copy.innerHTML=copy.innerHTML.replace(/\bteams\b/gi,'individuals').replace(/\bteam\b/gi,'individual');let welcome=document.querySelector?.('.auth-welcome p');if(welcome)welcome.textContent=welcome.textContent.replace(/\bteams\b/gi,'individuals').replace(/\bteam\b/gi,'individual')};

/* ===== v184-guided-control.js ===== */
'use strict';

// SiteSight V1.84 — reliable PDF take-off, clearer permissions and calmer navigation.

const V184_MEASURE_ROLES=['Director','Contracts Manager','Quantity Surveyor'];
const V184_MEASURE_PERMISSION=['measure_drawings','Measure Drawings'];
let v184PdfDocument=null,v184PdfSource='',v184PdfPage=1,v184PdfPageCount=0,v184PdfZoom='fit',v184PdfRenderToken=0,v184PdfRenderTask=null,v184AssistPlan=null,v184AssistFinalTranscript='';

function v184Esc(value){return String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]))}
function v184RoleMayMeasure(roleName){return V184_MEASURE_ROLES.includes(roleName)}
function v184CanMeasureDrawing(user=currentUser()){return !!user&&v184RoleMayMeasure(user.role)&&(user.role==='Director'||!!user.permissions?.measure_drawings)}
function v184IsPdfDrawing(d){return !!d&&(d.type==='application/pdf'||String(d.fileName||d.originalName||'').toLowerCase().endsWith('.pdf'))}
function v184DrawingRows(){return [...(db.siteDrawings||[]),...(db.items||[]).flatMap(item=>item.drawings||[])]}

// A separate drawing-measurement permission is visible in Admin, but the role gate is absolute.
const v184DrawingPermissionGroup=permissionGroups.find(group=>group.name==='Site Information');
if(v184DrawingPermissionGroup&&!v184DrawingPermissionGroup.items.some(([key])=>key==='measure_drawings')){
  let index=v184DrawingPermissionGroup.items.findIndex(([key])=>key==='manage_drawings');
  v184DrawingPermissionGroup.items.splice(index<0?v184DrawingPermissionGroup.items.length:index,0,V184_MEASURE_PERMISSION)
}
if(!permissionDefs.some(([key])=>key==='measure_drawings'))permissionDefs.push(V184_MEASURE_PERMISSION);
if(typeof V179_PERMISSION_COPY==='object')V179_PERMISSION_COPY.measure_drawings=['Measure drawings','Calibrate and save image or PDF take-off measurements. This is restricted to Quantity Surveyors, Contracts Managers and Directors.'];
for(const roleName of ['Contracts Manager','Quantity Surveyor']){
  let profile=V173_ROLE_PROFILES[roleName];if(profile&&Array.isArray(profile.permissions)&&!profile.permissions.includes('measure_drawings'))profile.permissions.push('measure_drawings')
}

function v184MigrateDrawing(d){
  if(!v184IsPdfDrawing(d))return false;let changed=false;
  d.takeoffPdfCalibrations=d.takeoffPdfCalibrations&&typeof d.takeoffPdfCalibrations==='object'?d.takeoffPdfCalibrations:{};
  if(d.takeoffCalibration&&!d.takeoffPdfCalibrations['1']){d.takeoffPdfCalibrations['1']=JSON.parse(JSON.stringify(d.takeoffCalibration));changed=true}
  (d.takeoffs||[]).forEach(row=>{if(!Number(row.pdfPage)){row.pdfPage=1;changed=true}});
  if(!Number(d.takeoffLastPage)){d.takeoffLastPage=1;changed=true}return changed
}
function v184Migration(){
  let changed=false;
  (db.users||[]).forEach(user=>{
    user.permissions=user.permissions||{};let allowed=v184RoleMayMeasure(user.role),old=Number(user.permissionTemplateVersion||0);
    if(!allowed&&user.permissions.measure_drawings){user.permissions.measure_drawings=false;changed=true}
    if(old<184){if(allowed&&!user.permissions.measure_drawings){user.permissions.measure_drawings=true;changed=true}user.permissionTemplateVersion=184;changed=true}
  });
  v184DrawingRows().forEach(drawing=>{if(v184MigrateDrawing(drawing))changed=true});return changed
}
const v184BaseMigrate=migrate;
migrate=function(persist=true){v184BaseMigrate(false);let changed=v184Migration();if(persist&&(changed||persist))save()};
if((db.users||[]).length&&v184Migration())persistCache();

// ---- Clickable permission boxes with concise help -----------------
function v184PermissionDescription(key,label){let copy=V179_PERMISSION_COPY?.[key];return copy?.[1]||`Allows this user to ${String(label||key).toLowerCase()} where their role and Site access permit it.`}
permissionInputs=function(permissions){
  let groups=permissionGroups.map(group=>`<section class="permission-group v184-permission-group"><h4>${v184Esc(group.name)}</h4><div class="v184-permission-grid">${group.items.map(([key,label])=>`<div class="v184-permission-box" data-permission-box="${key}"><label><input type="checkbox" data-perm="${key}" ${permissions?.[key]?'checked':''}><span class="v184-permission-check">✓</span><b>${v184Esc(label)}</b></label><button type="button" class="v184-permission-help" onclick="v184OpenPermissionHelp('${key}')" aria-label="What does ${v184Esc(label)} mean?">?</button></div>`).join('')}</div></section>`).join('');
  return `<div class="v184-permission-intro"><b>Choose this user’s access</b><span>The selected role has already ticked the recommended boxes. Tick or untick any box to tailor the account.</span></div><div class="permission-groups v184-permission-groups">${groups}</div>`
};
function v184OpenPermissionHelp(key){let def=permissionDefs.find(([name])=>name===key),label=def?.[1]||key;modal=`<div class="modal"><div class="modalbox v184-help-modal"><button class="v184-modal-close" onclick="modal=null;render()" aria-label="Close">×</button><span class="att-kicker">PERMISSION HELP</span><h2>${v184Esc(label)}</h2><p>${v184Esc(v184PermissionDescription(key,label))}</p>${key==='measure_drawings'?'<div class="v184-role-limit">Available only to Quantity Surveyors, Contracts Managers and Directors.</div>':''}<button class="btn blue" onclick="modal=null;render()">Understood</button></div></div>`;render()}
function v184ApplyRoleTemplate(selectId){
  v173ApplyRoleTemplate(selectId);let roleName=document.getElementById(selectId)?.value||'Operator',box=document.querySelector?.('[data-perm="measure_drawings"]'),card=box?.closest?.('[data-permission-box]'),allowed=v184RoleMayMeasure(roleName);
  if(box){box.disabled=!allowed;if(!allowed)box.checked=false}card?.classList?.toggle('restricted',!allowed)
}
refreshRolePerms=function(){v184ApplyRoleTemplate('ur')};
const v184BaseAddUser=addUser;
addUser=function(){v184BaseAddUser();if(!modal)return;modal=modal.replace(/<div class="role-template-note">[\s\S]*?<\/div>/,`<div class="v184-role-note"><b>1. Choose the closest role.</b><span>Recommended access is ticked automatically. Change any box below if this person needs something different.</span></div>`).replace(/<p class="muted">Recommended permissions are applied automatically from the selected role\.<\/p>/,'');render();setTimeout(()=>v184ApplyRoleTemplate('ur'),0)};
const v184BaseEditUserAccess=editUserAccess;
editUserAccess=function(id){v184BaseEditUserAccess(id);if(!modal)return;modal=modal.replace(/<p class="muted">Role defaults are applied automatically\. Open Customise permissions only when this user needs an exception\.<\/p>/,'<p class="muted">The role supplies a sensible base. Tick or untick the boxes below to change this individual account.</p>').replace(/onchange="v173ApplyRoleTemplate\('er'\)"/g,'onchange="v184ApplyRoleTemplate(\'er\')"');render();setTimeout(()=>v184ApplyRoleTemplate('er'),0)};
const v184BaseSaveNewUser=saveNewUser;
saveNewUser=function(...args){let before=new Set((db.users||[]).map(user=>user.id)),result=v184BaseSaveNewUser(...args),user=(db.users||[]).find(row=>!before.has(row.id));if(user){user.permissions=user.permissions||{};if(!v184RoleMayMeasure(user.role))user.permissions.measure_drawings=false;user.permissionTemplateVersion=184;save()}return result};
const v184BaseSaveUserAccess=saveUserAccess;
saveUserAccess=function(id,...args){let result=v184BaseSaveUserAccess(id,...args),user=(db.users||[]).find(row=>row.id===id);if(user&&modal===null){user.permissions=user.permissions||{};if(!v184RoleMayMeasure(user.role))user.permissions.measure_drawings=false;user.permissionTemplateVersion=184;save()}return result};

// ---- Updated role-specific New User Guide -------------------------
guideSteps=function(){
  let steps=[],add=(when,id,icon,title,summary,bullets,target,label)=>{if(when)steps.push({id,icon,title,summary,bullets,target,label})};
  add(hasPerm('dashboard'),'dashboard','⌂','Dashboard','See the day clearly before opening individual records.',['Needs Attention uses the pale panel for exceptions that require action.','Today’s Summary stays compact and expands only when there are more records.','Use the Site selector to focus every page on one development.'],'dashboard','Open Dashboard');
  add(hasPerm('clock_attendance'),'clock','◷','Clock In / Out','Start or finish a shift from the large clock button.',['Operators open here immediately after sign-in.','Operatives and subcontractors use the 250m Site check. Supervisors and higher use management clocking without a location check.','Early clock-ins do not create payable time before the person’s scheduled start.'],'attendance','Open Clock In / Out');
  add(hasPerm('work_items'),'work','▣','Work Items & Daywork','Plots, work packages and purple Daywork records share one clear list.',['Search with the number you naturally use, such as 20 for Plot 020.','In Progress, Complete and Signed Off sections can be collapsed.','Create Daywork as a purple Work Item and assign it directly to an individual.'],'work-items','Open Work Items');
  add(hasPerm('view_issues'),'issues','⚠','Issues','Record problems, ownership and resolution evidence against the correct Work Item.',[hasPerm('raise_issues')?'Raise an Issue with priority, target date, photos and a clear description.':'Open Issues to review the information available to your role.',hasPerm('manage_issues')?'Resolve an Issue when the evidence is complete.':'Resolution controls are limited to authorised roles.'],'issues','Open Issues');
  add(hasPerm('gangs'),'individuals','♙','Individuals','Find every person, linked login, hours and records in one place.',['Use the search box to find a name or trade.','Open a person to review their Site access, working hours, pay setup or qualifications where permitted.','New people can be linked to an existing login or given a new login.'],'gangs','Open Individuals');
  add(hasPerm('view_attendance'),'people-time','◷','People & Time','Review attendance, overtime, leave, timesheets and pay periods.',['Working hours control when payable time starts and scheduled-finish reminders are sent.','Only authorised overtime of more than 15 minutes is payable.','Use custom dates for weekly, fortnightly, monthly or other review periods.'],'attendance','Open People & Time');
  add(hasPerm('view_drawings'),'drawings','▤','Drawings','Find current revisions, highlights and saved take-off information.',[v184CanMeasureDrawing()?'PDF take-off has page controls and keeps a separate calibration for every page. Zoom can be changed without changing saved measurements.':'Take-off measurement is restricted to Quantity Surveyors, Contracts Managers and Directors.','Always check the Current/Superseded banner before using a drawing.'],'site-drawings','Open Drawings');
  add(hasPerm('view_calendar'),'calendar','▣','Calendar','See past and upcoming Site events in a visual month view.',['Choose the month directly from the month selector.','Custom dates are available when you need a specific reporting window.',hasPerm('manage_calendar')?'Add deliveries, scaffold moves, inspections and meetings.':'Calendar editing is hidden for this account.'],'calendar','Open Calendar');
  add(hasPerm('view_financials'),'commercial','£','Payments, CIS & Pay Review','Review individual amounts, approved hours and CIS deductions without exposing them to operational roles.',['Payments History can be filtered by individual role and includes a fortnight shortcut.','CIS exports show gross, materials, deduction and net payment information.',hasPerm('manage_payments')?'You can correct payment records where evidence supports it.':'Payment editing is controlled by another role.'],'payments-history','Open Payments');
  add(hasPerm('analytics'),'analytics','◒','Analytics','Use hours, wages, progress and individual performance to understand delivery.',['Select a Site and custom date range before comparing results.','Total operative hours and wages use recorded operational data.','Analytics is a summary; open the underlying person or Work Item before making a decision.'],'analytics','Open Analytics');
  let adminAccess=hasPerm('manage_sites')||hasPerm('manage_users')||hasPerm('manage_gangs')||hasPerm('view_archives');
  add(adminAccess,'admin','⚙','Admin & Permissions','Set up Sites, Individuals and user access away from the daily operational pages.',['Choose a role first, then use the simple permission boxes.','Press the small ? beside a permission for a plain-English explanation.','Existing delegated controls are edited on that user’s Admin record.'],'admin','Open Admin');
  steps.push({id:'help',icon:'🔔',title:'Notifications & Taskbot',summary:'Updates and help remain visible without interrupting normal work.',bullets:['The bell beside Settings shows unread notifications at all times.','Taskbot accepts typing or voice. The microphone turns red while listening.','Any Taskbot change is shown for confirmation and still follows your permissions.'],target:'more',label:'Open More'});return steps
};
const v184BaseNewUserGuidePage=newUserGuidePage;
newUserGuidePage=function(){return v184BaseNewUserGuidePage().replace('This guide only shows the features your account can access. It takes around two minutes','This updated guide covers the current tools available to your account. It takes around three minutes').replace('<div class="guide-welcome-main">','<div class="guide-welcome-main"><span class="v184-guide-updated">UPDATED FOR V1.84</span>')};

// ---- Reliable Taskbot voice and confirmation ----------------------
const v184BaseAssistSay=assistSay;
assistSay=function(message,actions=[]){let clean=String(message||'').replace(/Individuals\s*[\/&]\s*Teams|Teams\s*[\/&]\s*Individuals/gi,'Individuals').replace(/\bteams\b/gi,'individuals').replace(/\bteam\b/gi,'individual').replace(/Weekly Site Report/g,'Site Report');let mapped=(actions||[]).map(action=>({...action,label:String(action.label||'').replace(/\bteams\b/gi,'Individuals').replace(/\bteam\b/gi,'Individual').replace(/Weekly Site Report/g,'Site Report')}));return v184BaseAssistSay(clean,mapped)};
assistVoice=function(){
  if(v176AssistRecognition&&v176AssistListening){v176AssistRecognition.stop();return}
  let Speech=window.SpeechRecognition||window.webkitSpeechRecognition;if(!Speech)return toast('Voice commands need Chrome, Edge or another browser with speech recognition. You can still type the command.');
  if(window.isSecureContext===false&&!['localhost','127.0.0.1'].includes(location.hostname))return toast('Voice commands require the secure HTTPS SiteSight address.');
  let recognition=new Speech(),heard='',finalReceived=false;v176AssistRecognition=recognition;recognition.lang='en-GB';recognition.interimResults=true;recognition.continuous=false;recognition.maxAlternatives=1;
  recognition.onstart=()=>{v176AssistListening=true;assistRenderPanel()};
  recognition.onresult=event=>{heard=Array.from(event.results||[]).map(result=>result?.[0]?.transcript||'').join(' ').trim();let input=document.getElementById('assist-input');if(input)input.value=heard;let last=event.results?.[event.results.length-1];if(last?.isFinal&&heard){finalReceived=true;v184AssistFinalTranscript=heard;v176AssistListening=false;assistRenderPanel();setTimeout(()=>{let next=document.getElementById('assist-input');if(next){next.value=v184AssistFinalTranscript;assistSubmit()}},60)}};
  recognition.onerror=event=>{v176AssistListening=false;v176AssistRecognition=null;assistRenderPanel();let messages={'not-allowed':'Microphone permission was not allowed. Enable it for SiteSight, then try again.','audio-capture':'No working microphone was found.','no-speech':'I did not hear a command. Press the microphone and try again.','network':'Voice recognition could not reach the browser speech service.'};toast(messages[event.error]||'Voice input could not start')};
  recognition.onend=()=>{v176AssistRecognition=null;if(v176AssistListening){v176AssistListening=false;assistRenderPanel()}if(heard&&!finalReceived){v184AssistFinalTranscript=heard;setTimeout(()=>{let input=document.getElementById('assist-input');if(input){input.value=heard;assistSubmit()}},40)}};
  try{recognition.start()}catch(error){v176AssistRecognition=null;v176AssistListening=false;assistRenderPanel();toast('Voice input could not start')}
};
const v184BaseAssistExecutePending=assistExecutePending;
assistConfirmPlan=function(plan){assistClose();v184AssistPlan=JSON.parse(JSON.stringify(plan||{}));assistPending=JSON.parse(JSON.stringify(plan||{}));let summary=assistPlanSummary(plan);modal=`<div class="modal"><div class="modalbox assist-confirm"><span class="att-kicker">TASKBOT — REVIEW</span><h2>Confirm task</h2><p>${assistEsc(summary)}</p><div class="assist-confirm-rule">The change will use your current account, normal permissions and Audit history.</div><button class="btn orange" onclick="this.disabled=true;this.textContent='Applying…';assistExecutePending()">Confirm & do it</button> <button class="btn light" onclick="modal=null;assistPending=null;v184AssistPlan=null;render();assistOpenPanel()">Cancel</button></div></div>`;render()};
assistExecutePending=function(){if(!assistPending&&v184AssistPlan)assistPending=JSON.parse(JSON.stringify(v184AssistPlan));v184AssistPlan=null;return v184BaseAssistExecutePending()};

// ---- Notification bell beside Settings ----------------------------
const v184BaseShell=shell;
shell=function(content){
  v184BaseShell(content);let root=document.getElementById('root');if(!root)return;let user=currentUser(),unread=user?unreadNotifications(user).length:0,bell=`<button class="v184-notification-bell ${unread?'has-unread':''}" onclick="notificationSettings()" title="Notifications${unread?` — ${unread} unread`:''}" aria-label="Notifications${unread?` — ${unread} unread`:''}"><span>🔔</span>${unread?`<b>${unread>99?'99+':unread}</b>`:''}</button>`;
  root.innerHTML=root.innerHTML.replace(/<button onclick="notificationSettings\(\)">🔔 Notifications[\s\S]*?<\/button>/,'').replace('<div class="settingswrap">',`<div class="settingswrap v184-profile-actions">${bell}`).replace('<button class="mobile-global-search"',`${bell.replace('v184-notification-bell','v184-mobile-notification-bell')}<button class="mobile-global-search"`).replace(/v1\.83\.0 · INDIVIDUAL WORKFORCE(?: DEMO)?/g,`v1.84.0 · GUIDED CONTROL${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`)
};

// ---- PDF.js page-aware, zoom-independent drawing take-off ---------
function v184PdfAsset(path){return new URL(`assets/vendor/pdfjs/${path}`,document.baseURI||location.href).href}
function v184PdfActive(){return !!takeoffContext?.isPdf}
function v184PdfCalibrations(d=takeoffDrawing()){if(!d)return{};d.takeoffPdfCalibrations=d.takeoffPdfCalibrations||{};return d.takeoffPdfCalibrations}
function v184StorePdfCalibration(){if(!v184PdfActive())return;let d=takeoffDrawing();if(!d)return;let map=v184PdfCalibrations(d);if(d.takeoffCalibration)map[String(v184PdfPage)]=JSON.parse(JSON.stringify(d.takeoffCalibration));else delete map[String(v184PdfPage)];d.takeoffLastPage=v184PdfPage}
function v184LoadPdfCalibration(page=v184PdfPage){if(!v184PdfActive())return;let d=takeoffDrawing();if(!d)return;d.takeoffCalibration=v184PdfCalibrations(d)[String(page)]?JSON.parse(JSON.stringify(v184PdfCalibrations(d)[String(page)])):null;d.takeoffLastPage=page}
function v184PageTakeoffs(d=takeoffDrawing()){let rows=d?.takeoffs||[];return v184PdfActive()?rows.filter(row=>Number(row.pdfPage||1)===Number(v184PdfPage)):rows}
const v184BaseTakeoffTotals=takeoffTotals;
takeoffTotals=function(d=takeoffDrawing()){return v184PdfActive()&&d===takeoffDrawing()?v184BaseTakeoffTotals({...d,takeoffs:v184PageTakeoffs(d)}):v184BaseTakeoffTotals(d)};
takeoffListHtml=function(d=takeoffDrawing()){let rows=v184PageTakeoffs(d);if(!rows.length)return `<div class="takeoff-empty">No saved measurements${v184PdfActive()?` on page ${v184PdfPage}`:''}.</div>`;return `<div class="takeoff-list">${rows.map(row=>`<div class="takeoff-row"><div class="takeoff-row-main"><span class="takeoff-kind kind-${String(row.kind).toLowerCase().replace(/\s+/g,'-')}">${v184Esc(row.kind)}</span><b>${calendarSafe(row.label)}</b><small>${takeoffFmtQty(row)}${v184PdfActive()?` · Page ${Number(row.pdfPage||1)}`:''} · ${v184Esc(row.by||'Unknown user')}${row.createdAt?' · '+new Date(row.createdAt).toLocaleDateString('en-GB'):''}</small></div>${hasPerm('view_financials')?`<div class="takeoff-money"><span>${Number(row.rate||0)?money(row.rate)+' / '+row.unit:'No rate'}</span><b>${Number(row.rate||0)?money(takeoffValue(row)):'—'}</b></div>`:''}${v184CanMeasureDrawing()?`<button class="takeoff-delete" onclick="deleteTakeoffMeasurement('${row.id}')" title="Remove measurement">×</button>`:''}</div>`).join('')}</div>`};
renderTakeoffSvg=function(){let svg=document.getElementById('takeoff-svg'),d=takeoffDrawing();if(!svg||!d)return;let size={w:svg.clientWidth||Number(svg.getAttribute?.('width'))||1000,h:svg.clientHeight||Number(svg.getAttribute?.('height'))||700};svg.setAttribute('viewBox',`0 0 ${size.w} ${size.h}`);let saved=v184PageTakeoffs(d).map(row=>takeoffGeometrySvg(row,size,false)).join(''),draft=takeoffDraft.length?takeoffGeometrySvg({kind:takeoffTool,points:takeoffDraft},size,true):'',pointer=takeoffPointer&&takeoffDraft.length&&takeoffTool!=='Count'&&takeoffTool!=='Calibration'?(()=>{let a=takeoffPxPoint(takeoffDraft.at(-1),size),b=takeoffPxPoint(takeoffPointer,size);return `<line class="takeoff-pointer-line" x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}"></line>`})():'';svg.innerHTML=takeoffCalibrationSvg(d,size)+saved+draft+pointer};

function v184PdfControls(){let count=v184PdfPageCount||Number(takeoffDrawing()?.pdfPageCount||0);return `<div class="v184-pdf-controls"><button class="btn light smallbtn" onclick="v184ChangePdfPage(-1)" ${v184PdfPage<=1?'disabled':''}>← Previous</button><label>Page <input id="v184-pdf-page" type="number" min="1" ${count?`max="${count}"`:''} value="${v184PdfPage}" onchange="v184GoPdfPage(this.value)"> of <b id="v184-pdf-pages">${count||'…'}</b></label><button class="btn light smallbtn" onclick="v184ChangePdfPage(1)" ${count&&v184PdfPage>=count?'disabled':''}>Next →</button><label>Zoom <select onchange="v184SetPdfZoom(this.value)"><option value="fit" ${v184PdfZoom==='fit'?'selected':''}>Fit width</option><option value="0.75" ${v184PdfZoom==='0.75'?'selected':''}>75%</option><option value="1" ${v184PdfZoom==='1'?'selected':''}>100%</option><option value="1.25" ${v184PdfZoom==='1.25'?'selected':''}>125%</option><option value="1.5" ${v184PdfZoom==='1.5'?'selected':''}>150%</option></select></label><span id="v184-pdf-status">Loading page…</span></div>`}
function v184FinishMeasurementButton(){if(!takeoffDraft.length||!v184CanMeasureDrawing())return'';let ready=takeoffTool==='Calibration'?takeoffDraft.length===2:takeoffTool==='Length'?takeoffDraft.length>=2:(takeoffTool==='Area'||takeoffTool==='Deduct Area'?takeoffDraft.length>=3:takeoffTool==='Count'&&takeoffDraft.length>=1),call=takeoffTool==='Calibration'?'finishTakeoffCalibration()':'finishTakeoffGeometry()',label=takeoffTool==='Calibration'?'Finish calibration':`Finish ${String(takeoffTool).toLowerCase()}`;return `<button class="v184-finish-measurement" onclick="${call}" ${ready?'':'disabled'}><span>✓</span><b>${label}</b><small>${ready?'Ready to confirm':`Add more points · ${takeoffDraft.length} selected`}</small></button>`}
takeoffWorkspaceHtml=function(){
  let d=takeoffDrawing();if(!d||!takeoffContext)return'';let pdf=takeoffContext.isPdf,canMeasure=v184CanMeasureDrawing(),cal=!!d.takeoffCalibration,finishReady=takeoffTool==='Length'?takeoffDraft.length>=2:(takeoffTool==='Area'||takeoffTool==='Deduct Area'?takeoffDraft.length>=3:takeoffTool==='Count'&&takeoffDraft.length>=1),finishCall=takeoffTool==='Calibration'?'finishTakeoffCalibration()':'finishTakeoffGeometry()',pageRows=v184PageTakeoffs(d);
  let toolbar=canMeasure?`<button class="${takeoffTool==='Calibration'?'active':''}" onclick="chooseTakeoffTool('Calibration')">↔ Calibrate</button><button class="${takeoffTool==='Length'?'active':''}" onclick="chooseTakeoffTool('Length')" ${!cal?'disabled':''}>╱ Length</button><button class="${takeoffTool==='Area'?'active':''}" onclick="chooseTakeoffTool('Area')" ${!cal?'disabled':''}>▱ Area</button><button class="${takeoffTool==='Deduct Area'?'active':''}" onclick="chooseTakeoffTool('Deduct Area')" ${!cal?'disabled':''}>⊟ Deduct Area</button><button class="${takeoffTool==='Count'?'active':''}" onclick="chooseTakeoffTool('Count')">● Count</button>${takeoffDraft.length?`<button class="finish" onclick="${finishCall}" ${takeoffTool!=='Calibration'&&!finishReady?'disabled':''}>✓ Finish</button><button onclick="cancelTakeoffDraft()">Clear points</button>`:''}`:'';
  let stage=pdf?`${v184PdfControls()}<div id="v184-pdf-scroll" class="v184-pdf-scroll"><div id="v184-pdf-sheet" class="v184-pdf-sheet"><canvas id="v184-pdf-canvas"></canvas><svg id="takeoff-svg" onclick="takeoffCanvasClick(event)" onmousemove="takeoffCanvasMove(event)" onmouseleave="takeoffCanvasLeave()"></svg></div></div><div class="v184-pdf-note"><b>Page-aware PDF take-off</b><span>Each page keeps its own calibration. Zoom changes the display only; saved measurements stay unchanged.</span></div>`:`<div class="takeoff-drawing-stage"><div class="v184-image-sheet"><img src="${takeoffContext.src}" alt="${calendarSafe(d.name||'Drawing')}"><svg id="takeoff-svg" onclick="takeoffCanvasClick(event)" onmousemove="takeoffCanvasMove(event)" onmouseleave="takeoffCanvasLeave()"></svg></div></div>`;
  return `<div class="takeoff-workspace v184-takeoff-workspace"><div class="takeoff-head"><div><span class="takeoff-kicker">DRAWING TAKE-OFF</span><h2>${calendarSafe(d.name||d.originalName||'Drawing')}</h2><div class="muted">${takeoffContext.scope==='work'?`${db.items.find(item=>item.id===takeoffContext.itemId)?.ref||''} · `:''}${siteName(takeoffSiteId())}${pdf?` · PDF page ${v184PdfPage}`:''}</div></div><div class="actions"><button class="btn light" onclick="closeDrawingTakeoff()">✕ Close take-off</button></div></div>${takeoffGuideHtml()}<div class="takeoff-body"><div class="takeoff-canvas-column"><div class="takeoff-toolbar">${toolbar}<span id="takeoff-draft-status">${takeoffDraft.length?`${takeoffDraft.length} point${takeoffDraft.length===1?'':'s'} selected`:(canMeasure?'Choose a tool, then click the drawing':'View-only take-off')}</span></div>${stage}</div><aside class="takeoff-sidebar">${v184FinishMeasurementButton()}<div class="takeoff-scale ${cal?'calibrated':''}"><div><span>DRAWING SCALE${pdf?` · PAGE ${v184PdfPage}`:''}</span><b>${cal?'✓ Calibrated':'Not calibrated'}</b></div><p>${cal?calendarSafe(takeoffCalibrationText(d)):'Click Calibrate, select two known points and enter their real distance.'}</p>${canMeasure&&cal?'<button class="btn light smallbtn" onclick="chooseTakeoffTool(\'Calibration\')">Recalibrate</button> <button class="btn light smallbtn" onclick="clearDrawingCalibration()">Remove scale</button>':''}</div>${takeoffPendingHtml()}${takeoffTotalsHtml(d)}<div class="takeoff-list-head"><div><h3>Saved Take-off${pdf?` · Page ${v184PdfPage}`:''}</h3><span>${pageRows.length} on this page${pdf?` · ${(d.takeoffs||[]).length} all pages`:''}</span></div></div>${takeoffListHtml(d)}</aside></div></div>`
};

async function v184EnsurePdfDocument(){
  if(!v184PdfActive())return null;let source=takeoffContext.src;if(v184PdfDocument&&v184PdfSource===source)return v184PdfDocument;
  let pdfjs=await import(v184PdfAsset('pdf.min.mjs'));pdfjs.GlobalWorkerOptions.workerSrc=v184PdfAsset('pdf.worker.min.mjs');let response=await fetch(source,{credentials:'same-origin'});if(!response.ok)throw new Error(`PDF could not be loaded (${response.status})`);let data=new Uint8Array(await response.arrayBuffer());v184PdfDocument=await pdfjs.getDocument({data,cMapUrl:v184PdfAsset('cmaps/'),cMapPacked:true,standardFontDataUrl:v184PdfAsset('standard_fonts/'),wasmUrl:v184PdfAsset('wasm/')}).promise;v184PdfSource=source;v184PdfPageCount=v184PdfDocument.numPages||1;let d=takeoffDrawing();if(d&&d.pdfPageCount!==v184PdfPageCount){d.pdfPageCount=v184PdfPageCount;save()}return v184PdfDocument
}
async function v184RenderPdfPage(){
  if(!v184PdfActive())return;let token=++v184PdfRenderToken,status=document.getElementById('v184-pdf-status');if(status)status.textContent='Loading page…';
  try{let doc=await v184EnsurePdfDocument();if(token!==v184PdfRenderToken||!doc)return;v184PdfPage=Math.max(1,Math.min(v184PdfPage,doc.numPages));let page=await doc.getPage(v184PdfPage),host=document.getElementById('v184-pdf-scroll'),sheet=document.getElementById('v184-pdf-sheet'),canvas=document.getElementById('v184-pdf-canvas'),svg=document.getElementById('takeoff-svg');if(!host||!sheet||!canvas||!svg)return;let base=page.getViewport({scale:1}),available=Math.max(320,(host.clientWidth||1000)-24),scale=v184PdfZoom==='fit'?available/base.width:Number(v184PdfZoom||1),display=page.getViewport({scale}),ratio=Math.min(Number(window.devicePixelRatio||1),2),renderView=page.getViewport({scale:scale*ratio});canvas.width=Math.ceil(renderView.width);canvas.height=Math.ceil(renderView.height);canvas.style.width=`${display.width}px`;canvas.style.height=`${display.height}px`;sheet.style.width=`${display.width}px`;sheet.style.height=`${display.height}px`;svg.setAttribute('width',String(display.width));svg.setAttribute('height',String(display.height));svg.setAttribute('viewBox',`0 0 ${display.width} ${display.height}`);if(v184PdfRenderTask?.cancel)try{v184PdfRenderTask.cancel()}catch(error){}let context=canvas.getContext('2d',{alpha:false});v184PdfRenderTask=page.render({canvas,canvasContext:context,viewport:renderView});await v184PdfRenderTask.promise;if(token!==v184PdfRenderToken)return;renderTakeoffSvg();let total=document.getElementById('v184-pdf-pages'),input=document.getElementById('v184-pdf-page');if(total)total.textContent=String(doc.numPages);if(input){input.max=String(doc.numPages);input.value=String(v184PdfPage)}if(status)status.textContent=`Page ${v184PdfPage} ready · ${Math.round(scale*100)}%`;updateTakeoffDraftStatus()}
  catch(error){if(error?.name==='RenderingCancelledException')return;if(status)status.textContent='PDF could not be rendered';toast(error?.message||'PDF could not be rendered')}
}
renderTakeoffWorkspace=function(){if(!takeoffContext)return;modal=takeoffWorkspaceHtml();render();setTimeout(()=>{if(v184PdfActive())v184RenderPdfPage();else{renderTakeoffSvg();updateTakeoffDraftStatus()}},0)};
function v184GoPdfPage(value){let next=Math.max(1,Math.min(Number(value)||1,v184PdfPageCount||9999));if(next===v184PdfPage)return;if(takeoffDraft.length||takeoffPending)return toast('Finish or clear the current measurement before changing page.');v184StorePdfCalibration();v184PdfPage=next;v184LoadPdfCalibration(next);save();renderTakeoffWorkspace()}
function v184ChangePdfPage(amount){v184GoPdfPage(v184PdfPage+Number(amount||0))}
function v184SetPdfZoom(value){v184PdfZoom=value||'fit';v184RenderPdfPage()}

const v184BaseOpenDrawingTakeoff=openDrawingTakeoff;
openDrawingTakeoff=async function(...args){if(!v184CanMeasureDrawing())return toast('Drawing measurement is restricted to Quantity Surveyors, Contracts Managers and Directors.');await v184BaseOpenDrawingTakeoff(...args);if(takeoffContext?.isPdf){let d=takeoffDrawing();v184PdfDocument=null;v184PdfSource='';v184PdfPageCount=Number(d?.pdfPageCount||0);v184PdfPage=Math.max(1,Number(d?.takeoffLastPage||1));v184PdfZoom='fit';v184LoadPdfCalibration(v184PdfPage);renderTakeoffWorkspace()}};
function v184RemoveMeasureAction(){if(v184CanMeasureDrawing()||!modal)return;modal=modal.replace(/<button class="btn blue" onclick="openDrawingTakeoff\([^)]*\)">📐 Measure Drawing<\/button>/g,'');render()}
const v184BaseOpenSiteDrawing=openSiteDrawing;
openSiteDrawing=async function(...args){let result=await v184BaseOpenSiteDrawing(...args);v184RemoveMeasureAction();return result};
const v184BaseOpenDrawing=openDrawing;
openDrawing=async function(...args){let result=await v184BaseOpenDrawing(...args);v184RemoveMeasureAction();return result};

const v184BaseTakeoffCanvasClick=takeoffCanvasClick;
takeoffCanvasClick=function(event){if(!v184CanMeasureDrawing())return toast('You do not have drawing measurement access.');return v184BaseTakeoffCanvasClick(event)};
const v184BaseChooseTakeoffTool=chooseTakeoffTool;
chooseTakeoffTool=function(kind){if(!v184CanMeasureDrawing())return toast('You do not have drawing measurement access.');return v184BaseChooseTakeoffTool(kind)};
const v184BaseFinishTakeoffCalibration=finishTakeoffCalibration;
finishTakeoffCalibration=function(){if(!v184CanMeasureDrawing())return toast('You do not have drawing measurement access.');let result=v184BaseFinishTakeoffCalibration();if(v184PdfActive()){v184StorePdfCalibration();save()}return result};
const v184BaseSaveTakeoffMeasurement=saveTakeoffMeasurement;
saveTakeoffMeasurement=function(){if(!v184CanMeasureDrawing())return toast('You do not have drawing measurement access.');let d=takeoffDrawing(),before=(d?.takeoffs||[]).length,result=v184BaseSaveTakeoffMeasurement(),added=(d?.takeoffs||[]).slice(before);if(v184PdfActive()&&added.length){added.forEach(row=>row.pdfPage=v184PdfPage);v184StorePdfCalibration();save();renderTakeoffWorkspace()}return result};
recalculateTakeoffsWithCurrentCalibration=function(){if(!v184CanMeasureDrawing())return;let d=takeoffDrawing(),size=takeoffSvgSize();if(!d?.takeoffCalibration)return;v184PageTakeoffs(d).forEach(row=>{if(row.kind!=='Count'&&Array.isArray(row.points)&&row.points.length)row.quantity=takeoffQuantityFromGeometry(row.kind,row.points,size)});save();let entity=takeoffAuditEntity();audit('drawing','drawing take-off',d.id||entity.id,`${entity.label}: ${v184PdfActive()?`page ${v184PdfPage} `:''}measurements recalculated`,takeoffSiteId())};
clearDrawingCalibration=function(){if(!v184CanMeasureDrawing())return toast('You do not have drawing measurement access.');let d=takeoffDrawing();if(!d?.takeoffCalibration)return;if(!confirm(`Remove the calibration${v184PdfActive()?` for page ${v184PdfPage}`:''}? Saved quantities will stay recorded.`))return;d.takeoffCalibration=null;if(v184PdfActive())delete v184PdfCalibrations(d)[String(v184PdfPage)];takeoffDraft=[];takeoffTool='select';save();let entity=takeoffAuditEntity();audit('drawing','drawing take-off',d.id||entity.id,`${entity.label}: calibration removed${v184PdfActive()?` from page ${v184PdfPage}`:''}`,takeoffSiteId());renderTakeoffWorkspace()};
deleteTakeoffMeasurement=function(id){if(!v184CanMeasureDrawing())return toast('You do not have drawing measurement access.');let d=takeoffDrawing(),index=d?.takeoffs?.findIndex(row=>row.id===id);if(index<0)return;let old=d.takeoffs[index],entity=takeoffAuditEntity();d.takeoffs.splice(index,1);save();audit('drawing','drawing take-off',d.id||entity.id,`${entity.label}: take-off removed — ${old.label}`,takeoffSiteId());renderTakeoffWorkspace();showUndo('Take-off item removed',()=>{d.takeoffs.splice(Math.min(index,d.takeoffs.length),0,old);save();renderTakeoffWorkspace()})};
const v184BaseCloseDrawingTakeoff=closeDrawingTakeoff;
closeDrawingTakeoff=function(){v184StorePdfCalibration();v184PdfRenderToken++;if(v184PdfRenderTask?.cancel)try{v184PdfRenderTask.cancel()}catch(error){}v184PdfDocument=null;v184PdfSource='';v184PdfPageCount=0;return v184BaseCloseDrawingTakeoff()};

/* ===== v185-release-candidate.js ===== */
'use strict';

// SiteSight V1.85 — release-candidate hardening, accessibility and final-use polish.

function v185Esc(value){return String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]))}

function v185Migration(){
  let changed=false;
  (db.users||[]).forEach(user=>{if(Number(user.permissionTemplateVersion||0)<185){user.permissionTemplateVersion=185;changed=true}});
  return changed
}
const v185BaseMigrate=migrate;
migrate=function(persist=true){v185BaseMigrate(false);let changed=v185Migration();if(persist&&(changed||persist))save()};
if((db.users||[]).length&&v185Migration())persistCache();

// Rich notes keep useful formatting but cannot retain pasted scripts, event handlers or embeds.
function v185PlainRichFallback(value){
  let text=String(value||'').replace(/<br\s*\/?>/gi,'\n').replace(/<\/(p|div|li)>/gi,'\n').replace(/<[^>]*>/g,'').replace(/\n{3,}/g,'\n\n');
  return v185Esc(text).replace(/\n/g,'<br>')
}
function v185SanitiseRichHtml(value){
  let source=String(value||'');if(!source)return'';
  let template=document.createElement?.('template');if(!template?.content?.querySelectorAll)return v185PlainRichFallback(source);
  template.innerHTML=source;
  const allowed=new Set(['B','STRONG','I','EM','U','BR','P','DIV','UL','OL','LI']),blocked=new Set(['SCRIPT','STYLE','IFRAME','OBJECT','EMBED','SVG','MATH','FORM','INPUT','BUTTON','VIDEO','AUDIO']);
  [...template.content.querySelectorAll('*')].reverse().forEach(element=>{
    if(blocked.has(element.tagName)){element.remove();return}
    if(!allowed.has(element.tagName)){element.replaceWith(...element.childNodes);return}
    [...element.attributes].forEach(attribute=>element.removeAttribute(attribute.name))
  });
  return template.innerHTML
}
const v185BaseNoteHtml=noteHtml;
noteHtml=function(item){return item?.notesHtml?v185SanitiseRichHtml(item.notesHtml):v185BaseNoteHtml(item)};
const v185BaseSaveRichNotes=saveRichNotes;
saveRichNotes=function(){let editor=document.getElementById('ne-rich');if(editor)editor.innerHTML=v185SanitiseRichHtml(editor.innerHTML);return v185BaseSaveRichNotes()};

// A deliberate shared-mode logout removes the local data copy after all changes have synced.
const v185BaseLogout=logout;
logout=function(){
  if(sharedMode&&pendingSave){toast('SiteSight is still saving. Reconnect and wait for Saved before logging out.');return false}
  let result=v185BaseLogout();
  if(sharedMode){
    try{[CACHE_KEY,BASE_KEY,PENDING_KEY,CURRENT_USER_KEY].forEach(key=>localStorage.removeItem(key))}catch(error){}
    db=cloneDB(seed);lastSyncedDB=null;sharedRevision=0;pendingSave=false;syncState='connecting';syncError=''
  }
  return result
};

// Clickable cards are also usable with a keyboard and announce their purpose to assistive technology.
function v185EnhanceInteractiveCards(root=document){
  if(!root?.querySelectorAll)return;
  root.querySelectorAll('.more-card[onclick],.admin-clickable[onclick],.clickable-listrow[onclick]').forEach(card=>{
    if(card.tagName==='BUTTON'||card.tagName==='A')return;card.tabIndex=0;card.setAttribute('role','button');
    card.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();card.click()}})
  });
  root.querySelectorAll('.modal').forEach(row=>{row.setAttribute('role','dialog');row.setAttribute('aria-modal','true')})
}
const v185BaseToast=toast;
toast=function(message){let result=v185BaseToast(message),rows=document.querySelectorAll?.('.toast')||[],latest=rows[rows.length-1];if(latest){latest.setAttribute('role','status');latest.setAttribute('aria-live','polite')}return result};
const v185BaseSavedState=savedState;
savedState=function(message='Saved'){let result=v185BaseSavedState(message),row=document.getElementById('saved-toast');if(row){row.setAttribute('role','status');row.setAttribute('aria-live','polite')}return result};

// Sign-in help describes the actual managed reset route; there is no misleading self-service email promise.
const v185BaseLoginScreen=loginScreen;
loginScreen=function(){v185BaseLoginScreen();let shell=document.querySelector?.('.auth-shell');if(shell&&typeof v183ReplaceWorkforceLanguage==='function')v183ReplaceWorkforceLanguage(shell);let help=[...(document.querySelectorAll?.('.auth-text-link')||[])].find(button=>/forgot password/i.test(button.textContent||''));if(help){help.textContent='Need sign-in help?';help.setAttribute('aria-label','Get help signing in')}};
const v185BaseForgotPassword=forgotPassword;
forgotPassword=function(){v185BaseForgotPassword();let title=document.querySelector?.('.auth-reset-card h2');if(title)title.textContent='Get back into your account';let intro=document.querySelector?.('.auth-reset-card .auth-welcome p');if(intro)intro.textContent='Ask your Director, or someone with Reset Passwords access, to issue a temporary password.'};

const v185BaseGuideSteps=guideSteps;
guideSteps=function(){return v185BaseGuideSteps().map(step=>step.id==='help'?{...step,bullets:[...step.bullets,'If you cannot sign in, use Need sign-in help and ask an authorised manager for a temporary password.']}:step)};
const v185BaseNewUserGuidePage=newUserGuidePage;
newUserGuidePage=function(){return v185BaseNewUserGuidePage().replace(/UPDATED FOR V1\.84/g,'RELEASE CANDIDATE · V1.85').replace(/updated guide covers the current tools/i,'release-candidate guide covers the current tools')};

const v185BaseShell=shell;
shell=function(content){
  v185BaseShell(content);let root=document.getElementById('root');if(!root)return;
  root.innerHTML=root.innerHTML.replace(/v1\.84\.0 · GUIDED CONTROL(?: DEMO)?/g,`v1.85.0 · RELEASE CANDIDATE${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`);
  v185EnhanceInteractiveCards(root)
};

/* ===== v187-desktop-test.js ===== */
'use strict';

// SiteSight V1.87 — desktop test: guarded plot sign-off, stable drawing take-off,
// site-scoped time/pay views and clearer measurable clocking savings.

let v187PaySiteIds=[],v187PendingTakeoffScroll=null;

function v187Esc(value){return String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]))}
function v187ActiveMembers(){return attendanceAllMembers().filter(row=>row.member.activeForAttendance!==false&&row.member.employmentStatus!=='Leaver'&&row.member.employmentStatus!=='Inactive')}
function v187MemberAssignedToSite(row,siteId,from='',to=''){
  if(!row||siteId==='all')return true;let member=row.member,user=v183MemberUser(member);
  return (member.clockSiteIds||[]).includes(siteId)||(user?.allowedSites||[]).includes(siteId)||
    (db.items||[]).some(item=>item.site===siteId&&item.assignedId===member.id)||
    (db.attendanceShifts||[]).some(shift=>shift.workerId===member.id&&shift.siteId===siteId&&!shift.voided&&(!from||attendanceDateOnly(shift.clockIn?.time,siteId)>=from)&&(!to||attendanceDateOnly(shift.clockIn?.time,siteId)<=to));
}

function v187Migration(){let changed=false;(db.users||[]).forEach(user=>{if(Number(user.permissionTemplateVersion||0)<187){user.permissionTemplateVersion=187;changed=true}});return changed}
const v187BaseMigrate=migrate;
migrate=function(persist=true){v187BaseMigrate(false);let changed=v187Migration();if(persist&&(changed||persist))save()};
if((db.users||[]).length&&v187Migration())persistCache();

// A plot is a completed construction record only when every lift is complete.
function v187PlotReadyForSignoff(item){return !!item&&item.type==='House Plot'&&Array.isArray(item.lifts)&&item.lifts.length>0&&item.lifts.every(lift=>lift.status==='Complete')}
const v187BaseSignOff=signOff;
signOff=function(){let item=(db.items||[]).find(row=>row.id===selected);if(item?.type==='House Plot'&&!v187PlotReadyForSignoff(item))return toast('Complete every lift before signing off this plot.');return v187BaseSignOff()};
const v187BaseConfirmSignOff=confirmSignOff;
confirmSignOff=function(){let item=(db.items||[]).find(row=>row.id===selected);if(item?.type==='House Plot'&&!v187PlotReadyForSignoff(item)){modal=null;toast('Sign-off blocked: every lift must be Complete.');return render()}return v187BaseConfirmSignOff()};

// Keep the drawing exactly where the estimator was working when a point causes
// the modal to be rebuilt. This covers image and page-aware PDF drawings.
function v187CaptureTakeoffScroll(){
  let stage=document.getElementById('v184-pdf-scroll')||document.querySelector?.('.takeoff-drawing-stage'),workspace=document.querySelector?.('.takeoff-workspace'),sidebar=document.querySelector?.('.takeoff-sidebar');
  return {stageTop:stage?.scrollTop||0,stageLeft:stage?.scrollLeft||0,workspaceTop:workspace?.scrollTop||0,sidebarTop:sidebar?.scrollTop||0}
}
function v187RestoreTakeoffScroll(position=v187PendingTakeoffScroll){
  if(!position)return;let stage=document.getElementById('v184-pdf-scroll')||document.querySelector?.('.takeoff-drawing-stage'),workspace=document.querySelector?.('.takeoff-workspace'),sidebar=document.querySelector?.('.takeoff-sidebar');
  if(stage){stage.scrollTop=position.stageTop;stage.scrollLeft=position.stageLeft}if(workspace)workspace.scrollTop=position.workspaceTop;if(sidebar)sidebar.scrollTop=position.sidebarTop
}
const v187BaseRenderTakeoffWorkspace=renderTakeoffWorkspace;
renderTakeoffWorkspace=function(){if(takeoffContext)v187PendingTakeoffScroll=v187CaptureTakeoffScroll();let result=v187BaseRenderTakeoffWorkspace();requestAnimationFrame(()=>v187RestoreTakeoffScroll());setTimeout(()=>v187RestoreTakeoffScroll(),80);return result};
const v187BaseRenderPdfPage=v184RenderPdfPage;
v184RenderPdfPage=async function(){let position=v187PendingTakeoffScroll||v187CaptureTakeoffScroll(),result=await v187BaseRenderPdfPage();v187RestoreTakeoffScroll(position);setTimeout(()=>v187RestoreTakeoffScroll(position),40);return result};
const v187BaseCloseDrawingTakeoff=closeDrawingTakeoff;
closeDrawingTakeoff=function(){v187PendingTakeoffScroll=null;return v187BaseCloseDrawingTakeoff()};

function v187AllTakeoffTotals(d=takeoffDrawing()){
  let rows=d?.takeoffs||[],gross=rows.filter(row=>row.kind==='Area').reduce((sum,row)=>sum+Math.abs(Number(row.quantity||0)),0),deductions=rows.filter(row=>row.kind==='Deduct Area').reduce((sum,row)=>sum+Math.abs(Number(row.quantity||0)),0),length=rows.filter(row=>row.kind==='Length').reduce((sum,row)=>sum+Number(row.quantity||0),0),count=rows.filter(row=>row.kind==='Count').reduce((sum,row)=>sum+Number(row.quantity||0),0),value=rows.reduce((sum,row)=>sum+takeoffValue(row),0);return {rows,gross,deductions,net:Math.max(0,gross-deductions),length,count,value,areas:rows.filter(row=>row.kind==='Area').length,deductionRows:rows.filter(row=>row.kind==='Deduct Area').length}
}
takeoffTotalsHtml=function(d=takeoffDrawing()){
  let total=v187AllTakeoffTotals(d),bricks=Math.ceil(total.net*60),blocks440=Math.ceil(total.net*9),blocks620=Math.ceil(total.net*7),page=v184PdfActive()?takeoffTotals(d):null;
  return `<section class="v187-house-takeoff"><div class="v187-takeoff-title"><span>HOUSE QUANTITY SUMMARY</span><b>${total.net.toFixed(2)} m² final</b></div>${page?`<div class="v187-page-net">Current page: <b>${Math.max(0,page.net).toFixed(2)} m² net</b></div>`:''}<div class="v187-area-math"><div><span>Areas added (${total.areas})</span><b>${total.gross.toFixed(2)} m²</b></div><i>−</i><div><span>Deductions (${total.deductionRows})</span><b>${total.deductions.toFixed(2)} m²</b></div><i>=</i><div class="net"><span>Final wall area</span><b>${total.net.toFixed(2)} m²</b></div></div><div class="v187-material-grid"><div><span>Bricks · 60/m²</span><b>${bricks.toLocaleString('en-GB')}</b></div><div><span>440mm blocks · 9/m²</span><b>${blocks440.toLocaleString('en-GB')}</b></div><div><span>620mm blocks · 7/m²</span><b>${blocks620.toLocaleString('en-GB')}</b></div></div><small>Approximate unit quantities based on the final measured wall area. Allowances and waste are not included.</small><div class="v187-other-totals"><span>Total length <b>${total.length.toFixed(2)} m</b></span><span>Count marks <b>${Math.round(total.count)}</b></span>${hasPerm('view_financials')?`<span>Measured value <b>${money(total.value)}</b></span>`:''}</div></section>`
};

// Site filtering follows direct individual assignments, not the legacy shared
// Individuals container. A temporary-site shift remains visible for its period.
v174PayMembers=function(siteId,from,to){return v187ActiveMembers().filter(row=>v187MemberAssignedToSite(row,siteId,from,to))};
v174PayFilterOptions=function(){let members=v187ActiveMembers().slice().sort((a,b)=>a.member.name.localeCompare(b.member.name));return `<option value="all" ${v174Payee==='all'?'selected':''}>All individuals</option><optgroup label="Choose individual">${members.map(row=>`<option value="id:${row.member.id}" ${v174Payee==='id:'+row.member.id?'selected':''}>${v187Esc(row.member.name)} · ${v187Esc(row.member.role||'Individual')}</option>`).join('')}</optgroup>`};

function v187CanAddManualHours(){return v175CanCorrectTime(currentUser())}
function v187OpenManualHours(){
  if(!v187CanAddManualHours())return toast('Manual hours require an authorised Supervisor or Manager.');let sites=activeSites(),people=v187ActiveMembers().slice().sort((a,b)=>a.member.name.localeCompare(b.member.name));
  modal=`<div class="modal"><div class="modalbox v187-manual-hours"><span class="att-kicker">MANUAL HOURS</span><h2>Add hours for a temporary Site</h2><p class="muted">Use this when somebody worked at another development and could not Clock In. The resulting shift is permanently labelled Supervisor Entry and remains auditable.</p><div class="two"><div class="field"><label>Individual</label><select id="v187-manual-person" class="select">${people.map(row=>`<option value="${row.member.id}">${v187Esc(row.member.name)} · ${v187Esc(row.member.role||'Individual')}</option>`).join('')}</select></div><div class="field"><label>Site worked</label><select id="v187-manual-site" class="select">${sites.map(row=>`<option value="${row.id}" ${row.id===attendanceDefaultSiteId()?'selected':''}>${v187Esc(row.name)}</option>`).join('')}</select></div></div><button class="btn orange" onclick="v187ContinueManualHours()">Continue to hours →</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
}
function v187ContinueManualHours(){let person=document.getElementById('v187-manual-person')?.value||'',siteId=document.getElementById('v187-manual-site')?.value||'';if(!person||!siteId)return toast('Choose an individual and Site');modal=null;addHistoricalAttendance(person,siteId);let heading=document.querySelector?.('.modalbox h2');if(heading)heading.insertAdjacentHTML('afterend',`<div class="v187-temporary-site"><b>${v187Esc(siteName(siteId))}</b><span>Temporary-site manual entry</span></div>`)}

const v187BaseTimesheetView=attendanceTimesheetView;
attendanceTimesheetView=function(siteId){let html=v187BaseTimesheetView(siteId),count=v174PayMembers(siteId,v174TimesheetFrom,v174TimesheetTo).length,button=v187CanAddManualHours()?'<button class="btn orange" onclick="v187OpenManualHours()">＋ Add manual hours</button>':'';html=html.replace('<div class="att-export-row">',`<div class="v187-timesheet-scope"><b>${v187Esc(siteName(siteId))}</b><span>${count} assigned individual${count===1?'':'s'} in this view</span></div><div class="att-export-row">${button}`);return html};

// Once every active person has a working pattern the bulk setup is moved into a
// collapsed maintenance control. Individual changes remain on each People record.
const v187BaseWorkingPatternPanel=v175WorkingPatternPanel;
v175WorkingPatternPanel=function(siteId){let content=v187BaseWorkingPatternPanel(siteId),people=v187ActiveMembers(),complete=people.length&&people.every(row=>!!row.member.workingSchedule);return complete?`<details class="panel v187-pattern-maintenance"><summary><span><b>Working patterns</b><small>Setup complete · use this only for future bulk changes</small></span><strong>Manage patterns ▾</strong></summary><div>${content}</div></details>`:content};

function v187SelectedPaySites(){let allowed=new Set(activeSites().map(row=>row.id)),chosen=v187PaySiteIds.filter(id=>allowed.has(id));return chosen.length?chosen:[...allowed]}
function v187PaySiteLabel(){let ids=v187SelectedPaySites();if(!v187PaySiteIds.length)return'All accessible sites';if(ids.length===1)return siteName(ids[0]);return `${ids.length} selected sites`}
function v187SetAllPaySites(){v187PaySiteIds=[];v174Payee='all';render()}
function v187TogglePaySite(siteId,checked){let set=new Set(v187PaySiteIds);if(checked)set.add(siteId);else set.delete(siteId);v187PaySiteIds=[...set];v174Payee='all';render()}
function v187PaySitePicker(){let ids=new Set(v187PaySiteIds);return `<details class="v187-site-picker"><summary>Sites: <b>${v187Esc(v187PaySiteLabel())}</b> ▾</summary><div><label><input type="checkbox" ${!ids.size?'checked':''} onchange="v187SetAllPaySites()"> <span>All accessible sites</span></label>${activeSites().map(row=>`<label><input type="checkbox" ${ids.has(row.id)?'checked':''} onchange="v187TogglePaySite('${row.id}',this.checked)"> <span>${v187Esc(row.name)}</span></label>`).join('')}</div></details>`}
function v187AttendanceTotalsForSites(workerId,from,to,siteIds){if(siteIds.length===activeSites().length)return v174AttendanceTotalsForRange(workerId,from,to,'all');let member=attendanceMemberById(workerId),relevant=siteIds.filter(id=>v187MemberAssignedToSite(member,id,from,to)),parts=relevant.map(id=>v174AttendanceTotalsForRange(workerId,from,to,id)),shifts=parts.flatMap(row=>row.shifts);return {shifts,details:parts.flatMap(row=>row.details),totalHours:parts.reduce((sum,row)=>sum+row.totalHours,0),payableHours:parts.reduce((sum,row)=>sum+row.payableHours,0),basicHours:parts.reduce((sum,row)=>sum+row.basicHours,0),overtimeHours:parts.reduce((sum,row)=>sum+row.overtimeHours,0),authorisedOvertimeHours:parts.reduce((sum,row)=>sum+row.authorisedOvertimeHours,0),unauthorisedOvertimeHours:parts.reduce((sum,row)=>sum+row.unauthorisedOvertimeHours,0),gross:parts.reduce((sum,row)=>sum+row.gross,0),days:new Set(shifts.map(row=>row.date)).size,fullDayTopUp:parts.reduce((sum,row)=>sum+row.fullDayTopUp,0),standardHours:parts.reduce((sum,row)=>sum+row.standardHours,0)}}
function v187RecordedPaymentsPanel(siteIds,from,to){let rows=allPaymentRows().filter(row=>siteIds.includes(row.item?.site)&&v174DateInRange(row.date,from,to)).map(row=>({...row,payeeKey:paymentPayeeKey(row),payeeKind:paymentPayeeKind(row)})).filter(v174PayRowMatches),total=rows.reduce((sum,row)=>sum+Number(row.amount||0),0);return `<section class="v174-recorded-payments"><div class="sectionhead"><div><h3>Payments recorded in this period</h3><div class="muted">Filtered to the selected Site scope and individual.</div></div><b>${attendanceMoney(total)}</b></div>${rows.map(row=>`<div class="v174-pay-person"><span><b>${v187Esc(paymentPayeeName(row))}</b><small>${v187Esc(siteName(row.item.site))} · ${v187Esc(row.item.ref)} · ${row.date}</small></span><span>${v187Esc(paymentPayeeKind(row))}</span><strong>${attendanceMoney(row.amount||0)}</strong></div>`).join('')||'<div class="muted">No recorded payments match this selection.</div>'}</section>`}
function v187DownloadPayReview(){
  let ids=v187SelectedPaySites();if(!v174RangeValid(v174PayFrom,v174PayTo))return toast('Choose a valid pay period');let rows=[['SiteSight Pay Review'],['Sites',ids.map(siteName).join(' | ')],['From',v174PayFrom],['To',v174PayTo],[],['Individual','Site','Employment Type','Recorded Hours','Payable Hours','Authorised OT','Estimated Gross']],escape=value=>'"'+String(value??'').replace(/"/g,'""')+'"';
  v187ActiveMembers().forEach(row=>ids.forEach(siteId=>{if(!v187MemberAssignedToSite(row,siteId,v174PayFrom,v174PayTo))return;let total=v174AttendanceTotalsForRange(row.member.id,v174PayFrom,v174PayTo,siteId);if(total.shifts.length||row.member.employmentType!=='Not tracked')rows.push([row.member.name,siteName(siteId),row.member.employmentType,total.totalHours.toFixed(2),total.payableHours.toFixed(2),total.authorisedOvertimeHours.toFixed(2),total.gross.toFixed(2)])}));let blob=new Blob([rows.map(row=>row.map(escape).join(',')).join('\r\n')],{type:'text/csv;charset=utf-8'}),link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download=`sitesight-pay-review-${v174PayFrom}-to-${v174PayTo}.csv`;link.click();setTimeout(()=>URL.revokeObjectURL(link.href),1000)
}

contractorPayReview=function(){
  if(!hasPerm('view_financials'))return '<div class="panel empty-help"><b>You do not have permission to view pay information.</b></div>';v174EnsurePayPeriod();let siteIds=v187SelectedPaySites(),valid=v174RangeValid(v174PayFrom,v174PayTo),members=valid?v187ActiveMembers().filter(row=>siteIds.some(id=>v187MemberAssignedToSite(row,id,v174PayFrom,v174PayTo))).filter(row=>['Hourly','Day Rate'].includes(row.member.employmentType)):[],
      timeRows=members.map(row=>({...row,payeeKey:'id:'+row.member.id,payeeKind:'Individual',tot:v187AttendanceTotalsForSites(row.member.id,v174PayFrom,v174PayTo,siteIds)})),setRows=valid?siteIds.flatMap(id=>v174SetPriceRows(id,v174PayFrom,v174PayTo).map(row=>({...row,siteId:id}))):[],variationRows=valid?siteIds.flatMap(id=>v174VariationRows(id,v174PayFrom,v174PayTo).map(row=>({...row,siteId:id}))):[],visibleTime=timeRows.filter(v174PayRowMatches),visibleSet=setRows.filter(v174PayRowMatches),visibleVariations=variationRows.filter(v174PayRowMatches),timeTotal=visibleTime.reduce((sum,row)=>sum+row.tot.gross,0),setDue=visibleSet.reduce((sum,row)=>sum+row.due,0),variationDue=visibleVariations.reduce((sum,row)=>sum+row.due,0),unpaidLate=visibleTime.reduce((sum,row)=>sum+row.tot.unauthorisedOvertimeHours,0),breakdown=[...visibleTime.map(row=>({name:row.member.name,type:row.member.employmentType,hours:row.tot.payableHours,due:row.tot.gross})),...visibleSet.map(row=>({name:row.payee,type:`Set Price · ${siteName(row.siteId)}`,hours:0,due:row.due})),...visibleVariations.map(row=>({name:row.payee,type:`Variation · ${siteName(row.siteId)}`,hours:0,due:row.due}))];
  let single=siteIds.length===1?siteIds[0]:'';return `${single?v176ExportProfilePanel(single)+v175PayrollWorkflow(single):'<div class="panel v187-multisite-note"><b>Multi-site review</b><span>Choose one Site when you need its locked payroll workflow or accounting export profile.</span></div>'}<div class="contractor-section v174-pay-review"><div class="sectionhead"><div><h2>Pay Review</h2><div class="muted">All individuals across the business, one Site, or any chosen group of Sites.</div></div><div class="actions">${v187PaySitePicker()}<select class="select" onchange="v174Payee=this.value;render()">${v174PayFilterOptions()}</select><button class="btn light" onclick="v187DownloadPayReview()">⇩ Export selection</button></div></div>${v174PeriodBar('pay',v174PayPreset,v174PayFrom,v174PayTo)}${!valid?'<div class="calendar-prompt"><b>Choose a valid From and To date.</b></div>':`<div class="pay-review-total"><span>RECORDS INDICATE FOR THIS PERIOD</span><b>${attendanceMoney(timeTotal+setDue+variationDue)}</b><small>${v187Esc(v187PaySiteLabel())} · operational review only.</small></div><div class="v174-pay-metrics"><div><span>Recorded hours</span><b>${visibleTime.reduce((sum,row)=>sum+row.tot.totalHours,0).toFixed(2)}</b></div><div><span>Payable hours</span><b>${visibleTime.reduce((sum,row)=>sum+row.tot.payableHours,0).toFixed(2)}</b></div><div class="${unpaidLate?'warn':''}"><span>Late time excluded</span><b>${unpaidLate.toFixed(2)} hrs</b></div><div><span>Authorised OT</span><b>${visibleTime.reduce((sum,row)=>sum+row.tot.authorisedOvertimeHours,0).toFixed(2)} hrs</b></div></div><div class="v174-individual-breakdown"><div class="sectionhead"><div><h3>Individual / payee breakdown</h3><div class="muted">Every amount remains attributable to a named person.</div></div><span>${breakdown.length}</span></div>${breakdown.map(row=>`<div class="v174-pay-person"><span><b>${v187Esc(row.name)}</b><small>${v187Esc(row.type)}</small></span><span>${row.hours?row.hours.toFixed(2)+' payable hrs':'Price record'}</span><strong>${attendanceMoney(row.due)}</strong></div>`).join('')||'<div class="muted">No pay records match this selection.</div>'}</div><div class="pay-review-grid"><section><h3>Set Price — completed in period</h3>${visibleSet.map(row=>`<div class="pay-review-row"><div><b>${v187Esc(row.ref)}</b><span>${v187Esc(siteName(row.siteId))} · ${v187Esc(row.stage)} · ${v187Esc(row.payee)}</span></div><strong>${row.due>0?attendanceMoney(row.due):'Paid'}</strong></div>`).join('')||'<div class="muted">No completed set-price work.</div>'}</section><section><h3>Hourly / Day Rate</h3>${visibleTime.map(row=>`<div class="pay-review-row"><div><b>${v187Esc(row.member.name)}</b><span>${v187Esc(row.member.employmentType)} · ${row.tot.totalHours.toFixed(2)} recorded · ${row.tot.payableHours.toFixed(2)} payable</span></div><strong>${attendanceMoney(row.tot.gross)}</strong></div>`).join('')||'<div class="muted">No Hourly or Day Rate records.</div>'}</section></div>${v187RecordedPaymentsPanel(siteIds,v174PayFrom,v174PayTo)}`}</div>`
};

// Put saved time and money directly beside each individual's hours and wages.
v169AttendanceAnalyticsPanel=function(){
  if(!hasPerm('view_attendance')&&!hasPerm('manage_attendance'))return'';let data=v169AttendanceAnalytics(),siteIds=v169SiteIdsForAnalytics(),hours=data.people.reduce((sum,row)=>sum+row.hours,0),wages=v183AnalyticsWages(data.from,data.to,siteIds),approvedOt=data.shifts.reduce((sum,shift)=>sum+Number(shift.overtimeApproval?.status==='Approved'?shift.overtimeApproval.minutes||0:0),0)/60,financial=hasPerm('view_financials'),savingByPerson=new Map();
  data.shifts.forEach(shift=>{let config=attendanceSiteConfig(shift.siteId)||{},minutes=Math.max(0,Number(config.analyticsManualMinutes??8)-Number(config.analyticsSystemMinutes??1)),cost=minutes/60*Number(config.analyticsAdminHourlyCost??25),row=savingByPerson.get(shift.workerId)||{minutes:0,cost:0};row.minutes+=minutes;row.cost+=cost;savingByPerson.set(shift.workerId,row)});
  return `<div class="panel v169-attendance-analytics v183-workforce-analytics"><div class="sectionhead"><div><h2>Individual Hours, Wages & Clocking Savings</h2><div class="muted">Recorded attendance, actual individual payments and the administration avoided by digital clocking.</div></div></div><div class="v169-rangebar"><button class="${v169AttendanceRange==='week'?'active':''}" onclick="v169AttendanceRange='week';render()">This Week</button><button class="${v169AttendanceRange==='month'?'active':''}" onclick="v169AttendanceRange='month';render()">This Month</button><button class="${v169AttendanceRange==='30'?'active':''}" onclick="v169AttendanceRange='30';render()">Last 30 Days</button><button class="${v169AttendanceRange==='custom'?'active':''}" onclick="v169AttendanceRange='custom';render()">Custom</button>${v169AttendanceRange==='custom'?`<input class="input" type="date" value="${data.from}" onchange="v169AttendanceFrom=this.value;render()"><span>→</span><input class="input" type="date" value="${data.to}" onchange="v169AttendanceTo=this.value;render()">`:''}<span class="muted">${data.from} → ${data.to}</span></div><div class="analytics-grid v183-total-cards v187-savings-cards"><div class="card"><div class="muted">Total hours worked</div><div class="analytics-value">${hours.toFixed(2)} hrs</div></div>${financial?`<div class="card"><div class="muted">Total wages paid</div><div class="analytics-value">${attendanceMoney(wages)}</div></div>`:''}<div class="card saved"><div class="muted">Timesheet admin saved</div><div class="analytics-value">${data.adminHoursSaved.toFixed(1)} hrs</div><small>Using the saved manual-vs-SiteSight assumptions.</small></div>${financial?`<div class="card saved"><div class="muted">Admin cost saved</div><div class="analytics-value">${attendanceMoney(data.adminCostSaved)}</div></div>`:''}<div class="card"><div class="muted">Authorised overtime</div><div class="analytics-value">${approvedOt.toFixed(2)} hrs</div></div><div class="card"><div class="muted">Completed shifts</div><div class="analytics-value">${data.shifts.length}</div></div></div><details class="v169-breakdown" open><summary>Individual breakdown (${data.people.length})</summary><div style="overflow:auto"><table class="analytics-table"><thead><tr><th>Individual</th><th>Shifts</th><th>Total hours</th><th>Admin time saved</th>${financial?'<th>Admin cost saved</th>':''}<th>Punctual days</th></tr></thead><tbody>${data.people.sort((a,b)=>b.hours-a.hours).map(row=>{let saving=savingByPerson.get(row.member.id)||{minutes:0,cost:0};return `<tr><td><b>${v187Esc(row.member.name)}</b></td><td>${row.shifts}</td><td>${row.hours.toFixed(2)}</td><td>${v169MinutesLabel(saving.minutes)}</td>${financial?`<td>${attendanceMoney(saving.cost)}</td>`:''}<td>${row.onTime} (${row.punctuality}%)</td></tr>`}).join('')||`<tr><td colspan="${financial?6:5}">No completed shifts in this period.</td></tr>`}</tbody></table></div></details><div class="v169-performance-note">Savings are estimates based on the manual and SiteSight processing minutes saved in each Site's attendance settings.</div></div>`
};

// Complete setup disappears from the Dashboard instead of occupying daily space.
const v187BaseSetupPanel=v175SetupPanel;
v175SetupPanel=function(){let steps=v175SetupSteps(),complete=steps.length&&steps.every(step=>step.done);if(complete){let user=currentUser();if(user&&!user.setupChecklistCompletedAt){user.setupChecklistCompletedAt=new Date().toISOString();save()}return''}return v187BaseSetupPanel()};

const v187BaseShell=shell;
shell=function(content){v187BaseShell(content);let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace(/v1\.85\.0 · RELEASE CANDIDATE(?: DEMO)?/g,`v1.87.0 · DESKTOP TEST${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`)};
const v187BaseNewUserGuidePage=newUserGuidePage;
newUserGuidePage=function(){return v187BaseNewUserGuidePage().replace(/RELEASE CANDIDATE · V1\.85/g,'DESKTOP TEST · V1.87')};

/* ===== v188-operational-pay-control.js ===== */
'use strict';

// SiteSight V1.88 — simpler work control, explicit payment milestones and
// scheduled-finish management attendance.

const V188_MANAGEMENT_ROLES=['Director','Contracts Manager','Site Manager','Supervisor'];

function v188Esc(value){return String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]))}
function v188ManagementUser(user=currentUser()){return !!user&&V188_MANAGEMENT_ROLES.includes(user.role)}
function v188ItemFullyPaid(item){let value=Number(itemTotalAuthorisedValue(item)||0),paid=Number(itemTotalPaidIncludingVariations(item)||0);return value<=0||paid+0.005>=value}
function v188AssignmentName(item){return item?.assignmentNotRequired?'Not Required':v183AssigneeName(item)}
function v188AssignmentOptions(selected='',notRequired=false){return `<option value="">To be Confirmed</option><option value="__not_required__" ${notRequired?'selected':''}>Not Required — shared / changing labour</option>${v183ActiveMembers().slice().sort((a,b)=>a.name.localeCompare(b.name)).map(member=>`<option value="${member.id}" ${!notRequired&&selected===member.id?'selected':''}>${v188Esc(member.name)}${member.role?' · '+v188Esc(member.role):''}</option>`).join('')}`}

const v188BaseMigrate=migrate;
migrate=function(persist=true){
  v188BaseMigrate(false);let changed=false;
  (db.sites||[]).forEach(row=>{let cfg=row.attendanceConfig=row.attendanceConfig||{};if(cfg.lateArrivalGraceMinutes===undefined){cfg.lateArrivalGraceMinutes=0;changed=true}if(cfg.earlyLeaveGraceMinutes===undefined){cfg.earlyLeaveGraceMinutes=5;changed=true}});
  (db.items||[]).forEach(item=>{
    if('plannedStart' in item){delete item.plannedStart;changed=true}if('targetComplete' in item){delete item.targetComplete;changed=true}
    if(item.assignmentNotRequired){item.assignedId='';item.assignedName='Not Required'}
    if(item.type==='House Plot')(item.lifts||[]).forEach(lift=>{if(lift.status!=='Complete'&&lift.completedDate){lift.completedDate='';changed=true}if(lift.assignedId===undefined){lift.assignedId=item.assignedId||'';changed=true}if(lift.assignmentNotRequired===undefined){lift.assignmentNotRequired=!!item.assignmentNotRequired;changed=true}});
    else if(item.status!=='Complete'&&item.completedDate){item.completedDate='';changed=true}
  });
  (db.users||[]).forEach(user=>{if(Number(user.permissionTemplateVersion||0)<188){user.permissionTemplateVersion=188;changed=true}});
  if(persist&&(changed||persist))save()
};

// Work Items use one consistent priority order and keep long groups collapsed.
function v188WorkGroup(title,rows,open=false){let cls=title.toLowerCase().replace(/\s+/g,'-'),visible=rows.slice(0,10),more=rows.slice(10);return `<details class="v188-work-group ${cls}" ${open?'open':''}><summary><span>${icon(title)} <b>${title}</b></span><strong>${rows.length}</strong></summary><div class="v188-work-group-body">${visible.length?`<div class="cards">${visible.map(card).join('')}</div>`:smartEmpty(`No ${title.toLowerCase()} work in this view.`,'','','','✓')}${more.length?`<details class="v182-expander"><summary>View ${more.length} more</summary><div class="cards v188-more-cards">${more.map(card).join('')}</div></details>`:''}</div></details>`}
workItems=function(){
  let range=v174RecordRanges.work,original=db.items,dated=original.filter(item=>!range.from&&!range.to||v174WorkItemDates(item).some(date=>v174DateInRange(date,range.from,range.to))),rows,filterMarkup,summaryMarkup;
  db.items=dated;try{rows=filtered();filterMarkup=filters();summaryMarkup=filterSummary()}finally{db.items=original}
  let withIssues=rows.filter(item=>hasPerm('view_issues')&&issues(item)>0),issueIds=new Set(withIssues.map(item=>item.id)),inProgress=rows.filter(item=>overall(item)==='In progress'&&!issueIds.has(item.id)),notStarted=rows.filter(item=>overall(item)==='Not started'&&!issueIds.has(item.id)),complete=rows.filter(item=>overall(item)==='Complete'&&!issueIds.has(item.id)),signedRows=rows.filter(item=>overall(item)==='Signed Off'&&!issueIds.has(item.id)),hasFilters=site!=='all'||gang!=='all'||type!=='all'||status!=='all'||signed!=='all'||!!q||!!range.from||!!range.to;
  let groups=rows.length?[v188WorkGroup('In Progress',inProgress,true),hasPerm('view_issues')?v188WorkGroup('Open Issues',withIssues,withIssues.length>0):'',v188WorkGroup('Not Started',notStarted),v188WorkGroup('Complete',complete),v188WorkGroup('Signed Off',signedRows)].join(''):(hasFilters?smartEmpty('No work items match these filters.','Clear the filters or try a different search.','Clear filters','resetWorkFilters()','⌕'):smartEmpty('No work items yet.','Create the first Work Item for this site to get started.',hasPerm('manage_workitems')?'＋ Create Work Item':'',hasPerm('manage_workitems')?'newItem()':'','▣'));
  return `<div class="topbar"><div><h1>Work Items</h1><div class="sub">In Progress → Open Issues → Not Started → Complete → Signed Off</div>${selectedSiteHeading()}</div>${hasPerm('manage_workitems')?'<button class="btn orange primary-action" onclick="newItem()">＋ Add work item</button>':''}</div>${v174RecordRangeBar('work','Work record date range')}<div class="panel">${filterMarkup}<div class="v188-work-list">${summaryMarkup}${groups}</div></div>`
};

// Programme deadlines were intentionally removed from this product surface.
contractorProgrammeState=function(){return null};
const v188BaseDetail=detail;
detail=function(item){return v188BaseDetail(item).replace(/<div class="contractor-plan-line">[\s\S]*?<\/div>/,'').replace(`Individual: ${v183Esc(v183AssigneeName(item))}`,`Assignment: ${v188Esc(v188AssignmentName(item))}`)};
const v188BaseReport=contractorWeeklyReportPage;
contractorWeeklyReportPage=function(){return v188BaseReport().replace(/<div><strong>0<\/strong><span>Behind programme<\/span><\/div>/,'').replace('Issues / Programme','Issues')};

// Plot and lift assignment can explicitly be unnecessary where labour changes.
const v188BaseNewItem=newItem;
newItem=function(forced=''){v188BaseNewItem(forced);if(!modal)return;modal=modal.replace(v183PersonOptions(),v188AssignmentOptions());render()};
const v188BaseCreateItem=createItem;
createItem=function(){let select=document.getElementById('ng'),notRequired=select?.value==='__not_required__';if(notRequired)select.value='';v188BaseCreateItem();if(notRequired&&selected){let item=db.items.find(row=>row.id===selected);if(item){item.assignmentNotRequired=true;item.assignedId='';item.assignedName='Not Required';(item.lifts||[]).forEach(lift=>{lift.assignmentNotRequired=true;lift.assignedId='';lift.paidTo='' });item.paidTo='';save();render()}}};
editWorkItemTeam=function(){let item=db.items.find(row=>row.id===selected);if(!item)return;if(!hasPerm('manage_workitems')&&!hasPerm('manage_gangs'))return toast('You do not have permission to change the assignment');modal=`<div class="modal"><div class="modalbox"><h2>Edit Work Assignment</h2><p class="muted">${v188Esc(item.ref)} · ${v188Esc(siteName(item.site))}</p><div class="field"><label>Assigned individual</label><select id="wit-team" class="select">${v188AssignmentOptions(item.assignedId,item.assignmentNotRequired)}</select></div><button class="btn orange" onclick="saveWorkItemTeam()">Save changes</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()};
saveWorkItemTeam=function(){let item=db.items.find(row=>row.id===selected);if(!item)return;let old=item.assignedId||'',value=document.getElementById('wit-team')?.value||'',notRequired=value==='__not_required__',next=notRequired?'':value,person=v183Member(next);item.gang=V183_INDIVIDUALS_ID;item.assignmentNotRequired=notRequired;item.assignedType=notRequired?'Shared Labour':'Individual';item.assignedId=next;item.assignedName=notRequired?'Not Required':(person?.name||'To be Confirmed');if(item.type==='House Plot')(item.lifts||[]).forEach(lift=>{if(!lift.assignedId||lift.assignedId===old){lift.assignedId=next;lift.assignmentNotRequired=notRequired}if(!lift.paidTo||lift.paidTo===old)lift.paidTo=next});else if(!item.paidTo||item.paidTo===old)item.paidTo=next;touch(item);save();emitNotification('work',item.site,`${item.ref}: assignment changed to ${item.assignedName}`,item.id);modal=null;savedState('Work assignment updated');render()};

function v188ProgressStatusChanged(kind,index=''){let status=document.getElementById(kind==='lift'?`lf-status-${index}`:'gd-status')?.value||'',wrap=document.getElementById(kind==='lift'?`v188-lift-date-${index}`:'v188-garden-date'),input=document.getElementById(kind==='lift'?`lf-date-${index}`:'gd-date');if(wrap)wrap.hidden=status!=='Complete';if(input){input.disabled=status!=='Complete'||!canSupervisor();if(status!=='Complete')input.value=''}markDirty()}
const v188BaseLift=lift;
lift=function(item,row,index){let html=v188BaseLift(item,row,index),statusField=`<div class="field v188-completion-date" id="v188-lift-date-${index}" ${row.status==='Complete'?'':'hidden'}><label>Completion date</label><input id="lf-date-${index}" class="input" type="date" value="${row.status==='Complete'?(row.completedDate||''):''}" ${canSupervisor()?'':'disabled'} onchange="markDirty()"></div>`,assignment=`<div class="field v188-lift-assignment"><label>Lift assignment</label><select id="lf-assigned-${index}" class="select" ${hasPerm('manage_workitems')||hasPerm('edit_lifts')?'':'disabled'} onchange="markDirty()">${v188AssignmentOptions(row.assignedId||item.assignedId,row.assignmentNotRequired)}</select><small>Use Not Required when the labour changes regularly.</small></div>`;html=html.replace(`onchange="markDirty()">${['Not started','Started','Complete'].map(s=>`<option ${row.status===s?'selected':''}>${s}</option>`).join('')}</select>`,`onchange="v188ProgressStatusChanged('lift',${index})">${['Not started','Started','Complete'].map(s=>`<option ${row.status===s?'selected':''}>${s}</option>`).join('')}</select>`);html=html.replace(/<div class="field"><label>Completion date<\/label><input id="lf-date-[\s\S]*?<\/div>/,statusField);html=html.replace('<div class="field"><label>Variation details</label>',assignment+'<div class="field"><label>Variation details</label>');html=html.replace(new RegExp(`<select id="lf-pct-${index}"[\\s\\S]*?<\\/select>`),`<select id="lf-pct-${index}" class="select" ${hasPerm('manage_payments')?'':'disabled'} onchange="markDirty()">${[0,50,100].map(value=>`<option value="${value}" ${Number(row.pct||0)===value?'selected':''}>${value}%</option>`).join('')}</select>`);return html};
saveLiftCard=function(index){let item=db.items.find(row=>row.id===selected),liftRow=item?.lifts?.[index];if(!liftRow)return;liftRow.status=document.getElementById(`lf-status-${index}`)?.value||liftRow.status;if(canManager()){liftRow.price=Number(document.getElementById(`lf-price-${index}`)?.value||0);liftRow.pct=Number(document.getElementById(`lf-pct-${index}`)?.value||0);liftRow.paidTo=document.getElementById(`lf-paidto-${index}`)?.value||liftRow.paidTo;liftRow.paymentNotes=document.getElementById(`lf-paynotes-${index}`)?.value||''}let assigned=document.getElementById(`lf-assigned-${index}`)?.value||'',notRequired=assigned==='__not_required__';liftRow.assignmentNotRequired=notRequired;liftRow.assignedId=notRequired?'':assigned;liftRow.completedDate=liftRow.status==='Complete'?(document.getElementById(`lf-date-${index}`)?.value||iso()):'';liftRow.variation=document.getElementById(`lf-var-${index}`)?.value||'';liftRow.notes=document.getElementById(`lf-notes-${index}`)?.value||'';touch(item);clearDirty();openLift=-1;save();emitNotification('work',item.site,`${item.ref}: ${liftRow.name} updated`,item.id);savedState('Lift changes saved');render()};
const v188BaseGarden=garden;
garden=function(item){let html=v188BaseGarden(item),date=`<div class="field v188-completion-date" id="v188-garden-date" ${item.status==='Complete'?'':'hidden'}><label>Completion date</label><input id="gd-date" class="input" type="date" value="${item.status==='Complete'?(item.completedDate||''):''}" ${canSupervisor()?'':'disabled'} onchange="markDirty()"></div>`;html=html.replace(`onchange="markDirty()">${['Not started','Started','Complete'].map(s=>`<option ${item.status===s?'selected':''}>${s}</option>`).join('')}</select>`,`onchange="v188ProgressStatusChanged('garden')">${['Not started','Started','Complete'].map(s=>`<option ${item.status===s?'selected':''}>${s}</option>`).join('')}</select>`);html=html.replace(/<div class="field"><label>Completion date<\/label><input id="gd-date"[\s\S]*?<\/div>/,date);html=html.replace(/<select id="gd-pct"[\s\S]*?<\/select>/,`<select id="gd-pct" class="select" ${hasPerm('manage_payments')?'':'disabled'} onchange="markDirty()">${[0,50,100].map(value=>`<option value="${value}" ${Number(item.pct||0)===value?'selected':''}>${value}%</option>`).join('')}</select>`);return html};
saveGardenCard=function(){let item=db.items.find(row=>row.id===selected);if(!item)return;if(canSupervisor()){item.status=document.getElementById('gd-status')?.value||item.status;item.completedDate=item.status==='Complete'?(document.getElementById('gd-date')?.value||iso()):'';item.notes=document.getElementById('gd-notes')?.value||''}if(hasPerm('manage_payments')){item.price=Number(document.getElementById('gd-price')?.value||0);item.pct=Number(document.getElementById('gd-pct')?.value||0);item.paidTo=document.getElementById('gd-paidto')?.value||item.paidTo;item.paymentNotes=document.getElementById('gd-paynotes')?.value||''}touch(item);clearDirty();save();emitNotification('work',item.site,`${item.ref}: work item updated`,item.id);savedState('Work changes saved');render()};

const v188BaseQuickProgress=saveQuickProgress;
saveQuickProgress=function(id){v188BaseQuickProgress(id);let item=db.items.find(row=>row.id===id);if(!item)return;if(item.type==='House Plot')(item.lifts||[]).forEach(lift=>{if(lift.status!=='Complete')lift.completedDate='' });else if(item.status!=='Complete')item.completedDate='';save()};
const v188BaseAssistProgress=assistDoProgress;
assistDoProgress=function(payload){v188BaseAssistProgress(payload);let item=db.items.find(row=>row.id===payload?.itemId);if(!item)return;if(item.type==='House Plot'){let liftRow=item.lifts?.[Number(payload.stageIndex)];if(liftRow&&liftRow.status!=='Complete')liftRow.completedDate=''}else if(item.status!=='Complete')item.completedDate='';save()};

// Signed-off work remains commercially visible when payment is outstanding.
const v188BaseCard=card;
card=function(item){let html=v188BaseCard(item);if(item?.signed&&!v188ItemFullyPaid(item)&&hasPerm('view_financials'))html=html.replace('</span><div class="sectionhead">','</span><span class="v188-unpaid-ribbon">NOT FULLY PAID</span><div class="sectionhead">');return html};

// Separate punctuality tolerances: any late arrival is visible; up to five
// minutes early leaving remains within grace by default.
v169ShiftPunctuality=function(shift,member){if(!shift?.clockIn?.time)return {late:0,early:0,onTime:false};let date=attendanceDateOnly(shift.clockIn.time,shift.siteId),schedule=v174MemberScheduleForDate(member,shift.siteId,date),cfg=attendanceSiteConfig(shift.siteId)||{},start=v169TimeMinutes(shift.clockIn.time,shift.siteId),finish=shift.clockOut?.time?v169TimeMinutes(shift.clockOut.time,shift.siteId):null,plannedStart=v169ParseTime(schedule.start,'07:30'),plannedEnd=v169ParseTime(schedule.end,'16:30'),lateGrace=Number(cfg.lateArrivalGraceMinutes??0),earlyGrace=Number(cfg.earlyLeaveGraceMinutes??5),late=schedule.active?Math.max(0,start-plannedStart):0,early=schedule.active&&finish!==null?Math.max(0,plannedEnd-finish):0,lateFlag=late>lateGrace,earlyFlag=early>earlyGrace;return {late:lateFlag?late:0,early:earlyFlag?early:0,onTime:schedule.active&&!lateFlag&&!earlyFlag,startMinutes:start,endMinutes:finish}};
const v188BaseConfigureAttendance=configureAttendanceLocation;
configureAttendanceLocation=function(siteId){v188BaseConfigureAttendance(siteId);if(!modal)return;let cfg=attendanceSiteConfig(siteId)||{},replacement=`<div class="two v188-grace-settings"><div class="field"><label>Late-arrival grace</label><select id="att-late-grace" class="select">${[0,3,5,10,15].map(value=>`<option value="${value}" ${Number(cfg.lateArrivalGraceMinutes??0)===value?'selected':''}>${value} minutes</option>`).join('')}</select><small>Default: 0 minutes</small></div><div class="field"><label>Early-leaving grace</label><select id="att-early-grace" class="select">${[0,3,5,10,15].map(value=>`<option value="${value}" ${Number(cfg.earlyLeaveGraceMinutes??5)===value?'selected':''}>${value} minutes</option>`).join('')}</select><small>Default: 5 minutes</small></div></div>`;modal=modal.replace(/<div class="field"><label>Late \/ early grace<\/label>[\s\S]*?<\/div>/,replacement);render()};
const v188BaseReadAttendanceRules=v169ReadAttendanceRules;
v169ReadAttendanceRules=function(siteId){let cfg=v188BaseReadAttendanceRules(siteId);cfg.lateArrivalGraceMinutes=Number(document.getElementById('att-late-grace')?.value??0);cfg.earlyLeaveGraceMinutes=Number(document.getElementById('att-early-grace')?.value??5);return cfg};

function v188ScheduledFinishIso(date,time,siteId){let zone=db.sites.find(row=>row.id===siteId)?.timeZone||'Europe/London',target=Date.parse(`${date}T${time}:00Z`),guess=new Date(target),parts=new Intl.DateTimeFormat('en-GB',{timeZone:zone,year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit',hourCycle:'h23'}).formatToParts(guess),get=name=>Number(parts.find(part=>part.type===name)?.value||0),shown=Date.UTC(get('year'),get('month')-1,get('day'),get('hour'),get('minute'),get('second')),offset=shown-guess.getTime();return new Date(target-offset).toISOString()}
function v188AutoCloseLocalManagement(now=new Date()){if(sharedMode||!v188ManagementUser())return false;let linked=attendanceLinkedMember(),active=linked?attendanceActiveShift(linked.member.id):null;if(!active)return false;let date=attendanceDateOnly(now.toISOString(),active.siteId),shiftDate=attendanceDateOnly(active.clockIn.time,active.siteId),schedule=v174MemberScheduleForDate(linked.member,active.siteId,date);if(date!==shiftDate||!schedule.active||v169TimeMinutes(now.toISOString(),active.siteId)<v169ParseTime(schedule.end,'16:30'))return false;let closeAt=v188ScheduledFinishIso(date,schedule.end,active.siteId);active.clockOut={time:closeAt,status:'Automatic Scheduled Finish',serverTime:false,locationRequested:false,actorId:'system',actorName:'SiteSight'};active.autoClockedOut=true;active.autoClockedOutAt=now.toISOString();active.overtimeApproval={status:'Not Requested',minutes:0,reason:'',manualRequest:true,requestedBy:'',requestedAt:'',approvedBy:'',approvedById:'',approvedAt:''};attendanceMarkApprovalNeedsRecheck(active.workerId,active.siteId,active.clockIn.time);audit('attendance','automatic clock out',active.id,`${linked.member.name}: automatically Clocked Out at scheduled finish ${schedule.end}`,active.siteId);save();toast(`Automatically Clocked Out at ${schedule.end}. Request overtime if you continued working.`);render();return true}
const v188BaseReminderTick=v175FinishReminderTick;
v175FinishReminderTick=function(now=new Date()){if(v188AutoCloseLocalManagement(now))return true;return v188BaseReminderTick(now)};

const v188BaseBreakdown=v174ShiftPayBreakdown;
v174ShiftPayBreakdown=function(shift,member){let row=v188BaseBreakdown(shift,member),request=shift?.overtimeApproval;if(!request?.manualRequest)return row;let requested=Math.max(0,Number(request.minutes||0)),approved=request.status==='Approved'?requested:0;return {...row,approval:request,lateMinutes:requested,approvedMinutes:approved,unauthorisedMinutes:Math.max(0,requested-approved),payableHours:row.payableHours+approved/60}};
function v188LatestManagementShift(){let linked=attendanceLinkedMember();return linked?(db.attendanceShifts||[]).filter(row=>row.workerId===linked.member.id&&!row.voided&&row.clockOut?.time&&row.autoClockedOut).sort((a,b)=>String(b.clockOut.time).localeCompare(String(a.clockOut.time)))[0]||null:null}
function v188OpenOvertimeRequest(shiftId){let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId),request=shift?.overtimeApproval||{};if(!shift||!v188ManagementUser()||shift.workerId!==currentUser()?.linkedMemberId)return toast('This overtime request is not available');modal=`<div class="modal"><div class="modalbox v188-overtime-request"><span class="att-kicker">MANUAL OVERTIME REQUEST</span><h2>Request additional time</h2><p class="muted">Your recorded shift ended automatically at the scheduled finish. Enter only the additional time actually worked.</p><div class="field"><label>Overtime minutes</label><input id="v188-ot-request-minutes" class="input" type="number" min="1" max="480" step="5" value="${Number(request.minutes||30)}"></div><div class="field"><label>Work completed <span class="muted">(optional)</span></label><textarea id="v188-ot-request-reason" class="textarea">${v188Esc(request.reason||'')}</textarea></div><button class="btn orange" onclick="v188SubmitOvertimeRequest('${shift.id}')">Send for review</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
async function v188SubmitOvertimeRequest(shiftId){let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId),minutes=Math.round(Number(document.getElementById('v188-ot-request-minutes')?.value||0)),reason=(document.getElementById('v188-ot-request-reason')?.value||'').trim();if(!shift||minutes<1||minutes>480)return toast('Enter between 1 and 480 minutes');if(sharedMode&&navigator.onLine){try{if(pendingSave)await syncToServer(true);let response=await fetch('/api/attendance/overtime-request',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({shiftId,minutes,reason})}),result=await response.json();if(!response.ok)throw new Error(result.message||'Overtime request could not be saved');if(result.data){db=result.data;sharedRevision=result.revision||sharedRevision;lastSyncedDB=cloneDB(db);pendingSave=false;persistCache()}modal=null;savedState('Overtime sent for review');render();return}catch(error){return toast(error.message)}}shift.overtimeApproval={status:'Pending',minutes,reason,manualRequest:true,requestedBy:currentUser()?.name||'',requestedById:currentUser()?.id||'',requestedAt:new Date().toISOString(),approvedBy:'',approvedById:'',approvedAt:''};attendanceMarkApprovalNeedsRecheck(shift.workerId,shift.siteId,shift.clockIn.time);save();audit('attendance','overtime request',shift.id,`${currentUser()?.name||'Management user'}: ${minutes} minutes overtime requested`,shift.siteId);modal=null;savedState('Overtime sent for review');render()}
function v188OvertimeRequestCard(){if(!v188ManagementUser())return'';let shift=v188LatestManagementShift();if(!shift)return'<div class="panel v188-management-rule"><b>Scheduled-finish clocking</b><span>You will be Clocked Out automatically at your scheduled finish. If work continues, request only the extra time actually worked.</span></div>';let request=shift.overtimeApproval||{},status=request.status||'Not Requested',canRequest=!['Pending','Approved'].includes(status);return `<div class="panel v188-management-rule"><div><b>Scheduled-finish clocking</b><span>Last automatic finish: ${v169TimeLabel(shift.clockOut.time,shift.siteId)} · ${v188Esc(siteName(shift.siteId))}</span><small>${status==='Pending'?`${request.minutes} minutes awaiting management review`:status==='Approved'?`${request.minutes} minutes approved`:status==='Rejected'?'Previous request rejected — submit again only if needed':'No overtime requested'}</small></div>${canRequest?`<button class="btn orange" onclick="v188OpenOvertimeRequest('${shift.id}')">＋ Request overtime</button>`:''}</div>`}
const v188BaseClockPage=v175ClockPage;
v175ClockPage=function(){let html=v188BaseClockPage();if(!v188ManagementUser())return html;html=html.replace('Clock only when recording paid attendance. Normal SiteSight use never starts a shift or requests location.','Clock In starts paid attendance. SiteSight automatically closes the shift at your scheduled finish; normal system use records no time or location.');return html+v188OvertimeRequestCard()};

const v188BaseOpenOvertime=v174OpenOvertimeApproval;
v174OpenOvertimeApproval=function(shiftId){let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId);if(!shift?.overtimeApproval?.manualRequest)return v188BaseOpenOvertime(shiftId);if(!v175CanApproveOvertime())return toast('Only an authorised Supervisor or Manager can decide overtime');let wm=attendanceMemberById(shift.workerId),request=shift.overtimeApproval;if(!wm||!Number(request.minutes||0))return toast('No overtime has been requested');modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">OVERTIME DECISION</span><h2>${v188Esc(wm.member.name)}</h2><p class="muted">${attendanceDateOnly(shift.clockIn.time,shift.siteId)} · automatically finished at ${v169TimeLabel(shift.clockOut.time,shift.siteId)}</p><div class="v174-overtime-warning"><b>${v169MinutesLabel(request.minutes)} manually requested</b><span>This time remains unpaid until authorised.</span></div><div class="field"><label>Authorised overtime minutes</label><input id="v174-ot-minutes" class="input" type="number" min="1" max="${request.minutes}" value="${request.minutes}"></div><div class="field"><label>Reason <span class="muted">(optional)</span></label><textarea id="v174-ot-reason" class="textarea">${v188Esc(request.reason||'')}</textarea></div><button class="btn green" onclick="v174SaveOvertimeApproval('${shift.id}')">✓ Authorise overtime</button> <button class="btn red" onclick="v178RejectOvertime('${shift.id}')">✕ Reject overtime</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()};
const v188BaseApproveOvertime=v174SaveOvertimeApproval;
v174SaveOvertimeApproval=function(shiftId){let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId),manual=!!shift?.overtimeApproval?.manualRequest;if(!manual)return v188BaseApproveOvertime(shiftId);if(!v175CanApproveOvertime())return toast('Only an authorised Supervisor or Manager can approve overtime');let wm=attendanceMemberById(shift.workerId),requested=Number(shift.overtimeApproval.minutes||0),minutes=Math.round(Number(document.getElementById('v174-ot-minutes')?.value||0)),reason=(document.getElementById('v174-ot-reason')?.value||'').trim();if(!wm||minutes<1||minutes>requested)return toast(`Enter between 1 and ${requested} minutes`);shift.overtimeApproval={...shift.overtimeApproval,status:'Approved',minutes,reason,manualRequest:true,approvedBy:currentUser()?.name||'',approvedById:currentUser()?.id||'',approvedAt:new Date().toISOString()};attendanceMarkApprovalNeedsRecheck(shift.workerId,shift.siteId,shift.clockIn.time);save();audit('attendance','overtime approval',shift.id,`${wm.member.name}: ${minutes} requested minutes authorised`,shift.siteId);modal=null;savedState('Overtime authorised');render()};
const v188BaseRejectOvertime=v178RejectOvertime;
v178RejectOvertime=function(shiftId){let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId),manual=!!shift?.overtimeApproval?.manualRequest;if(!manual)return v188BaseRejectOvertime(shiftId);if(!v175CanApproveOvertime())return toast('Only an authorised Supervisor or Manager can decide overtime');let wm=attendanceMemberById(shift.workerId),reason=(document.getElementById('v174-ot-reason')?.value||'').trim();if(!wm)return;shift.overtimeApproval={...shift.overtimeApproval,status:'Rejected',reason,manualRequest:true,approvedBy:currentUser()?.name||'',approvedById:currentUser()?.id||'',approvedAt:new Date().toISOString()};attendanceMarkApprovalNeedsRecheck(shift.workerId,shift.siteId,shift.clockIn.time);save();audit('attendance','overtime approval',shift.id,`${wm.member.name}: requested overtime rejected`,shift.siteId);modal=null;savedState('Overtime rejected');render()};v174DeclineOvertime=v178RejectOvertime;
const v188BasePendingOvertime=v174PendingOvertimePanel;
v174PendingOvertimePanel=function(siteId){return v188BasePendingOvertime(siteId).replace('Only clock-outs more than 15 minutes late appear here. Time stays unpaid until authorised.','Late GPS clock-outs and management overtime requests appear here. Time stays unpaid until authorised.')};

const v188BaseShell=shell;
shell=function(content){v188BaseShell(content);let root=document.getElementById('root');if(root){root.innerHTML=root.innerHTML.replace(/v1\.87\.0 · DESKTOP TEST(?: DEMO)?/g,`v1.88.0 · OPERATIONAL PAY CONTROL${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`);if(db.demoProfile==='v188-two-site-director')root.innerHTML=root.innerHTML.replace('4 active sites · 351 plots · full operational dataset','2 active sites · 99 plots · contrasting operational datasets')}};
const v188BaseGuide=newUserGuidePage;
newUserGuidePage=function(){return v188BaseGuide().replace(/DESKTOP TEST · V1\.87/g,'OPERATIONAL PAY CONTROL · V1.88')};
const v188BaseDashboard=dashboard;
dashboard=function(){let html=v188BaseDashboard();return db.demoProfile==='v188-two-site-director'?html.replace('ENTERPRISE DEMO · four active developments and 351 plots under full operational use','DIRECTOR DEMO · a smooth 29-home Site beside a complex 70-home Site'):html};
const v188BaseLoginScreen=loginScreen;
loginScreen=function(){v188BaseLoginScreen();if(db.demoProfile!=='v188-two-site-director')return;let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace('Four live developments. One business view.','Two contrasting developments. One clear business view.').replace(/351 plots, 62 people and realistic operational records — ready to demonstrate SiteSight at contractor scale\./g,'99 plots, 30 frontline workers and complete pay/time records — built for a clear Director demonstration.').replace(/ENTERPRISE DEMO · 4 ACTIVE SITES/g,'DIRECTOR DEMO · 2 ACTIVE SITES').replace(/25, 50, 96 and 180-home developments/g,'29-home smooth Site · 70-home complex Site').replace(/<code>site\.riverside<\/code><small>Site Manager · 180 homes<\/small>/g,'<code>site.oakfield</code><small>Site Manager · complex 70-home Site</small>').replace(/<code>supervisor\.oakfield<\/code><small>Supervisor · 96 homes<\/small>/g,'<code>supervisor.kingsbrook</code><small>Supervisor · smooth 29-home Site</small>').replace(/<code>worker\.riverside<\/code><small>Operator · immediate Clock In \/ Out<\/small>/g,'<code>worker.liam</code><small>Operator · GPS Clock In / Out</small>')};

if((db.users||[]).length)migrate(false);

/* ===== v189-attendance-functionality.js ===== */
'use strict';

// SiteSight V1.89 — GPS-verified scheduled finishes, manual overtime for
// everyone, and a Director-only Site Functionality control centre.

const V189_PERMISSION=['manage_functionality','Manage Site Functionality'];
if(!permissionDefs.some(([key])=>key===V189_PERMISSION[0]))permissionDefs.push(V189_PERMISSION);

function v189Director(user=currentUser()){return !!user&&user.role==='Director'&&!!user.permissions?.manage_functionality}
function v189GpsClockUser(user=currentUser()){return !!user&&!!user.linkedMemberId&&!!user.permissions?.clock_attendance&&!v188ManagementUser(user)}
function v189SiteMembers(siteId){return v183ActiveMembers().filter(member=>(member.clockSiteIds||[]).includes(siteId)||(db.items||[]).some(item=>item.site===siteId&&item.assignedId===member.id)).sort((a,b)=>a.name.localeCompare(b.name))}
function v189TimeLabel(value,fallback){return String(value||fallback)}

const v189BaseMigrate=migrate;
migrate=function(persist=true){
  v189BaseMigrate(false);let changed=false;
  (db.sites||[]).forEach(siteRow=>{let cfg=siteRow.attendanceConfig=siteRow.attendanceConfig||{};if(cfg.autoClockOutGps===undefined){cfg.autoClockOutGps=true;changed=true}});
  (db.users||[]).forEach(user=>{user.permissions=user.permissions||{};if(user.role==='Director'&&user.permissions.manage_functionality!==true){user.permissions.manage_functionality=true;changed=true}if(user.role!=='Director'&&user.permissions.manage_functionality){user.permissions.manage_functionality=false;changed=true}if(Number(user.permissionTemplateVersion||0)<189){user.permissionTemplateVersion=189;changed=true}});
  if(persist&&(changed||persist))save()
};

// ---- Manual overtime for every linked clocking user ----------------
function v189LatestRequestableShift(){
  let linked=attendanceLinkedMember();if(!linked)return null;
  return (db.attendanceShifts||[]).filter(row=>row.workerId===linked.member.id&&!row.voided&&row.clockOut?.time).sort((a,b)=>String(b.clockOut.time).localeCompare(String(a.clockOut.time)))[0]||null
}
v188LatestManagementShift=v189LatestRequestableShift;
v188OpenOvertimeRequest=function(shiftId){
  let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId),request=shift?.overtimeApproval||{},linked=attendanceLinkedMember();
  if(!shift||!linked||shift.workerId!==linked.member.id||!shift.clockOut?.time)return toast('This overtime request is not available');
  modal=`<div class="modal"><div class="modalbox v188-overtime-request"><span class="att-kicker">MANUAL OVERTIME REQUEST</span><h2>Request additional time</h2><p class="muted">Your normal shift remains capped at the scheduled finish. Enter only the additional time you actually worked.</p><div class="field"><label>Overtime minutes</label><input id="v188-ot-request-minutes" class="input" type="number" min="1" max="480" step="5" value="${Number(request.minutes||30)}"></div><div class="field"><label>Work completed <span class="muted">(optional)</span></label><textarea id="v188-ot-request-reason" class="textarea">${v188Esc(request.reason||'')}</textarea></div><button class="btn orange" onclick="v188SubmitOvertimeRequest('${shift.id}')">Send for review</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()
};
v188OvertimeRequestCard=function(){
  let user=currentUser(),linked=attendanceLinkedMember();if(!user||!linked||!user.permissions?.clock_attendance)return'';
  let shift=v189LatestRequestableShift();
  if(!shift)return'<div class="panel v188-management-rule"><b>Scheduled-finish clocking</b><span>Your normal shift will finish at the saved end time. Overtime is recorded only when you submit a request.</span></div>';
  let request=shift.overtimeApproval||{},status=request.status||'Not Requested',canRequest=!['Pending','Approved'].includes(status);
  return `<div class="panel v188-management-rule"><div><b>Overtime is request-only</b><span>Latest shift: ${v169TimeLabel(shift.clockOut.time,shift.siteId)} · ${v188Esc(siteName(shift.siteId))}</span><small>${status==='Pending'?`${request.minutes} minutes awaiting review`:status==='Approved'?`${request.minutes} minutes approved`:status==='Rejected'?'Previous request rejected — submit again only if genuinely needed':'No overtime requested'}</small></div>${canRequest?`<button class="btn orange" onclick="v188OpenOvertimeRequest('${shift.id}')">＋ Request overtime</button>`:''}</div>`
};
const v189BaseClockPage=v175ClockPage;
v175ClockPage=function(){let html=v189BaseClockPage();return v188ManagementUser()?html:html+v188OvertimeRequestCard()};

const v189BaseBreakdown=v174ShiftPayBreakdown;
v174ShiftPayBreakdown=function(shift,member){
  let row=v189BaseBreakdown(shift,member),request=shift?.overtimeApproval||{};
  if(request.manualRequest)return row;
  let actualAfterFinish=Number((row.rawLateMinutes??row.lateMinutes)||0);
  return {...row,actualAfterFinishMinutes:actualAfterFinish,lateMinutes:0,approvedMinutes:0,unauthorisedMinutes:0,approval:{status:'Not Requested',minutes:0,manualRequest:true}}
};

// ---- One fresh GPS check at scheduled finish -----------------------
const v189GpsAttempts=new Map();
function v189GpsFinishState(now=new Date()){
  let user=currentUser(),linked=attendanceLinkedMember(),active=linked?attendanceActiveShift(linked.member.id):null;
  if(!v189GpsClockUser(user)||!linked||!active)return null;
  let date=attendanceDateOnly(now.toISOString(),active.siteId),shiftDate=attendanceDateOnly(active.clockIn.time,active.siteId),schedule=v174MemberScheduleForDate(linked.member,active.siteId,date),cfg=attendanceSiteConfig(active.siteId)||{},due=date===shiftDate&&schedule.active&&v169TimeMinutes(now.toISOString(),active.siteId)>=v169ParseTime(schedule.end,'16:30');
  return due&&cfg.autoClockOutGps!==false?{user,linked,active,date,schedule,cfg}:null
}
function v189ApplyServerData(result){if(!result?.data)return;db=result.data;sharedRevision=result.revision||sharedRevision;lastSyncedDB=cloneDB(db);pendingSave=false;syncState='synced';lastSyncAt=new Date().toISOString();migrate(false);persistCache();try{localStorage.setItem(BASE_KEY,JSON.stringify(db));localStorage.removeItem(PENDING_KEY)}catch(error){}}
function v189GpsFinishFailure(state,message,now=new Date()){
  let key=`${state.active.id}:${state.date}`,last=Number(state.active.autoClockOutGpsNotifiedAtMs||0);state.active.autoClockOutGpsLastError=String(message||'Location could not be verified');state.active.autoClockOutGpsLastAttemptAt=now.toISOString();
  if(now.getTime()-last>10*60*1000){state.active.autoClockOutGpsNotifiedAtMs=now.getTime();db.notifications=db.notifications||[];db.notifications.unshift({id:`gps-finish-${Date.now()}`,kind:'clock-reminder',siteId:state.active.siteId,text:`Automatic Clock Out needs your attention: ${state.active.autoClockOutGpsLastError}`,itemId:state.active.id,targetUserId:state.user.id,actorId:'system',actorName:'SiteSight',createdAt:now.toISOString(),readBy:[]});db.notifications=db.notifications.slice(0,250);toast('Automatic Clock Out needs location confirmation. Open Clock In / Out.');deliverNotifications();save()}
  v189GpsAttempts.set(key,now.getTime());if(view==='clock'||view==='attendance')render()
}
async function v189AutoClockGps(state,now=new Date()){
  let key=`${state.active.id}:${state.date}`,last=v189GpsAttempts.get(key)||0;if(now.getTime()-last<2*60*1000)return false;v189GpsAttempts.set(key,now.getTime());
  if(!sharedMode||!navigator.onLine||!attendanceLocationConfigured(state.active.siteId))return v189GpsFinishFailure(state,'Open SiteSight online so the Site location can be verified.',now),false;
  try{
    if(pendingSave)await syncToServer(true);let pos=await attendanceGetPosition(),payload={action:'clock-out',autoScheduled:true,workerId:state.linked.member.id,siteId:state.active.siteId,actorId:state.user.id,latitude:pos.latitude,longitude:pos.longitude,accuracy:pos.accuracy,positionTimestamp:pos.positionTimestamp||Date.now(),sampleCount:pos.sampleCount||1,sessionId:attendanceSessionId(),device:attendanceDeviceInfo()},response=await fetch('/api/attendance/stamp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}),result=await response.json();
    v189ApplyServerData(result);if(!result.ok)throw new Error(result.message||'Current location is outside the Site clocking area');savedState(`Automatically Clocked Out at ${state.schedule.end} — location verified`);if(view==='clock'||view==='attendance')render();return true
  }catch(error){v189GpsFinishFailure(state,error.message||'Location could not be verified.',now);return false}
}
const v189BaseReminderTick=v175FinishReminderTick;
v175FinishReminderTick=function(now=new Date()){let gps=v189GpsFinishState(now);if(gps){v189AutoClockGps(gps,now);return true}return v189BaseReminderTick(now)};

// ---- Director Functionality control centre -------------------------
V173_MORE_TOOLS.functionality=['⚙','Functionality','All Site settings in one place'];
const v189BaseCanOpen=v173CanOpenView;
v173CanOpenView=function(target,user=currentUser()){if(target==='functionality')return v189Director(user);return v189BaseCanOpen(target,user)};
const v189BaseMoreIds=v173MoreToolIds;
v173MoreToolIds=function(user=currentUser()){let rows=v189BaseMoreIds(user);if(v189Director(user)&&!rows.includes('functionality'))rows.unshift('functionality');return rows};

function v189SettingRow(label,value,action){return `<div class="v189-setting-row"><span><b>${v188Esc(label)}</b><small>${v188Esc(value)}</small></span><button class="btn light smallbtn" onclick="${action}">Edit</button></div>`}
function v189FunctionalityPage(){
  if(!v189Director())return smartEmpty('Director access required.','Only a Director can change company-wide Site functionality.','','','⚙');
  let rows=activeSites().map(siteRow=>{let cfg=attendanceSiteConfig(siteRow.id)||{},members=v189SiteMembers(siteRow.id);return `<details class="panel v189-site-settings" ${site==='all'||site===siteRow.id?'open':''}><summary><span><b>${v188Esc(siteRow.name)}</b><small>${members.length} active individual${members.length===1?'':'s'} · ${cfg.enabled===false?'Clocking disabled':'Clocking enabled'}</small></span><strong>Manage</strong></summary><div class="v189-settings-list">${v189SettingRow('Site default working day',`${v189TimeLabel(cfg.expectedStartTime,'07:30')}–${v189TimeLabel(cfg.expectedEndTime,'16:30')}`,`v189OpenSiteSettings('${siteRow.id}','hours')`)}${v189SettingRow('Individual working hours',`${members.length} people assigned`, `v189OpenSitePeople('${siteRow.id}')`)}${v189SettingRow('GPS geofence',attendanceLocationConfigured(siteRow.id)?`${Number(cfg.radiusM||250)}m radius · GPS ±${Number(cfg.maxAccuracyM||75)}m`:'Site location not configured',`v189OpenSiteSettings('${siteRow.id}','gps')`)}${v189SettingRow('Scheduled Clock Out',cfg.autoClockOutGps===false?'Manual Clock Out':'Automatic after a fresh GPS check',`v189OpenSiteSettings('${siteRow.id}','clock')`)}${v189SettingRow('Punctuality grace',`${Number(cfg.lateArrivalGraceMinutes??0)}m late · ${Number(cfg.earlyLeaveGraceMinutes??5)}m early`, `v189OpenSiteSettings('${siteRow.id}','grace')`)}${v189SettingRow('Shift safeguards',`${Number(cfg.maxShiftHours||12)} hour warning`, `v189OpenSiteSettings('${siteRow.id}','shift')`)}${v189SettingRow('Analytics assumptions',`${Number(cfg.analyticsManualMinutes||8)}m manual → ${Number(cfg.analyticsSystemMinutes||1)}m SiteSight · £${Number(cfg.analyticsAdminHourlyCost||25)}/hr`, `v189OpenSiteSettings('${siteRow.id}','analytics')`)}${v189SettingRow('Lift template',`${(siteRow.stageTemplate||[]).length||5} stages`, `v189OpenSiteSettings('${siteRow.id}','lifts')`)}</div></details>`}).join('');
  return `<div class="topbar"><div><span class="att-kicker">DIRECTOR CONTROL</span><h1>Functionality</h1><div class="sub">Every Site setting in one clear list. Open a Site, then edit only the row you need.</div></div></div><div class="v189-functionality-intro"><b>Changes apply to future clocking and operational records.</b><span>Existing approved timesheets and historical pay records are retained.</span></div>${rows||smartEmpty('No active Sites.','Create a Site in Admin before configuring functionality.','','','⚙')}`
}
const v189BaseAdminPage=adminPage;
adminPage=function(){return view==='functionality'?v189FunctionalityPage():v189BaseAdminPage()};
function v189OpenSiteSettings(siteId,focus=''){
  if(!v189Director())return toast('Director permission is required');let siteRow=db.sites.find(row=>row.id===siteId),cfg=attendanceSiteConfig(siteId);if(!siteRow||!cfg)return;
  modal=`<div class="modal"><div class="modalbox v189-settings-modal"><span class="att-kicker">SITE FUNCTIONALITY</span><h2>${v188Esc(siteRow.name)}</h2><p class="muted">Edit the Site rules below. Individual hours are managed from the separate Individual working hours list.</p><div class="v189-modal-grid"><div class="field"><label>Site name</label><input id="v189-site-name" class="input" value="${v188Esc(siteRow.name)}"></div><div class="field"><label>Time zone</label><select id="v189-time-zone" class="select"><option value="Europe/London" ${siteRow.timeZone!=='UTC'?'selected':''}>United Kingdom</option><option value="UTC" ${siteRow.timeZone==='UTC'?'selected':''}>UTC</option></select></div><div class="field"><label>Default start</label><input id="v189-start" class="input" type="time" value="${cfg.expectedStartTime||'07:30'}"></div><div class="field"><label>Default finish</label><input id="v189-finish" class="input" type="time" value="${cfg.expectedEndTime||'16:30'}"></div><div class="field"><label>Late-arrival grace</label><select id="v189-late" class="select">${[0,3,5,10,15].map(value=>`<option value="${value}" ${Number(cfg.lateArrivalGraceMinutes??0)===value?'selected':''}>${value} minutes</option>`).join('')}</select></div><div class="field"><label>Early-leaving grace</label><select id="v189-early" class="select">${[0,3,5,10,15].map(value=>`<option value="${value}" ${Number(cfg.earlyLeaveGraceMinutes??5)===value?'selected':''}>${value} minutes</option>`).join('')}</select></div><div class="field"><label>Geofence radius</label><select id="v189-radius" class="select">${[50,75,100,150,200,250,300].map(value=>`<option value="${value}" ${Number(cfg.radiusM||250)===value?'selected':''}>${value} metres</option>`).join('')}</select></div><div class="field"><label>Maximum GPS uncertainty</label><select id="v189-accuracy" class="select">${[20,30,50,75,100,150].map(value=>`<option value="${value}" ${Number(cfg.maxAccuracyM||75)===value?'selected':''}>±${value} metres</option>`).join('')}</select></div><div class="field"><label>Long-shift warning</label><select id="v189-max-shift" class="select">${[10,11,12,13,14,16].map(value=>`<option value="${value}" ${Number(cfg.maxShiftHours||12)===value?'selected':''}>${value} hours</option>`).join('')}</select></div><label class="v189-toggle"><input id="v189-enabled" type="checkbox" ${cfg.enabled===false?'':'checked'}><span><b>Enable Site clocking</b><small>Allow linked people to Clock In / Out.</small></span></label><label class="v189-toggle"><input id="v189-auto-gps" type="checkbox" ${cfg.autoClockOutGps===false?'':'checked'}><span><b>GPS-verified scheduled Clock Out</b><small>At shift finish, take one fresh location reading and close the shift if the person is inside the geofence.</small></span></label><div class="field"><label>Manual admin minutes / person / day</label><input id="v189-manual" class="input" type="number" min="0" max="60" value="${Number(cfg.analyticsManualMinutes||8)}"></div><div class="field"><label>SiteSight minutes / person / day</label><input id="v189-system" class="input" type="number" min="0" max="60" value="${Number(cfg.analyticsSystemMinutes||1)}"></div><div class="field"><label>Admin cost / hour</label><input id="v189-cost" class="input" type="number" min="0" step="0.5" value="${Number(cfg.analyticsAdminHourlyCost||25)}"></div><div class="field v189-wide"><label>Lift template <span class="muted">(one per line)</span></label><textarea id="v189-stages" class="textarea">${v188Esc((siteRow.stageTemplate||[]).join('\n'))}</textarea></div></div><div class="v189-location-box"><span>${attendanceLocationConfigured(siteId)?`Current location: ${Number(cfg.latitude).toFixed(6)}, ${Number(cfg.longitude).toFixed(6)}`:'No Site location configured'}</span><button class="btn light smallbtn" onclick="v189SetSiteLocation('${siteId}')">⌖ Use this device location</button></div><div id="v189-settings-status" class="att-clock-progress"></div><div class="actions"><button class="btn orange" onclick="v189SaveSiteSettings('${siteId}')">Save Site settings</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()
}
async function v189SetSiteLocation(siteId){if(!v189Director())return;let status=document.getElementById('v189-settings-status');if(status)status.textContent='Taking fresh GPS readings…';try{let pos=await attendanceGetPosition(),cfg=attendanceSiteConfig(siteId);cfg.latitude=pos.latitude;cfg.longitude=pos.longitude;cfg.locationSetAt=new Date().toISOString();cfg.locationSetBy=currentUser()?.name||'';cfg.demoPreset=false;save();savedState('Site location updated');v189OpenSiteSettings(siteId,'gps')}catch(error){if(status)status.textContent=error.message;toast(error.message)}}
function v189SaveSiteSettings(siteId){
  if(!v189Director())return toast('Director permission is required');let siteRow=db.sites.find(row=>row.id===siteId),cfg=attendanceSiteConfig(siteId),name=(document.getElementById('v189-site-name')?.value||'').trim(),start=document.getElementById('v189-start')?.value||'07:30',finish=document.getElementById('v189-finish')?.value||'16:30';if(!siteRow||!cfg||!name)return toast('Enter a Site name');if(v169ParseTime(finish)<=v169ParseTime(start))return toast('Finish time must be after start time');
  siteRow.name=name;siteRow.timeZone=document.getElementById('v189-time-zone')?.value||'Europe/London';cfg.expectedStartTime=start;cfg.expectedEndTime=finish;cfg.lateArrivalGraceMinutes=Number(document.getElementById('v189-late')?.value??0);cfg.earlyLeaveGraceMinutes=Number(document.getElementById('v189-early')?.value??5);cfg.radiusM=Number(document.getElementById('v189-radius')?.value||250);cfg.maxAccuracyM=Number(document.getElementById('v189-accuracy')?.value||75);cfg.maxShiftHours=Number(document.getElementById('v189-max-shift')?.value||12);cfg.enabled=!!document.getElementById('v189-enabled')?.checked;cfg.autoClockOutGps=!!document.getElementById('v189-auto-gps')?.checked;cfg.analyticsManualMinutes=Number(document.getElementById('v189-manual')?.value||8);cfg.analyticsSystemMinutes=Number(document.getElementById('v189-system')?.value||1);cfg.analyticsAdminHourlyCost=Number(document.getElementById('v189-cost')?.value||25);siteRow.stageTemplate=(document.getElementById('v189-stages')?.value||'').split(/\n+/).map(value=>value.trim()).filter(Boolean);if(!siteRow.stageTemplate.length)return toast('Keep at least one lift stage');save();audit('site','functionality',siteId,`${name}: Site functionality updated`,siteId,JSON.stringify({start,finish,radiusM:cfg.radiusM,autoClockOutGps:cfg.autoClockOutGps}));modal=null;savedState('Site functionality saved');render()
}
function v189OpenSitePeople(siteId){
  if(!v189Director())return toast('Director permission is required');let members=v189SiteMembers(siteId);modal=`<div class="modal"><div class="modalbox v189-people-modal"><span class="att-kicker">INDIVIDUAL WORKING HOURS</span><h2>${v188Esc(siteName(siteId))}</h2><p class="muted">Change a person’s own pattern when it differs from the Site default.</p><div class="v189-people-list">${members.map(member=>{let schedule=v174MemberScheduleForDate(member,siteId,iso());return `<div><span><b>${v188Esc(member.name)}</b><small>${schedule.active?`${schedule.start}–${schedule.end}`:'Not scheduled today'} · ${v188Esc(member.employmentType||'Not tracked')}</small></span><button class="btn light smallbtn" onclick="modal=null;v174OpenWorkingHours('${member.id}','${siteId}')">Edit hours</button></div>`}).join('')||'<div class="muted">No individuals are assigned to this Site.</div>'}</div><button class="btn light" onclick="modal=null;render()">Close</button></div></div>`;render()
}

const v189BaseShell=shell;
shell=function(content){v189BaseShell(content);let root=document.getElementById('root');if(root)root.innerHTML=root.innerHTML.replace(/v1\.88\.0 · OPERATIONAL PAY CONTROL(?: DEMO)?/g,`v1.89.0 · ATTENDANCE & FUNCTIONALITY${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`)};
const v189BaseGuide=newUserGuidePage;
newUserGuidePage=function(){return v189BaseGuide().replace(/OPERATIONAL PAY CONTROL · V1\.88/g,'ATTENDANCE & FUNCTIONALITY · V1.89')};

if((db.users||[]).length)migrate(false);

/* ===== v190-pay-run-attendance.js ===== */
'use strict';

// SiteSight V1.90 — current production runtime additions only.
// Operational pay preparation is included. Statutory payroll, tax, pension,
// CIS deductions and bank payment execution are deliberately out of scope.

const V190_PAY_PERMISSIONS=[
  ['view_pay_runs','View pay runs'],
  ['view_pay_rates','View individual pay rates'],
  ['edit_pay_records','Edit pay records'],
  ['approve_pay_runs','Approve and lock pay runs'],
  ['export_pay_runs','Export locked pay runs'],
  ['mark_payments_paid','Mark approved pay as paid'],
  ['view_sensitive_financials','View private financial settings']
];
let v190CommercialGroup=permissionGroups.find(group=>group.name==='Commercial');
if(!v190CommercialGroup){v190CommercialGroup={name:'Commercial',items:[]};permissionGroups.push(v190CommercialGroup)}
for(const definition of V190_PAY_PERMISSIONS){
  if(!v190CommercialGroup.items.some(([key])=>key===definition[0]))v190CommercialGroup.items.push(definition);
  if(!permissionDefs.some(([key])=>key===definition[0]))permissionDefs.push(definition)
}

V173_MORE_TOOLS['pay-run']=['£','Pay Run','Resolve exceptions, approve, lock and export'];
const v190BaseCanOpenView=v173CanOpenView;
v173CanOpenView=function(target,user=currentUser()){
  if(target==='pay-run')return v173UserHas(user,'view_pay_runs');
  return v190BaseCanOpenView(target,user)
};
for(const roleName of ['Quantity Surveyor','Accounts']){
  let profile=V173_ROLE_PROFILES[roleName];
  if(profile){profile.home='pay-run';profile.main=['pay-run',...profile.main.filter(id=>id!=='pay-run')]}
}

function v190RolePayPermissions(roleName){
  const grants={
    Director:V190_PAY_PERMISSIONS.map(([key])=>key),
    'Contracts Manager':['view_pay_runs','view_pay_rates','edit_pay_records','approve_pay_runs','export_pay_runs','mark_payments_paid'],
    'Quantity Surveyor':['view_pay_runs','view_pay_rates','edit_pay_records','approve_pay_runs','export_pay_runs'],
    Accounts:['view_pay_runs','view_pay_rates','edit_pay_records','export_pay_runs']
  };
  return new Set(grants[roleName]||[])
}
for(const roleName of ['Contracts Manager','Quantity Surveyor','Accounts']){let profile=V173_ROLE_PROFILES[roleName],grants=v190RolePayPermissions(roleName);if(profile&&Array.isArray(profile.permissions))for(const key of grants)if(!profile.permissions.includes(key))profile.permissions.push(key)}
function v190Migrate(){
  let changed=false;db.payRuns=Array.isArray(db.payRuns)?db.payRuns:[];db.deviceRegistrations=Array.isArray(db.deviceRegistrations)?db.deviceRegistrations:[];db.attendanceEvidence=Array.isArray(db.attendanceEvidence)?db.attendanceEvidence:[];
  for(const user of db.users||[]){
    if(Number(user.permissionTemplateVersion||0)>=190)continue;
    let grants=v190RolePayPermissions(user.role);for(const [key] of V190_PAY_PERMISSIONS){let next=user.role==='Director'||grants.has(key);if(!!user.permissions?.[key]!==next){user.permissions=user.permissions||{};user.permissions[key]=next;changed=true}}
    user.permissionTemplateVersion=190;changed=true
  }
  for(const siteRow of db.sites||[]){
    let cfg=siteRow.attendanceConfig=siteRow.attendanceConfig||{};
    if(!Array.isArray(cfg.geofenceZones)){cfg.geofenceZones=[];if(attendanceLocationConfigured(siteRow.id))cfg.geofenceZones.push({id:`zone-${siteRow.id}`,name:'Main Site',type:'circle',latitude:Number(cfg.latitude),longitude:Number(cfg.longitude),radiusM:Number(cfg.radiusM||250),active:true});changed=true}
  }
  for(const item of db.items||[]){
    let targets=item.type==='House Plot'?(item.lifts||[]):[item];
    for(const target of targets){
      if(!Array.isArray(target.priceworkAllocations)){
        let member=v183Member(target.paidTo||item.assignedId||'');target.priceworkAllocations=member?.employmentType==='Price Work'?[{memberId:member.id,sharePercent:100}]:[];changed=true
      }
      for(const payment of target.payments||[])if(!payment.paymentState){payment.paymentState='Paid';changed=true}
    }
  }
  return changed
}
const v190BaseMigrate=migrate;
migrate=function(persist=true){v190BaseMigrate(false);let changed=v190Migrate();if(persist&&(changed||persist))save()};
if(v190Migrate())persistCache();

// ---- Effective-dated rates and payable time ------------------------
function v190RateRecord(member,date){
  let rate={employmentType:member?.employmentType||'Not tracked',basePay:Number(member?.basePay||member?.hourlyRate||0),hourlyRate:Number(member?.hourlyRate||member?.basePay||0),overtimeRate:Number(member?.overtimeRate||member?.hourlyRate||member?.basePay||0),dayRate:Number(member?.dayRate||0)};
  let history=[...(member?.payHistory||[]),...(member?.payRateHistory||[])],later=history.filter(row=>row.effectiveFrom&&String(row.effectiveFrom).slice(0,10)>date&&row.previous).sort((a,b)=>String(b.effectiveFrom).localeCompare(String(a.effectiveFrom)));
  for(const change of later)rate={...rate,...change.previous};
  rate.basePay=Number(rate.basePay||rate.hourlyRate||0);rate.hourlyRate=Number(rate.hourlyRate||rate.basePay||0);rate.overtimeRate=Number(rate.overtimeRate||rate.hourlyRate||0);rate.dayRate=Number(rate.dayRate||0);return rate
}
function v190ScheduledIso(date,time,siteId){return typeof v188ScheduledFinishIso==='function'?v188ScheduledFinishIso(date,time,siteId):new Date(`${date}T${time}:00`).toISOString()}
function v190ShiftCalculation(shift,member){
  let date=attendanceDateOnly(shift?.clockIn?.time,shift?.siteId),schedule=v174MemberScheduleForDate(member,shift?.siteId,date),clockIn=new Date(shift?.clockIn?.time||0),clockOut=shift?.clockOut?.time?new Date(shift.clockOut.time):null,rate=v190RateRecord(member,date),approval=shift?.overtimeApproval||{},approvedMinutes=approval.status==='Approved'&&approval.manualRequest?Math.max(0,Number(approval.minutes||0)):0;
  if(!clockOut||Number.isNaN(clockIn.getTime())||Number.isNaN(clockOut.getTime()))return {date,schedule,rate,recordedHours:0,payableHours:0,basicHours:0,overtimeHours:0,approvedMinutes,unauthorisedMinutes:0,gross:0};
  let scheduledStart=new Date(v190ScheduledIso(date,schedule.start,shift.siteId)),scheduledEnd=new Date(v190ScheduledIso(date,schedule.end,shift.siteId)),paidStart=schedule.active&&clockIn<scheduledStart?scheduledStart:clockIn,regularEnd=schedule.active&&clockOut>scheduledEnd?scheduledEnd:clockOut,raw=Math.max(0,(clockOut-clockIn)/3600000-Number(shift.breakMinutes||0)/60),basic=Math.max(0,(regularEnd-paidStart)/3600000-Number(shift.breakMinutes||0)/60),actualLate=schedule.active?Math.max(0,(clockOut-scheduledEnd)/60000):0,overtimeHours=approvedMinutes/60,payable=basic+overtimeHours,gross=0;
  if(['Hourly','Day Rate'].includes(rate.employmentType))gross=basic*rate.hourlyRate+overtimeHours*rate.overtimeRate;
  if(rate.employmentType==='Day Rate'&&v169FullDayOverride(member.id,date,shift.siteId))gross+=Math.max(0,rate.dayRate-basic*rate.hourlyRate);
  return {date,schedule,rate,recordedHours:raw,payableHours:payable,basicHours:basic,overtimeHours,approvedMinutes,lateMinutes:approval.manualRequest?Number(approval.minutes||0):actualLate,actualLateMinutes:actualLate,unauthorisedMinutes:Math.max(0,Number(approval.manualRequest?approval.minutes:actualLate)-approvedMinutes),approval,gross}
}
v174ShiftPayBreakdown=function(shift,member){return v190ShiftCalculation(shift,member)};
v174AttendanceTotalsForRange=function(workerId,from,to,siteId='all'){
  let wm=attendanceMemberById(workerId),member=wm?.member||{},shifts=v174ShiftsForRange(workerId,from,to,siteId),details=shifts.map(shift=>({shift,...v190ShiftCalculation(shift,member)})),sum=key=>details.reduce((total,row)=>total+Number(row[key]||0),0),days=new Set(details.map(row=>row.date)).size;
  return {shifts,details,totalHours:sum('recordedHours'),payableHours:sum('payableHours'),basicHours:sum('basicHours'),overtimeHours:sum('overtimeHours'),authorisedOvertimeHours:sum('overtimeHours'),unauthorisedOvertimeHours:sum('unauthorisedMinutes')/60,gross:sum('gross'),days,fullDayTopUp:0,standardHours:v174ExpectedStandardHours(member,siteId==='all'?(shifts[0]?.siteId||attendanceDefaultSiteId()):siteId,from,to),rateLines:details.map(row=>({date:row.date,hourlyRate:row.rate.hourlyRate,overtimeRate:row.rate.overtimeRate,dayRate:row.rate.dayRate,employmentType:row.rate.employmentType}))}
};
attendanceWeekTotals=function(workerId,start,siteId='all'){return v174AttendanceTotalsForRange(workerId,start,v174DateAdd(start,6),siteId)};
v176CanEditPay=function(){return hasPerm('view_pay_rates')&&hasPerm('edit_pay_records')};

// ---- Individual pricework allocations ------------------------------
function v190Priceworkers(siteId='all'){
  return v183ActiveMembers().filter(member=>member.employmentType==='Price Work'&&(siteId==='all'||!(member.clockSiteIds||[]).length||(member.clockSiteIds||[]).includes(siteId))).sort((a,b)=>a.name.localeCompare(b.name))
}
function v190Allocations(target){return (target?.priceworkAllocations||[]).filter(row=>v183Member(row.memberId)&&Number(row.sharePercent)>0)}
function v190AllocationPaid(target,memberId){return (target?.payments||[]).filter(row=>!row.amended&&row.paidTo===memberId).reduce((sum,row)=>sum+Number(row.amount||0),0)}
function v190AllocationSummary(target){let rows=v190Allocations(target);return rows.length?rows.map(row=>`${v183PersonName(row.memberId)} ${Number(row.sharePercent).toFixed(Number(row.sharePercent)%1?1:0)}%`).join(' · '):'No pricework people selected'}
function v190OpenAllocations(itemId,targetKind,index=-1){
  if(!hasPerm('edit_pay_records')&&!hasPerm('manage_payments'))return toast('Pay-record permission is required');let item=db.items.find(row=>row.id===itemId),target=targetKind==='item'?item:item?.lifts?.[Number(index)];if(!item||!target)return;let current=new Map(v190Allocations(target).map(row=>[row.memberId,Number(row.sharePercent)])),people=v190Priceworkers(item.site);
  modal=`<div class="modal"><div class="modalbox v190-allocation-modal"><span class="att-kicker">INDIVIDUAL PRICEWORK</span><h2>${v190Esc(item.ref)} · ${v190Esc(target.name||item.type)}</h2><p class="muted">Select every person sharing this price. Their percentages must total exactly 100%.</p><div class="v190-allocation-list">${people.map(member=>`<label><input type="checkbox" data-v190-worker="${member.id}" ${current.has(member.id)?'checked':''} onchange="v190AllocationToggle(this)"><span><b>${v190Esc(member.name)}</b><small>${v190Esc(member.role||'Pricework')}</small></span><input class="input" type="number" min="0.01" max="100" step="0.01" data-v190-share="${member.id}" value="${current.get(member.id)||''}" ${current.has(member.id)?'':'disabled'} aria-label="${v190Esc(member.name)} share percentage"><strong>%</strong></label>`).join('')||'<div class="empty-help"><b>No Price Work individuals are assigned to this Site.</b><span>Set their pay type to Price Work on Individuals first.</span></div>'}</div><div class="v190-allocation-total">Total <b id="v190-allocation-total">${[...current.values()].reduce((n,v)=>n+v,0).toFixed(2)}%</b></div><div class="actions"><button class="btn light" onclick="v190EqualAllocations()">Split equally</button><button class="btn orange" onclick="v190SaveAllocations('${itemId}','${targetKind}',${Number(index)})">Save people</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()
}
function v190Esc(value){return String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]))}
function v190AllocationToggle(box){let input=document.querySelector(`[data-v190-share="${box.dataset.v190Worker}"]`);if(input){input.disabled=!box.checked;if(box.checked&&!Number(input.value))input.value='0'}v190UpdateAllocationTotal()}
function v190UpdateAllocationTotal(){let total=[...document.querySelectorAll('[data-v190-worker]:checked')].reduce((sum,box)=>sum+Number(document.querySelector(`[data-v190-share="${box.dataset.v190Worker}"]`)?.value||0),0),label=document.getElementById('v190-allocation-total');if(label)label.textContent=total.toFixed(2)+'%'}
function v190EqualAllocations(){let selected=[...document.querySelectorAll('[data-v190-worker]:checked')];if(!selected.length)return toast('Select at least one person');let pennies=10000,base=Math.floor(pennies/selected.length);selected.forEach((box,index)=>{let amount=(base+(index<pennies-base*selected.length?1:0))/100,input=document.querySelector(`[data-v190-share="${box.dataset.v190Worker}"]`);if(input)input.value=amount.toFixed(2)});v190UpdateAllocationTotal()}
function v190SaveAllocations(itemId,targetKind,index){
  let item=db.items.find(row=>row.id===itemId),target=targetKind==='item'?item:item?.lifts?.[Number(index)],allocations=[...document.querySelectorAll('[data-v190-worker]:checked')].map(box=>({memberId:box.dataset.v190Worker,sharePercent:Number(document.querySelector(`[data-v190-share="${box.dataset.v190Worker}"]`)?.value||0)})).filter(row=>row.sharePercent>0),total=allocations.reduce((sum,row)=>sum+row.sharePercent,0);if(!target)return;if(!allocations.length)return toast('Select at least one pricework person');if(Math.abs(total-100)>0.001)return toast(`Shares total ${total.toFixed(2)}%. They must total 100%.`);target.priceworkAllocations=allocations;target.paidTo=allocations.length===1?allocations[0].memberId:'';save();audit('payments','pricework allocation',itemId,`${item.ref}: ${allocations.length} pricework people allocated`,item.site);modal=null;savedState('Pricework people saved');render()
}
const v190BaseLift=lift;
lift=function(item,row,index){let html=v190BaseLift(item,row,index);if(!hasPerm('view_financials')&&!hasPerm('view_pay_runs'))return html;let block=`<div class="v190-pricework-box"><span><b>Pricework people</b><small>${v190Esc(v190AllocationSummary(row))}</small></span>${hasPerm('edit_pay_records')||hasPerm('manage_payments')?`<button class="btn light smallbtn" onclick="event.stopPropagation();v190OpenAllocations('${item.id}','lift',${index})">Choose people</button>`:''}</div>`;return html.replace('<h4>Payment</h4>','<h4>Payment</h4>'+block)};
const v190BaseGarden=garden;
garden=function(item){let html=v190BaseGarden(item);if(!hasPerm('view_financials')&&!hasPerm('view_pay_runs'))return html;let block=`<div class="v190-pricework-box"><span><b>Pricework people</b><small>${v190Esc(v190AllocationSummary(item))}</small></span>${hasPerm('edit_pay_records')||hasPerm('manage_payments')?`<button class="btn light smallbtn" onclick="v190OpenAllocations('${item.id}','item')">Choose people</button>`:''}</div>`;return html.replace('<h4>Payment</h4>','<h4>Payment</h4>'+block)};

v174SetPriceRows=function(siteId,from,to){
  let rows=[];(db.items||[]).filter(item=>(siteId==='all'||item.site===siteId)&&canAccessSite(item.site)).forEach(item=>{
    let add=(stage,date,target,targetKind,index)=>{if(!v174DateInRange(date,from,to)||Number(target.price||0)<=0)return;for(const allocation of v190Allocations(target)){let member=v183Member(allocation.memberId),value=Number(target.price||0)*Number(allocation.sharePercent||0)/100,paid=v190AllocationPaid(target,allocation.memberId);rows.push({kind:'Set Price',siteId:item.site,itemId:item.id,targetKind,targetIndex:index,memberId:allocation.memberId,ref:item.ref,stage,date,payee:member?.name||'Unknown individual',payeeKey:'id:'+allocation.memberId,payeeKind:'Individual',sharePercent:Number(allocation.sharePercent),value,paid,due:Math.max(0,value-paid),status:paid>=value?'Paid':paid>0?'Part Paid':'Earned'})}}
    if(item.type==='House Plot')(item.lifts||[]).forEach((row,index)=>{if(row.status==='Complete')add(row.name,row.completedDate,row,'lift',index)});else if(item.status==='Complete')add(item.type,item.completedDate,item,'item',-1)
  });return rows
};
v174PayFilterOptions=function(){let members=attendanceAllMembers().filter(row=>row.member.activeForAttendance!==false).sort((a,b)=>a.member.name.localeCompare(b.member.name));return `<option value="all" ${v174Payee==='all'?'selected':''}>All individuals</option><optgroup label="Choose individual">${members.map(row=>`<option value="id:${row.member.id}" ${v174Payee==='id:'+row.member.id?'selected':''}>${v190Esc(row.member.name)}</option>`).join('')}</optgroup>`};
v174PayRowMatches=function(row){return v174Payee==='all'||row.payeeKey===v174Payee};

// Every new payment is recorded to one allocated individual only.
addPayment=function(context){
  if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Pay-record permission is required');let parts=context.split('|'),item=db.items.find(row=>row.id===parts[1]),target=parts[0]==='garden'?item:item?.lifts?.[Number(parts[2])];if(!item||!target)return;let allocations=v190Allocations(target);if(!allocations.length)return toast('Choose the pricework people first');
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">RECORD PAYMENT</span><h2>${v190Esc(item.ref)} · ${v190Esc(target.name||item.type)}</h2><p class="muted">Record payment against one named individual. Team payments are not used.</p><div class="field"><label>Individual</label><select id="pto" class="select" onchange="v190RefreshPaymentLimit('${context}')">${allocations.map(row=>`<option value="${row.memberId}">${v190Esc(v183PersonName(row.memberId))} · ${row.sharePercent}% share</option>`).join('')}</select></div><div id="v190-payment-limit" class="v190-payment-limit"></div><div class="field"><label>Amount paid</label><input id="pa" class="input" type="number" min="0.01" step="0.01"></div><div class="field"><label>Payment date</label><input id="pd" class="input" type="date" value="${iso()}"></div><div class="field"><label>Reference or note <span class="muted">(optional)</span></label><input id="pn" class="input"></div><button class="btn green" onclick="savePayment('${context}')">Save as paid</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render();v190RefreshPaymentLimit(context)
};
function v190PaymentTarget(context){let parts=context.split('|'),item=db.items.find(row=>row.id===parts[1]),target=parts[0]==='garden'?item:item?.lifts?.[Number(parts[2])];return {parts,item,target}}
function v190AllocationRemaining(target,memberId){let allocation=v190Allocations(target).find(row=>row.memberId===memberId);return allocation?Math.max(0,Number(target.price||0)*Number(allocation.sharePercent||0)/100-v190AllocationPaid(target,memberId)):0}
function v190RefreshPaymentLimit(context){let {target}=v190PaymentTarget(context),memberId=document.getElementById('pto')?.value||'',remaining=v190AllocationRemaining(target,memberId),label=document.getElementById('v190-payment-limit'),input=document.getElementById('pa');if(label)label.innerHTML=`<span>Remaining for ${v190Esc(v183PersonName(memberId))}</span><b>${attendanceMoney(remaining)}</b>`;if(input){input.max=remaining;input.placeholder=`Up to £${remaining.toFixed(2)}`}}
savePayment=function(context){
  if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Pay-record permission is required');let {item,target}=v190PaymentTarget(context),memberId=document.getElementById('pto')?.value||'',amount=Number(document.getElementById('pa')?.value||0),remaining=v190AllocationRemaining(target,memberId);if(!item||!target||!memberId)return;if(amount<=0)return toast('Enter the amount paid');if(amount>remaining+0.005)return toast(`Only ${attendanceMoney(remaining)} remains for this person`);let record={id:'p'+Date.now(),pct:Number(target.price||0)?Math.round(amount/Number(target.price)*10000)/100:0,date:document.getElementById('pd')?.value||iso(),amount,paidTo:memberId,payeeName:'',notes:document.getElementById('pn')?.value||'',paymentState:'Paid',enteredBy:currentUser()?.name||'',enteredAt:new Date().toISOString()};target.payments=target.payments||[];target.payments.push(record);let total=stagePaidAmount(target);target.pct=Number(target.price||0)?Math.min(100,Math.round(total/Number(target.price)*100)):0;target.paymentState=target.pct>=100?'Paid':'Part Paid';touch(item);save();emitNotification('payment',item.site,`${item.ref}: ${attendanceMoney(amount)} paid to ${v183PersonName(memberId)}`,item.id);modal=null;savedState('Individual payment saved');render()
};
const v190BasePaymentHistory=paymentHistory;
paymentHistory=function(entries,context){return v190BasePaymentHistory(entries,context).replace(/<strong>([\d.]+)%<\/strong>/g,'<strong class="v190-paid-state">PAID · $1%</strong>').replace(/Payment history/g,'Paid history')};

// ---- Exceptions-first multi-site Pay Run ---------------------------
let v190PaySites=[];
function v190SelectedSites(){let accessible=activeSites().filter(row=>canAccessSite(row.id)).map(row=>row.id);if(!v190PaySites.length)v190PaySites=site==='all'?accessible:(accessible.includes(site)?[site]:accessible.slice(0,1));return v190PaySites.filter(id=>accessible.includes(id))}
function v190TogglePaySite(siteId,checked){v190PaySites=checked?[...new Set([...v190PaySites,siteId])]:v190PaySites.filter(id=>id!==siteId);render()}
function v190TimeLines(siteIds,from,to){
  let lines=[];for(const row of attendanceAllMembers()){
    let shifts=v174ShiftsForRange(row.member.id,from,to,'all').filter(shift=>siteIds.includes(shift.siteId));if(!shifts.length)continue;let details=shifts.map(shift=>v190ShiftCalculation(shift,row.member)),earned=details.reduce((n,d)=>n+d.gross,0),recorded=details.reduce((n,d)=>n+d.recordedHours,0),payable=details.reduce((n,d)=>n+d.payableHours,0),overtime=details.reduce((n,d)=>n+d.overtimeHours,0);lines.push({id:`time:${row.member.id}`,memberId:row.member.id,name:row.member.name,role:row.member.role||'',kind:row.member.employmentType,siteIds:[...new Set(shifts.map(s=>s.siteId))],recorded,payable,overtime,earned,paidBefore:0,outstanding:earned,rateLines:details.map(d=>({date:d.date,hourlyRate:d.rate.hourlyRate,overtimeRate:d.rate.overtimeRate,dayRate:d.rate.dayRate,employmentType:d.rate.employmentType}))})
  }return lines
}
function v190PriceLines(siteIds,from,to){return siteIds.flatMap(siteId=>v174SetPriceRows(siteId,from,to)).map(row=>({id:`price:${row.itemId}:${row.targetKind}:${row.targetIndex}:${row.memberId}`,memberId:row.memberId,name:row.payee,role:v183Member(row.memberId)?.role||'',kind:'Price Work',siteIds:[row.siteId],ref:row.ref,stage:row.stage,recorded:0,payable:0,overtime:0,earned:row.value,paidBefore:row.paid,outstanding:row.due,sharePercent:row.sharePercent}))}
function v190PayExceptions(siteIds,from,to){
  let rows=[],siteMatch=id=>siteIds.includes(id),workers=new Set();
  for(const shift of db.attendanceShifts||[]){if(!siteMatch(shift.siteId)||shift.voided||!v174DateInRange(attendanceDateOnly(shift.clockIn?.time,shift.siteId),from,to))continue;workers.add(shift.workerId);if(!shift.clockOut?.time)rows.push({type:'Open shift',person:v183PersonName(shift.workerId),detail:`${siteName(shift.siteId)} · ${attendanceDateOnly(shift.clockIn.time,shift.siteId)}`,action:"contractorAttendanceTab='today';goNav('attendance')"});let approval=shift.overtimeApproval||{};if(approval.manualRequest&&approval.status==='Pending')rows.push({type:'Overtime decision',person:v183PersonName(shift.workerId),detail:`${approval.minutes||0} minutes awaiting decision`,action:"contractorAttendanceTab='today';goNav('attendance')"})}
  for(const workerId of workers){let member=v183Member(workerId),type=member?.employmentType;if(['Hourly','Day Rate'].includes(type)&&!Number(member?.hourlyRate||member?.basePay||0))rows.push({type:'Missing pay rate',person:member?.name||'Unknown person',detail:'Set an effective hourly rate on Individuals',action:"goNav('gangs')"})}
  for(const siteId of siteIds)for(let week=attendanceWeekStartISO(from);week<=to;week=v174DateAdd(week,7)){for(const workerId of workers){if(!v174ShiftsForRange(workerId,week,v174DateAdd(week,6),siteId).length)continue;let approval=attendanceApproval(workerId,siteId,week);if(!approval)rows.push({type:'Timesheet approval',person:v183PersonName(workerId),detail:`Week ${week} has not been approved`,action:"contractorAttendanceTab='timesheets';goNav('attendance')"});else if(approval.needsReapproval)rows.push({type:'Timesheet changed',person:v183PersonName(workerId),detail:`Week ${week} needs approval again`,action:"contractorAttendanceTab='timesheets';goNav('attendance')"})}}
  for(const item of db.items||[]){if(!siteMatch(item.site))continue;let targets=item.type==='House Plot'?(item.lifts||[]):[item];targets.forEach(target=>{let date=target.completedDate;if(target.status!=='Complete'||!v174DateInRange(date,from,to)||!Number(target.price||0))return;let allocations=v190Allocations(target),total=allocations.reduce((n,row)=>n+Number(row.sharePercent||0),0);if(!allocations.length||Math.abs(total-100)>0.001)rows.push({type:'Pricework allocation',person:item.ref,detail:`${target.name||item.type} needs named people totalling 100%`,action:`openItem('${item.id}')`})})}
  return rows
}
function v190PaySnapshot(siteIds,from,to){let lines=[...v190TimeLines(siteIds,from,to),...v190PriceLines(siteIds,from,to)],exceptions=v190PayExceptions(siteIds,from,to),totals={earned:lines.reduce((n,row)=>n+row.earned,0),paidBefore:lines.reduce((n,row)=>n+row.paidBefore,0),outstanding:lines.reduce((n,row)=>n+row.outstanding,0),hours:lines.reduce((n,row)=>n+row.payable,0),overtime:lines.reduce((n,row)=>n+row.overtime,0)},payload={siteIds:[...siteIds].sort(),from,to,lines,exceptions:exceptions.map(row=>({type:row.type,person:row.person,detail:row.detail})),totals};return {...payload,hash:v175Hash(JSON.stringify(payload))}}
function v190CurrentRun(siteIds,from,to){let key=[...siteIds].sort().join('|');return (db.payRuns||[]).filter(row=>row.siteKey===key&&row.from===from&&row.to===to&&!row.superseded).sort((a,b)=>String(b.updatedAt||b.createdAt).localeCompare(String(a.updatedAt||a.createdAt)))[0]||null}
function v190CreateDraft(){v174EnsurePayPeriod();let sites=v190SelectedSites();if(!sites.length)return toast('Select at least one Site');let snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo),old=v190CurrentRun(sites,v174PayFrom,v174PayTo);if(old?.status==='Draft'){old.snapshot=snapshot;old.snapshotHash=snapshot.hash;old.updatedAt=new Date().toISOString();old.updatedBy=currentUser()?.name||''}else{if(old)old.superseded=true;db.payRuns.push({id:'payrun-'+Date.now(),siteId:sites.length===1?sites[0]:'all',siteIds:[...sites],siteKey:[...sites].sort().join('|'),from:v174PayFrom,to:v174PayTo,status:'Draft',snapshot,snapshotHash:snapshot.hash,createdAt:new Date().toISOString(),createdBy:currentUser()?.name||'',updatedAt:new Date().toISOString()})}save();savedState('Draft pay run refreshed');render()}
function v190ApproveRun(){let sites=v190SelectedSites(),snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo),run=v190CurrentRun(sites,v174PayFrom,v174PayTo);if(!hasPerm('approve_pay_runs'))return toast('Pay-run approval permission is required');if(!run||run.status!=='Draft')return toast('Create or refresh the draft first');if(run.snapshotHash!==snapshot.hash)return toast('Records changed after the draft was reviewed. Refresh the draft first.');if(snapshot.exceptions.length)return toast(`Resolve ${snapshot.exceptions.length} exception${snapshot.exceptions.length===1?'':'s'} before approval`);run.status='Approved';run.snapshot=snapshot;run.snapshotHash=snapshot.hash;run.approvedAt=new Date().toISOString();run.approvedBy=currentUser()?.name||'';run.updatedAt=run.approvedAt;save();audit('payments','pay run',run.id,`Pay run approved — ${v174RangeLabel(run.from,run.to)}`,run.siteId);savedState('Pay run approved');render()}
function v190LockRun(){let sites=v190SelectedSites(),snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo),run=v190CurrentRun(sites,v174PayFrom,v174PayTo);if(!hasPerm('approve_pay_runs'))return toast('Pay-run approval permission is required');if(!run||run.status!=='Approved')return toast('Approve the pay run first');if(snapshot.exceptions.length||snapshot.hash!==run.snapshotHash)return toast('The records changed. Refresh the draft and approve again.');run.status='Locked';run.lockedAt=new Date().toISOString();run.lockedBy=currentUser()?.name||'';run.updatedAt=run.lockedAt;save();audit('payments','pay run',run.id,`Pay run locked — ${v174RangeLabel(run.from,run.to)}`,run.siteId);savedState('Pay run locked');render()}
function v190CsvRows(run,draft=false){let snapshot=run.snapshot,title=draft?'DRAFT — NOT APPROVED FOR PAYMENT':'LOCKED PAY RUN';let rows=[[title],['Period',snapshot.from,snapshot.to],['Sites',snapshot.siteIds.map(siteName).join(' | ')],['Status',run.status],['Snapshot',run.snapshotHash],[],['Individual','Role','Pay type','Sites','Recorded hours','Payable hours','Approved overtime','Earned','Previously paid','Approved to pay','State']];for(const line of snapshot.lines)rows.push([line.name,line.role,line.kind,line.siteIds.map(siteName).join(' | '),line.recorded.toFixed(2),line.payable.toFixed(2),line.overtime.toFixed(2),line.earned.toFixed(2),line.paidBefore.toFixed(2),line.outstanding.toFixed(2),draft?'Earned':run.status==='Paid'?'Paid':'Approved']);return rows}
function v190DownloadCsv(rows,name){let escape=value=>'"'+String(value??'').replace(/"/g,'""')+'"',blob=new Blob([rows.map(row=>row.map(escape).join(',')).join('\r\n')],{type:'text/csv;charset=utf-8'}),link=document.createElement('a');link.href=URL.createObjectURL(blob);link.download=name;link.click();setTimeout(()=>URL.revokeObjectURL(link.href),1000)}
function v190ExportDraft(){let sites=v190SelectedSites(),snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo),run={status:'Draft',snapshot,snapshotHash:snapshot.hash};v190DownloadCsv(v190CsvRows(run,true),`DRAFT-sitesight-pay-review-${snapshot.from}-to-${snapshot.to}.csv`);audit('payments','draft pay export',snapshot.from,`Draft pay review exported — not approved for payment`,'')}
function v190ExportFinal(){let sites=v190SelectedSites(),snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo),run=v190CurrentRun(sites,v174PayFrom,v174PayTo);if(!hasPerm('export_pay_runs'))return toast('Pay-run export permission is required');if(!run||run.status!=='Locked'||run.snapshotHash!==snapshot.hash||snapshot.exceptions.length)return toast('Final export requires a current locked pay run with no exceptions');v190DownloadCsv(v190CsvRows(run,false),`sitesight-locked-pay-run-${run.from}-to-${run.to}.csv`);run.status='Exported';run.exportedAt=new Date().toISOString();run.exportedBy=currentUser()?.name||'';run.updatedAt=run.exportedAt;save();audit('payments','pay run export',run.id,'Locked pay run exported',run.siteId);savedState('Locked pay run exported');render()}
function v190MarkPaid(){let sites=v190SelectedSites(),snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo),run=v190CurrentRun(sites,v174PayFrom,v174PayTo);if(!hasPerm('mark_payments_paid'))return toast('Mark-paid permission is required');if(!run||!['Locked','Exported'].includes(run.status)||run.snapshotHash!==snapshot.hash)return toast('Only a current locked or exported run can be marked paid');let reference=prompt('Payment reference (optional)')||'';run.status='Paid';run.paidAt=new Date().toISOString();run.paidBy=currentUser()?.name||'';run.paymentReference=reference.trim();run.updatedAt=run.paidAt;save();audit('payments','pay run paid',run.id,`Pay run marked paid${reference?` · ${reference}`:''}`,run.siteId);savedState('Pay run marked paid');render()}
v175ExportPayroll=function(siteId){let sites=siteId==='all'?v190SelectedSites():[siteId],snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo),run=v190CurrentRun(sites,v174PayFrom,v174PayTo);if(run?.status==='Locked'&&run.snapshotHash===snapshot.hash&&!snapshot.exceptions.length)return v190ExportFinal();return v190ExportDraft()};
v175PayrollWorkflow=function(){return `<div class="panel v190-pay-run-link"><div><span class="att-kicker">PAY CONTROL</span><h2>Use the Pay Run</h2><p class="muted">Resolve exceptions, approve, lock, export and reconcile in one place.</p></div><button class="btn orange" onclick="goNav('pay-run')">Open Pay Run ›</button></div>`};
function v190StateAmounts(run,snapshot){let current=run?.snapshotHash===snapshot.hash,approved=current&&['Approved','Locked','Exported','Paid'].includes(run.status)?snapshot.totals.outstanding:0,paid=current&&run?.status==='Paid'?snapshot.totals.outstanding:0;return {earned:snapshot.totals.earned,approved,paid}}
function v190PayRunPage(){
  if(!hasPerm('view_pay_runs'))return smartEmpty('Pay Run permission required.','Ask a Director or Contracts Manager to grant Pay Run access.','','','£');v174EnsurePayPeriod();let sites=v190SelectedSites(),valid=v174RangeValid(v174PayFrom,v174PayTo),snapshot=valid&&sites.length?v190PaySnapshot(sites,v174PayFrom,v174PayTo):null,run=snapshot?v190CurrentRun(sites,v174PayFrom,v174PayTo):null,changed=!!run&&run.snapshotHash!==snapshot.hash,states=snapshot?v190StateAmounts(run,snapshot):{earned:0,approved:0,paid:0},status=changed?'Changed — refresh required':run?.status||'Not started';
  return `<div class="topbar"><div><span class="att-kicker">EXCEPTIONS-FIRST</span><h1>Pay Run</h1><div class="sub">Collect → Resolve → Approve → Lock → Export → Reconcile</div></div><span class="v190-run-status ${String(status).toLowerCase().replace(/\W+/g,'-')}">${v190Esc(status)}</span></div><div class="panel v190-pay-scope"><div><h2>1. Choose the pay period and Sites</h2><p class="muted">One run can cover one Site or the whole business.</p></div>${v174PeriodBar('pay',v174PayPreset,v174PayFrom,v174PayTo)}<div class="v190-site-picker">${activeSites().filter(row=>canAccessSite(row.id)).map(row=>`<label><input type="checkbox" ${sites.includes(row.id)?'checked':''} onchange="v190TogglePaySite('${row.id}',this.checked)"><span>${v190Esc(row.name)}</span></label>`).join('')}</div></div>${!snapshot?smartEmpty('Choose a valid period and at least one Site.','The review will appear here.','','','£'):`<div class="v190-money-states"><div><span>Earned</span><b>${attendanceMoney(states.earned)}</b><small>Calculated from approved records</small></div><div><span>Approved</span><b>${attendanceMoney(states.approved)}</b><small>Authorised for this run</small></div><div><span>Paid</span><b>${attendanceMoney(states.paid)}</b><small>Reconciled as paid</small></div></div><div class="panel v190-exceptions"><div class="sectionhead"><div><h2>2. Resolve exceptions</h2><p class="muted">Nothing can be approved while this list contains an item.</p></div><span class="badge ${snapshot.exceptions.length?'b-red':'b-green'}">${snapshot.exceptions.length||'All clear'}</span></div>${snapshot.exceptions.length?`<div class="v190-exception-list">${snapshot.exceptions.slice(0,10).map(row=>`<button onclick="${row.action}"><span><b>${v190Esc(row.type)}</b><small>${v190Esc(row.person)} · ${v190Esc(row.detail)}</small></span><strong>Fix ›</strong></button>`).join('')}${snapshot.exceptions.length>10?`<details><summary>Show ${snapshot.exceptions.length-10} more exceptions</summary>${snapshot.exceptions.slice(10).map(row=>`<button onclick="${row.action}"><span><b>${v190Esc(row.type)}</b><small>${v190Esc(row.person)} · ${v190Esc(row.detail)}</small></span><strong>Fix ›</strong></button>`).join('')}</details>`:''}</div>`:'<div class="v190-all-clear"><b>✓ No pay exceptions</b><span>This period is ready for approval once the draft has been refreshed.</span></div>'}</div><div class="panel v190-pay-lines"><div class="sectionhead"><div><h2>3. Individual breakdown</h2><p class="muted">Every amount remains attached to a named individual.</p></div><b>${snapshot.lines.length} lines</b></div>${snapshot.lines.slice(0,10).map(v190PayLineHtml).join('')||'<div class="muted">No payable records in this period.</div>'}${snapshot.lines.length>10?`<details><summary>Show all ${snapshot.lines.length} lines</summary>${snapshot.lines.slice(10).map(v190PayLineHtml).join('')}</details>`:''}</div><div class="panel v190-run-actions"><div><h2>4. Approve and hand off</h2><p class="muted">Draft exports are clearly marked. A final export is impossible until the same reviewed snapshot is approved and locked.</p></div><div class="actions"><button class="btn light" onclick="v190ExportDraft()">Export draft review</button><button class="btn orange" onclick="v190CreateDraft()">${run?'Refresh draft':'Create draft'}</button>${hasPerm('approve_pay_runs')?`<button class="btn blue" onclick="v190ApproveRun()">Approve</button><button class="btn green" onclick="v190LockRun()">Lock</button>`:''}${hasPerm('export_pay_runs')?'<button class="btn green" onclick="v190ExportFinal()">Export locked run</button>':''}${hasPerm('mark_payments_paid')?'<button class="btn orange" onclick="v190MarkPaid()">Mark paid</button>':''}</div><div class="v190-stage-strip">${['Draft','Approved','Locked','Exported','Paid'].map(name=>`<span class="${run&&['Draft','Approved','Locked','Exported','Paid'].indexOf(run.status)>=[ 'Draft','Approved','Locked','Exported','Paid'].indexOf(name)?'done':''}">${name}</span>`).join('')}</div></div>`}`
}
function v190PayLineHtml(row){return `<div class="v190-pay-line"><span><b>${v190Esc(row.name)}</b><small>${v190Esc(row.kind)}${row.ref?` · ${v190Esc(row.ref)} · ${v190Esc(row.stage)}`:''} · ${row.siteIds.map(siteName).map(v190Esc).join(', ')}</small></span><span>${row.payable?`${row.payable.toFixed(2)} hrs · ${row.overtime.toFixed(2)} OT`:row.sharePercent?`${row.sharePercent}% share`:'Pricework'}</span><strong>${attendanceMoney(row.outstanding)}</strong></div>`}
const v190BaseAdminPage=adminPage;
adminPage=function(){return view==='pay-run'?v190PayRunPage():v190BaseAdminPage()};

// ---- Multiple permitted Site zones --------------------------------
function v190Zones(siteId){let cfg=attendanceSiteConfig(siteId)||{};return Array.isArray(cfg.geofenceZones)?cfg.geofenceZones:[]}
function v190ZonePanel(){if(!v189Director())return'';return `<div class="panel v190-zone-panel"><div class="sectionhead"><div><span class="att-kicker">CLOCKING BOUNDARIES</span><h2>Permitted Site zones</h2><p class="muted">Add every valid entrance, compound or nearby work area. A Clock In is accepted inside any active zone.</p></div></div>${activeSites().map(siteRow=>`<details><summary><span><b>${v190Esc(siteRow.name)}</b><small>${v190Zones(siteRow.id).length} permitted zone${v190Zones(siteRow.id).length===1?'':'s'}</small></span><strong>Manage</strong></summary><div class="v190-zone-list">${v190Zones(siteRow.id).map(zone=>`<div><span><b>${v190Esc(zone.name||'Site zone')}</b><small>${Number(zone.radiusM||250)}m radius · ${Number(zone.latitude).toFixed(5)}, ${Number(zone.longitude).toFixed(5)}</small></span><button class="btn light smallbtn" onclick="v190EditZone('${siteRow.id}','${zone.id}')">Edit</button><button class="btn red smallbtn" onclick="v190DeleteZone('${siteRow.id}','${zone.id}')">Remove</button></div>`).join('')||'<div class="muted">No permitted zones yet.</div>'}</div><button class="btn orange smallbtn" onclick="v190AddZone('${siteRow.id}')">＋ Add zone at this device</button></details>`).join('')}</div>`}
function v190AddZone(siteId){if(!v189Director())return;let name=prompt('Short zone name, e.g. Main entrance');if(!String(name||'').trim())return;let radius=Math.max(20,Math.min(1000,Number(prompt('Clocking radius in metres','250')||250))),status=document.getElementById('v190-zone-status');attendanceGetPosition().then(pos=>{let cfg=attendanceSiteConfig(siteId),zone={id:'zone-'+Date.now(),name:String(name).trim(),type:'circle',latitude:pos.latitude,longitude:pos.longitude,radiusM:radius,active:true};cfg.geofenceZones=Array.isArray(cfg.geofenceZones)?cfg.geofenceZones:[];cfg.geofenceZones.push(zone);if(cfg.latitude===null||cfg.latitude===undefined){cfg.latitude=pos.latitude;cfg.longitude=pos.longitude;cfg.radiusM=radius}save();audit('attendance','geofence zone',zone.id,`${siteName(siteId)}: zone added — ${zone.name}`,siteId);savedState('Permitted zone added');render()}).catch(error=>{if(status)status.textContent=error.message;toast(error.message)})}
function v190EditZone(siteId,zoneId){let zone=v190Zones(siteId).find(row=>row.id===zoneId);if(!zone)return;let name=prompt('Zone name',zone.name||'Site zone');if(!String(name||'').trim())return;let radius=Math.max(20,Math.min(1000,Number(prompt('Radius in metres',String(zone.radiusM||250))||zone.radiusM||250)));zone.name=String(name).trim();zone.radiusM=radius;save();savedState('Zone updated');render()}
function v190DeleteZone(siteId,zoneId){let cfg=attendanceSiteConfig(siteId),zone=v190Zones(siteId).find(row=>row.id===zoneId);if(!zone||!confirm(`Remove permitted zone “${zone.name}”?`))return;cfg.geofenceZones=v190Zones(siteId).filter(row=>row.id!==zoneId);save();audit('attendance','geofence zone',zoneId,`${siteName(siteId)}: zone removed — ${zone.name}`,siteId);render()}
const v190BaseFunctionality=v189FunctionalityPage;
v189FunctionalityPage=function(){return v190BaseFunctionality()+v190ZonePanel()};

// ---- Native-ready and protected offline attendance -----------------
const v190BrowserGetPosition=attendanceGetPosition;
attendanceGetPosition=function(){let bridge=window.SiteSightNative;if(bridge?.getClockEvidence)return Promise.resolve(bridge.getClockEvidence()).then(point=>({latitude:Number(point.latitude),longitude:Number(point.longitude),accuracy:Number(point.accuracy),positionTimestamp:Number(point.positionTimestamp||Date.now()),sampleCount:Number(point.sampleCount||1),deviceId:String(point.deviceId||''),integrity:point.integrity||'trusted-native',signature:point.signature||'',monotonicMs:Number(point.monotonicMs||0)}));return v190BrowserGetPosition()};
function v190NativeBridge(){return !!window.SiteSightNative?.getClockEvidence}
function v190OfflineQueue(){try{return JSON.parse(localStorage.getItem('sitesight_v190_offline_clock')||'[]')}catch(error){return[]}}
function v190StoreOffline(payload){let rows=v190OfflineQueue();rows.push({...payload,id:payload.id||`offline-${Date.now()}-${Math.random().toString(16).slice(2)}`,capturedAt:new Date().toISOString()});localStorage.setItem('sitesight_v190_offline_clock',JSON.stringify(rows));toast('Clock evidence protected on this device. It will be sent for manager review when online.')}
async function v190SyncOffline(){if(!sharedMode||!navigator.onLine)return;let pending=v190OfflineQueue(),kept=[];for(const row of pending){try{let response=await fetch('/api/attendance/offline-request',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(row)});if(!response.ok)kept.push(row)}catch(error){kept.push(row)}}localStorage.setItem('sitesight_v190_offline_clock',JSON.stringify(kept))}
async function v190RegisterNativeDevice(){let bridge=window.SiteSightNative,user=currentUser();if(!bridge?.getDeviceRegistration||!user?.linkedMemberId||!sharedMode||!navigator.onLine)return false;try{let registration=await bridge.getDeviceRegistration(),response=await fetch('/api/attendance/device',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(registration)});return response.ok}catch(error){return false}}
const v190PriorAttendanceDoClock=attendanceDoClock;
attendanceDoClock=async function(action){
  if(typeof v176ManagementClockUser==='function'&&v176ManagementClockUser())return v190PriorAttendanceDoClock(action);let linked=attendanceLinkedMember();if(!linked)return;let active=attendanceActiveShift(linked.member.id),siteId=action==='clock-out'?active?.siteId:(document.getElementById('att-clock-site')?.value||''),button=document.getElementById('att-clock-action'),progress=document.getElementById('att-clock-progress');if(!siteId)return toast('Choose a Site');if(button){button.disabled=true;button.textContent='Checking location…'}
  try{let evidence=v190NativeBridge()?await window.SiteSightNative.getClockEvidence({action,workerId:linked.member.id,siteId}):await v190BrowserGetPosition(),payload={id:evidence.id||'',capturedAt:evidence.capturedAt||new Date().toISOString(),action,workerId:linked.member.id,siteId,latitude:evidence.latitude,longitude:evidence.longitude,accuracy:evidence.accuracy,positionTimestamp:evidence.positionTimestamp||Date.now(),sampleCount:evidence.sampleCount||1,sessionId:attendanceSessionId(),device:attendanceDeviceInfo(),deviceId:evidence.deviceId||'',integrity:evidence.integrity||evidence.deviceIntegrity||'browser',deviceIntegrity:evidence.deviceIntegrity||evidence.integrity||'browser',isMocked:evidence.isMocked===true,developerMode:evidence.developerMode===true,signature:evidence.signature||'',monotonicMs:evidence.monotonicMs||0};if(!navigator.onLine||!sharedMode){if(v190NativeBridge()&&payload.signature&&payload.id){v190StoreOffline(payload);modal=null;render();return}throw new Error('Verified browser clocking needs an internet connection')};if(pendingSave)await syncToServer(true);let response=await fetch('/api/attendance/stamp',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}),result=await response.json();v189ApplyServerData(result);if(!result.ok)throw new Error(result.message||'Clock attempt rejected');if(action==='clock-in'&&window.SiteSightNative?.scheduleShiftBoundary){let schedule=v174MemberScheduleForDate(linked.member,siteId,iso());window.SiteSightNative.scheduleShiftBoundary({siteId,workerId:linked.member.id,finish:schedule.end})}modal=null;view='clock';savedState(action==='clock-in'?'Clocked in — location verified':'Clocked out — location verified');render()}catch(error){if(button){button.disabled=false;button.textContent=action==='clock-in'?'▶ VERIFY LOCATION & CLOCK IN':'■ VERIFY LOCATION & CLOCK OUT'}if(progress)progress.textContent=error.message;toast(error.message)}
};
window.addEventListener?.('online',()=>v190SyncOffline());
const v190BaseLogin=login;
login=async function(){let result=await v190BaseLogin();setTimeout(()=>{v190RegisterNativeDevice();v190SyncOffline()},0);return result};

// ---- Supervisor exception check -----------------------------------
function v190SupervisorExceptionRows(siteId){let rows=[],today=iso();for(const shift of db.attendanceShifts||[]){if(shift.siteId!==siteId||shift.voided)continue;let wm=attendanceMemberById(shift.workerId),date=attendanceDateOnly(shift.clockIn?.time,shift.siteId),punctual=wm?v169ShiftPunctuality(shift,wm.member):{};if(!shift.clockOut?.time&&date<today)rows.push({name:wm?.member.name||'Unknown',detail:`Open shift from ${date}`});if(punctual.late)rows.push({name:wm?.member.name||'Unknown',detail:`${punctual.late} minutes late · ${date}`});if(punctual.early)rows.push({name:wm?.member.name||'Unknown',detail:`${punctual.early} minutes early · ${date}`});if(shift.overtimeApproval?.status==='Pending')rows.push({name:wm?.member.name||'Unknown',detail:`${shift.overtimeApproval.minutes||0} minutes overtime awaiting decision`})}for(const request of db.offlineClockRequests||[])if(request.siteId===siteId&&request.status==='Pending')rows.push({name:v183PersonName(request.workerId),detail:`Offline ${request.action} · ${request.evidenceRating||'Check'} evidence`});return rows}
function v190SupervisorCheck(siteId){if(!['Director','Contracts Manager','Site Manager','Supervisor'].includes(currentUser()?.role||'')||!hasPerm('view_attendance'))return'';let rows=v190SupervisorExceptionRows(siteId);return `<div class="panel v190-supervisor-check"><div class="sectionhead"><div><span class="att-kicker">SUPERVISOR CHECK</span><h2>Attendance exceptions</h2><p class="muted">Only unusual records are shown; normal shifts remain out of the way.</p></div><span class="badge ${rows.length?'b-amber':'b-green'}">${rows.length||'All clear'}</span></div>${rows.slice(0,10).map(row=>`<div><b>${v190Esc(row.name)}</b><span>${v190Esc(row.detail)}</span></div>`).join('')||'<div class="v190-all-clear"><b>✓ No attendance exceptions</b></div>'}${rows.length>10?`<details><summary>Show ${rows.length-10} more</summary>${rows.slice(10).map(row=>`<div><b>${v190Esc(row.name)}</b><span>${v190Esc(row.detail)}</span></div>`).join('')}</details>`:''}</div>`}
const v190BaseAttendanceToday=attendanceTodayView;
attendanceTodayView=function(siteId){return v190SupervisorCheck(siteId)+v190BaseAttendanceToday(siteId)};

// ---- Current release labels and boot -------------------------------
const v190BaseShell=shell;
shell=function(content){v190BaseShell(content);let root=document.getElementById('root');if(root)root.innerHTML=root.innerHTML.replace(/v1\.89\.0 · ATTENDANCE & FUNCTIONALITY(?: DEMO)?/g,`v1.90.0 · PAY RUN & ATTENDANCE CONTROL${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`)};
const v190BaseGuide=newUserGuidePage;
newUserGuidePage=function(){return v190BaseGuide().replace(/ATTENDANCE & FUNCTIONALITY · V1\.89/g,'PAY RUN & ATTENDANCE CONTROL · V1.90').replace('Payments History','Pay Run and Payments History')};
const v190BasePayReview=contractorPayReview;
contractorPayReview=function(siteId){return v190BasePayReview(siteId).replace(/Every payment can be isolated by its individual or Team payee\./g,'Every payment is attached to a named individual.').replace(/Use the filter above to isolate one person without changing Team records\./g,'Use the filter above to isolate one person.').replace(/Individual \/ payee breakdown/g,'Individual breakdown').replace(/Individuals only/g,'All individuals')};

// SiteSight V1.91 — retention, document control and clearer individual pay.
const V191_PERMISSIONS=[['view_documents','View documents'],['manage_documents','Manage documents']];
let v191RecordsGroup=permissionGroups.find(group=>group.name==='Site records');
if(!v191RecordsGroup){v191RecordsGroup={name:'Site records',items:[]};permissionGroups.push(v191RecordsGroup)}
for(const definition of V191_PERMISSIONS){if(!permissionDefs.some(([key])=>key===definition[0]))permissionDefs.push(definition);if(!v191RecordsGroup.items.some(([key])=>key===definition[0]))v191RecordsGroup.items.push(definition)}
for(const roleName of ['Contracts Manager','Site Manager','Supervisor','Quantity Surveyor','Accounts']){let profile=V173_ROLE_PROFILES[roleName];if(profile)for(const key of roleName==='Supervisor'?['view_documents']:['view_documents','manage_documents'])if(!profile.permissions.includes(key))profile.permissions.push(key)}
V173_MORE_TOOLS.documents=['▥','Documents','Folders, Site files and controlled sharing'];
const v191BaseCanOpenView=v173CanOpenView;
v173CanOpenView=function(target,user=currentUser()){if(target==='documents')return v173UserHasAny(user,['view_documents','manage_documents']);return v191BaseCanOpenView(target,user)};
const v191BaseRoleNavItems=v173RoleNavItems;
v173RoleNavItems=function(user=currentUser()){
  let items=v191BaseRoleNavItems(user);if(!user||!v173CanOpenView('attendance',user)||items.some(row=>row.id==='attendance'))return items;
  let row={id:'attendance',...V173_MAIN_NAV.attendance},after=Math.max(items.findIndex(item=>item.id==='gangs'),items.findIndex(item=>item.id==='work-items'))+1;items.splice(after,0,row);return items
};

function v191RoleDocumentPermissions(role){return role==='Director'||['Contracts Manager','Site Manager','Quantity Surveyor','Accounts'].includes(role)?{view:true,manage:true}:role==='Supervisor'?{view:true,manage:false}:{view:false,manage:false}}
function v191Migrate(){
  let changed=false;db.documents=Array.isArray(db.documents)?db.documents:[];
  for(const user of db.users||[]){if(Number(user.permissionTemplateVersion||0)>=191)continue;let grants=v191RoleDocumentPermissions(user.role);user.permissions=user.permissions||{};user.permissions.view_documents=grants.view;user.permissions.manage_documents=grants.manage;user.permissionTemplateVersion=191;changed=true}
  for(const siteRow of db.sites||[]){let cfg=siteRow.attendanceConfig=siteRow.attendanceConfig||{};if(!['week','fortnight','month'].includes(cfg.timesheetDefaultPreset)){cfg.timesheetDefaultPreset='week';changed=true}}
  for(const member of v183Members()){if(member.cisRegistered===undefined){member.cisRegistered=false;changed=true}if(![0,20,30].includes(Number(member.cisDeductionRate))){member.cisDeductionRate=20;changed=true}}
  for(const item of db.items||[]){let targets=item.type==='House Plot'?(item.lifts||[]):[item];for(const target of targets){if(target.retentionPercent===undefined){target.retentionPercent=0;changed=true}let next=[];for(const row of target.priceworkAllocations||[]){if(row?.memberId&&!next.some(value=>value.memberId===row.memberId))next.push({memberId:row.memberId})}if(JSON.stringify(next)!==JSON.stringify(target.priceworkAllocations||[])){target.priceworkAllocations=next;changed=true}for(const payment of target.payments||[]){if(payment.grossAmount===undefined){payment.grossAmount=Number(payment.amount||0)+Number(payment.retentionHeld||0)+Number(payment.cisDeduction||0);changed=true}if(payment.retentionHeld===undefined){payment.retentionHeld=0;changed=true}if(payment.retentionRelease===undefined){payment.retentionRelease=0;changed=true}}}}
  return changed
}
const v191BaseMigrate=migrate;
migrate=function(persist=true){v191BaseMigrate(false);let changed=v191Migrate();if(persist&&(changed||persist))save()};
if(v191Migrate())persistCache();

// ---- Price Work people, manual amounts, retention and CIS ----------
v190Allocations=function(target){return (target?.priceworkAllocations||[]).filter(row=>row?.memberId&&v183Member(row.memberId)).filter((row,index,all)=>all.findIndex(value=>value.memberId===row.memberId)===index)};
v190AllocationSummary=function(target){let rows=v190Allocations(target);return rows.length?rows.map(row=>v183PersonName(row.memberId)).join(' · '):'No pricework people selected'};
v190OpenAllocations=function(itemId,targetKind,index=-1){
  if(!hasPerm('edit_pay_records')&&!hasPerm('manage_payments'))return toast('Pay-record permission is required');let item=db.items.find(row=>row.id===itemId),target=targetKind==='item'?item:item?.lifts?.[Number(index)];if(!item||!target)return;let current=new Set(v190Allocations(target).map(row=>row.memberId)),people=v190Priceworkers(item.site);
  modal=`<div class="modal"><div class="modalbox v190-allocation-modal"><span class="att-kicker">PRICEWORK PEOPLE</span><h2>${v190Esc(item.ref)} · ${v190Esc(target.name||item.type)}</h2><p class="muted">Choose everyone who worked on this price. Enter each person’s actual amount when their payment or invoice is recorded—no percentage split is required.</p><div class="v190-allocation-list v191-allocation-list">${people.map(member=>`<label><input type="checkbox" data-v190-worker="${member.id}" ${current.has(member.id)?'checked':''}><span><b>${v190Esc(member.name)}</b><small>${v190Esc(member.role||'Pricework')}${member.cisRegistered?' · CIS registered':''}</small></span></label>`).join('')||'<div class="empty-help"><b>No Price Work individuals are assigned to this Site.</b><span>Set their pay type to Price Work on Individuals first.</span></div>'}</div><div class="actions"><button class="btn orange" onclick="v190SaveAllocations('${itemId}','${targetKind}',${Number(index)})">Save people</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()
};
v190SaveAllocations=function(itemId,targetKind,index){let item=db.items.find(row=>row.id===itemId),target=targetKind==='item'?item:item?.lifts?.[Number(index)],allocations=[...document.querySelectorAll('[data-v190-worker]:checked')].map(box=>({memberId:box.dataset.v190Worker}));if(!target)return;if(!allocations.length)return toast('Select at least one pricework person');target.priceworkAllocations=allocations;target.paidTo=allocations.length===1?allocations[0].memberId:'';save();audit('payments','pricework people',itemId,`${item.ref}: ${allocations.length} pricework people selected`,item.site);modal=null;savedState('Pricework people saved');render()};

function v191Number(value){let number=Number(String(value??'').replace(/,/g,'').trim());return Number.isFinite(number)?number:0}
function v191PaymentGross(payment){return Number(payment?.grossAmount??(Number(payment?.amount||0)+Number(payment?.retentionHeld||0)+Number(payment?.cisDeduction||0)))}
function v191PaymentSettled(payment){return Number(payment?.amount||0)+Number(payment?.cisDeduction||0)}
function v191TargetGrossClaims(target){return (target?.payments||[]).filter(row=>!row.amended&&!Number(row.retentionRelease||0)).reduce((sum,row)=>sum+v191PaymentGross(row),0)}
function v191TargetSettled(target){return (target?.payments||[]).filter(row=>!row.amended).reduce((sum,row)=>sum+v191PaymentSettled(row),0)}
function v191MemberRetention(target,memberId){let held=(target?.payments||[]).filter(row=>!row.amended&&row.paidTo===memberId).reduce((sum,row)=>sum+Number(row.retentionHeld||0),0),released=(target?.payments||[]).filter(row=>!row.amended&&row.paidTo===memberId).reduce((sum,row)=>sum+Number(row.retentionRelease||0),0);return Math.max(0,held-released)}
function v191CisRate(member){return member?.cisRegistered?[0,20,30].includes(Number(member.cisDeductionRate))?Number(member.cisDeductionRate):20:0}
stagePaidAmount=function(target){return v191TargetSettled(target)};
simplePaidAmount=function(target){return v191TargetSettled(target)};
function v191RemainingPrice(target){return Math.max(0,Number(target?.price||0)-v191TargetGrossClaims(target))}
v190AllocationRemaining=function(target){return v191RemainingPrice(target)};
v190RefreshPaymentLimit=function(context){let {target}=v190PaymentTarget(context),remaining=v191RemainingPrice(target),label=document.getElementById('v190-payment-limit'),input=document.getElementById('pa');if(label)label.innerHTML=`<span>Price still available to allocate</span><b>${attendanceMoney(remaining)}</b>`;if(input){input.max=remaining.toFixed(2);input.placeholder=`Up to ${attendanceMoney(remaining)}`}v191UpdatePaymentPreview()};
addPayment=function(context){
  if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Pay-record permission is required');let {item,target}=v190PaymentTarget(context);if(!item||!target)return;let allocations=v190Allocations(target),remaining=v191RemainingPrice(target);if(!allocations.length)return toast('Choose the pricework people first');if(remaining<=0)return toast('The full set price has already been allocated. Release any retained amount from the individual record after sign-off.');
  modal=`<div class="modal"><div class="modalbox v191-payment-modal"><span class="att-kicker">RECORD INDIVIDUAL PRICEWORK</span><h2>${v190Esc(item.ref)} · ${v190Esc(target.name||item.type)}</h2><p class="muted">Enter the actual gross amount supplied for this person. SiteSight applies retention and CIS separately, so the net payment is always clear.</p><div class="field"><label>Individual</label><select id="pto" class="select" onchange="v191UpdatePaymentPreview()">${allocations.map(row=>`<option value="${row.memberId}">${v190Esc(v183PersonName(row.memberId))}</option>`).join('')}</select></div><div id="v190-payment-limit" class="v190-payment-limit"></div><div class="two"><div class="field"><label>Gross Price Work amount</label><input id="pa" class="input" inputmode="decimal" type="number" min="0.01" max="${remaining.toFixed(2)}" step="0.01" oninput="v191UpdatePaymentPreview()"></div><div class="field"><label>Retention held</label><div class="v191-suffix-input"><input id="v191-retention-rate" class="input" type="number" min="0" max="25" step="0.5" value="${Number(target.retentionPercent||0)}" oninput="v191UpdatePaymentPreview()"><span>%</span></div></div></div><div id="v191-payment-preview" class="v191-payment-preview"></div><div class="field"><label>Payment date</label><input id="pd" class="input" type="date" value="${iso()}"></div><div class="field"><label>Invoice reference or note <span class="muted">(optional)</span></label><input id="pn" class="input"></div><button class="btn green" onclick="savePayment('${context}')">Save individual payment</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render();v190RefreshPaymentLimit(context)
};
function v191PaymentFigures(){let member=v183Member(document.getElementById('pto')?.value||''),gross=Math.max(0,v191Number(document.getElementById('pa')?.value)),retentionRate=Math.max(0,Math.min(25,v191Number(document.getElementById('v191-retention-rate')?.value))),retention=Math.round(gross*retentionRate)/100,cisRate=v191CisRate(member),cisBase=Math.max(0,gross-retention),cis=Math.round(cisBase*cisRate)/100,net=Math.max(0,gross-retention-cis);return {member,gross,retentionRate,retention,cisRate,cis,net}}
function v191UpdatePaymentPreview(){let box=document.getElementById('v191-payment-preview');if(!box)return;let f=v191PaymentFigures();box.innerHTML=`<span><small>Gross amount</small><b>${attendanceMoney(f.gross)}</b></span><span><small>Retention held</small><b>${attendanceMoney(f.retention)}</b></span><span><small>CIS retained${f.member?.cisRegistered?` · ${f.cisRate}%`:' · not registered'}</small><b>${attendanceMoney(f.cis)}</b></span><span class="net"><small>Net paid now</small><b>${attendanceMoney(f.net)}</b></span>`}
savePayment=function(context){
  if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Pay-record permission is required');let {item,target}=v190PaymentTarget(context),memberId=document.getElementById('pto')?.value||'',figures=v191PaymentFigures(),remaining=v191RemainingPrice(target);if(!item||!target||!memberId)return;if(figures.gross<=0)return toast('Enter the gross Price Work amount');if(figures.gross>remaining+0.005)return toast(`Only ${attendanceMoney(remaining)} remains in this set price`);target.retentionPercent=figures.retentionRate;let record={id:'p'+Date.now(),pct:Number(target.price||0)?Math.round(figures.gross/Number(target.price)*10000)/100:0,date:document.getElementById('pd')?.value||iso(),grossAmount:figures.gross,retentionPercent:figures.retentionRate,retentionHeld:figures.retention,retentionRelease:0,cisRegistered:!!figures.member?.cisRegistered,cisDeductionRate:figures.cisRate,cisGrossExVat:Math.max(0,figures.gross-figures.retention),cisLiable:Math.max(0,figures.gross-figures.retention),cisDeduction:figures.cis,cisNet:figures.net,amount:figures.net,paidTo:memberId,payeeName:'',notes:document.getElementById('pn')?.value||'',paymentState:'Paid',enteredBy:currentUser()?.name||'',enteredAt:new Date().toISOString()};target.payments=target.payments||[];target.payments.push(record);let settled=v191TargetSettled(target);target.pct=Number(target.price||0)?Math.min(100,Math.round(settled/Number(target.price)*100)):0;target.paymentState=target.pct>=100?'Paid':'Part Paid';touch(item);save();emitNotification('payment',item.site,`${item.ref}: ${attendanceMoney(figures.net)} net paid to ${v183PersonName(memberId)}`,item.id);modal=null;savedState('Individual payment saved');render()
};

function v191RetentionRecords(memberId){let rows=[];for(const item of db.items||[]){let targets=item.type==='House Plot'?(item.lifts||[]):[item];targets.forEach((target,index)=>{let amount=v191MemberRetention(target,memberId);if(amount>0.004)rows.push({item,target,index,targetKind:item.type==='House Plot'?'lift':'item',amount,eligible:!!item.signed})})}return rows.sort((a,b)=>Number(b.eligible)-Number(a.eligible)||a.item.ref.localeCompare(b.item.ref))}
function v191RetentionTotal(memberId,eligibleOnly=false){return v191RetentionRecords(memberId).filter(row=>!eligibleOnly||row.eligible).reduce((sum,row)=>sum+row.amount,0)}
function v191OpenRetentionRelease(memberId,itemId,targetKind,index){let item=db.items.find(row=>row.id===itemId),target=targetKind==='item'?item:item?.lifts?.[Number(index)],amount=v191MemberRetention(target,memberId),member=v183Member(memberId);if(!item?.signed)return toast('Retention can be released after the plot or work item is signed off');if(amount<=0)return toast('No retention is outstanding');modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">RELEASE RETENTION</span><h2>${v190Esc(member?.name||'Individual')}</h2><p class="muted">${v190Esc(item.ref)} · ${v190Esc(target.name||item.type)}</p><div class="v191-retention-due"><span>Retention available</span><b>${attendanceMoney(amount)}</b></div><div class="field"><label>Gross retention to release</label><input id="v191-release-amount" class="input" type="number" min="0.01" max="${amount.toFixed(2)}" step="0.01" value="${amount.toFixed(2)}" oninput="v191UpdateReleasePreview('${memberId}')"></div><div id="v191-release-preview" class="v191-payment-preview"></div><div class="field"><label>Payment date</label><input id="v191-release-date" class="input" type="date" value="${iso()}"></div><div class="field"><label>Reference <span class="muted">(optional)</span></label><input id="v191-release-note" class="input"></div><button class="btn green" onclick="v191SaveRetentionRelease('${memberId}','${itemId}','${targetKind}',${Number(index)})">Save retention payment</button> <button class="btn light" onclick="openTeamMember('${V183_INDIVIDUALS_ID}','${memberId}')">Cancel</button></div></div>`;render();v191UpdateReleasePreview(memberId)}
function v191UpdateReleasePreview(memberId){let gross=Math.max(0,v191Number(document.getElementById('v191-release-amount')?.value)),rate=v191CisRate(v183Member(memberId)),cis=Math.round(gross*rate)/100,net=gross-cis,box=document.getElementById('v191-release-preview');if(box)box.innerHTML=`<span><small>Gross retention</small><b>${attendanceMoney(gross)}</b></span><span><small>CIS retained · ${rate}%</small><b>${attendanceMoney(cis)}</b></span><span class="net"><small>Net paid</small><b>${attendanceMoney(net)}</b></span>`}
function v191SaveRetentionRelease(memberId,itemId,targetKind,index){if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Pay-record permission is required');let item=db.items.find(row=>row.id===itemId),target=targetKind==='item'?item:item?.lifts?.[Number(index)],available=v191MemberRetention(target,memberId),gross=v191Number(document.getElementById('v191-release-amount')?.value),member=v183Member(memberId),rate=v191CisRate(member),cis=Math.round(gross*rate)/100;if(!item?.signed)return toast('This work is not signed off');if(gross<=0||gross>available+0.005)return toast(`Enter up to ${attendanceMoney(available)}`);target.payments=target.payments||[];target.payments.push({id:'ret-'+Date.now(),date:document.getElementById('v191-release-date')?.value||iso(),grossAmount:gross,retentionHeld:0,retentionRelease:gross,cisRegistered:!!member?.cisRegistered,cisDeductionRate:rate,cisGrossExVat:gross,cisLiable:gross,cisDeduction:cis,cisNet:gross-cis,amount:gross-cis,paidTo:memberId,notes:document.getElementById('v191-release-note')?.value||'Retention released',paymentState:'Paid',enteredBy:currentUser()?.name||'',enteredAt:new Date().toISOString()});target.pct=Number(target.price||0)?Math.min(100,Math.round(v191TargetSettled(target)/Number(target.price)*100)):0;touch(item);save();audit('payments','retention release',item.id,`${item.ref}: ${attendanceMoney(gross)} retention released to ${v183PersonName(memberId)}`,item.site);savedState('Retention payment saved');openTeamMember(V183_INDIVIDUALS_ID,memberId)}
function v191IndividualRetentionPanel(member){if(!hasPerm('view_pay_runs')&&!hasPerm('view_financials'))return'';let rows=v191RetentionRecords(member.id),total=rows.reduce((sum,row)=>sum+row.amount,0),available=rows.filter(row=>row.eligible).reduce((sum,row)=>sum+row.amount,0);return `<div class="v191-individual-retention"><div class="sectionhead"><div><span class="att-kicker">RETENTION</span><h3>${attendanceMoney(total)} currently held</h3><small>${attendanceMoney(available)} is ready to release from signed-off work.</small></div></div>${rows.map(row=>`<div class="v191-retention-row"><span><b>${v190Esc(row.item.ref)} · ${v190Esc(row.target.name||row.item.type)}</b><small>${v190Esc(siteName(row.item.site))} · ${row.eligible?'Signed off — ready':'Held until sign-off'}</small></span><strong>${attendanceMoney(row.amount)}</strong>${row.eligible&&(hasPerm('record_payments')||hasPerm('edit_pay_records'))?`<button class="btn green smallbtn" onclick="v191OpenRetentionRelease('${member.id}','${row.item.id}','${row.targetKind}',${row.index})">Pay retention</button>`:''}</div>`).join('')||'<div class="muted">No retention is currently held for this individual.</div>'}</div>`}

const v191BaseOpenTeamMember=openTeamMember;
openTeamMember=function(teamId,memberId){v191BaseOpenTeamMember(teamId,memberId);let member=v183Member(memberId);if(!member||!modal)return;let panel=v191IndividualRetentionPanel(member),cis=hasPerm('view_pay_rates')?`<div class="v191-cis-status ${member.cisRegistered?'registered':''}"><b>${member.cisRegistered?'CIS registered':'Not marked as CIS registered'}</b><span>${member.cisRegistered?`${v191CisRate(member)}% deduction applied to new payments`:'No automatic CIS deduction'}</span></div>`:'';modal=modal.replace(/<div style="margin-top:14px">/,`${cis}${panel}<div style="margin-top:14px">`);render()};
const v191BaseWorkforceCard=v176WorkforceCard;
v176WorkforceCard=function(groupId,member){let html=v191BaseWorkforceCard(groupId,member);if(!hasPerm('view_pay_rates'))return html;return html.replace('</h3>',`</h3><div class="v191-cis-inline">${member.cisRegistered?`CIS registered · ${v191CisRate(member)}%`:'Not CIS registered'}</div>`)};

const v191BaseNewPerson=v176NewPerson;
v176NewPerson=function(){v191BaseNewPerson();if(!modal)return;modal=modal.replace('<button class="btn orange" onclick="v176SaveNewPerson()">',`<label class="v191-cis-choice"><input id="v191-new-cis" type="checkbox" onchange="document.getElementById('v191-new-cis-rate').disabled=!this.checked"><span><b>CIS registered</b><small>Tick when CIS deductions should be shown for this individual.</small></span><select id="v191-new-cis-rate" class="select" disabled><option value="20">20%</option><option value="0">0% gross status</option><option value="30">30%</option></select></label><button class="btn orange" onclick="v176SaveNewPerson()">`);render()};
const v191BaseSaveNewPerson=v176SaveNewPerson;
v176SaveNewPerson=function(){let before=new Set(v183Members().map(row=>row.id)),registered=!!document.getElementById('v191-new-cis')?.checked,rate=Number(document.getElementById('v191-new-cis-rate')?.value||20);v191BaseSaveNewPerson();let member=v183Members().find(row=>!before.has(row.id));if(member){member.cisRegistered=registered;member.cisDeductionRate=registered?rate:20;save();openTeamMember(V183_INDIVIDUALS_ID,member.id)}};
const v191BaseOpenWorkforceEditor=v176OpenWorkforceEditor;
v176OpenWorkforceEditor=function(teamId,memberId){v191BaseOpenWorkforceEditor(teamId,memberId);let member=v183Member(memberId);if(!member||!modal||!v176CanEditPay())return;let cis=`<label class="v191-cis-choice"><input id="v191-edit-cis" type="checkbox" ${member.cisRegistered?'checked':''} onchange="document.getElementById('v191-edit-cis-rate').disabled=!this.checked"><span><b>CIS registered</b><small>Show gross, CIS retained and net due in payment reviews.</small></span><select id="v191-edit-cis-rate" class="select" ${member.cisRegistered?'':'disabled'}><option value="20" ${v191CisRate(member)===20?'selected':''}>20%</option><option value="0" ${v191CisRate(member)===0?'selected':''}>0% gross status</option><option value="30" ${v191CisRate(member)===30?'selected':''}>30%</option></select></label>`;modal=modal.replace('<div class="field"><label>Pay change effective from</label>',cis+'<div class="field"><label>Pay change effective from</label>');render()};
const v191BaseSaveWorkforceEditor=v176SaveWorkforceEditor;
v176SaveWorkforceEditor=function(teamId,memberId){let member=v183Member(memberId),input=document.getElementById('v191-edit-cis');if(member&&input){member.cisRegistered=!!input.checked;member.cisDeductionRate=Number(document.getElementById('v191-edit-cis-rate')?.value||20)}return v191BaseSaveWorkforceEditor(teamId,memberId)};

gangsPage=function(){
  if(!hasPerm('gangs'))return '<div class="panel empty-help"><b>You do not have permission to view Individuals.</b></div>';let query=String(v177PeopleQuery||'').trim().toLowerCase(),people=v183ActiveMembers().filter(member=>v182MemberVisible(member,v176Individuals())&&(!query||[member.name,member.role,v183MemberUser(member)?.role].join(' ').toLowerCase().includes(query)));
  return `<div class="topbar"><div><h1>Individuals</h1><div class="sub">Find a person, open their record or review held retention</div></div><div class="actions">${viewingSitePill()}<select class="select" onchange="setSiteContext(this.value)"><option value="all">All sites</option>${activeSites().map(row=>`<option value="${row.id}" ${site===row.id?'selected':''}>${v183Esc(row.name)}</option>`).join('')}</select>${hasPerm('manage_gangs')?'<button class="btn light" onclick="goNav(\'workforce-archive\')">Former people</button><button class="btn orange" onclick="v176NewPerson()">＋ Add Individual</button>':''}</div></div>${v177PeopleSearchPanel(people.length)}<div class="panel v183-people-panel"><div class="sectionhead"><div><h2>People</h2><div class="muted">${people.length} individual${people.length===1?'':'s'} shown</div></div></div><div class="v183-people-grid">${people.map(member=>{let user=v183MemberUser(member),retention=v191RetentionTotal(member.id);return `<button class="v183-person-card" onclick="openTeamMember('${V183_INDIVIDUALS_ID}','${member.id}')"><span class="v183-avatar">${v183Esc(member.name.split(/\s+/).slice(0,2).map(part=>part[0]).join('').toUpperCase())}</span><span><b>${v183Esc(member.name)}</b><small>${v183Esc(member.role||'Individual')} · ${user?`${v183Esc(user.role)} login`:'No login linked'}${member.cisRegistered?' · CIS':''}</small>${retention>0?`<em class="v191-retention-badge">Retention held ${attendanceMoney(retention)}</em>`:''}</span><strong>Open ›</strong></button>`}).join('')||'<div class="empty-help"><b>No individuals match this search.</b></div>'}</div></div>`
};

const v191BasePaymentHistory=paymentHistory;
paymentHistory=function(entries,context){let rows=(entries||[]).map(row=>{let name=v183PersonName(row.paidTo,row.payeeName||'Individual'),parts=[`Paid to ${name}`];if(Number(row.grossAmount||0)>0)parts.push(`Gross ${attendanceMoney(row.grossAmount)}`);if(Number(row.retentionHeld||0)>0)parts.push(`Retention ${attendanceMoney(row.retentionHeld)}`);if(Number(row.retentionRelease||0)>0)parts.push(`Retention release ${attendanceMoney(row.retentionRelease)}`);if(Number(row.cisDeduction||0)>0)parts.push(`CIS ${attendanceMoney(row.cisDeduction)}`);if(row.notes)parts.push(row.notes);return {...row,notes:parts.join(' · ')}});return v191BasePaymentHistory(rows,context)};

v174SetPriceRows=function(siteId,from,to){
  let rows=[];(db.items||[]).filter(item=>(siteId==='all'||item.site===siteId)&&canAccessSite(item.site)).forEach(item=>{let add=(stage,date,target,targetKind,index)=>{if(!v174DateInRange(date,from,to)||Number(target.price||0)<=0)return;for(const allocation of v190Allocations(target)){let member=v183Member(allocation.memberId),payments=(target.payments||[]).filter(row=>!row.amended&&row.paidTo===allocation.memberId),gross=payments.filter(row=>!Number(row.retentionRelease||0)).reduce((sum,row)=>sum+v191PaymentGross(row),0),settled=payments.reduce((sum,row)=>sum+v191PaymentSettled(row),0),retention=v191MemberRetention(target,allocation.memberId),due=item.signed?retention:0;rows.push({kind:'Set Price',siteId:item.site,itemId:item.id,targetKind,targetIndex:index,memberId:allocation.memberId,ref:item.ref,stage,date,payee:member?.name||'Unknown individual',payeeKey:'id:'+allocation.memberId,payeeKind:'Individual',value:gross,paid:settled,due,status:due>0?'Retention Due':retention>0?'Retention Held':gross>0?'Paid':'Awaiting Amount',retentionHeld:retention,signed:!!item.signed})}};if(item.type==='House Plot')(item.lifts||[]).forEach((row,index)=>{if(row.status==='Complete')add(row.name,row.completedDate,row,'lift',index)});else if(item.status==='Complete')add(item.type,item.completedDate,item,'item',-1)});return rows
};

// ---- Pay Run: reliable fixes, Site scope, CIS and net figures ------
function v191LineCis(line,member=v183Member(line.memberId)){let gross=Math.max(0,Number(line.outstanding||0)),rate=v191CisRate(member),cis=Math.round(gross*rate)/100;return {...line,grossDue:gross,cisRate:rate,cisDeduction:cis,netDue:Math.max(0,gross-cis)}}
v190TimeLines=function(siteIds,from,to){let lines=[];for(const row of attendanceAllMembers()){let shifts=v174ShiftsForRange(row.member.id,from,to,'all').filter(shift=>siteIds.includes(shift.siteId));if(!shifts.length)continue;let details=shifts.map(shift=>v190ShiftCalculation(shift,row.member)),earned=details.reduce((n,d)=>n+d.gross,0),recorded=details.reduce((n,d)=>n+d.recordedHours,0),payable=details.reduce((n,d)=>n+d.payableHours,0),overtime=details.reduce((n,d)=>n+d.overtimeHours,0);lines.push(v191LineCis({id:`time:${row.member.id}`,memberId:row.member.id,name:row.member.name,role:row.member.role||'',kind:row.member.employmentType,siteIds:[...new Set(shifts.map(s=>s.siteId))],recorded,payable,overtime,earned,paidBefore:0,outstanding:earned,retentionDue:0,rateLines:details.map(d=>({date:d.date,hourlyRate:d.rate.hourlyRate,overtimeRate:d.rate.overtimeRate,dayRate:d.rate.dayRate,employmentType:d.rate.employmentType}))},row.member))}return lines};
v190PriceLines=function(siteIds,from,to){return siteIds.flatMap(siteId=>v174SetPriceRows(siteId,from,to)).filter(row=>row.value>0||row.due>0).map(row=>{let line={id:`price:${row.itemId}:${row.targetKind}:${row.targetIndex}:${row.memberId}`,memberId:row.memberId,name:row.payee,role:v183Member(row.memberId)?.role||'',kind:row.due>0?'Retention release':'Price Work',siteIds:[row.siteId],ref:row.ref,stage:row.stage,recorded:0,payable:0,overtime:0,earned:row.value,paidBefore:row.paid,outstanding:row.due,retentionDue:row.due};return v191LineCis(line)})};
v190PayExceptions=function(siteIds,from,to){
  let rows=[],siteMatch=id=>siteIds.includes(id),workers=new Set();for(const shift of db.attendanceShifts||[]){if(!siteMatch(shift.siteId)||shift.voided||!v174DateInRange(attendanceDateOnly(shift.clockIn?.time,shift.siteId),from,to))continue;workers.add(shift.workerId);if(!shift.clockOut?.time)rows.push({type:'Open shift',person:v183PersonName(shift.workerId),detail:`${siteName(shift.siteId)} · ${attendanceDateOnly(shift.clockIn.time,shift.siteId)}`,fixType:'attendance',tab:'today',workerId:shift.workerId,siteId:shift.siteId});let approval=shift.overtimeApproval||{};if(approval.manualRequest&&approval.status==='Pending')rows.push({type:'Overtime decision',person:v183PersonName(shift.workerId),detail:`${approval.minutes||0} minutes awaiting decision`,fixType:'attendance',tab:'today',workerId:shift.workerId,siteId:shift.siteId})}
  for(const workerId of workers){let member=v183Member(workerId),type=member?.employmentType;if(['Hourly','Day Rate'].includes(type)&&!Number(member?.hourlyRate||member?.basePay||0))rows.push({type:'Missing pay rate',person:member?.name||'Unknown person',detail:'Set an effective hourly rate on Individuals',fixType:'person',workerId})}
  for(const siteId of siteIds)for(let week=attendanceWeekStartISO(from);week<=to;week=v174DateAdd(week,7)){for(const workerId of workers){if(!v174ShiftsForRange(workerId,week,v174DateAdd(week,6),siteId).length)continue;let approval=attendanceApproval(workerId,siteId,week);if(!approval)rows.push({type:'Timesheet approval',person:v183PersonName(workerId),detail:`Week ${week} has not been approved`,fixType:'attendance',tab:'timesheets',workerId,siteId});else if(approval.needsReapproval)rows.push({type:'Timesheet changed',person:v183PersonName(workerId),detail:`Week ${week} needs approval again`,fixType:'attendance',tab:'timesheets',workerId,siteId})}}
  for(const item of db.items||[]){if(!siteMatch(item.site))continue;let targets=item.type==='House Plot'?(item.lifts||[]):[item];targets.forEach((target,index)=>{let date=target.completedDate;if(target.status!=='Complete'||!v174DateInRange(date,from,to)||!Number(target.price||0))return;let allocations=v190Allocations(target);if(!allocations.length)rows.push({type:'Pricework people',person:item.ref,detail:`${target.name||item.type} needs named people`,fixType:'work',itemId:item.id});let remaining=v191RemainingPrice(target);if(allocations.length&&remaining>0.005)rows.push({type:'Pricework amount',person:item.ref,detail:`${target.name||item.type} still has ${attendanceMoney(remaining)} to allocate from invoices`,fixType:'work',itemId:item.id})})}return rows
};
v190PaySnapshot=function(siteIds,from,to){let lines=[...v190TimeLines(siteIds,from,to),...v190PriceLines(siteIds,from,to)].filter(line=>line.siteIds.some(id=>siteIds.includes(id))),exceptions=v190PayExceptions(siteIds,from,to),totals={earned:lines.reduce((n,row)=>n+Number(row.earned||0),0),paidBefore:lines.reduce((n,row)=>n+Number(row.paidBefore||0),0),outstanding:lines.reduce((n,row)=>n+Number(row.grossDue||0),0),grossDue:lines.reduce((n,row)=>n+Number(row.grossDue||0),0),cisDeduction:lines.reduce((n,row)=>n+Number(row.cisDeduction||0),0),netDue:lines.reduce((n,row)=>n+Number(row.netDue||0),0),retentionDue:lines.reduce((n,row)=>n+Number(row.retentionDue||0),0),hours:lines.reduce((n,row)=>n+Number(row.payable||0),0),overtime:lines.reduce((n,row)=>n+Number(row.overtime||0),0)},hashPayload={siteIds:[...siteIds].sort(),from,to,lines,exceptions:exceptions.map(row=>({type:row.type,person:row.person,detail:row.detail})),totals};return {...hashPayload,exceptions,hash:v175Hash(JSON.stringify(hashPayload))}};
let v191CurrentExceptions=[];
function v191FixPayException(index){let row=v191CurrentExceptions[Number(index)];if(!row)return toast('This exception is no longer in the current pay review');if(row.fixType==='work')return openItem(row.itemId);if(row.fixType==='person'){view='gangs';selected=null;render();return openTeamMember(V183_INDIVIDUALS_ID,row.workerId)}if(row.fixType==='attendance'){if(row.siteId)site=row.siteId;attendanceWorker=row.workerId||'';attendanceTab=row.tab||'today';contractorAttendanceTab=row.tab||'today';view='attendance';selected=null;render();return}toast('Open the related record from its main section')}
function v191GroupedLines(lines){let groups=new Map();for(const line of lines){let row=groups.get(line.memberId)||{memberId:line.memberId,name:line.name,role:line.role,lines:[],gross:0,cis:0,net:0};row.lines.push(line);row.gross+=Number(line.grossDue||0);row.cis+=Number(line.cisDeduction||0);row.net+=Number(line.netDue||0);groups.set(line.memberId,row)}return [...groups.values()].sort((a,b)=>a.name.localeCompare(b.name))}
function v191PayGroupHtml(group){return `<details class="v191-pay-person"><summary><span><b>${v190Esc(group.name)}</b><small>${v190Esc(group.role||'Individual')} · ${group.lines.length} pay line${group.lines.length===1?'':'s'}</small></span><span><small>Gross</small><b>${attendanceMoney(group.gross)}</b></span><span class="cis"><small>CIS</small><b>${attendanceMoney(group.cis)}</b></span><span class="net"><small>Net due</small><b>${attendanceMoney(group.net)}</b></span></summary><div>${group.lines.map(v190PayLineHtml).join('')}</div></details>`}
v190PayLineHtml=function(row){return `<div class="v190-pay-line"><span><b>${v190Esc(row.kind)}</b><small>${row.ref?`${v190Esc(row.ref)} · ${v190Esc(row.stage)} · `:''}${row.siteIds.map(siteName).map(v190Esc).join(', ')}</small></span><span>${row.payable?`${row.payable.toFixed(2)} hrs · ${row.overtime.toFixed(2)} OT`:row.retentionDue?`Retention release ${attendanceMoney(row.retentionDue)}`:'Pricework record'}</span><strong>${attendanceMoney(row.netDue)} net</strong></div>`};
v190PayRunPage=function(){
  if(!hasPerm('view_pay_runs'))return smartEmpty('Pay Run permission required.','Ask a Director or Contracts Manager to grant Pay Run access.','','','£');v174EnsurePayPeriod();let sites=v190SelectedSites(),valid=v174RangeValid(v174PayFrom,v174PayTo),snapshot=valid&&sites.length?v190PaySnapshot(sites,v174PayFrom,v174PayTo):null,run=snapshot?v190CurrentRun(sites,v174PayFrom,v174PayTo):null,changed=!!run&&run.snapshotHash!==snapshot.hash,status=changed?'Changed — refresh required':run?.status||'Not started',groups=snapshot?v191GroupedLines(snapshot.lines):[];v191CurrentExceptions=snapshot?.exceptions||[];
  let exceptions=snapshot?.exceptions||[],exceptionRows=exceptions.map((row,index)=>`<button onclick="v191FixPayException(${index})"><span><b>${v190Esc(row.type)}</b><small>${v190Esc(row.person)} · ${v190Esc(row.detail)}</small></span><strong>Fix ›</strong></button>`);
  return `<div class="topbar"><div><span class="att-kicker">CLEAR PAY CONTROL</span><h1>Pay Run</h1><div class="sub">Collect → Resolve → Approve → Lock → Export → Reconcile</div></div><span class="v190-run-status ${String(status).toLowerCase().replace(/\W+/g,'-')}">${v190Esc(status)}</span></div><div class="panel v190-pay-scope"><div><h2>1. Choose pay period and Sites</h2><p class="muted">Selecting one Site limits the individual breakdown to people with payable records on that Site.</p></div>${v174PeriodBar('pay',v174PayPreset,v174PayFrom,v174PayTo)}<div class="v190-site-picker">${activeSites().filter(row=>canAccessSite(row.id)).map(row=>`<label><input type="checkbox" ${sites.includes(row.id)?'checked':''} onchange="v190TogglePaySite('${row.id}',this.checked)"><span>${v190Esc(row.name)}</span></label>`).join('')}</div></div>${!snapshot?smartEmpty('Choose a valid period and at least one Site.','The review will appear here.','','','£'):`<div class="v191-pay-totals"><div><span>Gross due</span><b>${attendanceMoney(snapshot.totals.grossDue)}</b><small>Before CIS deductions</small></div><div class="cis"><span>CIS to retain</span><b>${attendanceMoney(snapshot.totals.cisDeduction)}</b><small>From CIS-registered individuals</small></div><div class="retention"><span>Retention releases</span><b>${attendanceMoney(snapshot.totals.retentionDue)}</b><small>Signed-off work only</small></div><div class="net"><span>Net payment due</span><b>${attendanceMoney(snapshot.totals.netDue)}</b><small>Amount to pay individuals</small></div></div><div class="panel v190-exceptions"><div class="sectionhead"><div><h2>2. Resolve exceptions</h2><p class="muted">Use Fix to open the exact record that needs attention.</p></div><span class="badge ${exceptions.length?'b-red':'b-green'}">${exceptions.length||'All clear'}</span></div>${exceptions.length?`<div class="v190-exception-list">${exceptionRows.slice(0,10).join('')}${exceptionRows.length>10?`<details><summary>Show ${exceptionRows.length-10} more exceptions</summary>${exceptionRows.slice(10).join('')}</details>`:''}</div>`:'<div class="v190-all-clear"><b>✓ No pay exceptions</b><span>This period is ready for approval once the draft is refreshed.</span></div>'}</div><div class="panel v190-pay-lines"><div class="sectionhead"><div><h2>3. Individual breakdown</h2><p class="muted">Gross, CIS retained and net due are grouped by person.</p></div><b>${groups.length} individual${groups.length===1?'':'s'}</b></div>${groups.slice(0,10).map(v191PayGroupHtml).join('')||'<div class="muted">No payable records in this period.</div>'}${groups.length>10?`<details class="v191-group-expander"><summary>Show all ${groups.length} individuals</summary>${groups.slice(10).map(v191PayGroupHtml).join('')}</details>`:''}</div><div class="panel v190-run-actions"><div><h2>4. Approve and hand off</h2><p class="muted">Draft exports are clearly marked. A final export is impossible until the reviewed snapshot is approved and locked.</p></div><div class="actions"><button class="btn light" onclick="v190ExportDraft()">Export draft review</button><button class="btn orange" onclick="v190CreateDraft()">${run?'Refresh draft':'Create draft'}</button>${hasPerm('approve_pay_runs')?`<button class="btn blue" onclick="v190ApproveRun()">Approve</button><button class="btn green" onclick="v190LockRun()">Lock</button>`:''}${hasPerm('export_pay_runs')?'<button class="btn green" onclick="v190ExportFinal()">Export locked run</button>':''}${hasPerm('mark_payments_paid')?'<button class="btn orange" onclick="v190MarkPaid()">Mark paid</button>':''}</div><div class="v190-stage-strip">${['Draft','Approved','Locked','Exported','Paid'].map(name=>`<span class="${run&&['Draft','Approved','Locked','Exported','Paid'].indexOf(run.status)>=[ 'Draft','Approved','Locked','Exported','Paid'].indexOf(name)?'done':''}">${name}</span>`).join('')}</div></div>`}`
};
v190CsvRows=function(run,draft=false){let snapshot=run.snapshot,title=draft?'DRAFT — NOT APPROVED FOR PAYMENT':'LOCKED PAY RUN',rows=[[title],['Period',snapshot.from,snapshot.to],['Sites',snapshot.siteIds.map(siteName).join(' | ')],['Status',run.status],['Snapshot',run.snapshotHash],[],['Individual','Role','Pay type','Sites','Recorded hours','Payable hours','Approved overtime','Gross due','CIS rate','CIS retained','Retention release','Net payment','State']];for(const line of snapshot.lines)rows.push([line.name,line.role,line.kind,line.siteIds.map(siteName).join(' | '),Number(line.recorded||0).toFixed(2),Number(line.payable||0).toFixed(2),Number(line.overtime||0).toFixed(2),Number(line.grossDue||0).toFixed(2),`${Number(line.cisRate||0)}%`,Number(line.cisDeduction||0).toFixed(2),Number(line.retentionDue||0).toFixed(2),Number(line.netDue||0).toFixed(2),draft?'Earned':run.status==='Paid'?'Paid':'Approved']);return rows};

// ---- Pay Review search --------------------------------------------
let v191PayReviewSearch='';
function v191ApplyPaySearch(){let value=String(document.getElementById('v191-pay-review-search')?.value||v191PayReviewSearch).trim().toLowerCase();v191PayReviewSearch=value;document.querySelectorAll('.v174-pay-person,.pay-review-row').forEach(row=>row.hidden=!!value&&!row.textContent.toLowerCase().includes(value))}
const v191BasePayReview=contractorPayReview;
contractorPayReview=function(siteId){let html=v191BasePayReview(siteId);if(!hasPerm('view_financials'))return html;let search=`<div class="v191-pay-search"><label for="v191-pay-review-search">Find an individual</label><input id="v191-pay-review-search" class="input" value="${v190Esc(v191PayReviewSearch)}" placeholder="Type a name…" oninput="v191ApplyPaySearch()"></div>`;setTimeout(v191ApplyPaySearch,0);return html.replace('<div class="contractor-section v174-pay-review">','<div class="contractor-section v174-pay-review">'+search)};

// ---- Default timesheet period in Functionality --------------------
function v191SetTimesheetDefault(siteId,value){if(!v189Director())return;let cfg=attendanceSiteConfig(siteId);if(!cfg||!['week','fortnight','month'].includes(value))return;cfg.timesheetDefaultPreset=value;save();audit('attendance','timesheet default',siteId,`${siteName(siteId)}: default timesheet view changed to ${value}`,siteId);savedState('Default timesheet period saved')}
function v191TimesheetDefaultsPanel(){if(!v189Director())return'';return `<div class="panel v191-timesheet-defaults"><div><span class="att-kicker">PEOPLE & TIME</span><h2>Default timesheet period</h2><p class="muted">Choose what managers see first when opening each Site’s timesheets. They can still select any custom range.</p></div>${activeSites().map(siteRow=>`<label><span><b>${v190Esc(siteRow.name)}</b><small>Initial timesheet view</small></span><select class="select" onchange="v191SetTimesheetDefault('${siteRow.id}',this.value)">${[['week','Weekly'],['fortnight','Fortnightly'],['month','Monthly']].map(([value,label])=>`<option value="${value}" ${attendanceSiteConfig(siteRow.id)?.timesheetDefaultPreset===value?'selected':''}>${label}</option>`).join('')}</select></label>`).join('')}</div>`}
const v191BaseFunctionality=v189FunctionalityPage;
v189FunctionalityPage=function(){return v191BaseFunctionality()+v191TimesheetDefaultsPanel()};
let v191TimesheetDefaultApplied='';
const v191BaseTimesheetView=attendanceTimesheetView;
attendanceTimesheetView=function(siteId){if(v191TimesheetDefaultApplied!==siteId){v191TimesheetDefaultApplied=siteId;v174TimesheetPreset=attendanceSiteConfig(siteId)?.timesheetDefaultPreset||'week';let range=v174Period(v174TimesheetPreset);v174TimesheetFrom=range.from;v174TimesheetTo=range.to}return v191BaseTimesheetView(siteId)};

// ---- Documents: folders, subfolders, sharing and opening/printing --
let v191DocumentFolder='',v191DocumentQuery='',v191DocumentSite='all';
function v191DocumentAudienceLabel(value){if(!value||value==='all')return'All authorised users';let [kind,id]=String(value).split(':');if(kind==='role')return id;if(kind==='user')return db.users.find(row=>row.id===id)?.name||'Selected user';return'All authorised users'}
function v191DocumentVisible(row,user=currentUser()){if(!row||!user||!v173UserHasAny(user,['view_documents','manage_documents']))return false;let current=row,seen=new Set();while(current&&!seen.has(current.id)){seen.add(current.id);if(current.siteId&&!canAccessSite(current.siteId))return false;if(current.audience&&current.audience!=='all'&&user.role!=='Director'){let [kind,id]=String(current.audience).split(':');if(kind==='role'?user.role!==id:kind==='user'?user.id!==id:true)return false}current=current.parentId?(db.documents||[]).find(item=>item.id===current.parentId):null}return true}
function v191DocumentChildren(parentId){return (db.documents||[]).filter(row=>(row.parentId||'')===(parentId||'')&&v191DocumentVisible(row)).filter(row=>v191DocumentSite==='all'||!row.siteId||row.siteId===v191DocumentSite).filter(row=>!v191DocumentQuery||[row.name,row.originalName,row.notes].join(' ').toLowerCase().includes(v191DocumentQuery.toLowerCase())).sort((a,b)=>Number(a.type==='file')-Number(b.type==='file')||a.name.localeCompare(b.name))}
function v191FolderPath(){let rows=[],id=v191DocumentFolder,guard=0;while(id&&guard++<30){let row=(db.documents||[]).find(item=>item.id===id&&item.type==='folder');if(!row)break;rows.unshift(row);id=row.parentId||''}return rows}
function v191DocumentsPage(){if(!v173CanOpenView('documents'))return smartEmpty('Documents permission required.','Ask a Director or manager for document access.','','','▥');let rows=v191DocumentChildren(v191DocumentFolder),path=v191FolderPath();return `<div class="topbar"><div><span class="att-kicker">CONTROLLED SITE FILES</span><h1>Documents</h1><div class="sub">Create folders and subfolders, upload named files and control who they are provided to.</div></div>${hasPerm('manage_documents')?`<div class="actions"><button class="btn light" onclick="v191NewFolder()">＋ New folder</button><button class="btn orange" onclick="v191UploadDocuments()">⇧ Upload documents</button></div>`:''}</div><div class="panel v191-document-tools"><div class="v191-breadcrumbs"><button onclick="v191DocumentFolder='';render()">Documents</button>${path.map(row=>`<span>›</span><button onclick="v191DocumentFolder='${row.id}';render()">${v190Esc(row.name)}</button>`).join('')}</div><div class="v191-document-filters"><input class="input" placeholder="Search this folder…" value="${v190Esc(v191DocumentQuery)}" oninput="v191DocumentQuery=this.value;render()"><select class="select" onchange="v191DocumentSite=this.value;render()"><option value="all">All Sites</option><option value="">Company documents</option>${activeSites().filter(row=>canAccessSite(row.id)).map(row=>`<option value="${row.id}" ${v191DocumentSite===row.id?'selected':''}>${v190Esc(row.name)}</option>`).join('')}</select></div></div><div class="panel v191-document-list">${rows.map(row=>row.type==='folder'?`<div class="v191-document-row folder" ondblclick="v191DocumentFolder='${row.id}';render()"><span class="v191-document-icon">▥</span><span><b>${v190Esc(row.name)}</b><small>${row.siteId?v190Esc(siteName(row.siteId)):'Company'} · ${v190Esc(v191DocumentAudienceLabel(row.audience))}</small></span><button class="btn light smallbtn" onclick="v191DocumentFolder='${row.id}';render()">Open</button>${hasPerm('manage_documents')?`<button class="btn light smallbtn" onclick="v191RenameDocument('${row.id}')">Rename</button><button class="btn red smallbtn" onclick="v191DeleteDocument('${row.id}')">Remove</button>`:''}</div>`:`<div class="v191-document-row file"><span class="v191-document-icon">${String(row.mime||'').includes('pdf')?'▤':String(row.mime||'').startsWith('image/')?'▧':'▦'}</span><span><b>${v190Esc(row.name)}</b><small>${row.siteId?v190Esc(siteName(row.siteId)):'Company'} · ${v190Esc(v191DocumentAudienceLabel(row.audience))} · ${row.createdAt?new Date(row.createdAt).toLocaleDateString('en-GB'):''}</small></span><button class="btn blue smallbtn" onclick="v191OpenDocument('${row.id}')">Open / print</button>${hasPerm('manage_documents')?`<button class="btn light smallbtn" onclick="v191RenameDocument('${row.id}')">Rename</button><button class="btn red smallbtn" onclick="v191DeleteDocument('${row.id}')">Remove</button>`:''}</div>`).join('')||'<div class="empty-help"><b>This folder is empty.</b><span>Create a subfolder or upload the first document.</span></div>'}</div>`}
function v191AudienceOptions(selected='all'){let roles=['Director','Contracts Manager','Site Manager','Supervisor','Quantity Surveyor','Accounts','Operator'];return `<option value="all" ${selected==='all'?'selected':''}>All authorised users</option><optgroup label="Provide to a role">${roles.map(role=>`<option value="role:${role}" ${selected==='role:'+role?'selected':''}>${v190Esc(role)}</option>`).join('')}</optgroup><optgroup label="Provide to one user">${(db.users||[]).filter(user=>user.accountActive!==false).map(user=>`<option value="user:${user.id}" ${selected==='user:'+user.id?'selected':''}>${v190Esc(user.name)} · ${v190Esc(user.role)}</option>`).join('')}</optgroup>`}
function v191NewFolder(){if(!hasPerm('manage_documents'))return;modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">DOCUMENT FOLDER</span><h2>Create folder</h2><div class="field"><label>Folder name</label><input id="v191-folder-name" class="input" placeholder="e.g. Health & Safety"></div><div class="two"><div class="field"><label>Site</label><select id="v191-doc-site" class="select"><option value="">Company-wide</option>${activeSites().map(row=>`<option value="${row.id}" ${v191DocumentSite===row.id?'selected':''}>${v190Esc(row.name)}</option>`).join('')}</select></div><div class="field"><label>Provide to</label><select id="v191-doc-audience" class="select">${v191AudienceOptions()}</select></div></div><button class="btn orange" onclick="v191SaveFolder()">Create folder</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function v191SaveFolder(){let name=(document.getElementById('v191-folder-name')?.value||'').trim();if(!name)return toast('Enter a folder name');db.documents.push({id:'folder-'+Date.now(),type:'folder',name,parentId:v191DocumentFolder||'',siteId:document.getElementById('v191-doc-site')?.value||'',audience:document.getElementById('v191-doc-audience')?.value||'all',createdAt:new Date().toISOString(),createdBy:currentUser()?.name||''});save();audit('documents','folder',name,`Document folder created — ${name}`,document.getElementById('v191-doc-site')?.value||'');modal=null;savedState('Folder created');render()}
function v191UploadDocuments(){if(!hasPerm('manage_documents'))return;modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">DOCUMENT UPLOAD</span><h2>Upload documents</h2><p class="muted">PDF, Word, Excel, CSV, text and image files are supported. Use a short display name when uploading one file.</p><div class="field"><label>Files</label><input id="v191-doc-files" class="input" type="file" multiple accept=".pdf,.docx,.xlsx,.csv,.txt,image/png,image/jpeg,image/webp"></div><div class="field"><label>Display name <span class="muted">(optional for one file)</span></label><input id="v191-doc-name" class="input" placeholder="Defaults to the filename"></div><div class="two"><div class="field"><label>Site</label><select id="v191-doc-site" class="select"><option value="">Company-wide</option>${activeSites().map(row=>`<option value="${row.id}" ${v191DocumentSite===row.id?'selected':''}>${v190Esc(row.name)}</option>`).join('')}</select></div><div class="field"><label>Provide to</label><select id="v191-doc-audience" class="select">${v191AudienceOptions()}</select></div></div><div class="field"><label>Note <span class="muted">(optional)</span></label><input id="v191-doc-note" class="input"></div><button class="btn green" onclick="v191SaveDocuments()">Upload documents</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
async function v191SaveDocuments(){if(!hasPerm('manage_documents'))return;let files=[...(document.getElementById('v191-doc-files')?.files||[])],display=(document.getElementById('v191-doc-name')?.value||'').trim(),siteId=document.getElementById('v191-doc-site')?.value||'',audience=document.getElementById('v191-doc-audience')?.value||'all',notes=(document.getElementById('v191-doc-note')?.value||'').trim();if(!files.length)return toast('Choose at least one document');if(sharedMode&&!navigator.onLine&&files.some(file=>!['application/pdf','image/png','image/jpeg','image/webp'].includes(file.type)))return toast('Reconnect before uploading Word, Excel, CSV or text documents');let button=document.activeElement?.classList?.contains('btn')?document.activeElement:null;if(button){button.disabled=true;button.textContent='Uploading…'}try{for(const file of files){let data=await storeMediaFile(file,'',siteId);db.documents.push({id:'document-'+Date.now()+'-'+Math.random().toString(16).slice(2),type:'file',name:files.length===1&&display?display:(file.name||'Document'),originalName:file.name||'',mime:file.type||'',size:Number(file.size||0),data,parentId:v191DocumentFolder||'',siteId,audience,notes,createdAt:new Date().toISOString(),createdBy:currentUser()?.name||''})}save();audit('documents','upload',v191DocumentFolder||'root',`${files.length} document${files.length===1?'':'s'} uploaded`,siteId);modal=null;savedState('Documents uploaded');render()}catch(error){if(button){button.disabled=false;button.textContent='Upload documents'}toast(error.message||'A document could not be uploaded')}}
async function v191OpenDocument(id){let row=(db.documents||[]).find(item=>item.id===id&&item.type==='file');if(!row||!v191DocumentVisible(row))return toast('Document is unavailable');let url=await resolveMediaRef(row.data);if(!url)return toast('Document file is unavailable');let opened=window.open(url,'_blank','noopener');if(!opened)toast('Allow pop-ups to open or print this document')}
function v191RenameDocument(id){let row=(db.documents||[]).find(item=>item.id===id);if(!row||!hasPerm('manage_documents'))return;let name=prompt(`Rename ${row.type}`,row.name||'');if(!String(name||'').trim())return;row.name=String(name).trim();row.updatedAt=new Date().toISOString();row.updatedBy=currentUser()?.name||'';save();savedState('Document renamed');render()}
function v191DeleteDocument(id){let row=(db.documents||[]).find(item=>item.id===id);if(!row||!hasPerm('manage_documents'))return;let ids=new Set([id]),changed=true;while(changed){changed=false;for(const item of db.documents||[])if(ids.has(item.parentId)&&!ids.has(item.id)){ids.add(item.id);changed=true}}if(!confirm(`Remove ${row.type} “${row.name}”${ids.size>1?` and ${ids.size-1} item${ids.size===2?'':'s'} inside it`:''}?`))return;db.documents=db.documents.filter(item=>!ids.has(item.id));save();audit('documents','remove',id,`Document ${row.type} removed — ${row.name}`,row.siteId||'');if(v191DocumentFolder===id)v191DocumentFolder=row.parentId||'';savedState('Document record removed');render()}
const v191BaseRender=render;
render=function(){if(currentUser()&&view==='documents'){shell(v191DocumentsPage());updateSyncUI();return}return v191BaseRender()};

// ---- Notification profile layout and current release labels -------
const v191BaseShell=shell;
shell=function(content){v191BaseShell(content);let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace(/v1\.90\.0 · PAY RUN & ATTENDANCE CONTROL(?: DEMO)?/g,`v1.91.0 · RETENTION & DOCUMENT CONTROL${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`);if(db.demoProfile==='v191-two-site-director')root.innerHTML=root.innerHTML.replace('4 active sites · 351 plots · full operational dataset','2 active sites · 99 plots · live operational dataset')};
const v191BaseGuide=newUserGuidePage;
newUserGuidePage=function(){return v191BaseGuide().replace(/PAY RUN & ATTENDANCE CONTROL · V1\.90/g,'RETENTION & DOCUMENT CONTROL · V1.91').replace('Pay Run and Payments History','Pay Run, Retention, CIS and Documents')};
const v191BaseDashboard=dashboard;
dashboard=function(){let html=v191BaseDashboard();return db.demoProfile==='v191-two-site-director'?html.replace('ENTERPRISE DEMO · four active developments and 351 plots under full operational use','DIRECTOR DEMO · a smooth 29-home Site beside a complex 70-home Site'):html};
const v191BaseLoginScreen=loginScreen;
loginScreen=function(){v191BaseLoginScreen();if(db.demoProfile!=='v191-two-site-director')return;let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace('Four live developments. One business view.','Two contrasting developments. One clear business view.').replace(/351 plots, 62 people and realistic operational records — ready to demonstrate SiteSight at contractor scale\./g,'99 plots, 30 frontline workers and complete pay, retention and time records — ready for a clear Director demonstration.').replace(/ENTERPRISE DEMO · 4 ACTIVE SITES/g,'DIRECTOR DEMO · 2 ACTIVE SITES').replace(/25, 50, 96 and 180-home developments/g,'29-home smooth Site · 70-home complex Site')};

if((db.users||[]).length)migrate(false);
'use strict';

/* SiteSight V1.92 — current release additions only. */

function v192Esc(value){return String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]))}
function v192Js(value){return String(value??'').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\r?\n/g,' ')}
function v192Today(){return new Date().toISOString().slice(0,10)}

// ---- Document control: revisions, expiry and acknowledgement ------
function v192MigrateDocument(row){
  if(!row||row.type!=='file')return false;let changed=false;
  if(!Number.isInteger(row.versionNumber)||row.versionNumber<1){row.versionNumber=1;changed=true}
  if(!Array.isArray(row.versions)||!row.versions.length){row.versions=[{id:'revision-'+row.id+'-1',versionNumber:1,label:'Initial issue',data:row.data||'',originalName:row.originalName||row.name||'Document',mime:row.mime||'',size:Number(row.size||0),notes:row.notes||'',uploadedAt:row.createdAt||new Date().toISOString(),uploadedBy:row.createdBy||''}];changed=true}
  if(!Array.isArray(row.acknowledgements)){row.acknowledgements=[];changed=true}
  if(row.requiresAcknowledgement===undefined){row.requiresAcknowledgement=false;changed=true}
  if(row.expiryDate===undefined){row.expiryDate='';changed=true}
  return changed
}
function v192Migrate(){let changed=false;(db.documents||[]).forEach(row=>{if(v192MigrateDocument(row))changed=true});(db.sites||[]).forEach(siteRow=>{siteRow.attendanceConfig=siteRow.attendanceConfig||{};if(!['browser-allowed','native-required'].includes(siteRow.attendanceConfig.nativeSecurityMode)){siteRow.attendanceConfig.nativeSecurityMode='browser-allowed';changed=true}});(db.users||[]).forEach(user=>{if(Number(user.permissionTemplateVersion||0)<192){user.permissionTemplateVersion=192;changed=true}});return changed}
const v192BaseMigrate=migrate;
migrate=function(persist=true){v192BaseMigrate(false);let changed=v192Migrate();if(persist&&changed)save()};
v192Migrate();

function v192DocAck(row,user=currentUser()){return (row.acknowledgements||[]).find(item=>item.userId===user?.id&&Number(item.versionNumber)===Number(row.versionNumber||1))}
function v192DocState(row){let today=v192Today(),days=row.expiryDate?Math.ceil((new Date(row.expiryDate+'T23:59:59Z')-new Date())/86400000):null;if(days!==null&&row.expiryDate<today)return {kind:'expired',label:'Expired'};if(days!==null&&days<=30)return {kind:'expiring',label:`Expires in ${Math.max(0,days)}d`};if(row.requiresAcknowledgement&&!v192DocAck(row))return {kind:'ack',label:'Read required'};return {kind:'current',label:`Revision ${Number(row.versionNumber||1)}`}}
function v192DocumentAttention(){let files=(db.documents||[]).filter(row=>row.type==='file'&&v191DocumentVisible(row)),expired=files.filter(row=>row.expiryDate&&row.expiryDate<v192Today()).length,expiring=files.filter(row=>row.expiryDate&&row.expiryDate>=v192Today()&&new Date(row.expiryDate+'T23:59:59Z')-new Date()<=30*86400000).length,ack=files.filter(row=>row.requiresAcknowledgement&&!v192DocAck(row)).length;return {expired,expiring,ack,total:expired+expiring+ack}}
function v192FilterDocuments(){let q=String(v191DocumentQuery||'').trim().toLowerCase();document.querySelectorAll('[data-v192-document-search]').forEach(row=>row.hidden=!!q&&!row.dataset.v192DocumentSearch.includes(q))}

v191DocumentsPage=function(){
  if(!v173CanOpenView('documents'))return smartEmpty('Documents permission required.','Ask a Director or manager for document access.','','','▥');let rows=v191DocumentChildren(v191DocumentFolder),path=v191FolderPath(),attention=v192DocumentAttention();
  let summary=attention.total?`<div class="v192-doc-attention"><b>${attention.total} document action${attention.total===1?'':'s'}</b><span>${attention.expired} expired · ${attention.expiring} expiring soon · ${attention.ack} awaiting your acknowledgement</span></div>`:'<div class="v192-doc-attention clear"><b>✓ Document controls are up to date</b><span>No visible expiries or acknowledgements need attention.</span></div>';
  let content=rows.map(row=>{let search=v192Esc([row.name,row.originalName,row.notes].join(' ').toLowerCase());if(row.type==='folder')return `<div class="v191-document-row folder" data-v192-document-search="${search}" ondblclick="v191DocumentFolder='${row.id}';render()"><span class="v191-document-icon">▥</span><span><b>${v192Esc(row.name)}</b><small>${row.siteId?v192Esc(siteName(row.siteId)):'Company'} · ${v192Esc(v191DocumentAudienceLabel(row.audience))}</small></span><button class="btn light smallbtn" onclick="v191DocumentFolder='${row.id}';render()">Open</button>${hasPerm('manage_documents')?`<button class="btn light smallbtn" onclick="v191RenameDocument('${row.id}')">Rename</button><button class="btn red smallbtn" onclick="v191DeleteDocument('${row.id}')">Remove</button>`:''}</div>`;let state=v192DocState(row),ack=v192DocAck(row);return `<div class="v191-document-row file" data-v192-document-search="${search}"><span class="v191-document-icon">${String(row.mime||'').includes('pdf')?'▤':String(row.mime||'').startsWith('image/')?'▧':'▦'}</span><span><b>${v192Esc(row.name)}</b><small>${row.siteId?v192Esc(siteName(row.siteId)):'Company'} · ${v192Esc(v191DocumentAudienceLabel(row.audience))} · Revision ${Number(row.versionNumber||1)}${row.expiryDate?' · expires '+new Date(row.expiryDate+'T12:00:00').toLocaleDateString('en-GB'):''}</small><em class="v192-doc-chip ${state.kind}">${v192Esc(state.label)}</em>${ack?'<em class="v192-doc-chip read">✓ Read</em>':''}</span><button class="btn blue smallbtn" onclick="v191OpenDocument('${row.id}')">Open / print</button>${row.requiresAcknowledgement&&!ack?`<button class="btn orange smallbtn" onclick="v192AcknowledgeDocument('${row.id}')">I have read this</button>`:''}<button class="btn light smallbtn" onclick="v192DocumentHistory('${row.id}')">History</button>${hasPerm('manage_documents')?`<button class="btn light smallbtn" onclick="v192NewRevision('${row.id}')">New revision</button><button class="btn light smallbtn" onclick="v192EditDocumentControl('${row.id}')">Controls</button><button class="btn red smallbtn" onclick="v191DeleteDocument('${row.id}')">Remove</button>`:''}</div>`}).join('');
  setTimeout(v192FilterDocuments,0);return `<div class="topbar"><div><span class="att-kicker">CONTROLLED DOCUMENTS</span><h1>Documents</h1><div class="sub">One current document, a complete revision history and clear expiry/read status.</div></div>${hasPerm('manage_documents')?`<div class="actions"><button class="btn light" onclick="v191NewFolder()">＋ New folder</button><button class="btn orange" onclick="v191UploadDocuments()">⇧ Upload documents</button></div>`:''}</div>${summary}<div class="panel v191-document-tools"><div class="v191-breadcrumbs"><button onclick="v191DocumentFolder='';render()">Documents</button>${path.map(row=>`<span>›</span><button onclick="v191DocumentFolder='${row.id}';render()">${v192Esc(row.name)}</button>`).join('')}</div><div class="v191-document-filters"><input class="input" placeholder="Search this folder…" value="${v192Esc(v191DocumentQuery)}" oninput="v191DocumentQuery=this.value;v192FilterDocuments()"><select class="select" onchange="v191DocumentSite=this.value;render()"><option value="all">All Sites</option><option value="">Company documents</option>${activeSites().filter(row=>canAccessSite(row.id)).map(row=>`<option value="${row.id}" ${v191DocumentSite===row.id?'selected':''}>${v192Esc(row.name)}</option>`).join('')}</select></div></div><div class="panel v191-document-list">${content||'<div class="empty-help"><b>This folder is empty.</b><span>Create a subfolder or upload the first document.</span></div>'}</div>`
};

v191UploadDocuments=function(){if(!hasPerm('manage_documents'))return;modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">CONTROLLED UPLOAD</span><h2>Upload documents</h2><div class="field"><label>Files</label><input id="v191-doc-files" class="input" type="file" multiple accept=".pdf,.docx,.xlsx,.csv,.txt,image/png,image/jpeg,image/webp"></div><div class="field"><label>Display name <span class="muted">(optional for one file)</span></label><input id="v191-doc-name" class="input" placeholder="Defaults to the filename"></div><div class="two"><div class="field"><label>Site</label><select id="v191-doc-site" class="select"><option value="">Company-wide</option>${activeSites().map(row=>`<option value="${row.id}" ${v191DocumentSite===row.id?'selected':''}>${v192Esc(row.name)}</option>`).join('')}</select></div><div class="field"><label>Provide to</label><select id="v191-doc-audience" class="select">${v191AudienceOptions()}</select></div></div><div class="two"><div class="field"><label>Expiry date <span class="muted">(optional)</span></label><input id="v192-doc-expiry" class="input" type="date"></div><label class="v192-check"><input id="v192-doc-ack" type="checkbox"><span><b>Require acknowledgement</b><small>Each authorised user confirms the current revision has been read.</small></span></label></div><div class="field"><label>Issue note <span class="muted">(optional)</span></label><input id="v191-doc-note" class="input" placeholder="e.g. Approved construction issue"></div><button class="btn green" onclick="v191SaveDocuments()">Upload documents</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()};
const v192BaseSaveDocuments=v191SaveDocuments;
v191SaveDocuments=async function(){let before=new Set((db.documents||[]).map(row=>row.id)),expiry=document.getElementById('v192-doc-expiry')?.value||'',requiresAck=!!document.getElementById('v192-doc-ack')?.checked;await v192BaseSaveDocuments();for(const row of db.documents||[])if(!before.has(row.id)&&row.type==='file'){v192MigrateDocument(row);row.expiryDate=expiry;row.requiresAcknowledgement=requiresAck;row.versions[0].label=row.notes||'Initial issue'}if((db.documents||[]).some(row=>!before.has(row.id))){save();render()}};

function v192NewRevision(id){let row=(db.documents||[]).find(item=>item.id===id&&item.type==='file');if(!row||!hasPerm('manage_documents'))return;modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">NEW REVISION</span><h2>${v192Esc(row.name)}</h2><p class="muted">The current file remains in history. Everyone who must acknowledge it will be asked to read this new revision.</p><div class="field"><label>Replacement file</label><input id="v192-revision-file" class="input" type="file" accept=".pdf,.docx,.xlsx,.csv,.txt,image/png,image/jpeg,image/webp"></div><div class="field"><label>Revision note</label><input id="v192-revision-note" class="input" placeholder="e.g. Updated wall tie specification"></div><button class="btn green" onclick="v192SaveRevision('${id}')">Issue new revision</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
async function v192SaveRevision(id){let row=(db.documents||[]).find(item=>item.id===id&&item.type==='file'),file=document.getElementById('v192-revision-file')?.files?.[0],note=(document.getElementById('v192-revision-note')?.value||'').trim();if(!row||!file)return toast('Choose the replacement file');try{let data=await storeMediaFile(file,'',row.siteId||'');v192MigrateDocument(row);let version=Number(row.versionNumber||1)+1,entry={id:'revision-'+row.id+'-'+version,versionNumber:version,label:note||`Revision ${version}`,data,originalName:file.name||row.originalName,mime:file.type||'',size:Number(file.size||0),notes:note,uploadedAt:new Date().toISOString(),uploadedBy:currentUser()?.name||''};row.versions.push(entry);Object.assign(row,{versionNumber:version,data:entry.data,originalName:entry.originalName,mime:entry.mime,size:entry.size,notes:entry.notes,updatedAt:entry.uploadedAt,updatedBy:entry.uploadedBy});save();audit('documents','revision',row.id,`${row.name}: revision ${version} issued`,row.siteId||'',note);modal=null;savedState(`Revision ${version} issued`);render()}catch(error){toast(error.message||'The revision could not be uploaded')}}
function v192DocumentHistory(id){let row=(db.documents||[]).find(item=>item.id===id&&item.type==='file');if(!row||!v191DocumentVisible(row))return;v192MigrateDocument(row);let versions=[...(row.versions||[])].sort((a,b)=>Number(b.versionNumber)-Number(a.versionNumber));modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">REVISION HISTORY</span><h2>${v192Esc(row.name)}</h2><div class="v192-revision-list">${versions.map(version=>`<div><span><b>Revision ${Number(version.versionNumber)}</b><small>${v192Esc(version.label||'Issued document')} · ${version.uploadedAt?new Date(version.uploadedAt).toLocaleString('en-GB'):''} · ${v192Esc(version.uploadedBy||'')}</small></span><button class="btn ${Number(version.versionNumber)===Number(row.versionNumber)?'blue':'light'} smallbtn" onclick="v192OpenRevision('${id}',${Number(version.versionNumber)})">Open / print</button></div>`).join('')}</div><button class="btn light" onclick="modal=null;render()">Close</button></div></div>`;render()}
async function v192OpenRevision(id,versionNumber){let row=(db.documents||[]).find(item=>item.id===id&&item.type==='file'),version=(row?.versions||[]).find(item=>Number(item.versionNumber)===Number(versionNumber));if(!version)return toast('Revision unavailable');let url=sharedMode&&String(version.data||'').startsWith('/uploads/')?`/api/documents/file?documentId=${encodeURIComponent(id)}&version=${Number(versionNumber)}`:await resolveMediaRef(version.data);if(!url)return toast('Revision file unavailable');if(!window.open(url,'_blank','noopener'))toast('Allow pop-ups to open or print this document')}
function v192EditDocumentControl(id){let row=(db.documents||[]).find(item=>item.id===id&&item.type==='file');if(!row||!hasPerm('manage_documents'))return;modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">DOCUMENT CONTROLS</span><h2>${v192Esc(row.name)}</h2><div class="field"><label>Expiry date <span class="muted">(optional)</span></label><input id="v192-control-expiry" class="input" type="date" value="${v192Esc(row.expiryDate||'')}"></div><label class="v192-check"><input id="v192-control-ack" type="checkbox" ${row.requiresAcknowledgement?'checked':''}><span><b>Require acknowledgement</b><small>Applies to the current revision.</small></span></label><button class="btn orange" onclick="v192SaveDocumentControl('${id}')">Save controls</button> <button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function v192SaveDocumentControl(id){let row=(db.documents||[]).find(item=>item.id===id&&item.type==='file');if(!row||!hasPerm('manage_documents'))return;row.expiryDate=document.getElementById('v192-control-expiry')?.value||'';row.requiresAcknowledgement=!!document.getElementById('v192-control-ack')?.checked;row.updatedAt=new Date().toISOString();row.updatedBy=currentUser()?.name||'';save();audit('documents','controls',id,`${row.name}: expiry/read controls updated`,row.siteId||'');modal=null;savedState('Document controls saved');render()}
async function v192AcknowledgeDocument(id){let row=(db.documents||[]).find(item=>item.id===id&&item.type==='file'),user=currentUser();if(!row||!user||!v191DocumentVisible(row))return;if(sharedMode){try{let response=await fetch('/api/documents/acknowledge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({documentId:id,versionNumber:Number(row.versionNumber||1)})}),result=await response.json();v189ApplyServerData(result);if(!result.ok)throw new Error(result.message||'Could not save acknowledgement')}catch(error){return toast(error.message)}}else{row.acknowledgements=row.acknowledgements||[];if(!v192DocAck(row,user))row.acknowledgements.push({userId:user.id,userName:user.name,versionNumber:Number(row.versionNumber||1),acknowledgedAt:new Date().toISOString()});save()}savedState('Document marked as read');render()}

// ---- One Commercial workspace ------------------------------------
let v192CommercialTab='pay-run';
V173_MORE_TOOLS.commercial=['£','Commercial','Pay Run, payment history and CIS review'];delete V173_MORE_TOOLS['pay-run'];delete V173_MORE_TOOLS['payments-history'];
const v192BaseCanOpen=v173CanOpenView;
v173CanOpenView=function(target,user=currentUser()){if(target==='commercial')return v173UserHasAny(user,['view_pay_runs','view_financials']);return v192BaseCanOpen(target,user)};
const v192BaseHome=v173HomeView;
v173HomeView=function(user=currentUser()){let home=v192BaseHome(user);return ['pay-run','payments-history'].includes(home)?'commercial':home};
const v192BaseRoleNav=v173RoleNavItems;
v173RoleNavItems=function(user=currentUser()){let rows=v192BaseRoleNav(user).filter(row=>!['pay-run','payments-history'].includes(row.id));if(v173CanOpenView('commercial',user)&&!rows.some(row=>row.id==='commercial'))rows.splice(Math.min(2,rows.length),0,{id:'commercial',icon:'£',label:'Commercial'});return rows};
const v192BaseMoreIds=v173MoreToolIds;
v173MoreToolIds=function(user=currentUser()){let rows=v192BaseMoreIds(user).filter(id=>!['pay-run','payments-history'].includes(id));if(v173CanOpenView('commercial',user)&&!rows.includes('commercial')&&!v173RoleNavItems(user).some(row=>row.id==='commercial'))rows.unshift('commercial');return rows};
const v192BaseGoNav=goNav;
goNav=function(target){if(target==='pay-run'){v192CommercialTab='pay-run';target='commercial'}else if(target==='payments-history'){v192CommercialTab='history';target='commercial'}else if(target==='cis'){v192CommercialTab='cis';target='commercial'}return v192BaseGoNav(target)};
function v192CommercialCis(){let invalid=paymentFrom&&paymentTo&&paymentFrom>paymentTo,rows=invalid?[]:filteredPaymentRows();return `<div class="panel v192-cis-range"><div><h2>CIS review period</h2><p class="muted">Choose any date range, then review payees and individual payments below.</p></div><div class="actions"><button class="btn light" onclick="setPaymentDatePreset('week')">This week</button><button class="btn light" onclick="setPaymentDatePreset('fortnight')">Fortnight</button><button class="btn light" onclick="v179SetCurrentTaxMonth()">CIS tax month</button></div><div class="two"><div class="field"><label>From</label><input class="input" type="date" value="${paymentFrom||''}" onchange="paymentFrom=this.value;render()"></div><div class="field"><label>To</label><input class="input" type="date" value="${paymentTo||''}" onchange="paymentTo=this.value;render()"></div></div></div>${v177CisReconciliationPanel(rows)}`}
function v192CommercialPage(){if(!v173CanOpenView('commercial'))return smartEmpty('Commercial permission required.','Ask a Director to grant commercial access.','','','£');if(!hasPerm('view_pay_runs')&&v192CommercialTab==='pay-run')v192CommercialTab='history';let tabs=[hasPerm('view_pay_runs')&&['pay-run','Pay Run'],hasPerm('view_financials')&&['history','Payment History'],hasPerm('view_financials')&&['cis','CIS Review']].filter(Boolean),body=v192CommercialTab==='pay-run'?v190PayRunPage():v192CommercialTab==='cis'?v192CommercialCis():paymentsHistoryPage();return `<div class="topbar"><div><span class="att-kicker">COMMERCIAL CONTROL</span><h1>Commercial</h1><div class="sub">Pay preparation, recorded payment history and CIS checks in one place.</div></div></div><div class="v192-commercial-tabs">${tabs.map(([id,label])=>`<button class="${v192CommercialTab===id?'active':''}" onclick="v192CommercialTab='${id}';render()">${label}</button>`).join('')}</div><div class="v192-commercial-body">${body}</div>`}
const v192BaseRender=render;
render=function(){if(currentUser()&&view==='commercial'){shell(v192CommercialPage());updateSyncUI();return}return v192BaseRender()};

// ---- Taskbot is not part of the Operator workspace ---------------
function v192TaskbotAllowed(user=currentUser()){return !!user&&user.role!=='Operator'}
const v192BaseAssistMount=assistMount;
assistMount=function(){if(!v192TaskbotAllowed()){document.getElementById('sitesight-assist-fab')?.remove();document.getElementById('sitesight-assist-panel')?.remove();document.getElementById('sitesight-assist-scrim')?.remove();assistOpen=false;return}return v192BaseAssistMount()};
const v192BaseAssistOpen=assistOpenPanel;
assistOpenPanel=function(prefill=''){if(!v192TaskbotAllowed())return;return v192BaseAssistOpen(prefill)};

// ---- Director resilience and trusted-device control --------------
let v192SystemStatus=null;
async function v192LoadSystemStatus(){if(!v189Director()||!sharedMode)return;try{let response=await fetch('/api/system/status'),result=await response.json();if(response.ok)v192SystemStatus=result}catch(error){}if(view==='functionality')render()}
async function v192RunBackup(){if(!v189Director())return;try{let response=await fetch('/api/backup/run',{method:'POST'}),result=await response.json();if(!response.ok)throw new Error(result.message||'Backup failed');v192SystemStatus=result;savedState('Complete backup created');render()}catch(error){toast(error.message)}}
async function v192ReviewDevice(id,status){if(!v189Director())return;try{let response=await fetch('/api/attendance/device-review',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({deviceRegistrationId:id,status})}),result=await response.json();v189ApplyServerData(result);if(!response.ok)throw new Error(result.message||'Device update failed');await v192LoadSystemStatus();savedState(`Device ${status.toLowerCase()}`)}catch(error){toast(error.message)}}
function v192SetNativeMode(siteId,value){if(!v189Director()||!['browser-allowed','native-required'].includes(value))return;let cfg=attendanceSiteConfig(siteId);cfg.nativeSecurityMode=value;save();audit('attendance','device security',siteId,`${siteName(siteId)}: ${value==='native-required'?'registered native device required':'browser GPS allowed'}`,siteId);savedState('Clock security saved');render()}
function v192FunctionalityPanels(){if(!v189Director())return'';let status=v192SystemStatus,devices=status?.devices||db.deviceRegistrations||[];return `<div class="panel v192-resilience"><div class="sectionhead"><div><span class="att-kicker">RESILIENCE</span><h2>Backup and monitoring</h2><p class="muted">Complete database and uploaded-file backups, with a health endpoint for production monitoring.</p></div>${sharedMode?'<button class="btn light smallbtn" onclick="v192LoadSystemStatus()">Refresh</button>':''}</div>${!sharedMode?'<div class="v192-status warn"><b>Server status is available in the shared production version.</b></div>':!status?'<div class="v192-status"><b>Loading server status…</b></div>':`<div class="v192-status ${status.backupConfigured?'ok':'warn'}"><b>${status.backupConfigured?'Off-device backup configured':'Off-device backup not configured'}</b><span>${status.lastBackupAt?'Last complete backup '+new Date(status.lastBackupAt).toLocaleString('en-GB'):'No complete off-device backup recorded'} · Health ${status.healthy?'healthy':'needs attention'}</span></div><div class="actions"><button class="btn orange" onclick="v192RunBackup()" ${status.backupConfigured?'':'disabled'}>Run complete backup now</button><code>/api/health</code></div>`}</div><div class="panel v192-device-control"><div class="sectionhead"><div><span class="att-kicker">CLOCK EVIDENCE</span><h2>Trusted devices</h2><p class="muted">Browser GPS remains available unless a Site is switched to registered native devices.</p></div></div><div class="v192-site-security">${activeSites().map(siteRow=>`<label><span><b>${v192Esc(siteRow.name)}</b><small>Only enable Native required after the signed mobile app is deployed.</small></span><select class="select" onchange="v192SetNativeMode('${siteRow.id}',this.value)"><option value="browser-allowed" ${attendanceSiteConfig(siteRow.id)?.nativeSecurityMode!=='native-required'?'selected':''}>Browser GPS allowed</option><option value="native-required" ${attendanceSiteConfig(siteRow.id)?.nativeSecurityMode==='native-required'?'selected':''}>Registered native device required</option></select></label>`).join('')}</div><div class="v192-device-list">${devices.map(device=>`<div><span><b>${v192Esc((db.users||[]).find(user=>user.id===device.userId)?.name||'Clocking user')}</b><small>${v192Esc(device.platform||'Device')} · ${v192Esc(device.deviceId||'')} · ${v192Esc(device.status||'Pending')}</small></span><div class="actions">${device.status!=='Active'?`<button class="btn green smallbtn" onclick="v192ReviewDevice('${device.id}','Active')">Approve</button>`:''}${device.status!=='Revoked'?`<button class="btn red smallbtn" onclick="v192ReviewDevice('${device.id}','Revoked')">Revoke</button>`:''}</div></div>`).join('')||'<div class="muted">No native clocking devices have registered.</div>'}</div></div>`}
const v192BaseFunctionality=v189FunctionalityPage;
v189FunctionalityPage=function(){let html=v192BaseFunctionality()+v192FunctionalityPanels();if(sharedMode&&!v192SystemStatus)setTimeout(v192LoadSystemStatus,0);return html};

// Native bridge contract: these values are covered by the device signature.
const v192BrowserPosition=attendanceGetPosition;
attendanceGetPosition=function(){let bridge=window.SiteSightNative;if(bridge?.getClockEvidence)return Promise.resolve(bridge.getClockEvidence()).then(point=>({...point,latitude:Number(point.latitude),longitude:Number(point.longitude),accuracy:Number(point.accuracy),positionTimestamp:Number(point.positionTimestamp||Date.now()),sampleCount:Number(point.sampleCount||1),deviceId:String(point.deviceId||''),integrity:point.integrity||point.deviceIntegrity||'trusted-native',deviceIntegrity:point.deviceIntegrity||point.integrity||'trusted-native',isMocked:point.isMocked===true,developerMode:point.developerMode===true,signature:point.signature||'',monotonicMs:Number(point.monotonicMs||0)}));return v192BrowserPosition()};

// Current release labels and guide routes.
const v192BaseShell=shell;
shell=function(content){v192BaseShell(content);let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace(/v1\.91\.0 · RETENTION & DOCUMENT CONTROL(?: DEMO)?/g,`v1.92.0 · CONTROLLED OPERATIONS${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`);if(db.demoProfile==='v192-two-site-director')root.innerHTML=root.innerHTML.replace('4 active sites · 351 plots · full operational dataset','2 active sites · 99 plots · controlled operational dataset')};
const v192BaseDashboard=dashboard;
dashboard=function(){let html=v192BaseDashboard();return db.demoProfile==='v192-two-site-director'?html.replace('ENTERPRISE DEMO · four active developments and 351 plots under full operational use','DIRECTOR DEMO · a smooth 29-home Site beside a complex 70-home Site'):html};
const v192BaseLoginScreen=loginScreen;
loginScreen=function(){v192BaseLoginScreen();if(db.demoProfile!=='v192-two-site-director')return;let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace('Four live developments. One business view.','Two contrasting developments. One clear business view.').replace(/351 plots, 62 people and realistic operational records — ready to demonstrate SiteSight at contractor scale\./g,'99 plots, 30 frontline workers and complete pay, document and time controls — ready for a Director demonstration.').replace(/ENTERPRISE DEMO · 4 ACTIVE SITES/g,'DIRECTOR DEMO · 2 ACTIVE SITES').replace(/25, 50, 96 and 180-home developments/g,'29-home smooth Site · 70-home complex Site')};
const v192BaseGuide=newUserGuidePage;
newUserGuidePage=function(){return v192BaseGuide().replace(/RETENTION & DOCUMENT CONTROL · V1\.91/g,'CONTROLLED OPERATIONS · V1.92').replace(/Pay Run, Retention, CIS and Documents/g,'Commercial workspace, controlled Documents and trusted clocking')};
const v192BaseAssistLabel=assistPageLabel;
assistPageLabel=function(){return view==='commercial'?'Commercial':v192BaseAssistLabel()};
const v192BaseAssistGuide=assistGuide;
assistGuide=function(target,ctx){if(target==='commercial')return'Commercial keeps Pay Run, Payment History and CIS Review together. Choose the tab matching the job you need to complete.';return v192BaseAssistGuide(target,ctx)};

if((db.users||[]).length)migrate(false);
'use strict';

/* SiteSight V1.93 — current workflow-clarity additions only. */

const V193_MINIMUM_OVERTIME_MINUTES=15;

function v193Esc(value){return contractorEsc(String(value??''))}
function v193Migrate(){let changed=false;for(const user of db.users||[])if(Number(user.permissionTemplateVersion||0)<193){user.permissionTemplateVersion=193;changed=true}return changed}
const v193BaseMigrate=migrate;
migrate=function(persist=true){v193BaseMigrate(false);let changed=v193Migrate();if(changed&&persist)save();return changed};

// A person clocked in anywhere is never offered a second Supervisor Clock In.
const v193BasePeopleSummary=contractorPeopleSummary;
contractorPeopleSummary=function(...args){let summary=v193BasePeopleSummary(...args),activeIds=new Set((db.attendanceShifts||[]).filter(row=>!row.voided&&!row.clockOut?.time).map(row=>row.workerId));summary.notIn=(summary.notIn||[]).filter(row=>!activeIds.has(row.member.id));return summary};

// Attendance decisions already have dedicated action queues. Keep People & Time focused.
v190SupervisorCheck=function(){return''};

function v193ManualOvertimeRows(siteId='all'){
  return (db.attendanceShifts||[]).filter(shift=>(siteId==='all'||shift.siteId===siteId)&&!shift.voided&&shift.clockOut?.time&&shift.overtimeApproval?.manualRequest===true&&shift.overtimeApproval?.status==='Pending'&&Number(shift.overtimeApproval?.minutes||0)>=V193_MINIMUM_OVERTIME_MINUTES).map(shift=>({shift,wm:attendanceMemberById(shift.workerId),request:shift.overtimeApproval})).filter(row=>row.wm).sort((a,b)=>String(b.request.requestedAt||b.shift.clockOut.time).localeCompare(String(a.request.requestedAt||a.shift.clockOut.time)));
}

function v193RecentCompletedShifts(siteId){
  return (db.attendanceShifts||[]).filter(shift=>(!siteId||siteId==='all'||shift.siteId===siteId)&&!shift.voided&&shift.clockOut?.time&&!['Pending','Approved'].includes(shift.overtimeApproval?.status||'')).sort((a,b)=>String(b.clockOut.time).localeCompare(String(a.clockOut.time))).slice(0,80);
}
function v193OpenManagerOvertime(siteId=attendanceDefaultSiteId()){
  if(!v175CanApproveOvertime())return toast('Overtime permission is required');let shifts=v193RecentCompletedShifts(siteId);if(!shifts.length)return toast('There are no completed shifts available');
  modal=`<div class="modal"><div class="modalbox v193-manager-overtime"><span class="att-kicker">MANUAL OVERTIME</span><h2>Add overtime for a person</h2><p class="muted">Choose the completed shift and enter only additional time actually worked. Staying late never creates a request automatically.</p><div class="field"><label>Person and shift</label><select id="v193-manager-ot-shift" class="select">${shifts.map(shift=>{let wm=attendanceMemberById(shift.workerId);return `<option value="${shift.id}">${v193Esc(wm?.member.name||'Unknown')} · ${v193Esc(siteName(shift.siteId))} · ${attendanceDateOnly(shift.clockIn.time,shift.siteId)}</option>`}).join('')}</select></div><div class="field"><label>Overtime minutes</label><input id="v193-manager-ot-minutes" class="input" type="number" min="${V193_MINIMUM_OVERTIME_MINUTES}" max="480" step="5" value="30"></div><div class="field"><label>Note <span class="muted">(optional)</span></label><textarea id="v193-manager-ot-reason" class="textarea" placeholder="Work completed or instruction"></textarea></div><div class="actions"><button class="btn orange" onclick="v193SubmitManagerOvertime()">Add for review</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render();
}
async function v193SubmitManagerOvertime(){
  if(!v175CanApproveOvertime())return toast('Overtime permission is required');let shiftId=document.getElementById('v193-manager-ot-shift')?.value||'',minutes=Math.round(Number(document.getElementById('v193-manager-ot-minutes')?.value||0)),reason=(document.getElementById('v193-manager-ot-reason')?.value||'').trim(),shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId);
  if(!shift||minutes<V193_MINIMUM_OVERTIME_MINUTES||minutes>480)return toast(`Enter between ${V193_MINIMUM_OVERTIME_MINUTES} and 480 minutes`);
  if(sharedMode&&navigator.onLine){try{if(pendingSave)await syncToServer(true);let response=await fetch('/api/attendance/overtime-add',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({shiftId,minutes,reason})}),result=await response.json();if(!response.ok)throw new Error(result.message||'Overtime could not be added');v189ApplyServerData(result);modal=null;savedState('Overtime added for review');render();return}catch(error){return toast(error.message)}}
  shift.overtimeApproval={status:'Pending',minutes,reason,manualRequest:true,requestedBy:currentUser()?.name||'',requestedById:currentUser()?.id||'',requestedAt:new Date().toISOString(),addedBySupervisor:true,approvedBy:'',approvedById:'',approvedAt:''};attendanceMarkApprovalNeedsRecheck(shift.workerId,shift.siteId,shift.clockIn.time);save();audit('attendance','overtime request',shift.id,`${attendanceMemberById(shift.workerId)?.member.name||'Individual'}: ${minutes} minutes manually added for review`,shift.siteId);modal=null;savedState('Overtime added for review');render();
}

// Only deliberate requests appear here; raw time after finish stays excluded and invisible to approval.
v174PendingOvertimePanel=function(siteId){
  if(!v175CanApproveOvertime())return'';let rows=v193ManualOvertimeRows(siteId);
  return `<div class="panel v174-pending-overtime"><div class="sectionhead"><div><h2>Overtime awaiting decision</h2><div class="muted">Only manual requests of at least ${V193_MINIMUM_OVERTIME_MINUTES} minutes appear here. Late clock-outs never create overtime.</div></div><div class="actions"><span class="badge ${rows.length?'b-amber':'b-green'}">${rows.length}</span><button class="btn light smallbtn" onclick="v193OpenManagerOvertime('${siteId}')">＋ Add overtime</button></div></div>${rows.map(row=>`<div class="v174-overtime-row"><span><b>${v193Esc(row.wm.member.name)}</b><small>${attendanceDateOnly(row.shift.clockIn.time,row.shift.siteId)} · ${v193Esc(siteName(row.shift.siteId))} · requested by ${v193Esc(row.request.requestedBy||'user')}</small></span><strong>${v169MinutesLabel(row.request.minutes)}</strong><button class="btn orange smallbtn" onclick="v174OpenOvertimeApproval('${row.shift.id}')">Review</button></div>`).join('')||'<div class="muted">No manually requested overtime is awaiting a decision.</div>'}</div>`;
};

const v193BaseOpenOvertimeRequest=v188OpenOvertimeRequest;
v188OpenOvertimeRequest=function(shiftId){v193BaseOpenOvertimeRequest(shiftId);if(!modal)return;modal=modal.replace(/min="1"/,'min="15"').replace(/Enter only the additional time you actually worked\./,'Enter only the additional time you actually worked. Requests must be at least 15 minutes.');render()};
const v193BaseSubmitOvertimeRequest=v188SubmitOvertimeRequest;
v188SubmitOvertimeRequest=function(shiftId){let minutes=Math.round(Number(document.getElementById('v188-ot-request-minutes')?.value||0));if(minutes<V193_MINIMUM_OVERTIME_MINUTES)return toast(`Overtime requests start at ${V193_MINIMUM_OVERTIME_MINUTES} minutes`);return v193BaseSubmitOvertimeRequest(shiftId)};

const v193BaseOvertimeStatus=v174OvertimeStatusHtml;
v174OvertimeStatusHtml=function(shift,member){let request=shift?.overtimeApproval||{};if(!request.manualRequest||request.status==='Not Requested')return '<span class="v174-ot none">No overtime requested</span>';return v193BaseOvertimeStatus(shift,member)};

// Every dashboard action opens the exact record instead of a broad page.
function v193OpenTimesheetRecord(workerId,siteId,week){if(!v1998CanReviewTimeRecords())return toast('A Director must grant this account shift-review permission');if(siteId)site=siteId;attendanceWorker=workerId||'';contractorAttendanceTab='timesheets';view='attendance';selected=null;render();setTimeout(()=>openAttendanceWorkerWeek(workerId,siteId,week),40)}
function v193OpenPayLock(siteId,from,to){site=siteId;v174PayFrom=from;v174PayTo=to;v174PayPreset='custom';v190PaySites=[siteId];v192CommercialTab='pay-run';goNav('commercial')}
const v193BaseExceptionRows=v175ExceptionRows;
v175ExceptionRows=function(){
  let rows=v193BaseExceptionRows().filter(row=>!['overtime','clock','timesheet','issue','payroll'].includes(row.kind)),siteId=site==='all'?'all':site,today=iso(),match=id=>siteId==='all'||id===siteId;
  if(v175CanApproveOvertime()){let pending=v193ManualOvertimeRows(siteId);if(pending.length){let row=pending[0];rows.unshift({kind:'overtime',count:pending.length,title:`${row.wm.member.name}: overtime request`,detail:`${row.request.minutes} minutes · ${attendanceDateOnly(row.shift.clockIn.time,row.shift.siteId)}`,action:`v174OpenOvertimeApproval('${row.shift.id}')`})}}
  if(v175CanCorrectTime()){let open=(db.attendanceShifts||[]).filter(shift=>match(shift.siteId)&&!shift.voided&&!shift.clockOut?.time&&attendanceDateOnly(shift.clockIn?.time,shift.siteId)<today).sort((a,b)=>String(a.clockIn.time).localeCompare(String(b.clockIn.time)));if(open.length){let shift=open[0],wm=attendanceMemberById(shift.workerId);rows.push({kind:'clock',count:open.length,title:`${wm?.member.name||'Individual'}: missing Clock Out`,detail:`Open since ${attendanceDateOnly(shift.clockIn.time,shift.siteId)} · ${siteName(shift.siteId)}`,action:`supervisorAttendanceClock('${shift.workerId}','out','${shift.siteId}')`})}}
  if(hasPerm('approve_timesheets')){let approvals=(db.timesheetApprovals||[]).filter(row=>!row.superseded&&row.needsReapproval&&match(row.siteId));if(approvals.length){let approval=approvals[0];rows.push({kind:'timesheet',count:approvals.length,title:`${contractorMemberName(approval.workerId)}: timesheet changed`,detail:`Week ${approval.weekStart} · approval required again`,action:`v193OpenTimesheetRecord('${approval.workerId}','${approval.siteId}','${approval.weekStart}')`})}}
  if(hasPerm('view_issues')){let due=[];(db.items||[]).filter(item=>match(item.site)&&canAccessSite(item.site)).forEach(item=>(item.issues||[]).filter(issue=>issue.status==='Open'&&issue.targetDate&&issue.targetDate<today).forEach(issue=>due.push({item,issue})));if(due.length){let row=due.sort((a,b)=>String(a.issue.targetDate).localeCompare(String(b.issue.targetDate)))[0];rows.push({kind:'issue',count:due.length,title:`${row.item.ref}: ${row.issue.title}`,detail:`Overdue since ${row.issue.targetDate}`,action:`viewIssue('${row.item.id}','${row.issue.id}')`})}}
  if(hasPerm('lock_payroll')){let changed=(db.payrollLocks||[]).filter(lock=>!lock.superseded&&!lock.unlockedAt&&match(lock.siteId)&&v175PayrollSnapshot(lock.siteId,lock.from,lock.to).hash!==lock.snapshotHash);if(changed.length){let lock=changed[0];rows.push({kind:'payroll',count:changed.length,title:`${siteName(lock.siteId)}: locked pay changed`,detail:`${lock.from} to ${lock.to} · review and re-lock`,action:`v193OpenPayLock('${lock.siteId}','${lock.from}','${lock.to}')`})}}
  return rows;
};

// Fortnight is a first-class Analytics quick range, while custom dates remain available.
const v193BaseAnalyticsDates=v169AnalyticsDates;
v169AnalyticsDates=function(){if(v169AttendanceRange!=='fortnight')return v193BaseAnalyticsDates();let to=iso();return {from:attendanceAddDays(to,-13),to}};
const v193BaseAttendanceAnalyticsPanel=v169AttendanceAnalyticsPanel;
v169AttendanceAnalyticsPanel=function(){let html=v193BaseAttendanceAnalyticsPanel();return html.replace(/(<button class="[^"]*" onclick="v169AttendanceRange='week';render\(\)">This Week<\/button>)/,`$1<button class="${v169AttendanceRange==='fortnight'?'active':''}" onclick="v169AttendanceRange='fortnight';render()">Fortnightly</button>`)};

// Current release labels.
const v193BaseShell=shell;
shell=function(content){v193BaseShell(content);let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace(/v1\.92\.0 · CONTROLLED OPERATIONS(?: DEMO)?/g,`v1.93.0 · WORKFLOW CLARITY${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`);if(db.demoProfile==='v193-two-site-director')root.innerHTML=root.innerHTML.replace('4 active sites · 351 plots · full operational dataset','2 active sites · 99 plots · controlled operational dataset')};
const v193BaseDashboard=dashboard;
dashboard=function(){let html=v193BaseDashboard();return db.demoProfile==='v193-two-site-director'?html.replace('ENTERPRISE DEMO · four active developments and 351 plots under full operational use','DIRECTOR DEMO · a smooth 29-home Site beside a complex 70-home Site'):html};
const v193BaseLoginScreen=loginScreen;
loginScreen=function(){v193BaseLoginScreen();if(db.demoProfile!=='v193-two-site-director')return;let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace('Four live developments. One business view.','Two contrasting developments. One clear business view.').replace(/351 plots, 62 people and realistic operational records — ready to demonstrate SiteSight at contractor scale\./g,'99 plots, 30 frontline workers and complete pay, document and time controls — ready for a Director demonstration.').replace(/ENTERPRISE DEMO · 4 ACTIVE SITES/g,'DIRECTOR DEMO · 2 ACTIVE SITES').replace(/25, 50, 96 and 180-home developments/g,'29-home smooth Site · 70-home complex Site')};
const v193BaseGuide=newUserGuidePage;
newUserGuidePage=function(){return v193BaseGuide().replace(/CONTROLLED OPERATIONS · V1\.92/g,'WORKFLOW CLARITY · V1.93')};

if((db.users||[]).length)migrate(false);
'use strict';

/* SiteSight V1.94 — whole-product review fixes and accessibility polish. */

const V194_PERMISSION_TEMPLATE=194;

function v194Migrate(){
  let changed=false;
  for(const user of db.users||[])if(Number(user.permissionTemplateVersion||0)<V194_PERMISSION_TEMPLATE){
    user.permissionTemplateVersion=V194_PERMISSION_TEMPLATE;
    changed=true;
  }
  return changed;
}
const v194BaseMigrate=migrate;
migrate=function(persist=true){v194BaseMigrate(false);let changed=v194Migrate();if(changed&&persist)save();return changed};

// Expected Workforce is site-specific. The retired Team container must never
// make every individual appear on every Site merely because work exists there.
function v194MemberAssignedToSite(row,siteId){
  let member=row?.member;if(!member||!siteId)return false;
  if((member.clockSiteIds||[]).includes(siteId))return true;
  let linked=v183MemberUser?.(member);
  if(linked&&(linked.allowedSites||[]).includes(siteId))return true;
  return (db.items||[]).some(item=>item.site===siteId&&(item.assignedId===member.id||(item.lifts||[]).some(lift=>lift.assignedId===member.id)));
}
attendanceExpectedMembers=function(siteId,date=iso()){
  return attendanceAllMembers().filter(row=>{
    let member=row.member,activeHere=attendanceActiveShift(member?.id)?.siteId===siteId;
    if(!member||member.activeForAttendance===false||member.employmentStatus==='Leaver'||member.employmentStatus==='Inactive')return false;
    if(member.startDate&&date<member.startDate)return false;
    if(member.leavingDate&&date>member.leavingDate)return false;
    if(activeHere)return true;
    return v194MemberAssignedToSite(row,siteId)&&v174MemberScheduleForDate(member,siteId,date).active;
  });
};

// The live snapshot now reflects the actual manual-overtime workflow. Raw late
// clock-outs are excluded from pay and are not presented as awaiting approval.
v182AttendanceSnapshot=function(siteId){
  let today=iso(),shifts=(db.attendanceShifts||[]).filter(row=>row.siteId===siteId&&!row.voided&&attendanceDateOnly(row.clockIn?.time,row.siteId)===today),active=shifts.filter(row=>!row.clockOut?.time),people=[...new Set(shifts.map(row=>row.workerId))],approved=shifts.reduce((sum,row)=>sum+Number(row.overtimeApproval?.manualRequest===true&&row.overtimeApproval?.status==='Approved'?row.overtimeApproval.minutes||0:0),0),pending=shifts.filter(row=>row.clockOut?.time&&row.overtimeApproval?.manualRequest===true&&row.overtimeApproval?.status==='Pending'&&Number(row.overtimeApproval?.minutes||0)>=V193_MINIMUM_OVERTIME_MINUTES).length,leave=(db.leaveRequests||[]).filter(row=>row.siteId===siteId&&row.status==='Approved'&&row.from<=today&&row.to>=today).length;
  return {today,shifts,active,people,approved,pending,leave};
};

// Make the attendance rule explicit where managers interpret the figures.
const v194BaseAttendanceAnalyticsPanel=v169AttendanceAnalyticsPanel;
v169AttendanceAnalyticsPanel=function(){
  let html=v194BaseAttendanceAnalyticsPanel();
  return html.replace('Recorded attendance and actual individual payment records for the selected period.','Recorded attendance and actual individual payment records for the selected period.<br><span class="v194-rule-note">Punctuality uses each Site\'s saved late-arrival and early-leaving grace periods.</span>');
};

function v194EnhanceRenderedUi(root){
  if(!root)return;
  if(!root.innerHTML.includes('v194-skip-link'))root.innerHTML=`<button class="v194-skip-link" onclick="let target=document.querySelector('.main');if(target){target.setAttribute('tabindex','-1');target.focus()}">Skip to main content</button>${root.innerHTML}`;
  if(typeof root.querySelectorAll==='function')root.querySelectorAll('.nav button.active,.mobilebar button.active,.att-tabs button.active,.v192-commercial-tabs button.active').forEach(button=>button.setAttribute('aria-current','page'));
  if(typeof document.querySelector==='function'){
    let heading=document.querySelector('.main h1');
    if(heading?.textContent)document.title=`SiteSight — ${heading.textContent.trim()}`;
  }
}

const v194BaseShell=shell;
shell=function(content){
  v194BaseShell(content);
  let root=document.getElementById('root');if(!root)return;
  root.innerHTML=root.innerHTML.replace(/v1\.93\.0 · WORKFLOW CLARITY(?: DEMO)?/g,`v1.94.0 · PRODUCT REVIEW${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`);
  if(db.demoProfile==='v194-two-site-director')root.innerHTML=root.innerHTML.replace('4 active sites · 351 plots · full operational dataset','2 active sites · 99 plots · reviewed operational dataset');
  v194EnhanceRenderedUi(root);
};

const v194BaseDashboard=dashboard;
dashboard=function(){let html=v194BaseDashboard();return db.demoProfile==='v194-two-site-director'?html.replace('ENTERPRISE DEMO · four active developments and 351 plots under full operational use','DIRECTOR DEMO · a smooth 29-home Site beside a complex 70-home Site'):html};
const v194BaseLoginScreen=loginScreen;
loginScreen=function(){v194BaseLoginScreen();if(db.demoProfile!=='v194-two-site-director')return;let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace('Four live developments. One business view.','Two contrasting developments. One clear business view.').replace(/351 plots, 62 people and realistic operational records — ready to demonstrate SiteSight at contractor scale\./g,'99 plots, 30 frontline workers and complete pay, document and time controls — ready for a Director demonstration.').replace(/ENTERPRISE DEMO · 4 ACTIVE SITES/g,'DIRECTOR DEMO · 2 ACTIVE SITES').replace(/25, 50, 96 and 180-home developments/g,'29-home smooth Site · 70-home complex Site')};
const v194BaseGuide=newUserGuidePage;
newUserGuidePage=function(){return v194BaseGuide().replace(/WORKFLOW CLARITY · V1\.93/g,'PRODUCT REVIEW · V1.94')};

if((db.users||[]).length)migrate(false);
'use strict';

/* SiteSight V1.95 — role-focused daily workspaces and data-quality control. */

const V195_PERMISSION_TEMPLATE=195;

function v195Migrate(){
  let changed=false;
  if(!Array.isArray(db.commercialSavedViews)){db.commercialSavedViews=[];changed=true}
  for(const user of db.users||[]){
    if(Number(user.permissionTemplateVersion||0)>=V195_PERMISSION_TEMPLATE)continue;
    user.permissions=user.permissions||{};
    if(user.role==='Read Only'&&user.permissions.download_reports!==true){user.permissions.download_reports=true;changed=true}
    user.permissionTemplateVersion=V195_PERMISSION_TEMPLATE;changed=true;
  }
  return changed;
}
const v195BaseMigrate=migrate;
migrate=function(persist=true){v195BaseMigrate(false);let changed=v195Migrate();if(changed&&persist)save();return changed};

// Role routes --------------------------------------------------------
V173_MAIN_NAV['my-work']={icon:'▣',label:'My Work'};
V173_MORE_TOOLS['my-work']=['▣','My Work','Assigned work, evidence and issues in one focused list'];
V173_MORE_TOOLS['data-quality']=['✓','Data Quality','Find incomplete people, Site, pay and approval records'];
function v1998OperatorHasWork(){return false}
if(V173_ROLE_PROFILES['Read Only']){
  let profile=V173_ROLE_PROFILES['Read Only'];
  if(!profile.permissions.includes('download_reports'))profile.permissions.push('download_reports');
  if(!profile.extra.includes('weekly-report'))profile.extra.unshift('weekly-report');
}
const v195BaseCanOpenView=v173CanOpenView;
v173CanOpenView=function(target,user=currentUser()){
  if(target==='my-work')return v1998OperatorHasWork(user);
  if(target==='data-quality')return !!user&&user.role==='Director';
  return v195BaseCanOpenView(target,user);
};
const v195BaseMoreToolIds=v173MoreToolIds;
v173MoreToolIds=function(user=currentUser()){
  let rows=v195BaseMoreToolIds(user);
  if(user?.role==='Director'&&!rows.includes('data-quality'))rows.unshift('data-quality');
  return rows;
};

function v195OpenView(target){view=target;selected=null;selectedGang=null;render()}
function v195OpenItem(itemId){let item=db.items.find(row=>row.id===itemId);if(!canViewItem(item))return toast('Plot cards are available to Supervisors and above');selected=itemId;selectedGang=null;view='work-items';render()}
function v195OpenPerson(memberId){let row=attendanceMemberById(memberId);if(!row)return;view='gangs';selected=null;selectedGang=null;openTeamMember(row.gang.id,memberId)}
function v195OpenAdminUsers(){adminTab='users';v195OpenView('admin')}
function v195OpenPayReview(){contractorAttendanceTab='pay';v195OpenView('attendance')}

// Small contextual next step — shown only when a real action exists.
function v195NextAction(){
  let user=currentUser();if(!user)return null;
  if(user.role==='Director'){
    let count=v195DataQualitySummary().total;
    if(count)return {icon:'✓',title:`${count} data check${count===1?'':'s'} need attention`,text:'Review incomplete setup and unresolved operational records before they affect reporting.',label:'Review data quality',action:"v195OpenView('data-quality')"};
  }
  if(user.role==='Quantity Surveyor'){
    let saved=v195CommercialSavedData();
    if(saved.variations.length)return {icon:'£',title:`${saved.variations.length} variation${saved.variations.length===1?'':'s'} awaiting review`,text:'Open the Commercial saved view to see the exact plots and values.',label:'Open saved views',action:"v192CommercialTab='saved';v195OpenView('commercial')"};
    if(saved.retention.length)return {icon:'£',title:`${saved.retention.length} retention release${saved.retention.length===1?'':'s'} available`,text:'Signed-off work has retention ready for review.',label:'Review retention',action:"v192CommercialTab='saved';v195OpenView('commercial')"};
  }
  if(user.role==='Accounts')return {icon:'£',title:'Reconcile current payments',text:'Compare due, approved and paid values without opening operational editing screens.',label:'Open reconciliation',action:"v192CommercialTab='reconcile';v195OpenView('commercial')"};
  if(user.role==='Read Only'&&site!=='all'&&hasPerm('download_reports'))return {icon:'▤',title:`Share ${siteName(site)} status`,text:'Create a read-only Site report using the current information and no editing controls.',label:'Create report',action:`adminReportOptions('${site}')`};
  return null;
}
function v195NextActionHtml(){let row=v195NextAction();return row?`<section class="v195-next-action"><span>${row.icon}</span><div><small>NEXT ACTION</small><b>${v195Esc(row.title)}</b><p>${v195Esc(row.text)}</p></div><button class="btn orange" onclick="${row.action}">${v195Esc(row.label)} →</button></section>`:''}

// Operators with plot permission: only assigned work, evidence and relevant issues.
function v195ItemAssignedToUser(item,user=currentUser()){
  let memberId=user?.linkedMemberId;if(!memberId)return false;
  let assigned=target=>target?.assignedId===memberId||(target?.priceworkAllocations||[]).some(row=>row.memberId===memberId);
  return assigned(item)||(item.lifts||[]).some(assigned)||(item.issues||[]).some(issue=>issue.assignedTo===`user:${user.id}`||issue.assignedTo===`person:${memberId}`);
}
function v195MyWorkRows(user=currentUser()){
  return (db.items||[]).filter(item=>canAccessSite(item.site)&&v195ItemAssignedToUser(item,user)).sort((a,b)=>{
    let rank=item=>issues(item)>0?0:overall(item)==='In progress'?1:overall(item)==='Not started'?2:overall(item)==='Complete'?3:4;
    return rank(a)-rank(b)||String(a.ref||'').localeCompare(String(b.ref||''),undefined,{numeric:true});
  });
}
function v195MyWorkPage(){
  let rows=v195MyWorkRows(),openIssues=rows.reduce((sum,item)=>sum+issues(item),0),evidence=rows.filter(item=>overall(item)==='In progress'&&!(item.workPhotos||[]).length),next=rows.find(item=>issues(item)>0)||evidence[0]||rows.find(item=>overall(item)==='In progress')||rows[0];
  let nextHtml=next?`<section class="v195-next-action"><span>${issues(next)?'⚠':'▣'}</span><div><small>NEXT ACTION</small><b>${v195Esc(next.ref)} · ${v195Esc(issues(next)?'Check the open issue':evidence.includes(next)?'Add progress evidence':'Continue assigned work')}</b><p>${v195Esc(siteName(next.site))} · ${v195Esc(v176PlotProgressText(next))}</p></div><button class="btn orange" onclick="v195OpenItem('${next.id}')">Open ${v195Esc(next.ref)} →</button></section>`:'';
  return `<div class="topbar"><div><span class="att-kicker">OPERATOR WORKSPACE</span><h1>My Work</h1><div class="sub">Visual plot cards for the work assigned to you, its evidence and open issues.</div></div><div class="actions">${viewingSitePill()}${v173CanOpenView('clock')?'<button class="btn blue" onclick="goNav(\'clock\')">◷ Clock In / Out</button>':''}</div></div>${nextHtml}<div class="v195-my-work-summary"><div><span>ASSIGNED</span><b>${rows.length}</b></div><div class="${openIssues?'warn':''}"><span>OPEN ISSUES</span><b>${openIssues}</b></div><div class="${evidence.length?'warn':''}"><span>EVIDENCE NEEDED</span><b>${evidence.length}</b></div></div><section class="panel v1998-my-work-panel"><div class="sectionhead"><div><h2>Assigned plots & work</h2><p class="muted">Open a card to see its stages, evidence and permitted actions.</p></div></div>${rows.length?`<div class="cards v1998-my-work-grid">${rows.map(card).join('')}</div>`:smartEmpty('No work is currently assigned to you.','Your Supervisor or manager can assign a plot or lift.','','','▣')}</section>`;
}

// Director-only data quality ----------------------------------------
function v195DataQualitySummary(){
  let users=(db.users||[]).filter(user=>user.accountActive!==false),members=v183ActiveMembers(),today=iso(),scopedRoles=['Site Manager','Supervisor','Operator'];
  let unlinkedUsers=users.filter(user=>user.role==='Operator'&&user.permissions?.clock_attendance&&!user.linkedMemberId),missingSites=users.filter(user=>scopedRoles.includes(user.role)&&user.role!=='Director'&&!(user.allowedSites||[]).length).map(user=>({kind:'user',row:user}));
  members.filter(member=>!(member.clockSiteIds||[]).length).forEach(member=>missingSites.push({kind:'member',row:member}));
  let incompletePay=members.filter(member=>member.employmentType==='Not tracked'||member.employmentType==='Hourly'&&Number(member.hourlyRate||member.basePay||0)<=0||member.employmentType==='Day Rate'&&Number(member.dayRate||0)<=0);
  let openIssues=(db.items||[]).flatMap(item=>(item.issues||[]).filter(issue=>issue.status==='Open').map(issue=>({item,issue}))),pendingOt=v193ManualOvertimeRows('all'),oldOpen=(db.attendanceShifts||[]).filter(shift=>!shift.voided&&!shift.clockOut?.time&&attendanceDateOnly(shift.clockIn?.time,shift.siteId)<today),unlinkedPeople=members.filter(member=>!v183MemberUser(member));
  let total=unlinkedUsers.length+missingSites.length+incompletePay.length+openIssues.length+pendingOt.length+oldOpen.length;
  return {users,members,unlinkedUsers,unlinkedPeople,missingSites,incompletePay,openIssues,pendingOt,oldOpen,total};
}
function v195QualityRow(icon,title,count,text,action,label){return `<div class="v195-quality-row ${count?'attention':'clear'}"><span>${icon}</span><div><b>${v195Esc(title)}</b><small>${v195Esc(text)}</small></div><strong>${count||'✓'}</strong>${count?`<button class="btn light smallbtn" onclick="${action}">${v195Esc(label)} →</button>`:'<em>All clear</em>'}</div>`}
function v195DataQualityPage(){
  if(currentUser()?.role!=='Director')return smartEmpty('Director access required.','Data Quality is restricted to Directors.','','','✓');let d=v195DataQualitySummary();
  return `<div class="topbar"><div><span class="att-kicker">DIRECTOR CONTROL</span><h1>Data Quality</h1><div class="sub">One place to find incomplete setup and records that could weaken reporting.</div></div><span class="v195-quality-total ${d.total?'attention':'clear'}">${d.total?`${d.total} to check`:'All clear'}</span></div><div class="panel v195-quality-list">${v195QualityRow('♟','Clocking users without an individual link',d.unlinkedUsers.length,'Operator logins must link to an individual before clocking.','v195OpenAdminUsers()','Open users')}${v195QualityRow('⌖','Missing Site assignments',d.missingSites.length,'People and Site-scoped users need at least one current Site.','v195OpenView(\'gangs\')','Open individuals')}${v195QualityRow('£','Incomplete pay setup',d.incompletePay.length,'Active hourly, day-rate and tracked people need a valid pay basis.','v195OpenPayReview()','Open pay review')}${v195QualityRow('⚠','Open issues',d.openIssues.length,'Operational issues remain unresolved on active work.','v195OpenView(\'issues\')','Open issues')}${v195QualityRow('◷','Manual overtime awaiting decision',d.pendingOt.length,'Only deliberate requests of 15 minutes or more appear here.','contractorAttendanceTab=\'today\';v195OpenView(\'attendance\')','Review overtime')}${v195QualityRow('◷','Old shifts missing Clock Out',d.oldOpen.length,'A manager should correct shifts left open from an earlier day.','contractorAttendanceTab=\'today\';v195OpenView(\'attendance\')','Open People & Time')}</div><div class="panel v195-quality-note"><b>Information only</b><span>${d.unlinkedPeople.length} active individual${d.unlinkedPeople.length===1?' has':'s have'} no login. This is acceptable for people who do not use SiteSight; it is shown separately and is not counted as an error.</span></div>`;
}

// QS saved views and Accounts reconciliation ------------------------
function v195CommercialSavedData(){
  let unpaid=[],variations=[],retention=[];
  for(const item of db.items||[]){if(!canAccessSite(item.site))continue;let targets=item.type==='House Plot'?(item.lifts||[]):[item];targets.forEach((target,index)=>{let price=Number(target.price||0),remaining=Math.max(0,price-v191TargetSettled(target));if(price>0&&remaining>.005&&(target.status==='Complete'||item.signed))unpaid.push({item,target,index,remaining});for(const member of v183ActiveMembers()){let amount=v191MemberRetention(target,member.id);if(item.signed&&amount>.005)retention.push({item,target,index,member,amount})}});for(const variation of item.variations||[])if(!['Approved','Rejected'].includes(variation.approvalStatus||''))variations.push({item,variation})}
  let cis=v183ActiveMembers().filter(member=>member.cisRegistered),cisPayments=(db.items||[]).filter(item=>canAccessSite(item.site)).flatMap(item=>(item.type==='House Plot'?(item.lifts||[]):[item]).flatMap(target=>(target.payments||[]).filter(payment=>payment.cisRegistered).map(payment=>({item,payment}))));
  return {unpaid,variations,retention,cis,cisPayments};
}
function v195SavedMetric(title,count,value,kind){return `<div class="v195-saved-metric ${kind}"><span>${v195Esc(title)}</span><b>${count}</b><small>${value}</small></div>`}
function v195CommercialSavedViews(){
  let d=v195CommercialSavedData(),unpaidValue=d.unpaid.reduce((sum,row)=>sum+row.remaining,0),retentionValue=d.retention.reduce((sum,row)=>sum+row.amount,0),cisValue=d.cisPayments.reduce((sum,row)=>sum+Number(row.payment.cisDeduction||0),0),variationValue=d.variations.reduce((sum,row)=>sum+Number(row.variation.value||0),0);
  return `<div class="v195-commercial-role-head"><div><span>QUANTITY SURVEYOR</span><h2>Saved Commercial views</h2><p>Four repeatable checks, already filtered to Sites you can access.</p></div></div><div class="v195-saved-grid">${v195SavedMetric('Unpaid completed work',d.unpaid.length,attendanceMoney(unpaidValue),'unpaid')}${v195SavedMetric('Retention ready',d.retention.length,attendanceMoney(retentionValue),'retention')}${v195SavedMetric('CIS retained',d.cis.length+' people',attendanceMoney(cisValue),'cis')}${v195SavedMetric('Variations awaiting review',d.variations.length,attendanceMoney(variationValue),'variations')}</div>${v195SavedList('Unpaid completed work',d.unpaid,row=>`${row.item.ref} · ${row.target.name||row.item.type}`,row=>attendanceMoney(row.remaining),row=>`v195OpenItem('${row.item.id}')`)}${v195SavedList('Retention ready to release',d.retention,row=>`${row.member.name} · ${row.item.ref}`,row=>attendanceMoney(row.amount),row=>`v195OpenPerson('${row.member.id}')`)}${v195SavedList('Variations awaiting review',d.variations,row=>`${row.item.ref} · ${row.variation.name||'Variation'}`,row=>attendanceMoney(row.variation.value||0),row=>`v195OpenItem('${row.item.id}')`)}`;
}
function v195SavedList(title,rows,name,value,action){return `<details class="panel v195-saved-list" ${rows.length&&rows.length<=5?'open':''}><summary><span><b>${v195Esc(title)}</b><small>${rows.length} record${rows.length===1?'':'s'}</small></span><strong>View</strong></summary>${rows.slice(0,10).map(row=>`<button onclick="${action(row)}"><span>${v195Esc(name(row))}</span><b>${v195Esc(value(row))}</b><strong>›</strong></button>`).join('')||'<div class="muted">Nothing currently needs attention.</div>'}${rows.length>10?`<div class="muted">Showing the first 10 of ${rows.length}. Open the relevant full workspace for all records.</div>`:''}</details>`}
function v195AllPayments(){let rows=[];for(const item of db.items||[]){if(!canAccessSite(item.site))continue;for(const target of item.type==='House Plot'?(item.lifts||[]):[item])for(const payment of target.payments||[])if(!payment.amended)rows.push({item,target,payment})}return rows}
function v195AccountsReconciliation(){
  v174EnsurePayPeriod();let sites=activeSites().filter(row=>canAccessSite(row.id)).map(row=>row.id),snapshot=sites.length&&v174RangeValid(v174PayFrom,v174PayTo)?v190PaySnapshot(sites,v174PayFrom,v174PayTo):null,runs=(db.payRuns||[]).filter(run=>(run.siteIds?.length?run.siteIds:(run.snapshot?.siteIds||[])).some(id=>sites.includes(id))),approved=runs.filter(run=>['Approved','Locked','Exported'].includes(run.status)),paidRuns=runs.filter(run=>run.status==='Paid'),payments=v195AllPayments().filter(row=>v174DateInRange(row.payment.date,v174PayFrom,v174PayTo)),paid=payments.reduce((sum,row)=>sum+Number(row.payment.amount||0),0),approvedValue=approved.reduce((sum,run)=>sum+Number(run.snapshot?.totals?.netDue||0),0),paidRunValue=paidRuns.reduce((sum,run)=>sum+Number(run.snapshot?.totals?.netDue||0),0);
  return `<div class="v195-commercial-role-head accounts"><div><span>ACCOUNTS RECONCILIATION</span><h2>Due → Approved → Paid</h2><p>A read-first handover view. Operational editing is kept out of this screen.</p></div></div>${v174PeriodBar('pay',v174PayPreset,v174PayFrom,v174PayTo)}<div class="v195-reconcile-flow"><div><span>DUE NOW</span><b>${attendanceMoney(snapshot?.totals?.netDue||0)}</b><small>${snapshot?.lines?.length||0} individual lines</small></div><i>→</i><div><span>APPROVED / LOCKED</span><b>${attendanceMoney(approvedValue)}</b><small>${approved.length} pay run${approved.length===1?'':'s'}</small></div><i>→</i><div class="paid"><span>RECORDED PAID</span><b>${attendanceMoney(Math.max(paid,paidRunValue))}</b><small>${payments.length} payment record${payments.length===1?'':'s'}</small></div></div><div class="panel v195-reconcile-list"><div class="sectionhead"><div><h3>Recent recorded payments</h3><p class="muted">Most recent first for the selected period.</p></div></div>${payments.sort((a,b)=>String(b.payment.date).localeCompare(String(a.payment.date))).slice(0,10).map(row=>`<div><span><b>${v195Esc(v183PersonName(row.payment.paidTo,row.payment.payeeName||'Individual'))}</b><small>${v195Esc(row.item.ref)} · ${row.payment.date||'No date'}</small></span><strong>${attendanceMoney(row.payment.amount||0)}</strong></div>`).join('')||'<div class="muted">No payment records in this period.</div>'}</div>`;
}
let v195CommercialUserId='';
function v195CommercialPage(){
  let user=currentUser();if(!user||!v173CanOpenView('commercial'))return smartEmpty('Commercial permission required.','Ask a Director to grant commercial access.','','','£');
  if(v195CommercialUserId!==user.id){v195CommercialUserId=user.id;if(user.role==='Quantity Surveyor')v192CommercialTab='saved';else if(user.role==='Accounts')v192CommercialTab='reconcile'}
  let tabs=[hasPerm('view_pay_runs')&&['pay-run','Pay Run'],hasPerm('view_financials')&&['history','Payment History'],hasPerm('view_financials')&&['cis','CIS Review'],['Quantity Surveyor','Director','Contracts Manager'].includes(user.role)&&['saved','Saved Views'],['Accounts','Director'].includes(user.role)&&['reconcile','Reconcile']].filter(Boolean);if(!tabs.some(row=>row[0]===v192CommercialTab))v192CommercialTab=tabs[0]?.[0]||'history';
  let body=v192CommercialTab==='pay-run'?v190PayRunPage():v192CommercialTab==='cis'?v192CommercialCis():v192CommercialTab==='saved'?v195CommercialSavedViews():v192CommercialTab==='reconcile'?v195AccountsReconciliation():paymentsHistoryPage();
  return `<div class="topbar"><div><span class="att-kicker">COMMERCIAL CONTROL</span><h1>Commercial</h1><div class="sub">Pay preparation, saved reviews and reconciliation in one place.</div></div></div>${v195NextActionHtml()}<div class="v192-commercial-tabs">${tabs.map(([id,label])=>`<button class="${v192CommercialTab===id?'active':''}" onclick="v192CommercialTab='${id}';render()">${label}</button>`).join('')}</div><div class="v192-commercial-body">${body}</div>`;
}
v192CommercialPage=v195CommercialPage;

// Operator clock confidence -----------------------------------------
function v195ClockConfidence(){
  let user=currentUser(),linked=attendanceLinkedMember();if(!user||!linked)return'';let active=attendanceActiveShift(linked.member.id),siteId=active?.siteId||attendanceDefaultSiteId(),cfg=attendanceSiteConfig(siteId)||{},gps=v189GpsClockUser(user),pending=(db.offlineClockRequests||[]).filter(row=>row.workerId===linked.member.id&&row.status==='Pending').length,online=navigator.onLine!==false;
  return `<div class="v195-clock-confidence"><div class="${gps&&attendanceLocationConfigured(siteId)?'ok':'info'}"><span>${gps?'⌖':'✓'}</span><b>${gps?'GPS confirmed when you press the button':'No GPS required for this role'}</b><small>${gps?`${Number(cfg.radiusM||250)}m Site boundary · accuracy up to ±${Number(cfg.maxAccuracyM||75)}m`:'Normal system use records no hours or location.'}</small></div><div class="${online?'ok':'warn'}"><span>${online?'●':'○'}</span><b>${online?'Connected and ready':'Offline evidence mode'}</b><small>${online?'Your shift will be saved to SiteSight immediately.':'Signed evidence will wait for manager review when connection returns.'}</small></div><div class="${pending?'warn':'ok'}"><span>${pending?'↻':'✓'}</span><b>${pending?`${pending} offline request${pending===1?'':'s'} awaiting review`:'No offline requests waiting'}</b><small>${pending?'This does not change paid time until approved.':'Your latest submitted evidence is accounted for.'}</small></div></div>`;
}
const v195BaseClockPage=v175ClockPage;
v175ClockPage=function(){let html=v195BaseClockPage();if(currentUser()?.role!=='Operator')return html;return v174InsertAfterTopbar(html,v195ClockConfidence())};

// Progress photos: resize and compress before upload or offline storage.
async function v195StoreProgressPhoto(file,overrideData=''){
  let source=overrideData?dataUrlToBlob(overrideData):file,originalBytes=Number(source?.size||file?.size||0);if(!String(source?.type||file?.type||'').startsWith('image/'))return {data:await storeMediaFile(file,overrideData),originalBytes,storedBytes:originalBytes,optimised:false};
  let optimisedData=await readPhotoFile(source,1600,.78),blob=dataUrlToBlob(optimisedData),name=String(file?.name||'progress-photo.jpg').replace(/\.[^.]+$/,'.jpg'),data='';
  if(sharedMode&&navigator.onLine)try{data=await uploadBlobToServer(blob,name)}catch(error){}
  if(!data&&sharedMode){let key='m'+Date.now()+Math.random().toString(16).slice(2);await putPendingBlob(key,blob);syncState='offline';updateSyncUI();data='pending:'+key}
  if(!data)data=optimisedData;
  return {data,originalBytes,storedBytes:Number(blob.size||0),optimised:true};
}
const v195BaseAddWorkPhotos=addWorkPhotos;
addWorkPhotos=function(){v195BaseAddWorkPhotos();if(!modal)return;modal=modal.replace('Add progress photos directly to this Work Item.','Add progress photos directly to this Work Item. Images are automatically resized and compressed before upload.');render()};
saveWorkPhotos=async function(){
  if(!hasPerm('update_progress')&&!hasPerm('manage_workitems'))return toast('You do not have permission to add work photos');let item=db.items.find(row=>row.id===selected),files=uploadFiles('work-photo-files'),note=(document.getElementById('work-photo-note')?.value||'').trim();if(!item||!files.length)return toast('Choose or take at least one photo');item.workPhotos=item.workPhotos||[];let added=[];
  for(let index=0;index<files.length;index++){let file=files[index],annotated=photoAnnotations['work-photo-files']?.[index];try{let result=await v195StoreProgressPhoto(file,annotated||'');if(result.data){let photo={id:'wp'+Date.now()+Math.random().toString(16).slice(2),name:file.name||'Progress photo',data:result.data,note,created:iso(),createdAt:new Date().toISOString(),by:currentUser()?.name||'',annotated:!!annotated,optimised:result.optimised,originalBytes:result.originalBytes,storedBytes:result.storedBytes};item.workPhotos.push(photo);added.push(photo)}}catch(error){toast('A work photo could not be stored')}}
  clearCameraFiles('work-photo-files');touch(item);save();audit('photo','work item',item.id,`${item.ref}: ${added.length} optimised work photo${added.length===1?'':'s'} added`,item.site);let wasQuick=quickReturnAfterSave;quickReturnAfterSave=false;modal=null;if(wasQuick)selected=null;savedState('Work photos saved');render();
};

// Director portfolio and Read Only report handoff -------------------
function v195DirectorPortfolio(){
  let rows=v181AccessibleSites().map(v181SiteRow),active=(db.attendanceShifts||[]).filter(row=>!row.voided&&!row.clockOut?.time).length,issues=rows.reduce((sum,row)=>sum+row.openIssues,0),quality=v195DataQualitySummary().total;
  return `<section class="v195-director-portfolio"><div class="v195-director-head"><div><span>DIRECTOR PORTFOLIO</span><h2>${rows.length} active Site${rows.length===1?'':'s'} · ${active} people currently clocked in</h2><p>${issues} open issue${issues===1?'':'s'} · ${quality} data check${quality===1?'':'s'} requiring attention</p></div><button class="btn light" onclick="v195OpenView('data-quality')">✓ Data Quality</button></div><div class="v195-director-sites">${rows.map(row=>`<button onclick="setSiteContext('${row.site.id}')"><span><b>${v195Esc(row.site.name)}</b><small>${row.items.length} work items · ${row.inProgress} in progress</small></span><i><em style="width:${row.progress}%"></em></i><strong>${row.progress}% · ${row.openIssues} issue${row.openIssues===1?'':'s'} ›</strong></button>`).join('')}</div></section>`;
}
const v195BaseDashboard=dashboard;
dashboard=function(){let html=v195BaseDashboard(),user=currentUser();if(!user)return html;if(user.role==='Director'&&site==='all'){let panel=v195DirectorPortfolio();html=/<section class="v181-portfolio panel">[\s\S]*?<\/section>/.test(html)?html.replace(/<section class="v181-portfolio panel">[\s\S]*?<\/section>/,panel):v174InsertAfterTopbar(html,panel)}let next=v195NextActionHtml();if(next)html=v174InsertAfterTopbar(html,next);if(user.role==='Read Only'&&hasPerm('download_reports'))html=v174InsertAfterTopbar(html,v195ReadOnlyReports());return html};
function v195ReadOnlyReports(){let sites=activeSites().filter(row=>canAccessSite(row.id));return `<section class="v195-readonly-reports"><div><span>READ-ONLY REPORTS</span><b>Share the latest Site position</b><small>Reports contain only information this account may view and never show editing controls.</small></div><select class="select" id="v195-report-site"><option value="">Choose a Site…</option>${sites.map(row=>`<option value="${row.id}" ${site===row.id?'selected':''}>${v195Esc(row.name)}</option>`).join('')}</select><button class="btn blue" onclick="let id=document.getElementById('v195-report-site').value;if(id)adminReportOptions(id);else toast('Choose a Site')">Create report</button></section>`}

const v195BaseRender=render;
render=function(){let user=currentUser();if(user&&['my-work','data-quality'].includes(view)){if(!v173CanOpenView(view,user)){view=v173HomeView(user);return v195BaseRender()}shell(view==='my-work'?v195MyWorkPage():v195DataQualityPage());updateSyncUI();return}return v195BaseRender()};

// Current release labels --------------------------------------------
function v195Esc(value){return String(value??'').replace(/[&<>"']/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]))}
const v195BaseShell=shell;
shell=function(content){v195BaseShell(content);let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace(/v1\.94\.0 · PRODUCT REVIEW(?: DEMO)?/g,`v1.95.0 · ROLE FOCUS${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`);if(db.demoProfile==='v195-two-site-director')root.innerHTML=root.innerHTML.replace('4 active sites · 351 plots · full operational dataset','2 active sites · 99 plots · role-focused operational dataset');v194EnhanceRenderedUi(root)};
const v195BaseDashboardLabel=dashboard;
dashboard=function(){let html=v195BaseDashboardLabel();return db.demoProfile==='v195-two-site-director'?html.replace('ENTERPRISE DEMO · four active developments and 351 plots under full operational use','DIRECTOR DEMO · a smooth 29-home Site beside a complex 70-home Site'):html};
const v195BaseGuide=newUserGuidePage;
newUserGuidePage=function(){return v195BaseGuide().replace(/PRODUCT REVIEW · V1\.94/g,'ROLE FOCUS · V1.95')};

if((db.users||[]).length)migrate(false);
'use strict';

/* SiteSight V1.96 — personal-test review, reliability fixes and visual polish. */

const V196_PERMISSION_TEMPLATE=196;

function v196Migrate(){
  let changed=false;
  for(const user of db.users||[]){
    if(Number(user.permissionTemplateVersion||0)>=V196_PERMISSION_TEMPLATE)continue;
    user.permissionTemplateVersion=V196_PERMISSION_TEMPLATE;
    changed=true;
  }
  return changed;
}
const v196BaseMigrate=migrate;
migrate=function(persist=true){v196BaseMigrate(false);let changed=v196Migrate();if(changed&&persist)save();return changed};

// An individual's explicit clocking-Site list is authoritative. A linked
// account's Site permissions are only a fallback for older records without
// clockSiteIds, so broader management access cannot make a person expected at
// the wrong Site.
v194MemberAssignedToSite=function(row,siteId){
  let member=row?.member;if(!member||!siteId)return false;
  let explicit=Array.isArray(member.clockSiteIds)?member.clockSiteIds.filter(Boolean):[];
  if(explicit.length)return explicit.includes(siteId);
  let linked=v183MemberUser(member);
  if(linked&&(linked.allowedSites||[]).includes(siteId))return true;
  return (db.items||[]).some(item=>item.site===siteId&&(item.assignedId===member.id||(item.lifts||[]).some(lift=>lift.assignedId===member.id)));
};

// Notifications live under Settings. The Settings control carries a compact
// unread badge, preserving visibility without occupying the identity area.
const v196BaseShell=shell;
shell=function(content){
  v196BaseShell(content);
  let root=document.getElementById('root');if(!root)return;
  let user=currentUser(),unread=user?unreadNotifications(user).length:0,count=unread>99?'99+':String(unread);
  root.innerHTML=root.innerHTML
    .replace(/<button class="v184-notification-bell[\s\S]*?<\/button>/g,'')
    .replace(/<button class="v184-mobile-notification-bell[\s\S]*?<\/button>/g,'')
    .replace('<div class="settingswrap v184-profile-actions">','<div class="settingswrap v184-profile-actions v196-settings-only">')
    .replace(/<button class="btn logoutbtn" onclick="toggleSettings\(event\)" title="Settings">⚙<\/button>/,
      `<button class="btn logoutbtn v196-settings-button ${unread?'has-unread':''}" onclick="toggleSettings(event)" title="Settings${unread?` — ${unread} unread notification${unread===1?'':'s'}`:''}" aria-label="Settings${unread?` — ${unread} unread notification${unread===1?'':'s'}`:''}"><span aria-hidden="true">⚙</span>${unread?`<b>${count}</b>`:''}</button>`)
    .replace('<div class="settingsmenu">',`<div class="settingsmenu"><button class="v196-notification-menu-item ${unread?'has-unread':''}" onclick="notificationSettings()"><span aria-hidden="true">🔔</span><span>Notifications${unread?`<small>${unread} unread</small>`:'<small>All caught up</small>'}</span>${unread?`<b>${count}</b>`:''}</button>`)
    .replace(/v1\.95\.0 · ROLE FOCUS(?: DEMO)?/g,`v1.96.0 · PERSONAL TEST POLISH${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`);
  if(db.demoProfile==='v196-two-site-director'||db.demoProfile==='v195-two-site-director')root.innerHTML=root.innerHTML
    .replace('4 active sites · 351 plots · full operational dataset','2 active sites · 99 plots · polished personal-test dataset')
    .replace('2 active sites · 99 plots · role-focused operational dataset','2 active sites · 99 plots · polished personal-test dataset');
  v194EnhanceRenderedUi(root);
};

const v196BaseGuide=newUserGuidePage;
newUserGuidePage=function(){return v196BaseGuide().replace(/ROLE FOCUS · V1\.95/g,'PERSONAL TEST POLISH · V1.96')};
const v196BaseLoginScreen=loginScreen;
loginScreen=function(){
  v196BaseLoginScreen();if(db.demoProfile!=='v196-two-site-director')return;
  let root=document.getElementById('root');if(!root)return;
  root.innerHTML=root.innerHTML
    .replace('Four live developments. One business view.','Two contrasting developments. One clear business view.')
    .replace(/351 plots, 62 people and realistic operational records — ready to demonstrate SiteSight at contractor scale\./g,'99 plots, 35 individuals and complete personal-test records across two contrasting Sites.')
    .replace(/ENTERPRISE DEMO · 4 ACTIVE SITES/g,'PERSONAL TEST DEMO · 2 ACTIVE SITES')
    .replace(/25, 50, 96 and 180-home developments/g,'29-home smooth Site · 70-home complex Site');
};

if((db.users||[]).length)migrate(false);
'use strict';

/* SiteSight V1.97 — compact desktop review, reliable take-off saving and
   collapsible Pay Review sections. */

const V197_PERMISSION_TEMPLATE=197;

function v197Migrate(){
  let changed=false;
  for(const user of db.users||[]){
    if(Number(user.permissionTemplateVersion||0)>=V197_PERMISSION_TEMPLATE)continue;
    user.permissionTemplateVersion=V197_PERMISSION_TEMPLATE;
    changed=true;
  }
  return changed;
}
const v197BaseMigrate=migrate;
migrate=function(persist=true){v197BaseMigrate(false);let changed=v197Migrate();if(changed&&persist)save();return changed};

// Save a drawing take-off in one current handler. This avoids the former chain
// of save wrappers and assigns the PDF page before the first persistence call.
let v197TakeoffSaving=false;
saveTakeoffMeasurement=function(){
  if(v197TakeoffSaving)return false;
  if(!v184CanMeasureDrawing())return toast('You do not have drawing measurement access.'),false;
  let drawing=takeoffDrawing(),pending=takeoffPending,label=(document.getElementById('takeoff-label')?.value||'').trim();
  if(!drawing||!pending)return toast('Finish a measurement before saving it.'),false;
  if(!label)return toast('Enter a description for this measurement.'),false;
  let quantity=Number(pending.quantity||0);
  if(!Number.isFinite(quantity)||(pending.kind!=='Count'&&quantity<=0))return toast('This measurement has no valid quantity. Clear it and measure again.'),false;
  let rate=hasPerm('view_financials')?Number(document.getElementById('takeoff-rate')?.value||0):0;
  if(!Number.isFinite(rate)||rate<0)return toast('Enter a valid rate, or leave it blank.'),false;
  v197TakeoffSaving=true;
  try{
    let entry={id:`tk${Date.now()}${Math.random().toString(16).slice(2)}`,kind:pending.kind,label,points:JSON.parse(JSON.stringify(pending.points||[])),quantity,unit:pending.unit,rate,createdAt:new Date().toISOString(),by:currentUser()?.name||'',userId:currentUser()?.id||'',calibrationDistanceM:drawing.takeoffCalibration?.distanceM||0};
    if(v184PdfActive())entry.pdfPage=Number(v184PdfPage||1);
    drawing.takeoffs=Array.isArray(drawing.takeoffs)?drawing.takeoffs:[];
    drawing.takeoffs.push(entry);
    if(v184PdfActive())v184StorePdfCalibration();
    let entity=takeoffAuditEntity();
    save();
    audit('drawing','drawing take-off',drawing.id||entity.id,`${entity.label}: ${entry.kind} saved — ${entry.label} · ${takeoffFmtQty(entry)}${hasPerm('view_financials')&&rate?` · ${money(takeoffValue(entry))}`:''}`,takeoffSiteId());
    takeoffPending=null;takeoffDraft=[];takeoffPointer=null;takeoffTool='select';
    renderTakeoffWorkspace();savedState('Take-off measurement saved');
    return true;
  }finally{v197TakeoffSaving=false}
};

const v197BaseTakeoffPendingHtml=takeoffPendingHtml;
takeoffPendingHtml=function(){
  return v197BaseTakeoffPendingHtml().replace('onclick="saveTakeoffMeasurement()"','type="button" onclick="this.disabled=true;saveTakeoffMeasurement();if(takeoffPending)this.disabled=false"');
};

let v197PaySections={individual:false,setPrice:false,time:false,recorded:false};
function v197SetPaySection(key,open){if(Object.hasOwn(v197PaySections,key))v197PaySections[key]=!!open}
function v197PaySection(key,title,inner,rowClass){
  let count=(String(inner).match(new RegExp(`class="${rowClass}`, 'g'))||[]).length,open=v197PaySections[key];
  return `<details class="v197-pay-section" ${open?'open':''} ontoggle="v197SetPaySection('${key}',this.open)"><summary><span><b>${title}</b><small>${count} matching record${count===1?'':'s'}</small></span><strong>${open?'Close':'Open'} section</strong></summary><div class="v197-pay-section-body">${inner}</div></details>`;
}

const v197BaseRecordedPaymentsPanel=v187RecordedPaymentsPanel;
v187RecordedPaymentsPanel=function(...args){
  let html=v197BaseRecordedPaymentsPanel(...args),inner=html.replace(/^<section class="v174-recorded-payments">/,'').replace(/<\/section>$/,'');
  return v197PaySection('recorded','Payments recorded in this period',inner,'v174-pay-person');
};

const v197BaseContractorPayReview=contractorPayReview;
contractorPayReview=function(...args){
  let html=v197BaseContractorPayReview(...args);
  html=html.replace(/<div class="v174-individual-breakdown">([\s\S]*?)<\/div><div class="pay-review-grid">/,(_,inner)=>v197PaySection('individual','Individual / payee breakdown',`<div class="v174-individual-breakdown">${inner}</div>`,'v174-pay-person')+'<div class="pay-review-grid">');
  html=html.replace(/<div class="pay-review-grid"><section><h3>Set Price — completed in period<\/h3>([\s\S]*?)<\/section><section><h3>Hourly \/ Day Rate<\/h3>([\s\S]*?)<\/section><\/div>/,(_,setRows,timeRows)=>`<div class="pay-review-grid">${v197PaySection('setPrice','Set Price — completed in period',setRows,'pay-review-row')}${v197PaySection('time','Hourly / Day Rate',timeRows,'pay-review-row')}</div>`);
  return html;
};

const v197BaseApplyPaySearch=v191ApplyPaySearch;
v191ApplyPaySearch=function(){
  v197BaseApplyPaySearch();
  let value=String(document.getElementById('v191-pay-review-search')?.value||'').trim();
  if(value){Object.keys(v197PaySections).forEach(key=>v197PaySections[key]=true);document.querySelectorAll('.v197-pay-section').forEach(section=>section.open=true)}
};

const v197BaseShell=shell;
shell=function(content){
  v197BaseShell(content);
  let root=document.getElementById('root');if(!root)return;
  root.innerHTML=root.innerHTML.replace(/v1\.96\.0 · PERSONAL TEST POLISH(?: DEMO)?/g,`v1.97.0 · DESKTOP CLARITY${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`);
};

const v197BaseGuide=newUserGuidePage;
newUserGuidePage=function(){return v197BaseGuide().replace(/PERSONAL TEST POLISH · V1\.96/g,'DESKTOP CLARITY · V1.97')};

if((db.users||[]).length)migrate(false);
'use strict';

/* SiteSight V1.99 — exact action routing, retention clarity and guided use. */

const V199_PERMISSION_TEMPLATE=199;

function v199Migrate(){
  let changed=false;
  for(const user of db.users||[]){
    if(Number(user.permissionTemplateVersion||0)<V199_PERMISSION_TEMPLATE){user.permissionTemplateVersion=V199_PERMISSION_TEMPLATE;changed=true}
    if(user.trainingPreferenceInitialized===undefined){
      user.trainingPreferenceInitialized=Number(user.loginCount||0)>0;
      if(!user.trainingPreferenceInitialized&&user.trainingMode!==true)user.trainingMode=true;
      changed=true;
    }
  }
  for(const item of db.items||[]){
    let targets=item.type==='House Plot'?(item.lifts||[]):[item];
    for(const target of targets){
      if(!target||!Number(target.price||0)||!(target.payments||[]).length)continue;
      let percent=Math.min(100,Math.round(v191TargetGrossClaims(target)/Number(target.price)*100));
      if(Number(target.pct||0)!==percent){target.pct=percent;changed=true}
      let state=percent>=100?'Paid':percent>0?'Part Paid':target.paymentState;
      if(state&&target.paymentState!==state){target.paymentState=state;changed=true}
    }
  }
  return changed;
}
const v199BaseMigrate=migrate;
migrate=function(persist=true){v199BaseMigrate(false);let changed=v199Migrate();if(changed&&persist)save();return changed};

// ---------------------------------------------------------------------
// Retention is a separate liability, not an unpaid original-price balance.

function v199AllWorkComplete(item){
  if(!item)return false;
  if(item.type==='House Plot')return !!(item.lifts||[]).length&&(item.lifts||[]).every(lift=>lift.status==='Complete');
  return item.status==='Complete';
}
function v199RetentionEligible(item){return !!item?.signed&&v199AllWorkComplete(item)}
function v199OriginalPriceComplete(item){
  if(!item)return true;
  let targets=item.type==='House Plot'?(item.lifts||[]):[item],original=targets.every(target=>Number(target.price||0)<=0||v191RemainingPrice(target)<=.005),variations=typeof itemVariationRemaining==='function'?itemVariationRemaining(item)<=.005:true;
  return original&&variations;
}
function v199ItemRetentionHeld(item){
  if(!item)return 0;
  let total=0,targets=item.type==='House Plot'?(item.lifts||[]):[item];
  for(const target of targets){
    let ids=new Set(v190Allocations(target).map(row=>row.memberId));
    for(const payment of target.payments||[])if(payment.paidTo)ids.add(payment.paidTo);
    for(const memberId of ids)total+=v191MemberRetention(target,memberId);
  }
  return total;
}

// The gross invoice allocation closes the original set-price balance. Any
// retention still held remains visible in the separate retention ledger.
stagePaidAmount=function(target){return v191TargetGrossClaims(target)};
simplePaidAmount=function(target){return v191TargetGrossClaims(target)};
v188ItemFullyPaid=v199OriginalPriceComplete;

v191RetentionRecords=function(memberId){
  let rows=[];
  for(const item of db.items||[]){
    if(!canAccessSite(item.site))continue;
    let targets=item.type==='House Plot'?(item.lifts||[]):[item];
    targets.forEach((target,index)=>{let amount=v191MemberRetention(target,memberId);if(amount>.004)rows.push({item,target,index,targetKind:item.type==='House Plot'?'lift':'item',amount,eligible:v199RetentionEligible(item)})});
  }
  return rows.sort((a,b)=>Number(b.eligible)-Number(a.eligible)||String(a.item.ref||'').localeCompare(String(b.item.ref||''),undefined,{numeric:true}));
};
function v199OpenNextRetention(memberId){
  let row=v191RetentionRecords(memberId).find(entry=>entry.eligible);
  if(!row)return toast('No retention is ready to release for this individual');
  return v191OpenRetentionRelease(memberId,row.item.id,row.targetKind,row.index);
}
const v199BaseOpenRetentionRelease=v191OpenRetentionRelease;
v191OpenRetentionRelease=function(memberId,itemId,targetKind,index){let item=db.items.find(row=>row.id===itemId);if(!v199RetentionEligible(item))return toast(roleCanAccessPlots()?'Retention is released only after every stage is complete and the plot or work item is signed off':'This retention is not available to release');return v199BaseOpenRetentionRelease(memberId,itemId,targetKind,index)};
const v199BaseSaveRetentionRelease=v191SaveRetentionRelease;
v191SaveRetentionRelease=function(memberId,itemId,targetKind,index){let item=db.items.find(row=>row.id===itemId);if(!v199RetentionEligible(item))return toast('This retention is not ready to release');return v199BaseSaveRetentionRelease(memberId,itemId,targetKind,index)};
v191IndividualRetentionPanel=function(member){
  if(!hasPerm('view_pay_runs')&&!hasPerm('view_financials'))return'';
  let rows=v191RetentionRecords(member.id),total=rows.reduce((sum,row)=>sum+row.amount,0),readyRows=rows.filter(row=>row.eligible),available=readyRows.reduce((sum,row)=>sum+row.amount,0),canPay=hasPerm('record_payments')||hasPerm('edit_pay_records'),operational=roleCanAccessPlots();
  return `<div class="v191-individual-retention"><div class="v199-retention-summary"><div><span>RETENTION HELD</span><b>${attendanceMoney(total)}</b><small>${operational?'Outstanding across all work':'Outstanding financial retention'}</small></div><div class="${available?'ready':'clear'}"><span>READY TO RELEASE</span><b>${attendanceMoney(available)}</b><small>${available?(operational?'All work complete and signed off':'Available from authorised records'):'Nothing ready at present'}</small></div>${available&&canPay?`<button class="btn green" onclick="v199OpenNextRetention('${member.id}')">Pay retention ready for release →</button>`:''}</div>${rows.map(row=>`<div class="v191-retention-row"><span><b>${operational?`${v190Esc(row.item.ref)} · ${v190Esc(row.target.name||row.item.type)}`:'Financial retention record'}</b><small>${v190Esc(siteName(row.item.site))} · ${operational?(row.eligible?'All stages complete and signed off — ready':'Held until all work is complete and signed off'):(row.eligible?'Available to release':'Currently held')}</small></span><strong>${attendanceMoney(row.amount)}</strong>${row.eligible&&canPay?`<button class="btn green smallbtn" onclick="v191OpenRetentionRelease('${member.id}','${row.item.id}','${row.targetKind}',${row.index})">Pay this retention</button>`:''}</div>`).join('')||'<div class="muted">No retention is currently held for this individual.</div>'}</div>`;
};

userAttentionText=function(item){
  let parts=[],openCount=issues(item),overdue=itemOverdueIssues(item).length;
  if(hasPerm('view_issues')&&overdue)parts.push(`${overdue} overdue issue${overdue===1?'':'s'}`);else if(hasPerm('view_issues')&&openCount)parts.push(`${openCount} open issue${openCount===1?'':'s'}`);
  if((hasPerm('manage_workitems')||hasPerm('manage_gangs'))&&!item.assignmentNotRequired&&!item.assignedId)parts.push('Individual to be confirmed');
  if(hasPerm('sign_off')&&overall(item)==='Complete'&&!item.signed)parts.push('Ready for sign-off');
  if(hasPerm('view_financials')){
    let targets=item.type==='House Plot'?(item.lifts||[]):[item],unallocated=targets.filter(target=>target.status==='Complete'&&Number(target.price||0)>0&&v191RemainingPrice(target)>.005);
    if(unallocated.length)parts.push(item.type==='House Plot'?`${unallocated.length} completed lift${unallocated.length===1?'':'s'} still need invoice amounts`:'Completed work still needs its invoice amount');
    let retention=v199RetentionEligible(item)?v199ItemRetentionHeld(item):0;if(retention>.005)parts.push(`Retention ready for release ${attendanceMoney(retention)}`);
  }
  if(hasPerm('approve_variations')&&(item.variations||[]).some(variation=>(variation.approvalStatus||'Approved')!=='Approved'))parts.push('Variation awaiting approval');
  return parts.join(' · ');
};

v195CommercialSavedData=function(){
  let unpaid=[],variations=[],retention=[];
  for(const item of db.items||[]){
    if(!canAccessSite(item.site))continue;
    let targets=item.type==='House Plot'?(item.lifts||[]):[item];
    targets.forEach((target,index)=>{
      let price=Number(target.price||0),remaining=v191RemainingPrice(target);
      if(price>0&&remaining>.005&&(target.status==='Complete'||item.signed))unpaid.push({item,target,index,remaining});
      if(v199RetentionEligible(item))for(const member of v183ActiveMembers()){let amount=v191MemberRetention(target,member.id);if(amount>.005)retention.push({item,target,index,member,amount})}
    });
    for(const variation of item.variations||[])if(!['Approved','Rejected'].includes(variation.approvalStatus||''))variations.push({item,variation});
  }
  let cis=v183ActiveMembers().filter(member=>member.cisRegistered),cisPayments=(db.items||[]).filter(item=>canAccessSite(item.site)).flatMap(item=>(item.type==='House Plot'?(item.lifts||[]):[item]).flatMap(target=>(target.payments||[]).filter(payment=>payment.cisRegistered).map(payment=>({item,payment}))));
  return {unpaid,variations,retention,cis,cisPayments};
};

// ---------------------------------------------------------------------
// Exact record routing for Data Quality, Dashboard and Commercial.

function v199OpenUserIssue(userId){adminTab='users';view='admin';selected=null;selectedGang=null;render();setTimeout(()=>editUserAccess(userId),0)}
function v199OpenPersonIssue(memberId,editor=false){view='gangs';selected=null;selectedGang=null;render();setTimeout(()=>editor?v176OpenWorkforceEditor(V183_INDIVIDUALS_ID,memberId):openTeamMember(V183_INDIVIDUALS_ID,memberId),0)}
function v199OpenWorkTarget(itemId,targetKind='item',targetIndex=-1){let item=db.items.find(row=>row.id===itemId);if(!item)return toast('This Work Item no longer exists');if(!canViewItem(item))return toast('Plot cards are available to Supervisors and above');site=item.site;view='work-items';selected=itemId;selectedGang=null;openLift=targetKind==='lift'?Number(targetIndex):-1;render();setTimeout(()=>document.querySelector?.('.lift.open,.simple-card.open,.detail')?.scrollIntoView?.({block:'start',behavior:'smooth'}),40)}
function v199OpenOldShift(shiftId){if(!v1998CanReviewTimeRecords())return toast('A Director must grant this account shift-review permission');let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId);if(!shift)return toast('This shift no longer exists');site=shift.siteId;contractorAttendanceTab='timesheets';attendanceWorker=shift.workerId;view='attendance';selected=null;render();let week=attendanceWeekStartISO(new Date(shift.clockIn.time));setTimeout(()=>hasPerm('manage_attendance')?editAttendanceShift(shiftId):openAttendanceWorkerWeek(shift.workerId,shift.siteId,week),0)}
function v199OpenOvertimeException(shiftId){if(!v1998CanReviewTimeRecords())return toast('A Director must grant this account shift-review permission');let shift=(db.attendanceShifts||[]).find(row=>row.id===shiftId);if(!shift)return toast('This overtime request no longer exists');if(v175CanApproveOvertime())return v174OpenOvertimeApproval(shiftId);return v193OpenTimesheetRecord(shift.workerId,shift.siteId,attendanceWeekStartISO(new Date(shift.clockIn.time)))}

function v199QualityActionRow(title,detail,action,label='Review and fix'){
  return `<button class="v199-action-row" onclick="${action}"><span><b>${v195Esc(title)}</b><small>${v195Esc(detail)}</small></span><strong>${v195Esc(label)} →</strong></button>`;
}
function v199QualityGroup(icon,title,text,rows){
  return `<details class="v199-quality-group ${rows.length?'attention':'clear'}" ${rows.length&&rows.length<=4?'open':''}><summary><span class="v199-quality-icon">${icon}</span><span><b>${v195Esc(title)}</b><small>${v195Esc(text)}</small></span><strong>${rows.length||'✓'}</strong><em>${rows.length?'Open list':'All clear'}</em></summary>${rows.length?`<div class="v199-quality-actions">${rows.join('')}</div>`:'<div class="v199-quality-clear">No action is required.</div>'}</details>`;
}
v195DataQualityPage=function(){
  if(currentUser()?.role!=='Director')return smartEmpty('Director access required.','Data Quality is restricted to Directors.','','','✓');
  let d=v195DataQualitySummary(),groups=[];
  groups.push(v199QualityGroup('♙','Clocking users without an individual link','Open the exact user-access record and link the correct individual.',d.unlinkedUsers.map(user=>v199QualityActionRow(user.name,`${user.role} · @${user.username||''}`,`v199OpenUserIssue('${user.id}')`))));
  groups.push(v199QualityGroup('⌖','Missing Site assignments','Each button opens the exact user or individual that needs Sites.',d.missingSites.map(entry=>entry.kind==='user'?v199QualityActionRow(entry.row.name,`${entry.row.role} login has no Site access`,`v199OpenUserIssue('${entry.row.id}')`):v199QualityActionRow(entry.row.name,`${entry.row.role||'Individual'} has no working Site`,`v199OpenPersonIssue('${entry.row.id}')`))));
  groups.push(v199QualityGroup('£','Incomplete pay setup','Open the exact pay editor instead of searching Pay Review.',d.incompletePay.map(member=>v199QualityActionRow(member.name,`${member.employmentType||'Not tracked'} · pay basis needs completing`,`v199OpenPersonIssue('${member.id}',true)`,'Edit pay setup'))));
  groups.push(v199QualityGroup('⚠','Open issues','Review and resolve the exact issue on its Work Item.',d.openIssues.map(row=>v199QualityActionRow(`${row.item.ref} · ${row.issue.title}`,`${siteName(row.item.site)} · ${row.issue.priority||'Normal'} priority`,`viewIssue('${row.item.id}','${row.issue.id}')`,'Open issue'))));
  groups.push(v199QualityGroup('◷','Manual overtime awaiting decision','Only deliberate requests of at least 15 minutes are included.',d.pendingOt.map(row=>v199QualityActionRow(`${row.wm.member.name} · ${row.request.minutes} minutes`,`${siteName(row.shift.siteId)} · ${attendanceDateOnly(row.shift.clockIn.time,row.shift.siteId)}`,`v174OpenOvertimeApproval('${row.shift.id}')`,'Decide overtime'))));
  groups.push(v199QualityGroup('◷','Old shifts missing Clock Out','Open the exact correction form for each old shift.',d.oldOpen.map(shift=>v199QualityActionRow(v183PersonName(shift.workerId),`${siteName(shift.siteId)} · open since ${attendanceDateOnly(shift.clockIn.time,shift.siteId)}`,`v199OpenOldShift('${shift.id}')`,'Correct shift'))));
  return `<div class="topbar"><div><span class="att-kicker">DIRECTOR CONTROL</span><h1>Data Quality</h1><div class="sub">Every result below opens the exact record that needs review.</div></div><span class="v195-quality-total ${d.total?'attention':'clear'}">${d.total?`${d.total} to check`:'All clear'}</span></div><div class="panel v199-quality-list">${groups.join('')}</div><div class="panel v195-quality-note"><b>Information only</b><span>${d.unlinkedPeople.length} active individual${d.unlinkedPeople.length===1?' has':'s have'} no login. That is acceptable for people who do not use SiteSight and is not counted as an error.</span></div>`;
};

function v199DashboardExceptionRows(){
  let rows=[],siteId=site==='all'?'all':site,today=iso(),match=id=>siteId==='all'||id===siteId;
  if(v175CanApproveOvertime())for(const row of v193ManualOvertimeRows(siteId))rows.push({kind:'overtime',title:`${row.wm.member.name}: overtime request`,detail:`${row.request.minutes} minutes · ${attendanceDateOnly(row.shift.clockIn.time,row.shift.siteId)} · ${siteName(row.shift.siteId)}`,action:`v174OpenOvertimeApproval('${row.shift.id}')`});
  if(v175CanCorrectTime())for(const shift of (db.attendanceShifts||[]).filter(row=>match(row.siteId)&&!row.voided&&!row.clockOut?.time&&attendanceDateOnly(row.clockIn?.time,row.siteId)<today).sort((a,b)=>String(a.clockIn.time).localeCompare(String(b.clockIn.time))))rows.push({kind:'clock',title:`${v183PersonName(shift.workerId)}: missing Clock Out`,detail:`Open since ${attendanceDateOnly(shift.clockIn.time,shift.siteId)} · ${siteName(shift.siteId)}`,action:`v199OpenOldShift('${shift.id}')`});
  if(hasPerm('approve_timesheets'))for(const approval of (db.timesheetApprovals||[]).filter(row=>!row.superseded&&row.needsReapproval&&match(row.siteId)))rows.push({kind:'timesheet',title:`${contractorMemberName(approval.workerId)}: timesheet changed`,detail:`Week ${approval.weekStart} · approval required again`,action:`v193OpenTimesheetRecord('${approval.workerId}','${approval.siteId}','${approval.weekStart}')`});
  if(hasPerm('view_issues'))for(const item of (db.items||[]).filter(row=>match(row.site)&&canAccessSite(row.site)))for(const issue of (item.issues||[]).filter(row=>row.status==='Open'&&row.targetDate&&row.targetDate<today))rows.push({kind:'issue',title:`${item.ref}: ${issue.title}`,detail:`Overdue since ${issue.targetDate} · ${siteName(item.site)}`,action:`viewIssue('${item.id}','${issue.id}')`});
  if(hasPerm('manage_gangs')||hasPerm('gangs')){let soon=v174DateAdd(today,30);for(const person of attendanceAllMembers()){if(siteId!=='all'&&!v194MemberAssignedToSite(person,siteId))continue;for(const qualification of (person.member.qualifications||[]).filter(row=>row.expiry&&row.expiry>=today&&row.expiry<=soon))rows.push({kind:'qualification',title:`${person.member.name}: ${qualification.name||qualification.type||'Qualification'}`,detail:`Expires ${qualification.expiry}`,action:`v178OpenQualificationPerson('${person.member.id}')`})}}
  if(hasPerm('lock_payroll'))for(const lock of (db.payrollLocks||[]).filter(row=>!row.superseded&&!row.unlockedAt&&match(row.siteId)&&v175PayrollSnapshot(row.siteId,row.from,row.to).hash!==row.snapshotHash))rows.push({kind:'payroll',title:`${siteName(lock.siteId)}: locked pay changed`,detail:`${lock.from} to ${lock.to} · review and re-lock`,action:`v193OpenPayLock('${lock.siteId}','${lock.from}','${lock.to}')`});
  return rows;
}
v175ExceptionRows=v199DashboardExceptionRows;
v175ExceptionsPanel=function(){
  if(!v175CanSeeExceptions())return'';let rows=v199DashboardExceptionRows(),visible=rows.slice(0,8),rest=rows.slice(8),buttons=list=>list.map(row=>`<button class="${row.kind}" onclick="${row.action}"><i>1</i><span><b>${v195Esc(row.title)}</b><small>${v195Esc(row.detail)}</small></span><strong>Review exact record →</strong></button>`).join('');
  return `<div class="panel v175-exceptions"><div class="sectionhead"><div><span class="att-kicker">TODAY'S ACTIONS</span><h2>Exceptions to check</h2><div class="muted">Select one item to open its exact review or correction action.</div></div><span class="v175-exception-total ${rows.length?'attention':'clear'}">${rows.length||'✓'} ${rows.length?'to check':'All clear'}</span></div><div class="v175-exception-grid v199-exception-grid">${buttons(visible)}${rest.length?`<details><summary>Show ${rest.length} more exact exception${rest.length===1?'':'s'}</summary>${buttons(rest)}</details>`:''}${!rows.length?'<div class="v175-all-clear"><b>No exceptions need your attention.</b><span>Normal work stays out of this queue so it remains easy to scan.</span></div>':''}</div></div>`;
};

v190PayExceptions=function(siteIds,from,to){
  let rows=[],siteMatch=id=>siteIds.includes(id),workers=new Set(),canReviewTime=v1998CanReviewTimeRecords();
  for(const shift of db.attendanceShifts||[]){
    if(!siteMatch(shift.siteId)||shift.voided||!v174DateInRange(attendanceDateOnly(shift.clockIn?.time,shift.siteId),from,to))continue;
    workers.add(shift.workerId);
    if(canReviewTime&&!shift.clockOut?.time)rows.push({type:'Open shift',person:v183PersonName(shift.workerId),detail:`${siteName(shift.siteId)} · ${attendanceDateOnly(shift.clockIn.time,shift.siteId)}`,route:'shift',shiftId:shift.id,workerId:shift.workerId,siteId:shift.siteId});
    let approval=shift.overtimeApproval||{};if(canReviewTime&&approval.manualRequest===true&&approval.status==='Pending'&&Number(approval.minutes||0)>=V193_MINIMUM_OVERTIME_MINUTES)rows.push({type:'Overtime decision',person:v183PersonName(shift.workerId),detail:`${approval.minutes||0} minutes awaiting decision`,route:'overtime',shiftId:shift.id,workerId:shift.workerId,siteId:shift.siteId});
  }
  for(const workerId of workers){let member=v183Member(workerId),payType=member?.employmentType,missing=payType==='Hourly'&&Number(member?.hourlyRate||member?.basePay||0)<=0||payType==='Day Rate'&&Number(member?.dayRate||0)<=0;if(missing)rows.push({type:'Missing pay rate',person:member?.name||'Unknown person',detail:`Complete this ${payType} pay setup`,route:'person-pay',workerId})}
  if(canReviewTime)for(const siteId of siteIds)for(let week=attendanceWeekStartISO(from);week<=to;week=v174DateAdd(week,7))for(const workerId of workers){if(!v174ShiftsForRange(workerId,week,v174DateAdd(week,6),siteId).length)continue;let approval=attendanceApproval(workerId,siteId,week);if(!approval)rows.push({type:'Timesheet approval',person:v183PersonName(workerId),detail:`Week ${week} has not been approved`,route:'timesheet',workerId,siteId,week});else if(approval.needsReapproval)rows.push({type:'Timesheet changed',person:v183PersonName(workerId),detail:`Week ${week} needs approval again`,route:'timesheet',workerId,siteId,week})}
  for(const item of db.items||[]){
    if(!siteMatch(item.site))continue;
    let targets=item.type==='House Plot'?(item.lifts||[]):[item];
    targets.forEach((target,index)=>{let date=target.completedDate;if(target.status!=='Complete'||!v174DateInRange(date,from,to)||!Number(target.price||0))return;let allocations=v190Allocations(target),targetKind=item.type==='House Plot'?'lift':'item',name=target.name||item.type;if(!allocations.length)rows.push({type:'Pricework people',person:item.ref,detail:`${name} needs named people`,route:'work-target',itemId:item.id,targetKind,targetIndex:index});let remaining=v191RemainingPrice(target);if(allocations.length&&remaining>.005)rows.push({type:'Pricework amount',person:item.ref,detail:`${name} still has ${attendanceMoney(remaining)} to allocate from invoices`,route:'work-target',itemId:item.id,targetKind,targetIndex:index})});
  }
  return rows;
};
v191FixPayException=function(index){
  let row=v191CurrentExceptions[Number(index)];if(!row)return toast('This exception is no longer in the current pay review');
  if(['shift','overtime','timesheet'].includes(row.route)&&!v1998CanReviewTimeRecords())return toast('A Director must grant this account shift-review permission');
  if(row.route==='shift')return v199OpenOldShift(row.shiftId);
  if(row.route==='overtime')return v199OpenOvertimeException(row.shiftId);
  if(row.route==='timesheet')return v193OpenTimesheetRecord(row.workerId,row.siteId,row.week);
  if(row.route==='person-pay')return v199OpenPersonIssue(row.workerId,true);
  if(row.route==='work-target')return v199OpenWorkTarget(row.itemId,row.targetKind,row.targetIndex);
  return toast('The related record is no longer available');
};

// ---------------------------------------------------------------------
// Clearer live workforce, assigned-person status and calendar previews.

side=function(){
  let scoped=scopeItems(),recent=hasPerm('view_issues')?scoped.flatMap(item=>(item.issues||[]).map(issue=>({...issue,ref:item.ref,site:item.site,itemId:item.id}))).filter(issue=>issue.status==='Open').slice(0,4):[],counts=new Map();
  scoped.forEach(item=>{if(item.assignedId)counts.set(item.assignedId,(counts.get(item.assignedId)||0)+1)});
  let people=[...counts].map(([id,count])=>({member:v183Member(id),count})).filter(row=>row.member).sort((a,b)=>b.count-a.count||a.member.name.localeCompare(b.member.name)).slice(0,8);
  return `<div class="sidecol">${hasPerm('view_issues')?`<div class="panel sidepanel"><div class="sectionhead"><h3>Recent Issues</h3><button class="viewall" onclick="view='issues';selected=null;render()">View all</button></div><div class="list">${recent.map(issue=>`<button class="listrow clickable-listrow" onclick="viewIssue('${issue.itemId}','${issue.id}')"><span><b>${v183Esc(issue.title)}</b><small>${v183Esc(issue.ref)} · ${v183Esc(siteName(issue.site))}</small></span><span class="badge b-red">1</span></button>`).join('')||'<div class="empty-help"><b>No open issues.</b></div>'}</div></div>`:''}<div class="panel sidepanel"><div class="sectionhead"><h3>Assigned Individuals</h3><button class="viewall" onclick="view='gangs';selected=null;render()">View all</button></div><div class="v199-dot-key"><span><i class="on"></i>Clocked in</span><span><i class="off"></i>Not clocked in</span></div><div class="list">${people.map(row=>{let active=!!attendanceActiveShift(row.member.id);return `<button class="listrow v199-person-status" onclick="openTeamMember('${V183_INDIVIDUALS_ID}','${row.member.id}')"><i class="v199-status-dot ${active?'on':'off'}" title="${active?'Clocked in':'Not clocked in'}"></i><span><b>${v183Esc(row.member.name)}</b><small>${v183Esc(row.member.role||'Individual')} · ${active?'Clocked in':'Not clocked in'}</small></span><span class="badge b-blue">${row.count}</span></button>`}).join('')||'<div class="empty-help"><b>No assigned individuals.</b></div>'}</div></div></div>`;
};

const v199BaseAttendanceTodayView=attendanceTodayView;
attendanceTodayView=function(siteId){
  let html=v199BaseAttendanceTodayView(siteId);
  html=html.replace('Who is on site now','Clocked in now').replace(/Live workforce register ·/g,'People with an open Clock In record ·').replace('Expected workforce','Expected today — not clocked in').replace('People not currently clocked in','People scheduled here today who do not currently have an open Clock In record');
  html=html.replace(/<button class="btn light smallbtn" onclick="supervisorAttendanceClock\('([^']+)','out','([^']+)'\)">Supervisor Clock Out<\/button>/g,`<button class="btn light smallbtn v199-supervisor-out" title="Use this only when the person has finished but cannot clock out themselves" onclick="supervisorAttendanceClock('$1','out','$2')">Clock out for this person</button>`);
  let help='<div class="v199-clockout-help"><span>ⓘ</span><div><b>When should I clock someone out?</b><p>Use <strong>Clock out for this person</strong> only when they have finished work but cannot use their own phone. Your name and the time are recorded. To correct an old or incorrect shift, open that person’s Timesheet and use <strong>Correction</strong>.</p></div></div>';
  return html.replace('<div class="att-live-list">',help+'<div class="att-live-list">');
};

calendarHoverNotes=function(date){
  let events=calendarDayEvents(date);if(!events.length)return'';
  let label=new Date(date+'T12:00:00').toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long'}),names={issue:'Issue due',note:'Calendar note',complete:'Work complete',diary:'Site Diary',holiday:'Holiday',training:'Training',sick:'Sickness',leave:'Leave',programme:'Programme'};
  return `<div class="calendar-hover-notes v199-calendar-hover" role="tooltip"><div class="calendar-hover-head"><b>${label}</b><span>${events.length} item${events.length===1?'':'s'}</span></div>${events.map(event=>`<div class="calendar-hover-note event-${event.kind}"><em>${v195Esc(names[event.kind]||'Site activity')}</em><b>${calendarSafe(event.title)}</b><span>${calendarSafe(event.sub||event.notes||'')}</span></div>`).join('')}</div>`;
};
const v199BaseFullCalendarMonth=fullCalendarMonth;
fullCalendarMonth=function(){return v199BaseFullCalendarMonth().replace(/Calendar notes appear directly on their date\. Hover a highlighted date to read every note, or click it for the full day view\./,'The number on each date shows how many items are recorded. Hover any date to see the complete list, or click it for the full day view.')};

// Archive and restore are deliberate, confirmed actions.
const v199BaseArchiveSite=archiveSite;
archiveSite=function(id){let row=db.sites.find(siteRow=>siteRow.id===id);if(!row||row.archived)return;if(!confirm(`Archive “${row.name}”?\n\nIts history will be retained and the Site can be restored later.`))return false;return v199BaseArchiveSite(id)};
const v199BaseUnarchiveSite=unarchiveSite;
unarchiveSite=function(id){let row=db.sites.find(siteRow=>siteRow.id===id);if(!row)return;if(!confirm(`Restore “${row.name}”?\n\nThe Site and its retained records will return to active views.`))return false;return v199BaseUnarchiveSite(id)};

// ---------------------------------------------------------------------
// First-login training and a clearer, screen-pointing user guide.

let v199TrainingUserId='';
function v199ApplyTrainingPreference(){
  let user=currentUser();if(!user||v199TrainingUserId===user.id)return;
  v199TrainingUserId=user.id;let changed=false;
  if(user.trainingPreferenceInitialized===undefined){user.trainingPreferenceInitialized=Number(user.loginCount||0)>1;if(!user.trainingPreferenceInitialized){user.trainingMode=true}changed=true}
  trainingMode=user.trainingMode!==false;if(changed)save();
}
toggleTrainingMode=function(value){let user=currentUser();trainingMode=!!value;if(user){user.trainingMode=trainingMode;user.trainingPreferenceInitialized=true;save()}render()};

const v199BaseGuideSteps=guideSteps;
guideSteps=function(){return v199BaseGuideSteps().map(step=>({...step,bullets:[`Look for the large “LOOK HERE” arrow on the real ${step.title} screen.`,...(step.bullets||[])]}))};
const v199BaseGuideOverlay=guideOverlay;
guideOverlay=function(){let html=v199BaseGuideOverlay();if(!html)return'';return html+`<div id="v199-guide-arrow" class="v199-guide-arrow" aria-hidden="true"><b>LOOK HERE</b><i>➜</i></div>`};
function v199GuideTarget(){
  if(!guideActive)return null;let step=guideSteps()[guideStep],target=step?.target||'';
  return document.querySelector?.(`.nav button[onclick*="'${target}'"]`)||document.querySelector?.(`.mobilebar button[onclick*="'${target}'"]`)||document.querySelector?.('.main .topbar h1');
}
function v199PositionGuideArrow(){
  document.querySelectorAll?.('.v199-guide-target').forEach(row=>row.classList.remove('v199-guide-target'));
  let arrow=document.getElementById('v199-guide-arrow'),target=v199GuideTarget();if(!arrow||!target||typeof target.getBoundingClientRect!=='function')return;
  target.classList.add('v199-guide-target');let rect=target.getBoundingClientRect(),width=Number(window.innerWidth||1280),left=Math.min(width-190,Math.max(12,rect.right+14)),top=Math.min(Number(window.innerHeight||800)-90,Math.max(72,rect.top+rect.height/2-28));
  if(left>width-200){left=Math.max(12,rect.left-176);arrow.classList.add('reverse')}else arrow.classList.remove('reverse');arrow.style.left=`${left}px`;arrow.style.top=`${top}px`;
}
const v199BaseGuidePage=newUserGuidePage;
newUserGuidePage=function(){
  let html=v199BaseGuidePage(),basics=`<section class="panel v199-guide-basics"><div class="sectionhead"><div><span class="guide-kicker">THE SIMPLE WAY TO USE SITESIGHT</span><h2>Four steps on every screen</h2><p class="muted">You do not need to learn every tool. Use the same short pattern wherever you are.</p></div></div><div class="v199-guide-four"><div><i>1</i><b>Choose the area</b><span>Use the left menu on a computer or the bottom menu on a phone.</span></div><div><i>2</i><b>Check what needs action</b><span>Red and amber items need attention. Green normally means complete or ready.</span></div><div><i>3</i><b>Open one record</b><span>Buttons saying Review, Fix or Open now take you straight to that exact record.</span></div><div><i>4</i><b>Save and check</b><span>Look for the ✓ saved message before leaving the screen.</span></div></div><div class="v199-guide-words"><b>Useful words</b><span><strong>Work Item</strong> — a plot or piece of work</span><span><strong>Issue</strong> — a problem needing action</span><span><strong>Correction</strong> — an audited change to a time record</span><span><strong>Retention</strong> — money held separately until completed work is signed off</span></div></section>`;
  return html.replace('<div class="guide-layout">',basics+'<div class="guide-layout">').replace(/takes around two minutes and uses the real SiteSight screens — no generic training video to remember\./,'is an in-depth, step-by-step walkthrough using the real SiteSight screens. Large arrows show exactly where to look.');
};

// ---------------------------------------------------------------------
// Analytics: make individual hours, wages, late and early totals visible.

function v199IndividualGross(member,data){return (data.shifts||[]).filter(shift=>shift.workerId===member.id).reduce((sum,shift)=>{try{return sum+Number(v190ShiftCalculation(shift,member).gross||0)}catch(error){return sum}},0)}
v169AttendanceAnalyticsPanel=function(){
  if(!hasPerm('view_attendance')&&!hasPerm('manage_attendance'))return'';
  let data=v169AttendanceAnalytics(),siteIds=v169SiteIdsForAnalytics(),hours=data.people.reduce((sum,row)=>sum+row.hours,0),wages=v183AnalyticsWages(data.from,data.to,siteIds),approvedOt=data.shifts.reduce((sum,shift)=>sum+Number(shift.overtimeApproval?.manualRequest===true&&shift.overtimeApproval?.status==='Approved'?shift.overtimeApproval.minutes||0:0),0)/60,financial=hasPerm('view_financials'),savingByPerson=new Map();
  data.shifts.forEach(shift=>{let config=attendanceSiteConfig(shift.siteId)||{},minutes=Math.max(0,Number(config.analyticsManualMinutes??8)-Number(config.analyticsSystemMinutes??1)),cost=minutes/60*Number(config.analyticsAdminHourlyCost??25),row=savingByPerson.get(shift.workerId)||{minutes:0,cost:0};row.minutes+=minutes;row.cost+=cost;savingByPerson.set(shift.workerId,row)});
  let columns=financial?10:8,rows=data.people.slice().sort((a,b)=>b.hours-a.hours).map(row=>{let saving=savingByPerson.get(row.member.id)||{minutes:0,cost:0};return `<tr><td><b>${v187Esc(row.member.name)}</b></td><td>${row.shifts}</td><td>${row.hours.toFixed(2)}</td>${financial?`<td>${attendanceMoney(v199IndividualGross(row.member,data))}</td>`:''}<td>${row.lateCount}</td><td>${v169MinutesLabel(row.lateMinutes)}</td><td>${row.earlyCount}</td><td>${v169MinutesLabel(row.earlyMinutes)}</td><td>${v169MinutesLabel(saving.minutes)}</td>${financial?`<td>${attendanceMoney(saving.cost)}</td>`:''}</tr>`}).join('');
  return `<div class="panel v169-attendance-analytics v183-workforce-analytics"><div class="sectionhead"><div><h2>Individual Hours, Wages & Clocking Savings</h2><div class="muted">Every individual shows their hours, calculated gross wages, late arrivals and early departures for the selected period.</div><span class="v194-rule-note">Late and early totals use each Site's saved grace-period settings.</span></div></div><div class="v169-rangebar"><button class="${v169AttendanceRange==='week'?'active':''}" onclick="v169AttendanceRange='week';render()">This Week</button><button class="${v169AttendanceRange==='fortnight'?'active':''}" onclick="v169AttendanceRange='fortnight';render()">Fortnightly</button><button class="${v169AttendanceRange==='month'?'active':''}" onclick="v169AttendanceRange='month';render()">This Month</button><button class="${v169AttendanceRange==='30'?'active':''}" onclick="v169AttendanceRange='30';render()">Last 30 Days</button><button class="${v169AttendanceRange==='custom'?'active':''}" onclick="v169AttendanceRange='custom';render()">Custom</button>${v169AttendanceRange==='custom'?`<input class="input" type="date" value="${data.from}" onchange="v169AttendanceFrom=this.value;render()"><span>→</span><input class="input" type="date" value="${data.to}" onchange="v169AttendanceTo=this.value;render()">`:''}<span class="muted">${data.from} → ${data.to}</span></div><div class="analytics-grid v183-total-cards v187-savings-cards"><div class="card"><div class="muted">Total hours worked</div><div class="analytics-value">${hours.toFixed(2)} hrs</div></div>${financial?`<div class="card"><div class="muted">Total wages paid</div><div class="analytics-value">${attendanceMoney(wages)}</div></div>`:''}<div class="card saved"><div class="muted">Timesheet admin saved</div><div class="analytics-value">${data.adminHoursSaved.toFixed(1)} hrs</div></div>${financial?`<div class="card saved"><div class="muted">Admin cost saved</div><div class="analytics-value">${attendanceMoney(data.adminCostSaved)}</div></div>`:''}<div class="card"><div class="muted">Late arrival time</div><div class="analytics-value">${v169MinutesLabel(data.lateMins)}</div></div><div class="card"><div class="muted">Early departure time</div><div class="analytics-value">${v169MinutesLabel(data.earlyMins)}</div></div><div class="card"><div class="muted">Authorised overtime</div><div class="analytics-value">${approvedOt.toFixed(2)} hrs</div></div></div><details class="v169-breakdown" open><summary>Individual hours, wages and clocking (${data.people.length})</summary><div style="overflow:auto"><table class="analytics-table"><thead><tr><th>Individual</th><th>Shifts</th><th>Total hours</th>${financial?'<th>Gross wages</th>':''}<th>Late arrivals</th><th>Late time</th><th>Early departures</th><th>Early time</th><th>Admin time saved</th>${financial?'<th>Admin cost saved</th>':''}</tr></thead><tbody>${rows||`<tr><td colspan="${columns}">No completed shifts in this period.</td></tr>`}</tbody></table></div></details><div class="v169-performance-note">Attendance figures report recorded clocking patterns only. They do not judge workmanship, productivity or overall employee performance.</div></div>`;
};

// ---------------------------------------------------------------------
// Functionality opens only the setting selected by the Director.

function v199SettingOptions(values,current,suffix=''){return values.map(value=>`<option value="${value}" ${Number(current)===Number(value)?'selected':''}>${value}${suffix}</option>`).join('')}
function v199SettingDefinition(siteRow,cfg,focus){
  let definitions={
    details:{title:'Site details',help:'Change only this Site’s display name and time zone.',fields:`<div class="field"><label>Site name</label><input id="v199-site-name" class="input" value="${v188Esc(siteRow.name)}"></div><div class="field"><label>Time zone</label><select id="v199-time-zone" class="select"><option value="Europe/London" ${siteRow.timeZone!=='UTC'?'selected':''}>United Kingdom</option><option value="UTC" ${siteRow.timeZone==='UTC'?'selected':''}>UTC</option></select></div>`},
    hours:{title:'Site default working day',help:'These hours apply where an individual does not have their own working pattern.',fields:`<div class="two"><div class="field"><label>Default start</label><input id="v199-start" class="input" type="time" value="${cfg.expectedStartTime||'07:30'}"></div><div class="field"><label>Default finish</label><input id="v199-finish" class="input" type="time" value="${cfg.expectedEndTime||'16:30'}"></div></div>`},
    gps:{title:'GPS geofence',help:'Set the permitted clocking distance and the maximum GPS uncertainty.',fields:`<div class="two"><div class="field"><label>Geofence radius</label><select id="v199-radius" class="select">${v199SettingOptions([50,75,100,150,200,250,300],cfg.radiusM||250,' metres')}</select></div><div class="field"><label>Maximum GPS uncertainty</label><select id="v199-accuracy" class="select">${v199SettingOptions([20,30,50,75,100,150],cfg.maxAccuracyM||75,' metres')}</select></div></div><div class="v189-location-box"><span>${attendanceLocationConfigured(siteRow.id)?`Current location: ${Number(cfg.latitude).toFixed(6)}, ${Number(cfg.longitude).toFixed(6)}`:'No Site location configured'}</span><button class="btn light smallbtn" onclick="v189SetSiteLocation('${siteRow.id}')">⌖ Use this device location</button></div><div id="v189-settings-status" class="att-clock-progress"></div>`},
    clock:{title:'Site clocking and scheduled Clock Out',help:'Choose whether people can clock at this Site and whether a fresh GPS check can close a shift at its scheduled finish.',fields:`<label class="v189-toggle"><input id="v199-enabled" type="checkbox" ${cfg.enabled===false?'':'checked'}><span><b>Enable Site clocking</b><small>Allow linked people to Clock In and Clock Out.</small></span></label><label class="v189-toggle"><input id="v199-auto-gps" type="checkbox" ${cfg.autoClockOutGps===false?'':'checked'}><span><b>GPS-verified scheduled Clock Out</b><small>At shift finish, close only after a fresh valid location reading.</small></span></label>`},
    grace:{title:'Punctuality grace',help:'Late and early totals use these separate allowances before recording a deduction.',fields:`<div class="two"><div class="field"><label>Late-arrival grace</label><select id="v199-late" class="select">${v199SettingOptions([0,3,5,10,15],cfg.lateArrivalGraceMinutes??0,' minutes')}</select></div><div class="field"><label>Early-leaving grace</label><select id="v199-early" class="select">${v199SettingOptions([0,3,5,10,15],cfg.earlyLeaveGraceMinutes??5,' minutes')}</select></div></div>`},
    shift:{title:'Shift safeguards',help:'Show a warning when an open shift exceeds this duration.',fields:`<div class="field"><label>Long-shift warning</label><select id="v199-max-shift" class="select">${v199SettingOptions([10,11,12,13,14,16],cfg.maxShiftHours||12,' hours')}</select></div>`},
    analytics:{title:'Analytics assumptions',help:'These values affect only the estimated administration-time and cost savings shown in Analytics.',fields:`<div class="three"><div class="field"><label>Manual admin minutes / person / day</label><input id="v199-manual" class="input" type="number" min="0" max="60" value="${Number(cfg.analyticsManualMinutes||8)}"></div><div class="field"><label>SiteSight minutes / person / day</label><input id="v199-system" class="input" type="number" min="0" max="60" value="${Number(cfg.analyticsSystemMinutes||1)}"></div><div class="field"><label>Admin cost / hour</label><input id="v199-cost" class="input" type="number" min="0" step="0.5" value="${Number(cfg.analyticsAdminHourlyCost||25)}"></div></div>`},
    lifts:{title:'Lift template',help:'One lift name per line. This template is used for new plots on this Site.',fields:`<div class="field"><label>Lift stages <span class="muted">(one per line)</span></label><textarea id="v199-stages" class="textarea">${v188Esc((siteRow.stageTemplate||[]).join('\n'))}</textarea></div>`}
  };
  return definitions[focus]||definitions.details;
}
v189OpenSiteSettings=function(siteId,focus='details'){
  if(!v189Director())return toast('Director permission is required');let siteRow=db.sites.find(row=>row.id===siteId),cfg=attendanceSiteConfig(siteId);if(!siteRow||!cfg)return;let definition=v199SettingDefinition(siteRow,cfg,focus);
  modal=`<div class="modal"><div class="modalbox v189-settings-modal v199-focused-settings"><span class="att-kicker">SITE FUNCTIONALITY · ${v188Esc(siteRow.name)}</span><h2>${v188Esc(definition.title)}</h2><p class="muted">${v188Esc(definition.help)}</p><div class="v199-setting-fields">${definition.fields}</div><div class="actions"><button class="btn orange" onclick="v199SaveSiteSetting('${siteId}','${focus}')">Save ${v188Esc(definition.title)}</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render();
};
function v199SaveSiteSetting(siteId,focus){
  if(!v189Director())return toast('Director permission is required');let siteRow=db.sites.find(row=>row.id===siteId),cfg=attendanceSiteConfig(siteId);if(!siteRow||!cfg)return;
  if(focus==='details'){let name=(document.getElementById('v199-site-name')?.value||'').trim();if(!name)return toast('Enter a Site name');siteRow.name=name;siteRow.timeZone=document.getElementById('v199-time-zone')?.value||'Europe/London'}
  else if(focus==='hours'){let start=document.getElementById('v199-start')?.value||'07:30',finish=document.getElementById('v199-finish')?.value||'16:30';if(v169ParseTime(finish)<=v169ParseTime(start))return toast('Finish time must be after start time');cfg.expectedStartTime=start;cfg.expectedEndTime=finish}
  else if(focus==='gps'){cfg.radiusM=Number(document.getElementById('v199-radius')?.value||250);cfg.maxAccuracyM=Number(document.getElementById('v199-accuracy')?.value||75)}
  else if(focus==='clock'){cfg.enabled=!!document.getElementById('v199-enabled')?.checked;cfg.autoClockOutGps=!!document.getElementById('v199-auto-gps')?.checked}
  else if(focus==='grace'){cfg.lateArrivalGraceMinutes=Number(document.getElementById('v199-late')?.value??0);cfg.earlyLeaveGraceMinutes=Number(document.getElementById('v199-early')?.value??5)}
  else if(focus==='shift')cfg.maxShiftHours=Number(document.getElementById('v199-max-shift')?.value||12);
  else if(focus==='analytics'){cfg.analyticsManualMinutes=Math.max(0,Number(document.getElementById('v199-manual')?.value||0));cfg.analyticsSystemMinutes=Math.max(0,Number(document.getElementById('v199-system')?.value||0));cfg.analyticsAdminHourlyCost=Math.max(0,Number(document.getElementById('v199-cost')?.value||0))}
  else if(focus==='lifts'){let stages=(document.getElementById('v199-stages')?.value||'').split(/\n+/).map(value=>value.trim()).filter(Boolean);if(!stages.length)return toast('Keep at least one lift stage');siteRow.stageTemplate=stages}
  save();audit('site','functionality',siteId,`${siteRow.name}: ${v199SettingDefinition(siteRow,cfg,focus).title} updated`,siteId);modal=null;savedState('Site setting saved');render();
}
v189FunctionalityPage=function(){
  if(!v189Director())return smartEmpty('Director access required.','Only a Director can change company-wide Site functionality.','','','⚙');
  let rows=activeSites().map(siteRow=>{let cfg=attendanceSiteConfig(siteRow.id)||{},members=v189SiteMembers(siteRow.id);return `<details class="panel v189-site-settings" ${site==='all'||site===siteRow.id?'open':''}><summary><span><b>${v188Esc(siteRow.name)}</b><small>${members.length} active individual${members.length===1?'':'s'} · ${cfg.enabled===false?'Clocking disabled':'Clocking enabled'}</small></span><strong>Manage</strong></summary><div class="v189-settings-list">${v189SettingRow('Site details',`${siteRow.timeZone==='UTC'?'UTC':'United Kingdom'} time zone`,`v189OpenSiteSettings('${siteRow.id}','details')`)}${v189SettingRow('Site default working day',`${v189TimeLabel(cfg.expectedStartTime,'07:30')}–${v189TimeLabel(cfg.expectedEndTime,'16:30')}`,`v189OpenSiteSettings('${siteRow.id}','hours')`)}${v189SettingRow('Individual working hours',`${members.length} people assigned`,`v189OpenSitePeople('${siteRow.id}')`)}${v189SettingRow('GPS geofence',attendanceLocationConfigured(siteRow.id)?`${Number(cfg.radiusM||250)}m radius · GPS ±${Number(cfg.maxAccuracyM||75)}m`:'Site location not configured',`v189OpenSiteSettings('${siteRow.id}','gps')`)}${v189SettingRow('Site clocking and scheduled Clock Out',cfg.enabled===false?'Clocking disabled':cfg.autoClockOutGps===false?'Manual Clock Out':'Fresh GPS check at scheduled finish',`v189OpenSiteSettings('${siteRow.id}','clock')`)}${v189SettingRow('Punctuality grace',`${Number(cfg.lateArrivalGraceMinutes??0)}m late · ${Number(cfg.earlyLeaveGraceMinutes??5)}m early`,`v189OpenSiteSettings('${siteRow.id}','grace')`)}${v189SettingRow('Shift safeguards',`${Number(cfg.maxShiftHours||12)} hour warning`,`v189OpenSiteSettings('${siteRow.id}','shift')`)}${v189SettingRow('Analytics assumptions',`${Number(cfg.analyticsManualMinutes||8)}m manual → ${Number(cfg.analyticsSystemMinutes||1)}m SiteSight · £${Number(cfg.analyticsAdminHourlyCost||25)}/hr`,`v189OpenSiteSettings('${siteRow.id}','analytics')`)}${v189SettingRow('Lift template',`${(siteRow.stageTemplate||[]).length||5} stages`,`v189OpenSiteSettings('${siteRow.id}','lifts')`)}</div></details>`}).join('');
  let base=`<div class="topbar"><div><span class="att-kicker">DIRECTOR CONTROL</span><h1>Functionality</h1><div class="sub">Each Edit button now opens only the setting named on that row.</div></div></div><div class="v189-functionality-intro"><b>Changes apply to future clocking and operational records.</b><span>Existing approved timesheets and historical pay records are retained.</span></div>${rows||smartEmpty('No active Sites.','Create a Site in Admin before configuring functionality.','','','⚙')}`;
  if(sharedMode&&!v192SystemStatus)setTimeout(v192LoadSystemStatus,0);
  return base+(typeof v190ZonePanel==='function'?v190ZonePanel():'')+(typeof v191TimesheetDefaultsPanel==='function'?v191TimesheetDefaultsPanel():'')+(typeof v192FunctionalityPanels==='function'?v192FunctionalityPanels():'');
};

// ---------------------------------------------------------------------
// Notifications open the most specific related record available.

function v199NotificationItem(notification){return (db.items||[]).find(item=>item.id===notification.itemId)}
function v199NotificationIssue(notification,item){let text=String(notification.text||'').toLowerCase();return (item?.issues||[]).find(issue=>text.includes(String(issue.title||'').toLowerCase()))||(item?.issues||[]).filter(issue=>issue.status==='Open').sort((a,b)=>String(b.updatedAt||b.createdAt||b.created||'').localeCompare(String(a.updatedAt||a.createdAt||a.created||'')))[0]}
function v199OpenDocumentNotification(row,notification){if(!v191DocumentVisible(row))return toast('This document is not available to your account');v191DocumentFolder=row.parentId||'';v191DocumentSite='all';v191DocumentQuery=row.name||'';view='documents';selected=null;modal=null;render();if(/expired|expires/i.test(notification.text||'')&&hasPerm('manage_documents'))setTimeout(()=>v192EditDocumentControl(row.id),0)}
function v199OpenNotification(id){
  let user=currentUser(),notification=(db.notifications||[]).find(row=>row.id===id);if(!user||!notification)return toast('This notification is no longer available');notification.readBy=notification.readBy||[];if(!notification.readBy.includes(user.id))notification.readBy.push(user.id);save();settingsOpen=false;modal=null;
  let documentRow=(db.documents||[]).find(row=>row.id===notification.itemId&&row.type==='file');if(documentRow)return v199OpenDocumentNotification(documentRow,notification);
  let item=v199NotificationItem(notification);if(item&&canAccessSite(item.site)){if(notification.kind==='issue'&&hasPerm('view_issues')){let issue=v199NotificationIssue(notification,item);if(issue)return viewIssue(item.id,issue.id)}let targetIndex=item.type==='House Plot'?(item.lifts||[]).findIndex(lift=>String(notification.text||'').toLowerCase().includes(String(lift.name||'').toLowerCase())):-1;return v199OpenWorkTarget(item.id,targetIndex>=0?'lift':'item',targetIndex)}
  if(notification.kind==='site'){
    let diary=(db.siteDiary||[]).filter(row=>canAccessSite(row.siteId)&&row.siteId===notification.siteId&&String(notification.text||'').includes(row.date)).sort((a,b)=>String(b.createdAt||b.date).localeCompare(String(a.createdAt||a.date)))[0];if(diary){site=diary.siteId;view='diary';render();return setTimeout(()=>openDiaryEntry(diary.id),0)}
    let notes=(db.calendarNotes||[]).filter(row=>row.siteId===notification.siteId&&String(notification.text||'').toLowerCase().includes(String(row.title||'').toLowerCase())).sort((a,b)=>String(b.updatedAt||b.createdAt||'').localeCompare(String(a.updatedAt||a.createdAt||'')));if(notes[0]){site=notes[0].siteId;calendarMonth=String(notes[0].date).slice(0,7)+'-01';view='calendar';render();return setTimeout(()=>openCalendarNote(notes[0].id),0)}
    if(notification.siteId&&canAccessSite(notification.siteId)){site=notification.siteId;view='dashboard';render();return}
  }
  if(notification.kind==='attendance'){contractorAttendanceTab='today';view='attendance';render();return}
  toast('The original record is no longer available');render();
}
notificationSettings=function(){
  let user=currentUser(),items=relevantNotifications(user).slice(0,30),unread=unreadNotifications(user).length;settingsOpen=false;
  modal=`<div class="modal"><div class="modalbox v199-notifications"><h2>Notifications</h2><div class="notify-row"><div><b>Automatic notifications</b><div class="muted">Notify me about updates I am permitted to view.</div></div><label class="switch"><input type="checkbox" ${user.notificationsEnabled?'checked':''} onchange="enableNotifications(this.checked)"><span></span></label></div><p class="muted">Select a notification to open the closest exact record, date, document control or action.</p><div class="sectionhead"><h3>Recent notifications</h3>${unread?`<button class="btn light smallbtn" onclick="markNotificationsRead()">Mark ${unread} read</button>`:''}</div><div class="notification-list">${items.length?items.map(row=>`<button class="notification-item ${(row.readBy||[]).includes(user.id)?'':'unread'}" onclick="v199OpenNotification('${row.id}')"><span><b>${v195Esc(row.text)}</b><small>${v195Esc(row.actorName||'SiteSight')} · ${new Date(row.createdAt).toLocaleString()}</small></span><strong>Open →</strong></button>`).join(''):'<div class="empty">No notifications yet.</div>'}</div><div class="actions"><button class="btn light" onclick="modal=null;render()">Close</button></div></div></div>`;render();
};

// ---------------------------------------------------------------------
// V1.99.1 — every non-Director account starts with an explicit Site.
// Additional Sites are a deliberate exception; only a Director can grant
// unrestricted company-wide visibility.

function v1991AssignableSites(){return activeSites().filter(row=>canAccessSite(row.id))}
function v1991DefaultUserSites(ids=[]){
  let choices=v1991AssignableSites(),valid=(ids||[]).filter(id=>choices.some(row=>row.id===id));
  if(valid.length)return valid;let fallback=site!=='all'&&choices.some(row=>row.id===site)?site:choices[0]?.id;return fallback?[fallback]:[]
}
sitePermissionInputs=function(ids=[],allGranted=false){
  let choices=v1991AssignableSites(),selected=v1991DefaultUserSites(ids),director=currentUser()?.role==='Director',all=director&&allGranted===true;
  return `<div class="field v1991-site-access"><label>Designated Site access</label><p class="muted">Select the Site this person works on. Tick extra Sites only when their job genuinely needs them.</p>${director?`<label class="site-access-item v1991-all-sites"><input id="usa-all" type="checkbox" ${all?'checked':''} onchange="toggleSitePermissions()"> <span><b>Grant all current and future Sites</b><small>Director-approved exception</small></span></label>`:''}<div class="site-access-grid">${choices.map(row=>`<label class="site-access-item"><input class="usa-site" type="checkbox" value="${row.id}" ${selected.includes(row.id)&&!all?'checked':''} ${all?'disabled':''}> ${v195Esc(row.name)}</label>`).join('')||'<span class="muted">No active Site is available to assign.</span>'}</div><div id="v1991-site-access-summary" class="muted">${all?'All Sites granted':`${selected.length} designated Site${selected.length===1?'':'s'} selected`}</div></div>`
};
toggleSitePermissions=function(){
  let all=!!document.getElementById('usa-all')?.checked,boxes=[...document.querySelectorAll('.usa-site')];boxes.forEach(box=>box.disabled=all);if(!all&&!boxes.some(box=>box.checked)&&boxes[0])boxes[0].checked=true;let count=boxes.filter(box=>box.checked).length,summary=document.getElementById('v1991-site-access-summary');if(summary)summary.textContent=all?'All Sites granted':`${count} designated Site${count===1?'':'s'} selected`
};
collectAllowedSites=function(){return [...document.querySelectorAll('.usa-site:checked')].map(box=>box.value)};
function v1991CollectAllSitesAccess(){return currentUser()?.role==='Director'&&!!document.getElementById('usa-all')?.checked}
function v1991ValidateSiteGrant(roleName){if(roleName==='Director')return true;if(v1991CollectAllSitesAccess())return true;if(!collectAllowedSites().length){toast('Choose at least one designated Site');return false}return true}

const V1998_ACCOUNTS_TIME_PERMISSIONS=['view_attendance','manage_attendance','approve_timesheets','manage_working_hours','approve_overtime'];
function v1998ApplyAccountsTimePolicy(user,actor,requestedPerms){
  let permissions={...(requestedPerms||{})};
  if(user?.role!=='Accounts')return {permissions,timeApprovalDelegatedByDirector:false};
  let previous=user?.permissions||{},delegated=user?.timeApprovalDelegatedByDirector===true;
  if(actor?.role!=='Director'){
    for(const key of V1998_ACCOUNTS_TIME_PERMISSIONS)permissions[key]=delegated&&previous[key]===true;
    return {permissions,timeApprovalDelegatedByDirector:delegated&&V1998_ACCOUNTS_TIME_PERMISSIONS.some(key=>permissions[key])}
  }
  if(permissions.manage_attendance||permissions.approve_timesheets||permissions.manage_working_hours||permissions.approve_overtime)permissions.view_attendance=true;
  delegated=V1998_ACCOUNTS_TIME_PERMISSIONS.some(key=>permissions[key]===true);
  return {permissions,timeApprovalDelegatedByDirector:delegated}
}
function v1998CanReviewTimeRecords(user=currentUser()){
  if(!user||user.accountActive===false)return false;
  if(user.role==='Accounts'&&user.timeApprovalDelegatedByDirector!==true)return false;
  return ['view_attendance','manage_attendance','approve_timesheets','approve_overtime'].some(key=>user.permissions?.[key]===true)
}
const V1998_APP_PLOT_ROLES=['Director','Contracts Manager','Site Manager','Supervisor'];
const V1998_APP_PLOT_PERMISSIONS=['work_items','update_progress','manage_workitems','sign_off','edit_lifts','view_issues','raise_issues','manage_issues','access_all_site_work'];
function v1998AppRoleCanAccessPlots(roleName){return V1998_APP_PLOT_ROLES.includes(roleName)}
function v1998OperatorFlagInput(checked=false){return `<label class="v1998-operator-flag"><input id="v1998-operator-subcontractor" type="checkbox" ${checked?'checked':''} onchange="v1998ToggleOperatorFlag(this)"><span><b>This Operator is a subcontractor</b><small>Keeps one Operator workspace for time and attendance while recording subcontractor status for pay and CIS. Operators never receive plot-card or progress access.</small></span></label>`}
function v1998PlotAccessNote(){return `<div id="v1998-plot-access-note" class="v1998-accounts-time-note"><b>Plot access starts at Supervisor</b><span>Only Supervisors, Site Managers, Contracts Managers and Directors can view plot cards or update progress.</span></div>`}
function v1998AccountsTimeNote(){return `<div id="v1998-accounts-time-note" class="v1998-accounts-time-note" hidden><b>Accounts shift access is off by default</b><span>Only a Director can grant shift review, correction, timesheet approval or overtime approval permissions.</span></div>`}
function v1998ToggleOperatorFlag(input){if(input?.checked){let clock=document.querySelector('[data-perm="clock_attendance"]');if(clock)clock.checked=true}}
function v1998PrepareUserAccessForm(selectId,preservePermissions=false){
  let roleName=document.getElementById(selectId)?.value||'Operator';
  if(!preservePermissions)v184ApplyRoleTemplate(selectId);
  let flag=document.querySelector('.v1998-operator-flag');if(flag)flag.hidden=roleName!=='Operator';
  let plotAllowed=v1998AppRoleCanAccessPlots(roleName),plotNote=document.getElementById('v1998-plot-access-note');if(plotNote)plotNote.hidden=plotAllowed;
  for(const key of V1998_APP_PLOT_PERMISSIONS){let box=document.querySelector(`[data-perm="${key}"]`),card=box?.closest?.('[data-permission-box]');if(!box)continue;box.disabled=!plotAllowed;if(!plotAllowed)box.checked=false;card?.classList?.toggle('restricted',!plotAllowed)}
  let accounts=roleName==='Accounts',director=currentUser()?.role==='Director',note=document.getElementById('v1998-accounts-time-note');if(note)note.hidden=!accounts;
  for(const key of V1998_ACCOUNTS_TIME_PERMISSIONS){let box=document.querySelector(`[data-perm="${key}"]`),card=box?.closest?.('[data-permission-box]');if(!box)continue;box.disabled=accounts&&!director;card?.classList?.toggle('restricted',accounts&&!director)}
}
function v1998RoleChanged(selectId){v1998PrepareUserAccessForm(selectId,false)}

addUser=function(){
  if(!hasPerm('manage_users'))return toast('You do not have permission to manage users');let director=currentUser()?.role==='Director',roles=roleOptions.filter(name=>director||name!=='Director'),perms=defaultPerms('Operator');
  modal=`<div class="modal"><div class="modalbox v1991-user-access"><h2>Add User</h2><p class="muted">Start with the person’s normal job role and their main Site. Extra access can be granted later.</p><div class="two"><div class="field"><label>Full name</label><input id="un" class="input" placeholder="Alex Smith"></div><div class="field"><label>Username</label><input id="uu" class="input" placeholder="alex.smith"></div></div><div class="field"><label>Email address</label><input id="ue" class="input" type="email" placeholder="alex@company.co.uk"></div><div class="two"><div class="field"><label>Temporary password</label><input id="up" class="input" type="password" autocomplete="new-password" placeholder="Minimum 8 characters"></div><div class="field"><label>Role</label><select id="ur" class="select" onchange="v1998RoleChanged('ur')">${roles.map(name=>`<option ${name==='Operator'?'selected':''}>${name}</option>`).join('')}</select></div></div>${v1998OperatorFlagInput(false)}${v1998PlotAccessNote()}${v1998AccountsTimeNote()}<h3>What this person can do</h3>${permissionInputs(perms)}<h3>Where this person can work</h3>${sitePermissionInputs([],false)}<div class="actions"><button class="btn orange" onclick="saveNewUser()">Save user</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render();setTimeout(()=>v1998PrepareUserAccessForm('ur',false),0)
};
saveNewUser=function(){
  if(!hasPerm('manage_users'))return toast('You do not have permission to manage users');let name=(document.getElementById('un')?.value||'').trim(),username=(document.getElementById('uu')?.value||'').trim().toLowerCase(),email=(document.getElementById('ue')?.value||'').trim().toLowerCase(),password=document.getElementById('up')?.value||'',roleName=document.getElementById('ur')?.value||'Operator';
  if(!name||!username||!email||!password)return toast('Name, username, email and password are required');if(password.length<8)return toast('Use a password with at least 8 characters');if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))return toast('Enter a valid email address');if(db.users.some(row=>String(row.username||'').toLowerCase()===username))return toast('That username already exists');if(db.users.some(row=>String(row.email||'').toLowerCase()===email))return toast('That email address is already registered');if(roleName==='Director'&&currentUser()?.role!=='Director')return toast('Only a Director can create another Director');if(!v1991ValidateSiteGrant(roleName))return;
  let allSitesAccess=roleName==='Director'||v1991CollectAllSitesAccess(),allowedSites=allSitesAccess?[]:collectAllowedSites(),requested=roleName==='Director'?defaultPerms('Director'):collectPerms(),policy=v1998ApplyAccountsTimePolicy({role:roleName,permissions:{}},currentUser(),requested),permissions=policy.permissions;if(!v1998AppRoleCanAccessPlots(roleName))for(const key of V1998_APP_PLOT_PERMISSIONS)permissions[key]=false;if(!v184RoleMayMeasure(roleName))permissions.measure_drawings=false;
  let created={id:'u'+Date.now(),name,username,email,password,resetCode:'',resetExpires:0,role:roleName,isSubcontractor:roleName==='Operator'&&!!document.getElementById('v1998-operator-subcontractor')?.checked,timeApprovalDelegatedByDirector:policy.timeApprovalDelegatedByDirector,permissions,allowedSites,allSitesAccess,notificationsEnabled:false,trainingMode:true,trainingPreferenceInitialized:false,onboardingSeen:false,onboardingCompletedAt:'',loginCount:0,lastLoginAt:'',recentWorkItems:[],cameraIntroSeen:false,linkedMemberId:'',accountActive:true,disabledReason:'',disabledAt:'',disabledBy:'',permissionProfileMode:v173PermissionMode(roleName,permissions),permissionTemplateVersion:199,setupChecklistDismissed:false};db.users.push(created);save();audit('user','user access',created.id,`${name}: account created with ${allSitesAccess?'all Sites':allowedSites.length+' designated Site'+(allowedSites.length===1?'':'s')}`,'');modal=null;savedState('User saved');render()
};
editUserAccess=function(id){
  if(!hasPerm('manage_users'))return toast('You do not have permission to manage users');let user=db.users.find(row=>row.id===id);if(!user||user.role==='Director')return;
  modal=`<div class="modal"><div class="modalbox v1991-user-access"><h2>Edit User Access</h2><p><b>${v195Esc(user.name)}</b> · @${v195Esc(user.username)}</p><div class="v172-user-status ${user.accountActive===false?'disabled':'active'}"><b>${user.accountActive===false?'ACCOUNT DISABLED':'ACCOUNT ACTIVE'}</b><span>${user.accountActive===false?v195Esc(user.disabledReason||'Access disabled'):'User can sign in with their current permissions.'}</span></div><label class="permcheck v172-account-toggle"><input id="v172-account-active" type="checkbox" ${user.accountActive!==false?'checked':''}> <span><b>Allow this user to sign in</b><small>Untick to disable the account immediately.</small></span></label><div class="field"><label>Registered email</label><input id="ee" class="input" type="email" value="${v195Esc(user.email||'')}"></div><div class="field"><label>Role</label><select id="er" class="select" onchange="v1998RoleChanged('er')">${roleOptions.filter(name=>name!=='Director').map(name=>`<option ${user.role===name?'selected':''}>${name}</option>`).join('')}</select></div>${v1998OperatorFlagInput(user.role==='Operator'&&user.isSubcontractor===true)}${v1998PlotAccessNote()}${v1998AccountsTimeNote()}<h3>What this person can do</h3>${permissionInputs(user.permissions)}<h3>Where this person can work</h3>${sitePermissionInputs(user.allowedSites||[],user.allSitesAccess===true)}<div class="actions"><button class="btn orange" onclick="saveUserAccess('${user.id}')">Save access</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render();setTimeout(()=>v1998PrepareUserAccessForm('er',true),0)
};
saveUserAccess=function(id){
  if(!hasPerm('manage_users'))return toast('You do not have permission to manage users');let user=db.users.find(row=>row.id===id);if(!user||user.role==='Director')return;let email=(document.getElementById('ee')?.value||'').trim().toLowerCase(),roleName=document.getElementById('er')?.value||user.role;if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))return toast('Enter a valid email address');if(db.users.some(row=>row.id!==id&&String(row.email||'').toLowerCase()===email))return toast('That email is already registered');if(!v1991ValidateSiteGrant(roleName))return;
  let wasActive=user.accountActive!==false,nowActive=!!document.getElementById('v172-account-active')?.checked,requested=collectPerms(),policy=v1998ApplyAccountsTimePolicy({...user,role:roleName},currentUser(),requested),permissions=policy.permissions,allSitesAccess=v1991CollectAllSitesAccess(),allowedSites=allSitesAccess?[]:collectAllowedSites();if(!v1998AppRoleCanAccessPlots(roleName))for(const key of V1998_APP_PLOT_PERMISSIONS)permissions[key]=false;if(!v184RoleMayMeasure(roleName))permissions.measure_drawings=false;user.email=email;user.role=roleName;user.isSubcontractor=roleName==='Operator'&&!!document.getElementById('v1998-operator-subcontractor')?.checked;user.timeApprovalDelegatedByDirector=policy.timeApprovalDelegatedByDirector;user.permissions=permissions;user.allowedSites=allowedSites;user.allSitesAccess=allSitesAccess;user.accountActive=nowActive;user.permissionTemplateVersion=199;user.permissionProfileMode=v173PermissionMode(roleName,permissions);if(wasActive&&!nowActive){user.disabledReason='Disabled by administrator';user.disabledAt=new Date().toISOString();user.disabledBy=currentUser()?.name||''}else if(nowActive){user.disabledReason='';user.disabledAt='';user.disabledBy=''}save();audit('user','user access',user.id,`${user.name}: account ${nowActive?'enabled':'disabled'} · ${allSitesAccess?'all Sites':allowedSites.length+' designated Site'+(allowedSites.length===1?'':'s')}`,'');modal=null;savedState('User access updated');render()
};
const v1991BaseDetail=detail;
detail=function(item){let html=v1991BaseDetail(item);return html.replace(`<div class="muted">Paid</div><b>${pct(item)}%</b>`,`<div class="muted">Paid</div><b>${paymentPct(item)}%</b>`)};

// User-entered wording remains stored exactly as entered. Before any generated
// screen markup is inserted, active web content is removed so a title, note or
// report can never be interpreted as executable page code.
function v1991SanitiseAppMarkup(value){
  let source=String(value??'');if(!source||typeof DOMParser==='undefined')return source;let parsed=new DOMParser().parseFromString(`<body>${source}</body>`,'text/html'),blocked=new Set(['SCRIPT','STYLE','OBJECT','EMBED','BASE','META','LINK','MATH']),dangerousHandler=/(?:\b(?:fetch|xmlhttprequest|websocket|sendbeacon|eval|function|constructor|__proto__|import|document|window|globalthis|localstorage|sessionstorage|indexeddb|location|cookie|alert|prompt|settimeout|setinterval|open)\b|=>|`)/i;
  [...parsed.body.querySelectorAll('*')].forEach(element=>{if(blocked.has(element.tagName)){element.replaceWith(document.createTextNode(element.outerHTML));return}[...element.attributes].forEach(attribute=>{let name=attribute.name.toLowerCase(),content=String(attribute.value||'').trim();if(name==='srcdoc'||(name.startsWith('on')&&dangerousHandler.test(content))){element.removeAttribute(attribute.name);return}if(['href','src','action','formaction','poster','xlink:href'].includes(name)&&/^(?:javascript|vbscript|data:text\/html)/i.test(content))element.removeAttribute(attribute.name);if(name==='href'&&/^data:/i.test(content))element.removeAttribute(attribute.name);if(name==='src'&&/^data:(?!image\/(?:png|jpe?g|webp|gif|svg\+xml)[;,])/i.test(content))element.removeAttribute(attribute.name);if(name==='style'&&/(?:expression\s*\(|url\s*\(|-moz-binding|behavior\s*:)/i.test(content))element.removeAttribute(attribute.name)});if(element.tagName==='IFRAME'&&!/^(?:\/uploads\/|blob:|data:application\/pdf)/i.test(element.getAttribute('src')||''))element.replaceWith(document.createTextNode(element.outerHTML))});return parsed.body.innerHTML
}
if(typeof Element!=='undefined'&&!window.__sitesightSafeMarkupInstalled){
  let descriptor=Object.getOwnPropertyDescriptor(Element.prototype,'innerHTML');if(descriptor?.get&&descriptor?.set)Object.defineProperty(Element.prototype,'innerHTML',{configurable:true,enumerable:descriptor.enumerable,get:descriptor.get,set(value){if(this.namespaceURI==='http://www.w3.org/1999/xhtml'&&!['SCRIPT','STYLE'].includes(this.tagName))return descriptor.set.call(this,v1991SanitiseAppMarkup(value));return descriptor.set.call(this,value)}});window.__sitesightSafeMarkupInstalled=true
}

// Concurrent edits to different fields can still merge automatically. When
// two devices change the same field, saving pauses and asks the tester which
// value to keep instead of silently choosing one.
let v1991PendingConflict=null;
function v1991JsonEqual(left,right){return JSON.stringify(left)===JSON.stringify(right)}
function v1991ConflictPaths(base,local,remote,path='',out=[]){
  if(v1991JsonEqual(local,remote)||v1991JsonEqual(local,base)||v1991JsonEqual(remote,base))return out;if(['auditLog','notifications','assistUsage','pushSubscriptions'].includes(path.split('.')[0]))return out;
  let objects=[base,local,remote].every(value=>value&&typeof value==='object');if(!objects){out.push(path||'record');return out}
  if([base,local,remote].every(Array.isArray)){
    let keyed=[base,local,remote].every(list=>list.every(value=>value&&typeof value==='object'&&value.id));if(!keyed){out.push(path||'list');return out}let maps=[base,local,remote].map(list=>new Map(list.map(value=>[value.id,value]))),ids=new Set([...maps[0].keys(),...maps[1].keys(),...maps[2].keys()]);for(const id of ids){let [b,l,r]=maps.map(map=>map.get(id)),next=`${path||'record'}[${id}]`;if(l===undefined||r===undefined){if(!v1991JsonEqual(l,b)&&!v1991JsonEqual(r,b)&&!v1991JsonEqual(l,r))out.push(next)}else v1991ConflictPaths(b||{},l,r,next,out)}return out
  }
  if(Array.isArray(base)||Array.isArray(local)||Array.isArray(remote)){out.push(path||'record');return out}let keys=new Set([...Object.keys(base||{}),...Object.keys(local||{}),...Object.keys(remote||{})]);for(const key of keys)v1991ConflictPaths(base?.[key],local?.[key],remote?.[key],path?`${path}.${key}`:key,out);return out
}
function v1991ConflictLabel(path){let value=String(path||'record'),itemMatch=value.match(/^items\[([^\]]+)\]/),item=itemMatch&&(db.items||[]).find(row=>row.id===itemMatch[1]);if(item)value=value.replace(itemMatch[0],item.ref||'Work item');return value.replace(/\[([^\]]+)\]/g,' record').replace(/\.([a-z])/g,(_,letter)=>' › '+letter.toUpperCase()).replace(/([a-z])([A-Z])/g,'$1 $2')}
function v1991ShowConflict(remote,local,base,paths){
  v1991PendingConflict={remote,localChoice:threeWayMerge(base,local,remote.data||seed),remoteChoice:threeWayMerge(base,remote.data||seed,local),paths};pendingSave=true;syncState='error';syncError='Another device changed the same information. Choose which version to keep.';
  modal=`<div class="modal"><div class="modalbox v1991-conflict"><span class="att-kicker">SAVE PAUSED</span><h2>Another device changed the same information</h2><p>Your changes are safe on this device. Choose which version should win for the ${paths.length} overlapping field${paths.length===1?'':'s'} below.</p><div class="v1991-conflict-list">${paths.slice(0,8).map(value=>`<div><b>${v195Esc(v1991ConflictLabel(value))}</b></div>`).join('')}${paths.length>8?`<div>＋ ${paths.length-8} more overlapping fields</div>`:''}</div><div class="actions"><button class="btn orange" onclick="v1991ResolveConflict('local')">Keep my changes</button><button class="btn light" onclick="v1991ResolveConflict('remote')">Use shared version</button></div><p class="muted">Changes made to different fields are kept either way.</p></div></div>`;render();updateSyncUI()
}
async function v1991ResolveConflict(choice){let conflict=v1991PendingConflict;if(!conflict)return;let remoteData=conflict.remote.data||seed;db=choice==='remote'?conflict.remoteChoice:conflict.localChoice;sharedRevision=conflict.remote.revision||sharedRevision;lastSyncedDB=cloneDB(remoteData);pendingSave=!v1991JsonEqual(db,remoteData);v1991PendingConflict=null;modal=null;syncError='';persistCache();if(pendingSave){syncState='saving';render();await syncToServer(true)}else{syncState='synced';lastSyncAt=new Date().toISOString();render()}}
syncToServer=async function(force=false){
  if(!sharedMode||!syncReady||(!pendingSave&&!force)||v1991PendingConflict)return;if(!navigator.onLine){syncState='offline';updateSyncUI();return}syncState='saving';syncError='';updateSyncUI();
  try{await flushPendingMedia();let response=await fetch('/api/data',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({revision:sharedRevision,data:db,clientId:currentUserId||'setup'})});if(response.status===409){let remote=await response.json(),base=lastSyncedDB||remote.data||seed,local=cloneDB(db),paths=v1991ConflictPaths(base,local,remote.data||seed);if(paths.length){v1991ShowConflict(remote,local,base,paths);return}db=threeWayMerge(base,local,remote.data||seed);sharedRevision=remote.revision||0;lastSyncedDB=cloneDB(remote.data||seed);syncConflictCount++;persistCache();return syncToServer(true)}if(response.status===401){syncState='error';syncError='Your secure session has ended. Sign in again to save changes.';updateSyncUI();return}if(!response.ok)throw new Error(`Save failed (${response.status})`);let result=await response.json();sharedRevision=result.revision;if(result.data)db=result.data;lastSyncedDB=cloneDB(db);lastSyncAt=new Date().toISOString();pendingSave=false;syncState='synced';try{localStorage.setItem(BASE_KEY,JSON.stringify(lastSyncedDB));localStorage.removeItem(PENDING_KEY)}catch(error){}persistCache();updateSyncUI()}
  catch(error){pendingSave=true;syncState='offline';syncError=error.message||'Unable to reach the shared SiteSight computer.';try{localStorage.setItem(PENDING_KEY,'1')}catch(storageError){}updateSyncUI()}
};
pollSharedData=async function(){
  if(!sharedMode||!syncReady||!navigator.onLine||!currentUserId||v1991PendingConflict)return;try{let response=await fetch(`/api/data?revision=${sharedRevision}`,{cache:'no-store'});if(response.status===401){syncState='error';syncError='Your secure session ended. Sign in again.';currentUserId='';try{localStorage.removeItem(CURRENT_USER_KEY)}catch(error){}loginScreen();v180LoginMessage('Your session ended. Please sign in again.','error');return}if(!response.ok)return;let incoming=await response.json();if(incoming.unchanged||incoming.revision===sharedRevision)return;if(pendingSave){let base=lastSyncedDB||seed,local=cloneDB(db),paths=v1991ConflictPaths(base,local,incoming.data||seed);if(paths.length){v1991ShowConflict(incoming,local,base,paths);return}db=threeWayMerge(base,local,incoming.data||seed);sharedRevision=incoming.revision;lastSyncedDB=cloneDB(incoming.data||seed);syncConflictCount++;persistCache();scheduleSync(50)}else{v180InstallRemote(incoming);render()}}catch(error){if(pendingSave){syncState='offline';updateSyncUI()}}
};

// The populated Demo includes a short, repeatable test route for every role.
// Personal-test progress is kept only in this browser and does not alter the
// demonstration records.
const V1991_TEST_ROUTES={
  'Director':[
    ['dashboard','Read the business position','Confirm the exception cards and Site selector make the next decision obvious.'],['data-quality','Check data quality','Open an exact record from each available quality group.'],['admin','Review users and Site access','Confirm every non-Director has a designated Site and understandable permissions.'],['functionality','Review Site setup','Open the lift template for each Site and confirm it affects new plots only.'],['audit','Check accountability','Find the automatic record of a change made during this route.']],
  'Contracts Manager':[
    ['dashboard','Review delivery exceptions','Start with the highest-priority exception.'],['work-items','Check live plots','Filter to one Site and open a plot that is in progress.'],['attendance','Review people and time','Confirm only people on a permitted Site can be managed.'],['commercial','Review commercial records','Check progress evidence before a pay decision.'],['issues','Resolve the work queue','Open an overdue or urgent issue and inspect its owner.']],
  'Site Manager':[
    ['attendance','Start the Site day','Check who is expected, present, late or missing.'],['work-items','Update a plot','Open one plot and record a real lift-stage update.'],['issues','Record a problem','Check the issue owner, priority and target date.'],['diary','Complete the Site diary','Confirm the daily record is quick to scan and amend.'],['documents','Find a controlled document','Open the Site folder and verify only permitted files appear.']],
  'Supervisor':[
    ['work-items','Find today’s work','Open the current assigned plot without using search help.'],['issues','Raise or review an issue','Check the wording, photos and responsibility are clear.'],['attendance','Review the Site team','Confirm another Site’s people and devices are not available.'],['diary','Record today’s position','Enter job-specific wording and confirm it remains exactly readable.']],
  'Quantity Surveyor':[
    ['commercial','Review the commercial queue','Trace a figure back to its work item and progress evidence.'],['work-items','Check measured progress','Compare completed lifts with the overall plot percentage.'],['site-drawings','Review drawings','Open a current drawing and confirm superseded information is clear.'],['documents','Check revisions','Open a document’s current and previous revisions.']],
  'Accounts':[
    ['commercial','Review approved records','Confirm only authorised pay information is available.'],['documents','Find supporting documents','Locate a permitted company or Site document.'],['audit','Trace a change','Confirm who changed a record and when.']],
  'Read Only':[
    ['dashboard','Read the Site position','Confirm important information is visible without edit controls.'],['work-items','Inspect plot progress','Compare the percentage with completed lift stages.'],['calendar','Review dates','Find completed work and an upcoming note.'],['site-drawings','Open a drawing','Confirm current revision status is obvious.'],['documents','Open a permitted file','Confirm restricted files stay hidden.']],
  'Operator':[
    ['clock','Clocking screen','Confirm a new user can identify the correct Site and main button.'],['guide','Read the short guide','Check the instructions explain location and Clock Out simply.']]
};
let v1991TestRole='';
function v1991TestProgressKey(roleName){return `sitesight-v1991-test-route:${currentUser()?.id||'demo'}:${roleName}`}
function v1991TestProgress(roleName){try{return JSON.parse(localStorage.getItem(v1991TestProgressKey(roleName))||'[]')}catch(error){return[]}}
function v1991SetTestProgress(roleName,values){try{localStorage.setItem(v1991TestProgressKey(roleName),JSON.stringify([...new Set(values)]))}catch(error){}}
function v1991OpenTestStep(roleName,index,target){let done=v1991TestProgress(roleName);if(!done.includes(index))v1991SetTestProgress(roleName,[...done,index]);if(v173CanOpenView(target))goNav(target);else{toast('This login does not include that tool. Record this as a permission test result.');render()}}
function v1991ResetTestRoute(roleName){v1991SetTestProgress(roleName,[]);render()}
function v1991TestRoutePage(){
  if(!db.demoProfile)return smartEmpty('Demo test routes are not included in this build.','Use the populated two-Site Demo package.','','','✓');let user=currentUser(),roles=user?.role==='Director'?Object.keys(V1991_TEST_ROUTES):[user?.role||'Operator'];if(!roles.includes(v1991TestRole))v1991TestRole=roles[0];let steps=V1991_TEST_ROUTES[v1991TestRole]||[],done=v1991TestProgress(v1991TestRole),percent=steps.length?Math.round(done.length/steps.length*100):0;
  return newUserGuidePage()
}
const v1991BaseCanOpenView=v173CanOpenView;
v173CanOpenView=function(target,user=currentUser()){if(target==='test-route')return !!db.demoProfile&&!!user;return v1991BaseCanOpenView(target,user)};
if(typeof V173_MORE_TOOLS==='object')V173_MORE_TOOLS['test-route']=['?','New User Guide','A guided tour based on this user’s permissions'];
const v1991BaseMoreToolIds=v173MoreToolIds;
v173MoreToolIds=function(user=currentUser()){let ids=v1991BaseMoreToolIds(user);if(db.demoProfile&&user&&!ids.includes('test-route'))ids.unshift('test-route');return ids};

let v1991FocusBeforeModal=null;
function v1991CloseModal(){if(typeof dirty!=='undefined'&&dirty&&typeof confirmLeave==='function'&&!confirmLeave())return;modal=null;render()}
function v1991EnhanceRenderedUi(root=document){
  if(!root?.querySelectorAll)return;
  root.querySelectorAll('[onclick]').forEach(element=>{if(['BUTTON','A','INPUT','SELECT','TEXTAREA','SUMMARY','LABEL'].includes(element.tagName)||element.dataset.v1991Keyboard==='1')return;element.tabIndex=element.tabIndex>=0?element.tabIndex:0;element.setAttribute('role',element.getAttribute('role')||'button');element.dataset.v1991Keyboard='1';element.addEventListener('keydown',event=>{if((event.key==='Enter'||event.key===' ')&&!event.target.closest('button,a,input,select,textarea,summary')){event.preventDefault();element.click()}})});
  root.querySelectorAll('.modal').forEach((dialog,index)=>{let box=dialog.querySelector('.modalbox')||dialog;dialog.setAttribute('role','dialog');dialog.setAttribute('aria-modal','true');let heading=dialog.querySelector('h1,h2,h3');if(heading){heading.id=heading.id||`v1991-dialog-title-${index}`;dialog.setAttribute('aria-labelledby',heading.id)}if(!box.querySelector(':scope > .v1991-modal-close')){let close=document.createElement('button');close.type='button';close.className='v1991-modal-close';close.setAttribute('aria-label','Close dialog');close.textContent='×';close.addEventListener('click',v1991CloseModal);box.prepend(close)}if(dialog.dataset.v1991Dialog!=='1'){dialog.dataset.v1991Dialog='1';dialog.addEventListener('keydown',event=>{if(event.key==='Escape'){event.preventDefault();v1991CloseModal();return}if(event.key!=='Tab')return;let focusable=[...dialog.querySelectorAll('button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')].filter(element=>element.offsetParent!==null);if(!focusable.length)return;let first=focusable[0],last=focusable.at(-1);if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus()}else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus()}})}setTimeout(()=>{if(!dialog.contains(document.activeElement)){let target=dialog.querySelector('input:not([type="hidden"]):not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not(.v1991-modal-close):not([disabled]),.v1991-modal-close');target?.focus()}},0)})
}

// Apply current training state before every page render, and position the guide
// arrow after the real page has been mounted.
const v199BaseRender=render;
render=function(){let opening=!!modal&&!document.querySelector?.('.modal'),closing=!modal&&!!document.querySelector?.('.modal');if(opening)v1991FocusBeforeModal=document.activeElement;v199ApplyTrainingPreference();let result;if(currentUser()&&view==='test-route'){shell(v1991TestRoutePage());updateSyncUI()}else result=v199BaseRender();v1991EnhanceRenderedUi(document);if(closing&&v1991FocusBeforeModal?.isConnected)setTimeout(()=>v1991FocusBeforeModal.focus(),0);if(closing)v1991FocusBeforeModal=null;if(guideActive)setTimeout(v199PositionGuideArrow,0);return result};

addSite=function(){if(!hasPerm('manage_sites'))return toast('You do not have Site admin access');modal=`<div class="modal"><div class="modalbox v1991-add-site"><span class="att-kicker">SITE SETUP</span><h2>Add Site</h2><p class="muted">Enter this development’s normal lift stages now. They are only a starting template for new plots and can be changed on any individual plot.</p><div class="field"><label>Site / development name</label><input id="new-site-name" class="input" placeholder="e.g. Oak View"></div><div class="field"><label>Default lift stages <span class="muted">(one per line)</span></label><textarea id="v1991-new-site-stages" class="textarea">1st Lift\n2nd Lift (Joist)\n3rd Lift (Band)\n4th Lift (Top/Plate)\n5th Lift (Gables)</textarea></div><div class="actions"><button class="btn orange" onclick="saveNewSite()">Create Site</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render()};
saveNewSite=function(){if(!hasPerm('manage_sites'))return toast('You do not have Site admin access');let name=(document.getElementById('new-site-name')?.value||'').trim(),stages=(document.getElementById('v1991-new-site-stages')?.value||'').split(/\n+/).map(value=>value.trim()).filter(Boolean);if(!name)return toast('Enter the Site name');if(!stages.length)return toast('Enter at least one normal lift stage');if(db.sites.some(row=>!row.archived&&String(row.name||'').trim().toLowerCase()===name.toLowerCase()))return toast('A Site with that name already exists');let row={id:'s'+Date.now(),name,archived:false,createdAt:new Date().toISOString(),createdBy:currentUser()?.name||'',timeZone:'Europe/London',stageTemplate:stages,attendanceConfig:{enabled:true,latitude:null,longitude:null,radiusM:250,geofenceZones:[],maxAccuracyM:75,maxShiftHours:12,expectedStartTime:'07:30',expectedEndTime:'16:30',lateArrivalGraceMinutes:0,earlyLeaveGraceMinutes:5,autoClockOutGps:true,timesheetDefaultPreset:'week',nativeSecurityMode:'browser-allowed',analyticsManualMinutes:8,analyticsSystemMinutes:1,analyticsAdminHourlyCost:25}};db.sites.push(row);save();audit('site','site',row.id,`Site created — ${name} · ${stages.length} lift stages`,row.id);modal=null;savedState('Site saved');render()};

function v1991OpenPrintPage(title,body){let page=window.open('','_blank');if(!page)return toast('Allow pop-ups to print this record');page.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>${v195Esc(title)}</title><style>body{font:14px Arial;padding:28px;color:#17384b}h1{margin-bottom:3px}table{width:100%;border-collapse:collapse;margin-top:20px}th,td{padding:9px;border:1px solid #ccd7dd;text-align:left}th{background:#eef3f6}.ok{font-weight:bold}.note{color:#667b86}</style></head><body>${body}<script>window.onload=()=>window.print()<\/script></body></html>`);page.document.close()}
printAttendanceRegister=function(siteId){let active=attendanceExpectedMembers(siteId).map(row=>({...row,shift:attendanceActiveShift(row.member.id)})).filter(row=>row.shift?.siteId===siteId),rows=active.map(row=>`<tr><td><b>${v195Esc(row.member.name)}</b></td><td>${v195Esc(row.gang.name)}</td><td>${new Date(row.shift.clockIn.time).toLocaleString()}</td><td class="ok">${v195Esc(row.shift.clockIn.status)}${row.shift.clockIn.distanceM!==undefined?' · '+Math.round(row.shift.clockIn.distanceM)+'m from Site':''}</td></tr>`).join('');v1991OpenPrintPage('SiteSight Emergency Register',`<h1>SiteSight — Who is on Site</h1><p>${v195Esc(siteName(siteId))} · Generated ${new Date().toLocaleString()}</p><table><tr><th>Person</th><th>Team</th><th>Clock In</th><th>Status</th></tr>${rows}</table>`)};
printAttendanceTimesheet=function(){let siteId=attendanceResolvedSite(),start=attendanceResolvedWeekStart(),financial=hasPerm('view_financials'),rows=attendanceVisibleMembers(siteId).map(row=>{let totals=attendanceWeekTotals(row.member.id,start,siteId),approval=attendanceApproval(row.member.id,siteId,start),approvalText=approval?(approval.needsReapproval?'RE-APPROVAL REQUIRED':'Approved · '+approval.approvedBy+' · '+new Date(approval.approvedAt).toLocaleString()):'Awaiting approval';return `<tr><td>${v195Esc(row.member.name)}</td><td>${v195Esc(row.gang.name)}</td><td>${totals.totalHours.toFixed(2)}</td><td>${totals.overtimeHours.toFixed(2)}</td>${financial?`<td>${attendanceMoney(totals.gross)}</td>`:''}<td>${v195Esc(approvalText)}</td></tr>`}).join('');v1991OpenPrintPage('SiteSight Weekly Timesheet',`<h1>SiteSight — Weekly Timesheet</h1><p>${v195Esc(siteName(siteId))} · ${v195Esc(attendanceWeekLabel(start))}</p><table><tr><th>Person</th><th>Team</th><th>Hours</th><th>Overtime</th>${financial?'<th>Estimated Gross</th>':''}<th>Approval</th></tr>${rows}</table><p class="note">Estimated pay is a management calculation, not payroll, tax or statutory wage processing.</p>`)};

// Release identity and boot. V1.97 no longer boots before this final layer.
const v199BaseShell=shell;
shell=function(content){v199BaseShell(content);let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace(/v1\.97\.0 · DESKTOP CLARITY(?: DEMO)?/g,`v1.99.2 · USABILITY TEST${root.innerHTML.includes('demo-mode-banner')?' DEMO':''}`)};

/* =====================================================================
   SiteSight V1.99.2 — usability-first personal test
   A single final interaction layer: shorter role navigation, a focused
   daily overview, a clearer plot page, compact help and responsive records.
   Saved user-entered wording is never rewritten by this layer.
   ===================================================================== */

const V1992_PAYMENT_PAGE_SIZE=25;
let v1992PaymentPage=1,v1992PaymentInitialisedFor='',v1992CalendarMonthOpen=false;

// ---- Role-first navigation -------------------------------------------
const V1992_ROLE_NAV={
  'Director':['dashboard','work-items','attendance','commercial'],
  'Contracts Manager':['dashboard','work-items','attendance','commercial'],
  'Site Manager':['dashboard','work-items','issues','attendance'],
  'Supervisor':['work-items','issues','attendance','diary'],
  'Quantity Surveyor':['commercial','site-drawings','documents'],
  'Accounts':['commercial','documents'],
  'Read Only':['dashboard','site-drawings','calendar'],
  'Operator':['attendance']
};
const V1992_NAV_META={
  dashboard:{icon:'⌂',label:user=>user?.role==='Site Manager'?'Today':'Overview'},
  'work-items':{icon:'▦',label:user=>user?.role==='Quantity Surveyor'?'Work Evidence':'Plots & Work'},
  attendance:{icon:'◷',label:user=>user?.role==='Operator'?'My Time':user?.role==='Accounts'?'Time':user?.role==='Quantity Surveyor'?'Work Evidence':'People & Timesheets'},
  commercial:{icon:'£',label:'Commercial'},issues:{icon:'⚠',label:'Issues'},diary:{icon:'✎',label:'Site Diary'},
  'site-drawings':{icon:'▤',label:'Drawings'},documents:{icon:'▥',label:'Documents'},calendar:{icon:'□',label:'Calendar'},
  'my-work':{icon:'▦',label:'My Work'}
};

Object.assign(V173_MAIN_NAV,V1992_NAV_META);
Object.assign(V173_MORE_TOOLS,{
  dashboard:['⌂','Overview','Current Site or company position'],
  'work-items':['▦','Plots & Work','Plot progress, evidence and job-specific records'],
  attendance:['◷','People & Timesheets','Clocking, attendance, hours and approvals'],
  gangs:['◎','People','People, roles and linked accounts'],
  analytics:['↗','Analytics','Management summaries and trends'],
  'data-quality':['✓','Missing Information','Incomplete setup and records requiring review'],
  'site-drawings':['▤','Drawings','Current and superseded Site drawings'],
  documents:['▥','Documents','Controlled files, folders and revisions'],
  functionality:['⚙','Site Settings','Site hours, clocking, lift templates and rules'],
  guide:['?','Need help?','Screen help, hints and a short tour']
});

for(const [roleName,ids] of Object.entries(V1992_ROLE_NAV)){
  let profile=V173_ROLE_PROFILES[roleName];if(!profile)continue;
  profile.main=ids.slice();profile.home=ids[0]||'more';
}

v173HomeView=function(user=currentUser()){
  if(!user)return'more';let ids=v1998OperatorHasWork(user)?['my-work','attendance','issues']:(V1992_ROLE_NAV[user.role]||[]);
  return ids.find(id=>v173CanOpenView(id,user))||'more'
};
v173RoleNavItems=function(user=currentUser()){
  if(!user)return[];let ids=v1998OperatorHasWork(user)?['my-work','attendance','issues']:(V1992_ROLE_NAV[user.role]||[]);return ids.filter(id=>v173CanOpenView(id,user)).map(id=>({id,...V1992_NAV_META[id]}))
};
v173NavButton=function(item,user=currentUser()){
  let label=typeof item.label==='function'?item.label(user):item.label,issueCount=item.id==='issues'?scopeItems().reduce((sum,row)=>sum+issues(row),0):0;
  return `<button class="${v173NavActive(item.id)?'active':''}" onclick="goNav('${item.id}')"><span aria-hidden="true">${item.icon}</span><b>${label}</b>${issueCount?`<span class="count">${issueCount}</span>`:''}</button>`
};
function v1992HasMore(user=currentUser()){return !!user&&(user.role!=='Operator'||v1998OperatorHasWork(user))&&v1992MoreToolIds(user).length>0}
nav=function(){let user=currentUser(),items=v173RoleNavItems(user),primary=new Set(items.map(row=>row.id)),moreActive=view==='more'||(!primary.has(view)&&!selected);return items.map(row=>v173NavButton(row,user)).join('')+(v1992HasMore(user)?`<button class="${moreActive?'active':''}" onclick="goNav('more')"><span aria-hidden="true">•••</span><b>More</b></button>`:'')};
mobileNav=function(){return nav()};
selectedSiteHeading=function(){let label=site==='all'?'All permitted Sites':siteName(site);return `<div class="site-context-title">Viewing: <b>${label}</b>${site!=='all'?` <button onclick="setSiteContext('all')">× Show all permitted Sites</button>`:''}</div>`};

function v1992MoreToolIds(user=currentUser()){
  if(!user)return[];let primary=new Set(v173RoleNavItems(user).map(row=>row.id)),candidates=['weekly-report','analytics','data-quality','site-drawings','documents','calendar','diary','dayworks','audit','gangs','admin','functionality','test-route','about'];
  return candidates.filter((id,index)=>candidates.indexOf(id)===index&&!primary.has(id)&&v173CanOpenView(id,user))
}
v173MoreToolIds=v1992MoreToolIds;
function v1992MoreCard(id){let row=V173_MORE_TOOLS[id]||['□',id,''];return `<button class="v1992-more-card" onclick="goNav('${id}')"><span aria-hidden="true">${row[0]}</span><span><b>${row[1]}</b><small>${row[2]}</small></span><strong>Open ›</strong></button>`}
morePage=function(){
  let ids=v1992MoreToolIds(),groups=[
    ['Reports',['weekly-report','analytics','data-quality','payments-history']],
    ['Documents and records',['site-drawings','documents','calendar','diary','dayworks','audit']],
    ['Settings and administration',['gangs','admin','functionality']],
    ['Help',['test-route','about']]
  ];
  return `<div class="topbar"><div><h1>More</h1><div class="sub">Occasional tools, grouped by purpose</div></div></div><div class="v1992-more-groups">${groups.map(([title,tools])=>{let visible=tools.filter(id=>ids.includes(id));if(!visible.length)return'';return `<section class="panel"><h2>${title}</h2><div>${visible.map(v1992MoreCard).join('')}</div></section>`}).join('')}<section class="panel v1992-more-help"><h2>Need help?</h2><p>Get a short explanation, show the next useful control, start a tour or change hints.</p><button class="btn light" onclick="v1992OpenHelp()">Open help</button></section></div>`
};

// ---- Short, combined help -------------------------------------------
function v1992ScreenExplanation(){let roleName=currentUser()?.role||'user',labels={dashboard:'This is your starting point. Check the Site progress, today’s numbers and the first few items needing attention.', 'work-items':'Use the Site filter or plot search, then choose Quick update to change progress, add a photo or raise an issue.',issues:'Open an issue to see its owner, priority, target date, evidence and resolution controls.',attendance:roleName==='Operator'?'Use the large Clock In or Clock Out action, check the Site name, then review this week and your recent shifts.':'Use Today for the live Site position and Timesheets for hours, corrections and approvals.',commercial:'Choose the commercial tab for the exact job: pay run, payment history, CIS, saved checks or reconciliation.','site-drawings':'Search current drawings first. Superseded drawings remain available only when you deliberately include them.',documents:'Open a folder or search for the current controlled document and its revision history.',calendar:'On a phone, start with the agenda. Open the month view only when you need to scan dates.',diary:'Add one factual daily record for work, people, deliveries, delays, notes and photos.',more:'Occasional tools are grouped here so the main menu stays short.'};return labels[view]||`This screen contains the ${assistPageLabel()} tools available to your account.`}
function v1992HelpTarget(){return ({dashboard:'Update a plot','work-items':'Quick update',issues:'Open',attendance:currentUser()?.role==='Operator'?'CLOCK':'Today',commercial:'Payment History','site-drawings':'Search',documents:'Search',calendar:'Agenda',diary:'Add Site Diary Entry',more:'Reports'})[view]||''}
function v1992ShowWhere(){let label=v1992HelpTarget();modal=null;render();setTimeout(()=>label?assistHighlight(label):toast('Use the first large action on this screen.'),80)}
function v1992StartShortTour(){modal=null;startGuidedTour()}
function v1992OpenHelp(){settingsOpen=false;modal=`<div class="modal"><div class="modalbox v1992-help-modal"><span class="att-kicker">NEED HELP?</span><h2>${v195Esc(assistPageLabel())}</h2><p>${v195Esc(v1992ScreenExplanation())}</p><div class="v1992-help-actions"><button onclick="modal=null;render()"><span>i</span><b>Explain this screen</b><small>The short explanation above is all you need to get started.</small></button><button onclick="v1992ShowWhere()"><span>→</span><b>Show me where to click</b><small>Highlight the next useful control on this screen.</small></button><button onclick="modal=null;openUserGuide(false)"><span>▶</span><b>New User Guide</b><small>Tour every screen available to this account.</small></button><button onclick="toggleTrainingMode(!trainingMode)"><span>?</span><b>Turn hints ${trainingMode?'off':'on'}</b><small>Hints are currently ${trainingMode?'shown':'hidden'}.</small></button></div><button class="btn light" onclick="modal=null;render()">Close</button></div></div>`;render()}
assistOpenPanel=v1992OpenHelp;
assistMount=function(){
  document.getElementById('sitesight-assist-panel')?.remove();document.getElementById('sitesight-assist-scrim')?.remove();assistOpen=false;
  if(!currentUser())return;let button=document.getElementById('sitesight-assist-fab');if(!button){button=document.createElement('button');button.id='sitesight-assist-fab';button.className='assist-fab';button.innerHTML='<span>?</span><b>Need help?</b>';button.onclick=v1992OpenHelp;document.body.appendChild(button)}
};
guideSteps=function(){let user=currentUser(),items=v173RoleNavItems(user).slice(0,4);if(!items.length)items=[{id:'more',icon:'?',label:'More'}];return items.map(item=>{let label=typeof item.label==='function'?item.label(user):item.label;return {id:item.id,icon:item.icon,title:label,target:item.id,summary:v1992ScreenExplanationFor(item.id),bullets:[`Open ${label} from the main menu.`,`Use the largest labelled action first.`,`Look for the saved confirmation before leaving.`]}})};
function v1992ScreenExplanationFor(target){let old=view;view=target;let text=v1992ScreenExplanation();view=old;return text}
newUserGuidePage=function(){let user=currentUser(),steps=guideSteps(),first=guideFirstRun;return `<div class="topbar"><div><span class="att-kicker">${first?'WELCOME':'SHORT GUIDE'}</span><h1>${first?`Welcome, ${v195Esc(String(user?.name||'').split(' ')[0]||'there')}`:'Using SiteSight'}</h1><div class="sub">Learn the few areas that matter to your role; the rest can wait.</div></div><button class="btn light" onclick="closeUserGuide()">Close</button></div><section class="panel v1992-guide-intro"><h2>The same three steps everywhere</h2><div><span><b>1</b> Choose an area</span><span><b>2</b> Open one record</span><span><b>3</b> Save and check for ✓</span></div></section><section class="panel v1992-guide-list"><div class="sectionhead"><div><h2>Your ${v195Esc(user?.role||'')} route</h2><p class="muted">Only your main destinations are shown.</p></div></div>${steps.map((step,index)=>`<button onclick="guideStep=${index};guideActive=true;guideFirstRun=false;view='${step.target}';render()"><span>${step.icon}</span><span><b>${v195Esc(step.title)}</b><small>${v195Esc(step.summary)}</small></span><strong>Open ›</strong></button>`).join('')}<div class="actions"><button class="btn orange" onclick="startGuidedTour()">Start short tour</button>${first?'<button class="btn light" onclick="skipUserGuide()">I’ll explore myself</button>':''}</div></section>`};

// ---- Focused overview -----------------------------------------------
function v1992DashboardItems(siteId=site){return (db.items||[]).filter(item=>canViewItem(item)&&(siteId==='all'||item.site===siteId))}
function v1992ClockedIn(siteId='all'){return (db.attendanceShifts||[]).filter(shift=>!shift.voided&&!shift.clockOut?.time&&canAccessSite(shift.siteId)&&!db.sites.find(row=>row.id===shift.siteId)?.archived&&(siteId==='all'||shift.siteId===siteId)).length}
function v1992ExpectedPeople(siteId='all'){if(siteId!=='all')return attendanceExpectedMembers(siteId).length;let accessibleGangIds=new Set(v1992DashboardItems('all').filter(item=>item.gang).map(item=>item.gang));return attendanceAllMembers().filter(row=>row.member.activeForAttendance!==false&&row.member.employmentStatus!=='Leaver'&&(accessibleGangIds.has(row.gang.id)||(attendanceActiveShift(row.member.id)&&canAccessSite(attendanceActiveShift(row.member.id).siteId)))).length}
function v1992SiteExceptions(siteId){return v1992DashboardItems(siteId).filter(item=>userAttentionText(item)).length}
function v1992SiteSelector(){let sites=activeSites().filter(row=>canAccessSite(row.id));return `<select class="select" aria-label="Choose Site" onchange="setSiteContext(this.value)"><option value="all">All permitted Sites</option>${sites.map(row=>`<option value="${row.id}" ${site===row.id?'selected':''}>${v195Esc(row.name)}</option>`).join('')}</select>`}
function v1992DirectorOverview(){let rows=v181AccessibleSites().map(v181SiteRow);return `<div class="topbar"><div><span class="att-kicker">COMPANY OVERVIEW</span><h1>Overview</h1><div class="sub">Choose a Site to open its daily position.</div></div>${v1992SiteSelector()}</div><section class="v1992-site-grid">${rows.map(row=>{let clocked=v1992ClockedIn(row.site.id),exceptions=v1992SiteExceptions(row.site.id);return `<button class="v1992-site-card" onclick="setSiteContext('${row.site.id}')"><span><b>${v195Esc(row.site.name)}</b><small>${row.items.length} plots · ${row.inProgress} active</small></span><strong>${row.progress}%</strong><i><em style="width:${row.progress}%"></em></i><dl><div><dt>Clocked in</dt><dd>${clocked}</dd></div><div><dt>Open issues</dt><dd>${row.openIssues}</dd></div><div class="${exceptions?'warn':''}"><dt>Exceptions</dt><dd>${exceptions}</dd></div></dl><span class="v1992-open-site">Open Site ›</span></button>`}).join('')||smartEmpty('No permitted Sites.','Ask a Director to check your Site access.','','','⌂')}</section>`}
function v1992ChoosePlotAction(kind){let rows=v1992DashboardItems().filter(item=>kind==='issue'?hasPerm('raise_issues'):hasPerm('update_progress')||hasPerm('manage_workitems'));if(!rows.length)return toast('No accessible plots are available');let label=kind==='progress'?'Update progress':kind==='photo'?'Add photo':'Raise issue';modal=`<div class="modal"><div class="modalbox v1992-plot-picker"><h2>${label}</h2><p class="muted">Choose the plot or work record first.</p><div>${rows.slice(0,60).map(item=>`<button onclick="v1992RunPlotAction('${kind}','${item.id}')"><span><b>${v195Esc(item.ref)}</b><small>${v195Esc(siteName(item.site))} · ${v195Esc(v1992CurrentStage(item).label)}</small></span><strong>Choose ›</strong></button>`).join('')}</div><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div>`;render()}
function v1992RunPlotAction(kind,id){let item=db.items.find(row=>row.id===id),allowed=kind==='issue'?canViewItem(item)&&hasPerm('raise_issues'):canUpdateItem(item);if(!allowed)return toast('Plot cards and progress actions are available to Supervisors and above');selected=id;view='work-items';modal=null;render();setTimeout(()=>kind==='progress'?quickProgress(id):kind==='photo'?addWorkPhotos():addIssue(),30)}
function v1992QuickActions(){let actions=[];if(hasPerm('update_progress')||hasPerm('manage_workitems'))actions.push(`<button onclick="v1992ChoosePlotAction('progress')"><span>◐</span><b>Update a plot</b></button>`,`<button onclick="v1992ChoosePlotAction('photo')"><span>□</span><b>Take photo</b></button>`);if(hasPerm('raise_issues'))actions.push(`<button onclick="v1992ChoosePlotAction('issue')"><span>!</span><b>Raise issue</b></button>`);if(hasPerm('manage_site_diary'))actions.push(`<button onclick="addDiaryEntry()"><span>✎</span><b>Complete Site Diary</b></button>`);return actions.length?`<section class="v1992-quick-actions" aria-label="Quick actions">${actions.slice(0,4).join('')}</section>`:''}
function v1992SiteOverview(){let siteId=site,items=v1992DashboardItems(siteId),active=items.filter(item=>overall(item)==='In progress').length,openIssues=items.reduce((sum,item)=>sum+issues(item),0),clocked=v1992ClockedIn(siteId),expected=v1992ExpectedPeople(siteId),progress=items.length?Math.round(items.reduce((sum,item)=>sum+pct(item),0)/items.length):0,attention=items.filter(item=>userAttentionText(item)),visiblePlots=(attention.length?attention:items).slice(0,10),name=siteId==='all'?'All permitted Sites':siteName(siteId),user=currentUser();
  return `<div class="topbar"><div><span class="att-kicker">${user?.role==='Site Manager'?'TODAY':'SITE OVERVIEW'}</span><h1>${user?.role==='Site Manager'?'Today':'Overview'}</h1><div class="sub">${new Date().toLocaleDateString('en-GB',{weekday:'long',day:'numeric',month:'long',year:'numeric'})}</div></div>${v1992SiteSelector()}</div><section class="v1992-site-progress"><div><span>CURRENT SITE</span><h2>${v195Esc(name)}</h2><p>${items.length} plot / work record${items.length===1?'':'s'} in this view</p></div><strong>${progress}%<small>overall completion</small></strong><i><em style="width:${progress}%"></em></i></section>${v1992QuickActions()}<section class="v1992-today-metrics"><button onclick="goNav('attendance')"><span>Clocked in</span><b>${clocked}</b></button><button onclick="goNav('attendance')"><span>Expected</span><b>${expected}</b></button><button onclick="goNav('work-items')"><span>Active plots</span><b>${active}</b></button><button onclick="goNav('issues')"><span>Open issues</span><b>${openIssues}</b></button></section>${items.length?`<section class="panel v1998-dashboard-plots"><div class="sectionhead"><div><h2>${attention.length?'Plots needing attention':'Plot cards'}</h2><p class="muted">Status, current stage and next actions stay visible without opening a list.</p></div>${items.length>visiblePlots.length?'<button class="btn light smallbtn" onclick="goNav(\'work-items\')">View all</button>':''}</div><div class="cards v1998-dashboard-plot-grid">${visiblePlots.map(card).join('')}</div></section>`:smartEmpty('No permitted plots in this view.','Ask a Director to check Site and plot permissions.','','','▣')}<button class="btn light v1992-view-plots" onclick="goNav('work-items')">View all plots and work ›</button>`
}
dashboard=function(){let user=currentUser();return site==='all'&&['Director','Contracts Manager'].includes(user?.role)&&v181AccessibleSites().length>1?v1992DirectorOverview():v1992SiteOverview()};

// ---- Plot cards and plot detail -------------------------------------
function v1992CurrentStage(item){if(item?.type==='House Plot'){let lifts=item.lifts||[],row=lifts.find(lift=>lift.status!=='Complete')||lifts.at(-1);return {label:row?`${row.name} · ${row.status}`:'No lift stages',complete:!!lifts.length&&lifts.every(lift=>lift.status==='Complete')}}return {label:`${item?.type||'Work'} · ${overall(item)}`,complete:['Complete','Signed Off'].includes(overall(item))}}
const v1992BaseCard=card;
card=function(item){let html=v1992BaseCard(item),stage=v1992CurrentStage(item);return html.replace('<div class="progress-label">',`<div class="v1992-current-stage ${stage.complete?'complete':''}"><span>Current</span><b>${v195Esc(stage.label)}</b></div><div class="progress-label">`).replace(/⚡ Quick update/g,'Quick update').replace(/<span>♙ /g,'<span>Person · ')};
function v1992PlotHistory(item){let rows=(db.auditLog||[]).filter(row=>row.entityId===item.id||String(row.summary||'').includes(item.ref)).slice(0,10);return `<details class="work-section-card v1992-history"><summary><span><b>History</b><small>${rows.length} recent change${rows.length===1?'':'s'}</small></span><strong>Open ▾</strong></summary><div>${rows.map(row=>`<p><b>${v195Esc(row.summary||row.action||'Record updated')}</b><small>${row.createdAt?new Date(row.createdAt).toLocaleString('en-GB'):''} · ${v195Esc(row.userName||'SiteSight')}</small></p>`).join('')||'<p class="muted">No recent history is available.</p>'}</div></details>`}
const v1992BaseDetail=detail;
detail=function(item){let html=v1992BaseDetail(item),stage=v1992CurrentStage(item),openIssues=issues(item),actions=[];if(hasPerm('update_progress')||hasPerm('manage_workitems'))actions.push(`<button class="btn orange" onclick="quickProgress('${item.id}')"><span>◐</span>Update Progress</button>`,`<button class="btn light" onclick="addWorkPhotos()"><span>□</span>Add Photo</button>`);if(hasPerm('raise_issues'))actions.push(`<button class="btn light" onclick="addIssue()"><span>!</span>Raise Issue</button>`);let hero=`<section class="v1992-plot-hero"><div><span>${v195Esc(item.type)} · ${v195Esc(siteName(item.site))}</span><h1>${v195Esc(item.ref)}</h1><p>${item.houseType?v195Esc(item.houseType)+' · ':''}${pct(item)}% complete</p></div><div class="v1992-plot-now ${openIssues?'warn':''}"><span>CURRENT</span><b>${v195Esc(stage.label)}</b>${openIssues?`<small>⚠ ${openIssues} open issue${openIssues===1?'':'s'}</small>`:'<small>✓ No open issues</small>'}</div><div class="v1992-plot-actions">${actions.join('')}</div></section>`;html=html.replace('<div class="detailhead">','<div class="detailhead">'+hero).replace('<h3>Main Work</h3>',`<h3>${item.type==='House Plot'?'Lift stages':'Work details'}</h3>`).replace('<h3>Plot Drawings</h3>','<h3>Drawings</h3>').replace('Add from Drawing Library','Add from Drawings');let end=html.lastIndexOf('</div></div>');return end<0?html+v1992PlotHistory(item):html.slice(0,end)+v1992PlotHistory(item)+html.slice(end)};

// ---- Clear terminology without touching saved content ---------------
const v1992BaseWorkItems=workItems;
workItems=function(){return v1992BaseWorkItems().replace('<h1>Work Items</h1>','<h1>Plots & Work</h1>').replace(/Add work item/g,'Add plot / work').replace('Work record date range','Plot / work date range')};
let v1992WorkGroupsOpen={},v1992WorkGroupLimits={};
function v1992WorkGroupKey(title){return String(title||'work').toLowerCase().replace(/\s+/g,'-')}
function v1992ToggleWorkGroup(key){v1992WorkGroupsOpen[key]=!v1992WorkGroupsOpen[key];render()}
function v1992ShowMoreWork(key){v1992WorkGroupLimits[key]=Number(v1992WorkGroupLimits[key]||8)+8;render()}
v188WorkGroup=function(title,rows,open=false){let key=v1992WorkGroupKey(title),expanded=v1992WorkGroupsOpen[key]===undefined?!!open:!!v1992WorkGroupsOpen[key],limit=Number(v1992WorkGroupLimits[key]||8),shown=expanded?rows.slice(0,limit):[],remaining=Math.max(0,rows.length-shown.length);return `<section class="v188-work-group v1992-work-group ${key} ${expanded?'open':''}"><button class="v1992-work-group-head" onclick="v1992ToggleWorkGroup('${key}')"><span>${icon(title)} <b>${title}</b></span><strong>${rows.length}</strong><em>${expanded?'Hide':'Show'} ${expanded?'▲':'▼'}</em></button>${expanded?`<div class="v188-work-group-body">${shown.length?`<div class="cards">${shown.map(card).join('')}</div>`:smartEmpty(`No ${title.toLowerCase()} work in this view.`,'','','','✓')}${remaining?`<button class="btn light v1992-load-more" onclick="v1992ShowMoreWork('${key}')">Show 8 more · ${remaining} remaining</button>`:''}</div>`:''}</section>`};
const v1992BaseGangsPage=gangsPage;
gangsPage=function(){return v1992BaseGangsPage().replace('<h1>Individuals</h1>','<h1>People</h1>').replace(/Add Individual/g,'Add person').replace(/Former people/g,'Former people')};
const v1992BaseAdminPage=adminPage;
adminPage=function(){return v1992BaseAdminPage().replace(/>Individuals<\/button>/g,'>People</button>').replace('<h2>Individuals</h2>','<h2>People</h2>').replace(/Add Individual/g,'Add person')};
const v1992BaseDataQualityPage=v195DataQualityPage;
v195DataQualityPage=function(){return v1992BaseDataQualityPage().replace('<h1>Data Quality</h1>','<h1>Missing Information</h1>').replace(/data check/g,'information check')};
const v1992BaseFunctionalityPage=v189FunctionalityPage;
v189FunctionalityPage=function(){return v1992BaseFunctionalityPage().replace('<h1>Functionality</h1>','<h1>Site Settings</h1>').replace('SITE FUNCTIONALITY','SITE SETTINGS')};
const v1992BaseSiteDrawingsPage=siteDrawingsPage;
siteDrawingsPage=function(){return v1992BaseSiteDrawingsPage().replace('<h1>Drawing Library</h1>','<h1>Drawings</h1>').replace(/Drawing Library holds/g,'Drawings holds')};
const v1992BaseAttendancePage=attendancePage;
attendancePage=function(){let html=v1992BaseAttendancePage(),user=currentUser();if(user?.role==='Operator')return html.replace(/<h1>Clock In \/ Out<\/h1>|<h1>Time & Attendance<\/h1>/g,'<h1>My Time</h1>');return html.replace(/<h1>People & Time<\/h1>|<h1>Time & Attendance<\/h1>/g,'<h1>People & Timesheets</h1>')};
const v1992BaseMyWorkPage=v195MyWorkPage;
v195MyWorkPage=function(){return v1992BaseMyWorkPage().replace(/<button class="btn blue" onclick="goNav\('clock'\)">[\s\S]*?Clock In \/ Out<\/button>/,'')};

// ---- Paginated payments --------------------------------------------
function v1992InitialisePaymentRange(){let id=currentUser()?.id||'';if(v1992PaymentInitialisedFor===id)return;v1992PaymentInitialisedFor=id;v1992PaymentPage=1;if(!paymentFrom&&!paymentTo){let now=new Date();paymentFrom=iso(new Date(now.getFullYear(),now.getMonth(),1));paymentTo=iso(now)}}
function v1992PaymentReset(){paymentPayeeFilter='all';v183PaymentRole='all';paymentSearch='';setPaymentDatePreset('month');v1992PaymentPage=1}
function v1992PaymentPageChange(page,max){v1992PaymentPage=Math.max(1,Math.min(max,page));render();setTimeout(()=>document.querySelector('.v1992-payment-records')?.scrollIntoView({behavior:'smooth',block:'start'}),30)}
paymentsHistoryPage=function(){
  if(!hasPerm('view_financials'))return smartEmpty('Commercial permission required.','Ask a Director to check your permissions.','','','£');v1992InitialisePaymentRange();let invalid=paymentFrom&&paymentTo&&paymentFrom>paymentTo,rows=invalid?[]:filteredPaymentRows(),total=rows.reduce((sum,row)=>sum+Number(row.amount||0),0),pages=Math.max(1,Math.ceil(rows.length/V1992_PAYMENT_PAGE_SIZE));v1992PaymentPage=Math.min(v1992PaymentPage,pages);let start=(v1992PaymentPage-1)*V1992_PAYMENT_PAGE_SIZE,shown=rows.slice(start,start+V1992_PAYMENT_PAGE_SIZE),preset=paymentPresetState(),rowHtml=shown.map(row=>`<tr><td>${row.date||'—'}</td><td>${row.paymentType==='Variation'?'Variation':'Original work'}</td><td>${v195Esc(siteName(row.item.site))}</td><td>${v195Esc(row.item.ref)}</td><td>${v195Esc(row.paymentType==='Variation'?(row.variationName||row.stage||'Variation'):(row.stage||'—'))}</td><td><b>${v195Esc(paymentPayeeName(row))}</b></td><td>${row.pct||0}%</td><td><b>${money(row.amount||0)}</b></td><td>${v195Esc(row.enteredBy||'—')}</td><td>${v195Esc(row.notes||'')}</td></tr>`).join(''),cards=shown.map(row=>`<button class="v1992-payment-card" onclick="v199OpenWorkTarget('${row.item.id}')"><span><b>${v195Esc(row.item.ref)} · ${v195Esc(paymentPayeeName(row))}</b><small>${row.date||'No date'} · ${v195Esc(siteName(row.item.site))} · ${v195Esc(row.stage||row.variationName||'Payment')}</small></span><strong>${money(row.amount||0)}</strong></button>`).join('');
  return `<div class="topbar"><div><h1>Payment History</h1><div class="sub">Current month by default; use filters only when you need older records.</div></div>${v1992SiteSelector()}</div><div class="panel payment-filter-panel"><div class="payment-preset-row"><span>Quick range</span><button class="${preset==='week'?'active':''}" onclick="v1992PaymentPage=1;setPaymentDatePreset('week')">This week</button><button class="${preset==='fortnight'?'active':''}" onclick="v1992PaymentPage=1;setPaymentDatePreset('fortnight')">Fortnight</button><button class="${preset==='month'?'active':''}" onclick="v1992PaymentPage=1;setPaymentDatePreset('month')">This month</button><button class="${preset==='all'?'active':''}" onclick="v1992PaymentPage=1;setPaymentDatePreset('all')">All time</button></div><div class="payment-filter-grid v1992-payment-filters"><div class="field"><label>Paid to</label><select class="select" onchange="paymentPayeeFilter=this.value;v1992PaymentPage=1;render()">${paymentPayeeFilterOptions()}</select></div><div class="field"><label>User role</label><select class="select" onchange="v183PaymentRole=this.value;v1992PaymentPage=1;render()">${v183PaymentRoleOptions()}</select></div><div class="field"><label>From</label><input class="input" type="date" value="${paymentFrom}" onchange="paymentFrom=this.value;v1992PaymentPage=1;render()"></div><div class="field"><label>To</label><input class="input" type="date" value="${paymentTo}" onchange="paymentTo=this.value;v1992PaymentPage=1;render()"></div><div class="field"><label>Search</label><input class="input" placeholder="Plot, stage, notes or person…" value="${v195Esc(paymentSearch)}" oninput="paymentSearch=this.value;v1992PaymentPage=1;render()"></div><button class="btn light" onclick="v1992PaymentReset()">Reset</button></div>${invalid?'<div class="payment-range-warning">From date must be before To date.</div>':''}</div><section class="panel v1992-payment-records"><div class="sectionhead"><div><h2>Payment records</h2><p class="muted">${rows.length?`${start+1}–${Math.min(start+shown.length,rows.length)} of ${rows.length}`:'No'} matching record${rows.length===1?'':'s'}</p></div><b>${invalid?'—':money(total)+' total'}</b></div><div class="v1992-desktop-table"><table class="payment-history-table"><thead><tr><th>Date</th><th>Type</th><th>Site</th><th>Plot / work</th><th>Stage</th><th>Paid to</th><th>%</th><th>Amount</th><th>Entered by</th><th>Notes</th></tr></thead><tbody>${rowHtml||'<tr><td colspan="10">No payment records match this view.</td></tr>'}</tbody></table></div><div class="v1992-payment-cards">${cards||smartEmpty('No matching payments.','Change the range or clear a filter.','','','£')}</div><div class="v1992-pagination"><button class="btn light" ${v1992PaymentPage<=1?'disabled':''} onclick="v1992PaymentPageChange(${v1992PaymentPage-1},${pages})">← Previous</button><span>Page <b>${v1992PaymentPage}</b> of ${pages}</span><button class="btn light" ${v1992PaymentPage>=pages?'disabled':''} onclick="v1992PaymentPageChange(${v1992PaymentPage+1},${pages})">Next →</button></div></section>`
};

// ---- Mobile-first Calendar agenda ----------------------------------
function v1992CalendarAgenda(){let today=iso(),days=[];for(let offset=0;offset<31;offset++){let date=attendanceAddDays(today,offset),events=calendarDayEvents(date);if(events.length)days.push({date,events})}let rows=days.slice(0,10);return `<section class="panel v1992-calendar-agenda"><div class="sectionhead"><div><span class="att-kicker">NEXT 31 DAYS</span><h2>Agenda</h2><p class="muted">Upcoming Site notes, issues, completions and diary activity.</p></div><button class="btn light smallbtn" onclick="v1992CalendarMonthOpen=!v1992CalendarMonthOpen;render()">${v1992CalendarMonthOpen?'Hide month':'Show month'}</button></div>${rows.map(day=>`<button onclick="openCalendarDay('${day.date}')"><time>${new Date(day.date+'T12:00:00').toLocaleDateString('en-GB',{weekday:'short',day:'numeric',month:'short'})}</time><span><b>${day.events.length} item${day.events.length===1?'':'s'}</b><small>${v195Esc(day.events.slice(0,2).map(event=>event.title).join(' · '))}</small></span><strong>Open ›</strong></button>`).join('')||smartEmpty('Nothing scheduled in the next 31 days.','Calendar notes and issue target dates will appear here.','','','✓')}</section>`}
const v1992BaseCalendarPage=calendarPage;
calendarPage=function(){let html=v1992BaseCalendarPage().replace('<div class="panel full-calendar-panel">',`<div class="panel full-calendar-panel v1992-month-view ${v1992CalendarMonthOpen?'is-open':''}">`),panel=html.indexOf('<div class="panel');return panel<0?html+v1992CalendarAgenda():html.slice(0,panel)+v1992CalendarAgenda()+html.slice(panel)};

// ---- Simpler sign-in ------------------------------------------------
loginScreen=function(){let demo=!!db.demoProfile;document.getElementById('root').innerHTML=`<main class="v1992-login"><section><img src="assets/sitesight-login-logo.png" alt="SiteSight"><span class="auth-eyebrow">SITESIGHT</span><h1>Sign in</h1><p>Open the Site, plot or time record you need.</p><div class="field"><label>Username</label><input id="login-user" class="input" autocomplete="username" autofocus onkeydown="if(event.key==='Enter')document.getElementById('login-pass')?.focus()"></div><div class="field"><div class="auth-label-row"><label>Password</label><button class="auth-text-link" onclick="forgotPassword()">Forgot password?</button></div><div class="auth-input-wrap"><input id="login-pass" class="input auth-password-input" type="password" autocomplete="current-password" onkeydown="if(event.key==='Enter')login()"><button class="auth-password-toggle" onclick="toggleLoginPassword('login-pass',this)" type="button">Show</button></div></div><button class="btn orange v1992-login-button" onclick="login()">Sign in</button>${demo?`<details class="v1992-demo-logins"><summary>Demo test logins</summary><p>Use the role you want to test. Password for all: <b>Demo2026!</b></p><div><code>director</code><span>Director</span><code>site.manager</code><span>Site Manager</span><code>qs</code><span>Quantity Surveyor</span><code>worker.liam</code><span>Operator</span></div></details>`:''}<small class="v1992-login-note">Access is limited by role, individual permissions and permitted Sites.</small></section></main>`};

// ---- Demo routes now follow the simplified navigation ---------------
if(typeof V1991_TEST_ROUTES==='object'){
  V1991_TEST_ROUTES.Director=[['dashboard','Compare permitted Sites','Confirm completion, people, issues and exceptions are readable on each Site card.'],['work-items','Review Plots & Work','Open a plot and use its three large actions.'],['attendance','Review People & Timesheets','Check live attendance and a time approval.'],['commercial','Review Commercial','Trace a payment back to work evidence.'],['audit','Check accountability','Find the automatic record of a change.']];
  V1991_TEST_ROUTES['Contracts Manager']=[['dashboard','Compare permitted Sites','Open a Site card and confirm its completion and exceptions are clear.'],['work-items','Review Plots & Work','Find an active plot and confirm its current lift is visible.'],['attendance','Review People & Timesheets','Check the live Site position and one timesheet.'],['commercial','Review Commercial','Trace one figure back to its plot evidence.'],['guide','Use Need help','Confirm the screen explanation is brief and useful.']];
  V1991_TEST_ROUTES['Site Manager']=[['dashboard','Start on Today','Use one quick action and scan the five-item attention list.'],['work-items','Update a plot','Use Quick update for a real lift-stage change.'],['issues','Record a problem','Check owner, priority and target date.'],['attendance','Review People & Timesheets','Confirm only permitted Site people appear.'],['diary','Complete Site Diary','Enter job-specific wording and save it.']];
  V1991_TEST_ROUTES.Supervisor=[['work-items','Find today’s plots','Open the current plot without help and confirm the lift is obvious.'],['issues','Raise or review an issue','Check the wording, photos and responsibility are clear.'],['attendance','Review People & Timesheets','Confirm another Site’s people are unavailable.'],['diary','Record today’s position','Enter job-specific wording and confirm it remains unchanged.'],['guide','Use Need help','Ask the app to highlight the next useful control.']];
  V1991_TEST_ROUTES['Quantity Surveyor']=[['commercial','Review Commercial','Trace a figure back to its plot and progress evidence.'],['work-items','Check Work Evidence','Compare completed lifts with overall plot completion.'],['site-drawings','Review Drawings','Open a current drawing and identify superseded information.'],['documents','Check Documents','Open the current and previous document revisions.'],['guide','Use Need help','Confirm only the short QS route is shown.']];
  V1991_TEST_ROUTES.Accounts=[['commercial','Review Commercial','Confirm only authorised pay information is available.'],['documents','Find supporting Documents','Locate a permitted company or Site document.'],['audit','Trace a change','Confirm who changed a record and when.'],['guide','Use Need help','Confirm the explanation avoids construction jargon.']];
  delete V1991_TEST_ROUTES.Subcontractor;
  V1991_TEST_ROUTES['Read Only']=[['dashboard','Read the Overview','Confirm important information is visible without edit controls.'],['work-items','Inspect Plots & Work','Compare the percentage with completed lift stages.'],['site-drawings','Open Drawings','Confirm current revision status is obvious.'],['calendar','Review Calendar','Use the agenda to find an upcoming item.'],['guide','Use Need help','Confirm help does not reveal editing actions.']];
  V1991_TEST_ROUTES.Operator=[['attendance','Use My Time','Confirm the Site, clock action, week and recent shifts are clear on one screen.'],['guide','Use the short guide','Check location and Clock Out wording is simple.']];
}

// ---- Final DOM accessibility and responsive table enhancement -------
function v1992EnhanceRenderedUi(root=document){
  if(!root?.querySelectorAll)return;
  root.querySelectorAll('select option[value="all"]').forEach(option=>{if(/^\s*(?:⌖\s*)?all sites\s*$/i.test(option.textContent||''))option.textContent='All permitted Sites'});
  let settings=root.querySelector?.('.settingsmenu');if(settings){[...settings.querySelectorAll('button')].filter(button=>/Training mode|New User Guide/i.test(button.textContent||'')).forEach(button=>button.remove());if(![...settings.querySelectorAll('button')].some(button=>/Need help/i.test(button.textContent||''))){let button=document.createElement('button');button.innerHTML='? Need help';button.onclick=v1992OpenHelp;settings.insertBefore(button,settings.children[1]||null)}}
  root.querySelectorAll('.main table').forEach(table=>{let labels=[...table.querySelectorAll('thead th')].map(cell=>(cell.textContent||'').trim());if(!labels.length)return;table.classList.add('v1992-card-table');table.querySelectorAll('tbody tr').forEach(row=>[...row.children].forEach((cell,index)=>{if(labels[index])cell.dataset.label=labels[index]}))});
  let mobile=root.querySelector?.('.mobilebar'),count=mobile?.querySelectorAll?.('button')?.length||0;if(count&&mobile.style?.setProperty)mobile.style.setProperty('--v1992-nav-count',String(count));
  root.querySelectorAll('.nav button.active,.mobilebar button.active').forEach(button=>button.setAttribute('aria-current','page'));
}
const v1992BaseSavedState=savedState;
savedState=function(message='Saved'){let result=v1992BaseSavedState(message),row=document.getElementById('saved-toast');if(row){row.classList.add('v1992-saved');row.innerHTML=`<span>✓</span>${v195Esc(message)}`}return result};
const v1992BaseShell=shell;
shell=function(content){v1992BaseShell(content);let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace(/v1\.99\.1 · PERSONAL TEST(?: DEMO)?/g,`v1.99.2 · USABILITY TEST${db.demoProfile?' DEMO':''}`);v1992EnhanceRenderedUi(root)};
const v1992BaseRender=render;
render=function(){if(currentUser()&&view==='clock')view='attendance';let result=v1992BaseRender();v1992EnhanceRenderedUi(document);setTimeout(assistMount,0);return result};

/* =====================================================================
   SiteSight V1.99.3 — clearer payment control for personal testing
   ===================================================================== */

let v1993PaymentFiltersOpen=false,v1993PendingPayment=null,v1993PayPreferenceUser='';
const V1993_PAY_CADENCES=['week','fortnight','month'];

function v1993PayPreferenceKey(){let user=currentUser();return `sitesight-v1993-pay-cadence-${user?.id||user?.role||'guest'}`}
function v1993PayCadence(){let value='';try{value=localStorage.getItem(v1993PayPreferenceKey())||''}catch(error){}return V1993_PAY_CADENCES.includes(value)?value:'fortnight'}
function v1993RememberPayCadence(value){if(!V1993_PAY_CADENCES.includes(value))return;try{localStorage.setItem(v1993PayPreferenceKey(),value)}catch(error){}}
function v1993CurrentPayPeriod(cadence=v1993PayCadence()){
  let today=iso(),monday=attendanceWeekStartISO(new Date());
  if(cadence==='month')return {from:today.slice(0,8)+'01',to:v174MonthEnd(today)};
  if(cadence==='fortnight'){
    let anchor=new Date('2024-01-01T12:00:00'),weekStart=new Date(monday+'T12:00:00'),weeks=Math.floor((weekStart-anchor)/604800000);
    if(Math.abs(weeks)%2===1)monday=v174DateAdd(monday,-7);
    return {from:monday,to:v174DateAdd(monday,13)}
  }
  return {from:monday,to:v174DateAdd(monday,6)}
}
function v1993ApplySavedPayPeriod(){
  let id=currentUser()?.id||'';if(!id||v1993PayPreferenceUser===id)return;v1993PayPreferenceUser=id;let cadence=v1993PayCadence(),range=v1993CurrentPayPeriod(cadence);
  if(!v174PayFrom&&!v174PayTo){v174PayPreset=cadence;v174PayFrom=range.from;v174PayTo=range.to}
}
function v1993UseCurrentPayPeriod(scope='run',shouldRender=true){
  let cadence=v1993PayCadence(),range=v1993CurrentPayPeriod(cadence);v1993RememberPayCadence(cadence);
  if(scope==='history'){paymentFrom=range.from;paymentTo=range.to;v1992PaymentPage=1}else{v174PayPreset=cadence;v174PayFrom=range.from;v174PayTo=range.to}
  if(shouldRender)render()
}
function v1993SetPayCadence(value,scope='run'){if(!V1993_PAY_CADENCES.includes(value))return;v1993RememberPayCadence(value);v1993UseCurrentPayPeriod(scope)}
function v1993SetPayCustomDate(scope,which,value){
  if(scope==='history'){if(which==='from')paymentFrom=value;else paymentTo=value;v1992PaymentPage=1;render()}else v174SetPayDate(which,value)
}
const v1993BasePeriodBar=v174PeriodBar;
v174PeriodBar=function(kind,preset,from,to){
  if(kind!=='pay')return v1993BasePeriodBar(kind,preset,from,to);let cadence=v1993PayCadence(),current=v1993CurrentPayPeriod(cadence),isCurrent=from===current.from&&to===current.to;
  return `<div class="v174-period-bar v1993-period-bar"><div class="v1993-current-period"><button class="btn ${isCurrent?'orange':'light'}" onclick="v1993UseCurrentPayPeriod('run')">Current pay period</button><label>Pay cycle<select class="select" onchange="v1993SetPayCadence(this.value,'run')"><option value="week" ${cadence==='week'?'selected':''}>Weekly</option><option value="fortnight" ${cadence==='fortnight'?'selected':''}>Fortnightly</option><option value="month" ${cadence==='month'?'selected':''}>Monthly</option></select></label><small>Remembered for ${v195Esc(currentUser()?.name||'this user')}.</small></div><div class="v174-period-dates"><label>From<input class="input" type="date" value="${from}" onchange="v1993SetPayCustomDate('run','from',this.value)"></label><span>→</span><label>To<input class="input" type="date" value="${to}" onchange="v1993SetPayCustomDate('run','to',this.value)"></label><b>${v174RangeLabel(from,to)}</b></div>${from&&to&&from>to?'<div class="payment-range-warning">From date must be before To date.</div>':''}</div>`
};

function v1993OpenMoneyTerms(){
  modal=`<div class="modal"><div class="modalbox v1993-money-terms"><span class="att-kicker">PLAIN MONEY TERMS</span><h2>What each amount means</h2><div><b>Set price</b><span>The agreed labour price recorded for the work.</span></div><div><b>Earned</b><span>Completed work or approved time that has become payable.</span></div><div><b>Approved</b><span>An amount authorised in a pay run.</span></div><div><b>Paid</b><span>An amount recorded as actually paid.</span></div><div><b>Retention held</b><span>Money kept back until the work is signed off.</span></div><div><b>CIS retained</b><span>The CIS deduction recorded for a CIS individual.</span></div><div><b>Net payment</b><span>The amount paid after retention and CIS deductions.</span></div><button class="btn light" onclick="modal=null;render()">Close</button></div></div>`;render()
}

// Record an individual payment in two deliberate steps: review, then commit.
addPayment=function(context){
  if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Pay-record permission is required');let {item,target}=v190PaymentTarget(context);if(!item||!target)return;let allocations=v190Allocations(target),remaining=v191RemainingPrice(target);if(!allocations.length)return toast('Choose the Price Work people first');if(remaining<=0)return toast('The full set price has already been allocated. Release retained money from the individual record after sign-off.');
  modal=`<div class="modal"><div class="modalbox v191-payment-modal v1993-payment-entry"><span class="att-kicker">RECORD INDIVIDUAL PAYMENT</span><h2>${v190Esc(item.ref)} · ${v190Esc(target.name||item.type)}</h2><p class="muted">Enter the gross amount. Retention and CIS stay separate so the net payment is clear.</p><div class="field"><label>Individual</label><select id="pto" class="select" onchange="v191UpdatePaymentPreview()">${allocations.map(row=>`<option value="${row.memberId}">${v190Esc(v183PersonName(row.memberId))}</option>`).join('')}</select></div><div id="v190-payment-limit" class="v190-payment-limit"></div><div class="two"><div class="field"><label>Gross amount</label><input id="pa" class="input" inputmode="decimal" type="number" min="0.01" max="${remaining.toFixed(2)}" step="0.01" oninput="v191UpdatePaymentPreview()"></div><div class="field"><label>Retention held</label><div class="v191-suffix-input"><input id="v191-retention-rate" class="input" type="number" min="0" max="25" step="0.5" value="${Number(target.retentionPercent||0)}" oninput="v191UpdatePaymentPreview()"><span>%</span></div></div></div><div id="v191-payment-preview" class="v191-payment-preview"></div><div class="two"><div class="field"><label>Payment date</label><input id="pd" class="input" type="date" value="${iso()}"></div><div class="field"><label>Payment reference</label><input id="v1993-payment-reference" class="input" placeholder="Required · invoice or bank reference"></div></div><div class="field"><label>Notes <span class="muted">(optional)</span></label><input id="pn" class="input"></div><div class="actions"><button class="btn green" onclick="savePayment('${context}')">Review payment</button><button class="btn light" onclick="modal=null;render()">Cancel</button></div></div></div>`;render();v190RefreshPaymentLimit(context)
};
function v1993ReviewPayment(context){
  if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Pay-record permission is required');let {item,target}=v190PaymentTarget(context),memberId=document.getElementById('pto')?.value||'',figures=v191PaymentFigures(),remaining=v191RemainingPrice(target),date=document.getElementById('pd')?.value||iso(),reference=(document.getElementById('v1993-payment-reference')?.value||'').trim(),notes=(document.getElementById('pn')?.value||'').trim();if(!item||!target||!memberId)return;if(figures.gross<=0)return toast('Enter the gross amount');if(figures.gross>remaining+0.005)return toast(`Only ${attendanceMoney(remaining)} remains in this set price`);if(!reference)return toast('Enter an invoice or bank payment reference');
  v1993PendingPayment={context,itemId:item.id,memberId,date,reference,notes,figures:{gross:figures.gross,retentionRate:figures.retentionRate,retention:figures.retention,cisRate:figures.cisRate,cis:figures.cis,net:figures.net}};let p=v1993PendingPayment;
  modal=`<div class="modal"><div class="modalbox v1993-payment-review"><span class="att-kicker">CHECK BEFORE SAVING</span><h2>Review payment</h2><p class="v1993-review-warning"><b>This will record money as paid.</b> Check the individual, work, amounts and reference.</p><div class="v1993-review-context"><span><small>Individual</small><b>${v195Esc(v183PersonName(memberId))}</b></span><span><small>Work</small><b>${v195Esc(item.ref)} · ${v195Esc(target.name||item.type)}</b></span><span><small>Date</small><b>${v195Esc(date)}</b></span><span><small>Reference</small><b>${v195Esc(reference)}</b></span></div><div class="v191-payment-preview v1993-review-amounts"><span><small>Gross amount</small><b>${attendanceMoney(p.figures.gross)}</b></span><span><small>Retention held</small><b>${attendanceMoney(p.figures.retention)}</b></span><span><small>CIS retained · ${p.figures.cisRate}%</small><b>${attendanceMoney(p.figures.cis)}</b></span><span class="net"><small>Net payment</small><b>${attendanceMoney(p.figures.net)}</b></span></div>${notes?`<div class="v1993-review-note"><small>Notes</small><b>${v195Esc(notes)}</b></div>`:''}<div class="actions"><button class="btn light" onclick="v1993ReturnToPaymentEdit()">← Back to edit</button><button class="btn green" onclick="v1993CommitPayment()">Confirm and save paid</button></div></div></div>`;render()
}
function v1993ReturnToPaymentEdit(){let p=v1993PendingPayment;if(!p)return;addPayment(p.context);let set=(id,value)=>{let input=document.getElementById(id);if(input)input.value=value};set('pto',p.memberId);set('pa',p.figures.gross);set('v191-retention-rate',p.figures.retentionRate);set('pd',p.date);set('v1993-payment-reference',p.reference);set('pn',p.notes);v191UpdatePaymentPreview()}
function v1993CommitPayment(){
  let p=v1993PendingPayment;if(!p||(!hasPerm('record_payments')&&!hasPerm('edit_pay_records')))return toast('Payment review expired. Start again.');let {item,target}=v190PaymentTarget(p.context),remaining=v191RemainingPrice(target),member=v183Member(p.memberId),figures=p.figures;if(!item||!target||!member||!v190Allocations(target).some(row=>row.memberId===p.memberId))return toast('The payment record changed. Start again.');if(figures.gross<=0||figures.gross>remaining+0.005)return toast('The available set price changed. Review the payment again.');
  target.retentionPercent=figures.retentionRate;let record={id:'p'+Date.now(),pct:Number(target.price||0)?Math.round(figures.gross/Number(target.price)*10000)/100:0,date:p.date,grossAmount:figures.gross,retentionPercent:figures.retentionRate,retentionHeld:figures.retention,retentionRelease:0,cisRegistered:!!member.cisRegistered,cisDeductionRate:figures.cisRate,cisGrossExVat:Math.max(0,figures.gross-figures.retention),cisLiable:Math.max(0,figures.gross-figures.retention),cisDeduction:figures.cis,cisNet:figures.net,amount:figures.net,paidTo:p.memberId,payeeName:'',paymentReference:p.reference,notes:p.notes,paymentState:'Paid',enteredBy:currentUser()?.name||'',enteredAt:new Date().toISOString()};target.payments=target.payments||[];target.payments.push(record);target.paidTo=p.memberId;let settled=v191TargetSettled(target);target.pct=Number(target.price||0)?Math.min(100,Math.round(settled/Number(target.price)*100)):0;target.paymentState=target.pct>=100?'Paid':'Part Paid';touch(item);save();audit('payments','individual payment',record.id,`${item.ref}: ${attendanceMoney(figures.net)} net payment saved to ${v183PersonName(p.memberId)} · ${p.reference}`,item.site);emitNotification('payment',item.site,`${item.ref}: ${attendanceMoney(figures.net)} net paid to ${v183PersonName(p.memberId)}`,item.id);v1993PendingPayment=null;modal=null;savedState('Individual payment saved');render()
}
savePayment=function(context){return v1993ReviewPayment(context)};

// Marking a complete pay run as paid also requires an explicit review.
v190MarkPaid=function(){
  let sites=v190SelectedSites(),snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo),run=v190CurrentRun(sites,v174PayFrom,v174PayTo);if(!hasPerm('mark_payments_paid'))return toast('Mark-paid permission is required');if(!run||!['Locked','Exported'].includes(run.status)||run.snapshotHash!==snapshot.hash)return toast('Only a current locked or exported run can be marked paid');
  modal=`<div class="modal"><div class="modalbox v1993-payment-review"><span class="att-kicker">FINAL PAY-RUN CHECK</span><h2>Mark pay run as paid?</h2><p class="v1993-review-warning"><b>This changes the run to Paid.</b> Check the period, Sites and net payment before confirming.</p><div class="v1993-review-context"><span><small>Period</small><b>${v195Esc(v174RangeLabel(run.from,run.to))}</b></span><span><small>Sites</small><b>${v195Esc(run.siteIds.map(siteName).join(' · '))}</b></span><span><small>Status</small><b>${v195Esc(run.status)}</b></span><span><small>Net payment</small><b>${attendanceMoney(run.snapshot?.totals?.netDue||0)}</b></span></div><div class="field"><label>Payment reference</label><input id="v1993-run-payment-reference" class="input" placeholder="Required · e.g. BACS reference"></div><div class="actions"><button class="btn light" onclick="modal=null;render()">Cancel</button><button class="btn green" onclick="v1993ConfirmPayRunPaid('${run.id}')">Confirm paid</button></div></div></div>`;render()
};
function v1993ConfirmPayRunPaid(runId){let run=(db.payRuns||[]).find(row=>row.id===runId),reference=(document.getElementById('v1993-run-payment-reference')?.value||'').trim();if(!hasPerm('mark_payments_paid'))return toast('Mark-paid permission is required');if(!reference)return toast('Enter the bank payment reference');if(!run||!['Locked','Exported'].includes(run.status))return toast('This pay run is no longer ready to mark paid');let snapshot=v190PaySnapshot(run.siteIds,run.from,run.to);if(run.snapshotHash!==snapshot.hash)return toast('The pay records changed. Review and lock the run again.');run.status='Paid';run.paidAt=new Date().toISOString();run.paidBy=currentUser()?.name||'';run.paymentReference=reference;run.updatedAt=run.paidAt;save();audit('payments','pay run paid',run.id,`Pay run marked paid · ${reference}`,run.siteId);modal=null;savedState('Pay run marked paid');render()}

function v1993PaymentState(row){if(Number(row.retentionRelease||0)>0)return'Retention release';if(Number(row.retentionHeld||0)>0&&Number(row.cisDeduction||0)>0)return'Paid · retention & CIS';if(Number(row.retentionHeld||0)>0)return'Paid · retention held';if(Number(row.cisDeduction||0)>0)return'Paid · CIS retained';return'Paid'}
function v1993ClearPaymentFilter(kind){if(kind==='payee')paymentPayeeFilter='all';if(kind==='role')v183PaymentRole='all';if(kind==='search')paymentSearch='';v1992PaymentPage=1;render()}
function v1993PaymentFilterChips(){let chips=[];if(paymentPayeeFilter!=='all')chips.push(`<button onclick="v1993ClearPaymentFilter('payee')">Paid-to filter ×</button>`);if(v183PaymentRole!=='all')chips.push(`<button onclick="v1993ClearPaymentFilter('role')">${v195Esc(v183PaymentRole)} ×</button>`);if(paymentSearch)chips.push(`<button onclick="v1993ClearPaymentFilter('search')">Search: ${v195Esc(paymentSearch)} ×</button>`);return `<div class="v1993-filter-chips"><span>${v195Esc(v174RangeLabel(paymentFrom,paymentTo))}</span>${chips.join('')}</div>`}
function v1993TogglePaymentFilters(){v1993PaymentFiltersOpen=!v1993PaymentFiltersOpen;render()}
v1992InitialisePaymentRange=function(){let id=currentUser()?.id||'';if(v1992PaymentInitialisedFor===id)return;v1992PaymentInitialisedFor=id;v1992PaymentPage=1;if(!paymentFrom&&!paymentTo)v1993UseCurrentPayPeriod('history',false)};
v1992PaymentReset=function(){paymentPayeeFilter='all';v183PaymentRole='all';paymentSearch='';v1992PaymentPage=1;v1993UseCurrentPayPeriod('history')};
filteredPaymentRows=function(){
  let rows=allPaymentRows();if(paymentPayeeFilter!=='all')rows=rows.filter(row=>paymentPayeeKey(row)===paymentPayeeFilter);if(paymentFrom)rows=rows.filter(row=>String(row.date||'')>=paymentFrom);if(paymentTo)rows=rows.filter(row=>String(row.date||'')<=paymentTo);if(v183PaymentRole!=='all')rows=rows.filter(row=>v183PaymentRoleFor(row)===v183PaymentRole);let query=(paymentSearch||'').trim().toLowerCase();if(!query)return rows;
  return rows.filter(row=>[siteName(row.item.site),row.item.ref,row.stage,paymentPayeeName(row),row.paymentReference,row.notes,row.paymentType,row.variationName,v1993PaymentState(row)].join(' ').toLowerCase().includes(query))
};
paymentsHistoryPage=function(){
  if(!hasPerm('view_financials'))return smartEmpty('Commercial permission required.','Ask a Director to check your permissions.','','','£');v1992InitialisePaymentRange();let invalid=paymentFrom&&paymentTo&&paymentFrom>paymentTo,rows=invalid?[]:filteredPaymentRows(),total=rows.reduce((sum,row)=>sum+Number(row.amount||0),0),pages=Math.max(1,Math.ceil(rows.length/V1992_PAYMENT_PAGE_SIZE));v1992PaymentPage=Math.min(v1992PaymentPage,pages);let start=(v1992PaymentPage-1)*V1992_PAYMENT_PAGE_SIZE,shown=rows.slice(start,start+V1992_PAYMENT_PAGE_SIZE),cadence=v1993PayCadence(),rowHtml=shown.map(row=>`<tr><td>${row.date||'—'}</td><td>${v195Esc(siteName(row.item.site))}</td><td>${v195Esc(row.item.ref)}</td><td>${v195Esc(row.stage||row.variationName||'Payment')}</td><td><b>${v195Esc(paymentPayeeName(row))}</b></td><td><span class="v1993-payment-state">${v195Esc(v1993PaymentState(row))}</span></td><td><b>${attendanceMoney(row.amount||0)}</b></td><td>${v195Esc(row.paymentReference||row.notes||'—')}</td><td>${v195Esc(row.enteredBy||'—')}</td></tr>`).join(''),cards=shown.map(row=>`<details class="v1993-payment-card"><summary><span><b>${v195Esc(paymentPayeeName(row))}</b><small>${v195Esc(row.item.ref)} · ${v195Esc(row.stage||row.variationName||'Payment')} · ${row.date||'No date'}</small></span><span><em>${v195Esc(v1993PaymentState(row))}</em><strong>${attendanceMoney(row.amount||0)}</strong></span></summary><div class="v1993-payment-card-detail"><span><small>Site</small><b>${v195Esc(siteName(row.item.site))}</b></span><span><small>Gross amount</small><b>${attendanceMoney(v191PaymentGross(row))}</b></span><span><small>Retention held</small><b>${attendanceMoney(row.retentionHeld||0)}</b></span><span><small>CIS retained</small><b>${attendanceMoney(row.cisDeduction||0)}</b></span><span><small>Net payment</small><b>${attendanceMoney(row.amount||0)}</b></span><span><small>Reference</small><b>${v195Esc(row.paymentReference||row.notes||'Not entered')}</b></span></div>${roleCanAccessPlots()?`<button class="btn light" onclick="v199OpenWorkTarget('${row.item.id}')">View work record</button>`:''}</details>`).join('');
  let activeFilters=Number(paymentPayeeFilter!=='all')+Number(v183PaymentRole!=='all')+Number(!!paymentSearch);
  return `<div class="topbar"><div><h1>Payment History</h1><div class="sub">Paid records, with retention and CIS kept visible.</div></div>${v1992SiteSelector()}</div><div class="panel payment-filter-panel v1993-payment-filter-panel"><div class="v1993-history-period"><button class="btn orange" onclick="v1993UseCurrentPayPeriod('history')">Current pay period</button><label>Pay cycle<select class="select" onchange="v1993SetPayCadence(this.value,'history')"><option value="week" ${cadence==='week'?'selected':''}>Weekly</option><option value="fortnight" ${cadence==='fortnight'?'selected':''}>Fortnightly</option><option value="month" ${cadence==='month'?'selected':''}>Monthly</option></select></label><button class="btn light v1993-filter-toggle" onclick="v1993TogglePaymentFilters()">Filters${activeFilters?` · ${activeFilters}`:''}</button></div>${v1993PaymentFilterChips()}<div class="payment-filter-grid v1993-filter-drawer ${v1993PaymentFiltersOpen?'open':''}"><div class="field"><label>Paid to</label><select class="select" onchange="paymentPayeeFilter=this.value;v1992PaymentPage=1;render()">${paymentPayeeFilterOptions()}</select></div><div class="field"><label>User role</label><select class="select" onchange="v183PaymentRole=this.value;v1992PaymentPage=1;render()">${v183PaymentRoleOptions()}</select></div><div class="field"><label>From</label><input class="input" type="date" value="${paymentFrom}" onchange="v1993SetPayCustomDate('history','from',this.value)"></div><div class="field"><label>To</label><input class="input" type="date" value="${paymentTo}" onchange="v1993SetPayCustomDate('history','to',this.value)"></div><div class="field"><label>Search</label><input class="input" placeholder="Plot, stage, reference or person…" value="${v195Esc(paymentSearch)}" oninput="paymentSearch=this.value;v1992PaymentPage=1;render()"></div><button class="btn light" onclick="v1992PaymentReset()">Reset filters</button></div>${invalid?'<div class="payment-range-warning">From date must be before To date.</div>':''}</div><section class="panel v1992-payment-records"><div class="sectionhead"><div><h2>Paid records</h2><p class="muted">${rows.length?`${start+1}–${Math.min(start+shown.length,rows.length)} of ${rows.length}`:'No'} matching record${rows.length===1?'':'s'}</p></div><b>${invalid?'—':attendanceMoney(total)+' paid'}</b></div><div class="v1992-desktop-table"><table class="payment-history-table"><thead><tr><th>Date</th><th>Site</th><th>Plot / work</th><th>Stage</th><th>Individual</th><th>State</th><th>Net payment</th><th>Reference / note</th><th>Entered by</th></tr></thead><tbody>${rowHtml||'<tr><td colspan="9">No paid records match this view.</td></tr>'}</tbody></table></div><div class="v1992-payment-cards">${cards||smartEmpty('No matching paid records.','Change the pay period or clear a filter.','','','£')}</div><div class="v1992-pagination"><button class="btn light" ${v1992PaymentPage<=1?'disabled':''} onclick="v1992PaymentPageChange(${v1992PaymentPage-1},${pages})">← Previous</button><span>Page <b>${v1992PaymentPage}</b> of ${pages}</span><button class="btn light" ${v1992PaymentPage>=pages?'disabled':''} onclick="v1992PaymentPageChange(${v1992PaymentPage+1},${pages})">Next →</button></div></section>`
};

function v1993RunSiteIds(run){return run?.siteIds?.length?run.siteIds:(run?.snapshot?.siteIds||[run?.siteId].filter(Boolean))}
function v1993OpenRun(runId){let run=(db.payRuns||[]).find(row=>row.id===runId);if(!run)return;v174PayFrom=run.from;v174PayTo=run.to;v174PayPreset='custom';v190PaySites=v1993RunSiteIds(run);v192CommercialTab='pay-run';render()}
function v1993ReconcileDifference(kind,title,detail,value='',action=''){return `<div class="v1993-reconcile-difference ${kind}"><span><b>${v195Esc(title)}</b><small>${v195Esc(detail)}</small></span>${value?`<strong>${v195Esc(value)}</strong>`:''}${action?`<button class="btn light smallbtn" onclick="${action}">Review →</button>`:''}</div>`}
v195AccountsReconciliation=function(){
  v174EnsurePayPeriod();let sites=activeSites().filter(row=>canAccessSite(row.id)).map(row=>row.id),valid=v174RangeValid(v174PayFrom,v174PayTo),snapshot=valid&&sites.length?v190PaySnapshot(sites,v174PayFrom,v174PayTo):null,allRuns=(db.payRuns||[]).filter(run=>v1993RunSiteIds(run).some(id=>sites.includes(id))),periodRuns=allRuns.filter(run=>run.from===v174PayFrom&&run.to===v174PayTo),pendingRuns=periodRuns.filter(run=>['Approved','Locked','Exported'].includes(run.status)),paidRuns=periodRuns.filter(run=>run.status==='Paid'),payments=v195AllPayments().filter(row=>sites.includes(row.item.site)&&v174DateInRange(row.payment.date,v174PayFrom,v174PayTo)),directPaid=payments.reduce((sum,row)=>sum+Number(row.payment.amount||0),0),approvedValue=pendingRuns.reduce((sum,run)=>sum+Number(run.snapshot?.totals?.netDue||0),0),paidRunValue=paidRuns.reduce((sum,run)=>sum+Number(run.snapshot?.totals?.netDue||0),0),selectedKey=[...sites].sort().join('|'),selectedRun=periodRuns.find(run=>run.siteKey===selectedKey&&!run.superseded),differences=[];
  if(valid&&!periodRuns.length)differences.push(v1993ReconcileDifference('warn','No pay run for this exact period','Create a draft before Accounts can reconcile approval and payment.','','v192CommercialTab=\'pay-run\';render()'));
  if(selectedRun&&snapshot&&selectedRun.snapshotHash!==snapshot.hash)differences.push(v1993ReconcileDifference('danger','Records changed after review','Refresh the draft and approve it again before relying on the amount.','','v1993OpenRun(\''+selectedRun.id+'\')'));
  if(approvedValue>0)differences.push(v1993ReconcileDifference('warn','Approved but not marked paid',`${pendingRuns.length} run${pendingRuns.length===1?'':'s'} still need payment confirmation.`,attendanceMoney(approvedValue),`v1993OpenRun('${pendingRuns[0].id}')`));
  let missingRefs=paidRuns.filter(run=>!String(run.paymentReference||'').trim());if(missingRefs.length)differences.push(v1993ReconcileDifference('danger','Paid run missing a reference',`${missingRefs.length} paid run${missingRefs.length===1?' has':'s have'} no bank payment reference.`,'',`v1993OpenRun('${missingRefs[0].id}')`));
  if(paidRunValue>0&&Math.abs(paidRunValue-directPaid)>.01){let delta=Math.abs(paidRunValue-directPaid);differences.push(v1993ReconcileDifference('warn','Paid run and individual records differ',`Paid runs total ${attendanceMoney(paidRunValue)}; individual paid records total ${attendanceMoney(directPaid)}.`,attendanceMoney(delta)+' difference'))}
  let recentRuns=allRuns.slice().sort((a,b)=>String(b.updatedAt||b.createdAt).localeCompare(String(a.updatedAt||a.createdAt))).slice(0,8);
  return `<div class="v195-commercial-role-head accounts"><div><span>ACCOUNTS RECONCILIATION</span><h2>Due → Approved → Paid</h2><p>Use the differences below to see exactly what needs checking.</p></div></div>${v174PeriodBar('pay',v174PayPreset,v174PayFrom,v174PayTo)}<div class="v195-reconcile-flow"><div><span>NET PAYMENT DUE</span><b>${attendanceMoney(snapshot?.totals?.netDue||0)}</b><small>${snapshot?.lines?.length||0} individual lines</small></div><i>→</i><div><span>APPROVED / LOCKED</span><b>${attendanceMoney(approvedValue)}</b><small>${pendingRuns.length} current-period run${pendingRuns.length===1?'':'s'}</small></div><i>→</i><div class="paid"><span>PAID</span><b>${attendanceMoney(paidRunValue)}</b><small>${paidRuns.length} current-period run${paidRuns.length===1?'':'s'}</small></div></div><div class="panel v1993-reconcile-differences"><div class="sectionhead"><div><h3>Differences to check</h3><p class="muted">Current period only; no totals are hidden or replaced by a larger figure.</p></div><span class="badge ${differences.length?'b-amber':'b-green'}">${differences.length||'All clear'}</span></div>${differences.join('')||'<div class="v190-all-clear"><b>✓ This period reconciles</b><span>The reviewed run, paid state and individual records agree.</span></div>'}</div><div class="panel v1993-run-history"><div class="sectionhead"><div><h3>Recent pay runs</h3><p class="muted">Open any run to inspect its period, Sites, state and payment reference.</p></div></div>${recentRuns.map(run=>`<button onclick="v1993OpenRun('${run.id}')"><span><b>${v195Esc(v174RangeLabel(run.from,run.to))}</b><small>${v195Esc(v1993RunSiteIds(run).map(siteName).join(' · '))} · ${v195Esc(run.paymentReference||'No payment reference')}</small></span><em class="v1993-run-state ${String(run.status).toLowerCase()}">${v195Esc(run.status)}</em><strong>${attendanceMoney(run.snapshot?.totals?.netDue||0)} ›</strong></button>`).join('')||'<div class="muted">No pay runs have been created yet.</div>'}</div><div class="panel v195-reconcile-list"><div class="sectionhead"><div><h3>Individual paid records in this period</h3><p class="muted">${payments.length} record${payments.length===1?'':'s'} · ${attendanceMoney(directPaid)} total.</p></div></div>${payments.sort((a,b)=>String(b.payment.date).localeCompare(String(a.payment.date))).slice(0,10).map(row=>`<div><span><b>${v195Esc(v183PersonName(row.payment.paidTo,row.payment.payeeName||'Individual'))}</b><small>${v195Esc(row.item.ref)} · ${row.payment.date||'No date'} · ${v195Esc(row.payment.paymentReference||row.payment.notes||'No reference')}</small></span><strong>${attendanceMoney(row.payment.amount||0)}</strong></div>`).join('')||'<div class="muted">No individual paid records in this period.</div>'}</div>`
};

function v1993OwnerSiteIds(){return (site&&site!=='all'&&canAccessSite(site)?[site]:activeSites().filter(row=>canAccessSite(row.id)).map(row=>row.id))}
function v1993OwnerSummaryData(){
  v1993ApplySavedPayPeriod();v174EnsurePayPeriod();let siteIds=v1993OwnerSiteIds(),items=(db.items||[]).filter(item=>siteIds.includes(item.site)),targets=items.flatMap(item=>item.type==='House Plot'?(item.lifts||[]):[item]),snapshot=v174RangeValid(v174PayFrom,v174PayTo)&&siteIds.length?v190PaySnapshot(siteIds,v174PayFrom,v174PayTo):null,payments=v195AllPayments().filter(row=>siteIds.includes(row.item.site)&&v174DateInRange(row.payment.date,v174PayFrom,v174PayTo)),runs=(db.payRuns||[]).filter(run=>run.from===v174PayFrom&&run.to===v174PayTo&&v1993RunSiteIds(run).some(id=>siteIds.includes(id))),variations=items.flatMap(item=>(item.variations||[]).filter(row=>(row.approvalStatus||'Approved')==='Approved'&&v174DateInRange(row.date,v174PayFrom,v174PayTo)));
  return {siteIds,from:v174PayFrom,to:v174PayTo,plotCount:items.filter(item=>item.type==='House Plot').length,completion:targets.length?Math.round(targets.filter(row=>row.status==='Complete').length/targets.length*100):0,setPrice:targets.reduce((sum,row)=>sum+Number(row.price||0),0),completedValue:targets.filter(row=>row.status==='Complete').reduce((sum,row)=>sum+Number(row.price||0),0),grossPaid:payments.reduce((sum,row)=>sum+v191PaymentGross(row.payment),0),retentionHeld:payments.reduce((sum,row)=>sum+Number(row.payment.retentionHeld||0)-Number(row.payment.retentionRelease||0),0),cisRetained:payments.reduce((sum,row)=>sum+Number(row.payment.cisDeduction||0),0),netPaid:payments.reduce((sum,row)=>sum+Number(row.payment.amount||0),0),approved:runs.filter(row=>['Approved','Locked','Exported'].includes(row.status)).reduce((sum,row)=>sum+Number(row.snapshot?.totals?.netDue||0),0),paidRuns:runs.filter(row=>row.status==='Paid').reduce((sum,row)=>sum+Number(row.snapshot?.totals?.netDue||0),0),variations:variations.reduce((sum,row)=>sum+Number(row.value||0),0),netDue:Number(snapshot?.totals?.netDue||0),exceptions:snapshot?.exceptions||[]}
}
function v1993OwnerMetricRows(data){let rows=[['Active Sites',data.siteIds.map(siteName).join(' · ')],['Pay period',v174RangeLabel(data.from,data.to)],['Plots',String(data.plotCount)],['Construction completion',data.completion+'%'],['Set price',attendanceMoney(data.setPrice)],['Completed work value',attendanceMoney(data.completedValue)],['Net payment due',attendanceMoney(data.netDue)],['Approved / locked',attendanceMoney(data.approved)],['Paid pay runs',attendanceMoney(data.paidRuns)],['Gross payment records',attendanceMoney(data.grossPaid)],['Retention held in period',attendanceMoney(data.retentionHeld)],['CIS retained in period',attendanceMoney(data.cisRetained)],['Net paid records',attendanceMoney(data.netPaid)],['Approved variations in period',attendanceMoney(data.variations)],['Pay exceptions',String(data.exceptions.length)]];return roleCanAccessPlots()?rows:rows.filter(([label])=>!['Plots','Construction completion','Completed work value'].includes(label))}
function v1993CanOwnerSummary(){return hasPerm('view_financials')&&hasPerm('download_reports')}
function v1993OpenOwnerSummary(){if(!v1993CanOwnerSummary())return toast('Financial report permission is required');let data=v1993OwnerSummaryData(),rows=v1993OwnerMetricRows(data);modal=`<div class="modal"><div class="modalbox v1993-owner-summary"><span class="att-kicker">OWNER SUMMARY</span><h2>Work and payment position</h2><p class="muted">A traceable operational summary. It is not a profit or margin calculation.</p><div class="v1993-owner-period"><b>${v195Esc(data.siteIds.map(siteName).join(' · '))}</b><span>${v195Esc(v174RangeLabel(data.from,data.to))}</span></div><div class="v1993-owner-metrics">${rows.slice(2).map(([label,value])=>`<span><small>${v195Esc(label)}</small><b>${v195Esc(value)}</b></span>`).join('')}</div>${data.exceptions.length?`<div class="v1993-owner-exceptions"><b>${data.exceptions.length} pay exception${data.exceptions.length===1?'':'s'}</b><span>${data.exceptions.slice(0,4).map(row=>v195Esc(`${row.type}: ${row.person}`)).join('<br>')}</span></div>`:'<div class="v190-all-clear"><b>✓ No pay exceptions in this period</b></div>'}<div class="actions"><button class="btn light" onclick="v1993DownloadOwnerCsv()">Download CSV</button><button class="btn blue" onclick="v1993PrintOwnerSummary()">Print / Save PDF</button><button class="btn light" onclick="modal=null;render()">Close</button></div></div></div>`;render()}
function v1993DownloadOwnerCsv(){if(!v1993CanOwnerSummary())return toast('Financial report permission is required');let data=v1993OwnerSummaryData(),rows=[['SiteSight — Work and payment position'],...v1993OwnerMetricRows(data),[],['Exception','Individual / record','Detail'],...data.exceptions.map(row=>[row.type,row.person,row.detail])];v190DownloadCsv(rows,`sitesight-owner-summary-${data.from}-to-${data.to}.csv`);audit('payments','owner summary export',data.from,'Owner work and payment summary exported','')}
function v1993PrintOwnerSummary(){if(!v1993CanOwnerSummary())return toast('Financial report permission is required');let data=v1993OwnerSummaryData(),rows=v1993OwnerMetricRows(data),html=`<h1>SiteSight — Work and payment position</h1><p>${v195Esc(data.siteIds.map(siteName).join(' · '))}<br>${v195Esc(v174RangeLabel(data.from,data.to))}</p><table><tbody>${rows.slice(2).map(([label,value])=>`<tr><th>${v195Esc(label)}</th><td>${v195Esc(value)}</td></tr>`).join('')}</tbody></table><h2>Pay exceptions</h2>${data.exceptions.length?`<table><thead><tr><th>Exception</th><th>Individual / record</th><th>Detail</th></tr></thead><tbody>${data.exceptions.map(row=>`<tr><td>${v195Esc(row.type)}</td><td>${v195Esc(row.person)}</td><td>${v195Esc(row.detail)}</td></tr>`).join('')}</tbody></table>`:'<p>No pay exceptions in this period.</p>'}<p class="note">Operational work and payment summary only. This is not a profit, margin, statutory payroll or bank-payment report.</p>`;v1991OpenPrintPage('SiteSight Owner Summary',html);audit('payments','owner summary print',data.from,'Owner work and payment summary opened for print','')}

const v1993BasePayRunPage=v190PayRunPage;
v190PayRunPage=function(){return v1993BasePayRunPage().replace(/<span>Gross due<\/span>/g,'<span>Gross amount due</span>').replace(/<span>CIS to retain<\/span>/g,'<span>CIS retained</span>').replace(/<span>Net payment due<\/span>/g,'<span>Net payment</span>')};
const v1993BaseSavedViews=v195CommercialSavedViews;
v195CommercialSavedViews=function(){return v1993BaseSavedViews().replace(/Unpaid completed work/g,'Earned — amount not entered')};
const v1993BaseFinanceOverview=financeOverview;
financeOverview=function(item){return v1993BaseFinanceOverview(item).replace('<span>Original Contract</span>','<span>Set price</span>').replace('<span>Total Authorised</span>','<span>Set price + approved variations</span>')};
const v1993BaseItemPaymentHistory=itemPaymentHistory;
itemPaymentHistory=function(item){return v1993BaseItemPaymentHistory(item).replace('<span class="muted">Original contract</span>','<span class="muted">Set price</span>').replace('<span class="muted">Total authorised</span>','<span class="muted">Set price + approved variations</span>').replace('<span class="muted">Original paid</span>','<span class="muted">Set-price work paid</span>')};
const v1993BaseCommercialPage=v195CommercialPage;
v195CommercialPage=function(){
  v1993ApplySavedPayPeriod();let html=v1993BaseCommercialPage(),actions=`<div class="actions v1993-commercial-actions"><button class="btn light" onclick="v1993OpenMoneyTerms()">Money terms</button>${hasPerm('view_financials')&&hasPerm('download_reports')?'<button class="btn blue" onclick="v1993OpenOwnerSummary()">Owner summary</button>':''}</div>`;
  html=html.replace('<div class="sub">Pay preparation, saved reviews and reconciliation in one place.</div></div></div>',`<div class="sub">Pay preparation, saved reviews and reconciliation in one place.</div></div>${actions}</div>`);return html
};
v192CommercialPage=v195CommercialPage;

const v1993BaseShell=shell;
shell=function(content){v1993BaseShell(content);let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace(/v1\.99\.2 · USABILITY TEST(?: DEMO)?/g,`v1.99.3 · PAYMENT CONTROL${db.demoProfile?' DEMO':''}`)};

/* =====================================================================
   SiteSight V1.99.4 — workflow reliability
   Exact notification routes, grace-aware payable time, page history and
   dependable drawing deductions with selectable brickwork rate units.
   ===================================================================== */

const V1994_BRICKS_PER_SQUARE_METRE=60;

// ---- Drawing take-off deductions and rate units ---------------------
function v1994TakeoffRateUnit(row){
  if(['Area','Deduct Area'].includes(row?.kind))return row?.rateUnit==='1000_bricks'?'1000_bricks':'m2';
  if(row?.kind==='Length')return'm';if(row?.kind==='Count')return'count';return row?.rateUnit||''
}
function v1994TakeoffRateLabel(row){
  let unit=v1994TakeoffRateUnit(row);return unit==='1000_bricks'?'1,000 bricks':unit==='m2'?'m²':unit==='m'?'m':'No.'
}
takeoffValue=function(row){
  let quantity=Number(row?.quantity||0),rate=Number(row?.rate||0);
  return v1994TakeoffRateUnit(row)==='1000_bricks'?quantity*V1994_BRICKS_PER_SQUARE_METRE/1000*rate:quantity*rate
};
takeoffPendingHtml=function(){
  if(!takeoffPending)return'';let pending=takeoffPending,areaRate=['Area','Deduct Area'].includes(pending.kind);
  let rateFields=hasPerm('view_financials')?`<div class="field"><label>Optional rate</label><div class="v1994-rate-row"><input id="takeoff-rate" class="input" type="number" min="0" step="0.01" placeholder="Enter rate"><select id="takeoff-rate-unit" class="select" ${areaRate?'':'disabled'}>${areaRate?'<option value="m2">Per m²</option><option value="1000_bricks">Per 1,000 bricks</option>':`<option value="${pending.kind==='Length'?'m':'count'}">${pending.kind==='Length'?'Per metre':'Per item'}</option>`}</select></div><div class="muted">${areaRate?`Brick rates use ${V1994_BRICKS_PER_SQUARE_METRE} bricks per m². Deductions reduce the total value automatically.`:'Value = quantity × rate.'}</div></div>`:'';
  return `<div class="takeoff-pending"><span class="takeoff-kicker">SAVE MEASUREMENT</span><h3>${v184Esc(pending.kind)}</h3><div class="takeoff-pending-qty">${takeoffFmtQty(pending)}</div><div class="field"><label>Description</label><input id="takeoff-label" class="input" placeholder="${pending.kind==='Deduct Area'?'e.g. Windows and doors':pending.kind==='Area'?'e.g. Front elevation brickwork':pending.kind==='Length'?'e.g. Soldier course':'e.g. Air bricks'}"></div>${rateFields}<div class="actions"><button class="btn green" type="button" onclick="this.disabled=true;saveTakeoffMeasurement();if(takeoffPending)this.disabled=false">✓ Save measurement</button><button class="btn light" type="button" onclick="takeoffPending=null;renderTakeoffWorkspace()">Cancel</button></div></div>`
};
saveTakeoffMeasurement=function(){
  if(v197TakeoffSaving)return false;if(!v184CanMeasureDrawing())return toast('You do not have drawing measurement access.'),false;
  let drawing=takeoffDrawing(),pending=takeoffPending,label=(document.getElementById('takeoff-label')?.value||'').trim();if(!drawing||!pending)return toast('Finish a measurement before saving it.'),false;if(!label)return toast('Enter a description for this measurement.'),false;
  let quantity=Number(pending.quantity||0);if(!Number.isFinite(quantity)||Math.abs(quantity)<=0||(pending.kind==='Count'&&quantity<1))return toast('This measurement has no valid quantity. Clear it and measure again.'),false;
  quantity=pending.kind==='Deduct Area'?-Math.abs(quantity):Math.abs(quantity);
  let rate=hasPerm('view_financials')?Number(document.getElementById('takeoff-rate')?.value||0):0,rateUnit=['Area','Deduct Area'].includes(pending.kind)?(document.getElementById('takeoff-rate-unit')?.value||'m2'):(pending.kind==='Length'?'m':'count');
  if(!Number.isFinite(rate)||rate<0)return toast('Enter a valid rate, or leave it blank.'),false;if(!['m2','1000_bricks','m','count'].includes(rateUnit))rateUnit=v1994TakeoffRateUnit(pending);
  v197TakeoffSaving=true;try{
    let entry={id:`tk${Date.now()}${Math.random().toString(16).slice(2)}`,kind:pending.kind,label,points:JSON.parse(JSON.stringify(pending.points||[])),quantity,unit:pending.unit,rate,rateUnit,createdAt:new Date().toISOString(),by:currentUser()?.name||'',userId:currentUser()?.id||'',calibrationDistanceM:drawing.takeoffCalibration?.distanceM||0};
    if(v184PdfActive())entry.pdfPage=Number(v184PdfPage||1);drawing.takeoffs=Array.isArray(drawing.takeoffs)?drawing.takeoffs:[];drawing.takeoffs.push(entry);if(v184PdfActive())v184StorePdfCalibration();let entity=takeoffAuditEntity();save();audit('drawing','drawing take-off',drawing.id||entity.id,`${entity.label}: ${entry.kind} saved — ${entry.label} · ${takeoffFmtQty(entry)}${hasPerm('view_financials')&&rate?` · ${money(takeoffValue(entry))} at ${money(rate)} per ${v1994TakeoffRateLabel(entry)}`:''}`,takeoffSiteId());takeoffPending=null;takeoffDraft=[];takeoffPointer=null;takeoffTool='select';renderTakeoffWorkspace();savedState('Take-off measurement saved');return true
  }finally{v197TakeoffSaving=false}
};
takeoffListHtml=function(d=takeoffDrawing()){
  let rows=v184PageTakeoffs(d);if(!rows.length)return `<div class="takeoff-empty">No saved measurements${v184PdfActive()?` on page ${v184PdfPage}`:''}.</div>`;
  return `<div class="takeoff-list">${rows.map(row=>`<div class="takeoff-row"><div class="takeoff-row-main"><span class="takeoff-kind kind-${String(row.kind).toLowerCase().replace(/\s+/g,'-')}">${v184Esc(row.kind)}</span><b>${calendarSafe(row.label)}</b><small>${takeoffFmtQty(row)}${v184PdfActive()?` · Page ${Number(row.pdfPage||1)}`:''} · ${v184Esc(row.by||'Unknown user')}${row.createdAt?' · '+new Date(row.createdAt).toLocaleDateString('en-GB'):''}</small></div>${hasPerm('view_financials')?`<div class="takeoff-money"><span>${Number(row.rate||0)?money(row.rate)+' / '+v1994TakeoffRateLabel(row):'No rate'}</span><b>${Number(row.rate||0)?money(takeoffValue(row)):'—'}</b></div>`:''}${v184CanMeasureDrawing()?`<button class="takeoff-delete" type="button" onclick="deleteTakeoffMeasurement('${row.id}')" title="Remove measurement">×</button>`:''}</div>`).join('')}</div>`
};

// ---- Grace-aware payable time ---------------------------------------
v190ShiftCalculation=function(shift,member){
  let date=attendanceDateOnly(shift?.clockIn?.time,shift?.siteId),schedule=v174MemberScheduleForDate(member,shift?.siteId,date),clockIn=new Date(shift?.clockIn?.time||0),clockOut=shift?.clockOut?.time?new Date(shift.clockOut.time):null,rate=v190RateRecord(member,date),approval=shift?.overtimeApproval||{},approvedMinutes=approval.status==='Approved'&&approval.manualRequest?Math.max(0,Number(approval.minutes||0)):0,cfg=attendanceSiteConfig(shift?.siteId)||{},lateGraceMinutes=Math.max(0,Number(cfg.lateArrivalGraceMinutes??0)),earlyGraceMinutes=Math.max(0,Number(cfg.earlyLeaveGraceMinutes??5));
  let empty={date,schedule,rate,recordedHours:0,payableHours:0,basicHours:0,overtimeHours:0,approvedMinutes,lateMinutes:0,actualLateMinutes:0,unauthorisedMinutes:0,arrivalLateMinutes:0,earlyLeaveMinutes:0,lateGraceMinutes,earlyGraceMinutes,lateDeductedMinutes:0,earlyDeductedMinutes:0,punctualityDeductedMinutes:0,approval,gross:0};
  if(!clockOut||Number.isNaN(clockIn.getTime())||Number.isNaN(clockOut.getTime()))return empty;
  let scheduledStart=new Date(v190ScheduledIso(date,schedule.start,shift.siteId)),scheduledEnd=new Date(v190ScheduledIso(date,schedule.end,shift.siteId)),arrivalLateMinutes=schedule.active?Math.max(0,(clockIn-scheduledStart)/60000):0,earlyLeaveMinutes=schedule.active?Math.max(0,(scheduledEnd-clockOut)/60000):0,lateDeductedMinutes=arrivalLateMinutes>lateGraceMinutes?arrivalLateMinutes:0,earlyDeductedMinutes=earlyLeaveMinutes>earlyGraceMinutes?earlyLeaveMinutes:0;
  let paidStart=schedule.active?(lateDeductedMinutes?clockIn:scheduledStart):clockIn,regularEnd=schedule.active?(earlyDeductedMinutes?clockOut:scheduledEnd):clockOut;if(schedule.active&&clockIn<scheduledStart)paidStart=scheduledStart;if(schedule.active&&clockOut>scheduledEnd)regularEnd=scheduledEnd;
  let breakHours=Math.max(0,Number(shift.breakMinutes||0))/60,raw=Math.max(0,(clockOut-clockIn)/3600000-breakHours),basic=Math.max(0,(regularEnd-paidStart)/3600000-breakHours),afterFinishMinutes=schedule.active?Math.max(0,(clockOut-scheduledEnd)/60000):0,overtimeHours=approvedMinutes/60,payable=basic+overtimeHours,gross=0;
  if(['Hourly','Day Rate'].includes(rate.employmentType))gross=basic*rate.hourlyRate+overtimeHours*rate.overtimeRate;if(rate.employmentType==='Day Rate'&&v169FullDayOverride(member.id,date,shift.siteId))gross+=Math.max(0,rate.dayRate-basic*rate.hourlyRate);
  return {date,schedule,rate,recordedHours:raw,payableHours:payable,basicHours:basic,overtimeHours,approvedMinutes,lateMinutes:approval.manualRequest?Number(approval.minutes||0):afterFinishMinutes,actualLateMinutes:afterFinishMinutes,unauthorisedMinutes:Math.max(0,Number(approval.manualRequest?approval.minutes:afterFinishMinutes)-approvedMinutes),arrivalLateMinutes,earlyLeaveMinutes,lateGraceMinutes,earlyGraceMinutes,lateDeductedMinutes,earlyDeductedMinutes,punctualityDeductedMinutes:lateDeductedMinutes+earlyDeductedMinutes,approval,gross}
};
v174ShiftPayBreakdown=function(shift,member){return v190ShiftCalculation(shift,member)};
v174AttendanceTotalsForRange=function(workerId,from,to,siteId='all'){
  let wm=attendanceMemberById(workerId),member=wm?.member||{},shifts=v174ShiftsForRange(workerId,from,to,siteId),details=shifts.map(shift=>({shift,...v190ShiftCalculation(shift,member)})),sum=key=>details.reduce((total,row)=>total+Number(row[key]||0),0),days=new Set(details.map(row=>row.date)).size;
  return {shifts,details,totalHours:sum('recordedHours'),payableHours:sum('payableHours'),basicHours:sum('basicHours'),overtimeHours:sum('overtimeHours'),authorisedOvertimeHours:sum('overtimeHours'),unauthorisedOvertimeHours:sum('unauthorisedMinutes')/60,lateArrivalHours:sum('lateDeductedMinutes')/60,earlyLeavingHours:sum('earlyDeductedMinutes')/60,punctualityDeductedHours:sum('punctualityDeductedMinutes')/60,gross:sum('gross'),days,fullDayTopUp:0,standardHours:v174ExpectedStandardHours(member,siteId==='all'?(shifts[0]?.siteId||attendanceDefaultSiteId()):siteId,from,to),rateLines:details.map(row=>({date:row.date,hourlyRate:row.rate.hourlyRate,overtimeRate:row.rate.overtimeRate,dayRate:row.rate.dayRate,employmentType:row.rate.employmentType}))}
};
attendanceWeekTotals=function(workerId,start,siteId='all'){return v174AttendanceTotalsForRange(workerId,start,v174DateAdd(start,6),siteId)};

const v1994BaseTimeLines=v190TimeLines;
v190TimeLines=function(siteIds,from,to){
  let lines=v1994BaseTimeLines(siteIds,from,to);for(const line of lines){if(!String(line.id||'').startsWith('time:'))continue;let total=v174AttendanceTotalsForRange(line.memberId,from,to,'all'),allowedShifts=total.details.filter(row=>siteIds.includes(row.shift.siteId));line.lateArrivalDeducted=allowedShifts.reduce((sum,row)=>sum+Number(row.lateDeductedMinutes||0),0)/60;line.earlyLeavingDeducted=allowedShifts.reduce((sum,row)=>sum+Number(row.earlyDeductedMinutes||0),0)/60;line.punctualityDeducted=line.lateArrivalDeducted+line.earlyLeavingDeducted}return lines
};
const v1994BasePaySnapshot=v190PaySnapshot;
v190PaySnapshot=function(siteIds,from,to){let snapshot=v1994BasePaySnapshot(siteIds,from,to);snapshot.totals.lateArrivalDeducted=snapshot.lines.reduce((sum,row)=>sum+Number(row.lateArrivalDeducted||0),0);snapshot.totals.earlyLeavingDeducted=snapshot.lines.reduce((sum,row)=>sum+Number(row.earlyLeavingDeducted||0),0);snapshot.totals.punctualityDeducted=snapshot.lines.reduce((sum,row)=>sum+Number(row.punctualityDeducted||0),0);let {hash,...payload}=snapshot;snapshot.hash=v175Hash(JSON.stringify(payload));return snapshot};

const v1994BasePayrollSnapshot=v175PayrollSnapshot;
v175PayrollSnapshot=function(siteId,from,to){let snapshot=v1994BasePayrollSnapshot(siteId,from,to);for(const person of snapshot.people){let total=v174AttendanceTotalsForRange(person.id,from,to,siteId);person.lateArrivalDeducted=Number(total.lateArrivalHours.toFixed(4));person.earlyLeavingDeducted=Number(total.earlyLeavingHours.toFixed(4));person.punctualityDeducted=Number(total.punctualityDeductedHours.toFixed(4))}snapshot.totals.lateArrivalDeducted=snapshot.people.reduce((sum,row)=>sum+row.lateArrivalDeducted,0);snapshot.totals.earlyLeavingDeducted=snapshot.people.reduce((sum,row)=>sum+row.earlyLeavingDeducted,0);snapshot.totals.punctualityDeducted=snapshot.people.reduce((sum,row)=>sum+row.punctualityDeducted,0);let {hash,...payload}=snapshot;snapshot.hash=v175Hash(JSON.stringify(payload));return snapshot};
v175PayrollExportRows=function(siteId,from,to){let snapshot=v175PayrollSnapshot(siteId,from,to),lock=v175CurrentPayrollLock(siteId,from,to),rows=[['SiteSight Pay Period Export'],['Site',siteName(siteId)],['From',from],['To',to],['Lock status',lock?(lock.snapshotHash===snapshot.hash?'Locked':'Changed since lock'):'Not locked'],[],['Payee','Record type','Recorded hours','Payable hours','Late arrival deducted','Early leaving deducted','Authorised OT','Unapproved overtime','Gross / Due']];snapshot.people.forEach(row=>rows.push([row.name,`Individual · ${row.type}`,row.recorded.toFixed(2),row.payable.toFixed(2),row.lateArrivalDeducted.toFixed(2),row.earlyLeavingDeducted.toFixed(2),row.authorisedOT.toFixed(2),row.excluded.toFixed(2),row.gross.toFixed(2)]));snapshot.setPrice.forEach(row=>rows.push([row.payee,`Set Price · ${row.ref} · ${row.stage}`,'','','','','','',row.due.toFixed(2)]));snapshot.variations.forEach(row=>rows.push([row.payee,`Variation · ${row.ref} · ${row.stage}`,'','','','','','',row.due.toFixed(2)]));return rows};
v190CsvRows=function(run,draft=false){let snapshot=run.snapshot,title=draft?'DRAFT — NOT APPROVED FOR PAYMENT':'LOCKED PAY RUN',rows=[[title],['Period',snapshot.from,snapshot.to],['Sites',snapshot.siteIds.map(siteName).join(' | ')],['Status',run.status],['Snapshot',run.snapshotHash||snapshot.hash],[],['Individual','Role','Pay type','Sites','Recorded hours','Payable hours','Late arrival deducted','Early leaving deducted','Approved overtime','Gross due','CIS rate','CIS retained','Retention release','Net payment','State']];for(const line of snapshot.lines)rows.push([line.name,line.role,line.kind,line.siteIds.map(siteName).join(' | '),Number(line.recorded||0).toFixed(2),Number(line.payable||0).toFixed(2),Number(line.lateArrivalDeducted||0).toFixed(2),Number(line.earlyLeavingDeducted||0).toFixed(2),Number(line.overtime||0).toFixed(2),Number(line.grossDue||0).toFixed(2),`${Number(line.cisRate||0)}%`,Number(line.cisDeduction||0).toFixed(2),Number(line.retentionDue||0).toFixed(2),Number(line.netDue||0).toFixed(2),draft?'Earned':run.status==='Paid'?'Paid':'Approved']);return rows};

function v1994ApprovalFields(total){return {totalHours:Number(total.totalHours.toFixed(4)),basicHours:Number(total.basicHours.toFixed(4)),overtimeHours:Number(total.overtimeHours.toFixed(4)),lateArrivalHours:Number(total.lateArrivalHours.toFixed(4)),earlyLeavingHours:Number(total.earlyLeavingHours.toFixed(4)),punctualityDeductedHours:Number(total.punctualityDeductedHours.toFixed(4)),gross:Number(total.gross.toFixed(2))}}
attendanceApproveWorkerWeek=function(workerId,siteId,start){if(!hasPerm('approve_timesheets'))return toast('Timesheet approval permission is required');let wm=attendanceMemberById(workerId);if(!wm)return;let totals=attendanceWeekTotals(workerId,start,siteId),old=attendanceApproval(workerId,siteId,start);if(old)old.superseded=true;db.timesheetApprovals.push({id:'appr'+Date.now()+Math.random().toString(16).slice(2),workerId,siteId,weekStart:start,approvedBy:currentUser()?.name||'',approvedById:currentUser()?.id||'',approvedAt:new Date().toISOString(),...v1994ApprovalFields(totals),needsReapproval:false,superseded:false});save();audit('attendance','timesheet approval',workerId,`${wm.member.name}: week ${start} approved — ${totals.payableHours.toFixed(2)} payable hrs · ${totals.punctualityDeductedHours.toFixed(2)} late/early deducted`,siteId);savedState(`${wm.member.name} approved`);render()};
attendanceApproveAllWeek=function(){if(!hasPerm('approve_timesheets'))return;let sid=attendanceResolvedSite(),members=attendanceVisibleMembers(sid).filter(row=>row.member.employmentType!=='Not tracked'),start=attendanceResolvedWeekStart();if(!members.length)return toast('No attendance workers in this view');if(!confirm(`Approve ${members.length} timesheet${members.length===1?'':'s'} for ${attendanceWeekLabel(start)}?`))return;members.forEach(row=>{let old=attendanceApproval(row.member.id,sid,start);if(old)old.superseded=true;let total=attendanceWeekTotals(row.member.id,start,sid);db.timesheetApprovals.push({id:'appr'+Date.now()+Math.random().toString(16).slice(2),workerId:row.member.id,siteId:sid,weekStart:start,approvedBy:currentUser()?.name||'',approvedById:currentUser()?.id||'',approvedAt:new Date().toISOString(),...v1994ApprovalFields(total),needsReapproval:false,superseded:false})});save();audit('attendance','timesheet approval',start,`${members.length} weekly timesheets approved — ${attendanceWeekLabel(start)}`,sid);savedState('Weekly timesheets approved');render()};

function v1994MarkSiteApprovalsForRecheck(siteId){let changed=0;for(const row of db.timesheetApprovals||[])if(row.siteId===siteId&&!row.superseded&&!row.needsReapproval){row.needsReapproval=true;changed++}return changed}
const v1994BaseSaveSiteSetting=v199SaveSiteSetting;
v199SaveSiteSetting=function(siteId,focus){let cfg=attendanceSiteConfig(siteId)||{},before=JSON.stringify({start:cfg.expectedStartTime,end:cfg.expectedEndTime,late:cfg.lateArrivalGraceMinutes,early:cfg.earlyLeaveGraceMinutes});let result=v1994BaseSaveSiteSetting(siteId,focus),after=JSON.stringify({start:cfg.expectedStartTime,end:cfg.expectedEndTime,late:cfg.lateArrivalGraceMinutes,early:cfg.earlyLeaveGraceMinutes});if(['hours','grace'].includes(focus)&&before!==after){let count=v1994MarkSiteApprovalsForRecheck(siteId);if(count){save();audit('attendance','timesheet reapproval',siteId,`${siteName(siteId)}: ${count} approved timesheet${count===1?'':'s'} marked for re-approval after ${focus==='grace'?'grace-period':'working-hour'} change`,siteId)}}return result};
attendanceSaveSiteRules=function(siteId){let cfg=attendanceSiteConfig(siteId)||{},before=JSON.stringify({start:cfg.expectedStartTime,end:cfg.expectedEndTime,late:cfg.lateArrivalGraceMinutes,early:cfg.earlyLeaveGraceMinutes}),next=v169ReadAttendanceRules(siteId);if(v169ParseTime(next.expectedEndTime)<=v169ParseTime(next.expectedStartTime))return toast('Finish time must be after start time');next.radiusM=V178_ATTENDANCE_RADIUS_METRES;let after=JSON.stringify({start:next.expectedStartTime,end:next.expectedEndTime,late:next.lateArrivalGraceMinutes,early:next.earlyLeaveGraceMinutes}),count=before===after?0:v1994MarkSiteApprovalsForRecheck(siteId);save();audit('attendance','site',siteId,`Attendance rules updated — ${siteName(siteId)} · ${next.expectedStartTime}–${next.expectedEndTime} · ${next.lateArrivalGraceMinutes}m late grace · ${next.earlyLeaveGraceMinutes}m early grace`,siteId);if(count)audit('attendance','timesheet reapproval',siteId,`${count} approved timesheet${count===1?'':'s'} marked for re-approval`,siteId);savedState('Attendance rules saved');configureAttendanceLocation(siteId)};

const v1994BaseTimesheetView=attendanceTimesheetView;
attendanceTimesheetView=function(siteId){let html=v1994BaseTimesheetView(siteId);v174EnsureTimesheetPeriod();let valid=v174RangeValid(v174TimesheetFrom,v174TimesheetTo),members=valid?v174PayMembers(siteId,v174TimesheetFrom,v174TimesheetTo):[],rows=members.map(row=>({row,total:v174AttendanceTotalsForRange(row.member.id,v174TimesheetFrom,v174TimesheetTo,siteId)})),late=rows.reduce((sum,row)=>sum+row.total.lateArrivalHours,0),early=rows.reduce((sum,row)=>sum+row.total.earlyLeavingHours,0),panel=`<div class="panel v1994-time-deductions"><div class="sectionhead"><div><span class="att-kicker">AUTOMATIC TIME DEDUCTIONS</span><h3>Late arrival and early leaving</h3><p class="muted">Only clocking beyond each Site's saved grace is deducted from payable time.</p></div><b>${(late+early).toFixed(2)} hrs total</b></div><div class="v1994-deduction-totals"><span><small>Late arrival deducted</small><b>${late.toFixed(2)} hrs</b></span><span><small>Early leaving deducted</small><b>${early.toFixed(2)} hrs</b></span></div>${rows.some(entry=>entry.total.punctualityDeductedHours>0)?`<details><summary>View deductions by individual</summary><div class="v1994-deduction-list">${rows.filter(entry=>entry.total.punctualityDeductedHours>0).map(entry=>`<div><span><b>${v190Esc(entry.row.member.name)}</b><small>${v190Esc(entry.row.member.employmentType)}</small></span><span>${entry.total.lateArrivalHours.toFixed(2)} late</span><span>${entry.total.earlyLeavingHours.toFixed(2)} early</span><strong>${entry.total.punctualityDeductedHours.toFixed(2)} hrs</strong></div>`).join('')}</div></details>`:'<div class="v190-all-clear"><b>✓ No late-arrival or early-leaving deductions in this period</b></div>'}</div>`;return panel+html.replace(/EXCLUDED LATE TIME/g,'UNAPPROVED OVERTIME').replace(/Late time excluded/g,'Unapproved overtime').replace(/Payable<\/b> excludes unapproved time after the set finish/g,'Payable</b> excludes late arrival or early leaving beyond grace, unpaid breaks and unapproved time after the set finish')};
const v1994BaseOpenWorkerRange=v174OpenWorkerRange;
v174OpenWorkerRange=function(workerId,siteId){if(!v1998CanReviewTimeRecords())return toast('A Director must grant this account shift-review permission');let result=v1994BaseOpenWorkerRange(workerId,siteId),total=v174AttendanceTotalsForRange(workerId,v174TimesheetFrom,v174TimesheetTo,siteId);if(modal){let deductions=`<div class="v1994-worker-deductions"><span><small>LATE ARRIVAL DEDUCTED</small><b>${total.lateArrivalHours.toFixed(2)} hrs</b></span><span><small>EARLY LEAVING DEDUCTED</small><b>${total.earlyLeavingHours.toFixed(2)} hrs</b></span></div>`;modal=modal.replace('</div><div class="att-shift-list">','</div>'+deductions+'<div class="att-shift-list">');render()}return result};
const v1994BaseMyTimeView=attendanceMyTimeOnlyView;
attendanceMyTimeOnlyView=function(siteId,linked){let html=v1994BaseMyTimeView(siteId,linked);if(!linked)return html;let start=attendanceWeekStartISO(new Date()),total=v174AttendanceTotalsForRange(linked.member.id,start,v174DateAdd(start,6),siteId);if(!total.punctualityDeductedHours)return html;let notice=`<div class="v174-worker-notice v1994-worker-notice"><b>${total.punctualityDeductedHours.toFixed(2)} hours deducted for late arrival or early leaving.</b><span>This is outside the Site's saved grace: ${total.lateArrivalHours.toFixed(2)} late and ${total.earlyLeavingHours.toFixed(2)} early. Ask your manager to correct an inaccurate clocking record.</span></div>`;return html.replace('<div class="att-shift-list">',notice+'<div class="att-shift-list">')};
const v1994BasePayReview=contractorPayReview;
contractorPayReview=function(...args){return v1994BasePayReview(...args).replace(/Late time excluded/g,'Unapproved overtime')};
v190PayLineHtml=function(row){let deductions=Number(row.punctualityDeducted||0)?` · ${Number(row.punctualityDeducted).toFixed(2)} late/early deducted`:'';return `<div class="v190-pay-line"><span><b>${v190Esc(row.kind)}</b><small>${row.ref?`${v190Esc(row.ref)} · ${v190Esc(row.stage)} · `:''}${row.siteIds.map(siteName).map(v190Esc).join(', ')}</small></span><span>${row.payable?`${row.payable.toFixed(2)} hrs · ${row.overtime.toFixed(2)} OT${deductions}`:row.retentionDue?`Retention release ${attendanceMoney(row.retentionDue)}`:'Pricework record'}</span><strong>${attendanceMoney(row.netDue)} net</strong></div>`};

// ---- Notification target metadata and exact opening ------------------
function v1994NotificationTarget(kind,siteId,text,itemId=''){
  let documentRow=(db.documents||[]).find(row=>row.id===itemId&&row.type==='file');if(documentRow)return {targetType:'document',targetId:documentRow.id};
  let shift=(db.attendanceShifts||[]).find(row=>row.id===itemId);if(shift||['attendance','clock-reminder'].includes(kind))return {targetType:'shift',targetId:shift?.id||itemId||''};
  let item=(db.items||[]).find(row=>row.id===itemId);if(item){if(kind==='issue'){let issue=v199NotificationIssue({text},item);if(issue)return {targetType:'issue',targetId:issue.id,targetParentId:item.id}}return {targetType:'work',targetId:item.id}}
  let note=(db.calendarNotes||[]).find(row=>row.id===itemId)||(db.calendarNotes||[]).filter(row=>row.siteId===siteId&&String(text||'').toLowerCase().includes(String(row.title||'').toLowerCase())).sort((a,b)=>String(b.updatedAt||b.createdAt||'').localeCompare(String(a.updatedAt||a.createdAt||'')))[0];if(note)return {targetType:'calendar',targetId:note.id};
  let diary=(db.siteDiary||[]).find(row=>row.id===itemId)||(db.siteDiary||[]).filter(row=>row.siteId===siteId&&String(text||'').includes(String(row.date||''))).sort((a,b)=>String(b.createdAt||b.date||'').localeCompare(String(a.createdAt||a.date||'')))[0];if(diary)return {targetType:'diary',targetId:diary.id};
  return siteId?{targetType:'site',targetId:siteId}:{targetType:'',targetId:''}
}
emitNotification=function(kind,siteId,text,itemId=''){audit(kind,'update',itemId,text,siteId);let user=currentUser(),target=v1994NotificationTarget(kind,siteId,text,itemId);db.notifications=db.notifications||[];let event={id:'n'+Date.now()+Math.random().toString(16).slice(2),kind,siteId:siteId||'',text,itemId,actorId:user?.id||'',actorName:user?.name||'',createdAt:new Date().toISOString(),readBy:[],...target};db.notifications.unshift(event);db.notifications=db.notifications.slice(0,250);save();deliverNotifications()};
function v1994ResolvedNotificationTarget(notification){if(notification.targetType)return {targetType:notification.targetType,targetId:notification.targetId||'',targetParentId:notification.targetParentId||''};return v1994NotificationTarget(notification.kind,notification.siteId,notification.text,notification.itemId)}
v199OpenNotification=function(id){
  let user=currentUser(),notification=(db.notifications||[]).find(row=>row.id===id);if(!user||!notification)return toast('This notification is no longer available');notification.readBy=notification.readBy||[];if(!notification.readBy.includes(user.id))notification.readBy.push(user.id);let target=v1994ResolvedNotificationTarget(notification);notification.targetType=target.targetType;notification.targetId=target.targetId;notification.targetParentId=target.targetParentId||'';save();settingsOpen=false;modal=null;
  if(target.targetType==='document'){let row=(db.documents||[]).find(item=>item.id===target.targetId);return row?v199OpenDocumentNotification(row,notification):toast('This document is no longer available')}
  if(target.targetType==='issue'){let item=(db.items||[]).find(row=>row.id===target.targetParentId),issue=item?.issues?.find(row=>row.id===target.targetId);if(item&&issue&&canAccessSite(item.site)&&hasPerm('view_issues')){site=item.site;view='work-items';selected=item.id;selectedGang=null;render();return viewIssue(item.id,issue.id)}}
  if(target.targetType==='work'){let item=(db.items||[]).find(row=>row.id===target.targetId);if(item&&canAccessSite(item.site)){let index=item.type==='House Plot'?(item.lifts||[]).findIndex(lift=>String(notification.text||'').toLowerCase().includes(String(lift.name||'').toLowerCase())):-1;return v199OpenWorkTarget(item.id,index>=0?'lift':'item',index)}}
  if(target.targetType==='calendar'){let note=(db.calendarNotes||[]).find(row=>row.id===target.targetId);if(note&&canAccessSite(note.siteId)){site=note.siteId;calendarMonth=String(note.date).slice(0,7)+'-01';view='calendar';selected=null;render();return openCalendarNote(note.id)}}
  if(target.targetType==='diary'){let row=(db.siteDiary||[]).find(item=>item.id===target.targetId);if(row&&canAccessSite(row.siteId)){site=row.siteId;view='diary';selected=null;render();return openDiaryEntry(row.id)}}
  if(target.targetType==='shift'||['attendance','clock-reminder'].includes(notification.kind)){let shift=(db.attendanceShifts||[]).find(row=>row.id===(target.targetId||notification.itemId)),linked=attendanceLinkedMember();if(shift&&canAccessSite(shift.siteId)){if(!shift.clockOut?.time){site=shift.siteId;contractorAttendanceTab=linked?.member?.id===shift.workerId?'my-time':'today';attendanceTab=contractorAttendanceTab;view='attendance';selected=null;render();return}return v199OpenOldShift(shift.id)}if(notification.siteId&&canAccessSite(notification.siteId))site=notification.siteId;contractorAttendanceTab='today';attendanceTab='today';view='attendance';selected=null;render();return}
  let targetSite=target.targetType==='site'?target.targetId:notification.siteId;if(targetSite&&canAccessSite(targetSite)){site=targetSite;view='dashboard';selected=null;render();return}toast('The original record is no longer available');render()
};
notificationSettings=function(){let user=currentUser(),items=relevantNotifications(user).slice(0,30),unread=unreadNotifications(user).length;settingsOpen=false;modal=`<div class="modal"><div class="modalbox v199-notifications"><h2>Notifications</h2><div class="notify-row"><div><b>Automatic notifications</b><div class="muted">Notify me about updates I am permitted to view.</div></div><label class="switch"><input type="checkbox" ${user.notificationsEnabled?'checked':''} onchange="enableNotifications(this.checked)"><span></span></label></div><p class="muted">Select a notification to open its exact Work Item, issue, date, document or clocking record.</p><div class="sectionhead"><h3>Recent notifications</h3>${unread?`<button type="button" class="btn light smallbtn" onclick="markNotificationsRead()">Mark ${unread} read</button>`:''}</div><div class="notification-list">${items.length?items.map(row=>`<button type="button" class="notification-item ${(row.readBy||[]).includes(user.id)?'':'unread'}" onclick="v199OpenNotification('${row.id}')"><span><b>${v195Esc(row.text)}</b><small>${v195Esc(row.actorName||'SiteSight')} · ${new Date(row.createdAt).toLocaleString()}</small></span><strong>Open →</strong></button>`).join(''):'<div class="empty">No notifications yet.</div>'}</div><div class="actions"><button type="button" class="btn light" onclick="modal=null;render()">Close</button></div></div></div>`;render()};

// ---- Small, predictable in-app Back history -------------------------
let v1994History=[],v1994LastState=null,v1994HistoryUser='',v1994Restoring=false;
function v1994ReadState(){return {view,site,selected:selected||'',selectedGang:selectedGang||'',contractorAttendanceTab:typeof contractorAttendanceTab==='undefined'?'':contractorAttendanceTab,attendanceTab:typeof attendanceTab==='undefined'?'':attendanceTab,commercialTab:typeof v192CommercialTab==='undefined'?'':v192CommercialTab,adminTab:typeof adminTab==='undefined'?'':adminTab,documentFolder:typeof v191DocumentFolder==='undefined'?'':v191DocumentFolder}}
function v1994StateKey(state){return JSON.stringify(state)}
function v1994RestoreState(state){view=state.view;site=state.site;selected=state.selected||null;selectedGang=state.selectedGang||null;if(typeof contractorAttendanceTab!=='undefined')contractorAttendanceTab=state.contractorAttendanceTab||'today';if(typeof attendanceTab!=='undefined')attendanceTab=state.attendanceTab||'today';if(typeof v192CommercialTab!=='undefined')v192CommercialTab=state.commercialTab||v192CommercialTab;if(typeof adminTab!=='undefined')adminTab=state.adminTab||adminTab;if(typeof v191DocumentFolder!=='undefined')v191DocumentFolder=state.documentFolder||''}
function v1994GoBack(){if(!v1994History.length)return;if(typeof dirty!=='undefined'&&dirty&&!confirm('Leave this page without saving your changes?'))return;if(typeof clearDirty==='function')clearDirty();let state=v1994History.pop();v1994Restoring=true;modal=null;settingsOpen=false;v1994RestoreState(state);render();v1994Restoring=false}
const v1994BaseRender=render;
render=function(){let user=currentUser(),userId=user?.id||'';if(userId!==v1994HistoryUser){v1994History=[];v1994LastState=null;v1994HistoryUser=userId}let next=user?v1994ReadState():null;if(user&&v1994LastState&&!v1994Restoring&&v1994StateKey(next)!==v1994StateKey(v1994LastState)){v1994History.push(v1994LastState);if(v1994History.length>30)v1994History.shift()}let result=v1994BaseRender();if(user)v1994LastState=v1994ReadState();let root=document.getElementById('root');if(root&&user&&v1994History.length&&!modal)root.insertAdjacentHTML('beforeend',`<button type="button" class="v1994-back-overlay" onclick="v1994GoBack()" aria-label="Go back to the previous page">← <span>Back</span></button>`);return result};

// ---- V1.99.4 data defaults and release identity ---------------------
function v1994Migrate(){let changed=false;for(const siteRow of db.sites||[]){let cfg=siteRow.attendanceConfig=siteRow.attendanceConfig||{};if(!cfg.expectedStartTime){cfg.expectedStartTime='07:30';changed=true}if(!cfg.expectedEndTime){cfg.expectedEndTime='16:30';changed=true}if(cfg.lateArrivalGraceMinutes===undefined){cfg.lateArrivalGraceMinutes=0;changed=true}if(cfg.earlyLeaveGraceMinutes===undefined){cfg.earlyLeaveGraceMinutes=5;changed=true}if(cfg.autoClockOutGps===undefined){cfg.autoClockOutGps=true;changed=true}if(!Array.isArray(cfg.geofenceZones)){cfg.geofenceZones=[];changed=true}if(!['week','fortnight','month'].includes(cfg.timesheetDefaultPreset)){cfg.timesheetDefaultPreset='week';changed=true}if(!['browser-allowed','native-required'].includes(cfg.nativeSecurityMode)){cfg.nativeSecurityMode='browser-allowed';changed=true}}for(const collection of [(db.siteDrawings||[]),...(db.items||[]).map(item=>item.drawings||[])])for(const drawing of collection)for(const row of drawing.takeoffs||[])if(!row.rateUnit){row.rateUnit=v1994TakeoffRateUnit(row);changed=true}return changed}
const v1994BaseMigrate=migrate;
migrate=function(persist=true){
  let attendanceExtras=new Map((db.sites||[]).map(row=>[row.id,{expectedStartTime:row.attendanceConfig?.expectedStartTime,expectedEndTime:row.attendanceConfig?.expectedEndTime,graceMinutes:row.attendanceConfig?.graceMinutes,lateArrivalGraceMinutes:row.attendanceConfig?.lateArrivalGraceMinutes,earlyLeaveGraceMinutes:row.attendanceConfig?.earlyLeaveGraceMinutes,analyticsManualMinutes:row.attendanceConfig?.analyticsManualMinutes,analyticsSystemMinutes:row.attendanceConfig?.analyticsSystemMinutes,analyticsAdminHourlyCost:row.attendanceConfig?.analyticsAdminHourlyCost,autoClockOutGps:row.attendanceConfig?.autoClockOutGps,timesheetDefaultPreset:row.attendanceConfig?.timesheetDefaultPreset,nativeSecurityMode:row.attendanceConfig?.nativeSecurityMode,geofenceZones:row.attendanceConfig?.geofenceZones}]));
  v1994BaseMigrate(false);let restored=false;for(const row of db.sites||[]){let cfg=row.attendanceConfig=row.attendanceConfig||{},before=JSON.stringify(cfg),extras=attendanceExtras.get(row.id)||{};for(const [key,value] of Object.entries(extras))if(value!==undefined)cfg[key]=value;cfg.radiusM=V178_ATTENDANCE_RADIUS_METRES;if(JSON.stringify(cfg)!==before)restored=true}let changed=v1994Migrate()||restored;if(changed&&persist)save();return changed
};
const v1994BaseShell=shell;
shell=function(content){v1994BaseShell(content);let root=document.getElementById('root');if(!root)return;root.innerHTML=root.innerHTML.replace(/v1\.99\.3 · PAYMENT CONTROL(?: DEMO)?/g,`v1.99.4 · WORKFLOW FIXES${db.demoProfile?' DEMO':''}`)};

// The public personal-test link is deliberately browser-local. It loads the
// populated fictional Demo on first use, then keeps that tester's changes on
// their own device. It does not connect test edits to the live/shared server.
if(window.SITESIGHT_HOSTED_DEMO){
  sharedMode=false;
  const v1994HostedBaseLoginScreen=loginScreen;
  window.v1994ResetHostedDemo=function(){
    if(!confirm('Reset this device back to the original fictional Demo records?'))return;
    try{localStorage.removeItem(CACHE_KEY);localStorage.removeItem(BASE_KEY);localStorage.removeItem(PENDING_KEY);localStorage.removeItem(CURRENT_USER_KEY)}catch(error){}
    location.reload()
  };
  loginScreen=function(){
    v1994HostedBaseLoginScreen();let panel=document.querySelector('.v1992-login section');
    if(panel&&!document.getElementById('v1994-hosted-test-note'))panel.insertAdjacentHTML('beforeend','<div id="v1994-hosted-test-note" class="v1994-hosted-test-note"><b>Shareable test copy</b><span>Your test changes stay on this device. GPS-verified clocking and shared multi-user updates are not connected on this link.</span><button type="button" onclick="v1994ResetHostedDemo()">Reset this test copy</button></div>')
  };
  bootSiteSight=async function(){
    document.getElementById('root').innerHTML=`<div class="boot-screen"><img src="assets/sitesight-login-logo.png" alt="SiteSight"><div class="boot-spinner"></div><b>Opening the SiteSight test copy…</b><span>Loading fictional demonstration records on this device</span></div>`;
    try{
      let response=await fetch('/data/sitesight.json',{cache:'no-store'});
      if(!response.ok)throw new Error('The demonstration records could not be loaded.');
      let payload=await response.json(),packagedDemo=payload.data||seed,hasLocalDemo=!!(db?.demoProfile&&(db.users||[]).length),demoChanged=!hasLocalDemo||!db.demoDataVersion||db.demoDataVersion!==packagedDemo.demoDataVersion;
      if(demoChanged){db=packagedDemo;currentUserId='';try{localStorage.removeItem(CURRENT_USER_KEY)}catch(error){}}
      (db.users||[]).forEach(user=>{user.password='Demo2026!';delete user.passwordHash;delete user.resetCode;delete user.resetExpires});
      syncReady=true;syncState='local';syncError='';lastSyncAt='';pendingSave=false;migrate(false);persistCache();
      if(currentUser()){role=currentUser().role;trainingMode=!!currentUser().trainingMode;view=v173HomeView(currentUser());render()}
      else{currentUserId='';try{localStorage.removeItem(CURRENT_USER_KEY)}catch(error){}loginScreen()}
    }catch(error){
      syncReady=true;syncState='error';syncError=error.message||'The test copy could not be opened.';
      document.getElementById('root').innerHTML=`<main class="v1992-login"><section><img src="assets/sitesight-login-logo.png" alt="SiteSight"><h1>Test copy unavailable</h1><p>${v195Esc(syncError)}</p><button class="btn orange" onclick="location.reload()">Try again</button></section></main>`
    }
  }
}

if((db.users||[]).length)migrate(false);
bootSiteSight();
