feat: summary overdue highlighting, tracker row visual polish, bill table cleanup
This commit is contained in:
parent
bb04966bbc
commit
88cb9d5340
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TooltipProvider delayDuration={300}>
|
||||||
{prefs.showAutopay && !!bill.autopay_enabled && (
|
{prefs.showAutopay && !!bill.autopay_enabled && (
|
||||||
<span className="shrink-0 rounded bg-emerald-500/20 px-1.5 py-0.5 text-[11px] font-semibold text-emerald-300">
|
<Tooltip>
|
||||||
Autopay
|
<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">
|
||||||
|
AP
|
||||||
</span>
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Autopay enabled</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{prefs.show2fa && !!bill.has_2fa && (
|
{prefs.show2fa && !!bill.has_2fa && (
|
||||||
<span className="shrink-0 rounded bg-violet-500/20 px-1.5 py-0.5 text-[11px] font-semibold text-violet-300">
|
<Tooltip>
|
||||||
|
<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">
|
||||||
2FA
|
2FA
|
||||||
</span>
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Two-factor authentication configured</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{prefs.showSubscription && !!bill.is_subscription && (
|
{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">
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<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">
|
||||||
S
|
S
|
||||||
</span>
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Subscription</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{(!!bill.has_merchant_rule || !!bill.has_linked_transactions) && (
|
{(!!bill.has_merchant_rule || !!bill.has_linked_transactions) && (
|
||||||
<span
|
<Tooltip>
|
||||||
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 asChild>
|
||||||
title="Linked to bank transactions"
|
<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
|
L
|
||||||
</span>
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Linked to bank transactions</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{hasHistory && (
|
{hasHistory && (
|
||||||
<span
|
<Tooltip>
|
||||||
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"
|
<TooltipTrigger asChild>
|
||||||
title="Historical visibility configured"
|
<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" />
|
<Clock className="h-2.5 w-2.5" />
|
||||||
</span>
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Historical visibility configured</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Meta row */}
|
{/* Meta row */}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
<TooltipProvider delayDuration={300}>
|
||||||
{row.autopay_enabled && (
|
{row.autopay_enabled && (
|
||||||
<span
|
<Tooltip>
|
||||||
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"
|
<TooltipTrigger asChild>
|
||||||
title="Autopay"
|
<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>
|
||||||
|
<TooltipContent>Autopay enabled</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{(row.has_merchant_rule || row.has_linked_transactions) && (
|
{(row.has_merchant_rule || row.has_linked_transactions) && (
|
||||||
<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"
|
<TooltipTrigger asChild>
|
||||||
title="Linked to bank transactions"
|
<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
|
L
|
||||||
</span>
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Linked to bank transactions</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{row.is_subscription && (
|
{row.is_subscription && (
|
||||||
<span
|
<Tooltip>
|
||||||
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"
|
<TooltipTrigger asChild>
|
||||||
title="Subscription"
|
<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
|
S
|
||||||
</span>
|
</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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue