From 460e0c3d779d832ab757894d264c4eec84db0b30 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Thu, 2 Jul 2026 00:21:25 +0000 Subject: [PATCH] Weekly report: proper Excel drag-select (no text highlight) - Switch from onClick to onMouseDown + e.preventDefault() so the browser never starts its own text-selection on click or drag - Add document-level mousemove/mouseup listeners for drag-to-select: hold mouse button and sweep across cells to highlight a range - Shift+click still extends the rectangle from the anchor cell - userSelect: none on tables prevents any residual text highlighting - Stats bar (Count/Sum/Avg) now appears after mouseup so it reads the final selection, not mid-drag state Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/MultiDayReport.tsx | 101 +++++++++++++++++--------- 1 file changed, 66 insertions(+), 35 deletions(-) diff --git a/frontend/src/pages/MultiDayReport.tsx b/frontend/src/pages/MultiDayReport.tsx index 708c178..ebbc564 100644 --- a/frontend/src/pages/MultiDayReport.tsx +++ b/frontend/src/pages/MultiDayReport.tsx @@ -91,12 +91,32 @@ interface SelStats { count: number; numCount: number; sum: number; avg: number } function useExcelSelection() { type Sel = { tableId: string; r1: number; c1: number; r2: number; c2: number } const [sel, setSel] = useState(null) - const [anchor, setAnchor] = useState<{ tableId: string; row: number; col: number } | null>(null) const [stats, setStats] = useState(null) - const selRef = useRef(sel) - selRef.current = sel + const selRef = useRef(null) + const anchorRef = useRef<{ tableId: string; row: number; col: number } | null>(null) + const dragging = useRef(false) - function computeStats(s: Sel | null) { + function cellAt(tableId: string, x: number, y: number) { + const tbl = document.getElementById(tableId) as HTMLTableElement | null + if (!tbl) return null + const el = document.elementFromPoint(x, y) + const cell = el?.closest('td,th') as HTMLElement | null + if (!cell || !tbl.contains(cell)) return null + const row = cell.closest('tr') as HTMLTableRowElement + const allRows = Array.from(tbl.querySelectorAll('tr')) + const ri = allRows.indexOf(row) + const ci = Array.from(row.querySelectorAll('td,th')).indexOf(cell as HTMLTableCellElement) + return ri >= 0 && ci >= 0 ? { ri, ci } : null + } + + function applyExtend(tableId: string, ri: number, ci: number) { + const a = anchorRef.current + if (!a || a.tableId !== tableId) return + const s: Sel = { tableId, r1: Math.min(a.row, ri), c1: Math.min(a.col, ci), r2: Math.max(a.row, ri), c2: Math.max(a.col, ci) } + setSel(s); selRef.current = s + } + + function readStats(s: Sel | null) { if (!s) { setStats(null); return } const tbl = document.getElementById(s.tableId) as HTMLTableElement | null if (!tbl) { setStats(null); return } @@ -116,28 +136,33 @@ function useExcelSelection() { setStats({ count, numCount: nums.length, sum, avg: nums.length ? sum / nums.length : 0 }) } - const onClick = useCallback((tableId: string, e: React.MouseEvent) => { - const cell = (e.target as Element).closest('td,th') as HTMLElement | null - if (!cell) return - const tableEl = e.currentTarget - const allRows = Array.from(tableEl.querySelectorAll('tr')) - const row = cell.closest('tr') as HTMLTableRowElement - const ri = allRows.indexOf(row) - const ci = Array.from(row.querySelectorAll('td,th')).indexOf(cell as HTMLTableCellElement) - if (ri < 0 || ci < 0) return - let newSel: Sel - if (e.shiftKey && anchor?.tableId === tableId) { - newSel = { tableId, r1: Math.min(anchor.row, ri), c1: Math.min(anchor.col, ci), r2: Math.max(anchor.row, ri), c2: Math.max(anchor.col, ci) } + const onMouseDown = useCallback((tableId: string, e: React.MouseEvent) => { + e.preventDefault() // stops browser text-selection highlight + const pos = cellAt(tableId, e.clientX, e.clientY) + if (!pos) return + const { ri, ci } = pos + if (e.shiftKey && anchorRef.current?.tableId === tableId) { + applyExtend(tableId, ri, ci) + requestAnimationFrame(() => readStats(selRef.current)) } else { - newSel = { tableId, r1: ri, c1: ci, r2: ri, c2: ci } - setAnchor({ tableId, row: ri, col: ci }) + anchorRef.current = { tableId, row: ri, col: ci } + const s: Sel = { tableId, r1: ri, c1: ci, r2: ri, c2: ci } + setSel(s); selRef.current = s + dragging.current = true } - setSel(newSel) - // compute stats after next paint so DOM is settled - requestAnimationFrame(() => computeStats(newSel)) - }, [anchor]) + }, []) useEffect(() => { + function onMove(e: MouseEvent) { + if (!dragging.current || !anchorRef.current) return + const pos = cellAt(anchorRef.current.tableId, e.clientX, e.clientY) + if (pos) applyExtend(anchorRef.current.tableId, pos.ri, pos.ci) + } + function onUp() { + if (!dragging.current) return + dragging.current = false + requestAnimationFrame(() => readStats(selRef.current)) + } function onKey(e: KeyboardEvent) { if (!(e.ctrlKey || e.metaKey) || e.key !== 'c') return const s = selRef.current @@ -152,8 +177,14 @@ function useExcelSelection() { } navigator.clipboard.writeText(lines.join('\n')).catch(() => {}) } + document.addEventListener('mousemove', onMove) + document.addEventListener('mouseup', onUp) document.addEventListener('keydown', onKey) - return () => document.removeEventListener('keydown', onKey) + return () => { + document.removeEventListener('mousemove', onMove) + document.removeEventListener('mouseup', onUp) + document.removeEventListener('keydown', onKey) + } }, []) function cs(tableId: string, r: number, c: number): React.CSSProperties { @@ -161,9 +192,9 @@ function useExcelSelection() { return { outline: '2px solid #0078d4', outlineOffset: '-2px', background: '#cce4ff' } } - function clearSel() { setSel(null); setStats(null) } + function clearSel() { setSel(null); selRef.current = null; setStats(null) } - return { onClick, cs, stats, clearSel } + return { onMouseDown, cs, stats, clearSel } } // ── Occupancy stats ─────────────────────────────────────────────────────────── @@ -300,7 +331,7 @@ export function MultiDayReport() { const [error, setError] = useState('') const [debtors, setDebtors] = useState(null) const [debtorsLoading, setDebtorsLoading] = useState(false) - const { onClick, cs, stats, clearSel } = useExcelSelection() + const { onMouseDown: onTblDown, cs, stats, clearSel } = useExcelSelection() async function generate() { setLoading(true); setError(''); setResult(null); setDebtors(null) @@ -368,7 +399,7 @@ export function MultiDayReport() {
{/* BANKED table */} - onClick('tbl-banked', e)}> +
onTblDown('tbl-banked', e)}> @@ -429,7 +460,7 @@ export function MultiDayReport() {
BANKED
{/* REPORTED table */} - onClick('tbl-reported', e)}> +
onTblDown('tbl-reported', e)}> @@ -483,7 +514,7 @@ export function MultiDayReport() { {/* Gross Sales */}

Gross Sales (from Earned Revenue)

-
REPORTED
onClick('tbl-grosssales', e)}> +
onTblDown('tbl-grosssales', e)}> @@ -513,7 +544,7 @@ export function MultiDayReport() {

Sales Breakdown (Net Values)

Dates on rows, GL categories on columns. Click/Shift+Click to select, Ctrl+C to copy.

-
Date
onClick('tbl-sales', e)}> +
onTblDown('tbl-sales', e)}> @@ -572,7 +603,7 @@ export function MultiDayReport() {

Occupancy Statistics

{occStats.totalRooms > 0 &&

Total rooms: {occStats.totalRooms} (excluding overflow)

} -
Date
onClick('tbl-occ', e)}> +
onTblDown('tbl-occ', e)}> @@ -645,7 +676,7 @@ export function MultiDayReport() { {debtorsLoading ? (

Loading balances…

) : debtors ? ( -
Date
onClick('tbl-balances', e)}> +
onTblDown('tbl-balances', e)}> @@ -693,7 +724,7 @@ export function MultiDayReport() {

Copy/Paste Format (for Spreadsheet)

2-row format: Gateway Amex on second row. Select all and Ctrl+C.

Cash Up & Reconciliation

-
Date
onClick('tbl-paste-recon', e)}> +
onTblDown('tbl-paste-recon', e)}> @@ -736,7 +767,7 @@ export function MultiDayReport() {
BANKED

Gross Sales

- onClick('tbl-paste-gross', e)}> +
onTblDown('tbl-paste-gross', e)}> {['Date','','Gross Sales'].map((l, i) => )} @@ -778,6 +809,6 @@ export function MultiDayReport() { const lbl: React.CSSProperties = { fontSize: '0.75rem', color: 'var(--text-mid)', display: 'block', marginBottom: '0.25rem' } const inp: React.CSSProperties = { border: '1px solid var(--card-border)', borderRadius: '6px', padding: '0.4rem 0.6rem', fontSize: '0.875rem' } const sec: React.CSSProperties = { fontSize: '1rem', fontWeight: 700, marginBottom: '0.75rem' } -const tblSt: React.CSSProperties = { borderCollapse: 'collapse', fontSize: '0.8rem', width: '100%', cursor: 'cell', userSelect: 'text' } +const tblSt: React.CSSProperties = { borderCollapse: 'collapse', fontSize: '0.8rem', width: '100%', cursor: 'cell', userSelect: 'none' } const th: React.CSSProperties = { padding: '0.5rem 0.625rem', textAlign: 'right', fontWeight: 600, color: '#333', whiteSpace: 'nowrap', border: '1px solid #ccc' } const td: React.CSSProperties = { padding: '0.45rem 0.625rem', border: '1px solid #ddd' }
{l}