@@ -4,17 +4,46 @@ import { useEffect, useState } from "react";
44import Link from "next/link" ;
55import { splitClient } from "@/lib/stellar" ;
66import { getFreighterPublicKey } from "@/lib/freighter" ;
7+ import { formatAmount } from "@stellar-split/sdk" ;
78import InvoiceCard from "@/components/InvoiceCard" ;
9+ import BatchPayModal from "@/components/BatchPayModal" ;
810import type { Invoice } from "@stellar-split/sdk" ;
911
12+ function exportCSV ( invoices : Invoice [ ] , from : string , to : string ) {
13+ const fromTs = from ? new Date ( from ) . getTime ( ) / 1000 : 0 ;
14+ const toTs = to ? new Date ( to ) . getTime ( ) / 1000 : Infinity ;
15+ const rows = invoices . filter ( ( inv ) => inv . deadline >= fromTs && inv . deadline <= toTs ) ;
16+ const header = "ID,Status,Total (USDC),Funded (USDC),Deadline,Recipient Count" ;
17+ const lines = rows . map ( ( inv ) => {
18+ const total = inv . recipients . reduce ( ( s , r ) => s + r . amount , 0n ) ;
19+ const deadline = new Date ( inv . deadline * 1000 ) . toISOString ( ) . slice ( 0 , 10 ) ;
20+ return [ inv . id , inv . status , formatAmount ( total ) , formatAmount ( inv . funded ) , deadline , inv . recipients . length ] . join ( "," ) ;
21+ } ) ;
22+ const csv = [ header , ...lines ] . join ( "\n" ) ;
23+ const url = URL . createObjectURL ( new Blob ( [ csv ] , { type : "text/csv" } ) ) ;
24+ const a = document . createElement ( "a" ) ;
25+ a . href = url ;
26+ a . download = "invoices.csv" ;
27+ a . click ( ) ;
28+ URL . revokeObjectURL ( url ) ;
29+ }
30+
1031/**
1132 * Dashboard — lists invoices where the connected wallet is creator or recipient.
33+ * Supports multi-select mode for batch payments.
1234 */
1335export default function DashboardPage ( ) {
1436 const [ publicKey , setPublicKey ] = useState < string | null > ( null ) ;
1537 const [ invoices , setInvoices ] = useState < Invoice [ ] > ( [ ] ) ;
1638 const [ loading , setLoading ] = useState ( true ) ;
1739 const [ error , setError ] = useState < string | null > ( null ) ;
40+ const [ exportFrom , setExportFrom ] = useState ( "" ) ;
41+ const [ exportTo , setExportTo ] = useState ( "" ) ;
42+
43+ // Multi-select state
44+ const [ multiSelect , setMultiSelect ] = useState ( false ) ;
45+ const [ selected , setSelected ] = useState < Set < string > > ( new Set ( ) ) ;
46+ const [ showBatchModal , setShowBatchModal ] = useState ( false ) ;
1847
1948 useEffect ( ( ) => {
2049 getFreighterPublicKey ( )
@@ -25,8 +54,6 @@ export default function DashboardPage() {
2554 useEffect ( ( ) => {
2655 if ( ! publicKey ) return ;
2756
28- // Fetch invoices 1–50 and filter by creator or recipient.
29- // In production this would use an indexer; here we scan a range.
3057 const fetchInvoices = async ( ) => {
3158 setLoading ( true ) ;
3259 const results : Invoice [ ] = [ ] ;
@@ -37,7 +64,6 @@ export default function DashboardPage() {
3764 const isRecipient = inv . recipients . some ( ( r ) => r . address === publicKey ) ;
3865 if ( isCreator || isRecipient ) results . push ( inv ) ;
3966 } catch {
40- // Invoice doesn't exist — stop scanning.
4167 break ;
4268 }
4369 }
@@ -51,6 +77,23 @@ export default function DashboardPage() {
5177 } ) ;
5278 } , [ publicKey ] ) ;
5379
80+ const toggleSelect = ( id : string ) => {
81+ setSelected ( ( prev ) => {
82+ const next = new Set ( prev ) ;
83+ if ( next . has ( id ) ) next . delete ( id ) ;
84+ else next . add ( id ) ;
85+ return next ;
86+ } ) ;
87+ } ;
88+
89+ const exitMultiSelect = ( ) => {
90+ setMultiSelect ( false ) ;
91+ setSelected ( new Set ( ) ) ;
92+ } ;
93+
94+ const pendingInvoices = invoices . filter ( ( inv ) => inv . status === "Pending" ) ;
95+ const selectedInvoices = invoices . filter ( ( inv ) => selected . has ( inv . id ) ) ;
96+
5497 if ( error ) {
5598 return (
5699 < main className = "max-w-2xl mx-auto px-6 py-20 text-center" >
@@ -61,28 +104,107 @@ export default function DashboardPage() {
61104
62105 return (
63106 < main className = "max-w-3xl mx-auto px-6 py-16" >
64- < div className = "flex items-center justify-between mb-10" >
107+ < div className = "flex items-center justify-between mb-10 flex-wrap gap-3 " >
65108 < h1 className = "text-3xl font-bold" > Dashboard</ h1 >
66- < Link
67- href = "/invoice/new"
68- className = "px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-semibold transition-colors"
69- >
70- + New Invoice
71- </ Link >
109+ < div className = "flex gap-2 flex-wrap" >
110+ { ! multiSelect && pendingInvoices . length > 0 && (
111+ < button
112+ onClick = { ( ) => setMultiSelect ( true ) }
113+ className = "px-4 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-sm font-semibold transition-colors"
114+ aria-label = "Enter multi-select mode to pay multiple invoices"
115+ >
116+ Pay Multiple
117+ </ button >
118+ ) }
119+ { multiSelect && (
120+ < >
121+ < button
122+ onClick = { exitMultiSelect }
123+ className = "px-4 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-sm font-semibold transition-colors"
124+ >
125+ Cancel
126+ </ button >
127+ < button
128+ onClick = { ( ) => setShowBatchModal ( true ) }
129+ disabled = { selected . size === 0 }
130+ className = "px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-semibold transition-colors disabled:opacity-50"
131+ aria-label = { `Pay ${ selected . size } selected invoice${ selected . size !== 1 ? "s" : "" } ` }
132+ >
133+ Pay Selected ({ selected . size } )
134+ </ button >
135+ </ >
136+ ) }
137+ < Link
138+ href = "/invoice/new"
139+ className = "px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-sm font-semibold transition-colors"
140+ >
141+ + New Invoice
142+ </ Link >
143+ </ div >
72144 </ div >
73145
146+ { multiSelect && (
147+ < p className = "text-sm text-gray-400 mb-4" role = "status" >
148+ Select pending invoices to pay in a single transaction.
149+ </ p >
150+ ) }
151+
74152 { loading ? (
75153 < p className = "text-gray-400" > Loading invoices…</ p >
76154 ) : invoices . length === 0 ? (
77155 < p className = "text-gray-400" > No invoices found. Create your first one!</ p >
78156 ) : (
79- < div className = "flex flex-col gap-4" >
80- { invoices . map ( ( inv ) => (
81- < Link key = { inv . id } href = { `/invoice/${ inv . id } ` } >
82- < InvoiceCard invoice = { inv } />
83- </ Link >
84- ) ) }
85- </ div >
157+ < ul className = "flex flex-col gap-4" aria-label = "Invoice list" >
158+ { invoices . map ( ( inv ) => {
159+ const isSelectable = multiSelect && inv . status === "Pending" ;
160+ const isSelected = selected . has ( inv . id ) ;
161+
162+ return (
163+ < li key = { inv . id } >
164+ { isSelectable ? (
165+ < button
166+ type = "button"
167+ onClick = { ( ) => toggleSelect ( inv . id ) }
168+ aria-pressed = { isSelected }
169+ aria-label = { `${ isSelected ? "Deselect" : "Select" } Invoice #${ inv . id } ` }
170+ className = { `w-full text-left rounded-xl ring-2 transition-all ${
171+ isSelected
172+ ? "ring-indigo-500"
173+ : "ring-transparent hover:ring-gray-600"
174+ } `}
175+ >
176+ < div className = "relative" >
177+ { isSelected && (
178+ < span
179+ aria-hidden = "true"
180+ className = "absolute top-3 right-3 w-5 h-5 rounded-full bg-indigo-500 flex items-center justify-center text-white text-xs font-bold z-10"
181+ >
182+ ✓
183+ </ span >
184+ ) }
185+ < InvoiceCard invoice = { inv } />
186+ </ div >
187+ </ button >
188+ ) : (
189+ < Link href = { `/invoice/${ inv . id } ` } aria-label = { `View Invoice #${ inv . id } ` } >
190+ < InvoiceCard invoice = { inv } />
191+ </ Link >
192+ ) }
193+ </ li >
194+ ) ;
195+ } ) }
196+ </ ul >
197+ ) }
198+
199+ { showBatchModal && publicKey && selectedInvoices . length > 0 && (
200+ < BatchPayModal
201+ invoices = { selectedInvoices }
202+ publicKey = { publicKey }
203+ onClose = { ( ) => {
204+ setShowBatchModal ( false ) ;
205+ exitMultiSelect ( ) ;
206+ } }
207+ />
86208 ) }
87209 </ main >
88210 ) ;
0 commit comments