Unify entities navigation, redesign time clock, and add invoice PDF preview.

Combine clients and businesses under entities, polish the web time clock,
and show live invoice PDF preview with tighter line-item editing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 01:08:23 -04:00
co-authored by Cursor
parent 0b7ffac4e7
commit 480c50981d
19 changed files with 734 additions and 364 deletions
+12 -3
View File
@@ -3,7 +3,6 @@ import {
LayoutDashboard,
Users,
FileText,
Building,
Receipt,
BarChart2,
Shield,
@@ -22,14 +21,24 @@ export interface NavSection {
links: NavLink[];
}
export function isNavLinkActive(pathname: string, href: string): boolean {
if (href === "/dashboard/entities") {
return (
pathname === href ||
pathname.startsWith("/dashboard/clients") ||
pathname.startsWith("/dashboard/businesses")
);
}
return pathname === href;
}
export const navigationConfig: NavSection[] = [
{
title: "Main",
links: [
{ name: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
{ name: "Time clock", href: "/dashboard/time-clock", icon: Clock },
{ name: "Clients", href: "/dashboard/clients", icon: Users },
{ name: "Businesses", href: "/dashboard/businesses", icon: Building },
{ name: "Entities", href: "/dashboard/entities", icon: Users },
{ name: "Invoices", href: "/dashboard/invoices", icon: FileText },
{ name: "Recurring", href: "/dashboard/invoices/recurring", icon: RefreshCw },
{ name: "Expenses", href: "/dashboard/expenses", icon: Receipt },
+11
View File
@@ -0,0 +1,11 @@
const STORAGE_KEY = "beenvoice:time-clock:last-client";
export function getLastTimeClockClientId(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(STORAGE_KEY);
}
export function setLastTimeClockClientId(clientId: string): void {
if (!clientId || typeof window === "undefined") return;
localStorage.setItem(STORAGE_KEY, clientId);
}
+26
View File
@@ -1,3 +1,29 @@
export const DEFAULT_CLOCK_DESCRIPTION = "Professional services";
export function resolveEffectiveHourlyRate(
enteredRate: number,
client?: { defaultHourlyRate?: number | null } | null,
): number {
if (Number.isFinite(enteredRate) && enteredRate > 0) return enteredRate;
const clientRate = client?.defaultHourlyRate ?? 0;
if (Number.isFinite(clientRate) && clientRate > 0) return clientRate;
return 0;
}
export function startedAtFromMinutesAgo(minutes: number): Date {
return new Date(Date.now() - minutes * 60 * 1000);
}
export function resolveClockDescription(
title: string,
existingDescription?: string | null,
): string {
const trimmed = title.trim();
if (trimmed) return trimmed;
if (existingDescription?.trim()) return existingDescription.trim();
return DEFAULT_CLOCK_DESCRIPTION;
}
export type ClockOutOutcome =
| "linked_to_invoice"
| "saved_no_invoice"