Loading Caraveli Inc…
Dashboard
Welcome to Caraveli Inc
Loading…
Good morning.
Balance Due
All employees
Employees
In roster
Paid YTD
This year
Companies
In directory
Owed to Subs
All subcontractors
🕐
Timesheet & Expenses
Track daily hours, breaks, and expenses. Generate invoices and record payments.
employeesOpen →
👷
Subcontractors
Manage sub info, licenses, insurance, contracts and payments.
Coming Soon
🏗
Jobs & Progress
Create jobs, assign crews, track milestones and completion.
Coming Soon
🛒
Purchases
Log material purchases per job, track vendors and receipts.
Coming Soon

Employee Balances

Go to Timesheet →
Loading…
Connecting…
👥 Employee Roster
Manage your team and track balances.
🏢 Companies
Manage your company directory.
☑ Select Days to Pay
Week / Day Date Break(h) Net(h) Amount
Selected 0.00 $0.00
✏️ Enter Hours
Select an employee
DayDateInBrk StartBrk EndOutBrk(h)Gross(h)Net(h)Notes
This Week 0.00 0.00 0.00
0.0
Week Gross
0.0
Week Net
0.0
Week Break
0.0
All Hrs
Days
0
Hours
0.00
Expenses
$0.00
Total
$0.00
Filter:
#DateCategoryDescriptionJobAmountReceipt #StatusActions
Total / Selected $0.00
$0
Total Submitted
$0
Approved
$0
Pending
Items
0
Hrs sel.
0.00
Exp sel.
$0.00
Total
$0.00
Payment progress
⏳ Unpaid
$0 paid0%of $0
$0
Total Charged
$0
Total Paid
$0
Balance Due
#Date PaidReference #MethodNotes / CoversAmountRunning BalanceActions
Total Paid $0.00 $0.00 due
YTD
Invoice / Summary
Employee:
Company:
Period:
Generated:
Rate: $/hr
⏱ Hours Worked
WeekGross HrsBreak HrsNet HrsAmount
Subtotal
💳 Expenses
DateCategoryDescriptionAmountStatus
Subtotal
💰 Payment History
DateRef #MethodNotesAmountRunning Balance
Total Paid
💰 BALANCE DUE
$0.00
⏳ UNPAID
0
Hrs (YTD)
$0
Hrs Value
$0
Expenses
$0
Charged
$0
Paid
$0
Balance
$0
Total Contracted
$0
Total Paid
$0
Total Owed
$0
Change Orders
0
Total Jobs
$0
Total Value
$0
Collected
$0
Outstanding
🛒
Purchases & Materials
Track every purchase across all jobs.
  • Log purchases with vendor & job
  • Track approval status
  • Vendor directory
  • Monthly spending reports
n.classList.remove('on')); if(el) el.classList.add('on'); document.querySelectorAll('.page').forEach(p=>p.classList.remove('on')); document.getElementById('pg-'+id).classList.add('on'); const info=HUB_INFO[id]||{title:id,sub:''}; document.getElementById('pgTitle').textContent=info.title; document.getElementById('pgSub').textContent=info.sub; closeSidebar(); if(id==='dash') loadDash(); if(id==='ts'){ if(!weekData._loaded) tsLoadAll(); else tsRerender(); } if(id==='subs'){ if(!subsData._loaded) subsLoadAll(); else subsRerender(); } if(id==='jobs'){ if(!jobsData._loaded) jobsLoadAll(); else jobsRerender(); } if(id==='ts'&&!weekData._loaded){tsLoadAll();} } function toggleSidebar(){document.getElementById('sidebar').classList.toggle('open');document.getElementById('overlay').classList.toggle('show');} function closeSidebar(){document.getElementById('sidebar').classList.remove('open');document.getElementById('overlay').classList.remove('show');} /* ════════════════════════════════════════ STATUS ════════════════════════════════════════ */ function setStatus(msg,state){ document.getElementById('statusTxt').textContent=msg; document.getElementById('statusDot').className='status-dot '+(state||''); document.getElementById('tsSyncDot').className='status-dot '+(state||''); document.getElementById('tsSyncTxt').textContent=msg; } function showLoading(v){document.getElementById('loadingOverlay').classList.toggle('show',v);} /* ════════════════════════════════════════ DASHBOARD ════════════════════════════════════════ */ function setGreeting(){ const h=new Date().getHours(); document.getElementById('greetH').textContent=(h<12?'Good morning':h<17?'Good afternoon':'Good evening')+'.'; document.getElementById('greetD').textContent=new Date().toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'}); } function _h2h(t){if(!t)return 0;const[h,m]=t.split(':').map(Number);return h+m/60;} async function loadDash(){ setStatus('Loading…','yellow'); try{ const[c1,c2,c3,c4,c5]=await Promise.all([ db.from('companies').select('id,name'), db.from('employees').select('id,name,rate'), db.from('expenses').select('employee_id,amount,status'), db.from('payments').select('employee_id,amount'), db.from('week_data').select('employee_id,time_in,time_out,break_start,break_end'), ]); if(c1.error)throw c1.error; const cos=c1.data||[],emps=c2.data||[]; const hM={},eM={},pM={}; (c5.data||[]).forEach(r=>{ if(!r.time_in||!r.time_out)return; let g=_h2h(r.time_out)-_h2h(r.time_in);if(g<0)g+=24; let b=0;if(r.break_start&&r.break_end){b=_h2h(r.break_end)-_h2h(r.break_start);if(b<0)b=0;} hM[r.employee_id]=(hM[r.employee_id]||0)+Math.max(0,g-b); }); (c3.data||[]).forEach(e=>{if(e.status!=='Rejected')eM[e.employee_id]=(eM[e.employee_id]||0)+(parseFloat(e.amount)||0);}); (c4.data||[]).forEach(p=>{pM[p.employee_id]=(pM[p.employee_id]||0)+(parseFloat(p.amount)||0);}); let due=0,paid=0; const rows=emps.map(emp=>{ const rate=parseFloat(emp.rate)||0,hrs=hM[emp.id]||0; const charged=(hrs*rate)+(eM[emp.id]||0),p=pM[emp.id]||0,bal=Math.max(0,charged-p); due+=bal;paid+=p;return{emp,hrs,charged,paid:p,bal}; }); document.getElementById('dDue').textContent='$'+Math.round(due).toLocaleString(); document.getElementById('dPaid').textContent='$'+Math.round(paid).toLocaleString(); document.getElementById('dEmps').textContent=emps.length; document.getElementById('dEmps2').textContent=emps.length; document.getElementById('dCos').textContent=cos.length; document.getElementById('dPaidY').textContent=new Date().getFullYear()+' total'; const el=document.getElementById('dashEmpTable'); if(!rows.length){ el.innerHTML='
No employees yet. Add in Timesheet →
'; }else{ el.innerHTML=''+ rows.map(r=>'').join('')+ '
EmployeeHrsChargedPaidBalance
'+r.emp.name+''+r.hrs.toFixed(1)+'$'+r.charged.toFixed(2)+'$'+r.paid.toFixed(2)+'$'+r.bal.toFixed(2)+'
'; } // Sub balances if(subsData._loaded){ let subDue=0; subsData.subs.forEach(s=>{const t=calcSubTotals(s);subDue+=t.owed;}); const sdEl=document.getElementById('dashSubDue'); if(sdEl) sdEl.textContent='$'+Math.round(subDue).toLocaleString(); } setStatus('✅ Connected','green'); }catch(e){ setStatus('❌ '+e.message,'red'); document.getElementById('dashEmpTable').innerHTML='
Error: '+e.message+'
'; } } /* ════════════════════════════════════════ TIMESHEET — LOAD ALL DATA ════════════════════════════════════════ */ async function tsLoadAll(){ showLoading(true); setStatus('Loading data…','yellow'); try{ const[c1,c2,c3,c4,c5]=await Promise.all([ db.from('companies').select('*').order('name'), db.from('employees').select('*').order('name'), db.from('week_data').select('*'), db.from('expenses').select('*').order('date'), db.from('payments').select('*').order('date'), ]); if(c1.error)throw c1.error; if(c2.error)throw c2.error; companies=c1.data||[]; employees=c2.data||[]; weekData={_loaded:true}; (c3.data||[]).forEach(r=>{ if(!weekData[r.employee_id])weekData[r.employee_id]={}; if(!weekData[r.employee_id][r.week_offset])weekData[r.employee_id][r.week_offset]=[]; weekData[r.employee_id][r.week_offset][r.day_index]={tin:r.time_in||'',bs:r.break_start||'',be:r.break_end||'',tout:r.time_out||'',note:r.note||'',paid:r.paid||false,dbid:r.id}; }); expenses={}; (c4.data||[]).forEach(e=>{ if(!expenses[e.employee_id])expenses[e.employee_id]=[]; expenses[e.employee_id].push({id:e.id,date:e.date,cat:e.category,desc:e.description,amt:parseFloat(e.amount)||0,rcpt:e.receipt||'',status:e.status||'Pending',paid:e.paid||false}); }); payments={}; (c5.data||[]).forEach(p=>{ if(!payments[p.employee_id])payments[p.employee_id]=[]; payments[p.employee_id].push({id:p.id,date:p.date,amt:parseFloat(p.amount)||0,ref:p.ref||'',method:p.method||'Check',note:p.note||'',dayKeys:p.paid_day_keys||[],expIds:p.paid_exp_ids||[]}); }); if(!activeEmpId&&employees.length)activeEmpId=employees[0].id; setStatus('✅ Connected','green'); tsRerender(); }catch(e){ setStatus('❌ '+e.message,'red'); showToast('DB error: '+e.message); }finally{showLoading(false);} } /* ════════════════════════════════════════ HELPERS ════════════════════════════════════════ */ const getEmp=id=>employees.find(e=>e.id===id); const getCo=id=>companies.find(c=>c.id===id); const activeEmp=()=>getEmp(activeEmpId); function getMonday(off){const d=new Date(),day=d.getDay();d.setDate(d.getDate()-day+(day===0?-6:1)+off*7);d.setHours(0,0,0,0);return d;} function fmtD(d){return d.toLocaleDateString('en-US',{month:'short',day:'numeric'});} function fmtDL(d){return d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});} function isToday(d){return d.toDateString()===new Date().toDateString();} function t2h(t){if(!t)return 0;const[h,m]=t.split(':').map(Number);return h+m/60;} function dayDate(off,i){const m=getMonday(off),d=new Date(m);d.setDate(d.getDate()+i);return d;} function avColor(id){const i=employees.findIndex(e=>e.id===id);return ACOLORS[i%ACOLORS.length]||ACOLORS[0];} function initials(n){return n.split(' ').map(w=>w[0]).join('').toUpperCase().slice(0,2)||'?';} function dayNet(s){if(!s||!s.tin||!s.tout)return 0;let g=t2h(s.tout)-t2h(s.tin);if(g<0)g+=24;let b=0;if(s.bs&&s.be){b=t2h(s.be)-t2h(s.bs);if(b<0)b=0;}return Math.max(0,g-b);} function dayBrk(s){if(!s||!s.bs||!s.be)return 0;let b=t2h(s.be)-t2h(s.bs);return b<0?0:b;} function weekTotals(data){let g=0,b=0,n=0;(data||[]).forEach(s=>{if(!s)return;let gv=0,bv=0;if(s.tin&&s.tout){gv=t2h(s.tout)-t2h(s.tin);if(gv<0)gv+=24;}if(s.bs&&s.be){bv=t2h(s.be)-t2h(s.bs);if(bv<0)bv=0;}g+=gv;b+=bv;n+=Math.max(0,gv-bv);});return{g,b,n};} function allHours(emp){const wd=weekData[emp.id]||{};let g=0,b=0,n=0;Object.values(wd).forEach(d=>{if(!Array.isArray(d))return;const t=weekTotals(d);g+=t.g;b+=t.b;n+=t.n;});return{g,b,n};} function calcTotals(emp){ const h=allHours(emp),rate=parseFloat(emp.rate)||0,hAmt=h.n*rate; const eAmt=(expenses[emp.id]||[]).filter(e=>e.status!=='Rejected').reduce((s,e)=>s+e.amt,0); const charged=hAmt+eAmt,paid=(payments[emp.id]||[]).reduce((s,p)=>s+p.amt,0),balance=Math.max(0,charged-paid); return{h,hAmt,eAmt,charged,paid,balance}; } function payStatus(emp){const t=calcTotals(emp);if(t.charged===0)return'none';if(t.balance<=0)return'paid';if(t.paid>0)return'partial';return'unpaid';} function showToast(msg){ let t=document.getElementById('appToast'); if(!t){t=document.createElement('div');t.id='appToast';t.style.cssText='position:fixed;bottom:70px;left:50%;transform:translateX(-50%);background:#1a1a2e;color:#fff;padding:9px 20px;border-radius:9px;font-size:13px;font-weight:600;z-index:9999;box-shadow:0 4px 14px rgba(0,0,0,.25);transition:opacity .3s;';document.body.appendChild(t);} t.textContent=msg;t.style.opacity='1';clearTimeout(t._h);t._h=setTimeout(()=>t.style.opacity='0',2800); } function pushUndo(){ undoStack.push(JSON.stringify({companies,employees,weekData,expenses,payments,activeEmpId})); if(undoStack.length>20)undoStack.shift(); updateUndoBtn(); } function tsUndo(){ if(!undoStack.length){showToast('Nothing to undo');return;} if(!confirm('Undo last action?'))return; const s=JSON.parse(undoStack.pop()); companies=s.companies;employees=s.employees;weekData=s.weekData;expenses=s.expenses;payments=s.payments;activeEmpId=s.activeEmpId; tsRerender();updateUndoBtn();showToast('↩ Undone'); } function updateUndoBtn(){ const b=document.getElementById('tsUndoBtn');if(!b)return; b.disabled=undoStack.length===0;b.style.opacity=undoStack.length===0?'0.4':'1'; b.textContent='↩ Undo'+(undoStack.length?' ('+undoStack.length+')':''); } /* ════════════════════════════════════════ TS TABS / NAV ════════════════════════════════════════ */ function tsTab(id,el){ document.querySelectorAll('.tab').forEach(t=>t.classList.remove('on')); if(el)el.classList.add('on'); document.querySelectorAll('.panel').forEach(p=>p.classList.remove('on')); document.getElementById('tp-'+id).classList.add('on'); if(id==='sum')renderSummary(); if(id==='cos')renderCos(); if(id==='pay'){renderPay();updatePayBar();} if(id==='hours')renderAllWeeks(); } function tsPopulateJobSel(){ const jobOpts=''+ (jobsData.jobs||[]).map(j=> `` ).join(''); ['tsJobSel','pJobSel','eJobSel'].forEach(id=>{ const sel=document.getElementById(id); if(!sel)return; const cur=sel.value; sel.innerHTML=jobOpts; if(cur)sel.value=cur; }); } function tsPopulateSelects(){ ['tsEmpSel','expEmpSel','payEmpSel','sumEmpSel'].forEach(id=>{ const sel=document.getElementById(id);if(!sel)return; sel.innerHTML=employees.length ?employees.map(e=>``).join('') :''; }); } function tsSelectEmp(id){ if(!id)return; activeEmpId=id;weekOffset=0; tsPopulateSelects(); tsBannerUpdate(); tsBuildWeek(); renderAllWeeks(); renderExp(); renderPay(); updatePayBar(); renderSummary(); renderRoster(); } function tsBannerUpdate(){ const emp=activeEmp();if(!emp)return; const t=calcTotals(emp),rate=parseFloat(emp.rate)||0,co=getCo(emp.company_id); document.getElementById('tsBannerName').textContent=emp.name+(co?' @ '+co.name:''); document.getElementById('tsBannerHrs').textContent=t.h.n.toFixed(2)+' hrs logged'; document.getElementById('tsBannerAmt').textContent='@ $'+rate.toFixed(2)+'/hr = $'+t.hAmt.toFixed(2)+' + $'+t.eAmt.toFixed(2)+' exp'; document.getElementById('tsBannerBal').textContent='$'+t.balance.toFixed(2); document.getElementById('tsBannerPaid').textContent='$'+t.paid.toFixed(2)+' paid so far'; document.getElementById('mcAllHrs').textContent=t.h.n.toFixed(1); } /* ════════════════════════════════════════ ROSTER ════════════════════════════════════════ */ function renderRoster(){ const g=document.getElementById('rosterGrid'); if(!employees.length){g.innerHTML='
No employees yet.
';return;} g.innerHTML=employees.map(emp=>{ const co=getCo(emp.company_id),col=avColor(emp.id),t=calcTotals(emp),st=payStatus(emp); const bc=st==='paid'?'b-paid':st==='partial'?'b-partial':'b-unpaid'; const bt=st==='paid'?'✅ Paid':st==='partial'?'◑ Partial':'⏳ Unpaid'; return`
${bt}
${initials(emp.name)}
${emp.name}
${co?'🏢 '+co.name:'No company'}
${emp.role||'No role'} · $${emp.rate||0}/hr
Balance: $${t.balance.toFixed(2)}
`; }).join(''); } function openEmpModal(id){ editEmpId=id||null; document.getElementById('empModalTitle').textContent=id?'Edit Employee':'Add Employee'; const sel=document.getElementById('mECo'); sel.innerHTML=''+companies.map(c=>``).join(''); if(id){const e=getEmp(id);document.getElementById('mEName').value=e.name;document.getElementById('mERole').value=e.role||'';document.getElementById('mERate').value=e.rate||'';document.getElementById('mEEmail').value=e.email||'';document.getElementById('mEPhone').value=e.phone||'';sel.value=e.company_id||'';} else{['mEName','mERole','mERate','mEEmail','mEPhone'].forEach(i=>document.getElementById(i).value='');sel.value='';} document.getElementById('empModal').classList.add('open'); } async function saveEmployee(){ const name=document.getElementById('mEName').value.trim();if(!name){alert('Name required.');return;} pushUndo(); const data={name,role:document.getElementById('mERole').value.trim(),rate:parseFloat(document.getElementById('mERate').value)||0,company_id:document.getElementById('mECo').value||null,email:document.getElementById('mEEmail').value.trim(),phone:document.getElementById('mEPhone').value.trim()}; try{ if(editEmpId){const{error}=await db.from('employees').update(data).eq('id',editEmpId);if(error)throw error;Object.assign(getEmp(editEmpId),data);} else{const{data:row,error}=await db.from('employees').insert(data).select().single();if(error)throw error;employees.push(row);if(!activeEmpId)activeEmpId=row.id;} closeModals();tsRerender();showToast('✅ Employee saved'); }catch(e){showToast('❌ '+e.message);} } async function deleteEmployee(id){ if(!confirm('Delete this employee and all their data?'))return; pushUndo(); try{ await db.from('employees').delete().eq('id',id); employees=employees.filter(e=>e.id!==id);delete weekData[id];delete expenses[id];delete payments[id]; if(activeEmpId===id)activeEmpId=employees.length?employees[0].id:null; tsRerender();showToast('✅ Deleted'); }catch(e){showToast('❌ '+e.message);} } /* ════════════════════════════════════════ COMPANIES ════════════════════════════════════════ */ function renderCos(){ const g=document.getElementById('coGrid'); if(!companies.length){g.innerHTML='
No companies yet.
';document.getElementById('coDetail').style.display='none';return;} g.innerHTML=companies.map(co=>{ const emps=employees.filter(e=>e.company_id===co.id); const tts=emps.map(calcTotals); const tC=tts.reduce((s,t)=>s+t.charged,0),tP=tts.reduce((s,t)=>s+t.paid,0),tD=tts.reduce((s,t)=>s+t.balance,0); return`
🏢
${co.name}
${co.industry||'—'}
${emps.length} employee${emps.length!==1?'s':''}
Charged: $${tC.toFixed(2)}
Paid: $${tP.toFixed(2)}
Due: $${tD.toFixed(2)}
`; }).join(''); } function showCoDetail(id){ const co=getCo(id);if(!co)return; document.getElementById('coDetail').style.display='block'; document.getElementById('coDetailTitle').textContent='🏢 '+co.name+' — Employees'; const emps=employees.filter(e=>e.company_id===id),tbody=document.getElementById('coDetailBody'); tbody.innerHTML='';let tH=0,tE=0,tC=0,tP=0,tD=0; emps.forEach(emp=>{ const t=calcTotals(emp);tH+=t.hAmt;tE+=t.eAmt;tC+=t.charged;tP+=t.paid;tD+=t.balance; const st=payStatus(emp),bc=st==='paid'?'b-paid':st==='partial'?'b-partial':'b-unpaid',bt=st==='paid'?'✅ Paid':st==='partial'?'◑ Partial':'⏳ Unpaid'; const tr=document.createElement('tr'); tr.innerHTML=`${emp.name}
${emp.role||''} $${(parseFloat(emp.rate)||0).toFixed(2)}${t.h.n.toFixed(2)} hrs $${t.hAmt.toFixed(2)} $${t.eAmt.toFixed(2)} $${t.charged.toFixed(2)} $${t.paid.toFixed(2)} $${t.balance.toFixed(2)} ${bt}`; tbody.appendChild(tr); }); if(!emps.length)tbody.innerHTML='No employees.'; document.getElementById('coTH').textContent='$'+tH.toFixed(2); document.getElementById('coTE').textContent='$'+tE.toFixed(2); document.getElementById('coTC').textContent='$'+tC.toFixed(2); document.getElementById('coTP').textContent='$'+tP.toFixed(2); document.getElementById('coTD').textContent='$'+tD.toFixed(2); } function openCoModal(id){ editCoId=id||null; document.getElementById('coModalTitle').textContent=id?'Edit Company':'Add Company'; if(id){const c=getCo(id);document.getElementById('mCName').value=c.name;document.getElementById('mCInd').value=c.industry||'';document.getElementById('mCCon').value=c.contact||'';document.getElementById('mCEmail').value=c.email||'';document.getElementById('mCAddr').value=c.address||'';} else{['mCName','mCInd','mCCon','mCEmail','mCAddr'].forEach(i=>document.getElementById(i).value='');} document.getElementById('coModal').classList.add('open'); } async function saveCompany(){ const name=document.getElementById('mCName').value.trim();if(!name){alert('Name required.');return;} pushUndo(); const data={name,industry:document.getElementById('mCInd').value.trim(),contact:document.getElementById('mCCon').value.trim(),email:document.getElementById('mCEmail').value.trim(),address:document.getElementById('mCAddr').value.trim()}; try{ if(editCoId){const{error}=await db.from('companies').update(data).eq('id',editCoId);if(error)throw error;Object.assign(getCo(editCoId),data);} else{const{data:row,error}=await db.from('companies').insert(data).select().single();if(error)throw error;companies.push(row);} closeModals();renderCos();tsPopulateSelects();showToast('✅ Company saved'); }catch(e){showToast('❌ '+e.message);} } async function deleteCo(id){ if(!confirm('Delete company? Employees will be unassigned.'))return; pushUndo(); try{ await db.from('companies').delete().eq('id',id); companies=companies.filter(c=>c.id!==id); employees.forEach(e=>{if(e.company_id===id)e.company_id=null;}); renderCos();renderRoster();tsPopulateSelects();showToast('✅ Deleted'); }catch(e){showToast('❌ '+e.message);} } function closeModals(){document.querySelectorAll('.modal-bg').forEach(m=>m.classList.remove('open'));editEmpId=null;editCoId=null;} document.querySelectorAll('.modal-bg').forEach(m=>m.addEventListener('click',e=>{if(e.target===m)closeModals();})); /* ════════════════════════════════════════ TIMESHEET — WEEK ENTRY ════════════════════════════════════════ */ function tsCalcRow(row){ const tin=row.querySelector('.t-in').value,tout=row.querySelector('.t-out').value; const bs=row.querySelector('.b-st').value,be=row.querySelector('.b-en').value; let g=0,b=0; if(tin&&tout){g=t2h(tout)-t2h(tin);if(g<0)g+=24;} if(bs&&be){b=t2h(be)-t2h(bs);if(b<0)b=0;} const n=Math.max(0,g-b); row.querySelector('.c-b').textContent=b>0?b.toFixed(2):'-'; row.querySelector('.c-g').textContent=g>0?g.toFixed(2):'-'; row.querySelector('.c-n').textContent=n>0?n.toFixed(2):'-'; return{g,b,n}; } function tsUpdateTotals(){ let tG=0,tB=0,tN=0; document.querySelectorAll('#tsBody tr').forEach(r=>{const v=tsCalcRow(r);tG+=v.g;tB+=v.b;tN+=v.n;}); document.getElementById('totGross').textContent=tG.toFixed(2); document.getElementById('totBrk').textContent=tB.toFixed(2); document.getElementById('totNet').textContent=tN.toFixed(2); document.getElementById('mcGross').textContent=tG.toFixed(1); document.getElementById('mcNet').textContent=tN.toFixed(1); document.getElementById('mcBrk').textContent=tB.toFixed(1); tsBannerUpdate(); } let saveTimers={}; async function tsSaveSlot(off,dayIdx){ const emp=activeEmp();if(!emp)return; if(!weekData[emp.id])weekData[emp.id]={}; if(!weekData[emp.id][off])weekData[emp.id][off]=[]; const slot=weekData[emp.id][off][dayIdx]||{}; const rows=document.querySelectorAll('#tsBody tr'); const row=rows[dayIdx];if(!row)return; slot.tin=row.querySelector('.t-in').value||null; slot.bs=row.querySelector('.b-st').value||null; slot.be=row.querySelector('.b-en').value||null; slot.tout=row.querySelector('.t-out').value||null; slot.note=row.querySelector('.note-inp').value||null; weekData[emp.id][off][dayIdx]=slot; const key=emp.id+'_'+off+'_'+dayIdx; clearTimeout(saveTimers[key]); saveTimers[key]=setTimeout(async()=>{ try{ const payload={employee_id:emp.id,week_offset:parseInt(off),day_index:dayIdx,time_in:slot.tin,break_start:slot.bs,break_end:slot.be,time_out:slot.tout,note:slot.note,paid:slot.paid||false}; // Include job_id const jobId=document.getElementById('tsJobSel')?.value||null; payload.job_id=jobId||null; if(slot.dbid){await db.from('week_data').update(payload).eq('id',slot.dbid);slot.job_id=jobId||null;} else{const{data:r,error}=await db.from('week_data').upsert(payload,{onConflict:'employee_id,week_offset,day_index'}).select().single();if(!error&&r){slot.dbid=r.id;slot.job_id=jobId||null;}} }catch(e){console.warn('Save slot error:',e);} },1000); } function tsBuildWeek(){ const emp=activeEmp(); const mon=getMonday(weekOffset),sun=new Date(mon);sun.setDate(mon.getDate()+6); document.getElementById('weekLbl').textContent='Week of '+fmtD(mon)+' – '+fmtD(sun); const tbody=document.getElementById('tsBody');tbody.innerHTML=''; const saved=emp?(weekData[emp.id]||{})[weekOffset]||[]:{}; DAYS.forEach((day,i)=>{ const date=new Date(mon);date.setDate(mon.getDate()+i); const tdy=isToday(date),s=saved[i]||{}; const tr=document.createElement('tr');if(tdy)tr.classList.add('today'); tr.innerHTML=`${day}${tdy?' TODAY':''} ${fmtD(date)} --- `; tr.querySelectorAll('input').forEach(inp=>{inp.addEventListener('change',()=>{tsUpdateTotals();tsSaveSlot(weekOffset,i);renderAllWeeks();});}); tbody.appendChild(tr); }); tsUpdateTotals(); } function tsChangeWeek(dir){weekOffset+=dir;tsBuildWeek();} async function tsClearWeek(){ const emp=activeEmp();if(!emp)return; if(!confirm('Clear this week for '+emp.name+'?'))return; pushUndo(); const wd=weekData[emp.id]||{},slots=wd[weekOffset]||[]; try{ const ids=slots.filter(s=>s&&s.dbid).map(s=>s.dbid); if(ids.length)await db.from('week_data').delete().in('id',ids); if(weekData[emp.id])weekData[emp.id][weekOffset]=[]; tsBuildWeek();renderAllWeeks();tsBannerUpdate();showToast('✅ Week cleared'); }catch(e){showToast('❌ '+e.message);} } async function tsSaveRate(){ const emp=activeEmp();if(!emp)return; const rate=parseFloat(document.getElementById('tsRate').value)||0; emp.rate=rate; await db.from('employees').update({rate}).eq('id',emp.id); tsBannerUpdate();renderSummary();renderRoster(); } /* ════════════════════════════════════════ ALL WEEKS TABLE ════════════════════════════════════════ */ function renderAllWeeks(){ const emp=activeEmp(),tbody=document.getElementById('allWeeksBody'); tbody.innerHTML=''; if(!emp){tbody.innerHTML='Select an employee first.';updateTsTray();return;} const rate=parseFloat(emp.rate)||0,wd=weekData[emp.id]||{}; const offsets=Object.keys(wd).map(Number).filter(k=>!isNaN(k)).sort((a,b)=>a-b); let hasData=false; offsets.forEach(off=>{ const data=wd[off]||[],wt=weekTotals(data); if(wt.g===0&&wt.n===0)return; hasData=true; const mon=getMonday(off),sun=new Date(mon);sun.setDate(mon.getDate()+6); const wtr=document.createElement('tr');wtr.style.cssText='background:#f9fafb;'; wtr.innerHTML=` 📅 Week of ${fmtD(mon)} – ${fmtD(sun)} $${(wt.n*rate).toFixed(2)}`; tbody.appendChild(wtr); data.forEach((s,i)=>{ if(!s||(!s.tin&&!s.tout))return; const net=dayNet(s),brk=dayBrk(s),date=dayDate(off,i),isPaid=!!(s.paid); const dr=document.createElement('tr'); if(isPaid)dr.style.cssText='background:#f0fdf4;opacity:.6;'; dr.innerHTML=` ${isPaid?``:``} ${DAYS[i]}${isPaid?` PAID `:''} ${fmtD(date)} ${brk>0?brk.toFixed(2):'-'} ${net>0?net.toFixed(2):'-'} ${net>0?'$'+(net*rate).toFixed(2):'-'}`; tbody.appendChild(dr); }); }); if(!hasData)tbody.innerHTML='No hours logged yet.'; updateTsTray(); } function tsToggleWeek(off,checked){document.querySelectorAll(`.day-cb[data-off="${off}"]`).forEach(cb=>cb.checked=checked);updateTsTray();} function tsCheckAllDays(checked){document.querySelectorAll('.day-cb,.wk-cb').forEach(cb=>cb.checked=checked);const sa=document.getElementById('tsAllCb');if(sa)sa.checked=checked;updateTsTray();} function updateTsTray(){ const emp=activeEmp();if(!emp)return; const rate=parseFloat(emp.rate)||0; let days=0,hrs=0; document.querySelectorAll('.day-cb:checked').forEach(cb=>{ const off=parseInt(cb.dataset.off),day=parseInt(cb.dataset.day); const s=((weekData[emp.id]||{})[off]||[])[day]; const n=dayNet(s);if(n>0){days++;hrs+=n;} }); const hAmt=hrs*rate; let expAmt=0; document.querySelectorAll('.exp-cb-row:checked').forEach(cb=>{ const e=(expenses[emp.id]||[]).find(x=>x.id===cb.dataset.expId); if(e)expAmt+=e.amt; }); const tot=hAmt+expAmt; document.getElementById('tsSelHrs').textContent=hrs.toFixed(2); document.getElementById('tsSelAmt').textContent='$'+hAmt.toFixed(2); document.getElementById('trayDays').textContent=days; document.getElementById('trayHrs').textContent=hrs.toFixed(2); document.getElementById('trayExp').textContent='$'+expAmt.toFixed(2); document.getElementById('trayTot').textContent='$'+tot.toFixed(2); document.getElementById('tsTray').classList.toggle('hide',days===0&&expAmt===0); document.getElementById('expTrayN').textContent=document.querySelectorAll('.exp-cb-row:checked').length; document.getElementById('expTrayH').textContent=hrs.toFixed(2); document.getElementById('expTrayE').textContent='$'+expAmt.toFixed(2); document.getElementById('expTrayT').textContent='$'+tot.toFixed(2); document.getElementById('expTray').classList.toggle('hide',days===0&&expAmt===0); } /* ════════════════════════════════════════ EXPENSES ════════════════════════════════════════ */ async function addExpense(){ const emp=activeEmp();if(!emp){alert('Select an employee first.');return;} const date=document.getElementById('eDate').value,cat=document.getElementById('eCat').value,desc=document.getElementById('eDesc').value.trim(),amt=parseFloat(document.getElementById('eAmt').value),rcpt=document.getElementById('eRcpt').value.trim(); if(!date){alert('Enter a date.');return;}if(!desc){alert('Enter a description.');return;}if(!amt||amt<=0){alert('Enter a valid amount.');return;} pushUndo(); try{ const expJobId=document.getElementById('eJobSel')?.value||null; const{data:row,error}=await db.from('expenses').insert({employee_id:emp.id,date,category:cat,description:desc,amount:amt,receipt:rcpt,status:'Pending',paid:false,job_id:expJobId}).select().single(); if(error)throw error; if(!expenses[emp.id])expenses[emp.id]=[]; expenses[emp.id].push({id:row.id,date,cat,desc,amt,rcpt,status:'Pending',paid:false}); expenses[emp.id].sort((a,b)=>a.date.localeCompare(b.date)); ['eDate','eDesc','eAmt','eRcpt'].forEach(id=>document.getElementById(id).value=''); renderExp();renderSummary();renderRoster();tsBannerUpdate();updatePayBar();showToast('✅ Expense added'); }catch(e){showToast('❌ '+e.message);} } async function deleteExpense(id){ const emp=activeEmp();if(!emp)return; pushUndo(); try{ await db.from('expenses').delete().eq('id',id); expenses[emp.id]=(expenses[emp.id]||[]).filter(e=>e.id!==id); renderExp();renderSummary();renderRoster();tsBannerUpdate();updatePayBar();showToast('✅ Deleted'); }catch(e){showToast('❌ '+e.message);} } async function saveEditExp(id){ const emp=activeEmp();if(!emp)return; const e=(expenses[emp.id]||[]).find(x=>x.id===id);if(!e)return; const date=document.getElementById('ed-date').value,desc=document.getElementById('ed-desc').value.trim(),amt=parseFloat(document.getElementById('ed-amt').value); if(!date||!desc||!amt||amt<=0){alert('Fill all fields.');return;} pushUndo(); try{ const updates={date,category:document.getElementById('ed-cat').value,description:desc,amount:amt,receipt:document.getElementById('ed-rcpt').value.trim(),status:document.getElementById('ed-st').value}; await db.from('expenses').update(updates).eq('id',id); e.date=date;e.cat=updates.category;e.desc=desc;e.amt=amt;e.rcpt=updates.receipt;e.status=updates.status; expenses[emp.id].sort((a,b)=>a.date.localeCompare(b.date)); renderExp();renderSummary();renderRoster();tsBannerUpdate();updatePayBar();showToast('✅ Updated'); }catch(e2){showToast('❌ '+e2.message);} } function startEditExp(id){ const emp=activeEmp();if(!emp)return; const e=(expenses[emp.id]||[]).find(x=>x.id===id);if(!e)return; const tr=document.querySelector(`tr[data-eid="${id}"]`);if(!tr)return; tr.classList.add('editing'); tr.innerHTML=`# `; } function expCheckAll(checked){document.querySelectorAll('.exp-cb-row').forEach(cb=>cb.checked=checked);const sa=document.getElementById('expAllCb');if(sa)sa.checked=checked;updateTsTray();} function renderExp(){ const emp=activeEmp(),fc=document.getElementById('feCat').value,fs=document.getElementById('feStatus').value; const list=emp?(expenses[emp.id]||[]).filter(e=>(!fc||e.cat===fc)&&(!fs||e.status===fs)):[]; const tbody=document.getElementById('expBody');tbody.innerHTML=''; if(!list.length){tbody.innerHTML=`${emp?'No expenses yet.':'Select an employee first.'}`;} else{list.forEach((e,idx)=>{ const isPaid=!!e.paid,d=new Date(e.date+'T00:00:00'); const tr=document.createElement('tr');tr.setAttribute('data-eid',e.id); if(isPaid)tr.style.cssText='opacity:.5;background:#f0fdf4;'; const eJob=e.job_id?(jobsData.jobs||[]).find(j=>j.id===e.job_id):null; tr.innerHTML=`${isPaid?'':``} ${idx+1} ${fmtDL(d)} ${EICONS[e.cat]||'🗂️'} ${e.cat} ${e.desc} ${eJob?eJob.name:'—'} $${e.amt.toFixed(2)} ${e.rcpt||'—'} ${e.status} ${isPaid ?`` :` ` }`; tbody.appendChild(tr); });} const all=emp?(expenses[emp.id]||[]):[]; const tot=all.reduce((s,e)=>s+e.amt,0),app=all.filter(e=>e.status==='Approved').reduce((s,e)=>s+e.amt,0),pend=all.filter(e=>e.status==='Pending').reduce((s,e)=>s+e.amt,0); document.getElementById('expTotal').textContent='$'+tot.toFixed(2); document.getElementById('mcExpTot').textContent='$'+Math.round(tot); document.getElementById('mcExpApproved').textContent='$'+Math.round(app); document.getElementById('mcExpPending').textContent='$'+Math.round(pend); } /* ════════════════════════════════════════ PAYMENTS ════════════════════════════════════════ */ function tsSendToPay(){ const emp=activeEmp();if(!emp)return; const rate=parseFloat(emp.rate)||0; let hrs=0,expAmt=0; const dayKeys=[],expIds=[]; const weeks=new Set(); document.querySelectorAll('.day-cb:checked').forEach(cb=>{ const off=parseInt(cb.dataset.off),day=parseInt(cb.dataset.day); const s=((weekData[emp.id]||{})[off]||[])[day]; const n=dayNet(s);if(n>0){hrs+=n;weeks.add(off);dayKeys.push(off+'_'+day);} }); document.querySelectorAll('.exp-cb-row:checked').forEach(cb=>{ const e=(expenses[emp.id]||[]).find(x=>x.id===cb.dataset.expId); if(e){expAmt+=e.amt;expIds.push(e.id);} }); const tot=(hrs*rate)+expAmt; document.getElementById('pAmt').value=tot.toFixed(2); const wkLabels=[...weeks].sort().map(off=>'Wk '+fmtD(getMonday(off))); let note=wkLabels.join(', ');if(expAmt>0)note+=(note?', ':'')+`Exp $${expAmt.toFixed(2)}`; document.getElementById('pNote').value=note; document.getElementById('pDate').value=new Date().toISOString().slice(0,10); emp._pendingDayKeys=dayKeys;emp._pendingExpIds=expIds; tsCheckAllDays(false);expCheckAll(false); tsTab('pay',document.getElementById('ttab-pay')); } async function addPayment(){ const emp=activeEmp();if(!emp){alert('Select an employee first.');return;} const date=document.getElementById('pDate').value,amt=parseFloat(document.getElementById('pAmt').value),ref=document.getElementById('pRef').value.trim(),method=document.getElementById('pMethod').value,note=document.getElementById('pNote').value.trim(); if(!date){alert('Enter a date paid.');return;}if(!amt||amt<=0){alert('Enter a valid amount.');return;} pushUndo(); const dayKeys=emp._pendingDayKeys||[],expIds=emp._pendingExpIds||[]; delete emp._pendingDayKeys;delete emp._pendingExpIds; try{ const jobId=document.getElementById('pJobSel')?.value||null; const{data:row,error}=await db.from('payments').insert({employee_id:emp.id,date,amount:amt,ref,method,note,paid_day_keys:dayKeys,paid_exp_ids:expIds,job_id:jobId}).select().single(); if(error)throw error; if(!payments[emp.id])payments[emp.id]=[]; payments[emp.id].push({id:row.id,date,amt,ref,method,note,dayKeys,expIds}); payments[emp.id].sort((a,b)=>a.date.localeCompare(b.date)); dayKeys.forEach(key=>{ const[off,day]=key.split('_').map(Number); const s=((weekData[emp.id]||{})[off]||[])[day]; if(s){s.paid=true;db.from('week_data').update({paid:true}).eq('id',s.dbid).then(()=>{});} }); expIds.forEach(id=>{ const e=(expenses[emp.id]||[]).find(x=>x.id===id); if(e){e.paid=true;db.from('expenses').update({paid:true}).eq('id',id).then(()=>{});} }); ['pDate','pAmt','pRef','pNote'].forEach(id=>document.getElementById(id).value=''); renderPay();updatePayBar();renderSummary();renderRoster();tsBannerUpdate();renderAllWeeks();renderExp();showToast('✅ Payment recorded'); }catch(e){showToast('❌ '+e.message);} } async function deletePay(id){ const emp=activeEmp();if(!emp)return; if(!confirm('Delete this payment? Balance will be restored.'))return; pushUndo(); await _deletePay(emp,id); renderPay();updatePayBar();renderSummary();renderRoster();tsBannerUpdate();renderAllWeeks();renderExp();showToast('✅ Deleted'); } async function _deletePay(emp,id){ const p=(payments[emp.id]||[]).find(x=>x.id===id); if(p){ for(const key of(p.dayKeys||[])){const[off,day]=key.split('_').map(Number);const s=((weekData[emp.id]||{})[off]||[])[day];if(s){delete s.paid;await db.from('week_data').update({paid:false}).eq('id',s.dbid);}} for(const eid of(p.expIds||[])){const e=(expenses[emp.id]||[]).find(x=>x.id===eid);if(e){delete e.paid;await db.from('expenses').update({paid:false}).eq('id',eid);}} } await db.from('payments').delete().eq('id',id); payments[emp.id]=(payments[emp.id]||[]).filter(x=>x.id!==id); } async function tsUnpayDay(empId,off,day){ const emp=getEmp(empId);if(!emp)return; const key=off+'_'+day,p=(payments[emp.id]||[]).find(x=>(x.dayKeys||[]).includes(key)); if(!confirm('Un-pay this day?'+(p?' Payment record will be deleted.':'')))return; pushUndo(); if(p)await _deletePay(emp,p.id); else{const s=((weekData[emp.id]||{})[off]||[])[day];if(s){delete s.paid;await db.from('week_data').update({paid:false}).eq('id',s.dbid);}} renderAllWeeks();renderPay();updatePayBar();renderSummary();renderRoster();tsBannerUpdate();showToast('🔓 Un-locked'); } async function tsUnpayExp(id){ const emp=activeEmp();if(!emp)return; const e=(expenses[emp.id]||[]).find(x=>x.id===id),p=(payments[emp.id]||[]).find(x=>(x.expIds||[]).includes(id)); if(!confirm('Un-pay this expense?'+(p?' Payment record will be deleted.':'')))return; pushUndo(); if(p)await _deletePay(emp,p.id); else{if(e){delete e.paid;await db.from('expenses').update({paid:false}).eq('id',id);}} renderExp();renderPay();updatePayBar();renderSummary();renderRoster();tsBannerUpdate();showToast('🔓 Un-locked'); } async function startEditPay(id){ const emp=activeEmp();if(!emp)return; const p=(payments[emp.id]||[]).find(x=>x.id===id);if(!p)return; const tr=document.querySelector(`tr[data-pid="${id}"]`);if(!tr)return; tr.classList.add('editing'); tr.innerHTML=`# `; } async function saveEditPay(id){ const emp=activeEmp();if(!emp)return; const p=(payments[emp.id]||[]).find(x=>x.id===id);if(!p)return; const date=document.getElementById('ep-date').value,amt=parseFloat(document.getElementById('ep-amt').value); if(!date||!amt||amt<=0){alert('Fill all fields.');return;} pushUndo(); try{ const updates={date,ref:document.getElementById('ep-ref').value.trim(),method:document.getElementById('ep-method').value,note:document.getElementById('ep-note').value.trim(),amount:amt}; await db.from('payments').update(updates).eq('id',id); p.date=date;p.ref=updates.ref;p.method=updates.method;p.note=updates.note;p.amt=amt; payments[emp.id].sort((a,b)=>a.date.localeCompare(b.date)); renderPay();updatePayBar();renderSummary();renderRoster();tsBannerUpdate();showToast('✅ Updated'); }catch(e){showToast('❌ '+e.message);} } async function unpayPayment(id){ const emp=activeEmp();if(!emp)return; const p=(payments[emp.id]||[]).find(x=>x.id===id);if(!p)return; if(!confirm(`Reverse payment $${p.amt.toFixed(2)}?\nBalance will be restored.`))return; pushUndo(); await _deletePay(emp,id); renderPay();renderAllWeeks();renderExp();updatePayBar();renderSummary();renderRoster();tsBannerUpdate();showToast('🔓 Reversed'); } function renderPay(){ const emp=activeEmp(),pays=emp?(payments[emp.id]||[]):[]; const t=emp?calcTotals(emp):{charged:0,paid:0,balance:0}; document.getElementById('payBalName').textContent=emp?emp.name+' — Payments':'No employee'; const tbody=document.getElementById('payBody');tbody.innerHTML=''; if(!pays.length){tbody.innerHTML=`${emp?'No payments yet.':'Select an employee first.'}`;} else{ let running=t.charged; pays.forEach((p,idx)=>{ running-=p.amt; const d=new Date(p.date+'T00:00:00'); const covD=(p.dayKeys||[]).length,covE=(p.expIds||[]).length; const tr=document.createElement('tr');tr.setAttribute('data-pid',p.id); const covNote=(covD||covE)?`
${covD?covD+' day(s)':''}${covD&&covE?', ':''}${covE?covE+' exp':''} locked`:''; const pJob=p.job_id?(jobsData.jobs||[]).find(j=>j.id===p.job_id):null; tr.innerHTML=`${idx+1} ${fmtDL(d)} ${p.ref||'—'} ${p.method} ${pJob?pJob.name:'—'} ${p.note||'—'}${covNote} $${p.amt.toFixed(2)} $${Math.max(0,running).toFixed(2)} ${(covD||covE)?``:''} `; tbody.appendChild(tr); }); } const totPaid=pays.reduce((s,p)=>s+p.amt,0); document.getElementById('payTotTd').textContent='$'+totPaid.toFixed(2); document.getElementById('payRemTd').textContent='$'+t.balance.toFixed(2)+' due'; } function updatePayBar(){ const emp=activeEmp(); if(!emp){document.getElementById('balFill').style.width='0%';document.getElementById('balPct').textContent='0%';document.getElementById('balTotLbl').textContent='of $0';document.getElementById('balPaidLbl').textContent='$0 paid';document.getElementById('payCardC').textContent='$0';document.getElementById('payCardP').textContent='$0';document.getElementById('payCardD').textContent='$0';document.getElementById('payBalBadge').className='badge b-unpaid';document.getElementById('payBalBadge').textContent='⏳ Unpaid';return;} const t=calcTotals(emp),pct=t.charged>0?Math.min(100,(t.paid/t.charged)*100):0; document.getElementById('balFill').style.width=pct+'%'; document.getElementById('balPct').textContent=pct.toFixed(1)+'%'; document.getElementById('balTotLbl').textContent='of $'+t.charged.toFixed(2); document.getElementById('balPaidLbl').textContent='$'+t.paid.toFixed(2)+' paid'; document.getElementById('payCardC').textContent='$'+t.charged.toFixed(2); document.getElementById('payCardP').textContent='$'+t.paid.toFixed(2); document.getElementById('payCardD').textContent='$'+t.balance.toFixed(2); const st=payStatus(emp),bc=st==='paid'?'b-paid':st==='partial'?'b-partial':'b-unpaid',bt=st==='paid'?'✅ Fully Paid':st==='partial'?'◑ Partially Paid':'⏳ Unpaid'; document.getElementById('payBalBadge').className='badge '+bc; document.getElementById('payBalBadge').textContent=bt; } /* ════════════════════════════════════════ SUMMARY ════════════════════════════════════════ */ function populateYtdYears(){ const sel=document.getElementById('ytdYear'),cur=new Date().getFullYear(); sel.innerHTML=''; for(let y=cur;y>=cur-5;y--){const o=document.createElement('option');o.value=y;o.textContent=y+(y===cur?' (current)':'');sel.appendChild(o);} sel.value=cur; } function renderSummary(){ const emp=activeEmp(),yr=parseInt(document.getElementById('ytdYear').value)||new Date().getFullYear(); const isCur=yr===new Date().getFullYear(),yStart=new Date(yr,0,1),yEnd=isCur?new Date():new Date(yr,11,31,23,59,59); const period=isCur?`Jan 1 – ${fmtDL(new Date())} ${yr}`:`Full Year ${yr}`; document.getElementById('ytdBadge').textContent=isCur?'YTD':yr; ['invHrsPL','invExpPL'].forEach(id=>document.getElementById(id).textContent=period); ['sHrsPer'].forEach(id=>document.getElementById(id).textContent=isCur?'YTD':yr); if(!emp){['invName','invCo','invDate','invRate'].forEach(id=>document.getElementById(id).textContent='—');return;} const co=getCo(emp.company_id),rate=parseFloat(emp.rate)||0,allT=calcTotals(emp); document.getElementById('invName').textContent=emp.name; document.getElementById('invCo').textContent=co?co.name:'—'; document.getElementById('invPeriod').textContent=period; document.getElementById('invDate').textContent=new Date().toLocaleDateString('en-US',{month:'long',day:'numeric',year:'numeric'}); document.getElementById('invRate').textContent=rate.toFixed(2); const hBody=document.getElementById('invHrsBody');hBody.innerHTML=''; let allG=0,allB=0,allN=0; const wd=weekData[emp.id]||{}; Object.keys(wd).map(Number).filter(k=>!isNaN(k)).sort((a,b)=>a-b).forEach(off=>{ const mon=getMonday(off);if(monyEnd)return; const wt=weekTotals(wd[off]);if(wt.g===0&&wt.n===0)return; allG+=wt.g;allB+=wt.b;allN+=wt.n; const sun=new Date(mon);sun.setDate(mon.getDate()+6); const tr=document.createElement('tr'); tr.innerHTML=`${fmtD(mon)} – ${fmtD(sun)}${wt.g.toFixed(2)}${wt.b.toFixed(2)}${wt.n.toFixed(2)}$${(wt.n*rate).toFixed(2)}`; hBody.appendChild(tr); }); if(!allN&&!allG)hBody.innerHTML='No hours in '+yr+'.'; document.getElementById('invTG').textContent=allG.toFixed(2); document.getElementById('invTB').textContent=allB.toFixed(2); document.getElementById('invTN').textContent=allN.toFixed(2); const ytdHA=allN*rate; document.getElementById('invTHA').textContent='$'+ytdHA.toFixed(2); const eBody=document.getElementById('invExpBody');eBody.innerHTML=''; let ytdEA=0; const expShow=(expenses[emp.id]||[]).filter(e=>{if(e.status==='Rejected')return false;const d=new Date(e.date+'T00:00:00');return d>=yStart&&d<=yEnd;}); if(!expShow.length)eBody.innerHTML='No expenses in '+yr+'.'; else expShow.forEach(e=>{ytdEA+=e.amt;const d=new Date(e.date+'T00:00:00');const tr=document.createElement('tr');tr.innerHTML=`${fmtDL(d)}${EICONS[e.cat]||'🗂️'} ${e.cat}${e.desc}$${e.amt.toFixed(2)}${e.status}`;eBody.appendChild(tr);}); document.getElementById('invTE').textContent='$'+ytdEA.toFixed(2); const pBody=document.getElementById('invPayBody');pBody.innerHTML=''; const pays=payments[emp.id]||[]; if(!pays.length)pBody.innerHTML='No payments yet.'; else{let running=allT.charged;pays.forEach(p=>{running-=p.amt;const d=new Date(p.date+'T00:00:00');const tr=document.createElement('tr');tr.innerHTML=`${fmtDL(d)}${p.ref||'—'}${p.method}${p.note||'—'}$${p.amt.toFixed(2)}$${Math.max(0,running).toFixed(2)}`;pBody.appendChild(tr);});} document.getElementById('invTP').textContent='$'+allT.paid.toFixed(2); document.getElementById('invFB').textContent='$'+allT.balance.toFixed(2)+' due'; const ytdC=ytdHA+ytdEA,balanced=payStatus(emp)==='paid'; document.getElementById('grandBox').className='grand '+(balanced?'done':'owed'); document.getElementById('gtLbl').textContent=balanced?'✅ FULLY PAID':'💰 BALANCE DUE'; document.getElementById('gtAmt').textContent='$'+(balanced?allT.paid:allT.balance).toFixed(2); document.getElementById('gtNote').textContent=yr+' Hrs: $'+ytdHA.toFixed(2)+' + '+yr+' Exp: $'+ytdEA.toFixed(2)+' | Paid: $'+allT.paid.toFixed(2); document.getElementById('gtStatus').textContent=balanced?'✅ PAID':allT.paid>0?'◑ PARTIAL':'⏳ UNPAID'; document.getElementById('invStamp').innerHTML=balanced?'':''; document.getElementById('sAllHrs').textContent=allN.toFixed(1); document.getElementById('sHrsAmt').textContent='$'+Math.round(ytdHA); document.getElementById('sExpAmt').textContent='$'+Math.round(ytdEA); document.getElementById('sCharged').textContent='$'+Math.round(ytdC); document.getElementById('sPaidTot').textContent='$'+Math.round(allT.paid); document.getElementById('sBalDue').textContent='$'+Math.round(allT.balance); } /* ════════════════════════════════════════ RERENDER ════════════════════════════════════════ */ function tsRerender(){ populateYtdYears(); tsPopulateSelects(); renderRoster(); renderCos(); if(activeEmpId){ const emp=activeEmp(); const co=getCo(emp.company_id); document.getElementById('tsCo').value=co?co.name:'—'; document.getElementById('tsRate').value=emp.rate||''; tsBannerUpdate(); tsBuildWeek(); renderAllWeeks(); renderExp(); renderPay(); updatePayBar(); renderSummary(); } } /* ════════════════════════════════════════ INIT ════════════════════════════════════════ */ document.getElementById('tsStdHrs').addEventListener('input',tsUpdateTotals); const _now=new Date(); document.getElementById('expPeriod').value=_now.getFullYear()+'-'+String(_now.getMonth()+1).padStart(2,'0'); document.getElementById('pDate').value=_now.toISOString().slice(0,10); populateYtdYears(); setGreeting(); // Init Supabase function initApp(){ if(!window.supabase){ setTimeout(initApp,100); return; } db = window.supabase.createClient(SB_URL, SB_KEY); loadDash(); // Pre-load jobs so dropdown is ready in Timesheet jobsLoadAll(); // Pre-load subs subsLoadAll(); } initApp();