11import { AddAccountModal } from "@/components/custom/Modal/Accounts/AddAccountModal" ;
22import { useAccounts } from "@/components/hooks/useAccounts" ;
3+ import { useTransactions } from "@/components/hooks/useTransactions" ;
34import { Button } from "@/components/ui/button" ;
45import { Card , CardContent , CardHeader , CardTitle } from "@/components/ui/card" ;
6+ import {
7+ DropdownMenu ,
8+ DropdownMenuCheckboxItem ,
9+ DropdownMenuContent ,
10+ DropdownMenuSeparator ,
11+ DropdownMenuTrigger ,
12+ } from "@/components/ui/dropdown-menu" ;
513import {
614 Table ,
715 TableBody ,
@@ -11,14 +19,20 @@ import {
1119 TableRow ,
1220} from "@/components/ui/table" ;
1321import type { AccountAnalyticsListResponse } from "@/lib/models/analytics" ;
14- import { formatCurrency } from "@/lib/utils" ;
22+ import { formatCurrency , getTransactionColor } from "@/lib/utils" ;
23+ import { format } from "date-fns" ;
1524import { ChevronRight , Plus , Wallet } from "lucide-react" ;
16- import { useState } from "react" ;
25+ import { useTheme } from "next-themes" ;
26+ import { Fragment , useEffect , useState } from "react" ;
1727
1828interface AccountAnalyticsProps {
1929 data ?: AccountAnalyticsListResponse [ "account_analytics" ] ;
2030}
2131
32+ interface AccountTransactionsProps {
33+ accountId : number ;
34+ }
35+
2236// Color palette for different accounts
2337const accountColors = [
2438 "bg-purple-500" ,
@@ -33,12 +47,104 @@ const accountColors = [
3347 "bg-red-500" ,
3448] ;
3549
50+ function AccountTransactions ( { accountId } : AccountTransactionsProps ) {
51+ const { theme } = useTheme ( ) ;
52+ const { data, isLoading, error } = useTransactions ( {
53+ account_id : accountId ,
54+ page : 1 ,
55+ page_size : 5 ,
56+ sort_by : "date" ,
57+ sort_order : "desc" ,
58+ } ) ;
59+
60+ if ( isLoading ) {
61+ return (
62+ < TableRow >
63+ < TableCell colSpan = { 4 } className = "bg-muted/40" >
64+ < div className = "px-4 py-3 text-sm text-muted-foreground" >
65+ Loading latest transactions...
66+ </ div >
67+ </ TableCell >
68+ </ TableRow >
69+ ) ;
70+ }
71+
72+ if ( error ) {
73+ return (
74+ < TableRow >
75+ < TableCell colSpan = { 4 } className = "bg-muted/40" >
76+ < div className = "px-4 py-3 text-sm text-destructive" >
77+ Failed to load transactions.
78+ </ div >
79+ </ TableCell >
80+ </ TableRow >
81+ ) ;
82+ }
83+
84+ const transactions = data ?. transactions ?? [ ] ;
85+
86+ if ( transactions . length === 0 ) {
87+ return (
88+ < TableRow >
89+ < TableCell colSpan = { 4 } className = "bg-muted/40" >
90+ < div className = "px-4 py-3 text-sm text-muted-foreground" >
91+ No recent transactions for this account.
92+ </ div >
93+ </ TableCell >
94+ </ TableRow >
95+ ) ;
96+ }
97+
98+ return (
99+ < TableRow >
100+ < TableCell colSpan = { 4 } className = "bg-muted/40" >
101+ < div className = "px-4 py-3" >
102+ < div className = "text-xs uppercase tracking-wide text-muted-foreground" >
103+ Latest 5 transactions
104+ </ div >
105+ < div className = "mt-3 space-y-2" >
106+ { transactions . map ( ( transaction ) => (
107+ < div
108+ key = { transaction . id }
109+ className = "flex items-center justify-between gap-3 rounded-md border border-border/60 bg-background px-3 py-2"
110+ >
111+ < div className = "min-w-0" >
112+ < div className = "text-sm font-medium text-foreground" >
113+ { transaction . name }
114+ </ div >
115+ < div className = "text-xs text-muted-foreground" >
116+ { format ( new Date ( transaction . date ) , "MMM d, yyyy" ) }
117+ </ div >
118+ </ div >
119+ < div
120+ className = { `text-sm font-semibold ${ getTransactionColor (
121+ transaction . amount ,
122+ theme
123+ ) } `}
124+ >
125+ { formatCurrency ( Math . abs ( transaction . amount ) ) }
126+ </ div >
127+ </ div >
128+ ) ) }
129+ </ div >
130+ </ div >
131+ </ TableCell >
132+ </ TableRow >
133+ ) ;
134+ }
135+
36136export function AccountAnalytics ( { data } : AccountAnalyticsProps ) {
37137 const [ expandedAccounts , setExpandedAccounts ] = useState < Set < number > > (
38138 new Set ( )
39139 ) ;
40140 const [ isAddAccountModalOpen , setIsAddAccountModalOpen ] = useState ( false ) ;
41141 const { data : accountsData } = useAccounts ( ) ;
142+ const [ selectedAccountIds , setSelectedAccountIds ] = useState < number [ ] > ( [ ] ) ;
143+ const [ draftSelectedIds , setDraftSelectedIds ] = useState < number [ ] > ( [ ] ) ;
144+
145+ useEffect ( ( ) => {
146+ setDraftSelectedIds ( selectedAccountIds ) ;
147+ } , [ selectedAccountIds ] ) ;
42148
43149 if ( ! data || data . length === 0 ) {
44150 return (
@@ -85,8 +191,59 @@ export function AccountAnalytics({ data }: AccountAnalyticsProps) {
85191 ) ;
86192 }
87193
194+ const hasAccountList = ! ! accountsData && accountsData . length > 0 ;
195+ const showFilter = hasAccountList ;
196+ const allAccountIds = hasAccountList
197+ ? accountsData . map ( ( account ) => account . id )
198+ : [ ] ;
199+ const hasAllSelectedApplied =
200+ selectedAccountIds . length > 0 &&
201+ allAccountIds . every ( ( accountId ) => selectedAccountIds . includes ( accountId ) ) ;
202+ const hasAllSelectedDraft =
203+ draftSelectedIds . length > 0 &&
204+ allAccountIds . every ( ( accountId ) => draftSelectedIds . includes ( accountId ) ) ;
205+ const selectedAccountCount = selectedAccountIds . length ;
206+ const triggerLabel =
207+ selectedAccountCount === 0 || hasAllSelectedApplied
208+ ? "All accounts"
209+ : `${ selectedAccountCount } selected` ;
210+ const isDirty =
211+ selectedAccountIds . length !== draftSelectedIds . length ||
212+ selectedAccountIds . some (
213+ ( accountId ) => ! draftSelectedIds . includes ( accountId )
214+ ) ;
215+
216+ const toggleAccountSelection = ( accountId : number , checked : boolean ) => {
217+ if ( checked ) {
218+ if ( draftSelectedIds . includes ( accountId ) ) {
219+ return ;
220+ }
221+ setDraftSelectedIds ( [ ...draftSelectedIds , accountId ] ) ;
222+ return ;
223+ }
224+
225+ setDraftSelectedIds ( draftSelectedIds . filter ( ( id ) => id !== accountId ) ) ;
226+ } ;
227+
228+ const toggleSelectAll = ( ) => {
229+ if ( hasAllSelectedDraft ) {
230+ setDraftSelectedIds ( [ ] ) ;
231+ return ;
232+ }
233+
234+ setDraftSelectedIds ( allAccountIds ) ;
235+ } ;
236+
237+ const applyAccountFilter = ( ) => {
238+ setSelectedAccountIds ( draftSelectedIds ) ;
239+ } ;
240+
241+ const filteredData = selectedAccountIds . length
242+ ? data . filter ( ( account ) => selectedAccountIds . includes ( account . account_id ) )
243+ : data ;
244+
88245 // Calculate percentages and prepare data with account names and initial balances
89- const accountsWithBalances = data . map ( ( account , index ) => {
246+ const accountsWithBalances = filteredData . map ( ( account , index ) => {
90247 // Find the account info from accounts data
91248 const accountInfo = accountsData ?. find (
92249 ( acc ) => acc . id === account . account_id
@@ -137,22 +294,70 @@ export function AccountAnalytics({ data }: AccountAnalyticsProps) {
137294 < >
138295 < Card >
139296 < CardHeader >
140- < CardTitle className = "flex items-center justify-between" >
297+ < CardTitle className = "flex items-center justify-between gap-3 " >
141298 < div className = "flex items-center gap-2" >
142299 < span > Accounts</ span >
143300 < span className = "text-muted-foreground" > •</ span >
144301 < span > { formatCurrency ( totalBalance ) } </ span >
145302 </ div >
146- < Button
147- variant = "ghost"
148- size = "sm"
149- onClick = { ( ) => setIsAddAccountModalOpen ( true ) }
150- className = "h-8 w-8 p-0"
151- >
152- < Plus className = "h-4 w-4" />
153- </ Button >
303+ < div className = "flex items-center gap-2" >
304+ { showFilter && (
305+ < DropdownMenu >
306+ < DropdownMenuTrigger asChild >
307+ < Button variant = "outline" size = "sm" className = "h-8" >
308+ { triggerLabel }
309+ </ Button >
310+ </ DropdownMenuTrigger >
311+ < DropdownMenuContent align = "end" className = "w-56" >
312+ < div className = "flex items-center justify-between px-2 py-1.5 text-xs text-muted-foreground" >
313+ < span > Accounts</ span >
314+ < Button
315+ variant = "ghost"
316+ size = "sm"
317+ className = "h-6 px-2"
318+ onClick = { toggleSelectAll }
319+ >
320+ { hasAllSelectedDraft ? "Deselect all" : "Select all" }
321+ </ Button >
322+ </ div >
323+ < DropdownMenuSeparator />
324+ { accountsData ?. map ( ( account ) => (
325+ < DropdownMenuCheckboxItem
326+ key = { account . id }
327+ checked = { draftSelectedIds . includes ( account . id ) }
328+ onCheckedChange = { ( checked ) =>
329+ toggleAccountSelection ( account . id , Boolean ( checked ) )
330+ }
331+ onSelect = { ( event ) => event . preventDefault ( ) }
332+ >
333+ { account . name }
334+ </ DropdownMenuCheckboxItem >
335+ ) ) }
336+ < DropdownMenuSeparator />
337+ < div className = "flex justify-end px-2 py-2" >
338+ < Button
339+ size = "sm"
340+ onClick = { applyAccountFilter }
341+ disabled = { ! isDirty }
342+ >
343+ Apply
344+ </ Button >
345+ </ div >
346+ </ DropdownMenuContent >
347+ </ DropdownMenu >
348+ ) }
349+ < Button
350+ variant = "ghost"
351+ size = "sm"
352+ onClick = { ( ) => setIsAddAccountModalOpen ( true ) }
353+ className = "h-8 w-8 p-0"
354+ >
355+ < Plus className = "h-4 w-4" />
356+ </ Button >
357+ </ div >
154358 </ CardTitle >
155359 </ CardHeader >
360+
156361 < CardContent className = "space-y-6" >
157362 { /* Horizontal Progress Bar */ }
158363 < div className = "space-y-4" >
@@ -203,56 +408,65 @@ export function AccountAnalytics({ data }: AccountAnalyticsProps) {
203408 </ TableRow >
204409 </ TableHeader >
205410 < TableBody >
206- { accountsWithPercentages . map ( ( account ) => (
207- < TableRow key = { account . account_id } className = "border-b" >
208- < TableCell className = "w-12" >
209- < button
210- onClick = { ( ) =>
211- toggleAccountExpansion ( account . account_id )
212- }
213- className = "p-1 hover:bg-muted rounded transition-colors"
214- >
215- < ChevronRight
216- className = { `h-4 w-4 transition-transform ${
217- expandedAccounts . has ( account . account_id )
218- ? "rotate-90"
219- : ""
220- } `}
221- />
222- </ button >
223- </ TableCell >
224- < TableCell >
225- < span className = "font-medium" > { account . accountName } </ span >
226- </ TableCell >
227- < TableCell >
228- < div className = "flex items-center gap-2" >
229- < div className = "w-16 h-2 flex" >
230- { Array . from ( { length : 5 } ) . map ( ( _ , i ) => (
231- < div
232- key = { i }
233- className = { `flex-1 h-full ${
234- i < Math . floor ( account . percentage / 20 )
235- ? account . color
236- : "bg-gray-200"
411+ { accountsWithPercentages . map ( ( account ) => {
412+ const isExpanded = expandedAccounts . has ( account . account_id ) ;
413+
414+ return (
415+ < Fragment key = { account . account_id } >
416+ < TableRow className = "border-b" >
417+ < TableCell className = "w-12" >
418+ < button
419+ onClick = { ( ) =>
420+ toggleAccountExpansion ( account . account_id )
421+ }
422+ className = "p-1 hover:bg-muted rounded transition-colors"
423+ >
424+ < ChevronRight
425+ className = { `h-4 w-4 transition-transform ${
426+ isExpanded ? "rotate-90" : ""
237427 } `}
238- style = { {
239- marginRight : i < 4 ? "1px" : "0" ,
240- } }
241428 />
242- ) ) }
243- </ div >
244- < span className = "text-sm" >
245- { account . percentage . toFixed ( 2 ) } %
246- </ span >
247- </ div >
248- </ TableCell >
249- < TableCell className = "text-right" >
250- < span className = "font-medium" >
251- { formatCurrency ( account . absoluteBalance ) }
252- </ span >
253- </ TableCell >
254- </ TableRow >
255- ) ) }
429+ </ button >
430+ </ TableCell >
431+ < TableCell >
432+ < span className = "font-medium" >
433+ { account . accountName }
434+ </ span >
435+ </ TableCell >
436+ < TableCell >
437+ < div className = "flex items-center gap-2" >
438+ < div className = "w-16 h-2 flex" >
439+ { Array . from ( { length : 5 } ) . map ( ( _ , i ) => (
440+ < div
441+ key = { i }
442+ className = { `flex-1 h-full ${
443+ i < Math . floor ( account . percentage / 20 )
444+ ? account . color
445+ : "bg-gray-200"
446+ } `}
447+ style = { {
448+ marginRight : i < 4 ? "1px" : "0" ,
449+ } }
450+ />
451+ ) ) }
452+ </ div >
453+ < span className = "text-sm" >
454+ { account . percentage . toFixed ( 2 ) } %
455+ </ span >
456+ </ div >
457+ </ TableCell >
458+ < TableCell className = "text-right" >
459+ < span className = "font-medium" >
460+ { formatCurrency ( account . absoluteBalance ) }
461+ </ span >
462+ </ TableCell >
463+ </ TableRow >
464+ { isExpanded && (
465+ < AccountTransactions accountId = { account . account_id } />
466+ ) }
467+ </ Fragment >
468+ ) ;
469+ } ) }
256470 </ TableBody >
257471 </ Table >
258472 </ div >
0 commit comments