Wire Newbook credentials to settings service
This commit is contained in:
commit
63a5a72fa3
32 changed files with 3386 additions and 0 deletions
99
frontend/src/pages/CashSummary.tsx
Normal file
99
frontend/src/pages/CashSummary.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { useState } from 'react'
|
||||
import { api } from '../api'
|
||||
import { PageHeader, Card, Btn } from '../components/Layout'
|
||||
import { GBP_DENOMINATIONS, fmtGBP, today } from '../types'
|
||||
|
||||
interface DenomRow { denomination_value: string; total_quantity: string; total_value: string }
|
||||
interface SummaryResult { denominations: DenomRow[]; period: { from: string; to: string } }
|
||||
|
||||
export function CashSummary() {
|
||||
const [from, setFrom] = useState(() => { const d = new Date(); d.setDate(d.getDate() - 6); return d.toISOString().slice(0, 10) })
|
||||
const [to, setTo] = useState(today)
|
||||
const [result, setResult] = useState<SummaryResult | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
async function generate() {
|
||||
setLoading(true); setError(''); setResult(null)
|
||||
try {
|
||||
const data = await api.get<SummaryResult>(`/reports/cash-summary?from=${from}&to=${to}`)
|
||||
setResult(data)
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to generate summary')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const grandTotal = result?.denominations.reduce((s, r) => s + parseFloat(r.total_value), 0) ?? 0
|
||||
|
||||
return (
|
||||
<div style={{ padding: '1.5rem', maxWidth: '640px' }}>
|
||||
<PageHeader title="Cash Denomination Summary" subtitle="Aggregate cash count across a date range" />
|
||||
|
||||
<Card style={{ marginBottom: '1rem', display: 'flex', gap: '1rem', alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '0.75rem', color: 'var(--text-mid)', display: 'block', marginBottom: '0.25rem' }}>From</label>
|
||||
<input type="date" value={from} onChange={e => setFrom(e.target.value)}
|
||||
style={inpSt} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '0.75rem', color: 'var(--text-mid)', display: 'block', marginBottom: '0.25rem' }}>To</label>
|
||||
<input type="date" value={to} onChange={e => setTo(e.target.value)}
|
||||
style={inpSt} />
|
||||
</div>
|
||||
<Btn onClick={generate} disabled={loading}>{loading ? 'Loading…' : 'Generate'}</Btn>
|
||||
</Card>
|
||||
|
||||
{error && (
|
||||
<div style={{ background: '#fee2e2', color: 'var(--danger)', border: '1px solid #fecaca', borderRadius: '6px', padding: '0.75rem 1rem', marginBottom: '1rem' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<Card>
|
||||
<h2 style={{ fontSize: '0.875rem', fontWeight: 700, marginBottom: '0.25rem' }}>
|
||||
{result.period.from} → {result.period.to}
|
||||
</h2>
|
||||
<p style={{ fontSize: '0.8rem', color: 'var(--text-mid)', marginBottom: '1rem' }}>
|
||||
Takings only (excludes float counts)
|
||||
</p>
|
||||
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid var(--card-border)' }}>
|
||||
<th style={{ ...thSt, textAlign: 'left' }}>Denomination</th>
|
||||
<th style={thSt}>Total Qty</th>
|
||||
<th style={thSt}>Total Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{GBP_DENOMINATIONS.map(d => {
|
||||
const row = result.denominations.find(r => Math.abs(parseFloat(r.denomination_value) - d.value) < 0.001)
|
||||
if (!row) return null
|
||||
return (
|
||||
<tr key={d.value} style={{ borderBottom: '1px solid var(--card-border)' }}>
|
||||
<td style={{ padding: '0.45rem 0.5rem', fontWeight: 600 }}>{d.label}</td>
|
||||
<td style={{ padding: '0.45rem 0.5rem', textAlign: 'right' }}>{row.total_quantity}</td>
|
||||
<td style={{ padding: '0.45rem 0.5rem', textAlign: 'right' }}>{fmtGBP(row.total_value)}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr style={{ borderTop: '2px solid var(--card-border)', background: 'var(--body-bg)' }}>
|
||||
<td style={{ padding: '0.6rem 0.5rem', fontWeight: 700 }}>Grand Total</td>
|
||||
<td></td>
|
||||
<td style={{ padding: '0.6rem 0.5rem', textAlign: 'right', fontWeight: 700, fontSize: '1rem' }}>{fmtGBP(grandTotal)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const inpSt: React.CSSProperties = { border: '1px solid var(--card-border)', borderRadius: '6px', padding: '0.4rem 0.6rem', fontSize: '0.875rem' }
|
||||
const thSt: React.CSSProperties = { padding: '0.5rem 0.5rem', textAlign: 'right', fontWeight: 600, color: 'var(--text-mid)' }
|
||||
Loading…
Add table
Add a link
Reference in a new issue