Add Excel-style selection/sum to Safe Count's New Total column
Extract the click/shift-click cell selection + sum/avg status bar from the weekly report into a shared useExcelSelection hook, and wire it up to the New Total column (and its footer total) on Safe Count.
This commit is contained in:
parent
5c5d051c35
commit
742960e5a1
3 changed files with 158 additions and 121 deletions
117
frontend/src/hooks/useExcelSelection.ts
Normal file
117
frontend/src/hooks/useExcelSelection.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { useState, useRef, useCallback, useEffect } from 'react'
|
||||||
|
import type React from 'react'
|
||||||
|
|
||||||
|
export interface SelStats { count: number; numCount: number; sum: number; avg: number }
|
||||||
|
|
||||||
|
// Excel-like click / shift-click cell range selection for plain HTML tables,
|
||||||
|
// with a live sum/avg readout and Ctrl+C copy. Cells are matched by table id
|
||||||
|
// plus row/col index, so each table using the hook needs a unique DOM id and
|
||||||
|
// its cells tagged via cs(tableId, row, col).
|
||||||
|
export function useExcelSelection() {
|
||||||
|
type Sel = { tableId: string; r1: number; c1: number; r2: number; c2: number }
|
||||||
|
const [sel, setSel] = useState<Sel | null>(null)
|
||||||
|
const [stats, setStats] = useState<SelStats | null>(null)
|
||||||
|
const selRef = useRef<Sel | null>(null)
|
||||||
|
const anchorRef = useRef<{ tableId: string; row: number; col: number } | null>(null)
|
||||||
|
const dragging = useRef(false)
|
||||||
|
|
||||||
|
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 }
|
||||||
|
const rows = Array.from(tbl.querySelectorAll('tr'))
|
||||||
|
let count = 0; const nums: number[] = []
|
||||||
|
for (let r = s.r1; r <= s.r2; r++) {
|
||||||
|
const cells = Array.from(rows[r]?.querySelectorAll('td,th') ?? [])
|
||||||
|
for (let c = s.c1; c <= s.c2; c++) {
|
||||||
|
const text = (cells[c]?.textContent ?? '').trim()
|
||||||
|
if (!text) continue
|
||||||
|
count++
|
||||||
|
const n = parseFloat(text.replace(/[£,%\s]/g, '').replace(/,/g, ''))
|
||||||
|
if (!isNaN(n)) nums.push(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sum = nums.reduce((a, b) => a + b, 0)
|
||||||
|
setStats({ count, numCount: nums.length, sum, avg: nums.length ? sum / nums.length : 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseDown = useCallback((tableId: string, e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
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
|
||||||
|
if (!s) return
|
||||||
|
const tbl = document.getElementById(s.tableId) as HTMLTableElement | null
|
||||||
|
if (!tbl) return
|
||||||
|
const rows = Array.from(tbl.querySelectorAll('tr'))
|
||||||
|
const lines: string[] = []
|
||||||
|
for (let r = s.r1; r <= s.r2; r++) {
|
||||||
|
const cells = Array.from(rows[r]?.querySelectorAll('td,th') ?? [])
|
||||||
|
lines.push(cells.slice(s.c1, s.c2 + 1).map(c => (c.textContent ?? '').trim()).join('\t'))
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(lines.join('\n')).catch(() => {})
|
||||||
|
}
|
||||||
|
document.addEventListener('mousemove', onMove)
|
||||||
|
document.addEventListener('mouseup', onUp)
|
||||||
|
document.addEventListener('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 {
|
||||||
|
if (!sel || sel.tableId !== tableId || r < sel.r1 || r > sel.r2 || c < sel.c1 || c > sel.c2) return {}
|
||||||
|
return { outline: '2px solid #0078d4', outlineOffset: '-2px', background: '#cce4ff' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSel() { setSel(null); selRef.current = null; setStats(null) }
|
||||||
|
|
||||||
|
return { onMouseDown, cs, stats, clearSel }
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
import { PageHeader, Card, Btn, StatusBadge } from '../components/Layout'
|
import { PageHeader, Card, Btn, StatusBadge } from '../components/Layout'
|
||||||
import { fmtGBP } from '../types'
|
import { fmtGBP } from '../types'
|
||||||
|
import { useExcelSelection } from '../hooks/useExcelSelection'
|
||||||
|
|
||||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -86,119 +87,6 @@ function varLabel(banked: number, reported: number) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Excel-like cell selection ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
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<Sel | null>(null)
|
|
||||||
const [stats, setStats] = useState<SelStats | null>(null)
|
|
||||||
const selRef = useRef<Sel | null>(null)
|
|
||||||
const anchorRef = useRef<{ tableId: string; row: number; col: number } | null>(null)
|
|
||||||
const dragging = useRef(false)
|
|
||||||
|
|
||||||
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 }
|
|
||||||
const rows = Array.from(tbl.querySelectorAll('tr'))
|
|
||||||
let count = 0; const nums: number[] = []
|
|
||||||
for (let r = s.r1; r <= s.r2; r++) {
|
|
||||||
const cells = Array.from(rows[r]?.querySelectorAll('td,th') ?? [])
|
|
||||||
for (let c = s.c1; c <= s.c2; c++) {
|
|
||||||
const text = (cells[c]?.textContent ?? '').trim()
|
|
||||||
if (!text) continue
|
|
||||||
count++
|
|
||||||
const n = parseFloat(text.replace(/[£,%\s]/g, '').replace(/,/g, ''))
|
|
||||||
if (!isNaN(n)) nums.push(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const sum = nums.reduce((a, b) => a + b, 0)
|
|
||||||
setStats({ count, numCount: nums.length, sum, avg: nums.length ? sum / nums.length : 0 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseDown = useCallback((tableId: string, e: React.MouseEvent<HTMLTableElement>) => {
|
|
||||||
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 {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
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
|
|
||||||
if (!s) return
|
|
||||||
const tbl = document.getElementById(s.tableId) as HTMLTableElement | null
|
|
||||||
if (!tbl) return
|
|
||||||
const rows = Array.from(tbl.querySelectorAll('tr'))
|
|
||||||
const lines: string[] = []
|
|
||||||
for (let r = s.r1; r <= s.r2; r++) {
|
|
||||||
const cells = Array.from(rows[r]?.querySelectorAll('td,th') ?? [])
|
|
||||||
lines.push(cells.slice(s.c1, s.c2 + 1).map(c => (c.textContent ?? '').trim()).join('\t'))
|
|
||||||
}
|
|
||||||
navigator.clipboard.writeText(lines.join('\n')).catch(() => {})
|
|
||||||
}
|
|
||||||
document.addEventListener('mousemove', onMove)
|
|
||||||
document.addEventListener('mouseup', onUp)
|
|
||||||
document.addEventListener('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 {
|
|
||||||
if (!sel || sel.tableId !== tableId || r < sel.r1 || r > sel.r2 || c < sel.c1 || c > sel.c2) return {}
|
|
||||||
return { outline: '2px solid #0078d4', outlineOffset: '-2px', background: '#cce4ff' }
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearSel() { setSel(null); selRef.current = null; setStats(null) }
|
|
||||||
|
|
||||||
return { onMouseDown, cs, stats, clearSel }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Occupancy stats ───────────────────────────────────────────────────────────
|
// ── Occupancy stats ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function buildOccStats(
|
function buildOccStats(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ import { GBP_DENOMINATIONS, fmtGBP } from '../types'
|
||||||
import type { FloatCount, FloatDenomination } from '../types'
|
import type { FloatCount, FloatDenomination } from '../types'
|
||||||
import { FloatHistory, FloatRecordPrint } from './FloatManagement'
|
import { FloatHistory, FloatRecordPrint } from './FloatManagement'
|
||||||
import type { DetailRecord } from './FloatManagement'
|
import type { DetailRecord } from './FloatManagement'
|
||||||
|
import { useExcelSelection } from '../hooks/useExcelSelection'
|
||||||
|
|
||||||
|
const NEW_TOTAL_TABLE_ID = 'tbl-safe-newtotal'
|
||||||
|
const NEW_TOTAL_COL = 3
|
||||||
|
|
||||||
function SafeCountAdjust() {
|
function SafeCountAdjust() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
@ -26,6 +30,7 @@ function SafeCountAdjust() {
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [savedRecord, setSavedRecord] = useState<DetailRecord | null>(null)
|
const [savedRecord, setSavedRecord] = useState<DetailRecord | null>(null)
|
||||||
const [reloadKey, setReloadKey] = useState(0)
|
const [reloadKey, setReloadKey] = useState(0)
|
||||||
|
const { onMouseDown: onTblDown, cs, stats, clearSel } = useExcelSelection()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
@ -133,9 +138,12 @@ function SafeCountAdjust() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<p style={{ fontSize: '0.75rem', color: 'var(--text-mid)', margin: '0 0 0.5rem' }}>
|
||||||
|
Click a New Total cell, then Shift+Click to select a range. Ctrl+C to copy.
|
||||||
|
</p>
|
||||||
<Card style={{ marginBottom: '1rem', padding: 0, overflow: 'hidden' }}>
|
<Card style={{ marginBottom: '1rem', padding: 0, overflow: 'hidden' }}>
|
||||||
<div style={{ overflowX: 'auto' }}>
|
<div style={{ overflowX: 'auto' }}>
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem', minWidth: '380px' }}>
|
<table id={NEW_TOTAL_TABLE_ID} style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem', minWidth: '380px' }}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ background: 'var(--body-bg)', borderBottom: '2px solid var(--card-border)' }}>
|
<tr style={{ background: 'var(--body-bg)', borderBottom: '2px solid var(--card-border)' }}>
|
||||||
<th style={thL}>Denomination</th>
|
<th style={thL}>Denomination</th>
|
||||||
|
|
@ -145,10 +153,11 @@ function SafeCountAdjust() {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{GBP_DENOMINATIONS.map(d => {
|
{GBP_DENOMINATIONS.map((d, i) => {
|
||||||
const curr = current[d.value] || 0
|
const curr = current[d.value] || 0
|
||||||
const adj = adjust[d.value] || 0
|
const adj = adjust[d.value] || 0
|
||||||
const newT = newTotals[d.value]
|
const newT = newTotals[d.value]
|
||||||
|
const newTotalRow = i + 1
|
||||||
return (
|
return (
|
||||||
<tr key={d.value} style={{ borderBottom: '1px solid var(--card-border)' }}>
|
<tr key={d.value} style={{ borderBottom: '1px solid var(--card-border)' }}>
|
||||||
<td style={{ padding: '0.3rem 0.75rem', fontWeight: 600 }}>{d.label}</td>
|
<td style={{ padding: '0.3rem 0.75rem', fontWeight: 600 }}>{d.label}</td>
|
||||||
|
|
@ -172,10 +181,15 @@ function SafeCountAdjust() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td style={{
|
<td
|
||||||
padding: '0.3rem 0.75rem', textAlign: 'right', fontWeight: 600,
|
onMouseDown={e => onTblDown(NEW_TOTAL_TABLE_ID, e)}
|
||||||
color: newT === 0 && (curr > 0 || adj < 0) ? 'var(--danger)' : undefined,
|
style={{
|
||||||
}}>
|
padding: '0.3rem 0.75rem', textAlign: 'right', fontWeight: 600,
|
||||||
|
cursor: 'cell', userSelect: 'none',
|
||||||
|
color: newT === 0 && (curr > 0 || adj < 0) ? 'var(--danger)' : undefined,
|
||||||
|
...cs(NEW_TOTAL_TABLE_ID, newTotalRow, NEW_TOTAL_COL),
|
||||||
|
}}
|
||||||
|
>
|
||||||
{newT > 0 ? fmtGBP(newT) : (curr > 0 || adj !== 0) ? fmtGBP(0) : '—'}
|
{newT > 0 ? fmtGBP(newT) : (curr > 0 || adj !== 0) ? fmtGBP(0) : '—'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -193,7 +207,14 @@ function SafeCountAdjust() {
|
||||||
? (adjustTotal > 0 ? '+' : '-') + fmtGBP(Math.abs(adjustTotal))
|
? (adjustTotal > 0 ? '+' : '-') + fmtGBP(Math.abs(adjustTotal))
|
||||||
: '—'}
|
: '—'}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '0.5rem 0.75rem', textAlign: 'right', fontSize: '1rem' }}>
|
<td
|
||||||
|
onMouseDown={e => onTblDown(NEW_TOTAL_TABLE_ID, e)}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 0.75rem', textAlign: 'right', fontSize: '1rem',
|
||||||
|
cursor: 'cell', userSelect: 'none',
|
||||||
|
...cs(NEW_TOTAL_TABLE_ID, GBP_DENOMINATIONS.length + 1, NEW_TOTAL_COL),
|
||||||
|
}}
|
||||||
|
>
|
||||||
{fmtGBP(newTotal)}
|
{fmtGBP(newTotal)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -202,6 +223,17 @@ function SafeCountAdjust() {
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{stats && (
|
||||||
|
<div style={{ position: 'fixed', bottom: '20px', right: '20px', background: '#2c3e50', color: '#ecf0f1', padding: '8px 16px', borderRadius: '6px', fontSize: '0.78rem', zIndex: 9999, boxShadow: '0 4px 12px rgba(0,0,0,0.4)', display: 'flex', gap: '1.5rem', alignItems: 'center', userSelect: 'none' }}>
|
||||||
|
<span>Count: <strong>{stats.count}</strong></span>
|
||||||
|
{stats.numCount > 0 && <>
|
||||||
|
<span>Sum: <strong>{fmtGBP(stats.sum)}</strong></span>
|
||||||
|
<span>Average: <strong>{fmtGBP(stats.avg)}</strong></span>
|
||||||
|
</>}
|
||||||
|
<button onClick={clearSel} style={{ background: 'none', border: 'none', color: '#bdc3c7', cursor: 'pointer', fontSize: '0.9rem', lineHeight: 1, padding: '0 2px', marginLeft: '4px' }}>✕</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Card style={{ marginBottom: '1rem' }}>
|
<Card style={{ marginBottom: '1rem' }}>
|
||||||
<label style={{ fontSize: '0.875rem', fontWeight: 600, display: 'block', marginBottom: '0.5rem' }}>Notes</label>
|
<label style={{ fontSize: '0.875rem', fontWeight: 600, display: 'block', marginBottom: '0.5rem' }}>Notes</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue