feat: compact tracker, S badge, side-by-side buckets (batch 0.33.8.5)
- Sub badge changed to 'S' in all 4 locations with matching border style - Two-bucket grid at 2xl+ when both buckets have bills - Compact mode: narrow table, hide Last Month column, shrink Notes/Actions/Due - Bucket header shows remaining/Done labels alongside paid/total/overpaid - Removed standalone Remaining summary card (redundant with bucket header) - Row and Bucket accept compact=false - Bump v0.33.8.4 -> v0.33.8.5
This commit is contained in:
parent
a15436b637
commit
f99cd82438
16
HISTORY.md
16
HISTORY.md
|
|
@ -1,5 +1,21 @@
|
|||
# Bill Tracker — Changelog
|
||||
|
||||
## v0.33.8.5
|
||||
|
||||
### 🎨 Design
|
||||
|
||||
- **"S" badge (compact)** — Subscription badge shortened to "S" in all four locations (desktop tracker, mobile tracker, desktop bills table, mobile bills row), now with matching border style.
|
||||
- **Tracker bucket side-by-side** — When both buckets (1st–14th, 15th–31st) have bills, they render in a 2-column grid at 2xl+ instead of stacked.
|
||||
- **Compact bucket mode** — `compact` prop narrows table min-width to 700px at 2xl+, hides Last Month column, shrinks Notes (23%→16%) and Actions (10%→8%) columns, Due trimmed (10%→9%).
|
||||
- **Bucket header: remaining summary** — Shows "Remaining" and "Done" labels alongside paid/total/overpaid in bucket headers.
|
||||
|
||||
### 🛠 Internal
|
||||
|
||||
- Removed standalone `Remaining` summary card from the summary row (redundant with bucket header).
|
||||
- `Row` and `Bucket` components accept `compact = false` prop.
|
||||
|
||||
---
|
||||
|
||||
## v0.33.8.4
|
||||
|
||||
### 🚀 Features
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
|
|||
</span>
|
||||
)}
|
||||
{prefs.showSubscription && !!bill.is_subscription && (
|
||||
<span className="shrink-0 rounded bg-indigo-500/15 px-1.5 py-0.5 text-[11px] font-semibold text-indigo-600 dark:text-indigo-300">
|
||||
Sub
|
||||
<span className="shrink-0 rounded border border-indigo-500/25 bg-indigo-500/10 px-1.5 py-0.5 text-[11px] font-semibold text-indigo-600 dark:text-indigo-300">
|
||||
S
|
||||
</span>
|
||||
)}
|
||||
{hasHistory && (
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export const MobileBillRow = React.memo(function MobileBillRow({ bill, onEdit, o
|
|||
<span className="rounded bg-violet-500/20 px-1.5 py-0.5 text-[10px] font-semibold text-violet-300">2FA</span>
|
||||
)}
|
||||
{bill.is_subscription && (
|
||||
<span className="rounded bg-indigo-500/15 px-1.5 py-0.5 text-[10px] font-semibold text-indigo-600 dark:text-indigo-300">Sub</span>
|
||||
<span className="rounded border border-indigo-500/25 bg-indigo-500/10 px-1.5 py-0.5 text-[10px] font-semibold text-indigo-600 dark:text-indigo-300">S</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ export const MobileTrackerRow = React.memo(function MobileTrackerRow({ row, year
|
|||
className="inline-flex shrink-0 rounded border border-indigo-500/25 bg-indigo-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-indigo-600 dark:text-indigo-300"
|
||||
title="Subscription"
|
||||
>
|
||||
Sub
|
||||
S
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -789,7 +789,7 @@ function NotesCell({ row, refresh }) {
|
|||
}
|
||||
|
||||
// ── Table row ──────────────────────────────────────────────────────────────
|
||||
function Row({ row, year, month, refresh, index, onEditBill }) {
|
||||
function Row({ row, year, month, refresh, index, onEditBill, compact = false }) {
|
||||
const amountRef = useRef(null);
|
||||
const [editPayment, setEditPayment] = useState(null);
|
||||
const [paymentLedgerOpen, setPaymentLedgerOpen] = useState(false);
|
||||
|
|
@ -1049,7 +1049,7 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
|
|||
className="inline-flex shrink-0 rounded border border-indigo-500/25 bg-indigo-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-indigo-600 dark:text-indigo-300"
|
||||
title="Subscription"
|
||||
>
|
||||
Sub
|
||||
S
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
|
|
@ -1155,7 +1155,7 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
|
|||
</TableCell>
|
||||
|
||||
{/* Previous month paid */}
|
||||
<TableCell className="tracker-number w-[10%] py-3 text-right text-[13px] font-medium text-muted-foreground/80">
|
||||
<TableCell className={cn('tracker-number w-[10%] py-3 text-right text-[13px] font-medium text-muted-foreground/80', compact && '2xl:hidden')}>
|
||||
{row.previous_month_paid > 0 ? fmt(row.previous_month_paid) : '—'}
|
||||
</TableCell>
|
||||
|
||||
|
|
@ -1252,7 +1252,7 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
|
|||
</TableCell>
|
||||
|
||||
{/* Notes cell (monthly state notes) */}
|
||||
<TableCell className="w-[23%] py-3 border-l border-border pl-4">
|
||||
<TableCell className={cn(compact ? 'w-[16%]' : 'w-[23%]', 'py-3 border-l border-border pl-4')}>
|
||||
<NotesCell row={{ ...row, year, month }} refresh={refresh} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -1621,7 +1621,7 @@ function MobileTrackerRow({ row, year, month, refresh, index, onEditBill }) {
|
|||
}
|
||||
|
||||
// ── Bucket ─────────────────────────────────────────────────────────────────
|
||||
function Bucket({ label, rows, year, month, refresh, onEditBill, loading }) {
|
||||
function Bucket({ label, rows, year, month, refresh, onEditBill, loading, compact = false }) {
|
||||
// Use actual_amount (if set) as the per-row threshold; exclude skipped rows from totals
|
||||
const activeRows = rows.filter(r => !r.is_skipped);
|
||||
const totalThreshold = activeRows.reduce((s, r) => s + (r.actual_amount ?? r.expected_amount ?? 0), 0);
|
||||
|
|
@ -1631,10 +1631,11 @@ function Bucket({ label, rows, year, month, refresh, onEditBill, loading }) {
|
|||
const cappedPaid = Number(r.paid_toward_due);
|
||||
return s + (Number.isFinite(cappedPaid) ? cappedPaid : Math.min(Number(r.total_paid) || 0, threshold));
|
||||
}, 0);
|
||||
const totalOverpaid = Math.max(totalPaid - totalPaidTowardDue, 0);
|
||||
const skippedCount = rows.length - activeRows.length;
|
||||
const pct = totalThreshold > 0 ? Math.min((totalPaidTowardDue / totalThreshold) * 100, 100) : 0;
|
||||
const allPaid = pct >= 100;
|
||||
const totalOverpaid = Math.max(totalPaid - totalPaidTowardDue, 0);
|
||||
const totalRemaining = Math.max(totalThreshold - totalPaidTowardDue, 0);
|
||||
const skippedCount = rows.length - activeRows.length;
|
||||
const pct = totalThreshold > 0 ? Math.min((totalPaidTowardDue / totalThreshold) * 100, 100) : 0;
|
||||
const allPaid = pct >= 100;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border/80 overflow-hidden bg-card/95 shadow-sm shadow-black/15">
|
||||
|
|
@ -1665,16 +1666,24 @@ function Bucket({ label, rows, year, month, refresh, onEditBill, loading }) {
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs font-mono text-muted-foreground">
|
||||
<span className={cn(allPaid ? 'text-emerald-500' : 'text-foreground')}>
|
||||
{fmt(totalPaidTowardDue)}
|
||||
<div className="flex items-center gap-3 text-xs font-mono text-muted-foreground">
|
||||
<span>
|
||||
<span className={cn(allPaid ? 'text-emerald-500' : 'text-foreground')}>
|
||||
{fmt(totalPaidTowardDue)}
|
||||
</span>
|
||||
<span className="text-muted-foreground/50 mx-1">/</span>
|
||||
{fmt(totalThreshold)}
|
||||
{totalOverpaid > 0 && (
|
||||
<span className="ml-1 text-emerald-500">+{fmt(totalOverpaid)}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-muted-foreground/50 mx-1">/</span>
|
||||
{fmt(totalThreshold)}
|
||||
{totalOverpaid > 0 && (
|
||||
<span className="ml-1 text-emerald-500">+{fmt(totalOverpaid)}</span>
|
||||
{!allPaid && totalRemaining > 0 && (
|
||||
<span className="text-[11px] text-muted-foreground/70">{fmt(totalRemaining)} left</span>
|
||||
)}
|
||||
</span>
|
||||
{allPaid && (
|
||||
<span className="text-[11px] text-emerald-500">Done</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 p-3 lg:hidden" aria-busy={loading ? 'true' : 'false'}>
|
||||
|
|
@ -1723,18 +1732,18 @@ function Bucket({ label, rows, year, month, refresh, onEditBill, loading }) {
|
|||
|
||||
<div className="hidden lg:block" aria-busy={loading ? 'true' : 'false'}>
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="min-w-[1120px]">
|
||||
<Table className={cn('min-w-[1120px]', compact && '2xl:min-w-[700px]')}>
|
||||
<TableHeader>
|
||||
<TableRow className="border-border/80 bg-background/30 hover:bg-background/30">
|
||||
<TableHead className="w-[18%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90">Bill</TableHead>
|
||||
<TableHead className="w-[10%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90">Due</TableHead>
|
||||
<TableHead className="w-[9%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90">Due</TableHead>
|
||||
<TableHead className="w-[10%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90 text-right">Expected</TableHead>
|
||||
<TableHead className="w-[10%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/80 text-right">Last Month</TableHead>
|
||||
<TableHead className={cn('w-[10%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/80 text-right', compact && '2xl:hidden')}>Last Month</TableHead>
|
||||
<TableHead className="w-[10%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90 text-right">Paid</TableHead>
|
||||
<TableHead className="w-[10%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90">Paid Date</TableHead>
|
||||
<TableHead className="w-[9%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90">Status</TableHead>
|
||||
<TableHead className="w-[10%] py-2.5" />
|
||||
<TableHead className="w-[23%] py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90 border-l border-border/80 pl-4">
|
||||
<TableHead className="w-[8%] py-2.5" />
|
||||
<TableHead className={cn('py-2.5 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/90 border-l border-border/80 pl-4', compact ? 'w-[16%]' : 'w-[23%]')}>
|
||||
Notes
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
|
|
@ -1782,6 +1791,7 @@ function Bucket({ label, rows, year, month, refresh, onEditBill, loading }) {
|
|||
refresh={refresh}
|
||||
index={i}
|
||||
onEditBill={onEditBill}
|
||||
compact={compact}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
|
@ -1918,6 +1928,7 @@ export default function TrackerPage() {
|
|||
}, [filters, rows, search]);
|
||||
const first = filteredRows.filter(r => r.bucket === '1st');
|
||||
const second = filteredRows.filter(r => r.bucket === '15th');
|
||||
const hasBoth = first.length > 0 && second.length > 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
|
|
@ -2038,12 +2049,6 @@ export default function TrackerPage() {
|
|||
onEdit={() => setEditStartingOpen(true)}
|
||||
/>
|
||||
<SummaryCard type="paid" value={summary.total_paid} />
|
||||
<SummaryCard
|
||||
type="remaining"
|
||||
value={summary.remaining}
|
||||
label={summary.remaining_label || 'Remaining'}
|
||||
hint={summary.remaining_hint}
|
||||
/>
|
||||
<SummaryCard type="overdue" value={summary.overdue} />
|
||||
<SummaryCard type="paid" value={summary.previous_month_total} hint="Previous month"/>
|
||||
{summary.trend && <TrendCard trend={summary.trend} />}
|
||||
|
|
@ -2116,8 +2121,12 @@ export default function TrackerPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isError && first.length > 0 && <Bucket label="1st – 14th" rows={first} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} />}
|
||||
{!isError && second.length > 0 && <Bucket label="15th – 31st" rows={second} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} />}
|
||||
{!isError && (first.length > 0 || second.length > 0) && (
|
||||
<div className={cn('space-y-5', hasBoth && '2xl:space-y-0 2xl:grid 2xl:grid-cols-2 2xl:gap-5')}>
|
||||
{first.length > 0 && <Bucket label="1st – 14th" rows={first} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} compact={hasBoth} />}
|
||||
{second.length > 0 && <Bucket label="15th – 31st" rows={second} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} compact={hasBoth} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Bill modal — opened by clicking a bill name in any tracker row */}
|
||||
{editBillData && (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "bill-tracker",
|
||||
"version": "0.33.8.4",
|
||||
"version": "0.33.8.5",
|
||||
"description": "Monthly bill tracking system",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue