/* 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 ? (
) : (
onChange(e.target.value)}
style={{
width: '100%', boxSizing: 'border-box',
background: disabled ? C.cream + '80' : C.cream,
border: `1px solid ${disabled ? C.border : C.earth + '50'}`,
color: disabled ? C.muted : (value ? C.deep : C.muted),
fontFamily: "'DM Sans', sans-serif", fontSize: 15,
borderRadius: 9, padding: '13px 15px', appearance: 'none',
cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.7 : 1,
backgroundImage: `url("data:image/svg+xml;utf8, ")`,
backgroundRepeat: 'no-repeat', backgroundPosition: 'right 14px center',
}}>
{loading ? 'Loading…' : placeholder}
{options.map(o => {o} )}
)}
);
const StatCard = ({ label, value }) => (
);
const YearPills = ({ label, value, onPick, years }) => (
{label}
{years.map(y => (
onPick(value === String(y) ? '' : String(y))} style={{
border: `1px solid ${value === String(y) ? C.deep : C.border}`,
background: value === String(y) ? C.deep : C.cream,
color: value === String(y) ? C.cream : C.deep,
borderRadius: 9999, padding: '8px 13px', cursor: 'pointer',
fontFamily: "'DM Sans',sans-serif", fontSize: 13, fontWeight: 500,
transition: 'all .15s', fontFeatureSettings: "'tnum'",
}}>{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')}
p.onSearch && p.onSearch()} disabled={!p.canSearch} style={{
display: 'flex', alignItems: 'center', gap: 7,
border: 0, background: p.canSearch ? C.deep : C.border, color: p.canSearch ? C.cream : C.muted,
borderRadius: 9, padding: '9px 16px', cursor: p.canSearch ? 'pointer' : 'not-allowed',
fontFamily: "'DM Sans',sans-serif", fontSize: 13, fontWeight: 600,
}}>
{p.searched ? 'Update' : (isVal ? 'Value area' : 'Search')}
Clear all
Exit full page
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
onTypeChange(e.target.value)} style={{
width: '100%', boxSizing: 'border-box', background: C.cream,
border: `1px solid ${C.earth}50`, color: C.deep,
fontFamily: "'DM Sans',sans-serif", fontSize: 14.5, borderRadius: 9,
padding: '12px 15px', appearance: 'none', cursor: 'pointer',
backgroundImage: `url("data:image/svg+xml;utf8, ")`,
backgroundRepeat: 'no-repeat', backgroundPosition: 'right 14px center',
}}>
All property types
{availTypes.map(t => {t} )}
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) ? (
) : (
Year
Mo
Property Type
Scheme / Area
Road
Tenure
Floor (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 }) => (
);
Object.assign(window, { TxnFullPage });