11
22'use client' ;
33
4+ import { useState , useMemo } from 'react' ;
45import Link from 'next/link' ;
56import { useInfiniteQuery } from '@tanstack/react-query' ;
67import { apiClient } from '@/lib/api-client' ;
@@ -65,30 +66,44 @@ async function fetchInvoicesPage(page: number, pageSize: number): Promise<Invoic
6566}
6667
6768export default function InvoicesPage ( ) {
69+ const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
70+ const [ statusFilter , setStatusFilter ] = useState < 'all' | 'pending' | 'paid' | 'overdue' | 'cancelled' > ( 'all' ) ;
6871 const pageSize = 20 ;
6972
70- const { data, isLoading, error, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery ( {
73+ const { data, isLoading, error, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = useInfiniteQuery ( {
7174 queryKey : [ 'invoices' , pageSize ] ,
7275 queryFn : async ( { pageParam } : { pageParam : number } ) =>
7376 fetchInvoicesPage ( pageParam , pageSize ) ,
7477 initialPageParam : 1 ,
7578 getNextPageParam : ( lastPage ) => ( lastPage . hasMore ? lastPage . page + 1 : undefined ) ,
7679 } ) ;
7780
78- const invoices = data ?. pages . flatMap ( ( page ) => page . items ) ?? [ ] ;
81+ const filteredInvoices = useMemo ( ( ) => {
82+ const invoices = data ?. pages . flatMap ( ( page ) => page . items ) ?? [ ] ;
83+ return invoices . filter ( ( invoice ) => {
84+ const matchesSearch =
85+ invoice . clientName . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ||
86+ ( invoice . invoiceNumber && invoice . invoiceNumber . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ) ||
87+ invoice . id . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ;
88+
89+ const matchesStatus = statusFilter === 'all' || invoice . status === statusFilter ;
90+
91+ return matchesSearch && matchesStatus ;
92+ } ) ;
93+ } , [ data , searchQuery , statusFilter ] ) ;
7994
8095 const getStatusColor = ( status : string ) => {
8196 switch ( status ) {
8297 case 'paid' :
83- return 'bg-green-100 text-green-800 ' ;
98+ return 'bg-emerald-50 text-emerald-700 ring-emerald-600/20 ' ;
8499 case 'pending' :
85- return 'bg-yellow-100 text-yellow-800 ' ;
100+ return 'bg-amber-50 text-amber-700 ring-amber-600/20 ' ;
86101 case 'overdue' :
87- return 'bg-red-100 text-red-800 ' ;
102+ return 'bg-rose-50 text-rose-700 ring-rose-600/20 ' ;
88103 case 'cancelled' :
89- return 'bg-gray-100 text-gray-800 ' ;
104+ return 'bg-slate-50 text-slate-600 ring-slate-500/10 ' ;
90105 default :
91- return 'bg-gray-100 text-gray-800 ' ;
106+ return 'bg-gray-50 text-gray-600 ring-gray-500/10 ' ;
92107 }
93108 } ;
94109
@@ -109,8 +124,42 @@ export default function InvoicesPage() {
109124 < p className = "mt-2 text-gray-600" > View and manage your invoices</ p >
110125 </ div >
111126
112- < div className = "mb-6" >
127+ < div className = "mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between " >
113128 < WalletAuthControls />
129+ < div className = "flex gap-2" >
130+ < button
131+ onClick = { ( ) => refetch ( ) }
132+ className = "inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
133+ >
134+ Refresh
135+ </ button >
136+ </ div >
137+ </ div >
138+
139+ { /* Filters */ }
140+ < div className = "mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2" >
141+ < div className = "relative" >
142+ < input
143+ type = "text"
144+ placeholder = "Search by invoice # or client..."
145+ value = { searchQuery }
146+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
147+ className = "block w-full rounded-md border-0 py-2 pl-3 pr-10 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
148+ />
149+ </ div >
150+ < div >
151+ < select
152+ value = { statusFilter }
153+ onChange = { ( e ) => setStatusFilter ( e . target . value as 'all' | 'pending' | 'paid' | 'overdue' | 'cancelled' ) }
154+ className = "block w-full rounded-md border-0 py-2 pl-3 pr-10 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
155+ >
156+ < option value = "all" > All Statuses</ option >
157+ < option value = "pending" > Pending</ option >
158+ < option value = "paid" > Paid</ option >
159+ < option value = "overdue" > Overdue</ option >
160+ < option value = "cancelled" > Cancelled</ option >
161+ </ select >
162+ </ div >
114163 </ div >
115164
116165 { /* Error State */ }
@@ -129,9 +178,10 @@ export default function InvoicesPage() {
129178 < div key = { i } className = "h-20 animate-pulse rounded-lg bg-gray-200" />
130179 ) ) }
131180 </ div >
132- ) : invoices . length === 0 ? (
133- < div className = "text-center" >
134- < p className = "text-gray-500" > No invoices found</ p >
181+ ) : filteredInvoices . length === 0 ? (
182+ < div className = "rounded-lg border-2 border-dashed border-gray-300 p-12 text-center" >
183+ < p className = "text-sm font-medium text-gray-900" > No invoices found</ p >
184+ < p className = "mt-1 text-sm text-gray-500" > Try adjusting your filters or search terms.</ p >
135185 </ div >
136186 ) : (
137187 < >
@@ -162,15 +212,15 @@ export default function InvoicesPage() {
162212 </ tr >
163213 </ thead >
164214 < tbody className = "divide-y divide-gray-200" >
165- { invoices . map ( ( invoice ) => (
215+ { filteredInvoices . map ( ( invoice ) => (
166216 < tr key = { invoice . id } className = "hover:bg-gray-50" >
167- < td className = "px-6 py-4 text-sm font-medium text-gray-900" >
217+ < td className = "whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900" >
168218 { invoice . invoiceNumber || `#${ invoice . id . slice ( 0 , 8 ) } ` }
169219 </ td >
170- < td className = "px-6 py-4 text-sm text-gray-600" >
220+ < td className = "whitespace-nowrap px-6 py-4 text-sm text-gray-600" >
171221 { invoice . clientName }
172222 </ td >
173- < td className = "px-6 py-4 text-right text-sm font-medium text-gray-900" >
223+ < td className = "whitespace-nowrap px-6 py-4 text-right text-sm font-medium text-gray-900" >
174224 { invoice . amount . toLocaleString ( 'en-US' , {
175225 minimumFractionDigits : 2 ,
176226 maximumFractionDigits : 7 ,
@@ -180,9 +230,9 @@ export default function InvoicesPage() {
180230 < td className = "px-6 py-4 text-sm text-gray-600" >
181231 { formatDate ( invoice . createdAt ) }
182232 </ td >
183- < td className = "px-6 py-4 text-sm" >
233+ < td className = "whitespace-nowrap px-6 py-4 text-sm" >
184234 < span
185- className = { `inline-flex rounded-full px-3 py-1 text-xs font-medium ${ getStatusColor (
235+ className = { `inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ${ getStatusColor (
186236 invoice . status ,
187237 ) } `}
188238 >
@@ -204,9 +254,55 @@ export default function InvoicesPage() {
204254 </ div >
205255 </ div >
206256
207- { /* Load More Button */ }
257+ { /* Pagination Placeholder & Load More */ }
258+ < div className = "mt-8 flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg shadow-sm" >
259+ < div className = "flex flex-1 justify-between sm:hidden" >
260+ < button
261+ disabled
262+ className = "relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-300 cursor-not-allowed hover:bg-gray-50"
263+ >
264+ Previous
265+ </ button >
266+ < button
267+ disabled
268+ className = "relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-300 cursor-not-allowed hover:bg-gray-50"
269+ >
270+ Next
271+ </ button >
272+ </ div >
273+ < div className = "hidden sm:flex sm:flex-1 sm:items-center sm:justify-between" >
274+ < div >
275+ < p className = "text-sm text-gray-700" >
276+ Showing < span className = "font-medium" > { filteredInvoices . length } </ span > results
277+ </ p >
278+ </ div >
279+ < div >
280+ < nav className = "isolate inline-flex -space-gap-px rounded-md shadow-sm" aria-label = "Pagination" >
281+ < button
282+ disabled
283+ className = "relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-300 ring-1 ring-inset ring-gray-300 cursor-not-allowed hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
284+ >
285+ < span className = "sr-only" > Previous</ span >
286+ < svg className = "h-5 w-5" viewBox = "0 0 20 20" fill = "currentColor" aria-hidden = "true" >
287+ < path fillRule = "evenodd" d = "M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clipRule = "evenodd" />
288+ </ svg >
289+ </ button >
290+ < button
291+ disabled
292+ className = "relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-300 ring-1 ring-inset ring-gray-300 cursor-not-allowed hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
293+ >
294+ < span className = "sr-only" > Next</ span >
295+ < svg className = "h-5 w-5" viewBox = "0 0 20 20" fill = "currentColor" aria-hidden = "true" >
296+ < path fillRule = "evenodd" d = "M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clipRule = "evenodd" />
297+ </ svg >
298+ </ button >
299+ </ nav >
300+ </ div >
301+ </ div >
302+ </ div >
303+
208304 { hasNextPage && (
209- < div className = "mt-6 text-center" >
305+ < div className = "mt-4 text-center" >
210306 < button
211307 onClick = { ( ) => fetchNextPage ( ) }
212308 disabled = { isFetchingNextPage }
0 commit comments