@@ -0,0 +1,257 @@
'use client'
import { useEffect , useState } from 'react'
import { CartesianGrid , Line , LineChart , XAxis , YAxis } from 'recharts'
import { Button } from '@/components/ui/button'
import { ChartContainer , ChartTooltip , ChartTooltipContent , type ChartConfig } from '@/components/ui/chart'
import { Input } from '@/components/ui/input'
import { Skeleton } from '@/components/ui/skeleton'
import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from '@/components/ui/table'
import { dayEndReports , getApiError } from '@/lib/api/index'
import type { DayEndDailyResponse , DayEndResponse , DayEndRow } from '@/lib/api/reports'
import { exportPdf , type ExportColumn } from '@/lib/export'
import { daysFromToday , fmtCompact , fmtMoney , fmtNum , fmtShortDay } from '@/app/(dashboard)/network/warehouses/_components/dashboard/format'
import { EmptyNote , Metric , Section } from '@/app/(dashboard)/network/warehouses/_components/dashboard/shared'
const PRESETS = [
{ label : '7 days' , days : - 6 } ,
{ label : '30 days' , days : - 29 } ,
{ label : '92 days' , days : - 91 } ,
]
const trendConfig = {
totalSales : { label : 'Sales' , color : 'var(--chart-1)' } ,
expenses : { label : 'Expenses' , color : 'var(--chart-3)' } ,
income : { label : 'Income' , color : 'var(--chart-2)' } ,
} satisfies ChartConfig
const pdfColumns : ExportColumn < DayEndRow > [ ] = [
{ header : 'Date' , value : r = > r . date } ,
{ header : 'Sales' , value : r = > fmtNum ( r . totalSales ) , numeric : true } ,
{ header : 'Collected' , value : r = > fmtNum ( r . collected ) , numeric : true } ,
{ header : 'Due' , value : r = > fmtNum ( r . due ) , numeric : true } ,
{ header : 'Cash' , value : r = > fmtNum ( r . cash ) , numeric : true } ,
{ header : 'Cheque' , value : r = > fmtNum ( r . cheque ) , numeric : true } ,
{ header : 'Credit' , value : r = > fmtNum ( r . credit ) , numeric : true } ,
{ header : 'Returns' , value : r = > fmtNum ( r . returns ) , numeric : true } ,
{ header : 'Expenses' , value : r = > fmtNum ( r . expenses ) , numeric : true } ,
{ header : 'Income' , value : r = > fmtNum ( r . income ) , numeric : true } ,
]
const incomeTone = ( v : number ) = > ( v < 0 ? 'danger' : undefined )
export default function DayEndReportPage() {
const [ date , setDate ] = useState ( ( ) = > daysFromToday ( 0 ) )
const [ day , setDay ] = useState < DayEndResponse | null > ( null )
const [ dayError , setDayError ] = useState < string | null > ( null )
const [ dayLoading , setDayLoading ] = useState ( true )
const [ from , setFrom ] = useState ( ( ) = > daysFromToday ( - 29 ) )
const [ to , setTo ] = useState ( ( ) = > daysFromToday ( 0 ) )
const [ daily , setDaily ] = useState < DayEndDailyResponse | null > ( null )
const [ dailyError , setDailyError ] = useState < string | null > ( null )
const [ dailyLoading , setDailyLoading ] = useState ( true )
useEffect ( ( ) = > {
if ( ! date ) return
let cancelled = false
setDayLoading ( true )
setDayError ( null )
dayEndReports . day ( { date } )
. then ( res = > { if ( ! cancelled ) setDay ( res ) } )
. catch ( err = > { if ( ! cancelled ) setDayError ( getApiError ( err ) ) } )
. finally ( ( ) = > { if ( ! cancelled ) setDayLoading ( false ) } )
return ( ) = > { cancelled = true }
} , [ date ] )
useEffect ( ( ) = > {
if ( ! from || ! to ) return
let cancelled = false
setDailyLoading ( true )
setDailyError ( null )
dayEndReports . daily ( { from , to } )
. then ( res = > { if ( ! cancelled ) setDaily ( res ) } )
. catch ( err = > { if ( ! cancelled ) setDailyError ( getApiError ( err ) ) } )
. finally ( ( ) = > { if ( ! cancelled ) setDailyLoading ( false ) } )
return ( ) = > { cancelled = true }
} , [ from , to ] )
const s = day ? . summary
const hasActivity = ! ! daily && daily . days . some ( d = > d . totalSales > 0 || d . expenses > 0 )
const downloadPdf = ( ) = > {
if ( ! daily ) return
void exportPdf (
'day-end-sales' ,
'Day-End Sales Report' ,
pdfColumns ,
[ . . . daily . days , daily . totals ] ,
` ${ from } to ${ to } ` ,
)
}
return (
< div className = "flex flex-col gap-6" >
< div className = "flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between" >
< div >
< h1 className = "text-2xl font-semibold text-foreground" > Day - End Sales < / h1 >
< p className = "text-sm text-muted-foreground mt-0.5" > Sales , collections , returns , expenses and income for a day . < / p >
< / div >
< div className = "flex items-center gap-2" >
< Input type = "date" value = { date } max = { daysFromToday ( 0 ) } onChange = { e = > setDate ( e . target . value ) } className = "w-40" aria-label = "Report date" / >
< / div >
< / div >
{ dayError && (
< div className = "rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive" > { dayError } < / div >
) }
{ ! dayError && ! day && dayLoading && < Skeleton className = "h-24 w-full" / > }
{ day && s && (
< div className = { dayLoading ? 'flex flex-col gap-6 opacity-60 transition-opacity' : 'flex flex-col gap-6' } >
< div className = "grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-5" >
< Metric label = "Total sales" value = { fmtMoney ( s . totalSales ) } sub = { ` ${ fmtNum ( s . invoiceCount ) } invoices ` } / >
< Metric label = "Collected" value = { fmtMoney ( s . collected ) } sub = { ` ${ fmtNum ( s . paymentCount ) } payments · cash + cheque ` } / >
< Metric label = "Due" value = { fmtMoney ( s . due ) } sub = "Unpaid on today's invoices" tone = { s . due > 0 ? 'warning' : undefined } / >
< Metric label = "Cash" value = { fmtMoney ( s . cash ) } sub = { ` ${ fmtNum ( day . byMethod . find ( m = > m . method === 'Cash' ) ? . count ? ? 0 ) } payments ` } / >
< Metric label = "Cheque" value = { fmtMoney ( s . cheque ) } sub = { ` ${ fmtNum ( day . byMethod . find ( m = > m . method === 'Cheque' ) ? . count ? ? 0 ) } payments ` } / >
< Metric label = "Credit" value = { fmtMoney ( s . credit ) } sub = "Extended, not received" / >
< Metric label = "Returns" value = { fmtMoney ( s . returns ) } / >
< Metric label = "Expenses" value = { fmtMoney ( s . expenses ) } / >
< Metric label = "Net cash" value = { fmtMoney ( s . netCash ) } sub = "Cash collected − cash expenses paid" tone = { incomeTone ( s . netCash ) } / >
< Metric label = "Income" value = { fmtMoney ( s . income ) } sub = "Sales − returns − expenses" tone = { incomeTone ( s . income ) } / >
< / div >
< Section title = "Expenses" description = "Posted and paid expenses billed on this day" >
{ day . expenses . length === 0 ? (
< EmptyNote > No expenses on this day . < / EmptyNote >
) : (
< Table >
< TableHeader >
< TableRow >
< TableHead > Category < / TableHead >
< TableHead > Payee < / TableHead >
< TableHead > Description < / TableHead >
< TableHead > Method < / TableHead >
< TableHead className = "text-right" > Amount < / TableHead >
< / TableRow >
< / TableHeader >
< TableBody >
{ day . expenses . map ( e = > (
< TableRow key = { e . id } >
< TableCell className = "font-medium" > { e . category } < / TableCell >
< TableCell > { e . payee ? ? '—' } < / TableCell >
< TableCell > { e . description ? ? '—' } < / TableCell >
< TableCell > { e . method ? ? 'Unpaid' } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtMoney ( e . amount ) } < / TableCell >
< / TableRow >
) ) }
< / TableBody >
< / Table >
) }
< / Section >
< / div >
) }
< div className = "flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between" >
< div >
< h2 className = "text-lg font-semibold text-foreground" > Day - wise income < / h2 >
< p className = "text-sm text-muted-foreground mt-0.5" > Up to 92 days . < / p >
< / div >
< div className = "flex flex-wrap items-center gap-2" >
{ PRESETS . map ( p = > (
< Button
key = { p . label }
variant = { from === daysFromToday ( p . days ) && to === daysFromToday ( 0 ) ? 'default' : 'outline' }
size = "sm"
onClick = { ( ) = > { setFrom ( daysFromToday ( p . days ) ) ; setTo ( daysFromToday ( 0 ) ) } }
>
{ p . label }
< / Button >
) ) }
< Input type = "date" value = { from } max = { to } onChange = { e = > setFrom ( e . target . value ) } className = "w-40" aria-label = "From date" / >
< Input type = "date" value = { to } min = { from } onChange = { e = > setTo ( e . target . value ) } className = "w-40" aria-label = "To date" / >
< Button size = "sm" variant = "outline" onClick = { downloadPdf } disabled = { ! daily || dailyLoading } > Download PDF < / Button >
< / div >
< / div >
{ dailyError && (
< div className = "rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive" > { dailyError } < / div >
) }
{ ! dailyError && ! daily && dailyLoading && < Skeleton className = "h-64 w-full" / > }
{ daily && (
< div className = { dailyLoading ? 'flex flex-col gap-6 opacity-60 transition-opacity' : 'flex flex-col gap-6' } >
< Section title = "Sales, expenses and income" description = "Per day over the selected range" >
{ ! hasActivity ? (
< EmptyNote > No sales or expenses in this period . < / EmptyNote >
) : (
< ChartContainer config = { trendConfig } className = "w-full" style = { { height : 280 } } >
< LineChart data = { daily . days } margin = { { left : 8 } } >
< CartesianGrid vertical = { false } / >
< XAxis dataKey = "date" tickFormatter = { fmtShortDay } tickLine = { false } axisLine = { false } minTickGap = { 16 } / >
< YAxis tickFormatter = { fmtCompact } tickLine = { false } axisLine = { false } width = { 48 } / >
< ChartTooltip content = { < ChartTooltipContent labelFormatter = { ( l ) = > fmtShortDay ( String ( l ) ) } formatter = { ( v ) = > fmtMoney ( Number ( v ) ) } / > } / >
< Line dataKey = "totalSales" type = "monotone" stroke = "var(--color-totalSales)" strokeWidth = { 2 } dot = { false } / >
< Line dataKey = "expenses" type = "monotone" stroke = "var(--color-expenses)" strokeWidth = { 2 } dot = { false } / >
< Line dataKey = "income" type = "monotone" stroke = "var(--color-income)" strokeWidth = { 2 } dot = { false } / >
< / LineChart >
< / ChartContainer >
) }
< / Section >
< Section title = "Daily breakdown" >
< Table >
< TableHeader >
< TableRow >
< TableHead > Date < / TableHead >
< TableHead className = "text-right" > Sales < / TableHead >
< TableHead className = "text-right" > Collected < / TableHead >
< TableHead className = "text-right" > Due < / TableHead >
< TableHead className = "text-right" > Cash < / TableHead >
< TableHead className = "text-right" > Cheque < / TableHead >
< TableHead className = "text-right" > Credit < / TableHead >
< TableHead className = "text-right" > Returns < / TableHead >
< TableHead className = "text-right" > Expenses < / TableHead >
< TableHead className = "text-right" > Income < / TableHead >
< / TableRow >
< / TableHeader >
< TableBody >
{ [ . . . daily . days ] . reverse ( ) . map ( r = > (
< TableRow key = { r . date } >
< TableCell className = "font-medium" > { r . date } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( r . totalSales ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( r . collected ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( r . due ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( r . cash ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( r . cheque ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( r . credit ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( r . returns ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( r . expenses ) } < / TableCell >
< TableCell className = { ` text-right tabular-nums ${ r . income < 0 ? 'text-destructive' : '' } ` } > { fmtNum ( r . income ) } < / TableCell >
< / TableRow >
) ) }
< TableRow className = "font-semibold" >
< TableCell > Total < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( daily . totals . totalSales ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( daily . totals . collected ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( daily . totals . due ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( daily . totals . cash ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( daily . totals . cheque ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( daily . totals . credit ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( daily . totals . returns ) } < / TableCell >
< TableCell className = "text-right tabular-nums" > { fmtNum ( daily . totals . expenses ) } < / TableCell >
< TableCell className = { ` text-right tabular-nums ${ daily . totals . income < 0 ? 'text-destructive' : '' } ` } > { fmtNum ( daily . totals . income ) } < / TableCell >
< / TableRow >
< / TableBody >
< / Table >
< / Section >
< / div >
) }
< / div >
)
}