diff --git a/client/components/MobileTrackerRow.jsx b/client/components/MobileTrackerRow.jsx index a259257..40f4356 100644 --- a/client/components/MobileTrackerRow.jsx +++ b/client/components/MobileTrackerRow.jsx @@ -1,5 +1,5 @@ import React, { useMemo, useRef, useState } from 'react'; -import { Pencil, Settings2 } from 'lucide-react'; +import { AlertCircle, Pencil, Settings2 } from 'lucide-react'; import { toast } from 'sonner'; import { cn, fmt, fmtDate } from '@/lib/utils'; import { Button } from '@/components/ui/button'; @@ -17,8 +17,8 @@ const ROW_STATUS_CLS = { autodraft: 'bg-sky-500/[0.04] dark:bg-sky-400/[0.018]', upcoming: '', due_soon: 'bg-amber-400/[0.07] dark:bg-amber-300/[0.016]', - late: 'bg-orange-400/[0.08] dark:bg-orange-300/[0.014]', - missed: 'bg-red-400/[0.08] dark:bg-rose-300/[0.01]', + late: 'border-l-4 border-l-orange-400 bg-orange-500/[0.16] ring-1 ring-inset ring-orange-400/25 dark:bg-orange-400/[0.11] dark:ring-orange-300/25', + missed: 'border-l-4 border-l-rose-400 bg-rose-500/[0.18] ring-1 ring-inset ring-rose-400/30 dark:bg-rose-400/[0.13] dark:ring-rose-300/30', }; function paymentDateForTrackerMonth(year, month, dueDay) { @@ -146,6 +146,7 @@ export const MobileTrackerRow = React.memo(function MobileTrackerRow({ row, year }, [isSkipped, isPaidByThreshold, row.status]); const rowBg = useMemo(() => isSkipped ? '' : (ROW_STATUS_CLS[effectiveStatus] || ''), [isSkipped, effectiveStatus]); + const isUrgent = effectiveStatus === 'late' || effectiveStatus === 'missed'; const remaining = useMemo(() => Math.max((threshold || 0) - (row.total_paid || 0), 0), [threshold, row.total_paid]); async function handleQuickPay() { @@ -166,6 +167,7 @@ export const MobileTrackerRow = React.memo(function MobileTrackerRow({ row, year className={cn( 'rounded-lg border border-border/70 bg-card/90 p-3 shadow-sm shadow-black/10', 'space-y-3 transition-colors', + isUrgent && 'border-border/80 shadow-md shadow-rose-950/10', isSkipped ? 'opacity-55' : rowBg, )} style={{ animationDelay: `${index * 40}ms` }} @@ -199,6 +201,12 @@ export const MobileTrackerRow = React.memo(function MobileTrackerRow({ row, year {row.monthly_notes}

)} + {isUrgent && ( +

+ + Needs attention +

+ )} diff --git a/client/components/StatusBadge.jsx b/client/components/StatusBadge.jsx index 37366d1..6560c14 100644 --- a/client/components/StatusBadge.jsx +++ b/client/components/StatusBadge.jsx @@ -1,24 +1,28 @@ import React, { useMemo } from 'react'; +import { AlertCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; const STATUS_META = { paid: { label: 'Paid', cls: 'bg-emerald-500/15 text-emerald-500 border border-emerald-500/30 dark:bg-emerald-300/10 dark:text-emerald-200 dark:border-emerald-300/30' }, upcoming: { label: 'Upcoming', cls: 'bg-secondary text-muted-foreground border border-border' }, due_soon: { label: 'Due Soon', cls: 'bg-amber-400/15 text-amber-500 border border-amber-400/30 dark:bg-amber-300/10 dark:text-amber-200 dark:border-amber-300/28' }, - late: { label: 'Late', cls: 'bg-orange-400/15 text-orange-500 border border-orange-400/30 dark:bg-orange-300/10 dark:text-orange-200 dark:border-orange-300/26' }, - missed: { label: 'Missed', cls: 'bg-red-400/15 text-red-500 border border-red-400/30 dark:bg-rose-300/10 dark:text-rose-200 dark:border-rose-300/26' }, + late: { label: 'Late', cls: 'bg-orange-500/30 text-orange-800 border border-orange-500/60 shadow-sm shadow-orange-950/10 dark:bg-orange-400/25 dark:text-orange-100 dark:border-orange-300/60' }, + missed: { label: 'Missed', cls: 'bg-rose-500/30 text-rose-800 border border-rose-500/70 shadow-sm shadow-rose-950/10 dark:bg-rose-400/25 dark:text-rose-100 dark:border-rose-300/60' }, autodraft: { label: 'Autodraft', cls: 'bg-sky-400/15 text-sky-500 border border-sky-400/30 dark:bg-sky-300/10 dark:text-sky-200 dark:border-sky-300/28' }, skipped: { label: 'Skipped', cls: 'bg-muted text-muted-foreground border border-border' }, }; export const StatusBadge = React.memo(function StatusBadge({ status }) { const meta = useMemo(() => STATUS_META[status] || STATUS_META.upcoming, [status]); + const isUrgent = status === 'late' || status === 'missed'; return ( + {isUrgent && } {meta.label} ); diff --git a/client/components/ui/badge.jsx b/client/components/ui/badge.jsx index 5d0af65..9951dc4 100644 --- a/client/components/ui/badge.jsx +++ b/client/components/ui/badge.jsx @@ -13,8 +13,8 @@ const badgeVariants = cva( outline: 'text-foreground', // Bill status variants paid: 'bg-emerald-500/20 text-emerald-300 border-emerald-400/35', - late: 'bg-orange-500/20 text-orange-300 border-orange-400/35', - missed: 'bg-red-500/20 text-red-300 border-red-400/35', + late: 'bg-orange-500/30 text-orange-100 border-orange-300/60 shadow-sm shadow-orange-950/10', + missed: 'bg-rose-500/30 text-rose-100 border-rose-300/60 shadow-sm shadow-rose-950/10', due_soon: 'bg-yellow-500/20 text-yellow-200 border-yellow-400/35', upcoming: 'bg-slate-400/20 text-slate-200 border-slate-300/30', autodraft: 'bg-amber-500/20 text-amber-200 border-amber-400/35', diff --git a/client/pages/CalendarPage.jsx b/client/pages/CalendarPage.jsx index b63af82..37b0e72 100644 --- a/client/pages/CalendarPage.jsx +++ b/client/pages/CalendarPage.jsx @@ -46,7 +46,8 @@ function displayStatus(status) { function statusTone(status) { if (status === 'paid' || status === 'autodraft') return 'border-emerald-500/30 bg-emerald-500/15 text-emerald-700 dark:text-emerald-300'; if (status === 'skipped') return 'border-border bg-muted/80 text-muted-foreground'; - if (status === 'late' || status === 'missed') return 'border-destructive/30 bg-destructive/15 text-destructive'; + if (status === 'late') return 'border-orange-400/60 bg-orange-500/25 text-orange-800 shadow-sm shadow-orange-950/10 dark:text-orange-100'; + if (status === 'missed') return 'border-rose-400/60 bg-rose-500/30 text-rose-800 shadow-sm shadow-rose-950/10 dark:text-rose-100'; return 'border-primary/30 bg-primary/15 text-primary'; } @@ -204,7 +205,7 @@ function DayIndicators({ day, moneyMarker }) { {hasPaid && } {(hasDue || paymentOnly) && } {hasSkipped && } - {hasMissed && } + {hasMissed && } ); } @@ -252,7 +253,7 @@ function CalendarGrid({ data, selectedDate, onSelectDay, moneyMarkers }) { 'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50', hasActivity && 'bg-primary/[0.06] hover:bg-accent/70', isPaidDay && 'bg-emerald-500/[0.10]', - hasMissed && 'bg-destructive/[0.10]', + hasMissed && 'border-rose-400/40 bg-rose-500/[0.18] ring-1 ring-inset ring-rose-400/30 dark:bg-rose-400/[0.12]', isSelected && 'ring-2 ring-primary ring-inset bg-primary/[0.09]', )} aria-label={`View ${fmtDate(day.date)}`} @@ -265,7 +266,12 @@ function CalendarGrid({ data, selectedDate, onSelectDay, moneyMarkers }) { {day.day} {summary.due_count > 0 && ( - + {summary.due_count} )} @@ -450,7 +456,14 @@ function DayDetailDialog({ day, open, onOpenChange, moneyMarker }) { ) : (
{day.bills_due.map(bill => ( -
+

{bill.name}

diff --git a/client/pages/TrackerPage.jsx b/client/pages/TrackerPage.jsx index 6ec1b62..9faa8d8 100644 --- a/client/pages/TrackerPage.jsx +++ b/client/pages/TrackerPage.jsx @@ -56,16 +56,16 @@ const ROW_STATUS_CLS = { autodraft: 'bg-sky-500/[0.04] dark:bg-sky-400/[0.018]', upcoming: '', due_soon: 'bg-amber-400/[0.07] dark:bg-amber-300/[0.016]', - late: 'bg-orange-400/[0.08] dark:bg-orange-300/[0.014]', - missed: 'bg-red-400/[0.08] dark:bg-rose-300/[0.01]', + late: 'border-l-4 border-l-orange-400 bg-orange-500/[0.16] ring-1 ring-inset ring-orange-400/25 dark:bg-orange-400/[0.11] dark:ring-orange-300/25', + missed: 'border-l-4 border-l-rose-400 bg-rose-500/[0.18] ring-1 ring-inset ring-rose-400/30 dark:bg-rose-400/[0.13] dark:ring-rose-300/30', }; const STATUS_META = { paid: { label: 'Paid', cls: 'bg-emerald-500/15 text-emerald-500 border border-emerald-500/30 dark:bg-emerald-300/10 dark:text-emerald-200 dark:border-emerald-300/30' }, upcoming: { label: 'Upcoming', cls: 'bg-secondary text-muted-foreground border border-border' }, due_soon: { label: 'Due Soon', cls: 'bg-amber-400/15 text-amber-500 border border-amber-400/30 dark:bg-amber-300/10 dark:text-amber-200 dark:border-amber-300/28' }, - late: { label: 'Late', cls: 'bg-orange-400/15 text-orange-500 border border-orange-400/30 dark:bg-orange-300/10 dark:text-orange-200 dark:border-orange-300/26' }, - missed: { label: 'Missed', cls: 'bg-red-400/15 text-red-500 border border-red-400/30 dark:bg-rose-300/10 dark:text-rose-200 dark:border-rose-300/26' }, + late: { label: 'Late', cls: 'bg-orange-500/30 text-orange-800 border border-orange-500/60 shadow-sm shadow-orange-950/10 dark:bg-orange-400/25 dark:text-orange-100 dark:border-orange-300/60' }, + missed: { label: 'Missed', cls: 'bg-rose-500/30 text-rose-800 border border-rose-500/70 shadow-sm shadow-rose-950/10 dark:bg-rose-400/25 dark:text-rose-100 dark:border-rose-300/60' }, autodraft: { label: 'Autodraft', cls: 'bg-sky-400/15 text-sky-500 border border-sky-400/30 dark:bg-sky-300/10 dark:text-sky-200 dark:border-sky-300/28' }, skipped: { label: 'Skipped', cls: 'bg-muted text-muted-foreground border border-border' }, }; @@ -263,6 +263,7 @@ const StatusBadge = React.memo(function StatusBadge({ status, clickable, onClick const meta = useMemo(() => STATUS_META[status] || STATUS_META.upcoming, [status]); const isSkipped = status === 'skipped'; + const isUrgent = status === 'late' || status === 'missed'; const canClick = clickable && !isSkipped && !loading; return ( @@ -274,6 +275,7 @@ const StatusBadge = React.memo(function StatusBadge({ status, clickable, onClick 'inline-flex items-center px-2.5 py-0.5 text-[11px] rounded-md font-semibold', 'uppercase tracking-wide whitespace-nowrap', 'transition-all duration-150', + isUrgent && 'gap-1.5 px-2.5 py-1 text-xs', canClick && 'cursor-pointer hover:scale-105 hover:shadow-sm', canClick && status === 'paid' && 'hover:bg-red-500/20 hover:text-red-600 hover:border-red-500/40', canClick && status !== 'paid' && 'hover:bg-emerald-500/20 hover:text-emerald-600 hover:border-emerald-500/40', @@ -288,7 +290,10 @@ const StatusBadge = React.memo(function StatusBadge({ status, clickable, onClick {meta.label} ) : ( - meta.label + <> + {isUrgent && } + {meta.label} + )} ); diff --git a/package.json b/package.json index 61abae6..9eb4b7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bill-tracker", - "version": "0.30.3", + "version": "0.30.4", "description": "Monthly bill tracking system", "main": "server.js", "scripts": {