Wire Newbook credentials to settings service
This commit is contained in:
commit
63a5a72fa3
32 changed files with 3386 additions and 0 deletions
455
frontend/src/pages/DailyCashUp.tsx
Normal file
455
frontend/src/pages/DailyCashUp.tsx
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
import { useState, useCallback } from 'react'
|
||||
import { RefreshCw, Save, CheckCircle, Loader } from 'lucide-react'
|
||||
import { api } from '../api'
|
||||
import { PageHeader, Card, Btn, StatusBadge } from '../components/Layout'
|
||||
import {
|
||||
GBP_DENOMINATIONS, fmtGBP, today,
|
||||
type User, type CashUp, type Denomination, type CardMachine,
|
||||
type PaymentTotals, type ReconciliationRow, type TillPayment, type Attachment,
|
||||
} from '../types'
|
||||
|
||||
const MACHINES = ['Front Desk', 'Restaurant / Bar']
|
||||
|
||||
function initDenominations(countType: 'takings' | 'float'): Denomination[] {
|
||||
return GBP_DENOMINATIONS.map(d => ({
|
||||
count_type: countType,
|
||||
denomination_type: d.type,
|
||||
denomination_value: d.value,
|
||||
quantity: null,
|
||||
value_entered: null,
|
||||
total_amount: 0,
|
||||
}))
|
||||
}
|
||||
|
||||
function initMachines(): CardMachine[] {
|
||||
return MACHINES.map(name => ({ machine_name: name, total_amount: 0, amex_amount: 0, visa_mc_amount: 0 }))
|
||||
}
|
||||
|
||||
function denomTotal(denoms: Denomination[]) {
|
||||
return denoms.reduce((s, d) => s + d.total_amount, 0)
|
||||
}
|
||||
|
||||
interface Props { user: User }
|
||||
|
||||
export function DailyCashUp({ user: _user }: Props) {
|
||||
const [date, setDate] = useState(today())
|
||||
const [cashUp, setCashUp] = useState<CashUp | null>(null)
|
||||
const [takings, setTakings] = useState<Denomination[]>(initDenominations('takings'))
|
||||
const [float, setFloat] = useState<Denomination[]>(initDenominations('float'))
|
||||
const [machines, setMachines] = useState<CardMachine[]>(initMachines())
|
||||
const [notes, setNotes] = useState('')
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([])
|
||||
const [newbookTotals, setNewbookTotals] = useState<PaymentTotals | null>(null)
|
||||
const [tillPayments, setTillPayments] = useState<TillPayment[]>([])
|
||||
const [fetching, setFetching] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null)
|
||||
const [showFloat, setShowFloat] = useState(false)
|
||||
|
||||
const isFinal = cashUp?.status === 'final'
|
||||
|
||||
function flash(text: string, ok = true) {
|
||||
setMsg({ text, ok })
|
||||
setTimeout(() => setMsg(null), 4000)
|
||||
}
|
||||
|
||||
async function loadExisting() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await api.get<{
|
||||
cash_up: CashUp; denominations: Denomination[]; card_machines: CardMachine[];
|
||||
reconciliation: ReconciliationRow[]; attachments: Attachment[]
|
||||
}>(`/cashup?date=${date}`)
|
||||
|
||||
setCashUp(data.cash_up)
|
||||
setNotes(data.cash_up.notes || '')
|
||||
setAttachments(data.attachments || [])
|
||||
|
||||
// Rebuild denomination grids from saved data
|
||||
const rebuild = (ct: 'takings' | 'float') =>
|
||||
GBP_DENOMINATIONS.map(d => {
|
||||
const saved = data.denominations.find(
|
||||
s => s.count_type === ct && parseFloat(String(s.denomination_value)) === d.value
|
||||
)
|
||||
return saved
|
||||
? { ...saved, denomination_value: d.value }
|
||||
: { count_type: ct, denomination_type: d.type, denomination_value: d.value, quantity: null, value_entered: null, total_amount: 0 }
|
||||
})
|
||||
|
||||
setTakings(rebuild('takings'))
|
||||
setFloat(rebuild('float'))
|
||||
|
||||
if (data.card_machines.length) {
|
||||
setMachines(MACHINES.map(name => {
|
||||
const m = data.card_machines.find(c => c.machine_name === name)
|
||||
return m ?? { machine_name: name, total_amount: 0, amex_amount: 0, visa_mc_amount: 0 }
|
||||
}))
|
||||
}
|
||||
|
||||
flash('Loaded existing cash up.')
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error && e.message === 'Not found') flash('No cash up for this date.', false)
|
||||
else flash('Failed to load.', false)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setCashUp(null)
|
||||
setTakings(initDenominations('takings'))
|
||||
setFloat(initDenominations('float'))
|
||||
setMachines(initMachines())
|
||||
setNotes('')
|
||||
setAttachments([])
|
||||
setNewbookTotals(null)
|
||||
setTillPayments([])
|
||||
}
|
||||
|
||||
function updateDenom(list: Denomination[], setList: (d: Denomination[]) => void, idx: number, field: 'quantity' | 'value_entered', raw: string) {
|
||||
const val = raw === '' ? null : parseFloat(raw)
|
||||
const updated = list.map((d, i) => {
|
||||
if (i !== idx) return d
|
||||
if (field === 'quantity') {
|
||||
const qty = val === null ? null : Math.max(0, Math.round(val))
|
||||
return { ...d, quantity: qty, value_entered: null, total_amount: qty === null ? 0 : qty * d.denomination_value }
|
||||
} else {
|
||||
const ve = val === null ? null : Math.max(0, val)
|
||||
return { ...d, value_entered: ve, quantity: null, total_amount: ve ?? 0 }
|
||||
}
|
||||
})
|
||||
setList(updated)
|
||||
}
|
||||
|
||||
function updateMachine(idx: number, field: 'total_amount' | 'amex_amount', raw: string) {
|
||||
const val = raw === '' ? 0 : parseFloat(raw) || 0
|
||||
setMachines(machines.map((m, i) => {
|
||||
if (i !== idx) return m
|
||||
const total = field === 'total_amount' ? val : m.total_amount
|
||||
const amex = field === 'amex_amount' ? val : m.amex_amount
|
||||
return { ...m, total_amount: total, amex_amount: amex, visa_mc_amount: Math.max(0, total - amex) }
|
||||
}))
|
||||
}
|
||||
|
||||
async function fetchNewbook() {
|
||||
setFetching(true)
|
||||
try {
|
||||
const data = await api.post<{
|
||||
count: number; totals: PaymentTotals; till_payments: TillPayment[]
|
||||
}>('/newbook/payments', { date })
|
||||
setNewbookTotals(data.totals)
|
||||
setTillPayments(data.till_payments || [])
|
||||
flash(`Fetched ${data.count} payment(s) from Newbook.`)
|
||||
} catch (e: unknown) {
|
||||
flash(e instanceof Error ? e.message : 'Failed to fetch Newbook data.', false)
|
||||
} finally {
|
||||
setFetching(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function save(status: 'draft' | 'final') {
|
||||
setSaving(true)
|
||||
try {
|
||||
const allDenoms = [...takings, ...float].filter(d => d.total_amount > 0)
|
||||
const result = await api.post<{ cash_up_id: number; message: string }>('/cashup/save', {
|
||||
session_date: date,
|
||||
status,
|
||||
notes,
|
||||
denominations: allDenoms.map(d => ({
|
||||
count_type: d.count_type,
|
||||
type: d.denomination_type,
|
||||
value: d.denomination_value,
|
||||
quantity: d.quantity,
|
||||
value_entered: d.value_entered,
|
||||
total_amount: d.total_amount,
|
||||
})),
|
||||
card_machines: machines.map(m => ({
|
||||
name: m.machine_name,
|
||||
total: m.total_amount,
|
||||
amex: m.amex_amount,
|
||||
visa_mc: m.visa_mc_amount,
|
||||
})),
|
||||
})
|
||||
flash(result.message)
|
||||
if (status === 'final') {
|
||||
setCashUp(prev => prev ? { ...prev, status: 'final' } : null)
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
flash(e instanceof Error ? e.message : 'Save failed.', false)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Build reconciliation rows from local + Newbook data
|
||||
const recon: ReconciliationRow[] = newbookTotals ? [
|
||||
{ category: 'Cash', banked_amount: denomTotal(takings), reported_amount: newbookTotals.cash },
|
||||
{ category: 'PDQ Visa/MC', banked_amount: machines.reduce((s, m) => s + m.visa_mc_amount, 0), reported_amount: newbookTotals.manual_visa_mc },
|
||||
{ category: 'PDQ Amex', banked_amount: machines.reduce((s, m) => s + m.amex_amount, 0), reported_amount: newbookTotals.manual_amex },
|
||||
{ category: 'Gateway Visa/MC', banked_amount: newbookTotals.gateway_visa_mc, reported_amount: newbookTotals.gateway_visa_mc },
|
||||
{ category: 'Gateway Amex', banked_amount: newbookTotals.gateway_amex, reported_amount: newbookTotals.gateway_amex },
|
||||
{ category: 'BACS', banked_amount: newbookTotals.bacs, reported_amount: newbookTotals.bacs },
|
||||
] : []
|
||||
|
||||
const totalPdq = machines.reduce((s, m) => s + m.total_amount, 0)
|
||||
|
||||
return (
|
||||
<div style={{ padding: '1.5rem', maxWidth: '900px' }}>
|
||||
<PageHeader title="Daily Cash Up" subtitle={cashUp ? `Status: ` : undefined} />
|
||||
{cashUp && <div style={{ marginTop: '-1rem', marginBottom: '1rem' }}><StatusBadge status={cashUp.status} /></div>}
|
||||
|
||||
{msg && (
|
||||
<div style={{
|
||||
background: msg.ok ? '#dcfce7' : '#fee2e2', color: msg.ok ? '#16a34a' : 'var(--danger)',
|
||||
border: `1px solid ${msg.ok ? '#bbf7d0' : '#fecaca'}`, borderRadius: '6px',
|
||||
padding: '0.625rem 1rem', marginBottom: '1rem', fontSize: '0.875rem',
|
||||
}}>
|
||||
{msg.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Date selector */}
|
||||
<Card style={{ marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '0.8rem', color: 'var(--text-mid)', display: 'block', marginBottom: '0.25rem' }}>Business Date</label>
|
||||
<input type="date" value={date} disabled={isFinal}
|
||||
onChange={e => { setDate(e.target.value); reset() }}
|
||||
style={{ border: '1px solid var(--card-border)', borderRadius: '6px', padding: '0.45rem 0.75rem', fontSize: '0.9rem' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'flex-end', paddingBottom: '1px' }}>
|
||||
<Btn onClick={loadExisting} disabled={loading || isFinal} variant="secondary" small>
|
||||
{loading ? 'Loading…' : 'Load Existing'}
|
||||
</Btn>
|
||||
{cashUp && <Btn onClick={reset} variant="ghost" small>New</Btn>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Cash denomination — Takings */}
|
||||
<Card style={{ marginBottom: '1rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 700 }}>Cash Takings</h2>
|
||||
<span style={{ fontWeight: 700, fontSize: '1.1rem' }}>{fmtGBP(denomTotal(takings))}</span>
|
||||
</div>
|
||||
<DenomGrid denoms={takings} onChange={(i, f, v) => updateDenom(takings, setTakings, i, f, v)} disabled={isFinal} />
|
||||
</Card>
|
||||
|
||||
{/* Float (collapsible) */}
|
||||
<Card style={{ marginBottom: '1rem' }}>
|
||||
<button onClick={() => setShowFloat(f => !f)} style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
width: '100%', background: 'none', border: 'none', padding: 0, cursor: 'pointer',
|
||||
}}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 700 }}>Float Count</h2>
|
||||
<span style={{ fontSize: '0.9rem', color: 'var(--text-mid)' }}>
|
||||
{fmtGBP(denomTotal(float))} {showFloat ? '▲' : '▼'}
|
||||
</span>
|
||||
</button>
|
||||
{showFloat && (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<DenomGrid denoms={float} onChange={(i, f, v) => updateDenom(float, setFloat, i, f, v)} disabled={isFinal} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Card Machines */}
|
||||
<Card style={{ marginBottom: '1rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 700 }}>Card Machines (PDQ)</h2>
|
||||
<span style={{ fontWeight: 700, fontSize: '1.1rem' }}>Total: {fmtGBP(totalPdq)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||
{machines.map((m, i) => (
|
||||
<div key={m.machine_name} style={{ border: '1px solid var(--card-border)', borderRadius: '8px', padding: '1rem' }}>
|
||||
<h3 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.75rem', color: 'var(--text-mid)' }}>
|
||||
{m.machine_name}
|
||||
</h3>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
<MoneyInput label="Total" value={m.total_amount}
|
||||
onChange={v => updateMachine(i, 'total_amount', v)} disabled={isFinal} />
|
||||
<MoneyInput label="Amex" value={m.amex_amount}
|
||||
onChange={v => updateMachine(i, 'amex_amount', v)} disabled={isFinal} />
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.875rem', paddingTop: '0.25rem' }}>
|
||||
<span style={{ color: 'var(--text-mid)' }}>Visa / MC</span>
|
||||
<span style={{ fontWeight: 600 }}>{fmtGBP(m.visa_mc_amount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Newbook + Reconciliation */}
|
||||
<Card style={{ marginBottom: '1rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 700 }}>Newbook Reconciliation</h2>
|
||||
<Btn onClick={fetchNewbook} disabled={fetching || isFinal} small>
|
||||
{fetching ? <><Loader size={13} style={{ animation: 'spin 1s linear infinite' }} /> Fetching…</> : <><RefreshCw size={13} /> Fetch Payments</>}
|
||||
</Btn>
|
||||
</div>
|
||||
|
||||
{newbookTotals && (
|
||||
<>
|
||||
<table style={{ width: '100%', fontSize: '0.875rem', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid var(--card-border)' }}>
|
||||
{['Category', 'Banked', 'Reported', 'Variance'].map(h => (
|
||||
<th key={h} style={{ padding: '0.4rem 0.5rem', textAlign: h === 'Category' ? 'left' : 'right', color: 'var(--text-mid)', fontWeight: 600 }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recon.map(row => {
|
||||
const variance = row.banked_amount - row.reported_amount
|
||||
const varColor = Math.abs(variance) < 0.01 ? 'var(--text-mid)' : variance > 0 ? 'var(--success)' : 'var(--danger)'
|
||||
return (
|
||||
<tr key={row.category} style={{ borderBottom: '1px solid var(--card-border)' }}>
|
||||
<td style={{ padding: '0.5rem' }}>{row.category}</td>
|
||||
<td style={{ padding: '0.5rem', textAlign: 'right' }}>{fmtGBP(row.banked_amount)}</td>
|
||||
<td style={{ padding: '0.5rem', textAlign: 'right' }}>{fmtGBP(row.reported_amount)}</td>
|
||||
<td style={{ padding: '0.5rem', textAlign: 'right', color: varColor, fontWeight: 600 }}>
|
||||
{Math.abs(variance) < 0.01 ? '—' : (variance > 0 ? '+' : '') + fmtGBP(Math.abs(variance))}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{tillPayments.length > 0 && (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<h3 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.5rem', color: 'var(--text-mid)' }}>
|
||||
Till / Restaurant Transactions
|
||||
</h3>
|
||||
<table style={{ width: '100%', fontSize: '0.8rem', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid var(--card-border)' }}>
|
||||
<th style={{ padding: '0.35rem 0.5rem', textAlign: 'left', color: 'var(--text-mid)' }}>Type</th>
|
||||
<th style={{ padding: '0.35rem 0.5rem', textAlign: 'right', color: 'var(--text-mid)' }}>Qty</th>
|
||||
<th style={{ padding: '0.35rem 0.5rem', textAlign: 'right', color: 'var(--text-mid)' }}>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tillPayments.map(t => (
|
||||
<tr key={t.payment_type} style={{ borderBottom: '1px solid var(--card-border)' }}>
|
||||
<td style={{ padding: '0.35rem 0.5rem' }}>{t.payment_type}</td>
|
||||
<td style={{ padding: '0.35rem 0.5rem', textAlign: 'right' }}>{t.quantity}</td>
|
||||
<td style={{ padding: '0.35rem 0.5rem', textAlign: 'right' }}>{fmtGBP(t.total_value)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Notes */}
|
||||
<Card style={{ marginBottom: '1rem' }}>
|
||||
<label style={{ fontSize: '0.875rem', fontWeight: 600, display: 'block', marginBottom: '0.5rem' }}>Notes</label>
|
||||
<textarea
|
||||
value={notes} onChange={e => setNotes(e.target.value)}
|
||||
disabled={isFinal}
|
||||
rows={3}
|
||||
placeholder="Explain any variances or issues…"
|
||||
style={{
|
||||
width: '100%', border: '1px solid var(--card-border)', borderRadius: '6px',
|
||||
padding: '0.6rem 0.75rem', fontSize: '0.875rem', resize: 'vertical',
|
||||
background: isFinal ? 'var(--body-bg)' : 'white',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Action buttons */}
|
||||
{!isFinal && (
|
||||
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
||||
<Btn onClick={() => save('draft')} disabled={saving} variant="secondary">
|
||||
<Save size={14} style={{ marginRight: '0.4rem' }} />
|
||||
{saving ? 'Saving…' : 'Save Draft'}
|
||||
</Btn>
|
||||
<Btn onClick={() => save('final')} disabled={saving}>
|
||||
<CheckCircle size={14} style={{ marginRight: '0.4rem' }} />
|
||||
{saving ? 'Submitting…' : 'Submit Final'}
|
||||
</Btn>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFinal && (
|
||||
<div style={{ color: 'var(--text-mid)', fontSize: '0.875rem', fontStyle: 'italic' }}>
|
||||
This cash up has been finalised and cannot be edited.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`@keyframes spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DenomGrid({ denoms, onChange, disabled }: {
|
||||
denoms: Denomination[]
|
||||
onChange: (idx: number, field: 'quantity' | 'value_entered', val: string) => void
|
||||
disabled: boolean
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '80px 1fr 1fr 80px', gap: '0.25rem 0.5rem', marginBottom: '0.35rem' }}>
|
||||
{['Denom', 'Qty', 'Value Override', 'Total'].map(h => (
|
||||
<span key={h} style={{ fontSize: '0.75rem', color: 'var(--text-mid)', fontWeight: 600 }}>{h}</span>
|
||||
))}
|
||||
</div>
|
||||
{GBP_DENOMINATIONS.map((d, i) => {
|
||||
const row = denoms[i]
|
||||
return (
|
||||
<div key={d.value} style={{ display: 'grid', gridTemplateColumns: '80px 1fr 1fr 80px', gap: '0.25rem 0.5rem', marginBottom: '0.2rem', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '0.875rem', fontWeight: 600 }}>{d.label}</span>
|
||||
<input
|
||||
type="number" min="0" step="1"
|
||||
value={row.quantity ?? ''}
|
||||
onChange={e => onChange(i, 'quantity', e.target.value)}
|
||||
disabled={disabled}
|
||||
placeholder="0"
|
||||
style={inputSt}
|
||||
/>
|
||||
<input
|
||||
type="number" min="0" step="0.01"
|
||||
value={row.value_entered ?? ''}
|
||||
onChange={e => onChange(i, 'value_entered', e.target.value)}
|
||||
disabled={disabled || row.quantity !== null}
|
||||
placeholder="—"
|
||||
style={{ ...inputSt, opacity: row.quantity !== null ? 0.4 : 1 }}
|
||||
/>
|
||||
<span style={{ fontSize: '0.875rem', textAlign: 'right' }}>
|
||||
{row.total_amount > 0 ? fmtGBP(row.total_amount) : '—'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const inputSt: React.CSSProperties = {
|
||||
border: '1px solid var(--card-border)', borderRadius: '4px',
|
||||
padding: '0.3rem 0.5rem', fontSize: '0.875rem', width: '100%',
|
||||
background: 'white',
|
||||
}
|
||||
|
||||
function MoneyInput({ label, value, onChange, disabled }: {
|
||||
label: string; value: number; onChange: (v: string) => void; disabled: boolean
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<label style={{ fontSize: '0.8rem', color: 'var(--text-mid)', minWidth: '60px' }}>{label}</label>
|
||||
<input
|
||||
type="number" min="0" step="0.01"
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
placeholder="0.00"
|
||||
style={{ ...inputSt, width: '110px', textAlign: 'right' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue