11'use client' ;
22
33import { useMemo , useState } from 'react' ;
4- import { SimplifySuggestionDto } from '@fairshare/shared-types' ;
5- import { BellRing , CheckCircle2 } from 'lucide-react' ;
4+ import { ActivityDto , SimplifySuggestionDto } from '@fairshare/shared-types' ;
5+ import { BellRing , CheckCircle2 , Clock3 } from 'lucide-react' ;
66import { useRouter } from 'next/navigation' ;
77import { createSettlementAction , remindSettlementAction } from '../../lib/actions' ;
88import { useToast } from '../ui/Toaster' ;
@@ -12,14 +12,30 @@ type SettlementListProps = {
1212 currency : string ;
1313 suggestions : SimplifySuggestionDto [ ] ;
1414 memberLookup : Record < string , { name : string ; email : string } > ;
15+ initialReminderActivity : ActivityDto [ ] ;
1516} ;
1617
17- export function SettlementList ( { groupId, currency, suggestions, memberLookup } : SettlementListProps ) {
18+ const suggestionKey = ( fromUserId : string , toUserId : string , amountCents : string ) => `${ fromUserId } -${ toUserId } -${ amountCents } ` ;
19+
20+ function timeAgo ( iso : string ) : string {
21+ const now = Date . now ( ) ;
22+ const diff = Math . max ( 1 , Math . floor ( ( now - new Date ( iso ) . getTime ( ) ) / 1000 ) ) ;
23+ if ( diff < 60 ) return `${ diff } s ago` ;
24+ const minutes = Math . floor ( diff / 60 ) ;
25+ if ( minutes < 60 ) return `${ minutes } m ago` ;
26+ const hours = Math . floor ( minutes / 60 ) ;
27+ if ( hours < 24 ) return `${ hours } h ago` ;
28+ const days = Math . floor ( hours / 24 ) ;
29+ return `${ days } d ago` ;
30+ }
31+
32+ export function SettlementList ( { groupId, currency, suggestions, memberLookup, initialReminderActivity } : SettlementListProps ) {
1833 const [ pendingId , setPendingId ] = useState < string | null > ( null ) ;
1934 const [ reminderId , setReminderId ] = useState < string | null > ( null ) ;
2035 const [ query , setQuery ] = useState ( '' ) ;
2136 const [ fromUserId , setFromUserId ] = useState ( '' ) ;
2237 const [ toUserId , setToUserId ] = useState ( '' ) ;
38+ const [ reminderEvents , setReminderEvents ] = useState < ActivityDto [ ] > ( initialReminderActivity ) ;
2339 const { toast } = useToast ( ) ;
2440 const router = useRouter ( ) ;
2541
@@ -50,17 +66,47 @@ export function SettlementList({ groupId, currency, suggestions, memberLookup }:
5066 return true ;
5167 }
5268
53- const haystack = [
54- labelForUser ( item . fromUserId ) ,
55- labelForUser ( item . toUserId ) ,
56- formatAmount ( item . amountCents ) ,
57- ]
69+ const haystack = [ labelForUser ( item . fromUserId ) , labelForUser ( item . toUserId ) , formatAmount ( item . amountCents ) ]
5870 . join ( ' ' )
5971 . toLowerCase ( ) ;
6072
6173 return haystack . includes ( normalizedQuery ) ;
6274 } ) ;
63- } , [ fromUserId , query , suggestions , toUserId ] ) ;
75+ } , [ fromUserId , query , suggestions , toUserId , memberLookup ] ) ;
76+
77+ const latestReminderBySuggestion = useMemo ( ( ) => {
78+ const entries = new Map < string , ActivityDto > ( ) ;
79+
80+ reminderEvents . forEach ( ( event ) => {
81+ if ( event . type !== 'settlement_reminder' ) {
82+ return ;
83+ }
84+
85+ const payerId = typeof event . metadata ?. payerId === 'string' ? event . metadata . payerId : null ;
86+ const receiverId = typeof event . metadata ?. receiverId === 'string' ? event . metadata . receiverId : null ;
87+ const amountCents = typeof event . metadata ?. amountCents === 'string' ? event . metadata . amountCents : null ;
88+ if ( ! payerId || ! receiverId || ! amountCents ) {
89+ return ;
90+ }
91+
92+ const key = suggestionKey ( payerId , receiverId , amountCents ) ;
93+ const existing = entries . get ( key ) ;
94+ if ( ! existing || new Date ( event . createdAt ) . getTime ( ) > new Date ( existing . createdAt ) . getTime ( ) ) {
95+ entries . set ( key , event ) ;
96+ }
97+ } ) ;
98+
99+ return entries ;
100+ } , [ reminderEvents ] ) ;
101+
102+ const recentReminders = useMemo (
103+ ( ) =>
104+ reminderEvents
105+ . filter ( ( event ) => event . type === 'settlement_reminder' )
106+ . sort ( ( a , b ) => new Date ( b . createdAt ) . getTime ( ) - new Date ( a . createdAt ) . getTime ( ) )
107+ . slice ( 0 , 4 ) ,
108+ [ reminderEvents ] ,
109+ ) ;
64110
65111 const resetFilters = ( ) => {
66112 setQuery ( '' ) ;
@@ -69,7 +115,7 @@ export function SettlementList({ groupId, currency, suggestions, memberLookup }:
69115 } ;
70116
71117 const markSettled = async ( suggestion : SimplifySuggestionDto ) => {
72- setPendingId ( ` ${ suggestion . fromUserId } - ${ suggestion . toUserId } - ${ suggestion . amountCents } ` ) ;
118+ setPendingId ( suggestionKey ( suggestion . fromUserId , suggestion . toUserId , suggestion . amountCents ) ) ;
73119 const result = await createSettlementAction ( groupId , {
74120 payerId : suggestion . fromUserId ,
75121 receiverId : suggestion . toUserId ,
@@ -88,7 +134,7 @@ export function SettlementList({ groupId, currency, suggestions, memberLookup }:
88134 } ;
89135
90136 const sendReminder = async ( suggestion : SimplifySuggestionDto ) => {
91- const key = ` ${ suggestion . fromUserId } - ${ suggestion . toUserId } - ${ suggestion . amountCents } ` ;
137+ const key = suggestionKey ( suggestion . fromUserId , suggestion . toUserId , suggestion . amountCents ) ;
92138 setReminderId ( key ) ;
93139 const result = await remindSettlementAction ( groupId , {
94140 payerId : suggestion . fromUserId ,
@@ -102,6 +148,9 @@ export function SettlementList({ groupId, currency, suggestions, memberLookup }:
102148 return ;
103149 }
104150
151+ if ( 'activity' in result && result . activity ) {
152+ setReminderEvents ( ( prev ) => [ result . activity , ...prev . filter ( ( event ) => event . id !== result . activity . id ) ] ) ;
153+ }
105154 toast ( `Reminder sent to ${ labelForUser ( suggestion . fromUserId ) } ` ) ;
106155 setReminderId ( null ) ;
107156 } ;
@@ -156,6 +205,36 @@ export function SettlementList({ groupId, currency, suggestions, memberLookup }:
156205 </ div >
157206 </ div >
158207
208+ { recentReminders . length > 0 ? (
209+ < div className = "rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card)] p-5 shadow-[var(--fs-shadow-soft)]" >
210+ < div className = "flex items-center gap-2" >
211+ < BellRing className = "h-4 w-4 text-[var(--fs-primary)]" />
212+ < h3 className = "text-base font-extrabold text-[var(--fs-text-primary)]" > Recent reminders</ h3 >
213+ </ div >
214+ < div className = "mt-3 space-y-2" >
215+ { recentReminders . map ( ( event ) => {
216+ const payerId = typeof event . metadata ?. payerId === 'string' ? event . metadata . payerId : '' ;
217+ const receiverId = typeof event . metadata ?. receiverId === 'string' ? event . metadata . receiverId : '' ;
218+ const amountCents = typeof event . metadata ?. amountCents === 'string' ? event . metadata . amountCents : '0' ;
219+ return (
220+ < div key = { event . id } className = "rounded-2xl border border-[var(--fs-border)] bg-[var(--fs-background)]/70 px-4 py-3" >
221+ < p className = "text-sm font-semibold text-[var(--fs-text-primary)]" >
222+ { labelForUser ( event . actorUserId ) } reminded { labelForUser ( payerId ) } to pay { labelForUser ( receiverId ) }
223+ </ p >
224+ < div className = "mt-1 flex flex-wrap items-center gap-2 text-[11px] font-medium text-[var(--fs-text-muted)]" >
225+ < span > { formatAmount ( amountCents ) } </ span >
226+ < span className = "inline-flex items-center gap-1" >
227+ < Clock3 className = "h-3 w-3" />
228+ { timeAgo ( event . createdAt ) }
229+ </ span >
230+ </ div >
231+ </ div >
232+ ) ;
233+ } ) }
234+ </ div >
235+ </ div >
236+ ) : null }
237+
159238 { filteredSuggestions . length === 0 ? (
160239 < div className = "rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card)] p-8 text-center shadow-[var(--fs-shadow-soft)]" >
161240 < p className = "text-lg font-bold text-[var(--fs-text-primary)] mb-1" >
@@ -167,9 +246,11 @@ export function SettlementList({ groupId, currency, suggestions, memberLookup }:
167246 </ div >
168247 ) : (
169248 filteredSuggestions . map ( ( item ) => {
170- const key = ` ${ item . fromUserId } - ${ item . toUserId } - ${ item . amountCents } ` ;
249+ const key = suggestionKey ( item . fromUserId , item . toUserId , item . amountCents ) ;
171250 const pending = pendingId === key ;
172251 const reminding = reminderId === key ;
252+ const latestReminder = latestReminderBySuggestion . get ( key ) ;
253+
173254 return (
174255 < div
175256 key = { key }
@@ -181,6 +262,11 @@ export function SettlementList({ groupId, currency, suggestions, memberLookup }:
181262 { labelForUser ( item . toUserId ) }
182263 </ p >
183264 < p className = "text-[11px] font-medium text-[var(--fs-text-muted)]" > { formatAmount ( item . amountCents ) } </ p >
265+ { latestReminder ? (
266+ < p className = "mt-1 text-[11px] font-medium text-[var(--fs-text-muted)]" >
267+ Last reminded { timeAgo ( latestReminder . createdAt ) } by { labelForUser ( latestReminder . actorUserId ) }
268+ </ p >
269+ ) : null }
184270 </ div >
185271 < div className = "flex w-full sm:w-auto items-center gap-2" >
186272 < button
@@ -212,3 +298,4 @@ export function SettlementList({ groupId, currency, suggestions, memberLookup }:
212298 </ div >
213299 ) ;
214300}
301+
0 commit comments