@@ -20,6 +20,8 @@ import { formatAmount } from "@stellar-split/sdk";
2020import type { Invoice } from "@stellar-split/sdk" ;
2121import { useInfiniteInvoices } from "@/hooks/useInfiniteInvoices" ;
2222import InvoiceListSentinel from "@/components/InvoiceListSentinel" ;
23+ import { useInvoiceSelection } from "@/hooks/useInvoiceSelection" ;
24+ import BulkActionToolbar from "@/components/invoice/BulkActionToolbar" ;
2325import {
2426 DASHBOARD_PRESETS ,
2527 SORT_OPTIONS ,
@@ -81,6 +83,18 @@ export default function DashboardClient() {
8183 const { unreadCount } = useActivityFeed ( ) ;
8284 const [ splitMetaMap , setSplitMetaMap ] = useState < Record < string , { installments ?: { dueDate : number ; status : string } [ ] } > > ( { } ) ;
8385
86+ // Multi-select state management
87+ const {
88+ selectedIds,
89+ isSelecting,
90+ toggleSelecting,
91+ toggleInvoice,
92+ selectAll,
93+ deselectAll,
94+ isSelected,
95+ selectedCount,
96+ } = useInvoiceSelection ( ) ;
97+
8498 // ── URL mutation helpers ────────────────────────────────────────────────────
8599
86100 const pushParams = useCallback (
@@ -110,6 +124,47 @@ export default function DashboardClient() {
110124 const isFiltered =
111125 statuses . length > 0 || dateFrom || dateTo || sort !== "newest" || ! ! tag ;
112126
127+ const handleBulkArchive = async ( ) => {
128+ try {
129+ const response = await fetch ( "/api/invoices/bulk" , {
130+ method : "PATCH" ,
131+ headers : { "Content-Type" : "application/json" } ,
132+ body : JSON . stringify ( {
133+ invoiceIds : Array . from ( selectedIds ) ,
134+ action : "archive" ,
135+ archived : true ,
136+ } ) ,
137+ } ) ;
138+
139+ if ( response . ok ) {
140+ deselectAll ( ) ;
141+ // Optionally refresh the invoice list
142+ }
143+ } catch ( error ) {
144+ console . error ( "Bulk archive failed:" , error ) ;
145+ }
146+ } ;
147+
148+ const handleBulkDelete = async ( ) => {
149+ try {
150+ const response = await fetch ( "/api/invoices/bulk" , {
151+ method : "PATCH" ,
152+ headers : { "Content-Type" : "application/json" } ,
153+ body : JSON . stringify ( {
154+ invoiceIds : Array . from ( selectedIds ) ,
155+ action : "delete" ,
156+ } ) ,
157+ } ) ;
158+
159+ if ( response . ok ) {
160+ deselectAll ( ) ;
161+ // Optionally refresh the invoice list
162+ }
163+ } catch ( error ) {
164+ console . error ( "Bulk delete failed:" , error ) ;
165+ }
166+ } ;
167+
113168 // ── Data fetching ───────────────────────────────────────────────────────────
114169 const [ activePreset , setActivePreset ] = useState < DashboardPresetId > ( "all" ) ;
115170 const [ shareQRInvoiceId , setShareQRInvoiceId ] = useState < string | null > ( null ) ;
@@ -566,6 +621,24 @@ export default function DashboardClient() {
566621 </ button >
567622 </ >
568623 ) }
624+ { ! isSelecting && ! compareMode && ! reminderSelect && (
625+ < button
626+ onClick = { toggleSelecting }
627+ className = "min-h-11 px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-sm font-semibold transition-colors"
628+ aria-label = "Enable multi-select mode"
629+ >
630+ Select
631+ </ button >
632+ ) }
633+ { isSelecting && (
634+ < button
635+ onClick = { toggleSelecting }
636+ className = "min-h-11 px-4 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-sm font-semibold transition-colors text-gray-300"
637+ aria-label = "Exit multi-select mode"
638+ >
639+ Cancel Selection
640+ </ button >
641+ ) }
569642 < Link
570643 href = "/invoice/new"
571644 className = "min-h-11 inline-flex items-center px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-semibold transition-colors"
@@ -753,14 +826,44 @@ export default function DashboardClient() {
753826 const isReminderSelected = reminderSelected . has ( inv . id ) ;
754827 const isCompareSelectable = compareMode ;
755828 const isCompareSelected = compareSelected . has ( inv . id ) ;
829+ const isMultiSelectable = isSelecting ;
830+ const isMultiSelected = isSelected ( inv . id ) ;
756831
757832 const card = (
758833 < InvoiceCard invoice = { inv } displayNumber = { getOrAssignDisplayNumber ( inv . id ) } tags = { tagsByInvoice [ inv . id ] ?? [ ] } />
759834 ) ;
760835
761836 return (
762837 < div key = { inv . id } >
763- { isSelectable ? (
838+ { isMultiSelectable ? (
839+ < button
840+ type = "button"
841+ onClick = { ( ) => toggleInvoice ( inv . id ) }
842+ aria-pressed = { isMultiSelected }
843+ aria-label = { `${ isMultiSelected ? "Deselect" : "Select" } Invoice #${ inv . id } ` }
844+ className = { `w-full text-left rounded-xl ring-2 transition-all ${
845+ isMultiSelected
846+ ? "ring-indigo-500"
847+ : "ring-transparent hover:ring-gray-600"
848+ } `}
849+ >
850+ < div className = "relative" >
851+ { isMultiSelected && (
852+ < span
853+ aria-hidden = "true"
854+ 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"
855+ >
856+ ✓
857+ </ span >
858+ ) }
859+ < InvoiceCard
860+ invoice = { inv }
861+ displayNumber = { getOrAssignDisplayNumber ( inv . id ) }
862+ tags = { tagsByInvoice [ inv . id ] ?? [ ] }
863+ />
864+ </ div >
865+ </ button >
866+ ) : isSelectable ? (
764867 < button
765868 type = "button"
766869 onClick = { ( ) => toggleSelect ( inv . id ) }
@@ -928,6 +1031,21 @@ export default function DashboardClient() {
9281031 onClose = { ( ) => setShareQRInvoiceId ( null ) }
9291032 />
9301033
1034+ { isSelecting && selectedCount > 0 && (
1035+ < BulkActionToolbar
1036+ selectedCount = { selectedCount }
1037+ selectedIds = { selectedIds }
1038+ totalVisible = { visibleInvoices . length }
1039+ onSelectAll = { ( ) => selectAll ( visibleInvoices . map ( inv => inv . id ) ) }
1040+ onDeselectAll = { deselectAll }
1041+ onArchive = { handleBulkArchive }
1042+ onDelete = { handleBulkDelete }
1043+ onTag = { ( ) => {
1044+ // TODO: Implement tagging dialog
1045+ } }
1046+ />
1047+ ) }
1048+
9311049 < ActivityFeed open = { feedOpen } />
9321050 </ >
9331051 ) ;
0 commit comments