/* [NALA_CASHBOOK_V1] Buku Kas Harian — full port dari TRPH index.html [CASHBOOK_V1..V12] 2026-07-03. Self-contained: helper apiGet/FMT/Ic sendiri (scope babel terpisah dari script utama index.html). Expose: window.CashbookTab, window.CashbookReportTab */ (() => { /* [NALA_CASHBOOK_V1] IIFE — hindari bentrok const useState/Ic/FMT dengan script utama (babel share global scope) */ const {useState, useEffect, useRef, useMemo} = React; const EmptyState = ({msg})=>
{msg||'Tidak bisa memuat data. Coba lagi.'}
; const LoadingState = ({label})=>
{label||'Memuat data…'}
; const apiGet = (url)=>fetch(url,{credentials:'include'}).then(r=>r.ok?r.json():Promise.reject(r)); const FMT = { rp:(n)=>'Rp'+Math.round(Number(n)||0).toLocaleString('id-ID'), rpM:(n)=>{n=Number(n)||0;if(n>=1e9)return'Rp'+(n/1e9).toFixed(1)+'B';if(n>=1e6)return'Rp'+(n/1e6).toFixed(1)+'M';if(n>=1e3)return'Rp'+(n/1e3).toFixed(0)+'K';return'Rp'+n} }; /* Ic — subset icon TRPH yang dipakai cashbook (check/close/download/edit/plus/trash) */ const Ic = ({name, size=18}) => { const p = { download:'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3', close:'M18 6L6 18M6 6l12 12', plus:'M12 5v14M5 12h14', check:'M20 6L9 17l-5-5', trash:'M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2', edit:'M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4z', }[name] || ''; return ; }; /* ---------------- CASHBOOK TAB [CASHBOOK_V1] ---------------- */ const CB_STAFF=['Puput','Tutur','Joni','Ely']; const CB_CATEGORIES=[ {value:'room_payment', label:'Kamar Walk-In', pill:'p-room'}, /* [CASHBOOK_V11] renamed: distinguish from early/late add-ons */ {value:'parkir', label:'Parkir', pill:'p-park'}, {value:'upgrade_fee', label:'Upgrade fee', pill:'p-upg'}, {value:'early_checkin', label:'Early Check-In', pill:'p-room'}, /* [CASHBOOK_V11] */ {value:'late_checkout', label:'Late Check-Out', pill:'p-room'}, /* [CASHBOOK_V11] */ {value:'income_other', label:'Lain-lain', pill:'p-room'}, /* [CASHBOOK_V3] */ {value:'expense_bensin', label:'Bensin', pill:'p-out'}, {value:'expense_supplies', label:'Supplies', pill:'p-out'}, {value:'expense_other', label:'Pengeluaran lain', pill:'p-out'} /* [CASHBOOK_V3] renamed for distinct label */ ]; const CB_METHODS=[ {value:'cash', label:'Cash'}, {value:'transfer_bca', label:'TR BCA'}, {value:'transfer_bsi', label:'TR BSI'}, {value:'edc', label:'EDC'} ]; const CB_CAT_MAP=Object.fromEntries(CB_CATEGORIES.map(c=>[c.value,c])); const CB_METHOD_MAP=Object.fromEntries(CB_METHODS.map(m=>[m.value,m])); const CB_INCOME_CATS=new Set(['room_payment','parkir','upgrade_fee','early_checkin','late_checkout','income_other']); /* [CASHBOOK_V3] [CASHBOOK_V11] +early_checkin,late_checkout */ /* [CASHBOOK_V2] */ const CB_INCOME_CATEGORIES=CB_CATEGORIES.filter(c=>CB_INCOME_CATS.has(c.value)); const CB_EXPENSE_CATEGORIES=CB_CATEGORIES.filter(c=>!CB_INCOME_CATS.has(c.value)); const CB_SHIFT_LABEL={pagi:'Pagi',malam:'Malam'}; /* [CASHBOOK_V3] label maps for report tab */ const CB_CATEGORY_LABEL=Object.fromEntries(CB_CATEGORIES.map(c=>[c.value,c.label])); const CB_METHOD_LABEL=Object.fromEntries(CB_METHODS.map(m=>[m.value,m.label])); /* [CASHBOOK_V6] audit display helpers */ const CB_ACTION_LABEL={insert:'Tambah',update:'Ubah',delete:'Hapus'}; const CB_ACTION_COLOR={insert:'#27500A',update:'#854F0B',delete:'#A32D2D'}; const CB_ENTITY_LABEL={cashbook_entry:'Transaksi',cashbook_note:'Catatan'}; const cbAuditDiff=(audit)=>{ const trunc=(s,n)=>s&&s.length>n?s.slice(0,n)+'…':(s||''); if(audit.action==='insert'){ const a=audit.after_state||{}; if(audit.entity_type==='cashbook_entry'){ const amt=Number(a.amount||0).toLocaleString('id-ID'); return `[${a.category}] ${a.room_number||'—'} Rp${amt} ${a.payment_method||''}`; } return `Catatan: "${trunc(a.content,60)}"`; } if(audit.action==='delete'){ const b=audit.before_state||{}; if(audit.entity_type==='cashbook_entry'){ const amt=Number(b.amount||0).toLocaleString('id-ID'); return `Hapus: [${b.category}] ${b.room_number||'—'} Rp${amt}`; } return `Hapus catatan: "${trunc(b.content,60)}"`; } /* update: diff fields */ const before=audit.before_state||{}; const after=audit.after_state||{}; const skip=new Set(['updated_at','created_at']); const changes=[]; Object.keys(after).forEach(k=>{ if(skip.has(k)) return; if(JSON.stringify(before[k])!==JSON.stringify(after[k])){ changes.push(`${k}: ${before[k]??'∅'} → ${after[k]??'∅'}`); } }); return changes.join(', ')||'(tidak ada perubahan)'; }; const formatRp=(val)=>{ if(val===''||val===null||val===undefined)return''; const digits=String(val).replace(/\D/g,''); if(!digits)return''; return 'Rp'+parseInt(digits,10).toLocaleString('id-ID'); }; const parseRp=(str)=>{ const digits=String(str).replace(/\D/g,''); return digits?parseInt(digits,10):''; }; /* [CASHBOOK_V4] helpers */ const cbAutoShift=()=>new Date().getHours()<16?'pagi':'malam'; const cbFormatDate=(iso)=>{ if(!iso) return ''; const d=new Date(iso+'T00:00:00'); if(isNaN(d.getTime())) return iso; return d.toLocaleDateString('id-ID',{day:'numeric',month:'short',year:'numeric'}); }; const cbGroupByShiftStaff=(entries)=>{ const groups={}; entries.forEach(e=>{ const key=`${e.shift}__${e.staff_name}`; if(!groups[key]) groups[key]={shift:e.shift,staff_name:e.staff_name,entries:[],notes:[],firstAt:e.created_at}; groups[key].entries.push(e); }); return groups; }; const cbBlocksFromEntriesAndNotes=(entries,notes)=>{ const groups=cbGroupByShiftStaff(entries||[]); /* [CASHBOOK_V7] orphan notes: synthesize empty block kalau shift+staff ga ada di entries */ (notes||[]).forEach(n=>{ if(n.shift && n.staff_name){ const key=`${n.shift}__${n.staff_name}`; if(!groups[key]){ groups[key]={shift:n.shift,staff_name:n.staff_name,entries:[],notes:[],firstAt:n.created_at}; } groups[key].notes.push(n); } }); return Object.values(groups).sort((a,b)=>{ if(a.shift!==b.shift) return a.shift==='pagi'?-1:1; return new Date(a.firstAt||0)-new Date(b.firstAt||0); }); }; /* [CASHBOOK_V8] sort entries: pemasukan dulu, lalu pengeluaran (preserve created_at order within group) */ const cbIsIncome=(cat)=>CB_INCOME_CATS.has(cat); /* [CASHBOOK_V10] minus prefix for expense amounts */ const cbAmountDisplay=(entry)=>{ if(!entry || entry.amount==null) return FMT.rp(0); return cbIsIncome(entry.category) ? FMT.rp(entry.amount) : '-'+FMT.rp(entry.amount); }; const cbSortEntries=(entries)=>{ if(!entries) return []; return [...entries].sort((a,b)=>{ const aIn=cbIsIncome(a.category); const bIn=cbIsIncome(b.category); if(aIn!==bIn) return aIn?-1:1; return new Date(a.created_at||0)-new Date(b.created_at||0); }); }; const cbShiftFooter=(entries)=>{ /* [CASHBOOK_V7] empty case → friendly placeholder */ if(!entries || entries.length===0) return 'Tidak ada transaksi'; const inc=CB_INCOME_CATS; const exp=new Set(['expense_bensin','expense_supplies','expense_other']); const t={cash:0,transfer_bca:0,transfer_bsi:0,edc:0,outcome:0,kamar:0}; entries.forEach(e=>{ if(inc.has(e.category)){ t[e.payment_method]=(t[e.payment_method]||0)+(e.amount||0); if(e.category==='room_payment') t.kamar++; } else if(exp.has(e.category)){ t.outcome+=(e.amount||0); } }); const parts=[]; if(t.cash>0) parts.push('Cash '+FMT.rp(t.cash)); if(t.transfer_bca>0) parts.push('TR BCA '+FMT.rp(t.transfer_bca)); if(t.transfer_bsi>0) parts.push('TR BSI '+FMT.rp(t.transfer_bsi)); if(t.edc>0) parts.push('EDC '+FMT.rp(t.edc)); if(t.outcome>0) parts.push('Pengeluaran '+FMT.rp(t.outcome)); if(t.kamar>0) parts.push(t.kamar+' kamar'); return parts.length?parts.join(' · '):'Tidak ada transaksi'; }; /* [CASHBOOK_V4] CashbookTab — top-bar context (date+shift+staff), 3 buttons, per-shift blocks, footer recap, notes inside-shift + general */ function CashbookTab(){ const today=new Date().toISOString().slice(0,10); const [date,setDate]=useState(today); const [shift,setShift]=useState(cbAutoShift()); const [staff,setStaff]=useState(()=>{ try{return localStorage.getItem('cb_last_staff')||'Puput'}catch(e){return 'Puput'} }); const [entries,setEntries]=useState(null); const [notes,setNotes]=useState([]); const [summary,setSummary]=useState(null); const [modalMode,setModalMode]=useState(null); // 'income' | 'expense' | 'note' | null const [compactOpen,setCompactOpen]=useState(false); // [CASHBOOK_V7] tampilan ringkas const [waSending,setWaSending]=useState(null); // [CASHBOOK_V8] shiftKey while sending const [waSentKeys,setWaSentKeys]=useState({}); // [CASHBOOK_V8] session-scoped sent flags const [busy,setBusy]=useState(false); /* persist staff */ useEffect(()=>{ try{localStorage.setItem('cb_last_staff',staff)}catch(e){} },[staff]); const load=()=>{ apiGet('/api/cashbook?date='+date).then(setEntries).catch(()=>setEntries([])); apiGet('/api/cashbook/summary?date_from='+date+'&date_to='+date).then(setSummary).catch(()=>setSummary(null)); apiGet('/api/cashbook/notes?date='+date).then(setNotes).catch(()=>setNotes([])); }; useEffect(load,[date]); /* Submit batch — context comes from top-bar (date/shift/staff) */ const submitBatch=async(lines)=>{ const r=await fetch('/api/cashbook/batch',{ method:'POST',credentials:'include', headers:{'Content-Type':'application/json'}, body:JSON.stringify({entry_date:date, shift, staff_name:staff, lines}) }); if(!r.ok){const d=await r.json().catch(()=>({}));throw new Error(d.detail||'Submit failed')} setModalMode(null);load(); }; /* Submit note — content + includeShift (inherits date/staff from top-bar) */ const submitNote=async(content, includeShift)=>{ const r=await fetch('/api/cashbook/notes',{ method:'POST',credentials:'include', headers:{'Content-Type':'application/json'}, body:JSON.stringify({entry_date:date, shift:includeShift?shift:null, staff_name:staff, content}) }); if(!r.ok){const d=await r.json().catch(()=>({}));throw new Error(d.detail||'Submit failed')} setModalMode(null);load(); }; const deleteNote=async(id)=>{ if(!confirm('Hapus catatan?'))return; setBusy(true); try{await fetch('/api/cashbook/notes/'+id,{method:'DELETE',credentials:'include'});load()} catch(e){alert('Gagal hapus catatan: '+(e.message||''))} finally{setBusy(false)} }; /* [CASHBOOK_V8] reset sent-flags when date changes (each date stands alone) */ useEffect(()=>{setWaSentKeys({})},[date]); /* [CASHBOOK_V8] send shift recap to WA group */ const sendWA=async(shiftKey, shiftVal, staffName)=>{ if(!confirm(`Kirim rekap ${shiftVal==='pagi'?'Pagi':'Malam'} - ${staffName} (${cbFormatDate(date)}) ke grup WA TRPH?`)) return; setWaSending(shiftKey); try{ const r=await fetch('/api/cashbook/send-wa',{ method:'POST',credentials:'include', headers:{'Content-Type':'application/json'}, body:JSON.stringify({entry_date:date, shift:shiftVal, staff_name:staffName}) }); if(!r.ok){const d=await r.json().catch(()=>({}));throw new Error(d.detail||'Send failed')} setWaSentKeys(prev=>({...prev,[shiftKey]:true})); alert('Berhasil dikirim ke grup WA TRPH ✓'); }catch(e){alert('Gagal kirim WA: '+(e.message||'unknown'))} finally{setWaSending(null)} }; if(entries===null) return ; const blocks=cbBlocksFromEntriesAndNotes(entries,notes); const generalNotes=notes.filter(n=>!n.shift); const fmtRp=(n)=>FMT.rp(n); const transferTotal=summary ? (summary.income_transfer_bca||0)+(summary.income_transfer_bsi||0) : 0; const ctxLabel=`${cbFormatDate(date)} · Shift ${shift==='pagi'?'Pagi':'Malam'} · ${staff}`; return
{/* [CASHBOOK_V8] Top bar — pisah "Lihat tanggal" vs "Catat sebagai" */}
Lihat tanggal
setDate(e.target.value)}/>
Catat sebagai
{/* [CASHBOOK_V11] Malam → auto-switch staff ke Tutur (tetep bisa diubah manual) */}
{/* [CASHBOOK_V12] unified transaksi button — modal handles income+expense in one flow */}
{/* [CASHBOOK_V7] tampilan ringkas */}
{/* Day summary cards */}
Cash masuk
{fmtRp(summary?.income_cash||0)}
Transfer + EDC
{fmtRp(transferTotal+(summary?.income_edc||0))}
Pengeluaran
0?'var(--bad)':'var(--ink)'}}>{fmtRp(summary?.outcome_total||0)}
Net · Kamar
{fmtRp(summary?.net||0)} · {summary?.room_count||0} kamar
{/* Per-shift blocks */} {blocks.length===0 && generalNotes.length===0 ?
Belum ada entry untuk tanggal ini.
: blocks.map(b=>{ /* [CASHBOOK_V8] sort + separator + WA send key */ const sorted=cbSortEntries(b.entries); const incomeCount=sorted.filter(e=>cbIsIncome(e.category)).length; const hasIncome=incomeCount>0; const hasExpense=sorted.length-incomeCount>0; const shiftKey=`${b.shift}_${b.staff_name}`; const isSending=waSending===shiftKey; const isSent=!!waSentKeys[shiftKey]; const canSend=b.entries.length>0 || b.notes.length>0; return
{b.shift==='pagi'?'Pagi':'Malam'}
{b.staff_name}
· {cbFormatDate(date)}
{/* [CASHBOOK_V7] body only when there are entries; footer always renders so orphan-note blocks show "Tidak ada transaksi" */} {sorted.length>0 &&
{sorted.map((e,idx)=> {idx===incomeCount && hasIncome && hasExpense &&
─── Pengeluaran ───
}
)}
}
{cbShiftFooter(b.entries)} {/* [CASHBOOK_V9] kirim ke WA — filled WA-green, SVG icon, no emoji */} {canSend && (isSent ? ( Sudah dikirim ) : ( ))}
{b.notes.length>0 &&
{b.notes.map(n=>
{n.content}
)}
}
; }) } {/* General notes (no shift) */} {generalNotes.length>0 &&
Catatan umum (tanpa shift) · {generalNotes.length}
{generalNotes.map(n=>{ const meta=[n.staff_name||'—']; if(n.created_at) meta.push(new Date(n.created_at).toLocaleTimeString('id-ID',{hour:'2-digit',minute:'2-digit'})); return
{meta.join(' · ')}
{n.content}
; })}
} {/* Modals — context inherited from top-bar */} {(modalMode==='tx' || modalMode==='income' || modalMode==='expense') && setModalMode(null)} onSubmit={submitBatch} onAddNote={()=>setModalMode('note')}/>} {modalMode==='note' && setModalMode(null)} onSubmit={submitNote}/>} {/* [CASHBOOK_V7] tampilan ringkas */} {compactOpen && setCompactOpen(false)}/>}
; } /* [CASHBOOK_V4] CashbookEditRow — self-contained: own edit/delete state, view↔edit toggle */ function CashbookEditRow({entry, onUpdate, onDelete}){ const [editing,setEditing]=useState(false); const [busy,setBusy]=useState(false); const [amt,setAmt]=useState(parseRp(String(entry.amount))); const [room,setRoom]=useState(entry.room_number||''); const [desc,setDesc]=useState(entry.description||''); const [cat,setCat]=useState(entry.category); const [pm,setPm]=useState(entry.payment_method); const startEdit=()=>{ setAmt(parseRp(String(entry.amount))); setRoom(entry.room_number||''); setDesc(entry.description||''); setCat(entry.category); setPm(entry.payment_method); setEditing(true); }; const save=async()=>{ const a=Number(amt); if(!a||a<=0){alert('Nominal tidak valid');return} setBusy(true); try{ const r=await fetch('/api/cashbook/'+entry.id,{ method:'PATCH',credentials:'include', headers:{'Content-Type':'application/json'}, body:JSON.stringify({category:cat,room_number:room||null,description:desc||null,amount:a,payment_method:pm}) }); if(!r.ok){const d=await r.json().catch(()=>({}));throw new Error(d.detail||'Update failed')} setEditing(false);if(onUpdate)onUpdate(); }catch(e){alert('Gagal update: '+e.message)} finally{setBusy(false)} }; const del=async()=>{ if(!confirm('Hapus entry ini?'))return; setBusy(true); try{ await fetch('/api/cashbook/'+entry.id,{method:'DELETE',credentials:'include'}); if(onDelete)onDelete(); }catch(e){alert('Gagal hapus: '+e.message)} finally{setBusy(false)} }; if(editing){ /* Default cb-line cb-edit grid: 96px 56px 1fr 110px 70px 60px. Override (drop Kamar slot) when not room_payment. */ const editStyle = cat==='room_payment' ? undefined : {gridTemplateColumns:'96px 1fr 110px 70px 60px'}; return
{cat==='room_payment' && setRoom(e.target.value)}/>} setDesc(e.target.value)}/> setAmt(parseRp(e.target.value))}/>
; } const catMeta=CB_CAT_MAP[entry.category]||{label:entry.category,pill:'p-out'}; const methMeta=CB_METHOD_MAP[entry.payment_method]||{label:entry.payment_method}; return
{catMeta.label} {entry.room_number||''} {entry.description||''} {/* [CASHBOOK_V10] minus prefix + subtle red tint for expense */} {cbAmountDisplay(entry)} {methMeta.label}
; } /* [CASHBOOK_V12] CashbookSimpleModal — UNIFIED income+expense in one modal w/ per-line type toggle */ function CashbookSimpleModal({mode, contextLabel, onClose, onSubmit, onAddNote}){ /* mode optional, defaults first line kind. Lines can mix freely after. */ const initialKind = mode==='expense' ? 'expense' : 'income'; const defaultCatFor = (kind)=> kind==='income' ? 'room_payment' : 'expense_supplies'; const newLine=(kind=initialKind)=>({_kind:kind, category:defaultCatFor(kind), room_number:'', description:'', amount:'', payment_method:'cash'}); const [lines,setLines]=useState([newLine()]); const [busy,setBusy]=useState(false); const addLine=(kind)=>setLines(ls=>{ const k = kind || (ls.length ? ls[ls.length-1]._kind : 'income'); return [...ls, newLine(k)]; }); const removeLine=(i)=>setLines(ls=>ls.length>1?ls.filter((_,idx)=>idx!==i):ls); const updLine=(i,k,v)=>setLines(ls=>ls.map((l,idx)=>idx===i?{...l,[k]:v}:l)); const toggleKind=(i)=>setLines(ls=>ls.map((l,idx)=>{ if(idx!==i) return l; const newKind = l._kind==='income' ? 'expense' : 'income'; return {...l, _kind:newKind, category:defaultCatFor(newKind), room_number:''}; })); /* Counts for footer summary */ const incomeLines = lines.filter(l=>l._kind==='income' && l.amount!=='' && Number(l.amount)>0); const expenseLines = lines.filter(l=>l._kind==='expense' && l.amount!=='' && Number(l.amount)>0); const totalIn = incomeLines.reduce((s,l)=>s+Number(l.amount),0); const totalOut = expenseLines.reduce((s,l)=>s+Number(l.amount),0); const submit=async()=>{ const valid=lines.filter(l=>l.amount!==''&&Number(l.amount)>0); if(valid.length===0){alert('Minimal 1 baris dengan nominal > 0');return} setBusy(true); try{ await onSubmit(valid.map(l=>({ category:l.category, room_number:(l.category==='room_payment') && (l.room_number||'').trim()?l.room_number.trim():null, description:(l.description||'').trim()||null, amount:Number(l.amount), payment_method:l.payment_method }))); }catch(e){alert('Gagal simpan: '+(e.message||'unknown'))} finally{setBusy(false)} }; /* Grid templates: with-Kamar vs without-Kamar (chosen per-row by category) */ const gridColsWithRoom = '70px 130px 1fr 70px 130px 100px 32px'; const gridColsNoRoom = '70px 130px 1fr 130px 100px 32px'; /* Header follows: kalau gak ada baris yang room_payment, sembunyiin kolom Kamar di header juga */ const anyRoomLine = lines.some(l=>l.category==='room_payment'); const gridCols = anyRoomLine ? gridColsWithRoom : gridColsNoRoom; return
e.stopPropagation()} className="fade-in cb-modal-content" style={{width:840,maxWidth:'94vw',maxHeight:'86vh',background:'var(--surface)',border:'1px solid var(--line)',borderRadius:14,boxShadow:'var(--shadow-lg)',display:'flex',flexDirection:'column'}}>

Catat Transaksi

Untuk: {contextLabel}
Jenis
Kategori
Deskripsi
{anyRoomLine &&
Kamar
}
Nominal
Metode
{lines.map((l,i)=>{ const isIncome = l._kind==='income'; const cats = isIncome ? CB_INCOME_CATEGORIES : CB_EXPENSE_CATEGORIES; const rowCols = (l.category==='room_payment') ? gridColsWithRoom : gridColsNoRoom; return
updLine(i,'description',e.target.value)}/> {l.category==='room_payment' && updLine(i,'room_number',e.target.value)}/> } updLine(i,'amount',parseRp(e.target.value))}/>
; })}
{(incomeLines.length>0 || expenseLines.length>0) &&
{incomeLines.length>0 && {incomeLines.length} masuk · {fmtRp(totalIn)}} {incomeLines.length>0 && expenseLines.length>0 && · } {expenseLines.length>0 && {expenseLines.length} keluar · {fmtRp(totalOut)}} Net: {fmtRp(totalIn-totalOut)}
}
{onAddNote ? : }
; } /* [CASHBOOK_V4] CashbookNoteModalSimple — content + checkbox includeShift */ /* [CASHBOOK_V7] CashbookCompactView — clean B&W layout for screenshot → WA grup */ function CashbookCompactView({date, blocks, generalNotes, summary, onClose}){ const transferTotal=summary?(summary.income_transfer_bca||0)+(summary.income_transfer_bsi||0)+(summary.income_edc||0):0; return
e.stopPropagation()}>
The Raden Patah Heritage
Buku Kas Harian — {cbFormatDate(date)}
{summary &&
Cash masuk{FMT.rp(summary.income_cash||0)}
Transfer + EDC{FMT.rp(transferTotal)}
Pengeluaran{FMT.rp(summary.outcome_total||0)}
Net {FMT.rp(summary.net||0)}
Kamar terjual{summary.room_count||0}
} {blocks.map(b=>{ /* [CASHBOOK_V8] sort + separator (text-only style for compact view) */ const sorted=cbSortEntries(b.entries); const incomeCount=sorted.filter(e=>cbIsIncome(e.category)).length; const hasIncome=incomeCount>0; const hasExpense=sorted.length-incomeCount>0; return
{b.shift==='pagi'?'Pagi':'Malam'} — {b.staff_name}
{sorted.length===0 ?
Tidak ada transaksi
: <> {sorted.map((e,idx)=> {idx===incomeCount && hasIncome && hasExpense &&
─── Pengeluaran ───
}
{CB_CATEGORY_LABEL[e.category]||e.category}{e.room_number?` ${e.room_number}`:''}{e.description?` · ${e.description}`:''} {/* [CASHBOOK_V10] minus prefix; compact view stays B&W for screenshot */} {cbAmountDisplay(e)} {CB_METHOD_LABEL[e.payment_method]||e.payment_method}
)}
{cbShiftFooter(b.entries)}
} {b.notes.length>0 &&
{b.notes.map(n=>
📝 {n.content}
)}
}
; })} {generalNotes && generalNotes.length>0 &&
Catatan umum
{generalNotes.map(n=>
📝 {n.staff_name}: {n.content}
)}
}
ai.theradenpatah.com
; } function CashbookNoteModalSimple({contextLabel, shiftLabel, onClose, onSubmit}){ const [content,setContent]=useState(''); const [includeShift,setIncludeShift]=useState(true); const [busy,setBusy]=useState(false); const submit=async()=>{ const c=content.trim(); if(!c){alert('Catatan tidak boleh kosong');return} setBusy(true); try{ await onSubmit(c, includeShift); }catch(e){alert('Gagal simpan: '+(e.message||'unknown'))} finally{setBusy(false)} }; return
e.stopPropagation()} className="fade-in cb-modal-content" style={{width:520,maxWidth:'94vw',background:'var(--surface)',border:'1px solid var(--line)',borderRadius:14,boxShadow:'var(--shadow-lg)',display:'flex',flexDirection:'column'}}>

Tambah Catatan

Untuk: {contextLabel}{includeShift?` · Shift ${shiftLabel}`:' · Tanpa shift'}
Catatan