File size: 10,828 Bytes
d2169e1
eb3147c
 
ce56fb1
d0e945e
9d82a8d
1e4d367
88eb790
552dfdc
77b155b
8cb9c5e
def3f23
5af9f11
afee803
5b1f45d
9243a73
 
 
 
 
 
 
 
 
 
 
 
5b1f45d
9243a73
def3f23
046b27e
5af9f11
 
9243a73
ce56fb1
5af9f11
1e4d367
d0e945e
046b27e
 
 
 
 
 
06c006c
88eb790
404a444
 
0e5c866
 
9243a73
404a444
9243a73
 
2d79f8e
9243a73
 
046b27e
641143e
9243a73
046b27e
 
9243a73
7171bbb
 
 
 
fc91167
9243a73
def3f23
7171bbb
def3f23
 
d2169e1
def3f23
9243a73
5b1f45d
9243a73
def3f23
9243a73
 
641143e
9243a73
 
88eb790
9243a73
 
88eb790
9243a73
 
88eb790
7171bbb
88eb790
7171bbb
9243a73
 
 
cc1a9d9
7171bbb
 
9243a73
0e5c866
88eb790
9243a73
 
def3f23
7171bbb
 
9243a73
7171bbb
 
 
641143e
 
7171bbb
046b27e
 
 
5b1f45d
 
641143e
046b27e
9243a73
77b155b
5b1f45d
def3f23
 
5b1f45d
77b155b
def3f23
9243a73
 
 
 
 
def3f23
9243a73
046b27e
def3f23
9243a73
9d82a8d
7171bbb
88eb790
9243a73
9d82a8d
7171bbb
 
9243a73
ce651b0
88eb790
def3f23
641143e
7171bbb
 
def3f23
7171bbb
 
88eb790
 
9243a73
9d82a8d
9bbceff
4604dc0
d48cca8
d0e945e
06c006c
9243a73
dd31fa2
d0e945e
d2169e1
7171bbb
88eb790
7171bbb
 
cc1a9d9
9243a73
cc1a9d9
9243a73
046b27e
7171bbb
9243a73
 
 
 
cc1a9d9
9243a73
ce56fb1
9bbceff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import os
import pandas as pd
import numpy as np
import gradio as gr
import plotly.graph_objects as go
from google import genai

# --- CONFIG AI ---
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
client_ai = genai.Client(api_key=GOOGLE_API_KEY) if GOOGLE_API_KEY else None

# --- UI STYLE: BANK NAGARI PREMIUM ---
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap');
body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #FFFFFF !important; }

.nagari-header { 
    background: linear-gradient(135deg, #05A4DE 0%, #82C3EB 100%); 
    padding: 35px; border-radius: 15px; border-bottom: 6px solid #F7BD87; 
    margin-bottom: 25px; text-align: center;
}
.nagari-header h1 { color: #FFFFFF !important; font-weight: 800 !important; margin: 0; font-size: 2.2em; text-shadow: 2px 2px 4px rgba(0,0,0,0.2); }

.card-sidebar { 
    background: #E0EDF4; border-radius: 15px; padding: 25px; 
    border: 1.5px solid #82C3EB; box-shadow: 0 4px 12px rgba(5, 164, 222, 0.1); 
}
.health-badge { background: white; padding: 12px; border-radius: 8px; margin-bottom: 12px; border-left: 5px solid #05A4DE; font-size: 0.95em; }

.report-card { background: white; border-radius: 12px; padding: 30px; border: 1px solid #E2E8F0; line-height: 1.8; color: #1e293b; }
.nbo-box { background: #fffdf0; border: 2px solid #F7BD87; padding: 20px; border-radius: 10px; margin-top: 20px; }
"""

class ArchonMasterEngine:
    def __init__(self):
        self.load_data()

    def load_data(self):
        try:
            self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).sort_values('date')
            self.df_cust = pd.read_csv('customers.csv')
            self.df_bal = pd.read_csv('balances_revised.csv', parse_dates=['month']).sort_values('month')
            self.df_rep = pd.read_csv('repayments_revised.csv', parse_dates=['due_date']).fillna("on_time")
        except Exception as e: print(f"Load Error: {e}")

    def analyze(self, customer_id):
        cid = str(customer_id).strip().upper()
        u_txn = self.df_txn[self.df_txn['customer_id'] == cid].copy()
        u_bal = self.df_bal[self.df_bal['customer_id'] == cid].sort_values('month')
        u_rep = self.df_rep[self.df_rep['customer_id'] == cid]
        u_info_df = self.df_cust[self.df_cust['customer_id'] == cid]

        if u_txn.empty or u_info_df.empty: return None
        u_info = u_info_df.iloc[0]

        # --- FASE 2: INTELLIGENCE (Semantic Parser) ---
        essential_keywords = ['indomaret', 'alfamart', 'listrik', 'pdam', 'telkom', 'sekolah', 'rs ', 'obat', 'cicilan', 'pinjaman', 'gaji', 'asuransi', 'grocer', 'utilities']
        def classify_exp(row):
            desc = str(row.get('raw_description', '')).lower()
            return 'essential' if any(k in desc for k in essential_keywords) else 'discretionary'
        u_txn['expense_type'] = u_txn.apply(classify_exp, axis=1)

        # --- FASE 3 & 4: RISK SCORING ---
        income_txn = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
        ref_income = max(income_txn, u_info['monthly_income'])
        expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
        er = expense / ref_income if ref_income > 0 else 1.0
        
        # Bobot 30/20/20/20/10
        er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
        bt_s = 1.0 if len(u_bal) >= 2 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[0]['avg_balance'] else 0.0
        od_s = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
        mp_s = 1.0 if (u_rep['status'] == 'late').any() else 0.0
        
        score = (0.3 * er_s) + (0.2 * bt_s) + (0.2 * od_s) + (0.2 * mp_s) + 0.05
        risk_lv = "HIGH" if score >= 0.7 else ("MEDIUM" if score >= 0.4 else "LOW")

        # --- FASE 5: NBO ENGINE ---
        if risk_lv == "HIGH" or mp_s == 1:
            action, nbo_msg = "Program Restrukturisasi", "Kami menyarankan penyesuaian jadwal pembayaran cicilan agar kondisi keuangan Anda tetap terjaga."
            steps = ["Ajukan peninjauan tenor", "Konsolidasi tagihan harian", "Manfaatkan konsultasi dana."]
        elif er > 0.6:
            action, nbo_msg = "Pengaturan Limit Belanja (Spending Control)", "Kami mendeteksi pola belanja yang tinggi. Anda disarankan mengatur limit transaksi harian agar tabungan tetap aman."
            steps = ["Atur limit harian QRIS", "Aktifkan notifikasi saldo", "Prioritaskan kebutuhan pokok."]
        elif risk_lv == "LOW":
            action, nbo_msg = "Optimalkan Tabungan (Promote Saving)", "Kondisi keuangan Anda sangat prima. Ini waktu yang tepat untuk menumbuhkan aset Anda melalui Deposito."
            steps = ["Buka tabungan berjangka", "Pindahkan dana ke deposito", "Eksplorasi produk reksa dana."]
        else:
            action, nbo_msg = "Edukasi Pengelolaan Kas", "Mari perkuat daya tahan keuangan Anda dengan tips pengelolaan arus kas di aplikasi mobile kami."
            steps = ["Baca artikel cerdas belanja", "Gunakan fitur budgeting", "Review pengeluaran bulanan."]

        return risk_lv, score, er, u_bal, u_txn, expense, ref_income, action, nbo_msg, steps, er_s, bt_s, od_s, mp_s

    def build_narrative(self, risk_lv, score, er, u_bal, exp, inc, action, nbo_msg, steps, cid, u_txn):
        # FASE 6: EXPLAINABLE SUMMARY
        msg = f"### LAPORAN ANALISIS RESILIENSI FINANSIAL ANDA\n\n"
        msg += f"Halo Bapak/Ibu, sistem Archon menetapkan tingkat kesehatan finansial Anda ({cid}) pada kategori **{risk_lv}** (Skor: {score:.2f}).\n\n"
        
        msg += f"**1. Mengapa Skor Ini Muncul?**\n"
        msg += f"* **Efisiensi Belanja ({er:.1%})**: Anda menghabiskan Rp{exp:,.0f} dari pendapatan Rp{inc:,.0f}. "
        msg += "Ini menunjukkan pengeluaran yang melebihi pendapatan (defisit)." if er > 1 else "Manajemen belanja Anda terpantau cukup terkendali."
        
        if not u_bal.empty:
            msg += f"\n* **Analisis Saldo**: Saldo rata-rata Anda Rp{u_bal.iloc[-1]['avg_balance']:,.0f}. "
            msg += "Terdeteksi tren penurunan saldo, disarankan untuk mulai menabung kembali." if len(u_bal) > 1 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[0]['avg_balance'] else "Saldo Anda tumbuh dengan stabil dan aman."

        msg += f"\n\n<div class='nbo-box'>REKOMENDASI UNTUK ANDA: {action}\n\n"
        msg += f"**Tujuan:** {nbo_msg}\n\n"
        msg += f"**Langkah-Langkah Implementasi:**\n"
        for i, step in enumerate(steps, 1): msg += f"{i}. {step}\n"
        msg += f"</div>"
        return msg

    def create_viz(self, u_bal, u_txn):
        # 1. Cashflow
        u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
        cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
        f1 = go.Figure()
        f1.add_trace(go.Bar(x=cf.index, y=cf.get('credit', 0), name='Pemasukan (Inflow)', marker_color='#82C3EB'))
        f1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran (Outflow)', marker_color='#05A4DE'))
        f1.update_layout(title="Arus Kas Bulanan", barmode='group', template='plotly_white')

        # 2. Pie Chart
        exp_dist = u_txn[u_txn['transaction_type'] == 'debit'].groupby('expense_type')['amount'].sum().reset_index()
        color_map = {'essential': '#05A4DE', 'discretionary': '#F7BD87'}
        f2 = go.Figure(data=[go.Pie(
            labels=exp_dist['expense_type'], values=exp_dist['amount'], hole=.4,
            marker=dict(colors=[color_map.get(x, '#E0EDF4') for x in exp_dist['expense_type']])
        )])
        f2.update_layout(title="Komposisi Pengeluaran (Kebutuhan vs Gaya Hidup)")

        # 3. Tren Saldo (Restored)
        f3 = go.Figure()
        f3.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Saldo Rata-rata', line=dict(color='#F7BD87', width=4)))
        f3.update_layout(title="Tren Pertumbuhan Saldo", template='plotly_white')
        
        return f1, f2, f3

# --- UI LOGIC ---
engine = ArchonMasterEngine()

def run_app(cust_id):
    res = engine.analyze(cust_id)
    if not res: return "ID Tidak Valid", "Gunakan ID C0001 - C0120", None, None, None
    
    risk_lv, score, er, u_bal, u_txn, exp, inc, action, nbo_msg, steps, er_s, bt_s, od_s, mp_s = res
    report = engine.build_narrative(risk_lv, score, er, u_bal, exp, inc, action, nbo_msg, steps, cust_id, u_txn)
    p1, p2, p3 = engine.create_viz(u_bal, u_txn)
    
    color = "#ef4444" if risk_lv == "HIGH" else ("#f59e0b" if risk_lv == "MEDIUM" else "#10b981")
    sidebar = f"""
    <div class='card-sidebar'>
        <h2 style='color: #05A4DE; margin:0;'>Status Keuangan</h2>
        <div style='background:{color}; color:white; padding:10px 20px; border-radius:30px; font-weight:bold; display:inline-block; margin:15px 0;'>{risk_lv} RISK LEVEL</div>
        <div class='health-badge'><b>Skor Risiko:</b> {score:.2f} / 1.00</div>
        <div class='health-badge'><b>Efisiensi Belanja:</b> {er:.1%} {'⚠️' if er > 0.8 else '✔️'}</div>
        <div class='health-badge'><b>Kesehatan Saldo:</b> {'🔻 Menurun' if bt_s == 1 else '🔺 Stabil'}</div>
    </div>
    """
    return sidebar, report, p1, p2, p3

with gr.Blocks(css=custom_css) as demo:
    gr.HTML("<div class='nagari-header'><h1>ARCHON-AI</h1></div>")
    with gr.Row():
        with gr.Column(scale=1):
            id_in = gr.Textbox(label="Customer ID", placeholder="C0001")
            btn = gr.Button("ANALYZE CUSTOMER", variant="primary")
            out_side = gr.HTML()
        with gr.Column(scale=2):
            with gr.Tabs():
                with gr.Tab("Laporan Untuk Anda"):
                    out_report = gr.Markdown(elem_classes="report-card")
                with gr.Tab("Visualisasi Keuangan"):
                    gr.Markdown("### 1. Uang Masuk vs Uang Keluar")
                    plot_cf = gr.Plot()
                    gr.HTML("<div style='background:#f1f5f9; padding:15px; border-radius:8px;'><b>Interpretasi:</b> Batang muda (Inflow) idealnya lebih tinggi dari batang tua (Outflow).</div>")
                    gr.Markdown("---")
                    gr.Markdown("### 2. Komposisi Gaya Hidup")
                    plot_dist = gr.Plot()
                    gr.HTML("<div style='background:#f1f5f9; padding:15px; border-radius:8px;'><b>Warna:</b> Biru = Kebutuhan (Essential). Emas = Gaya Hidup (Discretionary).</div>")
                    gr.Markdown("---")
                    gr.Markdown("### 3. Tren Pertumbuhan Saldo")
                    plot_bal = gr.Plot()
                    gr.HTML("<div style='background:#f1f5f9; padding:15px; border-radius:8px;'><b>Interpretasi:</b> Menunjukkan daya tahan tabungan Anda terhadap krisis.</div>")

    btn.click(fn=run_app, inputs=id_in, outputs=[out_side, out_report, plot_cf, plot_dist, plot_bal])

demo.launch()