fix: NavPill and Sidebar UI refinements, trackerService adjustments
This commit is contained in:
parent
1ea6979903
commit
46bcf83d22
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
export const NavPill = React.memo(function NavPill({ item, onNavigate, badge }) {
|
export const NavPill = React.memo(function NavPill({ item, onNavigate, badge, badgeNames = [] }) {
|
||||||
const Icon = useMemo(() => item.icon, [item.icon]);
|
const Icon = useMemo(() => item.icon, [item.icon]);
|
||||||
const to = useMemo(() => item.to, [item.to]);
|
const to = useMemo(() => item.to, [item.to]);
|
||||||
const end = useMemo(() => item.end, [item.end]);
|
const end = useMemo(() => item.end, [item.end]);
|
||||||
|
|
@ -24,9 +25,24 @@ export const NavPill = React.memo(function NavPill({ item, onNavigate, badge })
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{badge > 0 && (
|
{badge > 0 && (
|
||||||
<span className="ml-0.5 flex h-4 min-w-[1rem] items-center justify-center rounded-full bg-rose-500 px-1 text-[10px] font-bold leading-none text-white">
|
<TooltipProvider delayDuration={300}>
|
||||||
{badge > 99 ? '99+' : badge}
|
<Tooltip>
|
||||||
</span>
|
<TooltipTrigger asChild>
|
||||||
|
<span className="ml-0.5 flex h-4 min-w-[1rem] items-center justify-center rounded-full bg-rose-500 px-1 text-[10px] font-bold leading-none text-white">
|
||||||
|
{badge > 99 ? '99+' : badge}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-[200px]">
|
||||||
|
<p className="font-semibold mb-1">{badge} past due</p>
|
||||||
|
{badgeNames.slice(0, 5).map(name => (
|
||||||
|
<p key={name} className="text-xs opacity-80">· {name}</p>
|
||||||
|
))}
|
||||||
|
{badgeNames.length > 5 && (
|
||||||
|
<p className="text-xs opacity-60">+{badgeNames.length - 5} more</p>
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useOverdueCount } from '@/hooks/useQueries';
|
import { useOverdueCount } from '@/hooks/useQueries';
|
||||||
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -44,7 +45,7 @@ const trackerItems = [
|
||||||
{ to: '/payoff', icon: Calculator, label: 'Payoff' },
|
{ to: '/payoff', icon: Calculator, label: 'Payoff' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function TrackerMenu({ onNavigate, badge }) {
|
function TrackerMenu({ onNavigate, badge, badgeNames = [] }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isTrackerActive = useMemo(() => trackerItems.some(item => (
|
const isTrackerActive = useMemo(() => trackerItems.some(item => (
|
||||||
|
|
@ -68,9 +69,24 @@ function TrackerMenu({ onNavigate, badge }) {
|
||||||
<LayoutGrid className="h-4 w-4" />
|
<LayoutGrid className="h-4 w-4" />
|
||||||
Tracker
|
Tracker
|
||||||
{badge > 0 && (
|
{badge > 0 && (
|
||||||
<span className="flex h-4 min-w-[1rem] items-center justify-center rounded-full bg-rose-500 px-1 text-[10px] font-bold leading-none text-white">
|
<TooltipProvider delayDuration={300}>
|
||||||
{badge > 99 ? '99+' : badge}
|
<Tooltip>
|
||||||
</span>
|
<TooltipTrigger asChild>
|
||||||
|
<span className="flex h-4 min-w-[1rem] items-center justify-center rounded-full bg-rose-500 px-1 text-[10px] font-bold leading-none text-white">
|
||||||
|
{badge > 99 ? '99+' : badge}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" className="max-w-[200px]">
|
||||||
|
<p className="font-semibold mb-1">{badge} past due</p>
|
||||||
|
{badgeNames.slice(0, 5).map(name => (
|
||||||
|
<p key={name} className="text-xs opacity-80">· {name}</p>
|
||||||
|
))}
|
||||||
|
{badgeNames.length > 5 && (
|
||||||
|
<p className="text-xs opacity-60">+{badgeNames.length - 5} more</p>
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
<ChevronDown className="h-3.5 w-3.5 opacity-75" />
|
<ChevronDown className="h-3.5 w-3.5 opacity-75" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -178,6 +194,7 @@ export default function Sidebar({ adminMode = false }) {
|
||||||
const items = useMemo(() => adminMode ? adminNavItems : userNavItems, [adminMode]);
|
const items = useMemo(() => adminMode ? adminNavItems : userNavItems, [adminMode]);
|
||||||
const { data: overdueData } = useOverdueCount();
|
const { data: overdueData } = useOverdueCount();
|
||||||
const overdueCount = (!adminMode && overdueData?.count) ? overdueData.count : 0;
|
const overdueCount = (!adminMode && overdueData?.count) ? overdueData.count : 0;
|
||||||
|
const overdueNames = (!adminMode && overdueData?.names) ? overdueData.names : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-40 border-b border-border/80 bg-card/95 shadow-sm shadow-foreground/10 backdrop-blur-md supports-[backdrop-filter]:bg-card/90">
|
<header className="sticky top-0 z-40 border-b border-border/80 bg-card/95 shadow-sm shadow-foreground/10 backdrop-blur-md supports-[backdrop-filter]:bg-card/90">
|
||||||
|
|
@ -185,7 +202,7 @@ export default function Sidebar({ adminMode = false }) {
|
||||||
<BrandBlock adminMode={adminMode} />
|
<BrandBlock adminMode={adminMode} />
|
||||||
|
|
||||||
<nav className="hidden items-center gap-1 lg:flex">
|
<nav className="hidden items-center gap-1 lg:flex">
|
||||||
{!adminMode && <TrackerMenu badge={overdueCount} />}
|
{!adminMode && <TrackerMenu badge={overdueCount} badgeNames={overdueNames} />}
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<NavPill key={item.to} item={item} />
|
<NavPill key={item.to} item={item} />
|
||||||
))}
|
))}
|
||||||
|
|
@ -232,6 +249,7 @@ export default function Sidebar({ adminMode = false }) {
|
||||||
item={item}
|
item={item}
|
||||||
onNavigate={() => setMobileOpen(false)}
|
onNavigate={() => setMobileOpen(false)}
|
||||||
badge={item.to === '/' ? overdueCount : undefined}
|
badge={item.to === '/' ? overdueCount : undefined}
|
||||||
|
badgeNames={item.to === '/' ? overdueNames : undefined}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
|
|
|
||||||
|
|
@ -537,21 +537,24 @@ function getOverdueCount(userId, now = new Date()) {
|
||||||
`).all(year, month, rangeStart, rangeEnd, userId);
|
`).all(year, month, rangeStart, rangeEnd, userId);
|
||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
const overdueNames = [];
|
||||||
for (const bill of bills) {
|
for (const bill of bills) {
|
||||||
if (bill.is_skipped) continue;
|
if (bill.is_skipped) continue;
|
||||||
if (bill.snoozed_until && bill.snoozed_until > todayStr) continue;
|
if (bill.snoozed_until && bill.snoozed_until > todayStr) continue;
|
||||||
if (bill.autopay_enabled && bill.autodraft_status === 'assumed_paid') continue;
|
if (bill.autopay_enabled && bill.autodraft_status === 'assumed_paid') continue;
|
||||||
|
|
||||||
const dueDate = resolveDueDate(bill, year, month);
|
const dueDate = resolveDueDate(bill, year, month);
|
||||||
if (!dueDate || dueDate > todayStr) continue;
|
// Use >= so bills due TODAY are not counted as overdue — only strictly past dates
|
||||||
|
if (!dueDate || dueDate >= todayStr) continue;
|
||||||
|
|
||||||
const threshold = bill.actual_amount != null ? bill.actual_amount : bill.expected_amount;
|
const threshold = bill.actual_amount != null ? bill.actual_amount : bill.expected_amount;
|
||||||
if (threshold > 0 && bill.total_paid >= threshold) continue;
|
if (threshold > 0 && bill.total_paid >= threshold) continue;
|
||||||
|
|
||||||
count++;
|
count++;
|
||||||
|
overdueNames.push(bill.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { count, month, year, today: todayStr };
|
return { count, names: overdueNames, month, year, today: todayStr };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue