11import { useQuery } from '@tanstack/react-query' ;
2- import { expensesApi } from '../../lib/api' ;
2+ import { expensesApi , transferencesApi } from '../../lib/api' ;
33import { DEFAULT_LOCALE , getExpenseCategoryName } from '../../lib/categories' ;
4- import type { Expense } from '../../types/models' ;
4+ import type { Expense , Transference } from '../../types/models' ;
55import { useMemo } from 'react' ;
66import { queryClient } from '../../lib/queryClient' ;
77
8+ type Transaction =
9+ | ( Expense & { type : 'expense' } )
10+ | ( Transference & { type : 'transference' } ) ;
11+
812const CATEGORY_COLORS : Record < string , string > = {
913 FOOD : 'bg-orange-500/20 text-orange-400' ,
1014 EXTERNAL_FOOD : 'bg-orange-500/20 text-orange-400' ,
@@ -22,31 +26,54 @@ const CATEGORY_COLORS: Record<string, string> = {
2226 ONLINE : 'bg-indigo-500/20 text-indigo-400' ,
2327 TRAVEL : 'bg-teal-500/20 text-teal-400' ,
2428 SPORTS : 'bg-lime-500/20 text-lime-400' ,
29+ LOAN : 'bg-yellow-500/20 text-yellow-400' ,
30+ FAMILY : 'bg-rose-500/20 text-rose-400' ,
2531 GENERAL : 'bg-slate-500/20 text-slate-400' ,
2632} ;
2733
28- async function handleDelete ( expenseId : number ) {
34+ async function handleDeleteExpense ( expenseId : number ) {
2935 if ( ! window . confirm ( 'Quieres eliminar este gasto?' ) ) return ;
3036 try {
31- await expensesApi . delete ( expenseId ) ;
37+ await expensesApi . delete ( expenseId ) ;
3238 queryClient . invalidateQueries ( { queryKey : [ 'expenses' ] } ) ;
3339 } catch ( error ) {
3440 console . error ( 'Failed to delete expense:' , error ) ;
3541 alert ( 'No se pudo eliminar el gasto. Intentalo de nuevo.' ) ;
3642 }
3743}
3844
45+ async function handleDeleteTransference ( transferenceId : number ) {
46+ if ( ! window . confirm ( 'Quieres eliminar esta transferencia?' ) ) return ;
47+ try {
48+ await transferencesApi . delete ( transferenceId ) ;
49+ queryClient . invalidateQueries ( { queryKey : [ 'transferences' ] } ) ;
50+ } catch ( error ) {
51+ console . error ( 'Failed to delete transference:' , error ) ;
52+ alert ( 'No se pudo eliminar la transferencia. Intentalo de nuevo.' ) ;
53+ }
54+ }
55+
3956export function ExpenseList ( ) {
40- const { data : expenses = [ ] , isLoading, error } = useQuery ( {
57+ const { data : expenses = [ ] , isLoading : loadingExpenses , error : expensesError } = useQuery ( {
4158 queryKey : [ 'expenses' ] ,
4259 queryFn : expensesApi . getAll ,
4360 } ) ;
4461
45- // Derived + memoized sorted list (newest first)
46- const sortedExpenses = useMemo ( ( ) => {
47- if ( ! expenses . length ) return [ ] ;
48- return [ ...expenses ] . sort ( ( a , b ) => new Date ( b . date ) . getTime ( ) - new Date ( a . date ) . getTime ( ) ) ;
49- } , [ expenses ] ) ;
62+ const { data : transferences = [ ] , isLoading : loadingTransferences , error : transferencesError } = useQuery ( {
63+ queryKey : [ 'transferences' ] ,
64+ queryFn : transferencesApi . getAll ,
65+ } ) ;
66+
67+ const isLoading = loadingExpenses || loadingTransferences ;
68+ const error = expensesError || transferencesError ;
69+
70+ // Combine and sort all transactions (newest first)
71+ const sortedTransactions = useMemo ( ( ) => {
72+ const expensesWithType : Transaction [ ] = expenses . map ( ( e ) => ( { ...e , type : 'expense' as const } ) ) ;
73+ const transferencesWithType : Transaction [ ] = transferences . map ( ( t ) => ( { ...t , type : 'transference' as const } ) ) ;
74+ const all = [ ...expensesWithType , ...transferencesWithType ] ;
75+ return all . sort ( ( a , b ) => new Date ( b . date ) . getTime ( ) - new Date ( a . date ) . getTime ( ) ) ;
76+ } , [ expenses , transferences ] ) ;
5077
5178 const formatCurrency = ( amount : number , currency : string ) => {
5279 const normalizedCurrency = currency . toUpperCase ( ) ;
@@ -91,56 +118,81 @@ export function ExpenseList() {
91118 ) ;
92119 }
93120
94- if ( sortedExpenses . length === 0 ) {
121+ if ( sortedTransactions . length === 0 ) {
95122 return (
96123 < div className = "bg-gray-800 rounded-lg border border-gray-700 p-8" >
97124 < div className = "text-center text-gray-500" >
98- Aun no hay gastos . Agrega el primero o conecta Gmail para importarlos automaticamente.
125+ Aun no hay transacciones . Agrega la primera o conecta Gmail para importarlas automaticamente.
99126 </ div >
100127 </ div >
101128 ) ;
102129 }
103130
131+ const getTransactionTitle = ( transaction : Transaction ) => {
132+ if ( transaction . type === 'expense' ) {
133+ return transaction . commerce ;
134+ }
135+ return transaction . recipient || 'Transferencia' ;
136+ } ;
137+
138+ const getTransactionCategoryName = ( transaction : Transaction ) => {
139+ if ( transaction . type === 'expense' ) {
140+ return getExpenseCategoryName ( transaction ) ;
141+ }
142+ return transaction . category_name ;
143+ } ;
144+
104145 return (
105146 < div className = "bg-gray-800 rounded-lg border border-gray-700" >
106147 < div className = "p-4 border-b border-gray-700" >
107148 < h2 className = "text-lg font-semibold text-white" > Transacciones recientes</ h2 >
108- < p className = "text-sm text-gray-400" > { sortedExpenses . length } total</ p >
149+ < p className = "text-sm text-gray-400" > { sortedTransactions . length } total</ p >
109150 </ div >
110151
111- < div className = "divide-y divide-gray-700 max-h-125 overflow-y-auto" > { /* ← fixed height example */ }
112- { sortedExpenses . map ( ( expense : Expense ) => (
152+ < div className = "divide-y divide-gray-700 max-h-125 overflow-y-auto" >
153+ { sortedTransactions . map ( ( transaction : Transaction ) => (
113154 < div
114- key = { expense . id }
155+ key = { ` ${ transaction . type } - ${ transaction . id } ` }
115156 className = "p-4 hover:bg-gray-750 transition-colors"
116157 >
117158 < div className = "flex justify-between items-start gap-3" >
118159 < div className = "min-w-0 flex-1" >
119- < h3 className = "font-medium text-white truncate" > { expense . commerce } </ h3 >
160+ < div className = "flex items-center gap-2" >
161+ < h3 className = "font-medium text-white truncate" > { getTransactionTitle ( transaction ) } </ h3 >
162+ { transaction . type === 'transference' && (
163+ < span className = "text-xs px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-400" >
164+ Transferencia
165+ </ span >
166+ ) }
167+ </ div >
120168 < div className = "flex items-center gap-2 mt-1" >
121169 < span
122- className = { `text-xs px-2 py-0.5 rounded-full ${ getCategoryStyle ( expense . category_slug ) } ` }
170+ className = { `text-xs px-2 py-0.5 rounded-full ${ getCategoryStyle ( transaction . category_slug ) } ` }
123171 >
124- { getExpenseCategoryName ( expense ) }
172+ { getTransactionCategoryName ( transaction ) }
125173 </ span >
126- < span className = "text-xs text-gray-500" > { formatDate ( expense . date ) } </ span >
174+ < span className = "text-xs text-gray-500" > { formatDate ( transaction . date ) } </ span >
127175 </ div >
128- { expense . description && (
129- < p className = "text-sm text-gray-500 mt-1 truncate" > { expense . description } </ p >
176+ { transaction . description && (
177+ < p className = "text-sm text-gray-500 mt-1 truncate" > { transaction . description } </ p >
130178 ) }
131179 </ div >
132180
133181 < div className = "text-right shrink-0" >
134182 < p className = "font-semibold text-white" >
135- { formatCurrency ( expense . amount , expense . currency ) }
183+ { formatCurrency ( transaction . amount , transaction . currency ) }
136184 </ p >
137- < p className = "text-xs text-gray-500" > { expense . currency . toUpperCase ( ) } </ p >
185+ < p className = "text-xs text-gray-500" > { transaction . currency . toUpperCase ( ) } </ p >
138186 </ div >
139187
140188 < button
141- onClick = { ( ) => handleDelete ( expense . id ) }
189+ onClick = { ( ) =>
190+ transaction . type === 'expense'
191+ ? handleDeleteExpense ( transaction . id )
192+ : handleDeleteTransference ( transaction . id )
193+ }
142194 className = "text-red-400 hover:text-red-300 transition-colors"
143- title = " Eliminar gasto"
195+ title = { transaction . type === 'expense' ? ' Eliminar gasto' : 'Eliminar transferencia' }
144196 >
145197 < svg
146198 xmlns = "http://www.w3.org/2000/svg"
0 commit comments