/* eslint-disable no-undef */ /* TxnFullPage.jsx — the maximized ("full page") presentation of the transaction explorer. Same underlying state/handlers as the docked bottom-sheet, re-laid-out as a spreadsheet-style workspace: a horizontal Location Search stepper, an expanded filter bar (property type + year-from/to pills + min/max price), a summary stats strip, and an airy light-header table. */ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const SQM = (sqft) => sqft ? +(sqft / 10.7639).toFixed(1) : null; const num1 = (n) => n == null ? '—' : n.toLocaleString('en-MY', { minimumFractionDigits: 1, maximumFractionDigits: 1 }); /* horizontal numbered cascade step */ const StepDrop = ({ n, label, value, placeholder, options, onChange, disabled, loading, onClear, searchable }) => (
{n} {label} {onClear && value && ( )}
{searchable ? ( ) : ( )}
); const StatCard = ({ label, value }) => (
{label}
{value}
); const YearPills = ({ label, value, onPick, years }) => (
{label}
{years.map(y => ( ))}
); const FullHeadCell = ({ children, right }) => ( {children} ); const FullCell = ({ children, right, mono, strong, muted }) => ( {children} ); const TxnFullPage = (p) => { const { sel, geo, districts, mukims, areas, roads, stateNames, selectState, selectDistrict, selectMukim, selectArea, selectRoad, selectPropertyType, clearAll, onExit, load, txns, filtered, availTypes, types, setTypes, yr, setYr, price, setPrice, years } = p; const isVal = p.variant === 'valuation'; const prices = filtered.map(r => r.price); const avg = prices.length ? Math.round(prices.reduce((a, b) => a + b, 0) / prices.length) : null; const sorted = prices.slice().sort((a, b) => a - b); const median = sorted.length ? sorted[Math.floor(sorted.length / 2)] : null; const min = sorted.length ? sorted[0] : null; const max = sorted.length ? sorted[sorted.length - 1] : null; const typeValue = (types.length === availTypes.length || types.length === 0) ? 'all' : types[0]; const onTypeChange = (v) => setTypes(v === 'all' ? availTypes : [v]); return (
{/* ===== LOCATION SEARCH ===== */}
Location Search
{isVal ? (sel.area ? `${sel.area} — automated valuation & area market data` : 'Drill down to a specific area to value the property') : (sel.state ? `${sel.state} — drill down to a specific area to view transactions` : 'Drill down to a specific area to view transactions')}
selectPropertyType('')} searchable/> selectState('')}/> selectDistrict('')} searchable/> selectMukim('')} searchable/> selectArea('')} searchable/> selectRoad('')}/>
{/* ===== VALUATION DASHBOARD (full-page) ===== */} {isVal && ( {p.searched ? : } )} {/* ===== FILTERS + STATS + TABLE ===== */} {!isVal && ( {/* filter bar */}
Property Type
setYr({ ...yr, single: '', from: v })}/> setYr({ ...yr, single: '', to: v })}/>
Min Price (RM) setPrice({ ...price, min: e.target.value.replace(/[^0-9]/g, '') })} placeholder="e.g. 200,000" inputMode="numeric" style={priceBox}/>
Max Price (RM) setPrice({ ...price, max: e.target.value.replace(/[^0-9]/g, '') })} placeholder="e.g. 1,000,000" inputMode="numeric" style={priceBox}/>
{/* stats strip */} {txns && txns.length > 0 && (
)} {/* table */} {load.t ? (
Loading property transactions…
) : (!txns) ? ( ) : (txns && txns.length === 0) ? ( ) : (filtered.length === 0) ? ( ) : (
YearMoProperty TypeScheme / AreaRoadTenureFloor (sqm)Land (sqm)Price (RM) {filtered.map((r, i) => { const mo = +r.date.slice(5, 7) - 1; return ( e.currentTarget.style.background = C.cream} onMouseLeave={e => e.currentTarget.style.background = i % 2 ? C.cream + '40' : 'transparent'}> {r.year}{MONTHS[mo]}{r.type}{r.area}{r.road}{r.tenure}{r.built == null ? '—' : r.built.toLocaleString('en-MY')}{r.land == null ? '—' : r.land.toLocaleString('en-MY')}{formatRM(r.price)} ); })}
)}
)}
); }; const priceBox = { width: 170, boxSizing: 'border-box', background: C.cream, border: `1px solid ${C.earth}50`, color: C.deep, fontFamily: "'JetBrains Mono',monospace", fontSize: 14, borderRadius: 9, padding: '12px 14px', outline: 'none', }; const FullEmpty = ({ title, sub }) => (
{title}
{sub}
); Object.assign(window, { TxnFullPage });