1-
2- import React from 'react' ;
3- import { Badge } from '@/components/ui/badge' ;
4- import { ArrowUp , ArrowDown , AlertCircle , Circle } from 'lucide-react' ;
5- import { Transaction } from '@/pages/Index' ;
1+ import React , { useState , useEffect } from 'react' ;
62import { Card , CardContent , CardHeader , CardTitle } from '@/components/ui/card' ;
3+ import { Quote } from 'lucide-react' ;
4+ import { Transaction } from '@/pages/Index' ;
75
86interface SmartInsightsProps {
97 transactions : Transaction [ ] ;
108}
119
12- const SmartInsights : React . FC < SmartInsightsProps > = ( { transactions } ) => {
13- const generateInsights = ( ) => {
14- const insights : Array < {
15- type : 'success' | 'warning' | 'info' | 'danger' ;
16- icon : React . ReactNode ;
17- title : string ;
18- message : string ;
19- } > = [ ] ;
20-
21- const currentDate = new Date ( ) ;
22- const currentMonth = currentDate . getMonth ( ) ;
23- const currentYear = currentDate . getFullYear ( ) ;
24- const lastMonth = currentMonth === 0 ? 11 : currentMonth - 1 ;
25- const lastMonthYear = currentMonth === 0 ? currentYear - 1 : currentYear ;
26-
27- // Current month transactions
28- const thisMonthTransactions = transactions . filter ( t => {
29- const date = new Date ( t . date ) ;
30- return date . getMonth ( ) === currentMonth && date . getFullYear ( ) === currentYear ;
31- } ) ;
32-
33- // Last month transactions
34- const lastMonthTransactions = transactions . filter ( t => {
35- const date = new Date ( t . date ) ;
36- return date . getMonth ( ) === lastMonth && date . getFullYear ( ) === lastMonthYear ;
37- } ) ;
38-
39- const thisMonthIncome = thisMonthTransactions . filter ( t => t . type === 'income' ) . reduce ( ( sum , t ) => sum + t . amount , 0 ) ;
40- const thisMonthExpense = thisMonthTransactions . filter ( t => t . type === 'expense' ) . reduce ( ( sum , t ) => sum + t . amount , 0 ) ;
41- const lastMonthIncome = lastMonthTransactions . filter ( t => t . type === 'income' ) . reduce ( ( sum , t ) => sum + t . amount , 0 ) ;
42- const lastMonthExpense = lastMonthTransactions . filter ( t => t . type === 'expense' ) . reduce ( ( sum , t ) => sum + t . amount , 0 ) ;
43-
44- // Income trend analysis
45- if ( lastMonthIncome > 0 ) {
46- const incomeChange = ( ( thisMonthIncome - lastMonthIncome ) / lastMonthIncome ) * 100 ;
47- if ( incomeChange > 10 ) {
48- insights . push ( {
49- type : 'success' ,
50- icon : < ArrowUp className = "h-4 w-4" /> ,
51- title : 'Pemasukan Meningkat!' ,
52- message : `Pemasukan Anda naik ${ incomeChange . toFixed ( 1 ) } % dibanding bulan lalu. Pertahankan!`
53- } ) ;
54- } else if ( incomeChange < - 10 ) {
55- insights . push ( {
56- type : 'warning' ,
57- icon : < ArrowDown className = "h-4 w-4" /> ,
58- title : 'Pemasukan Menurun' ,
59- message : `Pemasukan turun ${ Math . abs ( incomeChange ) . toFixed ( 1 ) } % dibanding bulan lalu. Perlu perhatian.`
60- } ) ;
61- }
62- }
63-
64- // Expense trend analysis
65- if ( lastMonthExpense > 0 ) {
66- const expenseChange = ( ( thisMonthExpense - lastMonthExpense ) / lastMonthExpense ) * 100 ;
67- if ( expenseChange > 20 ) {
68- insights . push ( {
69- type : 'danger' ,
70- icon : < AlertCircle className = "h-4 w-4" /> ,
71- title : 'Pengeluaran Melonjak!' ,
72- message : `Pengeluaran naik ${ expenseChange . toFixed ( 1 ) } % dibanding bulan lalu. Cek kategori terbesar.`
73- } ) ;
74- } else if ( expenseChange < - 10 ) {
75- insights . push ( {
76- type : 'success' ,
77- icon : < ArrowDown className = "h-4 w-4" /> ,
78- title : 'Pengeluaran Terkendali' ,
79- message : `Pengeluaran turun ${ Math . abs ( expenseChange ) . toFixed ( 1 ) } % dibanding bulan lalu. Bagus!`
80- } ) ;
81- }
82- }
83-
84- // Category analysis
85- const expensesByCategory = thisMonthTransactions
86- . filter ( t => t . type === 'expense' )
87- . reduce ( ( acc , t ) => {
88- acc [ t . category ] = ( acc [ t . category ] || 0 ) + t . amount ;
89- return acc ;
90- } , { } as Record < string , number > ) ;
91-
92- const topCategory = Object . entries ( expensesByCategory )
93- . sort ( ( [ , a ] , [ , b ] ) => b - a ) [ 0 ] ;
94-
95- if ( topCategory && topCategory [ 1 ] > thisMonthExpense * 0.4 ) {
96- insights . push ( {
97- type : 'info' ,
98- icon : < Circle className = "h-4 w-4" /> ,
99- title : 'Kategori Dominan' ,
100- message : `${ topCategory [ 0 ] } menghabiskan ${ ( ( topCategory [ 1 ] / thisMonthExpense ) * 100 ) . toFixed ( 1 ) } % total pengeluaran.`
101- } ) ;
102- }
103-
104- // Savings rate
105- if ( thisMonthIncome > 0 ) {
106- const savingsRate = ( ( thisMonthIncome - thisMonthExpense ) / thisMonthIncome ) * 100 ;
107- if ( savingsRate > 20 ) {
108- insights . push ( {
109- type : 'success' ,
110- icon : < ArrowUp className = "h-4 w-4" /> ,
111- title : 'Tingkat Tabungan Bagus' ,
112- message : `Anda menabung ${ savingsRate . toFixed ( 1 ) } % dari pendapatan. Luar biasa!`
113- } ) ;
114- } else if ( savingsRate < 0 ) {
115- insights . push ( {
116- type : 'danger' ,
117- icon : < AlertCircle className = "h-4 w-4" /> ,
118- title : 'Pengeluaran > Pemasukan' ,
119- message : 'Pengeluaran melebihi pemasukan bulan ini. Review anggaran Anda.'
120- } ) ;
121- }
122- }
123-
124- // Transaction frequency
125- const dailyTransactions = thisMonthTransactions . length / currentDate . getDate ( ) ;
126- if ( dailyTransactions > 5 ) {
127- insights . push ( {
128- type : 'info' ,
129- icon : < Circle className = "h-4 w-4" /> ,
130- title : 'Aktivitas Transaksi Tinggi' ,
131- message : `Rata-rata ${ dailyTransactions . toFixed ( 1 ) } transaksi per hari. Pertimbangkan konsolidasi.`
132- } ) ;
133- }
134-
135- return insights ;
136- } ;
137-
138- const insights = generateInsights ( ) ;
139-
140- if ( insights . length === 0 ) {
141- return null ;
142- }
143-
144- const getInsightStyle = ( type : string ) => {
145- switch ( type ) {
146- case 'success' :
147- return 'bg-success/10 border-success/30' ;
148- case 'warning' :
149- return 'bg-primary/10 border-primary/30' ;
150- case 'danger' :
151- return 'bg-destructive/10 border-destructive/30' ;
152- case 'info' :
153- default :
154- return 'bg-muted/40 border-border' ;
155- }
156- } ;
157-
158- const getTextStyle = ( type : string ) => {
159- switch ( type ) {
160- case 'success' :
161- return 'text-success' ;
162- case 'warning' :
163- return 'text-primary' ;
164- case 'danger' :
165- return 'text-destructive' ;
166- case 'info' :
167- default :
168- return 'text-foreground' ;
169- }
170- } ;
10+ const MONEY_QUOTES = [
11+ "Uang tidak bisa membeli kebahagiaan, tapi lebih enak menangis di dalam Mercedes daripada di atas sepeda." ,
12+ "Hemat pangkal kaya, tapi kalau pelit pangkal dijauhi teman." ,
13+ "Jangan menabung apa yang tersisa setelah belanja, tapi belanjalah apa yang tersisa setelah menabung." ,
14+ "Aturan No.1: Jangan pernah rugi. Aturan No.2: Jangan lupa Aturan No.1." ,
15+ "Terlalu banyak orang menghabiskan uang yang tidak mereka miliki, untuk membeli barang yang tidak mereka butuhkan, demi membuat kagum orang yang tidak mereka sukai." ,
16+ "Investasi dalam pengetahuan selalu membayar bunga terbaik." ,
17+ "Bukan seberapa banyak uang yang kamu hasilkan, tapi seberapa banyak yang kamu simpan." ,
18+ "Uang adalah hamba yang baik, tapi tuan yang buruk." ,
19+ "Orang kaya punya TV kecil dan perpustakaan besar. Orang miskin punya TV besar dan tidak punya perpustakaan." ,
20+ "Hati-hati dengan pengeluaran kecil; kebocoran kecil bisa menenggelamkan kapal besar." ,
21+ "Saya suka uang saya berada di tempat yang bisa saya lihat: tergantung di lemari pakaian saya. (Carrie Bradshaw)" ,
22+ "Satu-satunya cara untuk tidak memikirkan uang adalah dengan memiliki banyak uang." ,
23+ "Waktu adalah uang. Kalau kamu buang-buang waktu, kamu buang-buang uang." ,
24+ "Gaji itu seperti menstruasi, datang sebulan sekali, tapi nyerinya bisa seminggu." ,
25+ "Jika Anda berutang $100 pada bank, itu masalah Anda. Jika Anda berutang $100 juta, itu masalah bank."
26+ ] ;
27+
28+ const SmartInsights : React . FC < SmartInsightsProps > = ( ) => {
29+ const [ quote , setQuote ] = useState ( "" ) ;
30+
31+ useEffect ( ( ) => {
32+ // Pick a random quote on mount
33+ const randomQuote = MONEY_QUOTES [ Math . floor ( Math . random ( ) * MONEY_QUOTES . length ) ] ;
34+ setQuote ( randomQuote ) ;
35+ } , [ ] ) ;
17136
17237 return (
173- < Card >
174- < CardHeader className = "pb-4 " >
175- < CardTitle className = "flex items-center gap-2 text-base" >
176- < Circle className = "h-5 w-5 text-primary" />
177- Wawasan Keuangan
38+ < Card className = "border-2 border-primary/20 shadow-[4px_4px_0px_0px_rgba(var(--primary),0.2)] hover:shadow-[6px_6px_0px_0px_rgba(var(--primary),0.4)] transition-all duration-300" >
39+ < CardHeader className = "pb-2 " >
40+ < CardTitle className = "flex items-center gap-2 text-base font-display " >
41+ < Quote className = "h-5 w-5 text-primary rotate-180 " />
42+ Kata Bijak (Mungkin)
17843 </ CardTitle >
17944 </ CardHeader >
18045 < CardContent >
181- < div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
182- { insights . map ( ( insight , index ) => (
183- < div
184- key = { index }
185- className = { `rounded-lg border p-4 ${ getInsightStyle ( insight . type ) } ` }
186- >
187- < div className = "flex items-start gap-3" >
188- < div className = { getTextStyle ( insight . type ) } >
189- { insight . icon }
190- </ div >
191- < div className = "flex-1" >
192- < h4 className = { `font-medium mb-1 ${ getTextStyle ( insight . type ) } ` } >
193- { insight . title }
194- </ h4 >
195- < p className = "text-sm text-muted-foreground" >
196- { insight . message }
197- </ p >
198- </ div >
199- < Badge variant = { insight . type === 'success' ? 'default' : 'secondary' } className = "text-xs" >
200- { insight . type === 'success' ? 'Bagus' :
201- insight . type === 'warning' ? 'Perhatian' :
202- insight . type === 'danger' ? 'Penting' : 'Info' }
203- </ Badge >
204- </ div >
205- </ div >
206- ) ) }
207- </ div >
46+ < div className = "relative p-6 bg-primary/5 rounded-lg border border-dashed border-primary/30" >
47+ < Quote className = "absolute top-2 left-2 h-8 w-8 text-primary/10 -scale-x-100" />
48+ < p className = "text-lg font-medium text-center italic text-foreground/80 font-serif leading-relaxed" >
49+ "{ quote } "
50+ </ p >
51+ < Quote className = "absolute bottom-2 right-2 h-8 w-8 text-primary/10" />
52+ </ div >
20853 </ CardContent >
20954 </ Card >
21055 ) ;
21156} ;
21257
213- export default SmartInsights ;
58+ export default SmartInsights ;
0 commit comments