'use strict';

// ============================================================
// SiteSight V1.99.6 — Commercial ledger corrections
// Proposed compatibility layer. Load after mobile-v1996.js.
// This file is additive: legacy records remain readable and no
// user-entered descriptions, notes or references are rewritten.
// ============================================================

function v1996cPaymentState(payment){
  return payment?.paymentState||'Paid'
}
function v1996cIsPaid(payment){return v1996cPaymentState(payment)==='Paid'}
function v1996cIsReady(payment){return v1996cPaymentState(payment)==='Ready'}
function v1996cEntries(options={}){
  let includeAmended=options.includeAmended===true,respectAccess=options.respectAccess!==false,rows=[];
  for(const item of db.items||[]){
    if(respectAccess&&!canAccessSite(item.site))continue;
    let targets=item.type==='House Plot'?(item.lifts||[]):[item];
    targets.forEach((target,index)=>{
      for(const payment of target?.payments||[]){
        if(!includeAmended&&payment.amended)continue;
        rows.push({item,target,payment,variation:null,sourceKind:Number(payment.retentionRelease||0)>0?'retention':'set-price',stage:target?.name||item.type,targetKind:item.type==='House Plot'?'lift':'item',targetIndex:index})
      }
    });
    for(const variation of item.variations||[]){
      for(const payment of variation.payments||[]){
        if(!includeAmended&&payment.amended)continue;
        rows.push({item,target:variation,payment,variation,sourceKind:Number(payment.retentionRelease||0)>0?'retention':'variation',stage:variation.stage||variation.name||'Variation',targetKind:'variation',targetIndex:-1})
      }
    }
  }
  return rows
}
function v1996cEntryByPaymentId(id){return v1996cEntries().find(row=>row.payment.id===id)||null}
function v1996cFlatPaymentRow(entry){
  return {...entry.payment,item:entry.item,target:entry.target,stage:entry.stage,paymentType:entry.sourceKind==='retention'?'Retention release':entry.variation?'Variation':'Original Work',variationName:entry.variation?.name||entry.payment.variationName||'',sourceKind:entry.sourceKind}
}

// The Pay Run needs every non-amended state. Paid-history helpers do not.
v1996PaymentEntries=function(){return v1996cEntries()};
v1996PaymentById=function(id){return v1996cEntryByPaymentId(id)?.payment||null};
v195AllPayments=function(){return v1996cEntries().filter(row=>v1996cIsPaid(row.payment))};
allPaymentRows=function(){
  return v1996cEntries().filter(row=>v1996cIsPaid(row.payment)&&(site==='all'||row.item.site===site)).map(v1996cFlatPaymentRow).sort((a,b)=>String(b.date||'').localeCompare(String(a.date||'')))
};

function v1996cLedger(payments=[],options={}){
  let rows=(payments||[]).filter(row=>!row.amended),claims=rows.filter(row=>!Number(row.retentionRelease||0)),paid=rows.filter(v1996cIsPaid),paidClaims=claims.filter(v1996cIsPaid),ready=rows.filter(v1996cIsReady);
  let sum=(list,selector)=>v1996RoundMoney(list.reduce((total,row)=>total+Number(selector(v1996PaymentAmounts(row),row)||0),0));
  return {
    rows,claims,paid,ready,
    grossRecorded:sum(claims,amounts=>amounts.gross),
    grossPaid:sum(paidClaims,amounts=>amounts.gross),
    netPaid:sum(paid,amounts=>amounts.net),
    grossReady:sum(ready.filter(row=>!Number(row.retentionRelease||0)),amounts=>amounts.gross),
    retentionHeldPaid:sum(paidClaims,amounts=>amounts.retentionHeld),
    retentionReleasedPaid:sum(paid.filter(row=>Number(row.retentionRelease||0)>0),amounts=>amounts.retentionRelease)
  }
}
function v1996cVariationTotals(variation){
  let ledger=v1996cLedger(variation?.payments||[]),value=Math.max(0,Number(variation?.value||0));
  return {...ledger,value,remainingToRecord:Math.max(0,v1996RoundMoney(value-ledger.grossRecorded)),paidPercent:value?Math.min(100,Math.round(ledger.grossPaid/value*10000)/100):0}
}
function v1996cTargetTotals(target){
  let ledger=v1996cLedger(target?.payments||[]),setPrice=Math.max(0,Number(target?.price||0));
  return {...ledger,setPrice,remainingToRecord:Math.max(0,v1996RoundMoney(setPrice-ledger.grossRecorded)),paidPercent:setPrice?Math.min(100,Math.round(ledger.grossPaid/setPrice*10000)/100):0}
}
function v1996cItemTotals(item){
  let targets=item?.type==='House Plot'?(item.lifts||[]):[item],targetTotals=targets.map(v1996cTargetTotals),approvedVariations=(item?.variations||[]).filter(row=>(row.approvalStatus||'Approved')==='Approved'),variationTotals=approvedVariations.map(v1996cVariationTotals),sum=(rows,key)=>v1996RoundMoney(rows.reduce((total,row)=>total+Number(row[key]||0),0));
  let originalValue=sum(targetTotals,'setPrice'),variationValue=approvedVariations.reduce((total,row)=>total+Number(row.value||0),0),grossRecorded=sum(targetTotals,'grossRecorded')+sum(variationTotals,'grossRecorded'),grossPaid=sum(targetTotals,'grossPaid')+sum(variationTotals,'grossPaid'),netPaid=sum(targetTotals,'netPaid')+sum(variationTotals,'netPaid'),authorised=originalValue+variationValue;
  return {originalValue,variationValue,authorised,grossRecorded:v1996RoundMoney(grossRecorded),grossPaid:v1996RoundMoney(grossPaid),netPaid:v1996RoundMoney(netPaid),remainingToRecord:Math.max(0,v1996RoundMoney(authorised-grossRecorded))}
}

// Legacy helper names now have one unambiguous meaning. Capacity uses gross
// recorded; "paid" uses gross claims that reached the Paid state.
variationPaid=function(variation){return v1996cVariationTotals(variation).grossPaid};
variationRemaining=function(variation){return v1996cVariationTotals(variation).remainingToRecord};
variationPaidPct=function(variation){return v1996cVariationTotals(variation).paidPercent};
itemVariationPaid=function(item){return v1996RoundMoney((item?.variations||[]).filter(row=>(row.approvalStatus||'Approved')==='Approved').reduce((total,row)=>total+v1996cVariationTotals(row).grossPaid,0))};
itemVariationRemaining=function(item){return v1996RoundMoney((item?.variations||[]).filter(row=>(row.approvalStatus||'Approved')==='Approved').reduce((total,row)=>total+v1996cVariationTotals(row).remainingToRecord,0))};
itemPaidValue=function(item){let targets=item?.type==='House Plot'?(item.lifts||[]):[item];return v1996RoundMoney(targets.reduce((total,target)=>total+v1996cTargetTotals(target).grossPaid,0))};
itemTotalPaidIncludingVariations=function(item){return v1996cItemTotals(item).grossPaid};
itemRemainingValue=function(item){let targets=item?.type==='House Plot'?(item.lifts||[]):[item];return v1996RoundMoney(targets.reduce((total,target)=>total+v1996cTargetTotals(target).remainingToRecord,0))};
stagePaidAmount=function(target){return v1996cTargetTotals(target).grossPaid};
simplePaidAmount=function(target){return v1996cTargetTotals(target).grossPaid};
stageRemainingAmount=function(target){return v1996cTargetTotals(target).remainingToRecord};
simpleRemainingAmount=function(target){return v1996cTargetTotals(target).remainingToRecord};
stagePaidPct=function(target){return v1996cTargetTotals(target).paidPercent};
simplePaidPct=function(target){return v1996cTargetTotals(target).paidPercent};
v191TargetSettled=function(target){return v1996cTargetTotals(target).grossPaid};

financeOverview=function(item){
  let totals=v1996cItemTotals(item);
  return `<div class="finance-overview v1996c-finance-overview"><div class="fbox"><span>Original set price</span><b>${attendanceMoney(totals.originalValue)}</b></div><div class="fbox purple"><span>Approved variations</span><b>+ ${attendanceMoney(totals.variationValue)}</b></div><div class="fbox green"><span>Total authorised</span><b>${attendanceMoney(totals.authorised)}</b></div></div><div class="payment-summary-grid v1996c-payment-summary"><div class="payment-summary-card"><span class="muted">Gross recorded</span><b>${attendanceMoney(totals.grossRecorded)}</b></div><div class="payment-summary-card"><span class="muted">Gross paid</span><b>${attendanceMoney(totals.grossPaid)}</b></div><div class="payment-summary-card"><span class="muted">Net paid</span><b>${attendanceMoney(totals.netPaid)}</b></div><div class="payment-summary-card"><span class="muted">Available to record</span><b>${attendanceMoney(totals.remainingToRecord)}</b></div></div>`
};

function v1996cPaymentMutable(payment){
  if(!payment||v1996cIsPaid(payment)||payment.payRunId)return false;
  let terminal=['Approved','Locked','Exported','Paid'];
  return !(db.payRuns||[]).some(run=>!run.superseded&&terminal.includes(run.status)&&(run.snapshot?.lines||[]).some(line=>(line.paymentIds||[]).includes(payment.id)))
}
paymentHistory=function(entries,context){
  if(!hasPerm('view_financials'))return'';let rows=(entries||[]).filter(row=>!row.amended).slice().sort((a,b)=>String(b.date||'').localeCompare(String(a.date||'')));
  return `<div class="section"><div class="sectionhead"><h4>Payment records</h4>${hasPerm('record_payments')?`<button class="btn orange smallbtn" onclick="addPayment('${context}')">＋ Prepare payment</button>`:''}</div><div class="payment-history">${rows.map(payment=>{let amounts=v1996PaymentAmounts(payment),mutable=hasPerm('manage_payments')&&v1996cPaymentMutable(payment);return `<div class="payment-entry"><strong>${v1996Esc(v1993PaymentState(payment))}</strong><span>${v1996Esc(payment.date||'No date')}</span><span>${attendanceMoney(amounts.gross)} gross · ${attendanceMoney(amounts.net)} net</span><span>${v1996Esc(payment.paymentReference||payment.notes||'No reference')}</span>${mutable?`<button class="btn light smallbtn" onclick="v1996cDeletePaymentById('${context}','${payment.id}')">Remove</button>`:''}</div>`}).join('')||'<div class="empty-help"><b>No payment records yet.</b></div>'}</div></div>`
};
function v1996cDeletePaymentById(context,paymentId){let {target}=v190PaymentTarget(context),index=(target?.payments||[]).findIndex(row=>row.id===paymentId);if(index<0)return toast('This payment record is no longer available');return deletePayment(context,index)}
variationPaymentHistory=function(variation){
  let rows=(variation?.payments||[]).filter(row=>!row.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>Individual</th><th>State</th><th>Gross</th><th>Retention</th><th>CIS</th><th>Net</th><th>Reference / notes</th>${hasPerm('manage_payments')?'<th></th>':''}</tr></thead><tbody>${rows.map(payment=>{let amounts=v1996PaymentAmounts(payment),mutable=hasPerm('manage_payments')&&v1996cPaymentMutable(payment);return `<tr><td>${v1996Esc(payment.date||'—')}</td><td>${v1996Esc(paymentPayeeName(payment,variation.team||''))}</td><td>${v1996Esc(v1993PaymentState(payment))}</td><td>${attendanceMoney(amounts.gross)}</td><td>${attendanceMoney(amounts.retentionHeld)}</td><td>${attendanceMoney(amounts.cisDeduction)}</td><td>${attendanceMoney(amounts.net)}</td><td>${v1996Esc(payment.paymentReference||payment.notes||'—')}</td>${hasPerm('manage_payments')?`<td>${mutable?`<button class="btn light smallbtn" onclick="deleteVariationPayment('${variation.id}','${payment.id}')">Remove</button>`:''}</td>`:''}</tr>`}).join('')}</tbody></table></div>`:'<div class="empty-help"><b>No payment records for this variation.</b></div>'}</div>`
};
itemPaymentHistory=function(item){
  let rows=v1996cEntries().filter(row=>row.item.id===item.id).sort((a,b)=>String(b.payment.date||'').localeCompare(String(a.payment.date||''))),totals=v1996cItemTotals(item);
  return `<div class="card work-payment-summary"><div class="sectionhead"><div><h3>Payment records</h3><span class="muted">Ready records are separate from money actually paid</span></div></div><div class="payment-summary-grid"><div class="payment-summary-card"><span class="muted">Authorised value</span><b>${attendanceMoney(totals.authorised)}</b></div><div class="payment-summary-card"><span class="muted">Gross recorded</span><b>${attendanceMoney(totals.grossRecorded)}</b></div><div class="payment-summary-card"><span class="muted">Gross paid</span><b>${attendanceMoney(totals.grossPaid)}</b></div><div class="payment-summary-card"><span class="muted">Net paid</span><b>${attendanceMoney(totals.netPaid)}</b></div><div class="payment-summary-card"><span class="muted">Available to record</span><b>${attendanceMoney(totals.remainingToRecord)}</b></div></div><div style="overflow:auto"><table class="payment-history-table"><thead><tr><th>Date</th><th>Type</th><th>Stage / variation</th><th>Individual</th><th>State</th><th>Gross</th><th>Retention</th><th>CIS</th><th>Net</th><th>Reference / note</th></tr></thead><tbody>${rows.map(entry=>{let payment=entry.payment,amounts=v1996PaymentAmounts(payment);return `<tr><td>${v1996Esc(payment.date||'—')}</td><td>${v1996Esc(entry.sourceKind==='set-price'?'Original work':entry.sourceKind==='variation'?'Variation':'Retention release')}</td><td>${v1996Esc(entry.stage)}</td><td>${v1996Esc(paymentPayeeName({...payment,item}))}</td><td>${v1996Esc(v1993PaymentState(payment))}</td><td>${attendanceMoney(amounts.gross)}</td><td>${attendanceMoney(amounts.retentionHeld)}</td><td>${attendanceMoney(amounts.cisDeduction)}</td><td>${attendanceMoney(amounts.net)}</td><td>${v1996Esc(payment.paymentReference||payment.notes||'—')}</td></tr>`}).join('')||'<tr><td colspan="10">No payment records yet.</td></tr>'}</tbody></table></div></div>`
};
variationsBlock=function(item){
  if(!hasPerm('view_financials'))return'';let variations=item.variations||[],approved=variations.filter(row=>(row.approvalStatus||'Approved')==='Approved'),pending=variations.filter(row=>(row.approvalStatus||'Approved')!=='Approved'),approvedValue=v1996RoundMoney(approved.reduce((sum,row)=>sum+Number(row.value||0),0)),grossRecorded=v1996RoundMoney(approved.reduce((sum,row)=>sum+v1996cVariationTotals(row).grossRecorded,0)),grossPaid=v1996RoundMoney(approved.reduce((sum,row)=>sum+v1996cVariationTotals(row).grossPaid,0)),netPaid=v1996RoundMoney(approved.reduce((sum,row)=>sum+v1996cVariationTotals(row).netPaid,0)),available=v1996RoundMoney(approved.reduce((sum,row)=>sum+v1996cVariationTotals(row).remainingToRecord,0));
  return `<div class="card variation-panel"><div class="sectionhead"><div><h3>Variations / Extra Work</h3><div class="muted">Manual wording and authorised values stay separate from payment state</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 authorised totals.</span></div>`:''}<div class="variation-summary"><div class="card"><span class="muted">Approved value</span><b>${attendanceMoney(approvedValue)}</b></div><div class="card"><span class="muted">Gross recorded</span><b>${attendanceMoney(grossRecorded)}</b></div><div class="card"><span class="muted">Gross paid</span><b>${attendanceMoney(grossPaid)}</b></div><div class="card"><span class="muted">Net paid</span><b>${attendanceMoney(netPaid)}</b></div><div class="card"><span class="muted">Available to record</span><b>${attendanceMoney(available)}</b></div></div>${variations.map(variation=>{let ok=(variation.approvalStatus||'Approved')==='Approved',totals=v1996cVariationTotals(variation),state=!ok?'Awaiting Approval':totals.grossPaid>=totals.value-.005?'Paid':totals.grossReady>0&&totals.remainingToRecord<=.005?'Ready for Pay Run':totals.grossRecorded>0?'Part recorded':'Open';return `<div class="variation-card ${ok?'':'variation-awaiting'}" data-variation-id="${v1996Esc(variation.id)}"><div class="sectionhead"><div><h4>${v1996Esc(variation.name)}</h4><span class="variation-type">Variation</span> <span class="variation-approval ${ok?'approved':'pending'}">${v1996Esc(state)}</span></div><b>${attendanceMoney(variation.value||0)}</b></div><div class="variation-meta"><span>${v1996Esc(variation.date||'—')}</span><span>${v1996Esc(variation.stage||'General')}</span>${variation.instructionReference?`<span>Ref ${v1996Esc(variation.instructionReference)}</span>`:''}${ok&&variation.approvedBy?`<span>Approved by ${v1996Esc(variation.approvedBy)}</span>`:''}</div>${ok?`<div class="variation-summary"><div class="card"><span class="muted">Gross recorded</span><b>${attendanceMoney(totals.grossRecorded)}</b></div><div class="card"><span class="muted">Gross paid</span><b>${attendanceMoney(totals.grossPaid)}</b></div><div class="card"><span class="muted">Net paid</span><b>${attendanceMoney(totals.netPaid)}</b></div><div class="card"><span class="muted">Available</span><b>${attendanceMoney(totals.remainingToRecord)}</b></div></div>`:'<div class="muted" style="margin:8px 0">Payments remain locked until approval.</div>'}<div class="actions">${!ok&&hasPerm('approve_variations')?`<button class="btn green smallbtn" onclick="approveVariation('${variation.id}')">Review approval</button>`:''}${ok&&(hasPerm('record_payments')||hasPerm('edit_pay_records'))&&totals.remainingToRecord>.005?`<button class="btn green smallbtn" onclick="addVariationPayment('${variation.id}')">＋ Prepare payment</button>`:''}${hasPerm('manage_variations')&&!(variation.payments||[]).length?`<button class="btn red smallbtn danger-btn" onclick="deleteVariation('${variation.id}')">Delete</button>`:''}</div>${ok?variationPaymentHistory(variation):''}</div>`}).join('')||'<div class="empty-help"><b>No variations or extra work recorded.</b></div>'}</div>`
};

// ----- Variation payment review -----------------------------------
const v1996cBaseAddVariationPayment=addVariationPayment;
addVariationPayment=function(variationId){
  let item=(db.items||[]).find(row=>row.id===selected),variation=item?.variations?.find(row=>row.id===variationId);
  if(!item||!canAccessSite(item.site))return toast('This variation is outside your Site access');
  if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Payment permission is required');
  if(variation?.approvalStatus!=='Approved')return toast('Approve this variation before preparing payment');
  if(v1996cVariationTotals(variation).remainingToRecord<=.005)return toast('This variation is fully recorded');
  return v1996cBaseAddVariationPayment(variationId)
};
saveVariationPayment=function(variationId){
  let item=(db.items||[]).find(row=>row.id===selected),variation=item?.variations?.find(row=>row.id===variationId),gross=Number(document.getElementById('vpa')?.value||0),retentionRate=Number(document.getElementById('v1996-vret')?.value||0),memberId=document.getElementById('vpto')?.value||'',reference=(document.getElementById('v1996-vref')?.value||'').trim(),date=document.getElementById('vpd')?.value||iso(),notes=(document.getElementById('vpn')?.value||'').trim();
  if(!item||!variation||!canAccessSite(item.site))return toast('This variation is outside your Site access');
  if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Payment permission is required');
  if(variation.approvalStatus!=='Approved')return toast('Approve this variation before preparing payment');
  let remaining=v1996cVariationTotals(variation).remainingToRecord;if(gross<=0)return toast('Enter the gross amount');if(gross>remaining+.005)return toast(`Only ${attendanceMoney(remaining)} remains on this variation`);
  if(!Number.isFinite(retentionRate)||retentionRate<0||retentionRate>25)return toast('Retention must be between 0% and 25%');
  let member=v183Member(memberId);if(!member||!v1996MembersForSite(item.site).some(row=>row.member.id===memberId))return toast('Choose an individual assigned to this Site');if(!reference)return toast('Enter the invoice reference');
  let cisRegistered=!!member.cisRegistered,cisRate=v191CisRate(member),retentionHeld=v1996RoundMoney(gross*retentionRate/100),amounts=v1996PaymentAmounts({grossAmount:gross,retentionHeld,cisRegistered,cisDeductionRate:cisRate,cisGrossExVat:Math.max(0,gross-retentionHeld)});
  v1996PendingVariationPayment={itemId:item.id,variationId,memberId,reference,date,notes,gross:v1996RoundMoney(gross),retentionRate,cisRegistered,cisRate,amounts};
  modal=`<div class="modal"><div class="modalbox v1996-approval-review"><span class="att-kicker">CHECK BEFORE SAVING</span><h2>Review variation payment</h2><div class="v1996-review-grid"><span><small>Site</small><b>${v1996Esc(siteName(item.site))}</b></span><span><small>Work</small><b>${v1996Esc(item.ref)} · ${v1996Esc(variation.name)}</b></span><span><small>Individual</small><b>${v1996Esc(v183PersonName(memberId))}</b></span><span><small>Reference</small><b>${v1996Esc(reference)}</b></span><span><small>Gross</small><b>${attendanceMoney(amounts.gross)}</b></span><span><small>Retention held</small><b>${attendanceMoney(amounts.retentionHeld)}</b></span><span><small>CIS retained · ${cisRate}%</small><b>${attendanceMoney(amounts.cisDeduction)}</b></span><span><small>Net</small><b>${attendanceMoney(amounts.net)}</b></span></div><div class="field-mobile-actions"><button class="btn light" onclick="addVariationPayment('${variationId}')">Back</button><button class="btn green" onclick="v1996CommitVariationPayment()">Confirm ready</button></div></div></div>`;render()
};
v1996CommitVariationPayment=function(){
  let pending=v1996PendingVariationPayment,item=(db.items||[]).find(row=>row.id===pending?.itemId),variation=item?.variations?.find(row=>row.id===pending?.variationId),member=v183Member(pending?.memberId);
  if(!pending||!item||!variation||!canAccessSite(item.site))return toast('This variation changed. Start again.');
  if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Payment permission is required');
  if(variation.approvalStatus!=='Approved'||!member||!v1996MembersForSite(item.site).some(row=>row.member.id===member.id))return toast('This variation or individual changed. Start again.');
  let currentRegistered=!!member.cisRegistered,currentRate=v191CisRate(member);if(currentRegistered!==pending.cisRegistered||currentRate!==pending.cisRate)return toast('The CIS setup changed. Review the payment again.');
  let remaining=v1996cVariationTotals(variation).remainingToRecord;if(pending.gross<=0||pending.gross>remaining+.005)return toast('The remaining variation value changed. Review the payment again.');
  let retentionHeld=v1996RoundMoney(pending.gross*pending.retentionRate/100),amounts=v1996PaymentAmounts({grossAmount:pending.gross,retentionHeld,cisRegistered:currentRegistered,cisDeductionRate:currentRate,cisGrossExVat:Math.max(0,pending.gross-retentionHeld)}),record={id:'vp'+Date.now(),pct:Number(variation.value||0)?Math.round(amounts.gross/Number(variation.value)*10000)/100:0,date:pending.date,grossAmount:amounts.gross,retentionPercent:pending.retentionRate,retentionHeld:amounts.retentionHeld,retentionRelease:0,cisRegistered:currentRegistered,cisDeductionRate:amounts.cisRate,cisGrossExVat:amounts.cisGrossExVat,cisMaterials:amounts.cisMaterials,cisLiable:amounts.cisLiable,cisDeduction:amounts.cisDeduction,cisNet:amounts.net,amount:amounts.net,paidTo:pending.memberId,paymentReference:pending.reference,notes:pending.notes,variation:true,variationId:variation.id,variationName:variation.name,paymentState:'Ready',sourceKind:'variation',payRunId:'',paidAt:'',paidBy:'',enteredBy:currentUser()?.name||'',enteredAt:new Date().toISOString()};
  variation.payments=variation.payments||[];variation.payments.push(record);touch(item);save();audit('payments','variation payment',record.id,`${item.ref}: variation payment ready · ${pending.reference}`,item.site);emitNotification('payment',item.site,`${item.ref}: variation payment prepared for Pay Run`,item.id,{targetType:'payment',targetId:record.id,targetParentId:item.id});v1996PendingVariationPayment=null;v1996CommitDraft();modal=null;savedState('Variation payment ready for Pay Run');render()
};

// ----- Retention: actual liability versus Ready reservation --------
function v1996cRetentionPosition(target,memberId){
  let rows=(target?.payments||[]).filter(row=>!row.amended&&row.paidTo===memberId),paidClaims=rows.filter(row=>v1996cIsPaid(row)&&!Number(row.retentionRelease||0)),paidReleases=rows.filter(row=>v1996cIsPaid(row)&&Number(row.retentionRelease||0)>0),readyReleases=rows.filter(row=>v1996cIsReady(row)&&Number(row.retentionRelease||0)>0),held=v1996RoundMoney(paidClaims.reduce((sum,row)=>sum+v1996PaymentAmounts(row).retentionHeld,0)),released=v1996RoundMoney(paidReleases.reduce((sum,row)=>sum+v1996PaymentAmounts(row).retentionRelease,0)),reserved=v1996RoundMoney(readyReleases.reduce((sum,row)=>sum+v1996PaymentAmounts(row).retentionRelease,0)),actualHeld=Math.max(0,v1996RoundMoney(held-released)),available=Math.max(0,v1996RoundMoney(actualHeld-reserved));
  return {held,released,reserved,actualHeld,available}
}
v191MemberRetention=function(target,memberId){return v1996cRetentionPosition(target,memberId).actualHeld};
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 position=v1996cRetentionPosition(target,memberId);if(position.actualHeld>.004||position.reserved>.004)rows.push({item,target,index,targetKind:item.type==='House Plot'?'lift':'item',amount:position.actualHeld,reserved:position.reserved,available:position.available,eligible:!!item.signed&&position.available>.004})})}return rows.sort((a,b)=>Number(b.eligible)-Number(a.eligible)||a.item.ref.localeCompare(b.item.ref))
};
v191RetentionTotal=function(memberId,eligibleOnly=false){return v1996RoundMoney(v191RetentionRecords(memberId).filter(row=>!eligibleOnly||row.eligible).reduce((sum,row)=>sum+(eligibleOnly?row.available:row.amount),0))};
v191IndividualRetentionPanel=function(member){
  if(!hasPerm('view_pay_runs')&&!hasPerm('view_financials'))return'';let rows=v191RetentionRecords(member.id),held=v1996RoundMoney(rows.reduce((sum,row)=>sum+row.amount,0)),reserved=v1996RoundMoney(rows.reduce((sum,row)=>sum+row.reserved,0)),available=v1996RoundMoney(rows.filter(row=>row.eligible).reduce((sum,row)=>sum+row.available,0)),canPrepare=hasPerm('record_payments')||hasPerm('edit_pay_records');
  return `<div class="v191-individual-retention"><div class="sectionhead"><div><span class="att-kicker">RETENTION</span><h3>${attendanceMoney(held)} actually held</h3><small>${attendanceMoney(reserved)} prepared for a Pay Run · ${attendanceMoney(available)} available to prepare.</small></div></div>${rows.map(row=>`<div class="v191-retention-row"><span><b>${v1996Esc(row.item.ref)} · ${v1996Esc(row.target.name||row.item.type)}</b><small>${v1996Esc(siteName(row.item.site))} · held ${attendanceMoney(row.amount)}${row.reserved?` · ${attendanceMoney(row.reserved)} Ready`:''}</small></span><strong>${attendanceMoney(row.available)} available</strong>${row.eligible&&canPrepare?`<button class="btn green smallbtn" onclick="v191OpenRetentionRelease('${member.id}','${row.item.id}','${row.targetKind}',${row.index})">Prepare release</button>`:''}</div>`).join('')||'<div class="muted">No retention is currently held for this individual.</div>'}</div>`
};
v191OpenRetentionRelease=function(memberId,itemId,targetKind,index){
  let item=(db.items||[]).find(row=>row.id===itemId),target=targetKind==='item'?item:item?.lifts?.[Number(index)],member=v183Member(memberId),position=v1996cRetentionPosition(target,memberId);
  if(!item||!target||!canAccessSite(item.site))return toast('This retention record is outside your Site access');if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Payment permission is required');if(!item.signed)return toast('Retention can be released after sign-off');if(position.available<=.004)return toast(position.reserved>0?'All held retention is already Ready for a Pay Run':'No retention is available to release');
  modal=`<div class="modal"><div class="modalbox"><span class="att-kicker">PREPARE RETENTION RELEASE</span><h2>${v1996Esc(member?.name||'Individual')}</h2><p class="muted">${v1996Esc(item.ref)} · ${v1996Esc(target.name||item.type)}</p><div class="v191-retention-due"><span>Actually held</span><b>${attendanceMoney(position.actualHeld)}</b><span>Already Ready</span><b>${attendanceMoney(position.reserved)}</b><span>Available now</span><b>${attendanceMoney(position.available)}</b></div><div class="field"><label>Gross retention release</label><input id="v191-release-amount" class="input" type="number" min="0.01" max="${position.available.toFixed(2)}" step="0.01" value="${position.available.toFixed(2)}" oninput="v191UpdateReleasePreview('${memberId}')"></div><div id="v191-release-preview" class="v191-payment-preview"></div><div class="two"><div class="field"><label>Payment date</label><input id="v191-release-date" class="input" type="date" value="${iso()}"></div><div class="field"><label>Invoice / release reference</label><input id="v191-release-note" class="input" placeholder="Required"></div></div><div class="field-mobile-actions"><button class="btn light" onclick="openTeamMember('${V183_INDIVIDUALS_ID}','${memberId}')">Cancel</button><button class="btn green" onclick="v191SaveRetentionRelease('${memberId}','${itemId}','${targetKind}',${Number(index)})">Prepare for Pay Run</button></div></div></div>`;render();v191UpdateReleasePreview(memberId)
};
v191SaveRetentionRelease=function(memberId,itemId,targetKind,index){
  let item=(db.items||[]).find(row=>row.id===itemId),target=targetKind==='item'?item:item?.lifts?.[Number(index)],member=v183Member(memberId),gross=Number(document.getElementById('v191-release-amount')?.value||0),reference=(document.getElementById('v191-release-note')?.value||'').trim(),date=document.getElementById('v191-release-date')?.value||iso();
  if(!item||!target||!member||!canAccessSite(item.site))return toast('This retention record is unavailable');if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Payment permission is required');if(!item.signed)return toast('This work is not signed off');let position=v1996cRetentionPosition(target,memberId);if(gross<=0||gross>position.available+.005)return toast(`Enter up to ${attendanceMoney(position.available)}`);if(!reference)return toast('Enter the invoice or release reference');
  let cisRegistered=!!member.cisRegistered,cisRate=v191CisRate(member),amounts=v1996PaymentAmounts({grossAmount:gross,retentionRelease:gross,cisRegistered,cisDeductionRate:cisRate,cisGrossExVat:gross}),record={id:'ret-'+Date.now(),date,grossAmount:amounts.gross,retentionHeld:0,retentionRelease:amounts.retentionRelease,cisRegistered,cisDeductionRate:amounts.cisRate,cisGrossExVat:amounts.cisGrossExVat,cisMaterials:amounts.cisMaterials,cisLiable:amounts.cisLiable,cisDeduction:amounts.cisDeduction,cisNet:amounts.net,amount:amounts.net,paidTo:memberId,paymentReference:reference,notes:'Retention release',paymentState:'Ready',sourceKind:'retention',payRunId:'',paidAt:'',paidBy:'',enteredBy:currentUser()?.name||'',enteredAt:new Date().toISOString()};
  target.payments=target.payments||[];target.payments.push(record);touch(item);save();audit('payments','retention release',record.id,`${item.ref}: ${attendanceMoney(amounts.gross)} retention release ready · ${reference}`,item.site);emitNotification('payment',item.site,`${item.ref}: retention release prepared for Pay Run`,item.id,{targetType:'payment',targetId:record.id,targetParentId:item.id});v1996CommitDraft();modal=null;savedState('Retention release ready for Pay Run');openTeamMember(V183_INDIVIDUALS_ID,memberId)
};

// ----- Pay Run lines and immutable Paid snapshots -----------------
v190PriceLines=function(siteIds,from,to){
  return v1996cEntries().filter(row=>siteIds.includes(row.item.site)&&String(row.payment.date||'')>=from&&String(row.payment.date||'')<=to&&v1996cIsReady(row.payment)&&!row.payment.payRunId).map(row=>{let payment=row.payment,amounts=v1996PaymentAmounts(payment),member=v183Member(payment.paidTo),kind=row.sourceKind==='variation'?'Variation':row.sourceKind==='retention'?'Retention release':'Price Work';return {id:`ready:${payment.id}`,paymentIds:[payment.id],memberId:payment.paidTo||'',name:v183PersonName(payment.paidTo,payment.payeeName||'Individual'),role:member?.role||'',kind,siteIds:[row.item.site],ref:row.item.ref,stage:row.stage||'Payment',recorded:0,payable:0,overtime:0,earned:amounts.gross,paidBefore:0,outstanding:amounts.gross,grossDue:amounts.gross,retentionHeld:amounts.retentionHeld,retentionDue:amounts.retentionRelease,cisRate:amounts.cisRate,cisDeduction:amounts.cisDeduction,netDue:amounts.net,sourceHash:v175Hash(JSON.stringify({id:payment.id,gross:amounts.gross,retentionHeld:amounts.retentionHeld,retentionRelease:amounts.retentionRelease,cis:amounts.cisDeduction,net:amounts.net,date:payment.date,reference:payment.paymentReference,state:payment.paymentState}))}}
  )
};
const v1996cBasePaySnapshot=v190PaySnapshot;
v190PaySnapshot=function(siteIds,from,to){
  let snapshot=v1996cBasePaySnapshot(siteIds,from,to);snapshot.totals=snapshot.totals||{};snapshot.totals.retentionHeld=v1996RoundMoney((snapshot.lines||[]).reduce((sum,row)=>sum+Number(row.retentionHeld||0),0));let {hash,...payload}=snapshot;snapshot.hash=v175Hash(JSON.stringify(payload));return snapshot
};
v191GroupedLines=function(lines){
  let groups=new Map();for(const line of lines||[]){let key=line.memberId||line.name||line.id,row=groups.get(key)||{memberId:line.memberId,name:line.name||'Unknown individual',role:line.role||'',lines:[],gross:0,retentionHeld:0,retentionRelease:0,cis:0,net:0};row.lines.push(line);row.gross+=Number((line.grossDue??line.outstanding??line.earned)||0);row.retentionHeld+=Number(line.retentionHeld||0);row.retentionRelease+=Number(line.retentionDue||0);row.cis+=Number(line.cisDeduction||0);row.net+=Number((line.netDue??line.outstanding??line.earned)||0);groups.set(key,row)}return [...groups.values()].sort((a,b)=>String(a.name).localeCompare(String(b.name)))
};
v191PayGroupHtml=function(group){
  return `<details class="v191-pay-person"><summary><span><b>${v1996Esc(group.name)}</b><small>${v1996Esc(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><small>Retention held</small><b>${attendanceMoney(group.retentionHeld)}</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){
  let gross=Number((row.grossDue??row.outstanding??row.earned)||0),net=Number((row.netDue??row.outstanding??row.earned)||0),detail=row.payable?`${Number(row.payable).toFixed(2)} hrs · ${Number(row.overtime||0).toFixed(2)} OT`:row.retentionDue?`Retention release ${attendanceMoney(row.retentionDue)}`:`Gross ${attendanceMoney(gross)}${row.retentionHeld?` · retention held ${attendanceMoney(row.retentionHeld)}`:''}${row.cisDeduction?` · CIS ${attendanceMoney(row.cisDeduction)}`:''}`;
  return `<div class="v190-pay-line"><span><b>${v1996Esc(row.kind)}</b><small>${row.ref?`${v1996Esc(row.ref)} · ${v1996Esc(row.stage)} · `:''}${(row.siteIds||[]).map(siteName).map(v1996Esc).join(', ')}</small></span><span>${detail}</span><strong>${attendanceMoney(net)} net</strong></div>`
};
v190CsvRows=function(run,draft=false){
  let snapshot=run.snapshot||{},title=run.legacySnapshot?'IMPORTED DEMONSTRATION AGGREGATE — NOT RECORD-LEVEL PROOF':draft?'DRAFT — NOT APPROVED FOR PAYMENT':run.status==='Paid'?'PAID PAY RUN':'LOCKED PAY RUN',rows=[[title],['Period',snapshot.from||run.from,snapshot.to||run.to],['Sites',(snapshot.siteIds||run.siteIds||[]).map(siteName).join(' | ')],['Status',run.status],['Snapshot checksum',run.snapshotHash||snapshot.hash||''],[],['Individual','Role','Pay type','Sites','Recorded hours','Payable hours','Late arrival deducted','Early leaving deducted','Approved overtime','Gross due','Retention held','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??line.outstanding??line.earned)||0).toFixed(2),Number(line.retentionHeld||0).toFixed(2),`${Number(line.cisRate||0)}%`,Number(line.cisDeduction||0).toFixed(2),Number(line.retentionDue||0).toFixed(2),Number((line.netDue??line.outstanding??line.earned)||0).toFixed(2),draft?'Ready':run.status==='Paid'?'Paid':'Approved']);return rows
};
function v1996cSnapshotTotals(snapshot){
  let lines=snapshot?.lines||[],sum=key=>v1996RoundMoney(lines.reduce((total,row)=>total+Number(row[key]||0),0)),fallbackGross=v1996RoundMoney(lines.reduce((total,row)=>total+Number((row.grossDue??row.outstanding??row.earned)||0),0)),fallbackNet=v1996RoundMoney(lines.reduce((total,row)=>total+Number((row.netDue??row.outstanding??row.earned)||0),0));return {grossDue:Number(snapshot?.totals?.grossDue??fallbackGross),retentionHeld:Number(snapshot?.totals?.retentionHeld??sum('retentionHeld')),cisDeduction:Number(snapshot?.totals?.cisDeduction??sum('cisDeduction')),retentionDue:Number(snapshot?.totals?.retentionDue??sum('retentionDue')),netDue:Number(snapshot?.totals?.netDue??fallbackNet)}
}
function v1996cExportPaidRun(runId){let run=(db.payRuns||[]).find(row=>row.id===runId);if(!run||run.status!=='Paid')return toast('This Paid run is unavailable');if(!hasPerm('export_pay_runs')&&!hasPerm('download_reports'))return toast('Export permission is required');v190DownloadCsv(v190CsvRows(run,false),`sitesight-paid-pay-run-${run.from}-to-${run.to}.csv`)}
function v1996cPaidRunPage(run){
  let snapshot=run.snapshot||{lines:[],siteIds:run.siteIds||[],from:run.from,to:run.to,totals:{}},totals=v1996cSnapshotTotals(snapshot),groups=v191GroupedLines(snapshot.lines||[]),integrity=v1996cStoredSnapshotCheck(run),proof=run.legacySnapshot?'<div class="panel v1996c-supplement"><b>Imported demonstration aggregate</b><span>This example total has no record-level proof and must not be treated as a live payment record.</span></div>':integrity.ok?'<div class="panel v190-all-clear"><b>✓ Stored checksum verified</b><span>The saved line-by-line snapshot matches its recorded checksum.</span></div>':'<div class="panel v1996c-supplement"><b>Payment proof needs review</b><span>'+v1996Esc(integrity.errors.join(' · '))+'</span></div>';
  return `<div class="topbar"><div><span class="att-kicker">PAID PAYMENT RECORD</span><h1>Paid Pay Run</h1><div class="sub">${v1996Esc(v174RangeLabel(run.from,run.to))} · ${v1996Esc((run.siteIds||[]).map(siteName).join(' · '))}</div></div><span class="v190-run-status paid">Paid</span></div>${proof}<div class="panel v1996c-paid-run-proof"><div class="v1996-review-grid"><span><small>Bank reference</small><b>${v1996Esc(run.paymentReference||'Not recorded')}</b></span><span><small>Paid by</small><b>${v1996Esc(run.paidBy||'Not recorded')}</b></span><span><small>Paid at</small><b>${run.paidAt?new Date(run.paidAt).toLocaleString('en-GB'):'Not recorded'}</b></span><span><small>Snapshot checksum</small><b>${v1996Esc(run.legacySnapshot?'Imported demo aggregate':run.snapshotHash||snapshot.hash||'Not recorded')}</b></span></div><p class="muted">This screen uses the exact snapshot that was marked Paid. Later Ready records do not rewrite it.</p></div><div class="v191-pay-totals"><div><span>Gross due</span><b>${attendanceMoney(totals.grossDue)}</b></div><div class="retention"><span>Retention held</span><b>${attendanceMoney(totals.retentionHeld)}</b></div><div class="cis"><span>CIS retained</span><b>${attendanceMoney(totals.cisDeduction)}</b></div><div class="retention"><span>Retention released</span><b>${attendanceMoney(totals.retentionDue)}</b></div><div class="net"><span>Net paid</span><b>${attendanceMoney(totals.netDue)}</b></div></div><div class="panel v190-pay-lines"><div class="sectionhead"><div><h2>Paid individual breakdown</h2><p class="muted">Read-only values from the approved snapshot.</p></div><b>${groups.length} individual${groups.length===1?'':'s'}</b></div>${groups.map(v191PayGroupHtml).join('')||'<div class="muted">No individual lines were retained in this legacy snapshot.</div>'}</div>${integrity.ok&&(hasPerm('export_pay_runs')||hasPerm('download_reports'))?`<div class="panel"><button class="btn light" onclick="v1996cExportPaidRun('${run.id}')">Download Paid CSV</button></div>`:''}`
}
const v1996cBasePayRunPage=v190PayRunPage;
v190PayRunPage=function(){
  v174EnsurePayPeriod();let sites=v190SelectedSites(),valid=sites.length&&v174RangeValid(v174PayFrom,v174PayTo),run=valid?v190CurrentRun(sites,v174PayFrom,v174PayTo):null;if(run?.status==='Paid')return v1996cPaidRunPage(run);let html=v1996cBasePayRunPage();if(!valid)return html;let snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo),held=Number(snapshot?.totals?.retentionHeld||0);return html.replace('<div class="cis"><span>CIS to retain</span>',`<div class="retention"><span>Retention held</span><b>${attendanceMoney(held)}</b><small>Kept back from these claims</small></div><div class="cis"><span>CIS to retain</span>`).replace('Before CIS deductions','Before retention and CIS deductions')
};

// ----- Exact Accounts reconciliation in the visible Site scope ----
function v1996cRunSiteKey(run){return [...v1993RunSiteIds(run)].sort().join('|')}
let v1996cSelectedRunId='';
const v1996cOpenRunBeforeExact=v1993OpenRun;
v1993OpenRun=function(runId){let run=(db.payRuns||[]).find(function(row){return row.id===runId});v1996cSelectedRunId=run&&(run.status==='Paid'||run.superseded)?run.id:'';return v1996cOpenRunBeforeExact(runId)};
function v1996cClearSelectedRun(){v1996cSelectedRunId='';render()}
v1993OwnerSummaryData=function(){
  v1993ApplySavedPayPeriod();v174EnsurePayPeriod();let siteIds=v1993OwnerSiteIds(),siteKey=[...siteIds].sort().join('|'),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)),paidToPeriodEnd=v195AllPayments().filter(row=>siteIds.includes(row.item.site)&&String(row.payment.date||'')<=v174PayTo),runs=(db.payRuns||[]).filter(run=>!run.superseded&&run.from===v174PayFrom&&run.to===v174PayTo&&v1996cRunSiteKey(run)===siteKey),variations=items.flatMap(item=>(item.variations||[]).filter(row=>(row.approvalStatus||'Approved')==='Approved'&&v174DateInRange(row.date,v174PayFrom,v174PayTo))),retentionBalance=paidToPeriodEnd.reduce((sum,row)=>sum+Number(row.payment.retentionHeld||0)-Number(row.payment.retentionRelease||0),0);
  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:Math.max(0,v1996RoundMoney(retentionBalance)),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||[]}
};
v1993OwnerMetricRows=function(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 paid records',attendanceMoney(data.grossPaid)],['Retention still held at period end',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(function(row){return !['Plots','Construction completion','Completed work value'].includes(row[0])})};
function v1996cRunDirectExpected(run){let direct=(run?.snapshot?.lines||[]).filter(function(line){let id=String(line.id||'');return !id.startsWith('time-shift:')&&!id.startsWith('time-adjustment:')}),invalid=direct.filter(function(line){return !Array.isArray(line.paymentIds)||line.paymentIds.length!==1||!line.sourceHash}),lines=direct.filter(function(line){return !invalid.includes(line)}),ids=[...new Set(lines.flatMap(function(line){return line.paymentIds||[]}))],net=v1996RoundMoney(lines.reduce(function(sum,line){return sum+Number(line.netDue||0)},0));return {direct:direct,lines:lines,invalid:invalid,ids:ids,net:net}}
function v1996cLinkedRunCheck(run,entries){
  let expected=v1996cRunDirectExpected(run),byId=new Map(entries.map(row=>[row.payment.id,row])),expectedRows=expected.ids.map(id=>byId.get(id)).filter(Boolean),missing=expected.ids.filter(id=>!byId.has(id)),wrong=expectedRows.filter(row=>!v1996cIsPaid(row.payment)||row.payment.payRunId!==run.id),extras=entries.filter(row=>row.payment.payRunId===run.id&&!expected.ids.includes(row.payment.id)),actual=v1996RoundMoney(expectedRows.filter(row=>v1996cIsPaid(row.payment)&&row.payment.payRunId===run.id).reduce((sum,row)=>sum+v1996PaymentAmounts(row.payment).net,0));return {expected,expectedRows,missing,wrong,extras,actual,delta:v1996RoundMoney(actual-expected.net)}
}
v195AccountsReconciliation=function(){
  v174EnsurePayPeriod();let sites=v1996CommercialSiteIds(),siteKey=[...sites].sort().join('|'),valid=sites.length&&v174RangeValid(v174PayFrom,v174PayTo),scopedEntries=v1996cEntries().filter(row=>sites.includes(row.item.site)),entries=scopedEntries.filter(row=>v174DateInRange(row.payment.date,v174PayFrom,v174PayTo)),allRuns=(db.payRuns||[]).filter(run=>!run.superseded&&v1996cRunSiteKey(run)===siteKey),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'),snapshot=valid?v190PaySnapshot(sites,v174PayFrom,v174PayTo):null,ready=entries.filter(row=>v1996cIsReady(row.payment)&&!row.payment.payRunId),standalone=entries.filter(row=>v1996cIsPaid(row.payment)&&!row.payment.payRunId),linked=entries.filter(row=>v1996cIsPaid(row.payment)&&row.payment.payRunId),approvedValue=v1996RoundMoney(pendingRuns.reduce((sum,run)=>sum+Number(run.snapshot?.totals?.netDue||0),0)),paidValue=v1996RoundMoney(paidRuns.reduce((sum,run)=>sum+Number(run.snapshot?.totals?.netDue||0),0)),differences=[];
  if(valid&&!periodRuns.length)differences.push(v1993ReconcileDifference('warn','No Pay Run for this exact scope and period','Create a Draft for the Site scope shown above before reconciling.','','v192CommercialTab=\'pay-run\';render()'));
  let current=periodRuns.find(run=>!run.superseded);if(current&&current.status!=='Paid'&&snapshot&&current.snapshotHash!==snapshot.hash)differences.push(v1993ReconcileDifference('danger','Records changed after review','Refresh the Draft and approve it again before relying on the amount.','','v1993OpenRun(\''+current.id+'\')'));
  if(pendingRuns.length)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}')`));
  for(const run of paidRuns){if(!String(run.paymentReference||'').trim())differences.push(v1993ReconcileDifference('danger','Paid run missing a bank reference',v174RangeLabel(run.from,run.to),'',`v1993OpenRun('${run.id}')`));let check=v1996cLinkedRunCheck(run,scopedEntries);if(check.expected.ids.length&&(check.missing.length||check.wrong.length||check.extras.length||Math.abs(check.delta)>.01))differences.push(v1993ReconcileDifference('danger','Linked payment records do not match the Paid snapshot',`${check.missing.length} missing · ${check.wrong.length} wrong state/link · ${check.extras.length} unexpected · expected direct net ${attendanceMoney(check.expected.net)}.`,attendanceMoney(Math.abs(check.delta))+' difference',`v1993OpenRun('${run.id}')`))}
  let recentRuns=allRuns.slice().sort((a,b)=>String(b.updatedAt||b.createdAt).localeCompare(String(a.updatedAt||a.createdAt))).slice(0,8),standaloneNet=v1996RoundMoney(standalone.reduce((sum,row)=>sum+v1996PaymentAmounts(row.payment).net,0)),readyNet=v1996RoundMoney(ready.reduce((sum,row)=>sum+v1996PaymentAmounts(row.payment).net,0));
  return `<div class="v195-commercial-role-head accounts"><div><span>ACCOUNTS RECONCILIATION</span><h2>Due → Approved → Paid</h2><p>Exact Pay Run links for ${v1996Esc(site==='all'?'all permitted Sites':siteName(site))}. Time pay and legacy standalone records are shown separately.</p></div></div>${v174PeriodBar('pay',v174PayPreset,v174PayFrom,v174PayTo)}<div class="v195-reconcile-flow"><div><span>READY / DUE NOW</span><b>${attendanceMoney(snapshot?.totals?.netDue||0)}</b><small>${ready.length} direct Ready record${ready.length===1?'':'s'} · ${attendanceMoney(readyNet)}</small></div><i>→</i><div><span>APPROVED / LOCKED</span><b>${attendanceMoney(approvedValue)}</b><small>${pendingRuns.length} exact-scope run${pendingRuns.length===1?'':'s'}</small></div><i>→</i><div class="paid"><span>PAID RUNS</span><b>${attendanceMoney(paidValue)}</b><small>${paidRuns.length} exact-scope 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">Only direct records explicitly linked to each Paid run are compared with its direct snapshot lines.</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>✓ Linked records reconcile</b><span>Every current Paid-run direct record agrees with its immutable snapshot.</span></div>'}</div><div class="panel v1993-run-history"><div class="sectionhead"><div><h3>Recent exact-scope Pay Runs</h3><p class="muted">The selected Site scope is not mixed with another Site total.</p></div></div>${recentRuns.map(run=>`<button onclick="v1993OpenRun('${run.id}')"><span><b>${v1996Esc(v174RangeLabel(run.from,run.to))}</b><small>${v1996Esc((run.siteIds||run.snapshot?.siteIds||[]).map(siteName).join(' · '))} · ${v1996Esc(run.paymentReference||'No bank reference')}</small></span><em class="v1993-run-state ${String(run.status).toLowerCase()}">${v1996Esc(run.status)}</em><strong>${attendanceMoney(run.snapshot?.totals?.netDue||0)} ›</strong></button>`).join('')||'<div class="muted">No exact-scope Pay Runs yet.</div>'}</div><div class="panel v195-reconcile-list"><div class="sectionhead"><div><h3>Linked Paid direct records</h3><p class="muted">${linked.length} record${linked.length===1?'':'s'} in this period.</p></div></div>${linked.slice().sort((a,b)=>String(b.payment.date||'').localeCompare(String(a.payment.date||''))).slice(0,10).map(row=>`<div><span><b>${v1996Esc(v183PersonName(row.payment.paidTo,row.payment.payeeName||'Individual'))}</b><small>${v1996Esc(row.item.ref)} · ${v1996Esc(row.payment.bankPaymentReference||row.payment.paymentReference||'No reference')}</small></span><strong>${attendanceMoney(v1996PaymentAmounts(row.payment).net)}</strong></div>`).join('')||'<div class="muted">No linked direct records in this period.</div>'}</div><div class="panel v195-reconcile-list"><div class="sectionhead"><div><h3>Legacy standalone Paid records</h3><p class="muted">Readable but not compared with a Pay Run: ${standalone.length} record${standalone.length===1?'':'s'} · ${attendanceMoney(standaloneNet)}.</p></div></div>${standalone.slice().sort((a,b)=>String(b.payment.date||'').localeCompare(String(a.payment.date||''))).slice(0,10).map(row=>`<div><span><b>${v1996Esc(v183PersonName(row.payment.paidTo,row.payment.payeeName||'Individual'))}</b><small>${v1996Esc(row.item.ref)} · ${v1996Esc(row.payment.paymentReference||row.payment.notes||'No reference')}</small></span><strong>${attendanceMoney(v1996PaymentAmounts(row.payment).net)}</strong></div>`).join('')||'<div class="muted">No legacy standalone records in this period.</div>'}</div>`
};

// ----- Protect snapshot-linked records -----------------------------
const v1996cBaseOpenPaymentReconciliation=v177OpenPaymentReconciliation;
v177OpenPaymentReconciliation=function(paymentId){let payment=v176ActualPayment(paymentId)?.payment;if(payment&&!v1996cPaymentMutable(payment))return toast('This payment is fixed by an approved or Paid Pay Run. Use a correction record.');return v1996cBaseOpenPaymentReconciliation(paymentId)};
const v1996cBaseSavePaymentReconciliation=v177SavePaymentReconciliation;
v177SavePaymentReconciliation=function(paymentId){let payment=v176ActualPayment(paymentId)?.payment;if(payment&&!v1996cPaymentMutable(payment))return toast('This payment is fixed by an approved or Paid Pay Run. Use a correction record.');return v1996cBaseSavePaymentReconciliation(paymentId)};
const v1996cBaseDeletePayment=deletePayment;
deletePayment=function(context,index){let {target}=v190PaymentTarget(context),payment=target?.payments?.[Number(index)];if(payment&&!v1996cPaymentMutable(payment))return toast('This payment is fixed by an approved or Paid Pay Run and cannot be removed.');return v1996cBaseDeletePayment(context,index)};
const v1996cBaseDeleteVariationPayment=deleteVariationPayment;
deleteVariationPayment=function(variationId,paymentId){let item=(db.items||[]).find(row=>row.id===selected),variation=item?.variations?.find(row=>row.id===variationId),payment=variation?.payments?.find(row=>row.id===paymentId);if(payment&&!v1996cPaymentMutable(payment))return toast('This payment is fixed by an approved or Paid Pay Run and cannot be removed.');return v1996cBaseDeleteVariationPayment(variationId,paymentId)};

// ----- Additive derived-field migration ----------------------------
function v1996cMigrateCommercial(persist=true){
  let changed=false;for(const entry of v1996cEntries({includeAmended:true,respectAccess:false})){let payment=entry.payment,expected=Number(payment.retentionRelease||0)>0?'retention':entry.variation?'variation':'set-price';if(payment.sourceKind!==expected){payment.sourceKind=expected;changed=true}if(payment.paymentState===undefined){payment.paymentState='Paid';changed=true}if(payment.payRunId===undefined){payment.payRunId='';changed=true}}
  if(changed&&persist)save();return changed
}
const v1996cBaseMigrate=migrate;
migrate=function(persist=true){let baseChanged=!!v1996cBaseMigrate(false),commercialChanged=v1996cMigrateCommercial(false);if((baseChanged||commercialChanged)&&persist)save();return baseChanged||commercialChanged};

// ----- Final commercial integrity hardening -------------------------
// Snapshot hashes deliberately exclude workflow state. Moving a reviewed
// record from Ready to Paid must not make its financial source appear changed.
function v1996cPaymentSourceHash(payment,entry=null){
  entry=entry||v1996cEntries({respectAccess:false}).find(function(row){return row.payment.id===payment?.id})||null;
  let amounts=v1996PaymentAmounts(payment);
  return v175Hash(JSON.stringify({
    id:String(payment?.id||''),
    itemId:String(entry?.item?.id||''),
    siteId:String(entry?.item?.site||''),
    targetKind:String(entry?.targetKind||''),
    targetIndex:Number(entry?.targetIndex??-1),
    targetId:String(entry?.target?.id||''),
    variationId:String(entry?.variation?.id||payment?.variationId||''),
    gross:amounts.gross,
    retentionHeld:amounts.retentionHeld,
    retentionRelease:amounts.retentionRelease,
    cisGrossExVat:amounts.cisGrossExVat,
    cisMaterials:amounts.cisMaterials,
    cisRate:amounts.cisRate,
    cisDeduction:amounts.cisDeduction,
    net:amounts.net,
    date:String(payment?.date||''),
    reference:String(payment?.paymentReference||''),
    paidTo:String(payment?.paidTo||''),
    sourceKind:String(payment?.sourceKind||'')
  }))
}
v1996OpenPaymentResult=function(paymentId){
  let row=v1996cEntryByPaymentId(paymentId);
  if(!row||!canAccessSite(row.item.site)||!hasPerm('view_financials'))return toast('This payment is unavailable');
  let payment=row.payment,amounts=v1996PaymentAmounts(payment);
  if(v1996cIsReady(payment)){
    modal='<div class="modal"><div class="modalbox v1996-readonly-sheet" role="dialog" aria-modal="true" aria-label="Payment ready for Pay Run"><span class="att-kicker">READY FOR PAY RUN</span><h2>'+v1996Esc(row.item.ref)+' · '+v1996Esc(row.stage||'Payment')+'</h2><div class="v1996-review-grid"><span><small>Individual</small><b>'+v1996Esc(v183PersonName(payment.paidTo,payment.payeeName||'Individual'))+'</b></span><span><small>Date</small><b>'+v1996Esc(payment.date||'Not entered')+'</b></span><span><small>Gross</small><b>'+attendanceMoney(amounts.gross)+'</b></span><span><small>Retention held</small><b>'+attendanceMoney(amounts.retentionHeld)+'</b></span><span><small>CIS retained</small><b>'+attendanceMoney(amounts.cisDeduction)+'</b></span><span><small>Net due</small><b>'+attendanceMoney(amounts.net)+'</b></span><span><small>Reference</small><b>'+v1996Esc(payment.paymentReference||'Not entered')+'</b></span><span><small>State</small><b>Ready for Pay Run</b></span></div><div class="field-mobile-actions"><button class="btn light" onclick="modal=null;render()">Close</button><button class="btn orange" onclick="modal=null;v192CommercialTab=\'pay-run\';view=\'commercial\';render()">Open Pay Run</button></div></div></div>';render();return
  }
  site=row.item.site;paymentFrom=payment.date||'';paymentTo=payment.date||'';paymentSearch=payment.paymentReference||payment.bankPaymentReference||row.item.ref;if(typeof v1992PaymentPage!=='undefined')v1992PaymentPage=1;v192CommercialTab='history';view='commercial';modal=null;render()
};

v190PriceLines=function(siteIds,from,to){
  return v1996cEntries().filter(function(row){
    return siteIds.includes(row.item.site)&&String(row.payment.date||'')>=from&&String(row.payment.date||'')<=to&&v1996cIsReady(row.payment)&&!row.payment.payRunId
  }).map(function(row){
    let payment=row.payment,amounts=v1996PaymentAmounts(payment),member=v183Member(payment.paidTo),kind=row.sourceKind==='variation'?'Variation':row.sourceKind==='retention'?'Retention release':'Price Work';
    return {id:'ready:'+payment.id,paymentIds:[payment.id],memberId:payment.paidTo||'',name:v183PersonName(payment.paidTo,payment.payeeName||'Individual'),role:member?.role||'',kind:kind,siteIds:[row.item.site],ref:row.item.ref,stage:row.stage||'Payment',recorded:0,payable:0,overtime:0,earned:amounts.gross,paidBefore:0,outstanding:amounts.gross,grossDue:amounts.gross,retentionHeld:amounts.retentionHeld,retentionDue:amounts.retentionRelease,cisRate:amounts.cisRate,cisDeduction:amounts.cisDeduction,netDue:amounts.net,sourceHash:v1996cPaymentSourceHash(payment,row)}
  })
};

function v1996cTimeSourceHash(line){
  return v175Hash(JSON.stringify({
    id:line.id,
    memberId:line.memberId,
    siteIds:[...(line.siteIds||[])].sort(),
    recorded:v1996RoundMoney(line.recorded||0),
    payable:v1996RoundMoney(line.payable||0),
    overtime:v1996RoundMoney(line.overtime||0),
    lateArrivalDeducted:v1996RoundMoney(line.lateArrivalDeducted||0),
    earlyLeavingDeducted:v1996RoundMoney(line.earlyLeavingDeducted||0),
    gross:v1996RoundMoney(line.grossDue??line.earned??0),
    cisRate:Number(line.cisRate||0),
    cisDeduction:v1996RoundMoney(line.cisDeduction||0),
    net:v1996RoundMoney(line.netDue??line.outstanding??0),
    rateLines:line.rateLines||[]
  }))
}
function v1996cTimeLineFromShift(shift,member){
  let detail=v190ShiftCalculation(shift,member),earned=Number(detail.gross||0),line=v191LineCis({
    id:'time-shift:'+shift.id,
    legacyId:'time:'+member.id,
    shiftId:shift.id,
    shiftDate:detail.date,
    memberId:member.id,
    name:member.name,
    role:member.role||'',
    kind:member.employmentType,
    siteIds:[shift.siteId],
    recorded:Number(detail.recordedHours||0),
    payable:Number(detail.payableHours||0),
    overtime:Number(detail.overtimeHours||0),
    earned:earned,
    paidBefore:0,
    outstanding:earned,
    retentionDue:0,
    lateArrivalDeducted:Number(detail.lateDeductedMinutes||0)/60,
    earlyLeavingDeducted:Number(detail.earlyDeductedMinutes||0)/60,
    punctualityDeducted:Number(detail.punctualityDeductedMinutes||0)/60,
    rateLines:[{date:detail.date,hourlyRate:detail.rate.hourlyRate,overtimeRate:detail.rate.overtimeRate,dayRate:detail.rate.dayRate,employmentType:detail.rate.employmentType}]
  },member);
  line.sourceHash=v1996cTimeSourceHash(line);
  return line
}
function v1996cPaidRunsForLine(line,from,to){
  let lineSites=line.siteIds||[];
  return (db.payRuns||[]).filter(function(run){
    let runSites=v1993RunSiteIds(run),siteMatch=lineSites.every(function(id){return runSites.includes(id)});
    if(run.superseded||run.status!=='Paid'||!siteMatch)return false;
    return (run.snapshot?.lines||[]).some(function(candidate){
      if(run.legacySnapshot)return String(line.shiftDate||'')>=String(run.from||'')&&String(line.shiftDate||'')<=String(run.to||'')&&candidate.legacyClosed&&candidate.id===line.legacyId;
      return candidate.id===line.id||candidate.correctsLineId===line.id
    })
  }).sort(function(a,b){return String(b.paidAt||b.updatedAt||b.createdAt||'').localeCompare(String(a.paidAt||a.updatedAt||a.createdAt||''))})
}
function v1996cTimeClosure(line,from,to){
  let changed=null;
  for(const run of v1996cPaidRunsForLine(line,from,to)){
    let candidates=(run.snapshot?.lines||[]).filter(function(candidate){return candidate.id===line.id||candidate.correctsLineId===line.id||(run.legacySnapshot&&candidate.legacyClosed&&candidate.id===line.legacyId)});
    for(const old of candidates){
      if(run.legacySnapshot&&old.legacyClosed)return {closed:true,changed:false,run:run,old:old};
      if(old.sourceHash&&old.sourceHash===line.sourceHash)return {closed:true,changed:false,run:run,old:old};
      if(!changed)changed={closed:false,changed:true,run:run,old:old}
    }
  }
  return changed||{closed:false,changed:false,run:null,old:null}
}
function v1996cTimeAdjustment(line,closure){
  let old=closure?.old;if(!old||!closure?.run||closure.run.legacySnapshot)return null;
  let previousGross=Number(old.provedGross??old.grossDue??old.earned??0),previousNet=Number(old.provedNet??old.netDue??old.outstanding??0),previousCis=Number(old.provedCis??old.cisDeduction??0),gross=v1996RoundMoney(line.grossDue??line.earned??0),net=v1996RoundMoney(line.netDue??line.outstanding??0),cis=v1996RoundMoney(line.cisDeduction||0),grossDelta=v1996RoundMoney(gross-previousGross),netDelta=v1996RoundMoney(net-previousNet),cisDelta=v1996RoundMoney(cis-previousCis);
  if(grossDelta<=.005||netDelta<=.005||cisDelta<-.005)return null;
  return {...line,id:'time-adjustment:'+line.shiftId+':'+closure.run.id,kind:'Time correction',correctsLineId:line.id,baselineRunId:closure.run.id,baselineSourceHash:old.sourceHash||'',recorded:Math.max(0,v1996RoundMoney(Number(line.recorded||0)-Number(old.provedRecorded??old.recorded??0))),payable:Math.max(0,v1996RoundMoney(Number(line.payable||0)-Number(old.provedPayable??old.payable??0))),overtime:Math.max(0,v1996RoundMoney(Number(line.overtime||0)-Number(old.provedOvertime??old.overtime??0))),earned:grossDelta,paidBefore:0,outstanding:grossDelta,grossDue:grossDelta,cisDeduction:cisDelta,netDue:netDelta,provedGross:gross,provedNet:net,provedCis:cis,provedRecorded:Number(line.recorded||0),provedPayable:Number(line.payable||0),provedOvertime:Number(line.overtime||0)}
}
let v1996cTimeChangeCache={key:'',rows:[]};
function v1996cBuildTimeLines(siteIds,from,to){
  let due=[],changed=[],currentIds=new Set();
  for(const workforce of attendanceAllMembers()){
    for(const siteId of siteIds){
      let shifts=v174ShiftsForRange(workforce.member.id,from,to,siteId);
      if(!shifts.length)continue;
      for(const shift of shifts){
        let line=v1996cTimeLineFromShift(shift,workforce.member);currentIds.add(line.id);
        let closure=v1996cTimeClosure(line,from,to);
        if(closure.changed){let adjustment=v1996cTimeAdjustment(line,closure);if(adjustment)due.push(adjustment);else changed.push({type:'changed',line:line,run:closure.run})}
        else if(!closure.closed)due.push(line)
      }
    }
  }
  let seen=new Set(changed.map(function(row){return row.type+'|'+row.line.id}));
  for(const run of db.payRuns||[]){
    if(run.superseded||run.status!=='Paid'||run.legacySnapshot||!v1993RunSiteIds(run).some(function(id){return siteIds.includes(id)}))continue;
    for(const old of run.snapshot?.lines||[]){
      if(!String(old.id||'').startsWith('time-shift:')||!String(old.shiftDate||'')||old.shiftDate<from||old.shiftDate>to||!(old.siteIds||[]).some(function(id){return siteIds.includes(id)})||currentIds.has(old.id))continue;
      let shiftId=old.shiftId||String(old.id).slice('time-shift:'.length),shift=(db.attendanceShifts||[]).find(function(row){return row.id===shiftId}),workforce=attendanceMemberById(old.memberId);
      if(shift&&!shift.voided&&workforce?.member){let current=v1996cTimeLineFromShift(shift,workforce.member),closure=v1996cTimeClosure(current,from,to);currentIds.add(current.id);if(closure.closed)continue;let key='changed|'+current.id;if(seen.has(key))continue;seen.add(key);changed.push({type:'changed',line:current,run:closure.run||run});continue}
      let key='missing|'+old.id;if(seen.has(key))continue;seen.add(key);changed.push({type:'missing',line:old,run:run})
    }
  }
  v1996cTimeChangeCache={key:[...siteIds].sort().join('|')+'|'+from+'|'+to,rows:changed};
  return due
}
v190TimeLines=function(siteIds,from,to){return v1996cBuildTimeLines(siteIds,from,to)};
const v1996cPayExceptionsBeforeClosure=v190PayExceptions;
v190PayExceptions=function(siteIds,from,to){
  let rows=v1996cPayExceptionsBeforeClosure(siteIds,from,to),key=[...siteIds].sort().join('|')+'|'+from+'|'+to;
  if(v1996cTimeChangeCache.key!==key)v1996cBuildTimeLines(siteIds,from,to);
  for(const changed of v1996cTimeChangeCache.rows){let missing=changed.type==='missing';rows.push({type:missing?'Paid shift missing or voided':'Paid time changed',person:changed.line.name,detail:siteName(changed.line.siteIds[0])+' · '+(changed.line.shiftDate||'shift')+' · '+(missing?'restore the paid shift or complete a controlled reversal':'reapprove the timesheet; increases become a separate correction and reductions require a controlled reversal'),fixType:'attendance',tab:'timesheets',workerId:changed.line.memberId,siteId:changed.line.siteIds[0]})}
  return rows
};

function v1996cRunIntersectsSites(run,siteIds){let selected=new Set(siteIds);return v1993RunSiteIds(run).some(function(id){return selected.has(id)})}
function v1996cStoredSnapshotCheck(run){
  if(!run)return {ok:false,legacy:false,errors:['Pay Run not found']};
  if(run.legacySnapshot)return {ok:!!db.demoProfile,legacy:true,errors:db.demoProfile?[]:['Imported demonstration snapshots cannot be used outside demo mode']};
  let snapshot=run.snapshot,errors=[];
  if(!snapshot||typeof snapshot!=='object')return {ok:false,legacy:false,errors:['Stored snapshot is missing']};
  let storedHash=String(snapshot.hash||''),runHash=String(run.snapshotHash||''),payload={...snapshot};delete payload.hash;let calculated=v175Hash(JSON.stringify(payload));
  if(!storedHash||calculated!==storedHash||calculated!==runHash)errors.push('Stored snapshot checksum does not match');
  let paymentIds=new Set(),runSites=new Set(v1993RunSiteIds(run));
  for(const line of snapshot.lines||[]){
    let id=String(line?.id||''),lineSites=line?.siteIds||[];
    if(!lineSites.length||lineSites.some(function(siteId){return !runSites.has(siteId)}))errors.push('A line has an invalid Site scope');
    if(id.startsWith('time-shift:')){if(!line.shiftId||!line.sourceHash)errors.push('A time line is missing its shift proof');continue}
    if(id.startsWith('time-adjustment:')){if(!line.shiftId||!line.correctsLineId||!line.sourceHash)errors.push('A time correction is missing its source proof');continue}
    if(!Array.isArray(line.paymentIds)||line.paymentIds.length!==1||!line.sourceHash){errors.push('A direct payment line is missing record-level proof');continue}
    if(paymentIds.has(line.paymentIds[0]))errors.push('A direct payment is duplicated in the snapshot');paymentIds.add(line.paymentIds[0])
  }
  return {ok:errors.length===0,legacy:false,errors:errors,calculated:calculated}
}
v190CreateDraft=function(){
  v174EnsurePayPeriod();
  if(!hasPerm('approve_pay_runs'))return toast('Pay Run preparation permission is required');
  let sites=v190SelectedSites();
  if(!sites.length)return toast('Select at least one Site');
  if(!sites.every(canAccessSite))return toast('One of these Sites is outside your access');
  if(!v174RangeValid(v174PayFrom,v174PayTo))return toast('Choose a valid pay period');
  let snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo),old=v190CurrentRun(sites,v174PayFrom,v174PayTo);
  if(old?.status==='Draft'){
    Object.assign(old,{snapshot:snapshot,snapshotHash:snapshot.hash,updatedAt:new Date().toISOString(),updatedBy:currentUser()?.name||''});save();audit('payments','pay run draft',old.id,'Draft refreshed for '+v174RangeLabel(old.from,old.to),old.siteId);v1996cSelectedRunId='';savedState('Draft Pay Run refreshed');render();return
  }
  if(!(snapshot.lines||[]).length){
    if((snapshot.exceptions||[]).length)return toast('Resolve the pay checks before creating a Draft');
    return toast('There are no new payable records in this period')
  }
  if(old&&['Approved','Locked','Exported'].includes(old.status)){
    if(typeof confirm==='function'&&!confirm('Replace the reviewed '+old.status+' Pay Run with a new Draft? The previous version will remain in the audit history.'))return;
    Object.assign(old,{superseded:true,supersededAt:new Date().toISOString(),supersededBy:currentUser()?.name||'',supersededReason:'Replaced by an authorised refreshed Draft'});audit('payments','pay run superseded',old.id,old.status+' Pay Run replaced by authorised Draft',old.siteId)
  }
  let paidParents=(db.payRuns||[]).filter(function(run){return !run.superseded&&run.status==='Paid'&&run.from===v174PayFrom&&run.to===v174PayTo&&v1996cRunIntersectsSites(run,sites)}),parentIds=paidParents.map(function(run){return run.id}),related=(db.payRuns||[]).filter(function(run){return run.from===v174PayFrom&&run.to===v174PayTo&&v1996cRunIntersectsSites(run,sites)}),now=new Date().toISOString(),run={id:'payrun-'+Date.now(),siteId:sites.length===1?sites[0]:'all',siteIds:[...sites],siteKey:[...sites].sort().join('|'),from:v174PayFrom,to:v174PayTo,status:'Draft',snapshot:snapshot,snapshotHash:snapshot.hash,supplementalFor:parentIds[0]||'',supplementalForIds:parentIds,sequence:Math.max(0,...related.map(function(row){return Number(row.sequence||0)}))+1,createdAt:now,createdBy:currentUser()?.name||'',updatedAt:now};
  db.payRuns=db.payRuns||[];db.payRuns.push(run);save();audit('payments',parentIds.length?'supplemental pay run':'pay run draft',run.id,(parentIds.length?'Supplemental Draft created':'Draft created')+' for '+v174RangeLabel(run.from,run.to),run.siteId);v1996cSelectedRunId='';savedState(parentIds.length?'Supplemental Draft created':'Draft Pay Run created');render()
};
const v1996cPayRunPageBeforeSupplement=v190PayRunPage;
v190PayRunPage=function(){
  v174EnsurePayPeriod();
  let sites=v190SelectedSites(),valid=sites.length&&v174RangeValid(v174PayFrom,v174PayTo),run=valid?v190CurrentRun(sites,v174PayFrom,v174PayTo):null;
  if(run?.status!=='Paid')return v1996cPayRunPageBeforeSupplement();
  let snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo),hasLines=(snapshot.lines||[]).length,hasChecks=(snapshot.exceptions||[]).length,canPrepare=hasPerm('approve_pay_runs');
  let prompt=hasLines?'<div class="panel v1996c-supplement"><div><span class="att-kicker">NEW RECORDS AFTER PAYMENT</span><h2>Prepare a supplemental Pay Run</h2><p class="muted">'+snapshot.lines.length+' new payment line'+(snapshot.lines.length===1?'':'s')+' and '+snapshot.exceptions.length+' check'+(snapshot.exceptions.length===1?'':'s')+'. The Paid run below remains unchanged.</p></div>'+(canPrepare?'<button class="btn orange" onclick="v190CreateDraft()">Create supplemental Draft</button>':'<span class="muted">An authorised Pay Run approver can prepare the supplement.</span>')+'</div>':hasChecks?'<div class="panel v1996c-supplement"><div><span class="att-kicker">CHECK REQUIRED</span><h2>Resolve paid-period changes</h2><p class="muted">'+snapshot.exceptions.length+' check'+(snapshot.exceptions.length===1?'':'s')+' must be resolved before a supplemental Draft can be prepared.</p></div></div>':'<div class="panel v190-all-clear"><b>✓ Paid period closed</b><span>No new or changed records are waiting for this period.</span></div>';
  return prompt+v1996cPaidRunPage(run)
};

v1993ConfirmPayRunPaid=function(runId){
  let run=(db.payRuns||[]).find(function(row){return 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');
  let runSites=v1993RunSiteIds(run);
  if(!runSites.length||!runSites.every(canAccessSite))return toast('This Pay Run includes a Site outside your access');
  let stored=v1996cStoredSnapshotCheck(run);
  if(!stored.ok)return toast(stored.legacy?'Imported snapshots can only be confirmed in the demonstration':'The stored Pay Run proof is invalid. Create and review a new Draft.');
  let snapshot=run.legacySnapshot?run.snapshot:v190PaySnapshot(runSites,run.from,run.to);
  if(!run.legacySnapshot&&run.snapshotHash!==snapshot.hash)return toast('The pay information changed. Review and lock the run again.');
  let ids=v1996RunPaymentIds(run),entries=ids.map(function(id){return v1996cEntryByPaymentId(id)}),records=entries.map(function(entry){return entry?.payment});
  if(!run.legacySnapshot&&records.some(function(payment,index){
    if(!payment||payment.paymentState!=='Ready'||payment.payRunId||!entries[index]||!canAccessSite(entries[index].item.site))return true;
    let line=(run.snapshot?.lines||[]).find(function(candidate){return (candidate.paymentIds||[]).includes(ids[index])});
    return !line||line.sourceHash!==v1996cPaymentSourceHash(payment,entries[index])
  }))return toast('A payment in this run changed. Review the run again.');
  let now=new Date().toISOString();
  for(const payment of records)Object.assign(payment,{paymentState:'Paid',payRunId:run.id,paidAt:now,paidBy:currentUser()?.name||'',bankPaymentReference:reference});
  let targets=new Set(),items=new Set();for(const entry of entries){if(!entry)continue;if(!entry.variation)targets.add(entry.target);items.add(entry.item)}for(const target of targets)v1996cNormalizeTarget(target);for(const item of items)touch(item);
  Object.assign(run,{status:'Paid',paidAt:now,paidBy:currentUser()?.name||'',paymentReference:reference,updatedAt:now});
  save();audit('payments','pay run paid',run.id,'Pay Run marked paid · '+reference+' · '+ids.length+' linked payment records',run.siteId);v1996CommitDraft();modal=null;savedState('Pay Run paid and records linked');render()
};
function v1996cExportPaidRunChecked(runId){
  let run=(db.payRuns||[]).find(function(row){return row.id===runId});
  if(!run||run.status!=='Paid')return toast('This Paid run is unavailable');
  if(!hasPerm('export_pay_runs')&&!hasPerm('download_reports'))return toast('Export permission is required');
  if(!v1993RunSiteIds(run).every(canAccessSite))return toast('This Pay Run includes a Site outside your access');
  if(!v1996cStoredSnapshotCheck(run).ok)return toast('This Pay Run cannot be exported because its stored proof does not verify');
  v190DownloadCsv(v190CsvRows(run,false),'sitesight-paid-pay-run-'+run.from+'-to-'+run.to+'.csv')
}
v1996cExportPaidRun=v1996cExportPaidRunChecked;

function v1996cOwnerPeriodRuns(siteIds,from,to){
  return (db.payRuns||[]).filter(function(run){return !run.superseded&&run.from===from&&run.to===to&&v1996cRunIntersectsSites(run,siteIds)})
}
function v1996cRunLineKey(run,line,index){
  if((line.paymentIds||[]).length===1)return 'payment:'+line.paymentIds[0];
  if(String(line.id||'').startsWith('time-shift:'))return 'shift:'+line.id;
  if(String(line.id||'').startsWith('time-adjustment:'))return 'adjustment:'+line.id+':'+String(line.sourceHash||'');
  return 'legacy-or-other:'+run.id+':'+String(line.id||index)
}
function v1996cRunTotalsForSites(runs,siteIds,statuses=null){
  let selected=new Set(siteIds),seen=new Set(),totals={grossDue:0,retentionHeld:0,cisDeduction:0,retentionDue:0,netDue:0,lineCount:0,legacyCount:0};
  for(const run of runs||[]){
    if(statuses&&!statuses.includes(run.status))continue;
    let usedLine=false,lines=run.snapshot?.lines||[];if(run.legacySnapshot)totals.legacyCount++;
    lines.forEach(function(line,index){
      if((line.siteIds||[]).length&&!(line.siteIds||[]).some(function(id){return selected.has(id)}))return;
      if(!(line.siteIds||[]).length&&!v1993RunSiteIds(run).every(function(id){return selected.has(id)}))return;
      let key=v1996cRunLineKey(run,line,index);if(seen.has(key))return;seen.add(key);usedLine=true;totals.lineCount++;
      totals.grossDue+=Number(line.grossDue??line.outstanding??line.earned??0);totals.retentionHeld+=Number(line.retentionHeld||0);totals.cisDeduction+=Number(line.cisDeduction||0);totals.retentionDue+=Number(line.retentionDue||0);totals.netDue+=Number(line.netDue??line.outstanding??line.earned??0)
    });
    if(!usedLine&&!lines.length&&v1993RunSiteIds(run).every(function(id){return selected.has(id)})){let fallback=v1996cSnapshotTotals(run.snapshot);totals.grossDue+=fallback.grossDue;totals.retentionHeld+=fallback.retentionHeld;totals.cisDeduction+=fallback.cisDeduction;totals.retentionDue+=fallback.retentionDue;totals.netDue+=fallback.netDue}
  }
  for(const key of ['grossDue','retentionHeld','cisDeduction','retentionDue','netDue'])totals[key]=v1996RoundMoney(totals[key]);return totals
}
function v1996cLegacyCoversEntry(entry,runs){
  return (runs||[]).some(function(run){return run.legacySnapshot&&run.status==='Paid'&&v1993RunSiteIds(run).includes(entry.item.site)&&String(entry.payment.date||'')>=String(run.from||'')&&String(entry.payment.date||'')<=String(run.to||'')})
}
v1993OwnerSummaryData=function(){
  v1993ApplySavedPayPeriod();v174EnsurePayPeriod();
  let siteIds=v1993OwnerSiteIds(),items=(db.items||[]).filter(function(item){return siteIds.includes(item.site)}),targets=items.flatMap(function(item){return item.type==='House Plot'?(item.lifts||[]):[item]}),snapshot=v174RangeValid(v174PayFrom,v174PayTo)&&siteIds.length?v190PaySnapshot(siteIds,v174PayFrom,v174PayTo):null,payments=v195AllPayments().filter(function(row){return siteIds.includes(row.item.site)&&v174DateInRange(row.payment.date,v174PayFrom,v174PayTo)}),paidToPeriodEnd=v195AllPayments().filter(function(row){return siteIds.includes(row.item.site)&&String(row.payment.date||'')<=v174PayTo}),runs=v1996cOwnerPeriodRuns(siteIds,v174PayFrom,v174PayTo),approvedTotals=v1996cRunTotalsForSites(runs,siteIds,['Approved','Locked','Exported']),paidTotals=v1996cRunTotalsForSites(runs,siteIds,['Paid']),standalone=payments.filter(function(row){return !row.payment.payRunId&&!v1996cLegacyCoversEntry(row,runs)}),variations=items.flatMap(function(item){return (item.variations||[]).filter(function(row){return (row.approvalStatus||'Approved')==='Approved'&&v174DateInRange(row.date,v174PayFrom,v174PayTo)})}),retentionBalance=paidToPeriodEnd.reduce(function(sum,row){return sum+Number(row.payment.retentionHeld||0)-Number(row.payment.retentionRelease||0)},0),cisRetained=v1996RoundMoney(paidTotals.cisDeduction+standalone.reduce(function(sum,row){return sum+Number(row.payment.cisDeduction||0)},0));
  let operational=roleCanAccessPlots();return {siteIds:siteIds,from:v174PayFrom,to:v174PayTo,plotCount:operational?items.filter(function(item){return item.type==='House Plot'}).length:0,completion:operational&&targets.length?Math.round(targets.filter(function(row){return row.status==='Complete'}).length/targets.length*100):0,setPrice:targets.reduce(function(sum,row){return sum+Number(row.price||0)},0),completedValue:operational?targets.filter(function(row){return row.status==='Complete'}).reduce(function(sum,row){return sum+Number(row.price||0)},0):0,grossPaid:payments.reduce(function(sum,row){return sum+v191PaymentGross(row.payment)},0),retentionHeld:Math.max(0,v1996RoundMoney(retentionBalance)),cisRetained:cisRetained,netPaid:payments.reduce(function(sum,row){return sum+Number(row.payment.amount||0)},0),approved:approvedTotals.netDue,paidRuns:paidTotals.netDue,variations:variations.reduce(function(sum,row){return sum+Number(row.value||0)},0),netDue:Number(snapshot?.totals?.netDue||0),exceptions:snapshot?.exceptions||[]}
};

v1996cLinkedRunCheck=function(run,entries){
  let expected=v1996cRunDirectExpected(run),byId=new Map(entries.map(function(row){return [row.payment.id,row]})),expectedRows=expected.ids.map(function(id){return byId.get(id)}).filter(Boolean),missing=expected.ids.filter(function(id){return !byId.has(id)}),wrongState=expectedRows.filter(function(row){return !v1996cIsPaid(row.payment)||row.payment.payRunId!==run.id}),mismatched=[];
  for(const line of expected.lines){
    let row=byId.get(line.paymentIds[0]);
    if(row&&(v1996cPaymentSourceHash(row.payment,row)!==line.sourceHash||!(line.siteIds||[]).includes(row.item.site)||String(line.ref||'')!==String(row.item.ref||'')))mismatched.push(row)
  }
  let wrong=[...new Set([...wrongState,...mismatched])],extras=entries.filter(function(row){return row.payment.payRunId===run.id&&!expected.ids.includes(row.payment.id)}),actual=v1996RoundMoney(expectedRows.filter(function(row){return v1996cIsPaid(row.payment)&&row.payment.payRunId===run.id}).reduce(function(sum,row){return sum+v1996PaymentAmounts(row.payment).net},0));
  return {expected:expected,expectedRows:expectedRows,invalid:expected.invalid,missing:missing,wrong:wrong,mismatched:mismatched,extras:extras,actual:actual,delta:v1996RoundMoney(actual-expected.net)}
};

function v1996cNormalizeTarget(target){
  if(!target)return;
  let totals=v1996cTargetTotals(target),price=totals.setPrice;
  target.pct=totals.paidPercent;
  if(price>0&&totals.grossPaid>=price-.005)target.paymentState='Paid';
  else if(totals.grossPaid>.005)target.paymentState='Part Paid';
  else if(totals.grossReady>.005)target.paymentState='Ready';
  else if(target.status==='Complete')target.paymentState='Earned';
  else target.paymentState='Open'
}
const v1996cReviewPaymentWithProof=v1993ReviewPayment;
v1993ReviewPayment=function(context){
  let result=v1996cReviewPaymentWithProof(context),pending=v1993PendingPayment,member=v183Member(pending?.memberId);
  if(pending&&member){
    pending.reviewedCisRegistered=!!member.cisRegistered;
    pending.reviewedCisRate=v191CisRate(member);
    pending.reviewedRetentionRate=Number(pending.figures?.retentionRate||0)
  }
  return result
};
v1993CommitPayment=function(){
  let pending=v1993PendingPayment;
  if(!pending||(!hasPerm('record_payments')&&!hasPerm('edit_pay_records')))return toast('Payment review expired. Start again.');
  let resolved=v190PaymentTarget(pending.context),item=resolved.item,target=resolved.target,member=v183Member(pending.memberId),figures=pending.figures;
  if(!item||!target||!canAccessSite(item.site)||!member||!v190Allocations(target).some(function(row){return row.memberId===pending.memberId}))return toast('The payment record or Site access changed. Start again.');
  let currentRegistered=!!member.cisRegistered,currentRate=v191CisRate(member),retentionRate=Number(figures.retentionRate||0),remaining=v1996cTargetTotals(target).remainingToRecord;
  if(currentRegistered!==pending.reviewedCisRegistered||currentRate!==pending.reviewedCisRate)return toast('The CIS setup changed. Review the payment again.');
  if(retentionRate!==pending.reviewedRetentionRate||!Number.isFinite(retentionRate)||retentionRate<0||retentionRate>25)return toast('The retention setup changed. Review the payment again.');
  if(figures.gross<=0||figures.gross>remaining+.005)return toast('The available set price changed. Review the payment again.');
  let retentionHeld=v1996RoundMoney(figures.gross*retentionRate/100),amounts=v1996PaymentAmounts({grossAmount:figures.gross,retentionHeld:retentionHeld,cisRegistered:currentRegistered,cisDeductionRate:currentRate,cisGrossExVat:Math.max(0,figures.gross-retentionHeld)});
  if(Math.abs(Number(figures.retention||0)-amounts.retentionHeld)>.01||Math.abs(Number(figures.cis||0)-amounts.cisDeduction)>.01||Math.abs(Number(figures.net||0)-amounts.net)>.01)return toast('The payment calculation changed. Review it again.');
  let record={id:'p'+Date.now(),pct:Number(target.price||0)?Math.round(amounts.gross/Number(target.price)*10000)/100:0,date:pending.date,grossAmount:amounts.gross,retentionPercent:retentionRate,retentionHeld:amounts.retentionHeld,retentionRelease:0,cisRegistered:currentRegistered,cisDeductionRate:amounts.cisRate,cisGrossExVat:amounts.cisGrossExVat,cisMaterials:amounts.cisMaterials,cisLiable:amounts.cisLiable,cisDeduction:amounts.cisDeduction,cisNet:amounts.net,amount:amounts.net,paidTo:pending.memberId,payeeName:'',paymentReference:pending.reference,notes:pending.notes,paymentState:'Ready',sourceKind:'set-price',payRunId:'',paidAt:'',paidBy:'',enteredBy:currentUser()?.name||'',enteredAt:new Date().toISOString()};
  target.payments=target.payments||[];target.payments.push(record);target.paidTo=pending.memberId;v1996cNormalizeTarget(target);touch(item);save();audit('payments','individual payment',record.id,item.ref+': '+attendanceMoney(amounts.net)+' net prepared for Pay Run · '+pending.reference,item.site);emitNotification('payment',item.site,item.ref+': payment prepared for Pay Run',item.id,{targetType:'payment',targetId:record.id,targetParentId:item.id});v1993PendingPayment=null;v1996CommitDraft();modal=null;savedState('Payment ready for Pay Run');render()
};

function v1996cRetentionTarget(item,targetKind,index){
  if(!item)return null;
  if(targetKind==='item')return item;
  if(targetKind==='lift')return item.lifts?.[Number(index)]||null;
  if(targetKind==='variation')return item.variations?.[Number(index)]||null;
  return null
}
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(function(target,index){
      let position=v1996cRetentionPosition(target,memberId);
      if(position.actualHeld>.004||position.reserved>.004)rows.push({item:item,target:target,index:index,targetKind:item.type==='House Plot'?'lift':'item',amount:position.actualHeld,reserved:position.reserved,available:position.available,eligible:v199RetentionEligible(item)&&position.available>.004})
    });
    (item.variations||[]).forEach(function(variation,index){
      let position=v1996cRetentionPosition(variation,memberId);
      if(position.actualHeld>.004||position.reserved>.004)rows.push({item:item,target:variation,index:index,targetKind:'variation',amount:position.actualHeld,reserved:position.reserved,available:position.available,eligible:v199RetentionEligible(item)&&position.available>.004})
    })
  }
  return rows.sort(function(a,b){return Number(b.eligible)-Number(a.eligible)||String(a.item.ref||'').localeCompare(String(b.item.ref||''),undefined,{numeric:true})})
};
v191OpenRetentionRelease=function(memberId,itemId,targetKind,index){
  let item=(db.items||[]).find(function(row){return row.id===itemId}),target=v1996cRetentionTarget(item,targetKind,index),member=v183Member(memberId),position=v1996cRetentionPosition(target,memberId),operational=roleCanAccessPlots();
  if(!item||!target||!canAccessSite(item.site))return toast('This retention record is outside your Site access');
  if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Payment permission is required');
  if(!v199RetentionEligible(item))return toast(operational?'Retention can be released only after all work is complete and signed off':'This retention is not available to release');
  if(position.available<=.004)return toast(position.reserved>0?'All held retention is already Ready for a Pay Run':'No retention is available to release');
  modal='<div class="modal"><div class="modalbox"><span class="att-kicker">PREPARE RETENTION RELEASE</span><h2>'+v1996Esc(member?.name||'Individual')+'</h2><p class="muted">'+(operational?v1996Esc(item.ref)+' · '+v1996Esc(target.name||target.stage||item.type):v1996Esc(siteName(item.site))+' · Financial retention record')+'</p><div class="v191-retention-due"><span>Actually held</span><b>'+attendanceMoney(position.actualHeld)+'</b><span>Already Ready</span><b>'+attendanceMoney(position.reserved)+'</b><span>Available now</span><b>'+attendanceMoney(position.available)+'</b></div><div class="field"><label>Gross retention release</label><input id="v191-release-amount" class="input" type="number" min="0.01" max="'+position.available.toFixed(2)+'" step="0.01" value="'+position.available.toFixed(2)+'" oninput="v191UpdateReleasePreview(\''+memberId+'\')"></div><div id="v191-release-preview" class="v191-payment-preview"></div><div class="two"><div class="field"><label>Payment date</label><input id="v191-release-date" class="input" type="date" value="'+iso()+'"></div><div class="field"><label>Invoice / release reference</label><input id="v191-release-note" class="input" placeholder="Required"></div></div><div class="field-mobile-actions"><button class="btn light" onclick="openTeamMember(\''+V183_INDIVIDUALS_ID+'\',\''+memberId+'\')">Cancel</button><button class="btn green" onclick="v191SaveRetentionRelease(\''+memberId+'\',\''+itemId+'\',\''+targetKind+'\','+Number(index)+')">Prepare for Pay Run</button></div></div></div>';render();v191UpdateReleasePreview(memberId)
};
v191SaveRetentionRelease=function(memberId,itemId,targetKind,index){
  let item=(db.items||[]).find(function(row){return row.id===itemId}),target=v1996cRetentionTarget(item,targetKind,index),member=v183Member(memberId),gross=Number(document.getElementById('v191-release-amount')?.value||0),reference=(document.getElementById('v191-release-note')?.value||'').trim(),date=document.getElementById('v191-release-date')?.value||iso();
  if(!item||!target||!member||!canAccessSite(item.site))return toast('This retention record is unavailable');
  if(!hasPerm('record_payments')&&!hasPerm('edit_pay_records'))return toast('Payment permission is required');
  if(!v199RetentionEligible(item))return toast('This retention is not ready to release');
  let position=v1996cRetentionPosition(target,memberId);
  if(gross<=0||gross>position.available+.005)return toast('Enter up to '+attendanceMoney(position.available));
  if(!reference)return toast('Enter the invoice or release reference');
  let cisRegistered=!!member.cisRegistered,cisRate=v191CisRate(member),amounts=v1996PaymentAmounts({grossAmount:gross,retentionRelease:gross,cisRegistered:cisRegistered,cisDeductionRate:cisRate,cisGrossExVat:gross}),record={id:'ret-'+Date.now(),date:date,grossAmount:amounts.gross,retentionHeld:0,retentionRelease:amounts.retentionRelease,cisRegistered:cisRegistered,cisDeductionRate:amounts.cisRate,cisGrossExVat:amounts.cisGrossExVat,cisMaterials:amounts.cisMaterials,cisLiable:amounts.cisLiable,cisDeduction:amounts.cisDeduction,cisNet:amounts.net,amount:amounts.net,paidTo:memberId,paymentReference:reference,notes:'Retention release',paymentState:'Ready',sourceKind:'retention',payRunId:'',paidAt:'',paidBy:'',enteredBy:currentUser()?.name||'',enteredAt:new Date().toISOString()};
  if(targetKind==='variation')Object.assign(record,{variation:true,variationId:target.id,variationName:target.name||'Variation'});
  target.payments=target.payments||[];target.payments.push(record);touch(item);save();audit('payments','retention release',record.id,item.ref+': '+attendanceMoney(amounts.gross)+' retention release ready · '+reference,item.site);emitNotification('payment',item.site,item.ref+': retention release prepared for Pay Run',item.id,{targetType:'payment',targetId:record.id,targetParentId:item.id});v1996CommitDraft();modal=null;savedState('Retention release ready for Pay Run');openTeamMember(V183_INDIVIDUALS_ID,memberId)
};
v199ItemRetentionHeld=function(item){
  if(!item)return 0;
  let total=0,targets=[...(item.type==='House Plot'?(item.lifts||[]):[item]),...(item.variations||[])];
  for(const target of targets){
    let ids=new Set(v190Allocations(target).map(function(row){return row.memberId}));
    for(const payment of target.payments||[])if(payment.paidTo)ids.add(payment.paidTo);
    for(const memberId of ids)total+=v1996cRetentionPosition(target,memberId).actualHeld
  }
  return v1996RoundMoney(total)
};

deletePayment=function(context,index){
  if(!hasPerm('manage_payments'))return toast('Payment management permission is required');
  let resolved=v190PaymentTarget(context),item=resolved.item,target=resolved.target,payment=target?.payments?.[Number(index)];
  if(!item||!target||!payment||!canAccessSite(item.site))return toast('This payment record is unavailable');
  if(!v1996cPaymentMutable(payment))return toast('This payment is fixed by an approved or Paid Pay Run and cannot be removed.');
  if(!confirm('Remove this Ready payment record?'))return;
  target.payments.splice(Number(index),1);v1996cNormalizeTarget(target);touch(item);save();audit('payments','payment removed',payment.id,item.ref+': Ready payment removed',item.site);v1996CommitDraft();savedState('Ready payment removed');render()
};
deleteVariationPayment=function(variationId,paymentId){
  let item=(db.items||[]).find(function(row){return row.id===selected}),variation=item?.variations?.find(function(row){return row.id===variationId}),payment=variation?.payments?.find(function(row){return row.id===paymentId});
  if(!item||!canAccessSite(item.site))return toast('This variation is outside your Site access');
  if(payment&&!v1996cPaymentMutable(payment))return toast('This payment is fixed by an approved or Paid Pay Run and cannot be removed.');
  return v1996cBaseDeleteVariationPayment(variationId,paymentId)
};

v177OpenPaymentReconciliation=function(paymentId){
  if(!(hasPerm('manage_payments')||hasPerm('lock_payroll')))return toast('Payment management permission is required');
  let found=v176ActualPayment(paymentId);
  if(!found||!canAccessSite(found.item.site))return toast('This payment is outside your Site access');
  let payment=found.payment;
  if(v1996cPaymentMutable(payment))return v1996cBaseOpenPaymentReconciliation(paymentId);
  let amounts=v1996PaymentAmounts(payment),name=paymentPayeeName({...payment,item:found.item});
  modal='<div class="modal"><div class="modalbox"><span class="att-kicker">PAID RECORD CHECK</span><h2>'+v1996Esc(name)+'</h2><p class="muted">'+v1996Esc(found.item.ref)+' · '+v1996Esc(payment.date||'No date')+'</p><div class="v1996-review-grid"><span><small>Gross</small><b>'+attendanceMoney(amounts.gross)+'</b></span><span><small>Retention held</small><b>'+attendanceMoney(amounts.retentionHeld)+'</b></span><span><small>CIS retained</small><b>'+attendanceMoney(amounts.cisDeduction)+'</b></span><span><small>Net</small><b>'+attendanceMoney(amounts.net)+'</b></span></div><p class="muted">Paid amounts are locked. This check saves Accounts status and notes only; any value correction must be recorded separately.</p><div class="field"><label>Reconciliation status</label><select id="v177-recon-status" class="select">'+['Unreconciled','Matched','Checked','Query'].map(function(status){return '<option '+(payment.reconciliationStatus===status?'selected':'')+'>'+status+'</option>'}).join('')+'</select></div><div class="field"><label>Reference / accountant note</label><textarea id="v177-recon-note" class="textarea">'+v1996Esc(payment.reconciliationNote||'')+'</textarea></div><div class="field-mobile-actions"><button class="btn light" onclick="modal=null;render()">Cancel</button><button class="btn orange" onclick="v177SavePaymentReconciliation(\''+paymentId+'\')">Save check</button></div></div></div>';render()
};
v177SavePaymentReconciliation=function(paymentId){
  let found=v176ActualPayment(paymentId);
  if(!found||!canAccessSite(found.item.site))return toast('This payment is outside your Site access');
  if(v1996cPaymentMutable(found.payment))return v1996cBaseSavePaymentReconciliation(paymentId);
  if(!(hasPerm('manage_payments')||hasPerm('lock_payroll')))return toast('Payment management permission is required');
  let status=document.getElementById('v177-recon-status')?.value||'Unreconciled',note=(document.getElementById('v177-recon-note')?.value||'').trim();
  Object.assign(found.payment,{reconciliationStatus:status,reconciliationNote:note,reconciledAt:new Date().toISOString(),reconciledBy:currentUser()?.name||''});save();audit('payments','payment check',paymentId,found.item.ref+': Paid record '+status.toLowerCase()+'; amounts unchanged',found.item.site);modal=null;savedState('Paid record check saved');render()
};

const v1996cMigrateCommercialBeforeFinal=v1996cMigrateCommercial;
v1996cMigrateCommercial=function(persist=true){
  let changed=!!v1996cMigrateCommercialBeforeFinal(false);
  for(const item of db.items||[]){
    let targets=item.type==='House Plot'?(item.lifts||[]):[item];
    for(const target of targets){
      let before=JSON.stringify([target.pct,target.paymentState]);v1996cNormalizeTarget(target);if(before!==JSON.stringify([target.pct,target.paymentState]))changed=true
    }
  }
  for(const entry of v1996cEntries({includeAmended:true,respectAccess:false})){
    let expected=Number(entry.payment.retentionRelease||0)>0?'retention':entry.variation?'variation':'set-price';
    if(entry.payment.sourceKind!==expected){entry.payment.sourceKind=expected;changed=true}
  }
  if(changed&&persist)save();
  return changed
};

const v1996cPaidRunPageBeforeLegacyLabel=v1996cPaidRunPage;
v1996cPaidRunPage=function(run){
  let html=v1996cPaidRunPageBeforeLegacyLabel(run);
  if(!run?.legacySnapshot)return html;
  let old=v1996Esc(run.snapshotHash||run.snapshot?.hash||'Legacy snapshot');
  return html.replace(old,'Imported demonstration snapshot').replace('This screen uses the exact snapshot that was marked Paid. Later Ready records do not rewrite it.','This imported demonstration total is retained as a read-only historic record. New live Pay Runs use record-level proof.')
};
v190MarkPaid=function(){
  let sites=v190SelectedSites(),run=v190CurrentRun(sites,v174PayFrom,v174PayTo);
  if(!hasPerm('mark_payments_paid'))return toast('Mark-paid permission is required');
  if(!run||!['Locked','Exported'].includes(run.status))return toast('Only a current locked or exported run can be marked paid');
  if(!v1993RunSiteIds(run).every(canAccessSite))return toast('This Pay Run includes a Site outside your access');
  if(!v1996cStoredSnapshotCheck(run).ok)return toast('The stored Pay Run proof is invalid. Create and review a new Draft.');
  if(!run.legacySnapshot){
    let snapshot=v190PaySnapshot(sites,v174PayFrom,v174PayTo);
    if(run.snapshotHash!==snapshot.hash)return toast('The pay information changed. Review and lock the run again.')
  }
  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>'+v1996Esc(v174RangeLabel(run.from,run.to))+'</b></span><span><small>Sites</small><b>'+v1996Esc(v1993RunSiteIds(run).map(siteName).join(' · '))+'</b></span><span><small>Status</small><b>'+v1996Esc(run.status)+'</b></span><span><small>Net payment</small><b>'+attendanceMoney(run.snapshot?.totals?.netDue||0)+'</b></span></div>'+(run.legacySnapshot?'<p class="muted">Imported demonstration snapshot: this confirms the stored historic total without linking it to live payment records.</p>':'')+'<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()
};

const v1996cPayRunPageBeforeRetentionTile=v190PayRunPage;
v190PayRunPage=function(){
  let exact=(db.payRuns||[]).find(function(run){return run.id===v1996cSelectedRunId}),html=exact&&exact.status==='Paid'&&v1993RunSiteIds(exact).every(canAccessSite)?'<div class="panel"><button class="btn light" onclick="v1996cClearSelectedRun()">← Back to current Pay Run</button></div>'+v1996cPaidRunPage(exact):v1996cPayRunPageBeforeRetentionTile();
  if(!hasPerm('approve_pay_runs'))html=html.replace(/<button class="btn orange" onclick="v190CreateDraft\(\)">[^<]*<\/button>/g,'<button class="btn light" disabled>Approver permission required</button>');
  if(html.includes('Kept back from these claims')||html.includes('Retention held</span>'))return html;
  let sites=v190SelectedSites(),snapshot=sites.length&&v174RangeValid(v174PayFrom,v174PayTo)?v190PaySnapshot(sites,v174PayFrom,v174PayTo):null,held=Number(snapshot?.totals?.retentionHeld||0);
  return html.replace(/<div class="cis"><span>CIS (?:to retain|retained)<\/span>/,'<div class="retention"><span>Retention held</span><b>'+attendanceMoney(held)+'</b><small>Kept back from these claims</small></div>$&').replace('Before CIS deductions','Before retention and CIS deductions')
};

v174SetPriceRows=function(siteId,from,to){
  let rows=[];
  for(const item of db.items||[]){
    if((siteId!=='all'&&item.site!==siteId)||!canAccessSite(item.site))continue;
    let targets=item.type==='House Plot'?(item.lifts||[]):[item];
    targets.forEach(function(target,index){
      if(target.status!=='Complete'||Number(target.price||0)<=0)return;
      let claims=(target.payments||[]).filter(function(payment){return !payment.amended&&!Number(payment.retentionRelease||0)&&String(payment.date||'')<=to}),inRange=claims.filter(function(payment){return v174DateInRange(payment.date,from,to)}),targetLedger=v1996cLedger(claims),remaining=Math.max(0,v1996RoundMoney(Number(target.price||0)-targetLedger.grossRecorded)),completedInRange=v174DateInRange(target.completedDate,from,to),carryForward=String(target.completedDate||'')<=to&&(remaining>.005||claims.some(v1996cIsReady));
      if(!completedInRange&&!inRange.length&&!carryForward)return;
      let groups=new Map();for(const payment of claims){let key=payment.paidTo||'unassigned';if(!groups.has(key))groups.set(key,[]);groups.get(key).push(payment)}
      for(const [memberId,memberClaims] of groups){
        let ledger=v1996cLedger(memberClaims),memberActivity=memberClaims.some(function(payment){return v174DateInRange(payment.date,from,to)});if(!completedInRange&&!memberActivity&&!ledger.grossReady)continue;
        let latest=memberClaims.slice().sort(function(a,b){return String(b.date||'').localeCompare(String(a.date||''))})[0],due=ledger.grossReady,status=ledger.grossReady>.005&&ledger.grossPaid>.005?'Part Paid · Ready':ledger.grossReady>.005?'Ready for Pay Run':ledger.grossPaid>.005?'Paid':'Recorded',member=v183Member(memberId);
        rows.push({kind:'Set Price',siteId:item.site,itemId:item.id,targetKind:item.type==='House Plot'?'lift':'item',targetIndex:item.type==='House Plot'?index:-1,memberId:memberId==='unassigned'?'':memberId,ref:item.ref,stage:target.name||item.type,date:latest?.date||target.completedDate,payee:member?.name||'Unassigned individual',payeeKey:memberId==='unassigned'?'unassigned':'id:'+memberId,payeeKind:'Individual',sharePercent:0,value:ledger.grossRecorded,paid:ledger.grossPaid,ready:ledger.grossReady,recorded:ledger.grossRecorded,due:due,status:status})
      }
      if(remaining>.005)rows.push({kind:'Set Price',siteId:item.site,itemId:item.id,targetKind:item.type==='House Plot'?'lift':'item',targetIndex:item.type==='House Plot'?index:-1,memberId:'',ref:item.ref,stage:target.name||item.type,date:target.completedDate,payee:'Amount not entered',payeeKey:'unrecorded:'+item.id+':'+index,payeeKind:'Unallocated',sharePercent:0,value:remaining,paid:0,ready:0,recorded:0,due:remaining,status:'Amount not entered'})
    })
  }
  return rows
};
v174VariationRows=function(siteId,from,to){
  let rows=[];
  for(const item of db.items||[]){
    if((siteId!=='all'&&item.site!==siteId)||!canAccessSite(item.site))continue;
    for(const variation of item.variations||[]){
      if((variation.approvalStatus||'')!=='Approved')continue;
      let payments=(variation.payments||[]).filter(function(row){return !row.amended&&!Number(row.retentionRelease||0)&&String(row.date||'')<=to}),ledger=v1996cLedger(payments),value=Math.max(0,Number(variation.value||0)),remaining=Math.max(0,v1996RoundMoney(value-ledger.grossRecorded)),inRange=payments.filter(function(row){return v174DateInRange(row.date,from,to)}),latest=(inRange.length?inRange:payments).slice().sort(function(a,b){return String(b.date||'').localeCompare(String(a.date||''))})[0],date=latest?.date||variation.date;
      if(!v174DateInRange(variation.date,from,to)&&!inRange.length&&!(String(variation.date||'')<=to&&(remaining>.005||ledger.grossReady>.005)))continue;
      let memberId=latest?.paidTo||variation.team||'',due=v1996RoundMoney(ledger.grossReady+remaining),status=remaining<=.005&&ledger.grossReady>.005?'Ready for Pay Run':remaining<=.005&&ledger.grossPaid>=value-.005?'Paid':ledger.grossPaid>.005&&due>.005?'Part Paid':ledger.grossReady>.005?'Part recorded · Ready':ledger.grossRecorded>.005?'Part recorded':'Approved';
      rows.push({kind:'Variation',siteId:item.site,itemId:item.id,variationId:variation.id,memberId:memberId,ref:item.ref,stage:variation.name||variation.stage||'Variation',date:date,payee:v183PersonName(memberId,memberId?'Unassigned':'Amount not entered'),payeeKey:memberId?'id:'+memberId:'unrecorded:'+variation.id,payeeKind:memberId?'Individual':'Unallocated',value:value,paid:ledger.grossPaid,ready:ledger.grossReady,recorded:ledger.grossRecorded,due:due,status:status})
    }
  }
  return rows
};

v195CommercialSavedData=function(){
  let allowed=new Set(typeof v1996CommercialSiteIds==='function'?v1996CommercialSiteIds():activeSites().filter(function(row){return canAccessSite(row.id)}).map(function(row){return row.id})),unpaid=[],variations=[],retention=[];
  for(const item of db.items||[]){
    if(!allowed.has(item.site))continue;
    let targets=item.type==='House Plot'?(item.lifts||[]):[item];
    targets.forEach(function(target,index){
      let totals=v1996cTargetTotals(target);
      if(totals.setPrice>0&&totals.remainingToRecord>.005&&(target.status==='Complete'||item.signed))unpaid.push({item:item,target:target,index:index,remaining:totals.remainingToRecord})
    });
    for(const variation of item.variations||[])if(!['Approved','Rejected'].includes(variation.approvalStatus||''))variations.push({item:item,variation:variation})
  }
  for(const member of v183ActiveMembers())for(const row of v191RetentionRecords(member.id))if(allowed.has(row.item.site)&&row.eligible)retention.push({...row,member:member,amount:row.available});
  let entries=v1996cEntries().filter(function(row){return allowed.has(row.item.site)&&row.payment.cisRegistered}),cisPayments=entries.filter(function(row){return v1996cIsPaid(row.payment)}),cisReady=entries.filter(function(row){return v1996cIsReady(row.payment)});
  return {unpaid:unpaid,variations:variations,retention:retention,cis:v183ActiveMembers().filter(function(member){return member.cisRegistered}),cisPayments:cisPayments,cisReady:cisReady}
};
v195CommercialSavedViews=function(){
  let d=v195CommercialSavedData(),unpaidValue=d.unpaid.reduce(function(sum,row){return sum+row.remaining},0),retentionValue=d.retention.reduce(function(sum,row){return sum+row.amount},0),cisPaid=d.cisPayments.reduce(function(sum,row){return sum+Number(row.payment.cisDeduction||0)},0),cisReady=d.cisReady.reduce(function(sum,row){return sum+Number(row.payment.cisDeduction||0)},0),variationValue=d.variations.reduce(function(sum,row){return sum+Number(row.variation.value||0)},0);
  return '<div class="v195-commercial-role-head"><div><span>QUANTITY SURVEYOR</span><h2>Saved commercial checks</h2><p>Four repeatable checks, filtered to the Sites you can access.</p></div></div><div class="v195-saved-grid">'+v195SavedMetric('Completed work to record',d.unpaid.length,attendanceMoney(unpaidValue),'unpaid')+v195SavedMetric('Retention ready',d.retention.length,attendanceMoney(retentionValue),'retention')+v195SavedMetric('CIS position',d.cisPayments.length+d.cisReady.length,attendanceMoney(cisPaid)+' Paid · '+attendanceMoney(cisReady)+' Ready','cis')+v195SavedMetric('Variations awaiting review',d.variations.length,attendanceMoney(variationValue),'variations')+'</div>'+v195SavedList('Completed work still to record',d.unpaid,function(row){return row.item.ref+' · '+(row.target.name||row.item.type)},function(row){return attendanceMoney(row.remaining)},function(row){return "v195OpenItem('"+row.item.id+"')"})+v195SavedList('Retention ready to release',d.retention,function(row){return row.member.name+' · '+row.item.ref},function(row){return attendanceMoney(row.amount)},function(row){return "v195OpenPerson('"+row.member.id+"')"})+v195SavedList('Variations awaiting review',d.variations,function(row){return row.item.ref+' · '+(row.variation.name||'Variation')},function(row){return attendanceMoney(row.variation.value||0)},function(row){return "v195OpenItem('"+row.item.id+"')"})
};

// ----- Final role summaries and reconciliation ---------------------
v1996ManagementMoneyData=function(){
  if(typeof v1993ApplySavedPayPeriod==='function')v1993ApplySavedPayPeriod();if(typeof v174EnsurePayPeriod==='function')v174EnsurePayPeriod();
  let siteIds=v1996CommercialSiteIds(),valid=siteIds.length&&v174RangeValid(v174PayFrom,v174PayTo),snapshot=valid?v190PaySnapshot(siteIds,v174PayFrom,v174PayTo):null,runs=valid?v1996cOwnerPeriodRuns(siteIds,v174PayFrom,v174PayTo):[],approved=v1996cRunTotalsForSites(runs,siteIds,['Approved','Locked','Exported']),paid=v1996cRunTotalsForSites(runs,siteIds,['Paid']),currentRun=valid?v190CurrentRun(siteIds,v174PayFrom,v174PayTo):null,pendingVariations=(db.items||[]).filter(function(item){return siteIds.includes(item.site)}).flatMap(function(item){return (item.variations||[]).filter(function(row){return !['Approved','Rejected'].includes(row.approvalStatus||'')}).map(function(variation){return {item:item,variation:variation}})});
  return {siteIds:siteIds,from:v174PayFrom,to:v174PayTo,netDue:Number(snapshot?.totals?.netDue||0),approved:approved.netDue,paid:paid.netDue,payExceptions:snapshot?.exceptions||[],pendingVariations:pendingVariations,currentRun:currentRun}
};

v195CommercialSavedData=function(){
  v1993ApplySavedPayPeriod();v174EnsurePayPeriod();
  let allowedIds=typeof v1996CommercialSiteIds==='function'?v1996CommercialSiteIds():activeSites().filter(function(row){return canAccessSite(row.id)}).map(function(row){return row.id}),allowed=new Set(allowedIds),unpaid=[],variations=[],retention=[];
  for(const item of db.items||[]){
    if(!allowed.has(item.site))continue;
    let targets=item.type==='House Plot'?(item.lifts||[]):[item];targets.forEach(function(target,index){let totals=v1996cTargetTotals(target);if(totals.setPrice>0&&totals.remainingToRecord>.005&&(target.status==='Complete'||item.signed))unpaid.push({item:item,target:target,index:index,remaining:totals.remainingToRecord})});
    for(const variation of item.variations||[])if(!['Approved','Rejected'].includes(variation.approvalStatus||''))variations.push({item:item,variation:variation})
  }
  for(const member of v183ActiveMembers())for(const row of v191RetentionRecords(member.id))if(allowed.has(row.item.site)&&row.eligible)retention.push({...row,member:member,amount:row.available});
  let snapshot=allowedIds.length&&v174RangeValid(v174PayFrom,v174PayTo)?v190PaySnapshot(allowedIds,v174PayFrom,v174PayTo):null,runs=v1996cOwnerPeriodRuns(allowedIds,v174PayFrom,v174PayTo),paidRunTotals=v1996cRunTotalsForSites(runs,allowedIds,['Paid']),entries=v1996cEntries().filter(function(row){return allowed.has(row.item.site)&&row.payment.cisRegistered&&v174DateInRange(row.payment.date,v174PayFrom,v174PayTo)}),cisPayments=entries.filter(function(row){return v1996cIsPaid(row.payment)}),cisReady=entries.filter(function(row){return v1996cIsReady(row.payment)}),standalone=cisPayments.filter(function(row){return !row.payment.payRunId&&!v1996cLegacyCoversEntry(row,runs)}),cisPaidTotal=v1996RoundMoney(paidRunTotals.cisDeduction+standalone.reduce(function(sum,row){return sum+Number(row.payment.cisDeduction||0)},0)),cisReadyTotal=Number(snapshot?.totals?.cisDeduction||0),cisPaidCount=paidRunTotals.lineCount+standalone.length,cisReadyCount=(snapshot?.lines||[]).filter(function(line){return Number(line.cisDeduction||0)>.005}).length;
  return {unpaid:unpaid,variations:variations,retention:retention,cis:v183ActiveMembers().filter(function(member){return member.cisRegistered}),cisPayments:cisPayments,cisReady:cisReady,cisPaidTotal:cisPaidTotal,cisReadyTotal:cisReadyTotal,cisPaidCount:cisPaidCount,cisReadyCount:cisReadyCount}
};
v195CommercialSavedViews=function(){
  let d=v195CommercialSavedData(),unpaidValue=d.unpaid.reduce(function(sum,row){return sum+row.remaining},0),retentionValue=d.retention.reduce(function(sum,row){return sum+row.amount},0),variationValue=d.variations.reduce(function(sum,row){return sum+Number(row.variation.value||0)},0);
  return '<div class="v195-commercial-role-head"><div><span>QUANTITY SURVEYOR</span><h2>Saved commercial checks</h2><p>Four repeatable checks, filtered to the Sites you can access.</p></div></div><div class="v195-saved-grid">'+v195SavedMetric('Completed work to record',d.unpaid.length,attendanceMoney(unpaidValue),'unpaid')+v195SavedMetric('Retention ready',d.retention.length,attendanceMoney(retentionValue),'retention')+v195SavedMetric('CIS position',d.cisPaidCount+d.cisReadyCount,attendanceMoney(d.cisPaidTotal)+' Paid · '+attendanceMoney(d.cisReadyTotal)+' Ready','cis')+v195SavedMetric('Variations awaiting review',d.variations.length,attendanceMoney(variationValue),'variations')+'</div>'+v195SavedList('Completed work still to record',d.unpaid,function(row){return row.item.ref+' · '+(row.target.name||row.item.type)},function(row){return attendanceMoney(row.remaining)},function(row){return "v195OpenItem('"+row.item.id+"')"})+v195SavedList('Retention ready to release',d.retention,function(row){return row.member.name+' · '+row.item.ref},function(row){return attendanceMoney(row.amount)},function(row){return "v195OpenPerson('"+row.member.id+"')"})+v195SavedList('Variations awaiting review',d.variations,function(row){return row.item.ref+' · '+(row.variation.name||'Variation')},function(row){return attendanceMoney(row.variation.value||0)},function(row){return "v195OpenItem('"+row.item.id+"')"})
};

const v1998OperationalCommercialSavedViews=v195CommercialSavedViews;
v195CommercialSavedViews=function(){
  if(roleCanAccessPlots())return v1998OperationalCommercialSavedViews();
  let d=v195CommercialSavedData(),retentionValue=d.retention.reduce(function(sum,row){return sum+row.amount},0),variationValue=d.variations.reduce(function(sum,row){return sum+Number(row.variation.value||0)},0);
  return '<div class="v195-commercial-role-head"><div><span>COMMERCIAL CHECKS</span><h2>Saved financial checks</h2><p>Financial records only. Plot cards and progress require Supervisor access or above.</p></div></div><div class="v195-saved-grid">'+v195SavedMetric('Retention available',d.retention.length,attendanceMoney(retentionValue),'retention')+v195SavedMetric('CIS position',d.cisPaidCount+d.cisReadyCount,attendanceMoney(d.cisPaidTotal)+' Paid · '+attendanceMoney(d.cisReadyTotal)+' Ready','cis')+v195SavedMetric('Variations awaiting review',d.variations.length,attendanceMoney(variationValue),'variations')+'</div>'+v195SavedList('Retention available to release',d.retention,function(row){return row.member.name+' · '+siteName(row.item.site)},function(row){return attendanceMoney(row.amount)},function(row){return "v195OpenPerson('"+row.member.id+"')"})
};

v195AccountsReconciliation=function(){
  v174EnsurePayPeriod();
  let sites=v1996CommercialSiteIds(),operational=roleCanAccessPlots(),valid=sites.length&&v174RangeValid(v174PayFrom,v174PayTo),snapshot=valid?v190PaySnapshot(sites,v174PayFrom,v174PayTo):null,runs=valid?v1996cOwnerPeriodRuns(sites,v174PayFrom,v174PayTo):[],pendingRuns=runs.filter(function(run){return ['Approved','Locked','Exported'].includes(run.status)}),paidRuns=runs.filter(function(run){return run.status==='Paid'}),approvedTotals=v1996cRunTotalsForSites(pendingRuns,sites),paidTotals=v1996cRunTotalsForSites(paidRuns,sites),scopedEntries=v1996cEntries().filter(function(row){return sites.includes(row.item.site)}),entries=scopedEntries.filter(function(row){return v174DateInRange(row.payment.date,v174PayFrom,v174PayTo)}),ready=entries.filter(function(row){return v1996cIsReady(row.payment)&&!row.payment.payRunId}),standalone=entries.filter(function(row){return v1996cIsPaid(row.payment)&&!row.payment.payRunId&&!v1996cLegacyCoversEntry(row,runs)}),linked=entries.filter(function(row){return v1996cIsPaid(row.payment)&&row.payment.payRunId}),differences=[],current=valid?v190CurrentRun(sites,v174PayFrom,v174PayTo):null;
  if(valid&&!runs.length)differences.push(v1993ReconcileDifference('warn','No Pay Run for this period and Site selection','Create a Draft before reconciling approval and payment.','','v192CommercialTab=\'pay-run\';render()'));
  if(current&&current.status!=='Paid'&&snapshot&&current.snapshotHash!==snapshot.hash)differences.push(v1993ReconcileDifference('danger','Records changed after review','Refresh the Draft and approve it again before relying on the amount.','','v1993OpenRun(\''+current.id+'\')'));
  if(operational)for(const exception of snapshot?.exceptions||[])differences.push(v1993ReconcileDifference('danger',exception.type,exception.person+' · '+exception.detail,'','v192CommercialTab=\'pay-run\';render()'));
  if(pendingRuns.length)differences.push(v1993ReconcileDifference('warn','Approved but not marked Paid',pendingRuns.length+' run'+(pendingRuns.length===1?'':'s')+' still need payment confirmation.',attendanceMoney(approvedTotals.netDue),"v1993OpenRun('"+pendingRuns[0].id+"')"));
  for(const run of paidRuns){
    if(!String(run.paymentReference||'').trim())differences.push(v1993ReconcileDifference('danger','Paid run missing a bank reference',v174RangeLabel(run.from,run.to),'',"v1993OpenRun('"+run.id+"')"));
    if(run.legacySnapshot){differences.push(v1993ReconcileDifference('warn','Imported demo aggregate — no record-level proof',v174RangeLabel(run.from,run.to)+' · use this for demonstration only',attendanceMoney(v1996cRunTotalsForSites([run],sites).netDue),"v1993OpenRun('"+run.id+"')"));continue}
    let integrity=v1996cStoredSnapshotCheck(run);if(!integrity.ok){differences.push(v1993ReconcileDifference('danger','Stored Pay Run proof does not verify',integrity.errors.join(' · '),'',"v1993OpenRun('"+run.id+"')"));continue}
    if(!v1993RunSiteIds(run).every(canAccessSite)){differences.push(v1993ReconcileDifference('warn','Part of this Pay Run is outside your Site access','Only the permitted Site portion is included here.','',"v1993OpenRun('"+run.id+"')"));continue}
    let runEntries=v1996cEntries().filter(function(row){return v1993RunSiteIds(run).includes(row.item.site)}),check=v1996cLinkedRunCheck(run,runEntries);if(check.invalid.length||check.missing.length||check.wrong.length||check.extras.length||Math.abs(check.delta)>.01)differences.push(v1993ReconcileDifference('danger','Linked payment records do not match the Paid snapshot',check.invalid.length+' malformed · '+check.missing.length+' missing · '+check.wrong.length+' changed or wrongly linked · '+check.extras.length+' unexpected',attendanceMoney(Math.abs(check.delta))+' difference',"v1993OpenRun('"+run.id+"')"))
  }
  let recentRuns=runs.slice().sort(function(a,b){return String(b.updatedAt||b.createdAt||'').localeCompare(String(a.updatedAt||a.createdAt||''))}).slice(0,8),standaloneNet=v1996RoundMoney(standalone.reduce(function(sum,row){return sum+v1996PaymentAmounts(row.payment).net},0)),readyNet=v1996RoundMoney(ready.reduce(function(sum,row){return sum+v1996PaymentAmounts(row.payment).net},0));
  return '<div class="v195-commercial-role-head accounts"><div><span>ACCOUNTS RECONCILIATION</span><h2>Due → Approved → Paid</h2><p>Every relevant Site portion and supplemental Pay Run is included.</p></div></div>'+v174PeriodBar('pay',v174PayPreset,v174PayFrom,v174PayTo)+'<div class="v195-reconcile-flow"><div><span>READY / DUE NOW</span><b>'+attendanceMoney(snapshot?.totals?.netDue||0)+'</b><small>'+ready.length+' direct Ready record'+(ready.length===1?'':'s')+' · '+attendanceMoney(readyNet)+'</small></div><i>→</i><div><span>APPROVED / LOCKED</span><b>'+attendanceMoney(approvedTotals.netDue)+'</b><small>'+pendingRuns.length+' reviewed run'+(pendingRuns.length===1?'':'s')+'</small></div><i>→</i><div class="paid"><span>PAID RUNS</span><b>'+attendanceMoney(paidTotals.netDue)+'</b><small>'+paidRuns.length+' run'+(paidRuns.length===1?'':'s')+', supplements included</small></div></div><div class="panel v1993-reconcile-differences"><div class="sectionhead"><div><h3>Differences to check</h3><p class="muted">'+(operational?'Checks stored snapshots, exact payment links, current records and paid-time changes.':'Checks Pay Runs, payment references and stored financial records.')+'</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>✓ Payment records reconcile</b><span>Current Pay Runs, direct links and stored checksums agree.</span></div>')+'</div><div class="panel v1993-run-history"><div class="sectionhead"><div><h3>Relevant Pay Runs</h3><p class="muted">Open the exact historic or supplemental run.</p></div></div>'+((recentRuns.map(function(run){return '<button onclick="v1993OpenRun(\''+run.id+'\')"><span><b>'+v1996Esc(v174RangeLabel(run.from,run.to))+'</b><small>'+v1996Esc(v1993RunSiteIds(run).map(siteName).join(' · '))+' · '+v1996Esc(run.paymentReference||'No bank reference')+(run.supplementalFor?' · Supplemental':'')+'</small></span><em class="v1993-run-state '+String(run.status).toLowerCase()+'">'+v1996Esc(run.status)+'</em><strong>'+attendanceMoney(v1996cRunTotalsForSites([run],sites).netDue)+' ›</strong></button>'}).join(''))||'<div class="muted">No Pay Runs yet.</div>')+'</div><div class="panel v195-reconcile-list"><div class="sectionhead"><div><h3>Linked Paid direct records</h3><p class="muted">'+linked.length+' record'+(linked.length===1?'':'s')+' in this period.</p></div></div>'+((linked.slice().sort(function(a,b){return String(b.payment.date||'').localeCompare(String(a.payment.date||''))}).slice(0,10).map(function(row){let reference=row.payment.bankPaymentReference||row.payment.paymentReference||'No reference',context=operational?row.item.ref:siteName(row.item.site);return '<div><span><b>'+v1996Esc(v183PersonName(row.payment.paidTo,row.payment.payeeName||'Individual'))+'</b><small>'+v1996Esc(context)+' · '+v1996Esc(reference)+'</small></span><strong>'+attendanceMoney(v1996PaymentAmounts(row.payment).net)+'</strong></div>'}).join(''))||'<div class="muted">No linked direct records in this period.</div>')+'</div><div class="panel v195-reconcile-list"><div class="sectionhead"><div><h3>Standalone historic Paid records</h3><p class="muted">Not linked to a Pay Run: '+standalone.length+' · '+attendanceMoney(standaloneNet)+'.</p></div></div>'+((standalone.slice(0,10).map(function(row){let reference=row.payment.paymentReference||(operational?row.payment.notes:'Financial record')||'No reference',context=operational?row.item.ref:siteName(row.item.site);return '<div><span><b>'+v1996Esc(v183PersonName(row.payment.paidTo,row.payment.payeeName||'Individual'))+'</b><small>'+v1996Esc(context)+' · '+v1996Esc(reference)+'</small></span><strong>'+attendanceMoney(v1996PaymentAmounts(row.payment).net)+'</strong></div>'}).join(''))||'<div class="muted">No standalone records in this period.</div>')+'</div>'
};

// Apply only derived defaults to data already loaded before this late layer.
v1996cMigrateCommercial(false);
if(currentUser())render();
