feat: summary overdue highlighting, tracker row visual polish, bill table cleanup

This commit is contained in:
null 2026-06-06 23:29:34 -05:00
parent bb04966bbc
commit 88cb9d5340
4 changed files with 133 additions and 57 deletions

View File

@ -2,6 +2,7 @@ import { ArrowDown, ArrowUp, Copy, GripVertical, PenLine, EyeOff, Eye, Clock, Tr
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { scheduleLabel } from '@/lib/billingSchedule'; import { scheduleLabel } from '@/lib/billingSchedule';
import { MobileBillRow } from '@/components/MobileBillRow'; import { MobileBillRow } from '@/components/MobileBillRow';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
function ordinal(n) { function ordinal(n) {
const d = Number(n); const d = Number(n);
@ -104,37 +105,58 @@ function BillCard({ bill, prefs = ALL_ON, onEdit, onToggle, onDelete, onHistory,
</span> </span>
)} )}
{prefs.showAutopay && !!bill.autopay_enabled && ( <TooltipProvider delayDuration={300}>
<span className="shrink-0 rounded bg-emerald-500/20 px-1.5 py-0.5 text-[11px] font-semibold text-emerald-300"> {prefs.showAutopay && !!bill.autopay_enabled && (
Autopay <Tooltip>
</span> <TooltipTrigger asChild>
)} <span className="shrink-0 rounded border border-sky-500/25 bg-sky-500/10 px-1.5 py-0.5 text-[11px] font-semibold text-sky-600 dark:text-sky-300 cursor-default">
{prefs.show2fa && !!bill.has_2fa && ( AP
<span className="shrink-0 rounded bg-violet-500/20 px-1.5 py-0.5 text-[11px] font-semibold text-violet-300"> </span>
2FA </TooltipTrigger>
</span> <TooltipContent>Autopay enabled</TooltipContent>
)} </Tooltip>
{prefs.showSubscription && !!bill.is_subscription && ( )}
<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"> {prefs.show2fa && !!bill.has_2fa && (
S <Tooltip>
</span> <TooltipTrigger asChild>
)} <span className="shrink-0 rounded bg-violet-500/20 px-1.5 py-0.5 text-[11px] font-semibold text-violet-300 cursor-default">
{(!!bill.has_merchant_rule || !!bill.has_linked_transactions) && ( 2FA
<span </span>
className="shrink-0 rounded border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[11px] font-semibold text-emerald-600 dark:text-emerald-400" </TooltipTrigger>
title="Linked to bank transactions" <TooltipContent>Two-factor authentication configured</TooltipContent>
> </Tooltip>
L )}
</span> {prefs.showSubscription && !!bill.is_subscription && (
)} <Tooltip>
{hasHistory && ( <TooltipTrigger asChild>
<span <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 cursor-default">
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-sky-500/25 bg-sky-500/10 text-sky-500" S
title="Historical visibility configured" </span>
> </TooltipTrigger>
<Clock className="h-2.5 w-2.5" /> <TooltipContent>Subscription</TooltipContent>
</span> </Tooltip>
)} )}
{(!!bill.has_merchant_rule || !!bill.has_linked_transactions) && (
<Tooltip>
<TooltipTrigger asChild>
<span className="shrink-0 rounded border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[11px] font-semibold text-emerald-600 dark:text-emerald-400 cursor-default">
L
</span>
</TooltipTrigger>
<TooltipContent>Linked to bank transactions</TooltipContent>
</Tooltip>
)}
{hasHistory && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-sky-500/25 bg-sky-500/10 text-sky-500 cursor-default">
<Clock className="h-2.5 w-2.5" />
</span>
</TooltipTrigger>
<TooltipContent>Historical visibility configured</TooltipContent>
</Tooltip>
)}
</TooltipProvider>
</div> </div>
{/* Meta row */} {/* Meta row */}

View File

@ -1,5 +1,6 @@
import React, { useState, useRef, useTransition } from 'react'; import React, { useState, useRef, useTransition } from 'react';
import { ArrowDown, ArrowUp, GripVertical, Pencil, X } from 'lucide-react'; import { ArrowDown, ArrowUp, GripVertical, Pencil, X } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { api } from '@/api.js'; import { api } from '@/api.js';
import { cn, fmt, fmtDate } from '@/lib/utils'; import { cn, fmt, fmtDate } from '@/lib/utils';
@ -356,30 +357,38 @@ export function TrackerRow({ row, year, month, refresh, index, onEditBill, moveC
{row.name} {row.name}
</span> </span>
)} )}
{row.autopay_enabled && ( <TooltipProvider delayDuration={300}>
<span {row.autopay_enabled && (
className="inline-flex shrink-0 rounded border border-sky-500/25 bg-sky-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-sky-600 dark:text-sky-300" <Tooltip>
title="Autopay" <TooltipTrigger asChild>
> <span className="inline-flex shrink-0 rounded border border-sky-500/25 bg-sky-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-sky-600 dark:text-sky-300 cursor-default">
AP AP
</span> </span>
)} </TooltipTrigger>
{(row.has_merchant_rule || row.has_linked_transactions) && ( <TooltipContent>Autopay enabled</TooltipContent>
<span </Tooltip>
className="inline-flex shrink-0 rounded border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-emerald-600 dark:text-emerald-400" )}
title="Linked to bank transactions" {(row.has_merchant_rule || row.has_linked_transactions) && (
> <Tooltip>
L <TooltipTrigger asChild>
</span> <span className="inline-flex shrink-0 rounded border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-emerald-600 dark:text-emerald-400 cursor-default">
)} L
{row.is_subscription && ( </span>
<span </TooltipTrigger>
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" <TooltipContent>Linked to bank transactions</TooltipContent>
title="Subscription" </Tooltip>
> )}
S {row.is_subscription && (
</span> <Tooltip>
)} <TooltipTrigger asChild>
<span 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 cursor-default">
S
</span>
</TooltipTrigger>
<TooltipContent>Subscription</TooltipContent>
</Tooltip>
)}
</TooltipProvider>
<Button <Button
size="icon" variant="ghost" size="icon" variant="ghost"
className="h-7 w-7 opacity-100 transition-opacity text-muted-foreground hover:text-foreground hover:bg-accent lg:opacity-0 lg:group-hover:opacity-100" className="h-7 w-7 opacity-100 transition-opacity text-muted-foreground hover:text-foreground hover:bg-accent lg:opacity-0 lg:group-hover:opacity-100"

View File

@ -19,6 +19,7 @@ import { api } from '@/api.js';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { cn, fmt } from '@/lib/utils'; import { cn, fmt } from '@/lib/utils';
import { moveInArray, reorderPayload } from '@/lib/reorder'; import { moveInArray, reorderPayload } from '@/lib/reorder';
@ -183,7 +184,41 @@ function ExpenseRow({ expense, moveControls, dragProps }) {
</div> </div>
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate text-sm font-semibold text-foreground">{expense.name}</div> <div className="flex flex-wrap items-center gap-1.5">
<span className="truncate text-sm font-semibold text-foreground">{expense.name}</span>
<TooltipProvider delayDuration={300}>
{expense.autopay_enabled && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex shrink-0 rounded border border-sky-500/25 bg-sky-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-sky-600 dark:text-sky-300 cursor-default">
AP
</span>
</TooltipTrigger>
<TooltipContent>Autopay enabled</TooltipContent>
</Tooltip>
)}
{expense.is_subscription && (
<Tooltip>
<TooltipTrigger asChild>
<span 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 cursor-default">
S
</span>
</TooltipTrigger>
<TooltipContent>Subscription</TooltipContent>
</Tooltip>
)}
{(expense.has_merchant_rule || expense.has_linked_transactions) && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex shrink-0 rounded border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-emerald-600 dark:text-emerald-400 cursor-default">
L
</span>
</TooltipTrigger>
<TooltipContent>Linked to bank transactions</TooltipContent>
</Tooltip>
)}
</TooltipProvider>
</div>
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground"> <div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
{expense.category_name && <span>{expense.category_name}</span>} {expense.category_name && <span>{expense.category_name}</span>}
<span>Due day {expense.due_day}</span> <span>Due day {expense.due_day}</span>

View File

@ -238,10 +238,16 @@ function buildSummary(db, userId, year, month) {
c.name AS category_name, c.name AS category_name,
m.actual_amount, m.actual_amount,
m.is_skipped, m.is_skipped,
b.sort_order b.sort_order,
b.autopay_enabled,
b.is_subscription,
CASE WHEN mr.bill_id IS NOT NULL THEN 1 ELSE 0 END AS has_merchant_rule,
CASE WHEN lt.matched_bill_id IS NOT NULL THEN 1 ELSE 0 END AS has_linked_transactions
FROM bills b FROM bills b
LEFT JOIN categories c ON c.id = b.category_id AND c.user_id = b.user_id AND c.deleted_at IS NULL LEFT JOIN categories c ON c.id = b.category_id AND c.user_id = b.user_id AND c.deleted_at IS NULL
LEFT JOIN monthly_bill_state m ON m.bill_id = b.id AND m.year = ? AND m.month = ? LEFT JOIN monthly_bill_state m ON m.bill_id = b.id AND m.year = ? AND m.month = ?
LEFT JOIN (SELECT DISTINCT bill_id FROM bill_merchant_rules) mr ON mr.bill_id = b.id
LEFT JOIN (SELECT DISTINCT matched_bill_id FROM transactions WHERE match_status = 'matched') lt ON lt.matched_bill_id = b.id
WHERE b.user_id = ? AND b.active = 1 AND b.deleted_at IS NULL WHERE b.user_id = ? AND b.active = 1 AND b.deleted_at IS NULL
ORDER BY CASE WHEN b.sort_order IS NULL THEN 1 ELSE 0 END, ORDER BY CASE WHEN b.sort_order IS NULL THEN 1 ELSE 0 END,
b.sort_order ASC, b.sort_order ASC,
@ -292,6 +298,10 @@ function buildSummary(db, userId, year, month) {
is_skipped: !!row.is_skipped, is_skipped: !!row.is_skipped,
due_day: row.due_day, due_day: row.due_day,
category_name: row.category_name || null, category_name: row.category_name || null,
autopay_enabled: !!row.autopay_enabled,
is_subscription: !!row.is_subscription,
has_merchant_rule: !!row.has_merchant_rule,
has_linked_transactions: !!row.has_linked_transactions,
}; };
}); });