Fix invalid date in history; expand weekly report
- History: slice session_date to YYYY-MM-DD before Date parse (postgres returns date columns as ISO timestamps in JSON) - History: fix Edit/View navigate link passing full ISO string as date param - Weekly report: add daily status row, reconciliation variance (banked vs Newbook reported), sales with gross+net, debtors/creditors lazy-loaded Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4caf307fd1
commit
e4ab137025
2 changed files with 219 additions and 48 deletions
|
|
@ -136,7 +136,7 @@ export function History() {
|
|||
</td>
|
||||
<td style={tdSt}>
|
||||
<span style={{ fontWeight: 600 }}>
|
||||
{new Date(row.session_date + 'T12:00:00').toLocaleDateString('en-GB', { weekday: 'short', day: '2-digit', month: 'short', year: 'numeric' })}
|
||||
{new Date(row.session_date.slice(0, 10) + 'T12:00:00').toLocaleDateString('en-GB', { weekday: 'short', day: '2-digit', month: 'short', year: 'numeric' })}
|
||||
</span>
|
||||
</td>
|
||||
<td style={tdSt}><StatusBadge status={row.status} /></td>
|
||||
|
|
@ -148,7 +148,7 @@ export function History() {
|
|||
</td>
|
||||
<td style={{ ...tdSt, display: 'flex', gap: '0.4rem', justifyContent: 'flex-end' }}>
|
||||
<Btn small variant="ghost"
|
||||
onClick={() => navigate(`/daily?date=${row.session_date}`)}>
|
||||
onClick={() => navigate(`/daily?date=${row.session_date.slice(0, 10)}`)}>
|
||||
{row.status === 'draft' ? 'Edit' : 'View'}
|
||||
</Btn>
|
||||
{row.status === 'draft' && (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { api } from '../api'
|
||||
import { PageHeader, Card, Btn } from '../components/Layout'
|
||||
import { PageHeader, Card, Btn, StatusBadge } from '../components/Layout'
|
||||
import { fmtGBP } from '../types'
|
||||
|
||||
interface SalesCol { gl_code: string; category: string; net_amount: number; vat_amount: number; gross_amount: number }
|
||||
interface ReconRow { category: string; banked_amount: number; reported_amount: number }
|
||||
interface DayData {
|
||||
date: string
|
||||
cash_up: { total_cash_counted: string; status: string } | null
|
||||
cash_up: { id: number; status: 'draft' | 'final'; total_cash_counted: string } | null
|
||||
reconciliation: ReconRow[]
|
||||
daily_stats: { gross_sales: number; transaction_count: number } | null
|
||||
sales_breakdown: SalesCol[]
|
||||
|
|
@ -17,15 +17,38 @@ interface ReportResult {
|
|||
report_data: DayData[]
|
||||
sales_columns: Array<{ gl_code: string; display_name: string }>
|
||||
occupancy_data: OccupancyItem[]
|
||||
bookings_data: Array<{ date?: string; period?: string; arrivals?: number; departures?: number; [k: string]: unknown }>
|
||||
}
|
||||
interface DebtorBalance { creditors: number; debtors: number; overall: number }
|
||||
interface DebtorsResult {
|
||||
period_open_balance: DebtorBalance
|
||||
balances_by_date: Record<string, DebtorBalance>
|
||||
}
|
||||
|
||||
const RECON_LABELS: Record<string, string> = {
|
||||
cash: 'Cash', gateway_visa_mc: 'Gateway V/MC', gateway_amex: 'Gateway Amex',
|
||||
pdq_visa_mc: 'PDQ V/MC', pdq_amex: 'PDQ Amex', bacs: 'BACS',
|
||||
cash: 'Cash',
|
||||
gateway_visa_mc: 'Gateway V/MC',
|
||||
gateway_amex: 'Gateway Amex',
|
||||
pdq_visa_mc: 'PDQ V/MC',
|
||||
pdq_amex: 'PDQ Amex',
|
||||
bacs: 'BACS',
|
||||
}
|
||||
|
||||
const VARIANCE_CATEGORIES = new Set(['cash', 'pdq_visa_mc', 'pdq_amex'])
|
||||
|
||||
function fmtDate(d: string) {
|
||||
return new Date(d + 'T12:00:00').toLocaleDateString('en-GB', { weekday: 'short', day: '2-digit', month: 'short' })
|
||||
const s = (d ?? '').slice(0, 10)
|
||||
if (!s) return '—'
|
||||
return new Date(s + 'T12:00:00').toLocaleDateString('en-GB', { weekday: 'short', day: '2-digit', month: 'short' })
|
||||
}
|
||||
|
||||
function variance(banked: number, reported: number) {
|
||||
return banked - reported
|
||||
}
|
||||
|
||||
function varStyle(v: number): React.CSSProperties {
|
||||
if (Math.abs(v) < 0.005) return { textAlign: 'right', color: 'var(--text-mid)' }
|
||||
return { textAlign: 'right', color: v < 0 ? 'var(--danger)' : '#16a34a', fontWeight: 600 }
|
||||
}
|
||||
|
||||
export function MultiDayReport() {
|
||||
|
|
@ -36,9 +59,11 @@ export function MultiDayReport() {
|
|||
const [result, setResult] = useState<ReportResult | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [debtors, setDebtors] = useState<DebtorsResult | null>(null)
|
||||
const [debtorsLoading, setDebtorsLoading] = useState(false)
|
||||
|
||||
async function generate() {
|
||||
setLoading(true); setError(''); setResult(null)
|
||||
setLoading(true); setError(''); setResult(null); setDebtors(null)
|
||||
try {
|
||||
const data = await api.post<ReportResult>('/reports/multiday', { start_date: startDate, num_days: numDays })
|
||||
setResult(data)
|
||||
|
|
@ -49,6 +74,15 @@ export function MultiDayReport() {
|
|||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!result) return
|
||||
setDebtorsLoading(true)
|
||||
api.post<DebtorsResult>('/reports/debtors-creditors', { start_date: startDate, num_days: numDays })
|
||||
.then(d => setDebtors(d))
|
||||
.catch(() => {})
|
||||
.finally(() => setDebtorsLoading(false))
|
||||
}, [result, startDate, numDays])
|
||||
|
||||
const dates = result?.report_data.map(d => d.date) ?? []
|
||||
const salesCols = result?.sales_columns ?? []
|
||||
|
||||
|
|
@ -56,17 +90,17 @@ export function MultiDayReport() {
|
|||
<div style={{ padding: '1.5rem' }}>
|
||||
<PageHeader title="Weekly / Multi-Day Report" />
|
||||
|
||||
{/* Controls */}
|
||||
<Card style={{ marginBottom: '1.5rem', display: 'flex', gap: '1rem', alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '0.75rem', color: 'var(--text-mid)', display: 'block', marginBottom: '0.25rem' }}>Start Date</label>
|
||||
<input type="date" value={startDate} onChange={e => setStartDate(e.target.value)}
|
||||
style={{ border: '1px solid var(--card-border)', borderRadius: '6px', padding: '0.4rem 0.6rem', fontSize: '0.875rem' }} />
|
||||
<label style={labelSt}>Start Date</label>
|
||||
<input type="date" value={startDate} onChange={e => setStartDate(e.target.value)} style={inpSt} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '0.75rem', color: 'var(--text-mid)', display: 'block', marginBottom: '0.25rem' }}>Number of Days</label>
|
||||
<label style={labelSt}>Number of Days</label>
|
||||
<input type="number" value={numDays} min={1} max={365}
|
||||
onChange={e => setNumDays(Math.max(1, Math.min(365, parseInt(e.target.value) || 7)))}
|
||||
style={{ border: '1px solid var(--card-border)', borderRadius: '6px', padding: '0.4rem 0.6rem', fontSize: '0.875rem', width: '80px' }} />
|
||||
style={{ ...inpSt, width: '80px' }} />
|
||||
</div>
|
||||
<Btn onClick={generate} disabled={loading}>
|
||||
{loading ? 'Generating…' : 'Generate Report'}
|
||||
|
|
@ -81,39 +115,110 @@ export function MultiDayReport() {
|
|||
|
||||
{result && (
|
||||
<>
|
||||
{/* Table 1: Reconciliation Summary */}
|
||||
{/* Daily Status Row */}
|
||||
<Card style={{ marginBottom: '1.5rem' }}>
|
||||
<h2 style={sectionTitle}>Daily Cash Up Status</h2>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
{result.report_data.map(day => (
|
||||
<div key={day.date} style={{
|
||||
textAlign: 'center', padding: '0.75rem 1rem',
|
||||
background: 'var(--body-bg)', borderRadius: '6px',
|
||||
border: '1px solid var(--card-border)', minWidth: '80px',
|
||||
}}>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-mid)', marginBottom: '0.375rem' }}>{fmtDate(day.date)}</div>
|
||||
{day.cash_up
|
||||
? <StatusBadge status={day.cash_up.status} />
|
||||
: <span style={{ fontSize: '0.7rem', color: 'var(--text-mid)', fontWeight: 600, textTransform: 'uppercase', background: 'var(--card-border)', padding: '0.2rem 0.5rem', borderRadius: '4px' }}>None</span>
|
||||
}
|
||||
{day.cash_up && (
|
||||
<div style={{ fontSize: '0.8rem', fontWeight: 700, marginTop: '0.375rem' }}>
|
||||
{fmtGBP(day.cash_up.total_cash_counted)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Table 1: Reconciliation */}
|
||||
<Card style={{ marginBottom: '1.5rem', overflowX: 'auto' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 700, marginBottom: '1rem' }}>Reconciliation Summary</h2>
|
||||
<table style={{ borderCollapse: 'collapse', fontSize: '0.8rem', minWidth: '600px' }}>
|
||||
<h2 style={sectionTitle}>Payment Reconciliation</h2>
|
||||
<table style={{ borderCollapse: 'collapse', fontSize: '0.8rem', minWidth: '600px', width: '100%' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid var(--card-border)' }}>
|
||||
<th style={{ ...th, textAlign: 'left' }}>Category</th>
|
||||
{dates.map(d => <th key={d} style={th}>{fmtDate(d)}</th>)}
|
||||
{dates.map(d => (
|
||||
<th key={d} style={th} colSpan={VARIANCE_CATEGORIES.has('') ? 1 : 1}>
|
||||
{fmtDate(d)}
|
||||
</th>
|
||||
))}
|
||||
<th style={th}>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(RECON_LABELS).map(([key, label]) => {
|
||||
const values = result.report_data.map(day => {
|
||||
const row = day.reconciliation.find(r => r.category === key)
|
||||
return row ? row.banked_amount : 0
|
||||
})
|
||||
const rowTotal = values.reduce((s, v) => s + v, 0)
|
||||
if (values.every(v => v === 0) && rowTotal === 0) return null
|
||||
return (
|
||||
<tr key={key} style={{ borderBottom: '1px solid var(--card-border)' }}>
|
||||
<td style={{ ...td, fontWeight: 600 }}>{label}</td>
|
||||
{values.map((v, i) => <td key={i} style={{ ...td, textAlign: 'right' }}>{v ? fmtGBP(v) : '—'}</td>)}
|
||||
<td style={{ ...td, textAlign: 'right', fontWeight: 700 }}>{fmtGBP(rowTotal)}</td>
|
||||
const rows = result.report_data.map(day => day.reconciliation.find(r => r.category === key))
|
||||
const bankedVals = rows.map(r => r?.banked_amount ?? 0)
|
||||
const reportedVals = rows.map(r => r?.reported_amount ?? 0)
|
||||
const hasVariance = VARIANCE_CATEGORIES.has(key)
|
||||
const totalBanked = bankedVals.reduce((s, v) => s + v, 0)
|
||||
const totalReported = reportedVals.reduce((s, v) => s + v, 0)
|
||||
|
||||
if (bankedVals.every(v => v === 0) && reportedVals.every(v => v === 0)) return null
|
||||
|
||||
return [
|
||||
/* Reported row */
|
||||
hasVariance && (
|
||||
<tr key={`${key}-reported`} style={{ borderBottom: '1px solid transparent' }}>
|
||||
<td style={{ ...td, paddingLeft: '0.5rem', color: 'var(--text-mid)', fontSize: '0.75rem' }}>
|
||||
{label} — Newbook
|
||||
</td>
|
||||
{reportedVals.map((v, i) => (
|
||||
<td key={i} style={{ ...td, textAlign: 'right', color: 'var(--text-mid)', fontSize: '0.75rem' }}>
|
||||
{v ? fmtGBP(v) : '—'}
|
||||
</td>
|
||||
))}
|
||||
<td style={{ ...td, textAlign: 'right', color: 'var(--text-mid)', fontSize: '0.75rem' }}>
|
||||
{fmtGBP(totalReported)}
|
||||
</td>
|
||||
</tr>
|
||||
),
|
||||
/* Banked / main row */
|
||||
<tr key={`${key}-banked`} style={{ borderBottom: '1px solid var(--card-border)' }}>
|
||||
<td style={{ ...td, fontWeight: 600 }}>
|
||||
{hasVariance ? `${label} — Banked` : label}
|
||||
</td>
|
||||
{bankedVals.map((v, i) => {
|
||||
const rep = reportedVals[i]
|
||||
const v2 = hasVariance ? variance(v, rep) : null
|
||||
return (
|
||||
<td key={i} style={{ ...td, textAlign: 'right' }}>
|
||||
{v ? fmtGBP(v) : '—'}
|
||||
{v2 !== null && Math.abs(v2) > 0.005 && (
|
||||
<div style={{ fontSize: '0.7rem', color: v2 < 0 ? 'var(--danger)' : '#16a34a' }}>
|
||||
{v2 > 0 ? '+' : ''}{fmtGBP(v2)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
<td style={{ ...td, textAlign: 'right', fontWeight: 700 }}>
|
||||
{fmtGBP(totalBanked)}
|
||||
{hasVariance && Math.abs(totalBanked - totalReported) > 0.005 && (
|
||||
<div style={varStyle(totalBanked - totalReported)}>
|
||||
{totalBanked - totalReported > 0 ? '+' : ''}{fmtGBP(totalBanked - totalReported)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
].filter(Boolean)
|
||||
})}
|
||||
{/* Cash total row */}
|
||||
{/* Total row */}
|
||||
<tr style={{ borderTop: '2px solid var(--card-border)', background: 'var(--body-bg)' }}>
|
||||
<td style={{ ...td, fontWeight: 700 }}>Total Banked</td>
|
||||
<td style={{ ...td, fontWeight: 700 }}>Total</td>
|
||||
{result.report_data.map((day, i) => {
|
||||
const dayTotal = day.reconciliation.reduce((s, r) => s + r.banked_amount, 0)
|
||||
return <td key={i} style={{ ...td, textAlign: 'right', fontWeight: 700 }}>{fmtGBP(dayTotal)}</td>
|
||||
const t = day.reconciliation.reduce((s, r) => s + r.banked_amount, 0)
|
||||
return <td key={i} style={{ ...td, textAlign: 'right', fontWeight: 700 }}>{t ? fmtGBP(t) : '—'}</td>
|
||||
})}
|
||||
<td style={{ ...td, textAlign: 'right', fontWeight: 700 }}>
|
||||
{fmtGBP(result.report_data.reduce((s, d) => s + d.reconciliation.reduce((ss, r) => ss + r.banked_amount, 0), 0))}
|
||||
|
|
@ -126,8 +231,8 @@ export function MultiDayReport() {
|
|||
{/* Table 2: Sales Breakdown */}
|
||||
{salesCols.length > 0 && (
|
||||
<Card style={{ marginBottom: '1.5rem', overflowX: 'auto' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 700, marginBottom: '1rem' }}>Sales Breakdown (Net)</h2>
|
||||
<table style={{ borderCollapse: 'collapse', fontSize: '0.8rem', minWidth: '600px' }}>
|
||||
<h2 style={sectionTitle}>Sales Breakdown</h2>
|
||||
<table style={{ borderCollapse: 'collapse', fontSize: '0.8rem', minWidth: '600px', width: '100%' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid var(--card-border)' }}>
|
||||
<th style={{ ...th, textAlign: 'left' }}>Category</th>
|
||||
|
|
@ -137,26 +242,46 @@ export function MultiDayReport() {
|
|||
</thead>
|
||||
<tbody>
|
||||
{salesCols.map(col => {
|
||||
const values = result.report_data.map(day => {
|
||||
const vals = result.report_data.map(day => {
|
||||
const sb = day.sales_breakdown.find(s => s.gl_code === col.gl_code)
|
||||
return sb?.net_amount ?? 0
|
||||
return { net: sb?.net_amount ?? 0, gross: sb?.gross_amount ?? 0, vat: sb?.vat_amount ?? 0 }
|
||||
})
|
||||
const rowTotal = values.reduce((s, v) => s + v, 0)
|
||||
const totalGross = vals.reduce((s, v) => s + v.gross, 0)
|
||||
if (vals.every(v => v.gross === 0)) return null
|
||||
return (
|
||||
<tr key={col.gl_code} style={{ borderBottom: '1px solid var(--card-border)' }}>
|
||||
<td style={td}>{col.display_name}</td>
|
||||
{values.map((v, i) => <td key={i} style={{ ...td, textAlign: 'right' }}>{v ? fmtGBP(v) : '—'}</td>)}
|
||||
<td style={{ ...td, textAlign: 'right', fontWeight: 700 }}>{fmtGBP(rowTotal)}</td>
|
||||
{vals.map((v, i) => (
|
||||
<td key={i} style={{ ...td, textAlign: 'right' }}>
|
||||
{v.gross ? fmtGBP(v.gross) : '—'}
|
||||
{v.vat > 0 && (
|
||||
<div style={{ fontSize: '0.7rem', color: 'var(--text-mid)' }}>
|
||||
net {fmtGBP(v.net)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
<td style={{ ...td, textAlign: 'right', fontWeight: 700 }}>{fmtGBP(totalGross)}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
<tr style={{ borderTop: '2px solid var(--card-border)', background: 'var(--body-bg)' }}>
|
||||
<td style={{ ...td, fontWeight: 700 }}>Total</td>
|
||||
<td style={{ ...td, fontWeight: 700 }}>Total Gross</td>
|
||||
{result.report_data.map((day, i) => {
|
||||
const dayTotal = day.sales_breakdown.reduce((s, sb) => s + (sb.net_amount ?? 0), 0)
|
||||
return <td key={i} style={{ ...td, textAlign: 'right', fontWeight: 700 }}>{fmtGBP(dayTotal)}</td>
|
||||
const t = day.sales_breakdown.reduce((s, sb) => s + (sb.gross_amount ?? 0), 0)
|
||||
return <td key={i} style={{ ...td, textAlign: 'right', fontWeight: 700 }}>{t ? fmtGBP(t) : '—'}</td>
|
||||
})}
|
||||
<td style={{ ...td, textAlign: 'right', fontWeight: 700 }}>
|
||||
{fmtGBP(result.report_data.reduce((s, d) => s + d.sales_breakdown.reduce((ss, sb) => ss + (sb.gross_amount ?? 0), 0), 0))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr style={{ background: 'var(--body-bg)' }}>
|
||||
<td style={{ ...td, fontWeight: 700, color: 'var(--text-mid)', fontSize: '0.75rem' }}>Total Net</td>
|
||||
{result.report_data.map((day, i) => {
|
||||
const t = day.sales_breakdown.reduce((s, sb) => s + (sb.net_amount ?? 0), 0)
|
||||
return <td key={i} style={{ ...td, textAlign: 'right', color: 'var(--text-mid)', fontSize: '0.75rem' }}>{t ? fmtGBP(t) : '—'}</td>
|
||||
})}
|
||||
<td style={{ ...td, textAlign: 'right', color: 'var(--text-mid)', fontSize: '0.75rem' }}>
|
||||
{fmtGBP(result.report_data.reduce((s, d) => s + d.sales_breakdown.reduce((ss, sb) => ss + (sb.net_amount ?? 0), 0), 0))}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -168,8 +293,8 @@ export function MultiDayReport() {
|
|||
{/* Table 3: Occupancy */}
|
||||
{result.occupancy_data.length > 0 && (
|
||||
<Card style={{ marginBottom: '1.5rem', overflowX: 'auto' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 700, marginBottom: '1rem' }}>Occupancy</h2>
|
||||
<table style={{ borderCollapse: 'collapse', fontSize: '0.8rem', minWidth: '400px' }}>
|
||||
<h2 style={sectionTitle}>Occupancy</h2>
|
||||
<table style={{ borderCollapse: 'collapse', fontSize: '0.8rem', minWidth: '400px', width: '100%' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid var(--card-border)' }}>
|
||||
<th style={{ ...th, textAlign: 'left' }}>Date</th>
|
||||
|
|
@ -182,10 +307,12 @@ export function MultiDayReport() {
|
|||
<tbody>
|
||||
{result.occupancy_data.map((row, i) => {
|
||||
const date = row.period ?? row.date ?? ''
|
||||
const occ = row.total_rooms && row.rooms_sold ? ((row.rooms_sold / row.total_rooms) * 100).toFixed(1) : '—'
|
||||
const occ = row.total_rooms && row.rooms_sold
|
||||
? ((row.rooms_sold / row.total_rooms) * 100).toFixed(1)
|
||||
: '—'
|
||||
return (
|
||||
<tr key={i} style={{ borderBottom: '1px solid var(--card-border)' }}>
|
||||
<td style={td}>{date ? fmtDate(date) : '—'}</td>
|
||||
<td style={td}>{fmtDate(date)}</td>
|
||||
<td style={{ ...td, textAlign: 'right' }}>{row.rooms_sold ?? '—'}</td>
|
||||
<td style={{ ...td, textAlign: 'right' }}>{row.total_rooms ?? '—'}</td>
|
||||
<td style={{ ...td, textAlign: 'right' }}>{row.total_people ?? '—'}</td>
|
||||
|
|
@ -197,11 +324,55 @@ export function MultiDayReport() {
|
|||
</table>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Table 4: Debtors / Creditors */}
|
||||
<Card style={{ marginBottom: '1.5rem', overflowX: 'auto' }}>
|
||||
<h2 style={sectionTitle}>Debtors / Creditors</h2>
|
||||
{debtorsLoading ? (
|
||||
<p style={{ color: 'var(--text-mid)', fontSize: '0.875rem' }}>Loading balances…</p>
|
||||
) : debtors ? (
|
||||
<table style={{ borderCollapse: 'collapse', fontSize: '0.8rem', minWidth: '400px', width: '100%' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid var(--card-border)' }}>
|
||||
<th style={{ ...th, textAlign: 'left' }}>Date</th>
|
||||
<th style={th}>Debtors</th>
|
||||
<th style={th}>Creditors</th>
|
||||
<th style={th}>Net</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style={{ borderBottom: '1px solid var(--card-border)', background: 'var(--body-bg)' }}>
|
||||
<td style={{ ...td, fontWeight: 600, color: 'var(--text-mid)' }}>Opening Balance</td>
|
||||
<td style={{ ...td, textAlign: 'right' }}>{fmtGBP(debtors.period_open_balance.debtors)}</td>
|
||||
<td style={{ ...td, textAlign: 'right' }}>{fmtGBP(debtors.period_open_balance.creditors)}</td>
|
||||
<td style={{ ...td, textAlign: 'right', fontWeight: 700 }}>{fmtGBP(debtors.period_open_balance.overall)}</td>
|
||||
</tr>
|
||||
{dates.map(d => {
|
||||
const b = debtors.balances_by_date[d]
|
||||
if (!b) return null
|
||||
return (
|
||||
<tr key={d} style={{ borderBottom: '1px solid var(--card-border)' }}>
|
||||
<td style={td}>{fmtDate(d)}</td>
|
||||
<td style={{ ...td, textAlign: 'right' }}>{fmtGBP(b.debtors)}</td>
|
||||
<td style={{ ...td, textAlign: 'right' }}>{fmtGBP(b.creditors)}</td>
|
||||
<td style={{ ...td, textAlign: 'right', fontWeight: 700 }}>{fmtGBP(b.overall)}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p style={{ color: 'var(--text-mid)', fontSize: '0.875rem' }}>Balances unavailable.</p>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const labelSt: React.CSSProperties = { fontSize: '0.75rem', color: 'var(--text-mid)', display: 'block', marginBottom: '0.25rem' }
|
||||
const inpSt: React.CSSProperties = { border: '1px solid var(--card-border)', borderRadius: '6px', padding: '0.4rem 0.6rem', fontSize: '0.875rem' }
|
||||
const sectionTitle: React.CSSProperties = { fontSize: '1rem', fontWeight: 700, marginBottom: '1rem' }
|
||||
const th: React.CSSProperties = { padding: '0.5rem 0.625rem', textAlign: 'right', color: 'var(--text-mid)', fontWeight: 600, whiteSpace: 'nowrap' }
|
||||
const td: React.CSSProperties = { padding: '0.45rem 0.625rem' }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue