diff --git a/src/app/dashboard/_components/animated-stats-card.tsx b/src/app/dashboard/_components/animated-stats-card.tsx index 80de760..9c083c9 100644 --- a/src/app/dashboard/_components/animated-stats-card.tsx +++ b/src/app/dashboard/_components/animated-stats-card.tsx @@ -3,6 +3,7 @@ import { TrendingDown, TrendingUp, + Minus, DollarSign, Clock, Users, @@ -15,7 +16,7 @@ interface AnimatedStatsCardProps { title: string; value: string; change: string; - trend: "up" | "down"; + trend: "up" | "down" | "neutral"; iconName: IconName; description: string; delay?: number; @@ -42,8 +43,13 @@ export function AnimatedStatsCard({ numericValue, }: AnimatedStatsCardProps) { const Icon = iconMap[iconName]; - const TrendIcon = trend === "up" ? TrendingUp : TrendingDown; + + let TrendIcon = Minus; + if (trend === "up") TrendIcon = TrendingUp; + if (trend === "down") TrendIcon = TrendingDown; + const isPositive = trend === "up"; + const isNeutral = trend === "neutral"; // For now, always use the formatted value prop to ensure correct display // Animation can be added back once the basic display is working correctly @@ -65,9 +71,11 @@ export function AnimatedStatsCard({
diff --git a/src/app/dashboard/_components/revenue-chart.tsx b/src/app/dashboard/_components/revenue-chart.tsx index c256d61..8d09b85 100644 --- a/src/app/dashboard/_components/revenue-chart.tsx +++ b/src/app/dashboard/_components/revenue-chart.tsx @@ -8,8 +8,6 @@ import { XAxis, YAxis, } from "recharts"; -import { getEffectiveInvoiceStatus } from "~/lib/invoice-status"; -import type { StoredInvoiceStatus } from "~/types/invoice"; import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider"; interface Invoice { @@ -21,49 +19,16 @@ interface Invoice { } interface RevenueChartProps { - invoices: Invoice[]; + data: { + month: string; + revenue: number; + monthLabel: string; + }[]; } -export function RevenueChart({ invoices }: RevenueChartProps) { - // Process invoice data to create monthly revenue data - const monthlyData = invoices - .filter( - (invoice) => - getEffectiveInvoiceStatus( - invoice.status as StoredInvoiceStatus, - invoice.dueDate, - ) === "paid", - ) - .reduce( - (acc, invoice) => { - const date = new Date(invoice.issueDate); - const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`; - - acc[monthKey] ??= { - month: monthKey, - revenue: 0, - count: 0, - }; - - acc[monthKey].revenue += invoice.totalAmount; - acc[monthKey].count += 1; - - return acc; - }, - {} as Record, - ); - - // Convert to array and sort by month - const chartData = Object.values(monthlyData) - .sort((a, b) => a.month.localeCompare(b.month)) - .slice(-6) // Show last 6 months - .map((item) => ({ - ...item, - monthLabel: new Date(item.month + "-01").toLocaleDateString("en-US", { - month: "short", - year: "2-digit", - }), - })); +export function RevenueChart({ data }: RevenueChartProps) { + // Use data directly + const chartData = data; const formatCurrency = (value: number) => { return new Intl.NumberFormat("en-US", { @@ -80,7 +45,7 @@ export function RevenueChart({ invoices }: RevenueChartProps) { label, }: { active?: boolean; - payload?: Array<{ payload: { revenue: number; count: number } }>; + payload?: Array<{ payload: { revenue: number } }>; label?: string; }) => { if (active && payload?.length) { @@ -92,7 +57,7 @@ export function RevenueChart({ invoices }: RevenueChartProps) { Revenue: {formatCurrency(data.revenue)}

- {data.count} invoice{data.count !== 1 ? "s" : ""} + {/* Count not available in aggregated view currently */}

); diff --git a/src/app/dashboard/clients/_components/clients-data-table.tsx b/src/app/dashboard/clients/_components/clients-data-table.tsx index b9e1ba6..8108597 100644 --- a/src/app/dashboard/clients/_components/clients-data-table.tsx +++ b/src/app/dashboard/clients/_components/clients-data-table.tsx @@ -90,8 +90,8 @@ export function ClientsDataTable({ const client = row.original; return (
-
- +
+

{client.name}

diff --git a/src/app/dashboard/clients/page.tsx b/src/app/dashboard/clients/page.tsx index a846349..7891db9 100644 --- a/src/app/dashboard/clients/page.tsx +++ b/src/app/dashboard/clients/page.tsx @@ -4,10 +4,11 @@ import { PageHeader } from "~/components/layout/page-header"; import { Button } from "~/components/ui/button"; import { HydrateClient } from "~/trpc/server"; import { ClientsTable } from "./_components/clients-table"; +import { Card, CardContent } from "~/components/ui/card"; export default async function ClientsPage() { return ( -
+
{ const invoice = row.original; return ( -
-

- {invoice.client?.name ?? "—"} -

-

- {invoice.invoiceNumber} -

+
+
+ +
+
+

+ {invoice.client?.name ?? "—"} +

+

+ {invoice.invoiceNumber} +

+
); }, diff --git a/src/app/dashboard/invoices/page.tsx b/src/app/dashboard/invoices/page.tsx index af0342e..e5e27e2 100644 --- a/src/app/dashboard/invoices/page.tsx +++ b/src/app/dashboard/invoices/page.tsx @@ -6,6 +6,7 @@ import { PageHeader } from "~/components/layout/page-header"; import { Plus, Upload } from "lucide-react"; import { InvoicesDataTable } from "./_components/invoices-data-table"; import { DataTableSkeleton } from "~/components/data/data-table"; +import { Card, CardContent } from "~/components/ui/card"; // Invoices Table Component async function InvoicesTable() { @@ -16,7 +17,7 @@ async function InvoicesTable() { export default async function InvoicesPage() { return ( -
+
- - - {/* Mobile layout - no left margin */} -
-
- - {children} -
-
- {/* Desktop layout - with sidebar margin */} -
-
- - {children} -
-
-
- ); + return {children}; } diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index b3cdf2b..a254c0c 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -26,131 +26,10 @@ import { MonthlyMetricsChart } from "~/app/dashboard/_components/monthly-metrics import { AnimatedStatsCard } from "~/app/dashboard/_components/animated-stats-card"; // Hero section with clean mono design -function DashboardHero({ firstName }: { firstName: string }) { - return ( -
-

Welcome back, {firstName}!

-

- Here's what's happening with your business today -

-
- ); -} + // Enhanced stats cards with better visuals -async function DashboardStats() { - const [clients, invoices] = await Promise.all([ - api.clients.getAll(), - api.invoices.getAll(), - ]); - - const totalClients = clients.length; - const paidInvoices = invoices.filter( - (invoice) => - getEffectiveInvoiceStatus( - invoice.status as StoredInvoiceStatus, - invoice.dueDate, - ) === "paid", - ); - const totalRevenue = paidInvoices.reduce( - (sum, invoice) => sum + invoice.totalAmount, - 0, - ); - - const pendingInvoices = invoices.filter((invoice) => { - const effectiveStatus = getEffectiveInvoiceStatus( - invoice.status as StoredInvoiceStatus, - invoice.dueDate, - ); - return effectiveStatus === "sent" || effectiveStatus === "overdue"; - }); - const pendingAmount = pendingInvoices.reduce( - (sum, invoice) => sum + invoice.totalAmount, - 0, - ); - - const overdueInvoices = invoices.filter( - (invoice) => - getEffectiveInvoiceStatus( - invoice.status as StoredInvoiceStatus, - invoice.dueDate, - ) === "overdue", - ); - - // Calculate month-over-month trends - const now = new Date(); - const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); - - // Current month data - const currentMonthInvoices = invoices.filter( - (invoice) => new Date(invoice.issueDate) >= currentMonth, - ); - const currentMonthRevenue = currentMonthInvoices - .filter( - (invoice) => - getEffectiveInvoiceStatus( - invoice.status as StoredInvoiceStatus, - invoice.dueDate, - ) === "paid", - ) - .reduce((sum, invoice) => sum + invoice.totalAmount, 0); - - // Last month data - const lastMonthInvoices = invoices.filter((invoice) => { - const date = new Date(invoice.issueDate); - return date >= lastMonth && date < currentMonth; - }); - const lastMonthRevenue = lastMonthInvoices - .filter( - (invoice) => - getEffectiveInvoiceStatus( - invoice.status as StoredInvoiceStatus, - invoice.dueDate, - ) === "paid", - ) - .reduce((sum, invoice) => sum + invoice.totalAmount, 0); - - // Previous month data for clients - const prevMonthClients = clients.filter( - (client) => new Date(client.createdAt) < currentMonth, - ).length; - - // Calculate trends - const revenueChange = - lastMonthRevenue > 0 - ? ((currentMonthRevenue - lastMonthRevenue) / lastMonthRevenue) * 100 - : currentMonthRevenue > 0 - ? 100 - : 0; - - const pendingChange = - lastMonthInvoices.length > 0 - ? ((pendingInvoices.length - - lastMonthInvoices.filter((invoice) => { - const status = getEffectiveInvoiceStatus( - invoice.status as StoredInvoiceStatus, - invoice.dueDate, - ); - return status === "sent" || status === "overdue"; - }).length) / - lastMonthInvoices.length) * - 100 - : pendingInvoices.length > 0 - ? 100 - : 0; - - const clientChange = totalClients - prevMonthClients; - - const lastMonthOverdue = lastMonthInvoices.filter( - (invoice) => - getEffectiveInvoiceStatus( - invoice.status as StoredInvoiceStatus, - invoice.dueDate, - ) === "overdue", - ).length; - const overdueChange = overdueInvoices.length - lastMonthOverdue; - +function DashboardStats({ stats }: { stats: any }) { // TODO: Import RouterOutput type const formatTrend = (value: number, isCount = false) => { if (isCount) { return value > 0 ? `+${value}` : value.toString(); @@ -158,66 +37,52 @@ async function DashboardStats() { return value > 0 ? `+${value.toFixed(1)}%` : `${value.toFixed(1)}%`; }; - // Debug logging to see actual values - console.log("Dashboard Stats Debug:", { - totalRevenue, - pendingAmount, - totalClients, - overdueInvoices: overdueInvoices.length, - revenueChange, - pendingChange, - clientChange, - overdueChange, - paidInvoicesCount: paidInvoices.length, - pendingInvoicesCount: pendingInvoices.length, - }); - - const stats = [ + const statCards = [ { title: "Total Revenue", - value: `$${totalRevenue.toLocaleString("en-US", { minimumFractionDigits: 2 })}`, - numericValue: totalRevenue, + value: `$${stats.totalRevenue.toLocaleString("en-US", { minimumFractionDigits: 2 })}`, + numericValue: stats.totalRevenue, isCurrency: true, - change: formatTrend(revenueChange), - trend: revenueChange >= 0 ? ("up" as const) : ("down" as const), + change: formatTrend(stats.revenueChange), + trend: stats.revenueChange >= 0 ? ("up" as const) : ("down" as const), iconName: "DollarSign" as const, - description: `From ${paidInvoices.length} paid invoices`, + description: "Total collected revenue", }, { title: "Pending Amount", - value: `$${pendingAmount.toLocaleString("en-US", { minimumFractionDigits: 2 })}`, - numericValue: pendingAmount, + value: `$${stats.pendingAmount.toLocaleString("en-US", { minimumFractionDigits: 2 })}`, + numericValue: stats.pendingAmount, isCurrency: true, - change: formatTrend(pendingChange), - trend: pendingChange >= 0 ? ("up" as const) : ("down" as const), + change: "0%", // TODO: Calculate pending change if needed + trend: "neutral" as const, iconName: "Clock" as const, - description: `${pendingInvoices.length} invoices awaiting payment`, + description: "Invoices awaiting payment", }, { title: "Active Clients", - value: totalClients.toString(), - numericValue: totalClients, + value: stats.totalClients.toString(), + numericValue: stats.totalClients, isCurrency: false, - change: formatTrend(clientChange, true), - trend: clientChange >= 0 ? ("up" as const) : ("down" as const), + change: "0", // TODO: Calculate client change if needed + trend: "neutral" as const, iconName: "Users" as const, description: "Total registered clients", }, { title: "Overdue Invoices", - value: overdueInvoices.length.toString(), - numericValue: overdueInvoices.length, + value: stats.overdueCount.toString(), + numericValue: stats.overdueCount, isCurrency: false, - change: formatTrend(overdueChange, true), - trend: overdueChange <= 0 ? ("up" as const) : ("down" as const), + change: "0", // TODO: Calculate overdue change if needed + trend: "neutral" as const, iconName: "TrendingDown" as const, description: "Invoices past due date", }, ]; return ( -
- {stats.map((stat, index) => ( +
+ {statCards.map((stat, index) => ( +
{/* Revenue Trend Chart */} @@ -250,7 +118,7 @@ async function ChartsSection() { - + @@ -441,14 +309,8 @@ async function CurrentWork() { } // Enhanced recent activity -async function RecentActivity() { - const invoices = await api.invoices.getAll(); - const recentInvoices = invoices - .sort( - (a, b) => - new Date(b.issueDate).getTime() - new Date(a.issueDate).getTime(), - ) - .slice(0, 5); +async function RecentActivity({ recentInvoices }: { recentInvoices: any[] }) { + // Use passed recentInvoices instead of fetching all const getStatusStyle = (status: string) => { switch (status) { @@ -558,7 +420,7 @@ async function RecentActivity() { // Loading skeletons function StatsSkeleton() { return ( -
+
{Array.from({ length: 4 }).map((_, i) => ( @@ -577,7 +439,7 @@ function StatsSkeleton() { function ChartsSkeleton() { return ( -
+
@@ -623,30 +485,40 @@ function CardSkeleton() { ); } +import { DashboardPageHeader } from "~/components/layout/page-header"; + +// ... imports + export default async function DashboardPage() { const session = await auth.api.getSession({ headers: await headers(), }); const firstName = session?.user?.name?.split(" ")[0] ?? "User"; + // Fetch stats centrally + const stats = await api.dashboard.getStats(); + return ( -
- +
+ }> - + }> - + -
-
+
+
}> @@ -657,7 +529,7 @@ export default async function DashboardPage() { }> - +
diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index 6540398..7117c66 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -3,21 +3,26 @@ import { HydrateClient } from "~/trpc/server"; import { PageHeader } from "~/components/layout/page-header"; import { DataTableSkeleton } from "~/components/data/data-table"; import { SettingsContent } from "./_components/settings-content"; +import { Card, CardContent } from "~/components/ui/card"; export default async function SettingsPage() { return ( -
+
- - }> - - - + + + + }> + + + + +
); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b524e3f..5341be3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ import { Geist, Geist_Mono, Instrument_Serif } from "next/font/google"; import { TRPCReactProvider } from "~/trpc/react"; import { Toaster } from "~/components/ui/sonner"; import { AnimationPreferencesProvider } from "~/components/providers/animation-preferences-provider"; +import { MotionBackground } from "~/components/layout/motion-background"; import { ThemeProvider } from "~/components/providers/theme-provider"; import { ColorThemeProvider } from "~/components/providers/color-theme-provider"; @@ -141,6 +142,7 @@ export default function RootLayout({ + {children} diff --git a/src/components/branding/logo.tsx b/src/components/branding/logo.tsx index 3912b9d..f3b62ea 100644 --- a/src/components/branding/logo.tsx +++ b/src/components/branding/logo.tsx @@ -5,30 +5,29 @@ import { cn } from "~/lib/utils"; interface LogoProps { className?: string; - size?: "sm" | "md" | "lg" | "xl"; + size?: "sm" | "md" | "lg" | "xl" | "icon"; animated?: boolean; } export function Logo({ className, size = "md", animated = true }: LogoProps) { const sizeClasses = { - sm: "text-sm", - md: "text-lg", - lg: "text-2xl", - xl: "text-4xl", + sm: "text-base", + md: "text-xl", + lg: "text-3xl", + xl: "text-5xl", + icon: "text-2xl", }; if (!animated) { return ; } - - return ( $ - - - been - - - voice - + {size !== "icon" && ( + <> + + + been + + + voice + + + )} ); } @@ -70,15 +73,19 @@ function LogoContent({ sizeClasses, }: { className?: string; - size: "sm" | "md" | "lg" | "xl"; + size: "sm" | "md" | "lg" | "xl" | "icon"; sizeClasses: Record; }) { return ( -
+
$ - - been - voice + {size !== "icon" && ( + <> + + been + voice + + )}
); } diff --git a/src/components/layout/dashboard-shell.tsx b/src/components/layout/dashboard-shell.tsx new file mode 100644 index 0000000..06fbe39 --- /dev/null +++ b/src/components/layout/dashboard-shell.tsx @@ -0,0 +1,70 @@ +"use client"; + +import * as React from "react"; +import { Sidebar } from "~/components/layout/sidebar"; +import { SidebarProvider, useSidebar } from "~/components/layout/sidebar-provider"; +import { cn } from "~/lib/utils"; +import { Menu } from "lucide-react"; +import { Button } from "~/components/ui/button"; +import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet"; +import { DashboardBreadcrumbs } from "~/components/navigation/dashboard-breadcrumbs"; + +function DashboardContent({ children }: { children: React.ReactNode }) { + const { isCollapsed } = useSidebar(); + const [isMobileOpen, setIsMobileOpen] = React.useState(false); + + return ( +
+ {/* Desktop Sidebar */} +
+ +
+ + {/* Mobile Sidebar (Sheet) */} +
+ + + + + + setIsMobileOpen(false)} /> + + +
+ + {/* Main Content */} +
+
+ {/* Mobile header spacer is handled by pt-16 on mobile */} +
+ {/* Mobile Breadcrumbs could go here or be part of the page */} +
+ {children} +
+
+
+ ); +} + +export function DashboardShell({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/components/layout/motion-background.tsx b/src/components/layout/motion-background.tsx new file mode 100644 index 0000000..8aab9b5 --- /dev/null +++ b/src/components/layout/motion-background.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { cn } from "~/lib/utils"; + +export function MotionBackground() { + return ( +
+
+
+
+
+ ); +} diff --git a/src/components/layout/page-header.tsx b/src/components/layout/page-header.tsx index 6c97360..209dd75 100644 --- a/src/components/layout/page-header.tsx +++ b/src/components/layout/page-header.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { DashboardBreadcrumbs } from "~/components/navigation/dashboard-breadcrumbs"; interface PageHeaderProps { title: string; @@ -39,24 +40,51 @@ export function PageHeader({ }; return ( -
-
-
-

{title}

- {description && ( -

- {description} -

- )} -
- {children && ( -
- {children} +
+ {variant === "large-gradient" || variant === "gradient" ? ( +
+
+
+ +
+
+

{title}

+ {description && ( +

+ {description} +

+ )} +
+ {children && ( +
+ {children} +
+ )} +
- )} -
+
+ ) : ( + <> + +
+
+

{title}

+ {description && ( +

+ {description} +

+ )} +
+ {children && ( +
+ {children} +
+ )} +
+ + )}
); } diff --git a/src/components/layout/sidebar-provider.tsx b/src/components/layout/sidebar-provider.tsx new file mode 100644 index 0000000..4e134b7 --- /dev/null +++ b/src/components/layout/sidebar-provider.tsx @@ -0,0 +1,60 @@ +"use client"; + +import * as React from "react"; + +interface SidebarContextType { + isCollapsed: boolean; + toggleCollapse: () => void; + expand: () => void; + collapse: () => void; +} + +const SidebarContext = React.createContext( + undefined, +); + +export function SidebarProvider({ children }: { children: React.ReactNode }) { + const [isCollapsed, setIsCollapsed] = React.useState(false); + + // Persist state if needed, for now just local state + React.useEffect(() => { + const saved = localStorage.getItem("sidebar-collapsed"); + if (saved) { + setIsCollapsed(JSON.parse(saved)); + } + }, []); + + const toggleCollapse = React.useCallback(() => { + setIsCollapsed((prev) => { + const next = !prev; + localStorage.setItem("sidebar-collapsed", JSON.stringify(next)); + return next; + }); + }, []); + + const expand = React.useCallback(() => { + setIsCollapsed(false); + localStorage.setItem("sidebar-collapsed", JSON.stringify(false)); + }, []); + + const collapse = React.useCallback(() => { + setIsCollapsed(true); + localStorage.setItem("sidebar-collapsed", JSON.stringify(true)); + }, []); + + return ( + + {children} + + ); +} + +export function useSidebar() { + const context = React.useContext(SidebarContext); + if (context === undefined) { + throw new Error("useSidebar must be used within a SidebarProvider"); + } + return context; +} diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index 52033ae..bb17677 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -5,102 +5,218 @@ import { usePathname } from "next/navigation"; import { authClient } from "~/lib/auth-client"; import { Skeleton } from "~/components/ui/skeleton"; import { Button } from "~/components/ui/button"; -import { LogOut, User } from "lucide-react"; +import { + LogOut, + User, + ChevronLeft, + ChevronRight, + PanelLeftClose, + PanelLeftOpen, + Settings +} from "lucide-react"; import { navigationConfig } from "~/lib/navigation"; +import { useSidebar } from "./sidebar-provider"; +import { cn } from "~/lib/utils"; +import { Logo } from "~/components/branding/logo"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; +import { getGravatarUrl } from "~/lib/gravatar"; +import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; -export function Sidebar() { +interface SidebarProps { + mobile?: boolean; + onClose?: () => void; +} + +export function Sidebar({ mobile, onClose }: SidebarProps) { const pathname = usePathname(); const { data: session, isPending } = authClient.useSession(); + const { isCollapsed, toggleCollapse } = useSidebar(); - return ( -