Add modern UI with light/dark mode
- CSS custom properties design system with full light/dark themes - ThemeContext with localStorage persistence and system preference detection - Theme toggle button in nav (moon/sun icon) - Modern Inter font, card-based layout, sticky nav - All pages restyled with CSS classes instead of inline styles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,6 @@ function formatCurrency(value) {
|
||||
}
|
||||
|
||||
function formatPayDate(dateStr) {
|
||||
// dateStr is YYYY-MM-DD
|
||||
const [year, month, day] = dateStr.split('-').map(Number);
|
||||
return `${MONTH_NAMES[month - 1]} ${day}, ${year}`;
|
||||
}
|
||||
@@ -62,11 +61,7 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
|
||||
async function handleAddActual(e) {
|
||||
e.preventDefault();
|
||||
if (!formAmount) {
|
||||
setFormError('Amount is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formAmount) { setFormError('Amount is required'); return; }
|
||||
setFormSubmitting(true);
|
||||
setFormError(null);
|
||||
try {
|
||||
@@ -85,9 +80,7 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
const body = await res.json();
|
||||
throw new Error(body.error || `Server error: ${res.status}`);
|
||||
}
|
||||
// Refresh the actuals list
|
||||
await loadActuals(paycheck.id);
|
||||
// Reset form fields (keep date as today)
|
||||
setFormCategoryId('');
|
||||
setFormAmount('');
|
||||
setFormNote('');
|
||||
@@ -114,8 +107,10 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
|
||||
if (!paycheck) {
|
||||
return (
|
||||
<div style={styles.column}>
|
||||
<p style={{ color: '#888' }}>No data</p>
|
||||
<div className="paycheck-card">
|
||||
<div className="paycheck-card__body">
|
||||
<p className="empty-state">No data</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -125,196 +120,180 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
const otesTotal = paycheck.one_time_expenses.reduce((sum, e) => sum + (parseFloat(e.amount) || 0), 0);
|
||||
const actualsTotal = actuals.reduce((sum, a) => sum + (parseFloat(a.amount) || 0), 0);
|
||||
const remaining = net - billsTotal - otesTotal - actualsTotal;
|
||||
const remainingColor = remaining >= 0 ? '#2a7a2a' : '#c0392b';
|
||||
|
||||
return (
|
||||
<div style={styles.column}>
|
||||
<div style={styles.columnHeader}>
|
||||
<h2 style={styles.paycheckTitle}>Paycheck {paycheck.paycheck_number}</h2>
|
||||
<div style={styles.payDate}>{formatPayDate(paycheck.pay_date)}</div>
|
||||
<div style={styles.payAmounts}>
|
||||
<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 style={{ marginLeft: '1rem' }}>Net: <strong>{formatCurrency(paycheck.net)}</strong></span>
|
||||
<span>Net: <strong>{formatCurrency(paycheck.net)}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionLabel}>Bills</div>
|
||||
<div style={styles.divider} />
|
||||
{paycheck.bills.length === 0 ? (
|
||||
<div style={styles.emptyNote}>(none)</div>
|
||||
) : (
|
||||
paycheck.bills.map((bill) => (
|
||||
<div
|
||||
key={bill.paycheck_bill_id}
|
||||
style={{
|
||||
...styles.billRow,
|
||||
opacity: bill.paid ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!bill.paid}
|
||||
onChange={() => onBillPaidToggle(bill.paycheck_bill_id, !bill.paid)}
|
||||
style={styles.checkbox}
|
||||
/>
|
||||
<div style={styles.billDetails}>
|
||||
<div style={bill.paid ? styles.billNamePaid : styles.billName}>
|
||||
{bill.name}
|
||||
<span style={styles.billAmount}>{formatCurrency(bill.effective_amount)}</span>
|
||||
</div>
|
||||
<div style={styles.billMeta}>
|
||||
<span>due {ordinal(bill.due_day)}</span>
|
||||
{bill.category && (
|
||||
<span style={styles.category}>{bill.category}</span>
|
||||
)}
|
||||
<div className="paycheck-card__body">
|
||||
{/* Bills */}
|
||||
<div className="mb-2">
|
||||
<div className="section-title">Bills</div>
|
||||
{paycheck.bills.length === 0 ? (
|
||||
<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 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!bill.paid}
|
||||
onChange={() => onBillPaidToggle(bill.paycheck_bill_id, !bill.paid)}
|
||||
className="bill-row__check"
|
||||
/>
|
||||
<div className="bill-row__info">
|
||||
<div className={`bill-row__name${bill.paid ? ' paid' : ''}`}>
|
||||
<span>{bill.name}</span>
|
||||
<span className="bill-row__amount">{formatCurrency(bill.effective_amount)}</span>
|
||||
</div>
|
||||
<div className="bill-row__meta">
|
||||
<span>due {ordinal(bill.due_day)}</span>
|
||||
{bill.category && <span className="badge badge-category">{bill.category}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionLabel}>One-time expenses</div>
|
||||
<div style={styles.divider} />
|
||||
{paycheck.one_time_expenses.length === 0 ? (
|
||||
<div style={styles.emptyNote}>(none)</div>
|
||||
) : (
|
||||
paycheck.one_time_expenses.map((ote) => (
|
||||
<div key={ote.id} style={{ ...styles.oteRow, opacity: ote.paid ? 0.6 : 1 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!ote.paid}
|
||||
onChange={() => onOtePaidToggle(ote.id, !ote.paid)}
|
||||
style={styles.checkbox}
|
||||
/>
|
||||
<span style={ote.paid ? { ...styles.oteName, textDecoration: 'line-through', color: '#999' } : styles.oteName}>
|
||||
{ote.name}
|
||||
</span>
|
||||
<span style={styles.oteAmount}>{formatCurrency(ote.amount)}</span>
|
||||
<button onClick={() => onOteDelete(ote.id)} style={styles.deleteButton} title="Remove expense">×</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div style={styles.oteAddForm}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={newOteName}
|
||||
onChange={(e) => setNewOteName(e.target.value)}
|
||||
style={styles.oteAddInput}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
value={newOteAmount}
|
||||
onChange={(e) => setNewOteAmount(e.target.value)}
|
||||
min="0"
|
||||
step="0.01"
|
||||
style={{ ...styles.oteAddInput, width: '80px' }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!newOteName.trim() || !newOteAmount) return;
|
||||
onOteAdd(paycheck.id, newOteName.trim(), parseFloat(newOteAmount));
|
||||
setNewOteName('');
|
||||
setNewOteAmount('');
|
||||
}}
|
||||
style={styles.oteAddButton}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionLabel}>Variable Spending</div>
|
||||
<div style={styles.divider} />
|
||||
|
||||
{actualsLoading && <div style={styles.emptyNote}>Loading…</div>}
|
||||
{actualsError && <div style={styles.actualsError}>Error: {actualsError}</div>}
|
||||
|
||||
{!actualsLoading && actuals.length === 0 && (
|
||||
<div style={styles.emptyNote}>(none)</div>
|
||||
)}
|
||||
|
||||
{actuals.map((actual) => (
|
||||
<div key={actual.id} style={styles.actualRow}>
|
||||
<div style={styles.actualMain}>
|
||||
<span style={styles.actualCategory}>
|
||||
{actual.category_name || <em style={{ color: '#aaa' }}>Uncategorized</em>}
|
||||
</span>
|
||||
<span style={styles.actualAmount}>{formatCurrency(actual.amount)}</span>
|
||||
</div>
|
||||
<div style={styles.actualMeta}>
|
||||
{actual.note && <span style={styles.actualNote}>{actual.note}</span>}
|
||||
<span style={styles.actualDate}>{actual.date}</span>
|
||||
<button
|
||||
style={styles.deleteButton}
|
||||
onClick={() => handleDeleteActual(actual.id)}
|
||||
title="Remove"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<form onSubmit={handleAddActual} style={styles.actualForm}>
|
||||
<div style={styles.actualFormRow}>
|
||||
<select
|
||||
value={formCategoryId}
|
||||
onChange={e => setFormCategoryId(e.target.value)}
|
||||
style={styles.formSelect}
|
||||
>
|
||||
<option value="">— Category —</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat.id} value={cat.id}>{cat.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{/* One-time expenses */}
|
||||
<div className="mb-2">
|
||||
<div className="section-title">One-time Expenses</div>
|
||||
{paycheck.one_time_expenses.length === 0 ? (
|
||||
<p className="empty-state">(none)</p>
|
||||
) : (
|
||||
paycheck.one_time_expenses.map((ote) => (
|
||||
<div key={ote.id} className="ote-row" style={{ opacity: ote.paid ? 0.6 : 1 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!ote.paid}
|
||||
onChange={() => onOtePaidToggle(ote.id, !ote.paid)}
|
||||
className="ote-row__check"
|
||||
/>
|
||||
<span className={`ote-row__name${ote.paid ? ' paid' : ''}`}>{ote.name}</span>
|
||||
<span className="ote-row__amount">{formatCurrency(ote.amount)}</span>
|
||||
<button className="btn-icon" onClick={() => onOteDelete(ote.id)} title="Remove">×</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<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={formAmount}
|
||||
onChange={e => setFormAmount(e.target.value)}
|
||||
step="0.01"
|
||||
value={newOteAmount}
|
||||
onChange={(e) => setNewOteAmount(e.target.value)}
|
||||
min="0"
|
||||
style={styles.formInput}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div style={styles.actualFormRow}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Note (optional)"
|
||||
value={formNote}
|
||||
onChange={e => setFormNote(e.target.value)}
|
||||
style={{ ...styles.formInput, flex: 2 }}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
value={formDate}
|
||||
onChange={e => setFormDate(e.target.value)}
|
||||
style={styles.formInput}
|
||||
step="0.01"
|
||||
className="form-input"
|
||||
style={{ maxWidth: '100px' }}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={formSubmitting}
|
||||
style={styles.addButton}
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => {
|
||||
if (!newOteName.trim() || !newOteAmount) return;
|
||||
onOteAdd(paycheck.id, newOteName.trim(), parseFloat(newOteAmount));
|
||||
setNewOteName('');
|
||||
setNewOteAmount('');
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{formError && <div style={styles.formError}>{formError}</div>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.remainingRow}>
|
||||
<span style={styles.remainingLabel}>Remaining:</span>
|
||||
<span style={{ ...styles.remainingAmount, color: remainingColor }}>
|
||||
{formatCurrency(remaining)}
|
||||
</span>
|
||||
{/* 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>}
|
||||
|
||||
{actuals.map((actual) => (
|
||||
<div key={actual.id} className="actual-row">
|
||||
<div className="actual-row__main">
|
||||
<span className="actual-row__category">
|
||||
{actual.category_name || <em className="text-faint">Uncategorized</em>}
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||
<span className="actual-row__amount">{formatCurrency(actual.amount)}</span>
|
||||
<button className="btn-icon" onClick={() => handleDeleteActual(actual.id)} title="Remove">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="actual-row__meta">
|
||||
{actual.note && <span className="actual-row__note">{actual.note}</span>}
|
||||
<span>{actual.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<form onSubmit={handleAddActual} className="form-rows">
|
||||
<div className="form-row">
|
||||
<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
|
||||
/>
|
||||
</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' }}
|
||||
/>
|
||||
<button type="submit" disabled={formSubmitting} className="btn btn-sm btn-primary" style={{ flexShrink: 0 }}>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{formError && <div className="form-error">{formError}</div>}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Remaining */}
|
||||
<div className="remaining-row">
|
||||
<span className="remaining-row__label">Remaining</span>
|
||||
<span className={`remaining-row__amount ${remaining >= 0 ? 'positive' : 'negative'}`}>
|
||||
{formatCurrency(remaining)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -323,19 +302,14 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
||||
function PaycheckView() {
|
||||
const now = new Date();
|
||||
const [year, setYear] = useState(now.getFullYear());
|
||||
const [month, setMonth] = useState(now.getMonth() + 1); // 1-based
|
||||
const [month, setMonth] = useState(now.getMonth() + 1);
|
||||
const [paychecks, setPaychecks] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [categories, setCategories] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadPaychecks(year, month);
|
||||
}, [year, month]);
|
||||
|
||||
useEffect(() => {
|
||||
loadCategories();
|
||||
}, []);
|
||||
useEffect(() => { loadPaychecks(year, month); }, [year, month]);
|
||||
useEffect(() => { loadCategories(); }, []);
|
||||
|
||||
async function loadPaychecks(y, m) {
|
||||
setLoading(true);
|
||||
@@ -343,8 +317,7 @@ function PaycheckView() {
|
||||
try {
|
||||
const res = await fetch(`/api/paychecks?year=${y}&month=${m}`);
|
||||
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
||||
const data = await res.json();
|
||||
setPaychecks(data);
|
||||
setPaychecks(await res.json());
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
@@ -355,30 +328,17 @@ function PaycheckView() {
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const res = await fetch('/api/expense-categories');
|
||||
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
||||
const data = await res.json();
|
||||
setCategories(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to load expense categories:', err.message);
|
||||
}
|
||||
if (!res.ok) return;
|
||||
setCategories(await res.json());
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
function prevMonth() {
|
||||
if (month === 1) {
|
||||
setYear(y => y - 1);
|
||||
setMonth(12);
|
||||
} else {
|
||||
setMonth(m => m - 1);
|
||||
}
|
||||
if (month === 1) { setYear(y => y - 1); setMonth(12); } else { setMonth(m => m - 1); }
|
||||
}
|
||||
|
||||
function nextMonth() {
|
||||
if (month === 12) {
|
||||
setYear(y => y + 1);
|
||||
setMonth(1);
|
||||
} else {
|
||||
setMonth(m => m + 1);
|
||||
}
|
||||
if (month === 12) { setYear(y => y + 1); setMonth(1); } else { setMonth(m => m + 1); }
|
||||
}
|
||||
|
||||
async function handleOtePaidToggle(oteId, paid) {
|
||||
@@ -421,7 +381,6 @@ function PaycheckView() {
|
||||
}
|
||||
|
||||
async function handleBillPaidToggle(paycheckBillId, paid) {
|
||||
// Optimistic update
|
||||
setPaychecks(prev =>
|
||||
prev.map(pc => ({
|
||||
...pc,
|
||||
@@ -441,7 +400,6 @@ function PaycheckView() {
|
||||
});
|
||||
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
||||
const updated = await res.json();
|
||||
// Sync server response
|
||||
setPaychecks(prev =>
|
||||
prev.map(pc => ({
|
||||
...pc,
|
||||
@@ -453,14 +411,11 @@ function PaycheckView() {
|
||||
}))
|
||||
);
|
||||
} catch (err) {
|
||||
// Revert optimistic update on failure
|
||||
setPaychecks(prev =>
|
||||
prev.map(pc => ({
|
||||
...pc,
|
||||
bills: pc.bills.map(b =>
|
||||
b.paycheck_bill_id === paycheckBillId
|
||||
? { ...b, paid: !paid }
|
||||
: b
|
||||
b.paycheck_bill_id === paycheckBillId ? { ...b, paid: !paid } : b
|
||||
),
|
||||
}))
|
||||
);
|
||||
@@ -472,21 +427,19 @@ function PaycheckView() {
|
||||
const pc2 = paychecks.find(p => p.paycheck_number === 2) || null;
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.monthNav}>
|
||||
<button style={styles.navButton} onClick={prevMonth}>←</button>
|
||||
<span style={styles.monthLabel}>{MONTH_NAMES[month - 1]} {year}</span>
|
||||
<button style={styles.navButton} onClick={nextMonth}>→</button>
|
||||
<div>
|
||||
<div className="period-nav">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={styles.errorBanner}>Error: {error}</div>
|
||||
)}
|
||||
{error && <div className="alert alert-error">Error: {error}</div>}
|
||||
|
||||
{loading ? (
|
||||
<div style={styles.loadingMsg}>Loading...</div>
|
||||
<p className="text-muted">Loading…</p>
|
||||
) : (
|
||||
<div style={styles.grid}>
|
||||
<div className="paycheck-grid">
|
||||
<PaycheckColumn
|
||||
paycheck={pc1}
|
||||
onBillPaidToggle={handleBillPaidToggle}
|
||||
@@ -509,282 +462,4 @@ function PaycheckView() {
|
||||
);
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
maxWidth: '960px',
|
||||
margin: '0 auto',
|
||||
},
|
||||
monthNav: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
marginBottom: '1.25rem',
|
||||
},
|
||||
navButton: {
|
||||
padding: '0.3rem 0.75rem',
|
||||
fontSize: '1rem',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid #bbb',
|
||||
borderRadius: '4px',
|
||||
background: '#f5f5f5',
|
||||
},
|
||||
monthLabel: {
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: '600',
|
||||
minWidth: '160px',
|
||||
textAlign: 'center',
|
||||
},
|
||||
errorBanner: {
|
||||
background: '#fde8e8',
|
||||
border: '1px solid #f5a0a0',
|
||||
borderRadius: '4px',
|
||||
padding: '0.75rem 1rem',
|
||||
marginBottom: '1rem',
|
||||
color: '#c0392b',
|
||||
},
|
||||
loadingMsg: {
|
||||
padding: '2rem',
|
||||
color: '#888',
|
||||
},
|
||||
grid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '1.5rem',
|
||||
alignItems: 'start',
|
||||
},
|
||||
column: {
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '6px',
|
||||
padding: '1rem',
|
||||
background: '#fafafa',
|
||||
},
|
||||
columnHeader: {
|
||||
marginBottom: '1rem',
|
||||
paddingBottom: '0.75rem',
|
||||
borderBottom: '2px solid #eee',
|
||||
},
|
||||
paycheckTitle: {
|
||||
margin: '0 0 0.25rem 0',
|
||||
fontSize: '1.1rem',
|
||||
fontWeight: '700',
|
||||
},
|
||||
payDate: {
|
||||
color: '#555',
|
||||
marginBottom: '0.4rem',
|
||||
fontSize: '0.95rem',
|
||||
},
|
||||
payAmounts: {
|
||||
fontSize: '0.95rem',
|
||||
color: '#333',
|
||||
},
|
||||
section: {
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
sectionLabel: {
|
||||
fontWeight: '600',
|
||||
fontSize: '0.9rem',
|
||||
color: '#444',
|
||||
marginBottom: '0.25rem',
|
||||
},
|
||||
divider: {
|
||||
borderTop: '1px solid #ddd',
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
emptyNote: {
|
||||
color: '#aaa',
|
||||
fontSize: '0.875rem',
|
||||
fontStyle: 'italic',
|
||||
paddingLeft: '0.25rem',
|
||||
},
|
||||
billRow: {
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '0.5rem',
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
checkbox: {
|
||||
marginTop: '3px',
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
},
|
||||
billDetails: {
|
||||
flex: 1,
|
||||
},
|
||||
billName: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontWeight: '500',
|
||||
fontSize: '0.95rem',
|
||||
},
|
||||
billNamePaid: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontWeight: '500',
|
||||
fontSize: '0.95rem',
|
||||
textDecoration: 'line-through',
|
||||
color: '#999',
|
||||
},
|
||||
billAmount: {
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
marginLeft: '0.5rem',
|
||||
},
|
||||
billMeta: {
|
||||
fontSize: '0.8rem',
|
||||
color: '#888',
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
marginTop: '1px',
|
||||
},
|
||||
category: {
|
||||
background: '#e8eaf0',
|
||||
borderRadius: '3px',
|
||||
padding: '0 4px',
|
||||
fontSize: '0.75rem',
|
||||
color: '#666',
|
||||
},
|
||||
oteRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: '0.95rem',
|
||||
padding: '0.2rem 0',
|
||||
},
|
||||
oteName: {
|
||||
color: '#333',
|
||||
},
|
||||
oteAmount: {
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
color: '#333',
|
||||
},
|
||||
// Actuals styles
|
||||
actualsError: {
|
||||
color: '#c0392b',
|
||||
fontSize: '0.85rem',
|
||||
marginBottom: '0.4rem',
|
||||
},
|
||||
actualRow: {
|
||||
marginBottom: '0.5rem',
|
||||
fontSize: '0.9rem',
|
||||
},
|
||||
actualMain: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
actualCategory: {
|
||||
fontWeight: '500',
|
||||
color: '#333',
|
||||
},
|
||||
actualAmount: {
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
color: '#333',
|
||||
marginLeft: '0.5rem',
|
||||
},
|
||||
actualMeta: {
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
alignItems: 'center',
|
||||
fontSize: '0.8rem',
|
||||
color: '#888',
|
||||
marginTop: '1px',
|
||||
},
|
||||
actualNote: {
|
||||
fontStyle: 'italic',
|
||||
flex: 1,
|
||||
},
|
||||
actualDate: {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
deleteButton: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: '#c0392b',
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1',
|
||||
padding: '0 2px',
|
||||
opacity: 0.7,
|
||||
},
|
||||
actualForm: {
|
||||
marginTop: '0.75rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.4rem',
|
||||
},
|
||||
actualFormRow: {
|
||||
display: 'flex',
|
||||
gap: '0.4rem',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
formSelect: {
|
||||
flex: 1,
|
||||
minWidth: '100px',
|
||||
fontSize: '0.85rem',
|
||||
padding: '0.3rem 0.4rem',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
formInput: {
|
||||
flex: 1,
|
||||
minWidth: '70px',
|
||||
fontSize: '0.85rem',
|
||||
padding: '0.3rem 0.4rem',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
addButton: {
|
||||
padding: '0.3rem 0.75rem',
|
||||
fontSize: '0.85rem',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid #bbb',
|
||||
borderRadius: '4px',
|
||||
background: '#e8f0e8',
|
||||
color: '#2a7a2a',
|
||||
fontWeight: '600',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
formError: {
|
||||
color: '#c0392b',
|
||||
fontSize: '0.8rem',
|
||||
},
|
||||
oteAddForm: {
|
||||
display: 'flex',
|
||||
gap: '0.4rem',
|
||||
marginTop: '0.5rem',
|
||||
alignItems: 'center',
|
||||
},
|
||||
oteAddInput: {
|
||||
padding: '0.2rem 0.4rem',
|
||||
fontSize: '0.875rem',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '3px',
|
||||
flex: 1,
|
||||
},
|
||||
oteAddButton: {
|
||||
padding: '0.2rem 0.6rem',
|
||||
fontSize: '0.875rem',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid #bbb',
|
||||
borderRadius: '3px',
|
||||
background: '#f0f0f0',
|
||||
flexShrink: 0,
|
||||
},
|
||||
remainingRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: '0.5rem',
|
||||
paddingTop: '0.75rem',
|
||||
borderTop: '2px solid #ddd',
|
||||
},
|
||||
remainingLabel: {
|
||||
fontWeight: '600',
|
||||
fontSize: '1rem',
|
||||
},
|
||||
remainingAmount: {
|
||||
fontWeight: '700',
|
||||
fontSize: '1.1rem',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
},
|
||||
};
|
||||
|
||||
export default PaycheckView;
|
||||
|
||||
Reference in New Issue
Block a user