@@ -34,6 +34,8 @@ const recurringLabels: Record<RecurringExpenseDto['frequency'], string> = {
3434 monthly : 'Monthly' ,
3535} ;
3636
37+ type RecurringSectionKey = 'overdue' | 'today' | 'upcoming' ;
38+
3739const categoryLabels : Record < ExpenseCategory , string > = {
3840 FOOD : 'Food' ,
3941 TRAVEL : 'Travel' ,
@@ -43,14 +45,27 @@ const categoryLabels: Record<ExpenseCategory, string> = {
4345 OTHER : 'Other' ,
4446} ;
4547
46- const getRecurringScheduleLabel = ( nextOccurrenceAt : string ) => {
48+ const recurringSectionTitles : Record < RecurringSectionKey , string > = {
49+ overdue : 'Overdue' ,
50+ today : 'Due Today' ,
51+ upcoming : 'Upcoming' ,
52+ } ;
53+
54+ const getRecurringSection = ( nextOccurrenceAt : string ) : RecurringSectionKey => {
4755 const now = new Date ( ) ;
4856 const today = new Date ( now . getFullYear ( ) , now . getMonth ( ) , now . getDate ( ) ) . getTime ( ) ;
4957 const next = new Date ( nextOccurrenceAt ) ;
5058 const nextDay = new Date ( next . getFullYear ( ) , next . getMonth ( ) , next . getDate ( ) ) . getTime ( ) ;
5159
52- if ( nextDay < today ) return 'Overdue' ;
53- if ( nextDay === today ) return 'Due today' ;
60+ if ( nextDay < today ) return 'overdue' ;
61+ if ( nextDay === today ) return 'today' ;
62+ return 'upcoming' ;
63+ } ;
64+
65+ const getRecurringScheduleLabel = ( nextOccurrenceAt : string ) => {
66+ const section = getRecurringSection ( nextOccurrenceAt ) ;
67+ if ( section === 'overdue' ) return 'Overdue' ;
68+ if ( section === 'today' ) return 'Due today' ;
5469 return 'Upcoming' ;
5570} ;
5671
@@ -190,6 +205,22 @@ export function GroupDetailScreen({
190205 } ) ;
191206 } , [ categoryFilter , expenses , memberById , payerFilter , query ] ) ;
192207
208+ const recurringSections = React . useMemo ( ( ) => {
209+ const grouped : Record < RecurringSectionKey , RecurringExpenseDto [ ] > = {
210+ overdue : [ ] ,
211+ today : [ ] ,
212+ upcoming : [ ] ,
213+ } ;
214+
215+ [ ...recurringExpenses ]
216+ . sort ( ( a , b ) => new Date ( a . nextOccurrenceAt ) . getTime ( ) - new Date ( b . nextOccurrenceAt ) . getTime ( ) )
217+ . forEach ( ( item ) => {
218+ grouped [ getRecurringSection ( item . nextOccurrenceAt ) ] . push ( item ) ;
219+ } ) ;
220+
221+ return grouped ;
222+ } , [ recurringExpenses ] ) ;
223+
193224 const groupedExpenses = React . useMemo ( ( ) => {
194225 const now = new Date ( ) ;
195226 const startOfToday = new Date ( now . getFullYear ( ) , now . getMonth ( ) , now . getDate ( ) ) . getTime ( ) ;
@@ -456,34 +487,43 @@ export function GroupDetailScreen({
456487 < SectionHeader title = "Recurring Bills" />
457488 { recurringExpenses . length > 0 ? (
458489 < View style = { styles . recurringList } >
459- { recurringExpenses . map ( ( item ) => {
460- const payer = memberById . get ( item . payerId ) ;
461- const scheduleLabel = getRecurringScheduleLabel ( item . nextOccurrenceAt ) ;
462- const lastGeneratedLabel = formatLastGeneratedLabel ( item . lastGeneratedAt ) ;
463- return (
464- < Card key = { item . id } style = { styles . recurringCard } >
465- < View style = { styles . recurringHeaderRow } >
466- < View style = { { flex : 1 } } >
467- < Text style = { [ styles . recurringTitle , { color : colors . text_primary } ] } > { item . description } </ Text >
468- < Text style = { [ styles . recurringMeta , { color : colors . text_secondary } ] } > { recurringLabels [ item . frequency ] } { scheduleLabel } </ Text >
469- </ View >
470- < View style = { styles . recurringActions } >
471- < TouchableOpacity onPress = { ( ) => startRecurringEdit ( item ) } >
472- < MaterialCommunityIcons name = "pencil-outline" size = { 22 } color = { colors . text_primary } />
473- </ TouchableOpacity >
474- < TouchableOpacity onPress = { ( ) => setDeleteRecurringTarget ( item ) } >
475- < MaterialCommunityIcons name = "trash-can-outline" size = { 22 } color = { colors . danger } />
476- </ TouchableOpacity >
477- </ View >
490+ { ( Object . keys ( recurringSections ) as RecurringSectionKey [ ] ) . map ( ( sectionKey ) =>
491+ recurringSections [ sectionKey ] . length > 0 ? (
492+ < View key = { sectionKey } style = { styles . recurringSectionGroup } >
493+ < View style = { styles . recurringSectionHeader } >
494+ < Text style = { [ styles . recurringSectionTitle , { color : colors . text_secondary } ] } > { recurringSectionTitles [ sectionKey ] } </ Text >
495+ < Text style = { [ styles . recurringSectionCount , { color : colors . text_secondary } ] } > { recurringSections [ sectionKey ] . length } </ Text >
478496 </ View >
479- < Text style = { [ styles . recurringAmount , { color : colors . primary } ] } > { currencySymbol } { ( Number ( item . totalAmountCents ) / 100 ) . toFixed ( 2 ) } </ Text >
480- < Text style = { [ styles . recurringMeta , { color : colors . text_secondary } ] } > Next { new Date ( item . nextOccurrenceAt ) . toLocaleDateString ( ) } { lastGeneratedLabel } </ Text >
481- < Text style = { [ styles . recurringMeta , { color : colors . text_secondary } ] } > Paid by { payer ?. name ?? 'Unknown' } { item . splits . length } participants</ Text >
482- </ Card >
483- ) ;
484- } ) }
485- </ View >
486- ) : (
497+ { recurringSections [ sectionKey ] . map ( ( item ) => {
498+ const payer = memberById . get ( item . payerId ) ;
499+ const scheduleLabel = getRecurringScheduleLabel ( item . nextOccurrenceAt ) ;
500+ const lastGeneratedLabel = formatLastGeneratedLabel ( item . lastGeneratedAt ) ;
501+ return (
502+ < Card key = { item . id } style = { styles . recurringCard } >
503+ < View style = { styles . recurringHeaderRow } >
504+ < View style = { { flex : 1 } } >
505+ < Text style = { [ styles . recurringTitle , { color : colors . text_primary } ] } > { item . description } </ Text >
506+ < Text style = { [ styles . recurringMeta , { color : colors . text_secondary } ] } > { recurringLabels [ item . frequency ] } { scheduleLabel } </ Text >
507+ </ View >
508+ < View style = { styles . recurringActions } >
509+ < TouchableOpacity onPress = { ( ) => startRecurringEdit ( item ) } >
510+ < MaterialCommunityIcons name = "pencil-outline" size = { 22 } color = { colors . text_primary } />
511+ </ TouchableOpacity >
512+ < TouchableOpacity onPress = { ( ) => setDeleteRecurringTarget ( item ) } >
513+ < MaterialCommunityIcons name = "trash-can-outline" size = { 22 } color = { colors . danger } />
514+ </ TouchableOpacity >
515+ </ View >
516+ </ View >
517+ < Text style = { [ styles . recurringAmount , { color : colors . primary } ] } > { currencySymbol } { ( Number ( item . totalAmountCents ) / 100 ) . toFixed ( 2 ) } </ Text >
518+ < Text style = { [ styles . recurringMeta , { color : colors . text_secondary } ] } > Next { new Date ( item . nextOccurrenceAt ) . toLocaleDateString ( ) } { lastGeneratedLabel } </ Text >
519+ < Text style = { [ styles . recurringMeta , { color : colors . text_secondary } ] } > Paid by { payer ?. name ?? 'Unknown' } { item . splits . length } participants</ Text >
520+ </ Card >
521+ ) ;
522+ } ) }
523+ </ View >
524+ ) : null ,
525+ ) }
526+ </ View > ) : (
487527 < Card style = { styles . recurringEmptyCard } >
488528 < Text style = { [ styles . recurringTitle , { color : colors . text_primary } ] } > No recurring bills yet</ Text >
489529 < Text style = { [ styles . recurringMeta , { color : colors . text_secondary } ] } > Turn on recurring when you add rent, subscriptions, or utilities.</ Text >
@@ -679,7 +719,25 @@ const styles = StyleSheet.create({
679719 marginBottom : spacing . xl ,
680720 } ,
681721 recurringList : {
682- gap : spacing . md ,
722+ gap : spacing . lg ,
723+ } ,
724+ recurringSectionGroup : {
725+ gap : spacing . sm ,
726+ } ,
727+ recurringSectionHeader : {
728+ flexDirection : 'row' ,
729+ justifyContent : 'space-between' ,
730+ alignItems : 'center' ,
731+ } ,
732+ recurringSectionTitle : {
733+ fontSize : 12 ,
734+ fontWeight : '800' ,
735+ textTransform : 'uppercase' ,
736+ letterSpacing : 0.6 ,
737+ } ,
738+ recurringSectionCount : {
739+ fontSize : 12 ,
740+ fontWeight : '700' ,
683741 } ,
684742 recurringCard : {
685743 padding : spacing . lg ,
@@ -779,3 +837,5 @@ const styles = StyleSheet.create({
779837
780838
781839
840+
841+
0 commit comments