@@ -1,10 +1,14 @@
"use client" ;
import { useMemo } from "react" ;
import { useMemo , useState } from "react" ;
import { api } from "~/trpc/react" ;
import { PageHeader } from "~/components/layout/page-header" ;
import { Card , CardContent , CardHeader , CardTitle } from "~/components/ui/card" ;
import { StatusBadge } from "~/components/data/status-badge" ;
import { Button } from "~/components/ui/button" ;
import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "~/components/ui/select" ;
import { Separator } from "~/components/ui/separator" ;
import { Tabs , TabsContent , TabsList , TabsTrigger } from "~/components/ui/tabs" ;
import { formatCurrency } from "~/lib/currency" ;
import { getEffectiveInvoiceStatus } from "~/lib/invoice-status" ;
import type { StoredInvoiceStatus } from "~/types/invoice" ;
@@ -19,18 +23,23 @@ import {
Tooltip ,
ResponsiveContainer ,
} from "recharts" ;
import { TrendingUp , DollarSign , Clock , Users } from "lucide-react" ;
import { TrendingUp , DollarSign , Clock , Users , Download , Receipt , FileText } from "lucide-react" ;
export default function ReportsPage() {
const { data : invoices = [ ] , isLoading } = api . invoices . getAll . useQuery ( ) ;
const { data : invoices = [ ] , isLoading : invoicesLoading } = api . invoices . getAll . useQuery ( ) ;
const { data : expenses = [ ] , isLoading : expensesLoading } = api . expenses . getAll . useQuery ( ) ;
const { data : stats } = api . dashboard . getStats . useQuery ( ) ;
const now = new Date ( ) ;
const isLoading = invoicesLoading || expensesLoading ;
const reportData = useMemo ( ( ) = > {
const now = new Date ( ) ;
const currentYear = now . getFullYear ( ) ;
const [ taxYear , setTaxYear ] = useState ( String ( currentYear ) ) ;
// Overview data (last 12 months)
const overviewData = useMemo ( ( ) = > {
if ( ! invoices . length ) return null ;
// Revenue by month (last 12 months)
const monthMap : Record < string , number > = { } ;
for ( let i = 11 ; i >= 0 ; i -- ) {
const d = new Date ( now . getFullYear ( ) , now . getMonth ( ) - i , 1 ) ;
@@ -41,7 +50,6 @@ export default function ReportsPage() {
let totalRevenue = 0 ;
let totalPending = 0 ;
let totalHours = 0 ;
let overdueCount = 0 ;
for ( const inv of invoices ) {
const status = getEffectiveInvoiceStatus ( inv . status as StoredInvoiceStatus , inv . dueDate ) ;
@@ -52,7 +60,6 @@ export default function ReportsPage() {
} else if ( status === "sent" || status === "overdue" ) {
totalPending += inv . totalAmount ;
}
if ( status === "overdue" ) overdueCount ++ ;
totalHours += ( inv . items ? ? [ ] ) . reduce ( ( s , item ) = > s + item . hours , 0 ) ;
}
@@ -61,35 +68,122 @@ export default function ReportsPage() {
revenue ,
} ) ) ;
// Top clients by revenue (paid only)
const clientMap : Record < string , { name : string ; revenue : number ; count : number } > = { } ;
const clientMap : Record < string , { name : string ; revenue : number } > = { } ;
for ( const inv of invoices ) {
const status = getEffectiveInvoiceStatus ( inv . status as StoredInvoiceStatus , inv . dueDate ) ;
if ( status === "paid" && inv . client ) {
const id = inv . client . id ;
if ( ! clientMap [ id ] ) clientMap [ id ] = { name : inv.client.name , revenue : 0 , count : 0 } ;
if ( ! clientMap [ id ] ) clientMap [ id ] = { name : inv.client.name , revenue : 0 } ;
clientMap [ id ] ! . revenue += inv . totalAmount ;
clientMap [ id ] ! . count += 1 ;
}
}
const topClients = Object . values ( clientMap )
. sort ( ( a , b ) = > b . revenue - a . revenue )
. slice ( 0 , 6 ) ;
const topClients = Object . values ( clientMap ) . sort ( ( a , b ) = > b . revenue - a . revenue ) . slice ( 0 , 6 ) ;
// Status breakdown
const statusCount : Record < string , number > = { draft : 0 , sent : 0 , paid : 0 , overdue : 0 } ;
for ( const inv of invoices ) {
const s = getEffectiveInvoiceStatus ( inv . status as StoredInvoiceStatus , inv . dueDate ) ;
statusCount [ s ] = ( statusCount [ s ] ? ? 0 ) + 1 ;
}
return { revenueByMonth , topClients , totalRevenue , totalPending , totalHours , overdueCount , statusCount } ;
return { revenueByMonth , topClients , totalRevenue , totalPending , totalHours , statusCount } ;
} , [ invoices ] ) ;
// Tax summary for selected year
const taxData = useMemo ( ( ) = > {
const year = parseInt ( taxYear ) ;
const yearInvoices = invoices . filter ( ( inv ) = > {
const status = getEffectiveInvoiceStatus ( inv . status as StoredInvoiceStatus , inv . dueDate ) ;
return status === "paid" && new Date ( inv . issueDate ) . getFullYear ( ) === year ;
} ) ;
const yearExpenses = expenses . filter ( ( exp ) = > new Date ( exp . date ) . getFullYear ( ) === year ) ;
const grossIncome = yearInvoices . reduce ( ( s , inv ) = > s + inv . totalAmount , 0 ) ;
const taxCollected = yearInvoices . reduce ( ( s , inv ) = > s + inv . totalAmount * ( inv . taxRate ? ? 0 ) , 0 ) ;
const totalExpenses = yearExpenses . reduce ( ( s , exp ) = > s + exp . amount , 0 ) ;
const deductibleExpenses = yearExpenses
. filter ( ( exp ) = > ( exp as typeof exp & { taxDeductible? : boolean } ) . taxDeductible )
. reduce ( ( s , exp ) = > s + exp . amount , 0 ) ;
const netProfit = grossIncome - deductibleExpenses ;
const seTaxBase = Math . max ( 0 , netProfit ) * 0.9235 ;
const selfEmploymentTax = seTaxBase * 0.153 ;
const taxableIncome = Math . max ( 0 , netProfit - selfEmploymentTax / 2 ) ;
const federalEstimate = taxableIncome * 0.22 ;
const totalEstimated = selfEmploymentTax + federalEstimate ;
const quarters = [ 1 , 2 , 3 , 4 ] . map ( ( q ) = > {
const qMonths = [ ( q - 1 ) * 3 , ( q - 1 ) * 3 + 1 , ( q - 1 ) * 3 + 2 ] ;
return {
label : ` Q ${ q } ` ,
income : yearInvoices.filter ( ( inv ) = > qMonths . includes ( new Date ( inv . issueDate ) . getMonth ( ) ) ) . reduce ( ( s , inv ) = > s + inv . totalAmount , 0 ) ,
expenses : yearExpenses.filter ( ( exp ) = > qMonths . includes ( new Date ( exp . date ) . getMonth ( ) ) ) . reduce ( ( s , exp ) = > s + exp . amount , 0 ) ,
} ;
} ) ;
return { grossIncome , taxCollected , totalInvoiced : grossIncome + taxCollected , totalExpenses , deductibleExpenses , netProfit , selfEmploymentTax , federalEstimate , totalEstimated , quarters , yearInvoices , yearExpenses } ;
} , [ invoices , expenses , taxYear ] ) ;
const availableYears = useMemo ( ( ) = > {
const years = new Set < number > ( [ currentYear , currentYear - 1 ] ) ;
for ( const inv of invoices ) years . add ( new Date ( inv . issueDate ) . getFullYear ( ) ) ;
for ( const exp of expenses ) years . add ( new Date ( exp . date ) . getFullYear ( ) ) ;
return Array . from ( years ) . sort ( ( a , b ) = > b - a ) ;
} , [ invoices , expenses , currentYear ] ) ;
const avgInvoice = invoices . length > 0
? ( overviewData ? . totalRevenue ? ? 0 ) / ( invoices . filter ( ( i ) = > getEffectiveInvoiceStatus ( i . status as StoredInvoiceStatus , i . dueDate ) === "paid" ) . length || 1 )
: 0 ;
function exportCSV() {
const rows : string [ ] = [
` Tax Year ${ taxYear } - Income & Expense Report ` ,
` Generated: ${ new Date ( ) . toLocaleDateString ( "en-US" , { year : "numeric" , month : "long" , day : "numeric" } )} ` ,
"" ,
"INCOME (Paid Invoices)" ,
"Date,Invoice #,Client,Subtotal,Tax Rate,Tax Amount,Total" ,
. . . taxData . yearInvoices . map ( ( inv ) = > {
const taxAmt = inv . totalAmount * ( inv . taxRate ? ? 0 ) ;
return [ new Date ( inv . issueDate ) . toLocaleDateString ( "en-US" ) , inv . invoiceNumber , ` " ${ inv . client ? . name ? ? "" } " ` , inv . totalAmount . toFixed ( 2 ) , ` ${ ( ( inv . taxRate ? ? 0 ) * 100 ) . toFixed ( 1 ) } % ` , taxAmt . toFixed ( 2 ) , ( inv . totalAmount + taxAmt ) . toFixed ( 2 ) ] . join ( "," ) ;
} ) ,
` ,,Totals, ${ taxData . grossIncome . toFixed ( 2 ) } ,, ${ taxData . taxCollected . toFixed ( 2 ) } , ${ taxData . totalInvoiced . toFixed ( 2 ) } ` ,
"" ,
"EXPENSES" ,
"Date,Description,Category,Amount,Currency,Billable,Reimbursable,Tax Deductible" ,
. . . taxData . yearExpenses . map ( ( exp ) = > [
new Date ( exp . date ) . toLocaleDateString ( "en-US" ) ,
` " ${ exp . description } " ` ,
` " ${ exp . category ? ? "" } " ` ,
exp . amount . toFixed ( 2 ) ,
exp . currency ,
exp . billable ? "Yes" : "No" ,
exp . reimbursable ? "Yes" : "No" ,
( exp as typeof exp & { taxDeductible? : boolean } ) . taxDeductible ? "Yes" : "No" ,
] . join ( "," ) ) ,
` ,,Totals, ${ taxData . totalExpenses . toFixed ( 2 ) } ,,,,"Deductible: ${ taxData . deductibleExpenses . toFixed ( 2 ) } " ` ,
"" ,
"TAX SUMMARY" ,
` Gross Income, ${ taxData . grossIncome . toFixed ( 2 ) } ` ,
` Tax Collected, ${ taxData . taxCollected . toFixed ( 2 ) } ` ,
` Deductible Expenses, ${ taxData . deductibleExpenses . toFixed ( 2 ) } ` ,
` Net Profit, ${ taxData . netProfit . toFixed ( 2 ) } ` ,
` Est. Self-Employment Tax (15.3%), ${ taxData . selfEmploymentTax . toFixed ( 2 ) } ` ,
` Est. Federal Income Tax (22%), ${ taxData . federalEstimate . toFixed ( 2 ) } ` ,
` Total Estimated Tax, ${ taxData . totalEstimated . toFixed ( 2 ) } ` ,
] ;
const blob = new Blob ( [ rows . join ( "\n" ) ] , { type : "text/csv;charset=utf-8;" } ) ;
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( "a" ) ;
a . href = url ;
a . download = ` tax-report- ${ taxYear } .csv ` ;
a . click ( ) ;
URL . revokeObjectURL ( url ) ;
}
if ( isLoading ) {
return (
< div className = "page-enter space-y-6" >
< PageHeader title = "Reports" description = "Revenue and invoice analytics" variant = "gradient" / >
< PageHeader title = "Reports" description = "Revenue and tax analytics" variant = "gradient" / >
< div className = "grid grid-cols-2 gap-4 sm:grid-cols-4" >
{ [ . . . Array ( 4 ) ] . map ( ( _ , i ) = > < div key = { i } className = "bg-muted h-24 animate-pulse rounded-xl" / > ) }
< / div >
@@ -97,165 +191,264 @@ export default function ReportsPage() {
) ;
}
const avgInvoice = invoices . length > 0 ? ( reportData ? . totalRevenue ? ? 0 ) / invoices . filter ( ( i ) = > getEffectiveInvoiceStatus ( i . status as StoredInvoiceStatus , i . dueDate ) === "paid" ) . length || 0 : 0 ;
return (
< div className = "page-enter space-y-6 pb-6" >
< PageHeader title = "Reports" description = "Revenue and invoice analytics" variant = "gradient" / >
< PageHeader title = "Reports" description = "Revenue and tax analytics" variant = "gradient" / >
{ /* KPI cards */ }
< div className = "grid grid-cols-2 gap-4 sm: grid-cols-4 " >
< Card >
< CardContent className = "p-4" >
< div className = "flex items-center gap-2" >
< div className = "bg-primary/10 rounded p-1.5" >
< DollarSign className = "text-primary h-4 w-4" / >
< / div >
< p className = "text-muted-foreground text-xs font-medium" > Total Revenue < / p >
< / div >
< p className = "mt-2 text-2xl font-bold" > { formatCurrency ( reportData ? . totalRevenue ? ? 0 ) } < / p >
< / CardContent >
< / Card >
< Card >
< CardContent className = "p-4" >
< div className = "flex items-center gap-2" >
< div className = "bg-yellow-500/10 rounded p-1.5" >
< Clock className = "h-4 w-4 text-yellow-500" / >
< / div >
< p className = "text-muted-foreground text-xs font-medium" > Pending < / p >
< / div >
< p className = "mt-2 text-2xl font-bold" > { formatCurrency ( reportData ? . totalPending ? ? 0 ) } < / p >
< / CardContent >
< / Card >
< Card >
< CardContent className = "p-4" >
< div className = "flex items-center gap-2" >
< div className = "bg-blue-500/10 rounded p-1.5" >
< TrendingUp className = "h-4 w-4 text-blue-500" / >
< / div >
< p className = "text-muted-foreground text-xs font-medium" > Avg Invoice < / p >
< / div >
< p className = "mt-2 text-2xl font-bold" > { formatCurrency ( isNaN ( avgInvoice ) ? 0 : avgInvoice ) } < / p >
< / CardContent >
< / Card >
< Card >
< CardContent className = "p-4" >
< div className = "flex items-center gap-2" >
< div className = "bg-green-500/10 rounded p-1.5" >
< Users className = "h-4 w-4 text-green-500" / >
< / div >
< p className = "text-muted-foreground text-xs font-medium" > Total Hours < / p >
< / div >
< p className = "mt-2 text-2xl font-bold" > { ( reportData ? . totalHours ? ? 0 ) . toFixed ( 1 ) } h < / p >
< / CardContent >
< / Card >
< / div >
< Tabs defaultValue = "overview" >
< TabsList className = "grid w-full grid-cols-2 " >
< TabsTrigger value = "overview" > < TrendingUp className = "mr-1.5 h-4 w-4" / > Overview < / TabsTrigger >
< TabsTrigger value = "tax" > < FileText className = "mr-1.5 h-4 w-4" / > Tax Summary < / TabsTrigger >
< / TabsList >
{ /* Revenue trend chart */ }
< Card >
< CardHeader >
< CardTitle className = "flex items-center gap-2" >
< TrendingUp className = "h-5 w-5" / > Revenue ( Last 12 Months )
< / CardTitle >
< / CardHeader >
< CardContent >
< div className = "h-48 w-full md:h-64" >
< ResponsiveContainer width = "100%" height = "100%" >
< AreaChart data = { reportData ? . revenueByMonth ? ? [ ] } >
< defs >
< linearGradient id = "revenueGrad" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
< stop offset = "5%" stopColor = "hsl(142, 76%, 36%)" stopOpacity = { 0.3 } / >
< stop offset = "95%" stopColor = "hsl(142, 76%, 36%)" stopOpacity = { 0.02 } / >
< / linearGradient >
< / defs >
< CartesianGrid strokeDasharray = "3 3" className = "stroke-border" / >
< XAxis dataKey = "month" tick = { { fontSize : 11 , fill : "hsl(var(--muted-foreground))" } } axisLine = { false } tickLine = { false } / >
< YAxis tick = { { fontSize : 11 , fill : "hsl(var(--muted-foreground))" } } axisLine = { false } tickLine = { false } tickFormatter = { ( v : number ) = > ` $ ${ v >= 1000 ? ` ${ ( v / 1000 ) . toFixed ( 0 ) } k ` : v } ` } / >
< Tooltip formatter = { ( v : number ) = > [ formatCurrency ( v ) , "Revenue" ] } contentStyle = { { background : "hsl(var(--card))" , border : "1px solid hsl(var(--border))" , borderRadius : "8px" , fontSize : 12 } } / >
< Area type = "monotone" dataKey = "revenue" stroke = "hsl(142, 76%, 36%)" fill = "url(#revenueGrad)" strokeWidth = { 2 } dot = { false } / >
< / AreaChart >
< / ResponsiveContainer >
{ /* ── OVERVIEW TAB ── */ }
< TabsContent value = "overview" className = "mt-4 space-y-6" >
< div className = "grid grid-cols-2 gap-4 sm:grid-cols-4" >
< Card >
< CardContent className = "p-4" >
< div className = "flex items-center gap-2" >
< div className = "bg-primary/10 rounded p-1.5" > < DollarSign className = "text-primary h-4 w-4" / > < / div >
< p className = "text-muted-foreground text-xs font-medium" > Total Revenue < / p >
< / div >
< p className = "mt-2 text-2xl font-bold" > { formatCurrency ( overviewData ? . totalRevenue ? ? 0 ) } < / p >
< / CardContent >
< / Card >
< Card >
< CardContent className = "p-4" >
< div className = "flex items-center gap-2" >
< div className = "bg-yellow-500/10 rounded p-1.5" > < Clock className = "h-4 w-4 text-yellow-500" / > < / div >
< p className = "text-muted-foreground text-xs font-medium" > Pending < / p >
< / div >
< p className = "mt-2 text-2xl font-bold" > { formatCurrency ( overviewData ? . totalPending ? ? 0 ) } < / p >
< / CardContent >
< / Card >
< Card >
< CardContent className = "p-4" >
< div className = "flex items-center gap-2" >
< div className = "bg-blue-500/10 rounded p-1.5" > < TrendingUp className = "h-4 w-4 text-blue-500" / > < / div >
< p className = "text-muted-foreground text-xs font-medium" > Avg Invoice < / p >
< / div >
< p className = "mt-2 text-2xl font-bold" > { formatCurrency ( isNaN ( avgInvoice ) ? 0 : avgInvoice ) } < / p >
< / CardContent >
< / Card >
< Card >
< CardContent className = "p-4" >
< div className = "flex items-center gap-2" >
< div className = "bg-green-500/10 rounded p-1.5" > < Users className = "h-4 w-4 text-green-500" / > < / div >
< p className = "text-muted-foreground text-xs font-medium" > Total Hours < / p >
< / div >
< p className = "mt-2 text-2xl font-bold" > { ( overviewData ? . totalHours ? ? 0 ) . toFixed ( 1 ) } h < / p >
< / CardContent >
< / Card >
< / div >
< / CardContent >
< / Card >
< div className = "grid grid-cols-1 gap-6 lg:grid-cols-2" >
{ /* Top clients */ }
< Card >
< CardHeader >
< CardTitle className = "flex items-center gap-2" >
< Users className = "h-5 w-5" / > Top Clients by Revenue
< / CardTitle >
< / CardHeader >
< CardContent >
{ ! reportData ? . topClients . length ? (
< p className = "text-muted-foreground py-6 text-center text-sm" > No paid invoices yet . < / p >
) : (
< div className = "h-48 md:h-56" >
< Card >
< CardHeader >
< CardTitle className = "flex items-center gap-2" > < TrendingUp className = "h-5 w-5" / > Revenue ( Last 12 Months ) < / CardTitle >
< / CardHeader >
< CardContent >
< div className = "h-48 w-full md:h-64" >
< ResponsiveContainer width = "100%" height = "100%" >
< BarChart data = { reportData . topClients } layout = "vertical" >
< XAxis type = "number" tick = { { fontSize : 11 , fill : "hsl(var(--muted-foreground))" } } axisLine = { false } tickLine = { false } tickFormatter = { ( v : number ) = > ` $ ${ v >= 1000 ? ` ${ ( v / 1000 ) . toFixed ( 0 ) } k ` : v } ` } / >
< YAxis type = "category " dataKey = "name " tick = { { fontSize : 11 , fill : "hsl(var(--muted-foreground))" } } axisLine = { false } tickLine = { false } width = { 80 } / >
< AreaChart data = { overviewData ? . revenueByMonth ? ? [ ] } >
< defs >
< linearGradient id = "revenueGrad " x1 = "0 " y1 = "0" x2 = "0" y2 = "1" >
< stop offset = "5%" stopColor = "hsl(142, 76%, 36%)" stopOpacity = { 0.3 } / >
< stop offset = "95%" stopColor = "hsl(142, 76%, 36%)" stopOpacity = { 0.02 } / >
< / linearGradient >
< / defs >
< CartesianGrid strokeDasharray = "3 3" className = "stroke-border" / >
< XAxis dataKey = "month" tick = { { fontSize : 11 , fill : "hsl(var(--muted-foreground))" } } axisLine = { false } tickLine = { false } / >
< YAxis tick = { { fontSize : 11 , fill : "hsl(var(--muted-foreground))" } } axisLine = { false } tickLine = { false } tickFormatter = { ( v : number ) = > ` $ ${ v >= 1000 ? ` ${ ( v / 1000 ) . toFixed ( 0 ) } k ` : v } ` } / >
< Tooltip formatter = { ( v : number ) = > [ formatCurrency ( v ) , "Revenue" ] } contentStyle = { { background : "hsl(var(--card))" , border : "1px solid hsl(var(--border))" , borderRadius : "8px" , fontSize : 12 } } / >
< Bar dataKey = "revenue" fill = "hsl(142, 76%, 36%)" radius = { [ 0 , 4 , 4 , 0 ] } / >
< Area type = "monotone" dataKey = "revenue" stroke = "hsl(142, 76%, 36%)" fill = "url(#revenueGrad)" strokeWidth = { 2 } dot = { false } / >
< / AreaChart >
< / ResponsiveContainer >
< / div >
< / CardContent >
< / Card >
< div className = "grid grid-cols-1 gap-6 lg:grid-cols-2" >
< Card >
< CardHeader >
< CardTitle className = "flex items-center gap-2" > < Users className = "h-5 w-5" / > Top Clients by Revenue < / CardTitle >
< / CardHeader >
< CardContent >
{ ! overviewData ? . topClients . length ? (
< p className = "text-muted-foreground py-6 text-center text-sm" > No paid invoices yet . < / p >
) : (
< div className = "h-48 md:h-56" >
< ResponsiveContainer width = "100%" height = "100%" >
< BarChart data = { overviewData . topClients } layout = "vertical" >
< XAxis type = "number" tick = { { fontSize : 11 , fill : "hsl(var(--muted-foreground))" } } axisLine = { false } tickLine = { false } tickFormatter = { ( v : number ) = > ` $ ${ v >= 1000 ? ` ${ ( v / 1000 ) . toFixed ( 0 ) } k ` : v } ` } / >
< YAxis type = "category" dataKey = "name" tick = { { fontSize : 11 , fill : "hsl(var(--muted-foreground))" } } axisLine = { false } tickLine = { false } width = { 80 } / >
< Tooltip formatter = { ( v : number ) = > [ formatCurrency ( v ) , "Revenue" ] } contentStyle = { { background : "hsl(var(--card))" , border : "1px solid hsl(var(--border))" , borderRadius : "8px" , fontSize : 12 } } / >
< Bar dataKey = "revenue" fill = "hsl(142, 76%, 36%)" radius = { [ 0 , 4 , 4 , 0 ] } / >
< / BarChart >
< / ResponsiveContainer >
< / div >
) }
< / CardContent >
< / Card >
< Card >
< CardHeader > < CardTitle > Invoice Status Breakdown < / CardTitle > < / CardHeader >
< CardContent className = "space-y-3" >
{ Object . entries ( overviewData ? . statusCount ? ? { } ) . map ( ( [ status , count ] ) = > (
< div key = { status } className = "flex items-center justify-between" >
< StatusBadge status = { status as never } / >
< div className = "flex items-center gap-3" >
< div className = "bg-muted h-2 w-24 overflow-hidden rounded-full sm:w-32" >
< div className = "bg-primary h-full rounded-full" style = { { width : ` ${ invoices . length ? ( count / invoices . length ) * 100 : 0 } % ` } } / >
< / div >
< span className = "text-muted-foreground w-8 text-right text-sm" > { count } < / span >
< / div >
< / div >
) ) }
{ invoices . length === 0 && < p className = "text-muted-foreground py-6 text-center text-sm" > No invoices yet . < / p > }
< / CardContent >
< / Card >
< / div >
{ stats && (
< Card >
< CardHeader > < CardTitle > Recent Activity < / CardTitle > < / CardHeader >
< CardContent >
< div className = "divide-y" >
{ stats . recentInvoices . map ( ( inv ) = > (
< div key = { inv . id } className = "flex items-center justify-between py-3" >
< div >
< p className = "font-medium" > { inv . client ? . name ? ? "—" } < / p >
< p className = "text-muted-foreground text-xs" > { new Date ( inv . issueDate ) . toLocaleDateString ( "en-US" , { month : "short" , day : "numeric" , year : "numeric" } ) } < / p >
< / div >
< div className = "flex items-center gap-3" >
< StatusBadge status = { getEffectiveInvoiceStatus ( inv . status as StoredInvoiceStatus , inv . dueDate ) as never } / >
< p className = "font-semibold" > { formatCurrency ( inv . totalAmount ) } < / p >
< / div >
< / div >
) ) }
< / div >
< / CardContent >
< / Card >
) }
< / TabsContent >
{ /* ── TAX SUMMARY TAB ── */ }
< TabsContent value = "tax" className = "mt-4 space-y-6" >
< div className = "flex items-center justify-between gap-4" >
< div className = "flex items-center gap-3" >
< span className = "text-sm font-medium" > Tax Year < / span >
< Select value = { taxYear } onValueChange = { setTaxYear } >
< SelectTrigger className = "w-28" > < SelectValue / > < / SelectTrigger >
< SelectContent >
{ availableYears . map ( ( y ) = > < SelectItem key = { y } value = { String ( y ) } > { y } < / SelectItem > ) }
< / SelectContent >
< / Select >
< / div >
< Button variant = "outline" onClick = { exportCSV } className = "gap-2" >
< Download className = "h-4 w-4" / > Export CSV
< / Button >
< / div >
{ /* Income */ }
< Card >
< CardHeader >
< CardTitle className = "flex items-center gap-2" > < DollarSign className = "h-5 w-5" / > Income < / CardTitle >
< / CardHeader >
< CardContent className = "space-y-3" >
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Gross Income ( paid invoices ) < / span >
< span className = "font-medium" > { formatCurrency ( taxData . grossIncome ) } < / span >
< / div >
{ taxData . taxCollected > 0 && (
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Tax Collected from Clients < / span >
< span className = "font-medium" > { formatCurrency ( taxData . taxCollected ) } < / span >
< / div >
) }
< Separator / >
< div className = "flex justify-between font-medium" >
< span > Total Invoiced ( inc . tax ) < / span >
< span > { formatCurrency ( taxData . totalInvoiced ) } < / span >
< / div >
< / CardContent >
< / Card >
{ /* Expenses */ }
< Card >
< CardHeader >
< CardTitle className = "flex items-center gap-2" > < Receipt className = "h-5 w-5" / > Expenses & Deductions < / CardTitle >
< / CardHeader >
< CardContent className = "space-y-3" >
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Total Expenses < / span >
< span className = "font-medium" > { formatCurrency ( taxData . totalExpenses ) } < / span >
< / div >
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Tax - Deductible Expenses < / span >
< span className = "font-medium text-green-600" > { formatCurrency ( taxData . deductibleExpenses ) } < / span >
< / div >
{ taxData . totalExpenses > 0 && taxData . deductibleExpenses === 0 && (
< p className = "text-muted-foreground text-xs" > Mark expenses as "Tax Deductible" in the Expenses page to include them here . < / p >
) }
< / CardContent >
< / Card >
{ /* Estimated tax */ }
< Card >
< CardHeader >
< CardTitle className = "flex items-center gap-2" > < FileText className = "h-5 w-5" / > Estimated Tax Liability < / CardTitle >
< / CardHeader >
< CardContent className = "space-y-3" >
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Net Profit ( income − deductible expenses ) < / span >
< span className = "font-medium" > { formatCurrency ( taxData . netProfit ) } < / span >
< / div >
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Self - Employment Tax ( 15.3 % on 92.35 % of net ) < / span >
< span className = "font-medium" > { formatCurrency ( taxData . selfEmploymentTax ) } < / span >
< / div >
< div className = "flex justify-between text-sm" >
< span className = "text-muted-foreground" > Federal Income Tax ( est . 22 % bracket ) < / span >
< span className = "font-medium" > { formatCurrency ( taxData . federalEstimate ) } < / span >
< / div >
< Separator / >
< div className = "flex justify-between text-lg font-bold" >
< span > Total Estimated Tax < / span >
< span className = "text-destructive" > { formatCurrency ( taxData . totalEstimated ) } < / span >
< / div >
< p className = "text-muted-foreground text-xs pt-1" >
Assumes US self - employment tax rules and the 22 % federal bracket . Consult a tax professional for accurate filing .
< / p >
< / CardContent >
< / Card >
{ /* Quarterly chart */ }
< Card >
< CardHeader > < CardTitle > Quarterly Breakdown < / CardTitle > < / CardHeader >
< CardContent >
< div className = "h-48 md:h-64" >
< ResponsiveContainer width = "100%" height = "100%" >
< BarChart data = { taxData . quarters } >
< CartesianGrid strokeDasharray = "3 3" className = "stroke-border" / >
< XAxis dataKey = "label" tick = { { fontSize : 11 , fill : "hsl(var(--muted-foreground))" } } axisLine = { false } tickLine = { false } / >
< YAxis tick = { { fontSize : 11 , fill : "hsl(var(--muted-foreground))" } } axisLine = { false } tickLine = { false } tickFormatter = { ( v : number ) = > ` $ ${ v >= 1000 ? ` ${ ( v / 1000 ) . toFixed ( 0 ) } k ` : v } ` } / >
< Tooltip
formatter = { ( v : number , name : string ) = > [ formatCurrency ( v ) , name === "income" ? "Income" : "Expenses" ] }
contentStyle = { { background : "hsl(var(--card))" , border : "1px solid hsl(var(--border))" , borderRadius : "8px" , fontSize : 12 } }
/ >
< Bar dataKey = "income" name = "income" fill = "hsl(142, 76%, 36%)" radius = { [ 4 , 4 , 0 , 0 ] } / >
< Bar dataKey = "expenses" name = "expenses" fill = "hsl(0, 84%, 60%)" radius = { [ 4 , 4 , 0 , 0 ] } opacity = { 0.75 } / >
< / BarChart >
< / ResponsiveContainer >
< / div >
) }
< / CardContent >
< / Card >
{ /* Invoice status breakdown */ }
< Card >
< CardHeader >
< CardTitle > Invoice Status Breakdown < / CardTitle >
< / CardHeader >
< CardContent className = "space-y-3" >
{ Object . entries ( reportData ? . statusCount ? ? { } ) . map ( ( [ status , count ] ) = > (
< div key = { status } className = "flex items-center justify-between" >
< StatusBadge status = { status as never } / >
< div className = "flex items-center gap-3" >
< div className = "bg-muted h-2 w-24 overflow-hidden rounded-full sm:w-32" >
< div
className = "bg-primary h-full rounded-full"
style = { { width : ` ${ invoices . length ? ( count / invoices . length ) * 100 : 0 } % ` } }
/ >
< / div >
< span className = "text-muted-foreground w-8 text-right text-sm" > { count } < / span >
< / div >
< div className = "mt-2 flex justify-center gap-6 text-xs text-muted-foreground" >
< span className = "flex items-center gap-1.5" > < span className = "inline-block h-2.5 w-2.5 rounded-sm bg-green-600" / > Income < / span >
< span className = "flex items-center gap-1.5" > < span className = "inline-block h-2.5 w-2.5 rounded-sm bg-red-500/75" / > Expenses < / span >
< / div >
) ) }
{ invoices . length === 0 && (
< p className = "text-muted-foreground py-6 text-center text-sm" > No invoices yet . < / p >
) }
< / CardContent >
< / Card >
< / div >
{ /* Monthly stats table */ }
{ stats && (
< Card >
< CardHeader >
< CardTitle > Recent Activity < / CardTitle >
< / CardHeader >
< CardContent >
< div className = "divide-y" >
{ stats . recentInvoices . map ( ( inv ) = > (
< div key = { inv . id } className = "flex items-center justify-between py-3" >
< div >
< p className = "font-medium" > { inv . client ? . name ? ? "—" } < / p >
< p className = "text-muted-foreground text-xs" > { new Date ( inv . issueDate ) . toLocaleDateString ( "en-US" , { month : "short" , day : "numeric" , year : "numeric" } ) } < / p >
< / div >
< div className = "flex items-center gap-3" >
< StatusBadge status = { getEffectiveInvoiceStatus ( inv . status as StoredInvoiceStatus , inv . dueDate ) as never } / >
< p className = "font-semibold" > { formatCurrency ( inv . totalAmount ) } < / p >
< / div >
< / div >
) ) }
< / div >
< / CardContent >
< / Card >
) }
< / CardContent >
< / Card >
< / TabsContent >
< / Tabs >
< / div >
) ;
}