Lazy paycheck generation, regenerate button, inline gross/net edit
- GET /api/paychecks returns virtual data (id: null) without writing to DB when no records exist for the month - First interaction (bill toggle, OTE add, actual add) lazily calls POST /api/paychecks/generate to persist the paycheck - New PATCH /api/paychecks/:id to update gross and net - Regenerate/refresh button syncs gross/net from current Settings - Inline pencil edit for gross/net on each paycheck column header - 'preview' badge and info banner shown for unsaved months Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,9 @@ function todayISO() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggle, onOteDelete, onOteAdd }) {
|
||||
// ─── PaycheckColumn ───────────────────────────────────────────────────────────
|
||||
|
||||
function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggle, onOteDelete, onOteAdd, onGenerate, onAmountSave }) {
|
||||
const [newOteName, setNewOteName] = useState('');
|
||||
const [newOteAmount, setNewOteAmount] = useState('');
|
||||
const [actuals, setActuals] = useState([]);
|
||||
@@ -39,8 +41,15 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
const [formSubmitting, setFormSubmitting] = useState(false);
|
||||
const [formError, setFormError] = useState(null);
|
||||
|
||||
// Inline gross/net editing
|
||||
const [editingAmounts, setEditingAmounts] = useState(false);
|
||||
const [editGross, setEditGross] = useState('');
|
||||
const [editNet, setEditNet] = useState('');
|
||||
const [amountSaving, setAmountSaving] = useState(false);
|
||||
const [amountError, setAmountError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!paycheck) return;
|
||||
if (!paycheck?.id) { setActuals([]); return; }
|
||||
loadActuals(paycheck.id);
|
||||
}, [paycheck?.id]);
|
||||
|
||||
@@ -50,8 +59,7 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
try {
|
||||
const res = await fetch(`/api/actuals?paycheck_id=${paycheckId}`);
|
||||
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
||||
const data = await res.json();
|
||||
setActuals(data);
|
||||
setActuals(await res.json());
|
||||
} catch (err) {
|
||||
setActualsError(err.message);
|
||||
} finally {
|
||||
@@ -65,11 +73,18 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
setFormSubmitting(true);
|
||||
setFormError(null);
|
||||
try {
|
||||
// Lazy generate if this is a virtual paycheck
|
||||
let paycheckId = paycheck.id;
|
||||
if (!paycheckId) {
|
||||
const generated = await onGenerate();
|
||||
paycheckId = generated.find(p => p.paycheck_number === paycheck.paycheck_number).id;
|
||||
}
|
||||
|
||||
const res = await fetch('/api/actuals', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
paycheck_id: paycheck.id,
|
||||
paycheck_id: paycheckId,
|
||||
category_id: formCategoryId || null,
|
||||
amount: parseFloat(formAmount),
|
||||
note: formNote || null,
|
||||
@@ -80,7 +95,7 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
const body = await res.json();
|
||||
throw new Error(body.error || `Server error: ${res.status}`);
|
||||
}
|
||||
await loadActuals(paycheck.id);
|
||||
await loadActuals(paycheckId);
|
||||
setFormCategoryId('');
|
||||
setFormAmount('');
|
||||
setFormNote('');
|
||||
@@ -105,6 +120,27 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
}
|
||||
}
|
||||
|
||||
function openAmountEdit() {
|
||||
setEditGross(parseFloat(paycheck.gross) || '');
|
||||
setEditNet(parseFloat(paycheck.net) || '');
|
||||
setAmountError(null);
|
||||
setEditingAmounts(true);
|
||||
}
|
||||
|
||||
async function saveAmounts(e) {
|
||||
e.preventDefault();
|
||||
setAmountSaving(true);
|
||||
setAmountError(null);
|
||||
try {
|
||||
await onAmountSave(paycheck.paycheck_number, parseFloat(editGross) || 0, parseFloat(editNet) || 0);
|
||||
setEditingAmounts(false);
|
||||
} catch (err) {
|
||||
setAmountError(err.message);
|
||||
} finally {
|
||||
setAmountSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!paycheck) {
|
||||
return (
|
||||
<div className="paycheck-card">
|
||||
@@ -115,6 +151,7 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
);
|
||||
}
|
||||
|
||||
const isVirtual = paycheck.id === null;
|
||||
const net = parseFloat(paycheck.net) || 0;
|
||||
const billsTotal = paycheck.bills.reduce((sum, b) => sum + (parseFloat(b.effective_amount) || 0), 0);
|
||||
const otesTotal = paycheck.one_time_expenses.reduce((sum, e) => sum + (parseFloat(e.amount) || 0), 0);
|
||||
@@ -124,12 +161,67 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
return (
|
||||
<div className="paycheck-card">
|
||||
<div className="paycheck-card__header">
|
||||
<div className="paycheck-card__number">Paycheck {paycheck.paycheck_number}</div>
|
||||
<div className="paycheck-card__date">{formatPayDate(paycheck.pay_date)}</div>
|
||||
<div className="paycheck-card__amounts">
|
||||
<span>Gross: <strong>{formatCurrency(paycheck.gross)}</strong></span>
|
||||
<span>Net: <strong>{formatCurrency(paycheck.net)}</strong></span>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<div className="paycheck-card__number">
|
||||
Paycheck {paycheck.paycheck_number}
|
||||
{isVirtual && (
|
||||
<span style={{
|
||||
marginLeft: '0.5rem',
|
||||
fontSize: '0.65rem',
|
||||
background: 'var(--accent-subtle)',
|
||||
color: 'var(--accent)',
|
||||
padding: '0.1rem 0.4rem',
|
||||
borderRadius: '999px',
|
||||
border: '1px solid var(--accent)',
|
||||
verticalAlign: 'middle',
|
||||
}}>preview</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="paycheck-card__date">{formatPayDate(paycheck.pay_date)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editingAmounts ? (
|
||||
<form onSubmit={saveAmounts} style={{ marginTop: '0.5rem' }}>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<div className="form-group" style={{ flex: '1 1 120px' }}>
|
||||
<label className="form-label">Gross</label>
|
||||
<input type="number" min="0" step="0.01" value={editGross}
|
||||
onChange={e => setEditGross(e.target.value)} className="form-input" />
|
||||
</div>
|
||||
<div className="form-group" style={{ flex: '1 1 120px' }}>
|
||||
<label className="form-label">Net</label>
|
||||
<input type="number" min="0" step="0.01" value={editNet}
|
||||
onChange={e => setEditNet(e.target.value)} className="form-input" />
|
||||
</div>
|
||||
</div>
|
||||
{amountError && <div className="form-error">{amountError}</div>}
|
||||
<div style={{ display: 'flex', gap: '0.4rem', marginTop: '0.5rem' }}>
|
||||
<button type="submit" className="btn btn-sm btn-primary" disabled={amountSaving}>
|
||||
{amountSaving ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm" onClick={() => setEditingAmounts(false)}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '0.35rem' }}>
|
||||
<div className="paycheck-card__amounts">
|
||||
<span>Gross: <strong>{formatCurrency(paycheck.gross)}</strong></span>
|
||||
<span>Net: <strong>{formatCurrency(paycheck.net)}</strong></span>
|
||||
</div>
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={openAmountEdit}
|
||||
title="Edit gross / net"
|
||||
style={{ fontSize: '0.85rem', color: 'var(--text-faint)', flexShrink: 0 }}
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="paycheck-card__body">
|
||||
@@ -140,11 +232,11 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
<p className="empty-state">(none)</p>
|
||||
) : (
|
||||
paycheck.bills.map((bill) => (
|
||||
<div key={bill.paycheck_bill_id} className="bill-row" style={{ opacity: bill.paid ? 0.6 : 1 }}>
|
||||
<div key={bill.bill_id} className="bill-row" style={{ opacity: bill.paid ? 0.6 : 1 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!bill.paid}
|
||||
onChange={() => onBillPaidToggle(bill.paycheck_bill_id, !bill.paid)}
|
||||
onChange={() => onBillPaidToggle(bill.paycheck_bill_id, !bill.paid, bill.bill_id, paycheck.paycheck_number)}
|
||||
className="bill-row__check"
|
||||
/>
|
||||
<div className="bill-row__info">
|
||||
@@ -183,32 +275,17 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
))
|
||||
)}
|
||||
<div className="inline-add-form">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={newOteName}
|
||||
onChange={(e) => setNewOteName(e.target.value)}
|
||||
className="form-input"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={newOteAmount}
|
||||
onChange={(e) => setNewOteAmount(e.target.value)}
|
||||
min="0"
|
||||
step="0.01"
|
||||
className="form-input"
|
||||
style={{ maxWidth: '100px' }}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => {
|
||||
if (!newOteName.trim() || !newOteAmount) return;
|
||||
onOteAdd(paycheck.id, newOteName.trim(), parseFloat(newOteAmount));
|
||||
setNewOteName('');
|
||||
setNewOteAmount('');
|
||||
}}
|
||||
>
|
||||
<input type="text" placeholder="Name" value={newOteName}
|
||||
onChange={(e) => setNewOteName(e.target.value)} className="form-input" />
|
||||
<input type="number" placeholder="Amount" value={newOteAmount}
|
||||
onChange={(e) => setNewOteAmount(e.target.value)} min="0" step="0.01"
|
||||
className="form-input" style={{ maxWidth: '100px' }} />
|
||||
<button className="btn btn-sm btn-primary" onClick={() => {
|
||||
if (!newOteName.trim() || !newOteAmount) return;
|
||||
onOteAdd(paycheck.paycheck_number, newOteName.trim(), parseFloat(newOteAmount));
|
||||
setNewOteName('');
|
||||
setNewOteAmount('');
|
||||
}}>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
@@ -217,7 +294,6 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
{/* Variable spending */}
|
||||
<div className="mb-2">
|
||||
<div className="section-title">Variable Spending</div>
|
||||
|
||||
{actualsLoading && <p className="empty-state">Loading…</p>}
|
||||
{actualsError && <div className="alert alert-error">Error: {actualsError}</div>}
|
||||
{!actualsLoading && actuals.length === 0 && <p className="empty-state">(none)</p>}
|
||||
@@ -242,43 +318,21 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
|
||||
<form onSubmit={handleAddActual} className="form-rows">
|
||||
<div className="form-row">
|
||||
<select
|
||||
value={formCategoryId}
|
||||
onChange={e => setFormCategoryId(e.target.value)}
|
||||
className="form-select"
|
||||
>
|
||||
<select value={formCategoryId} onChange={e => setFormCategoryId(e.target.value)} className="form-select">
|
||||
<option value="">— Category —</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat.id} value={cat.id}>{cat.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={formAmount}
|
||||
onChange={e => setFormAmount(e.target.value)}
|
||||
step="0.01"
|
||||
min="0"
|
||||
className="form-input"
|
||||
style={{ maxWidth: '110px' }}
|
||||
required
|
||||
/>
|
||||
<input type="number" placeholder="Amount" value={formAmount}
|
||||
onChange={e => setFormAmount(e.target.value)} step="0.01" min="0"
|
||||
className="form-input" style={{ maxWidth: '110px' }} required />
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Note (optional)"
|
||||
value={formNote}
|
||||
onChange={e => setFormNote(e.target.value)}
|
||||
className="form-input"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={formDate}
|
||||
onChange={e => setFormDate(e.target.value)}
|
||||
className="form-input"
|
||||
style={{ maxWidth: '140px' }}
|
||||
/>
|
||||
<input type="text" placeholder="Note (optional)" value={formNote}
|
||||
onChange={e => setFormNote(e.target.value)} className="form-input" />
|
||||
<input type="date" value={formDate} onChange={e => setFormDate(e.target.value)}
|
||||
className="form-input" style={{ maxWidth: '140px' }} />
|
||||
<button type="submit" disabled={formSubmitting} className="btn btn-sm btn-primary" style={{ flexShrink: 0 }}>
|
||||
Add
|
||||
</button>
|
||||
@@ -299,6 +353,8 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
);
|
||||
}
|
||||
|
||||
// ─── PaycheckView ─────────────────────────────────────────────────────────────
|
||||
|
||||
function PaycheckView() {
|
||||
const now = new Date();
|
||||
const [year, setYear] = useState(now.getFullYear());
|
||||
@@ -307,6 +363,7 @@ function PaycheckView() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [regenerating, setRegenerating] = useState(false);
|
||||
|
||||
useEffect(() => { loadPaychecks(year, month); }, [year, month]);
|
||||
useEffect(() => { loadCategories(); }, []);
|
||||
@@ -333,6 +390,27 @@ function PaycheckView() {
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
// Generates (or regenerates) paychecks for the current month, updates state,
|
||||
// and returns the new paychecks array.
|
||||
async function generateMonth() {
|
||||
const res = await fetch(`/api/paychecks/generate?year=${year}&month=${month}`, { method: 'POST' });
|
||||
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
||||
const data = await res.json();
|
||||
setPaychecks(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function handleRegenerate() {
|
||||
setRegenerating(true);
|
||||
try {
|
||||
await generateMonth();
|
||||
} catch (err) {
|
||||
alert(`Failed to regenerate: ${err.message}`);
|
||||
} finally {
|
||||
setRegenerating(false);
|
||||
}
|
||||
}
|
||||
|
||||
function prevMonth() {
|
||||
if (month === 1) { setYear(y => y - 1); setMonth(12); } else { setMonth(m => m - 1); }
|
||||
}
|
||||
@@ -341,6 +419,81 @@ function PaycheckView() {
|
||||
if (month === 12) { setYear(y => y + 1); setMonth(1); } else { setMonth(m => m + 1); }
|
||||
}
|
||||
|
||||
// Saves gross/net for a paycheck. If virtual, generates first then patches.
|
||||
async function handleAmountSave(paycheckNumber, gross, net) {
|
||||
let pc = paychecks.find(p => p.paycheck_number === paycheckNumber);
|
||||
|
||||
if (!pc.id) {
|
||||
const generated = await generateMonth();
|
||||
pc = generated.find(p => p.paycheck_number === paycheckNumber);
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/paychecks/${pc.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ gross, net }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json();
|
||||
throw new Error(body.error || `Server error: ${res.status}`);
|
||||
}
|
||||
const updated = await res.json();
|
||||
setPaychecks(prev => prev.map(p =>
|
||||
p.paycheck_number === paycheckNumber
|
||||
? { ...p, id: updated.id, gross: updated.gross, net: updated.net }
|
||||
: p
|
||||
));
|
||||
}
|
||||
|
||||
async function handleBillPaidToggle(paycheckBillId, paid, billId, paycheckNumber) {
|
||||
let realPaycheckBillId = paycheckBillId;
|
||||
|
||||
if (!realPaycheckBillId) {
|
||||
// Virtual paycheck — generate first, then find the real paycheck_bill_id
|
||||
const generated = await generateMonth();
|
||||
const pc = generated.find(p => p.paycheck_number === paycheckNumber);
|
||||
const bill = pc.bills.find(b => b.bill_id === billId);
|
||||
realPaycheckBillId = bill.paycheck_bill_id;
|
||||
}
|
||||
|
||||
// Optimistic update
|
||||
setPaychecks(prev => prev.map(pc => ({
|
||||
...pc,
|
||||
bills: pc.bills.map(b =>
|
||||
b.paycheck_bill_id === realPaycheckBillId
|
||||
? { ...b, paid, paid_at: paid ? new Date().toISOString() : null }
|
||||
: b
|
||||
),
|
||||
})));
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/paycheck-bills/${realPaycheckBillId}/paid`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paid }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
||||
const updated = await res.json();
|
||||
setPaychecks(prev => prev.map(pc => ({
|
||||
...pc,
|
||||
bills: pc.bills.map(b =>
|
||||
b.paycheck_bill_id === realPaycheckBillId
|
||||
? { ...b, paid: updated.paid, paid_at: updated.paid_at }
|
||||
: b
|
||||
),
|
||||
})));
|
||||
} catch (err) {
|
||||
// Revert on failure
|
||||
setPaychecks(prev => prev.map(pc => ({
|
||||
...pc,
|
||||
bills: pc.bills.map(b =>
|
||||
b.paycheck_bill_id === realPaycheckBillId ? { ...b, paid: !paid } : b
|
||||
),
|
||||
})));
|
||||
alert(`Failed to update bill: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOtePaidToggle(oteId, paid) {
|
||||
try {
|
||||
const res = await fetch(`/api/one-time-expenses/${oteId}/paid`, {
|
||||
@@ -366,7 +519,15 @@ function PaycheckView() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOteAdd(paycheckId, name, amount) {
|
||||
async function handleOteAdd(paycheckNumber, name, amount) {
|
||||
let pc = paychecks.find(p => p.paycheck_number === paycheckNumber);
|
||||
let paycheckId = pc?.id;
|
||||
|
||||
if (!paycheckId) {
|
||||
const generated = await generateMonth();
|
||||
paycheckId = generated.find(p => p.paycheck_number === paycheckNumber).id;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/one-time-expenses', {
|
||||
method: 'POST',
|
||||
@@ -380,51 +541,9 @@ function PaycheckView() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBillPaidToggle(paycheckBillId, paid) {
|
||||
setPaychecks(prev =>
|
||||
prev.map(pc => ({
|
||||
...pc,
|
||||
bills: pc.bills.map(b =>
|
||||
b.paycheck_bill_id === paycheckBillId
|
||||
? { ...b, paid, paid_at: paid ? new Date().toISOString() : null }
|
||||
: b
|
||||
),
|
||||
}))
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/paycheck-bills/${paycheckBillId}/paid`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paid }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
||||
const updated = await res.json();
|
||||
setPaychecks(prev =>
|
||||
prev.map(pc => ({
|
||||
...pc,
|
||||
bills: pc.bills.map(b =>
|
||||
b.paycheck_bill_id === paycheckBillId
|
||||
? { ...b, paid: updated.paid, paid_at: updated.paid_at }
|
||||
: b
|
||||
),
|
||||
}))
|
||||
);
|
||||
} catch (err) {
|
||||
setPaychecks(prev =>
|
||||
prev.map(pc => ({
|
||||
...pc,
|
||||
bills: pc.bills.map(b =>
|
||||
b.paycheck_bill_id === paycheckBillId ? { ...b, paid: !paid } : b
|
||||
),
|
||||
}))
|
||||
);
|
||||
alert(`Failed to update bill: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const pc1 = paychecks.find(p => p.paycheck_number === 1) || null;
|
||||
const pc2 = paychecks.find(p => p.paycheck_number === 2) || null;
|
||||
const isVirtual = paychecks.length > 0 && paychecks.every(p => p.id === null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -432,8 +551,29 @@ function PaycheckView() {
|
||||
<button className="btn-nav" onClick={prevMonth}>←</button>
|
||||
<span className="period-nav__label">{MONTH_NAMES[month - 1]} {year}</span>
|
||||
<button className="btn-nav" onClick={nextMonth}>→</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={handleRegenerate}
|
||||
disabled={regenerating}
|
||||
title="Refresh gross/net from current Settings for this month"
|
||||
style={{ marginLeft: '0.5rem' }}
|
||||
>
|
||||
{regenerating ? 'Refreshing…' : '↺ Refresh amounts'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isVirtual && (
|
||||
<div className="alert" style={{
|
||||
background: 'var(--accent-subtle)',
|
||||
border: '1px solid var(--accent)',
|
||||
color: 'var(--accent)',
|
||||
marginBottom: '1rem',
|
||||
fontSize: '0.875rem',
|
||||
}}>
|
||||
Previewing from current settings — no data saved yet for this month.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div className="alert alert-error">Error: {error}</div>}
|
||||
|
||||
{loading ? (
|
||||
@@ -447,6 +587,8 @@ function PaycheckView() {
|
||||
onOteDelete={handleOteDelete}
|
||||
onOteAdd={handleOteAdd}
|
||||
categories={categories}
|
||||
onGenerate={generateMonth}
|
||||
onAmountSave={handleAmountSave}
|
||||
/>
|
||||
<PaycheckColumn
|
||||
paycheck={pc2}
|
||||
@@ -455,6 +597,8 @@ function PaycheckView() {
|
||||
onOteDelete={handleOteDelete}
|
||||
onOteAdd={handleOteAdd}
|
||||
categories={categories}
|
||||
onGenerate={generateMonth}
|
||||
onAmountSave={handleAmountSave}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user