22
33import { useState , useEffect , useMemo , useCallback } from "react" ;
44import Link from "next/link" ;
5+ import { useRouter , usePathname , useSearchParams } from "next/navigation" ;
56import { useRouter , useSearchParams } from "next/navigation" ;
67import { getFreighterPublicKey } from "@/lib/freighter" ;
78import { splitClient } from "@/lib/stellar" ;
@@ -12,6 +13,7 @@ import { useActivityFeed } from "@/hooks/useActivityFeed";
1213import InvoiceShareQRModal from "@/components/InvoiceShareQRModal" ;
1314import { InvoiceListSkeleton , SkeletonCard } from "@/components/Skeleton" ;
1415import BatchPayModal from "@/components/BatchPayModal" ;
16+ import StatusFilterChips from "@/components/invoice/StatusFilterChips" ;
1517import { setBulkReminders , type BulkReminderResult } from "@/lib/reminders" ;
1618import { getOrAssignDisplayNumber } from "@/lib/invoiceNumbering" ;
1719import { formatAmount } from "@stellar-split/sdk" ;
@@ -23,6 +25,9 @@ import {
2325 SORT_OPTIONS ,
2426 filterDashboardInvoices ,
2527 getDashboardPresetCounts ,
28+ INVOICE_STATUS_FILTERS ,
29+ type DashboardPresetId ,
30+ type InvoiceStatusFilter ,
2631 sortInvoices ,
2732 filterByDateRange ,
2833 type DashboardPresetId ,
@@ -111,6 +116,35 @@ export default function DashboardClient() {
111116 const [ compareMode , setCompareMode ] = useState ( false ) ;
112117 const [ compareSelected , setCompareSelected ] = useState < Set < string > > ( new Set ( ) ) ;
113118
119+ const router = useRouter ( ) ;
120+ const pathname = usePathname ( ) ;
121+ const searchParams = useSearchParams ( ) ;
122+
123+ const selectedStatuses = useMemo < InvoiceStatusFilter [ ] > ( ( ) => {
124+ const raw = searchParams . get ( "status" ) ;
125+ if ( ! raw ) return [ ] ;
126+ return raw
127+ . split ( "," )
128+ . filter ( ( s ) : s is InvoiceStatusFilter =>
129+ INVOICE_STATUS_FILTERS . includes ( s as InvoiceStatusFilter ) ,
130+ ) ;
131+ } , [ searchParams ] ) ;
132+
133+ const toggleStatus = ( status : InvoiceStatusFilter ) => {
134+ const next = selectedStatuses . includes ( status )
135+ ? selectedStatuses . filter ( ( s ) => s !== status )
136+ : [ ...selectedStatuses , status ] ;
137+ const params = new URLSearchParams ( searchParams . toString ( ) ) ;
138+ if ( next . length > 0 ) {
139+ params . set ( "status" , next . join ( "," ) ) ;
140+ } else {
141+ params . delete ( "status" ) ;
142+ }
143+ const qs = params . toString ( ) ;
144+ router . replace ( qs ? `${ pathname } ?${ qs } ` : pathname , { scroll : false } ) ;
145+ } ;
146+
147+ // Get wallet public key
114148 useEffect ( ( ) => {
115149 getFreighterPublicKey ( )
116150 . then ( setPublicKey )
@@ -284,6 +318,13 @@ export default function DashboardClient() {
284318 setBulkReminderResults ( null ) ;
285319 } ;
286320
321+ const clearFilters = ( ) => {
322+ setActivePreset ( "all" ) ;
323+ setSearchValue ( "" ) ;
324+ setNumericResult ( null ) ;
325+ if ( searchParams . get ( "status" ) ) {
326+ router . replace ( pathname , { scroll : false } ) ;
327+ }
287328 const toggleCompareSelect = ( id : string ) => {
288329 if ( compareSelected . size >= 2 && ! compareSelected . has ( id ) ) {
289330 return ; // Max 2 invoices
@@ -308,6 +349,24 @@ export default function DashboardClient() {
308349 }
309350 } ;
310351
352+ const pendingInvoices = invoices . filter ( ( inv ) => inv . status === "Pending" ) ;
353+ const selectedInvoices = invoices . filter ( ( inv ) => selected . has ( inv . id ) ) ;
354+ const presetCounts = useMemo (
355+ ( ) => getDashboardPresetCounts ( invoices , publicKey ) ,
356+ [ invoices , publicKey ] ,
357+ ) ;
358+ const visibleInvoices = useMemo (
359+ ( ) =>
360+ filterDashboardInvoices (
361+ invoices ,
362+ publicKey ,
363+ activePreset ,
364+ searchValue ,
365+ undefined ,
366+ selectedStatuses ,
367+ ) ,
368+ [ invoices , publicKey , activePreset , searchValue , selectedStatuses ] ,
369+ ) ;
311370 const handleScheduleBulkReminders = ( ) => {
312371 if ( ! reminderDateTime || reminderSelected . size === 0 ) return ;
313372 const results = setBulkReminders (
@@ -530,6 +589,9 @@ export default function DashboardClient() {
530589 </ div >
531590 </ div >
532591
592+ < StatusFilterChips selected = { selectedStatuses } onToggle = { toggleStatus } />
593+
594+ < div className = "flex flex-wrap items-center gap-2 mb-6" >
533595 { /* Summary Stats */ }
534596 { ! loading && invoices . length > 0 && (
535597 < div className = "grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8" >
@@ -567,6 +629,35 @@ export default function DashboardClient() {
567629 </ svg >
568630 Filters { isFiltered && < span className = "ml-1 rounded-full bg-indigo-600 text-white text-xs px-1.5 py-0.5" > on</ span > }
569631 </ button >
632+ { DASHBOARD_PRESETS . map ( ( preset ) => {
633+ const isActive = activePreset === preset . id ;
634+ const count = presetCounts [ preset . id ] ?? 0 ;
635+
636+ return (
637+ < button
638+ key = { preset . id }
639+ type = "button"
640+ onClick = { ( ) => handlePresetToggle ( preset . id ) }
641+ className = { `rounded-full px-3 py-1.5 text-sm font-semibold transition-colors ${
642+ isActive
643+ ? "bg-indigo-600 text-white"
644+ : "bg-gray-800 text-gray-300 hover:bg-gray-700"
645+ } `}
646+ aria-pressed = { isActive }
647+ >
648+ < span > { preset . label } </ span >
649+ < span className = "ml-2 rounded-full bg-white/15 px-2 py-0.5 text-xs" >
650+ { count }
651+ </ span >
652+ </ button >
653+ ) ;
654+ } ) }
655+ { ( activePreset !== "all" || searchValue . trim ( ) . length > 0 || selectedStatuses . length > 0 ) && (
656+ < button
657+ type = "button"
658+ onClick = { clearFilters }
659+ className = "rounded-full border border-gray-700 px-3 py-1.5 text-sm font-semibold text-gray-300 transition-colors hover:bg-gray-800"
660+ >
570661 { isFiltered && (
571662 < button type = "button" onClick = { clearFilters } className = "text-sm text-indigo-400 hover:text-indigo-300 transition-colors" >
572663 Clear filters
@@ -624,6 +715,11 @@ export default function DashboardClient() {
624715
625716 { /* Invoice grid */ }
626717 { loading && invoices . length === 0 ? (
718+ < div className = "flex flex-col gap-4" >
719+ { [ ...Array ( 8 ) ] . map ( ( _ , i ) => (
720+ < SkeletonCard key = { i } />
721+ ) ) }
722+ </ div >
627723 < InvoiceListSkeleton / >
628724 ) : invoices . length === 0 ? (
629725 < div className = "rounded-xl border border-gray-800 bg-gray-900/60 p-12 text-center" >
@@ -634,6 +730,17 @@ export default function DashboardClient() {
634730 </ Link >
635731 </ div >
636732 ) : visibleInvoices . length === 0 ? (
733+ < div className = "rounded-xl border border-gray-800 bg-gray-900/60 p-6 text-center" >
734+ < p className = "text-gray-400" >
735+ { activePreset !== "all"
736+ ? DASHBOARD_PRESETS . find ( ( preset ) => preset . id === activePreset )
737+ ?. emptyState ?? "No invoices match this view."
738+ : searchValue . trim ( )
739+ ? "No invoices match your search."
740+ : selectedStatuses . length > 0
741+ ? "No invoices match the selected status."
742+ : "No invoices found. Create your first one!" }
743+ </ p >
637744 < div className = "rounded-xl border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900/60 p-6 text-center" >
638745 < p className = "text-gray-400" > No invoices match the current filters.</ p >
639746 </ div >
0 commit comments