danghungithp commited on
Commit
8dbb2f9
·
verified ·
1 Parent(s): f87b408

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +824 -0
app.py ADDED
@@ -0,0 +1,824 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import numpy as np
4
+ from datetime import datetime
5
+ import os
6
+ import uuid
7
+ import requests
8
+ import json
9
+ from functools import lru_cache
10
+
11
+ # ==============================================================================
12
+ # 1. CẤU HÌNH VÀ HẰNG SỐ
13
+ # ==============================================================================
14
+
15
+ # Thiết lập API Keys từ Biến môi trường (Cần đặt trong Hugging Face Secrets)
16
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
17
+ VOUCHER_AUTH_TOKEN = os.environ.get("VOUCHER_AUTH_TOKEN", "Token TGHFVZbSBxPoAF-4_al6NU-h47fzcvyQ")
18
+
19
+ # API Endpoints
20
+ GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent"
21
+ VOUCHER_API_URL = "https://api.accesstrade.vn/v1/offers_informations"
22
+ PRODUCTS_API_URL = "https://api.accesstrade.vn/v2/tiktokshop_product_feeds"
23
+
24
+ # CSV Files
25
+ TRANSACTIONS_CSV = 'transactions.csv'
26
+ INVESTMENTS_CSV = 'investments.csv'
27
+ # User ID cố định cho mô hình CSV đơn giản
28
+ MOCK_USER_ID = 'family_finance_user'
29
+
30
+ # ==============================================================================
31
+ # 2. XỬ LÝ DỮ LIỆU CSV (PANDAS)
32
+ # ==============================================================================
33
+
34
+ def load_data(file_path, default_columns):
35
+ """Tải dữ liệu từ CSV hoặc tạo DataFrame mới nếu file không tồn tại."""
36
+ try:
37
+ df = pd.read_csv(file_path)
38
+ # Đảm bảo các cột ngày tháng được parse đúng cách
39
+ if 'date' in df.columns:
40
+ df['date'] = pd.to_datetime(df['date'])
41
+ if 'purchaseDate' in df.columns:
42
+ df['purchaseDate'] = pd.to_datetime(df['purchaseDate'])
43
+
44
+ # Bổ sung các cột bị thiếu nếu cần
45
+ for col in default_columns:
46
+ if col not in df.columns:
47
+ df[col] = pd.NA
48
+
49
+ except FileNotFoundError:
50
+ df = pd.DataFrame(columns=default_columns)
51
+ # Khởi tạo một số dữ liệu mock nếu file không tồn tại
52
+ if file_path == TRANSACTIONS_CSV:
53
+ mock_data = {
54
+ 'id': ['t1', 't2', 't3'],
55
+ 'user_id': [MOCK_USER_ID] * 3,
56
+ 'type': ['INCOME', 'EXPENSE', 'EXPENSE'],
57
+ 'category': ['Lương', 'Ăn uống', 'Giải trí'],
58
+ 'amount': [30000000, 5000000, 2000000],
59
+ 'date': [datetime(2025, 6, 1), datetime(2025, 6, 5), datetime(2025, 6, 15)],
60
+ 'description': ['Lương tháng 6', 'Chi phí ăn uống gia đình', 'Xem phim và du lịch ngắn'],
61
+ }
62
+ df = pd.DataFrame(mock_data)
63
+ elif file_path == INVESTMENTS_CSV:
64
+ mock_data = {
65
+ 'id': ['i1', 'i2'],
66
+ 'user_id': [MOCK_USER_ID] * 2,
67
+ 'ticker': ['VNM', 'FTSE'],
68
+ 'quantity': [500, 100],
69
+ 'averagePrice': [75000, 150000],
70
+ 'currentValue': [78000, 145000],
71
+ 'purchaseDate': [datetime(2024, 1, 10), datetime(2024, 5, 20)],
72
+ }
73
+ df = pd.DataFrame(mock_data)
74
+
75
+ # Sắp xếp theo ngày giảm dần
76
+ if 'date' in df.columns:
77
+ df = df.sort_values(by='date', ascending=False)
78
+
79
+ return df
80
+
81
+ def save_data(df, file_path):
82
+ """Lưu DataFrame vào CSV."""
83
+ df.to_csv(file_path, index=False)
84
+
85
+ # Tải dữ liệu lần đầu
86
+ transaction_cols = ['id', 'user_id', 'type', 'category', 'amount', 'date', 'description']
87
+ investment_cols = ['id', 'user_id', 'ticker', 'quantity', 'averagePrice', 'currentValue', 'purchaseDate']
88
+ transactions_df = load_data(TRANSACTIONS_CSV, transaction_cols)
89
+ investments_df = load_data(INVESTMENTS_CSV, investment_cols)
90
+
91
+ # ==============================================================================
92
+ # 3. HÀM HỖ TRỢ CHUNG
93
+ # ==============================================================================
94
+
95
+ def format_currency(amount):
96
+ """Định dạng tiền tệ VND."""
97
+ if pd.isna(amount):
98
+ return "N/A"
99
+ return f"{int(amount):,} VND".replace(",", ".")
100
+
101
+ def format_percentage(value):
102
+ """Định dạng phần trăm."""
103
+ if pd.isna(value):
104
+ return "N/A"
105
+ return f"{value:.2f}%"
106
+
107
+ def get_current_month_data(df):
108
+ """Lọc dữ liệu giao dịch trong tháng hiện tại."""
109
+ if df.empty or 'date' not in df.columns:
110
+ return pd.DataFrame(columns=transaction_cols)
111
+
112
+ today = datetime.now()
113
+ start_of_month = datetime(today.year, today.month, 1)
114
+ # Lọc giao dịch của người dùng hiện tại (MOCK_USER_ID)
115
+ filtered_df = df[df['user_id'] == MOCK_USER_ID]
116
+
117
+ return filtered_df[
118
+ (filtered_df['date'] >= start_of_month) &
119
+ (filtered_df['date'] <= today)
120
+ ]
121
+
122
+ # ==============================================================================
123
+ # 4. LOGIC TABS
124
+ # ==============================================================================
125
+
126
+ def update_dataframes():
127
+ """Tải lại DataFrames từ CSV (được gọi sau mỗi thao tác ghi)."""
128
+ global transactions_df, investments_df
129
+ transactions_df = load_data(TRANSACTIONS_CSV, transaction_cols)
130
+ investments_df = load_data(INVESTMENTS_CSV, investment_cols)
131
+ return transactions_df, investments_df
132
+
133
+ # --- TAB DASHBOARD ---
134
+
135
+ def calculate_dashboard_stats(transactions_df, investments_df):
136
+ """Tính toán các chỉ số Dashboard."""
137
+
138
+ # 1. Stats Thu Chi Tháng này
139
+ monthly_df = get_current_month_data(transactions_df)
140
+
141
+ total_income = monthly_df[monthly_df['type'] == 'INCOME']['amount'].sum()
142
+ total_expense = monthly_df[monthly_df['type'] == 'EXPENSE']['amount'].sum()
143
+ net_savings = total_income - total_expense
144
+
145
+ # 2. Stats Đầu tư
146
+ investments_df_user = investments_df[investments_df['user_id'] == MOCK_USER_ID]
147
+ total_invested = (investments_df_user['quantity'] * investments_df_user['averagePrice']).sum()
148
+ total_current_value = (investments_df_user['quantity'] * investments_df_user['currentValue']).sum()
149
+ net_return = total_current_value - total_invested
150
+ roi = (net_return / total_invested) * 100 if total_invested > 0 else 0
151
+
152
+ # 3. Phân tích chi tiêu
153
+ expense_by_category = monthly_df[monthly_df['type'] == 'EXPENSE'].groupby('category')['amount'].sum().reset_index()
154
+ expense_by_category['amount_formatted'] = expense_by_category['amount'].apply(format_currency)
155
+ expense_by_category = expense_by_category.sort_values(by='amount', ascending=False)
156
+
157
+ # Chuẩn bị output cho Gradio
158
+ stats_output = [
159
+ format_currency(total_income),
160
+ format_currency(total_expense),
161
+ format_currency(net_savings),
162
+ format_percentage(roi),
163
+ ]
164
+
165
+ # Dataframe cho Phân tích
166
+ if expense_by_category.empty:
167
+ expense_table = pd.DataFrame({'Danh Mục': ['N/A'], 'Số Tiền': ['N/A']})
168
+ else:
169
+ expense_table = expense_by_category[['category', 'amount_formatted']].rename(columns={'category': 'Danh Mục', 'amount_formatted': 'Số Tiền'})
170
+
171
+ return stats_output + [expense_table]
172
+
173
+
174
+ # --- TAB TRANSACTIONS ---
175
+
176
+ def add_transaction(transactions_df, type, category, amount, date, description):
177
+ """Thêm giao dịch mới vào DataFrame và lưu CSV."""
178
+
179
+ if not amount or float(amount) <= 0:
180
+ return gr.update(visible=True, value="Lỗi: Số tiền phải lớn hơn 0."), transactions_df.head(10)
181
+
182
+ new_row = pd.DataFrame([{
183
+ 'id': str(uuid.uuid4()),
184
+ 'user_id': MOCK_USER_ID,
185
+ 'type': type,
186
+ 'category': category,
187
+ 'amount': float(amount),
188
+ 'date': pd.to_datetime(date),
189
+ 'description': description
190
+ }])
191
+
192
+ transactions_df = pd.concat([new_row, transactions_df], ignore_index=True)
193
+ save_data(transactions_df, TRANSACTIONS_CSV)
194
+
195
+ # Tải lại DF và cập nhật bảng
196
+ transactions_df, _ = update_dataframes()
197
+ return gr.update(visible=False), transactions_df.head(10)
198
+
199
+ def delete_transaction(transactions_df, evt: gr.SelectData):
200
+ """Xóa giao dịch khỏi DataFrame và lưu CSV."""
201
+ if not evt.value:
202
+ return transactions_df.head(10)
203
+
204
+ # Lấy ID từ hàng được chọn
205
+ selected_row = transactions_df.iloc[evt.index[0]]
206
+ transaction_id = selected_row['id']
207
+
208
+ # Xóa hàng
209
+ transactions_df = transactions_df[transactions_df['id'] != transaction_id]
210
+ save_data(transactions_df, TRANSACTIONS_CSV)
211
+
212
+ # Tải lại DF và cập nhật bảng
213
+ transactions_df, _ = update_dataframes()
214
+ return transactions_df.head(10)
215
+
216
+ # --- TAB INVESTMENTS ---
217
+
218
+ def add_investment(investments_df, ticker, quantity, average_price, current_value, purchase_date):
219
+ """Thêm khoản đầu tư mới vào DataFrame và lưu CSV."""
220
+
221
+ if not ticker or not quantity or float(quantity) <= 0 or float(average_price) <= 0:
222
+ return gr.update(visible=True, value="Lỗi: Vui lòng nhập đầy đủ Ticker, Số lượng, và Giá mua."), investments_df.head(10)
223
+
224
+ new_row = pd.DataFrame([{
225
+ 'id': str(uuid.uuid4()),
226
+ 'user_id': MOCK_USER_ID,
227
+ 'ticker': ticker.upper(),
228
+ 'quantity': float(quantity),
229
+ 'averagePrice': float(average_price),
230
+ 'currentValue': float(current_value) if current_value else float(average_price),
231
+ 'purchaseDate': pd.to_datetime(purchase_date)
232
+ }])
233
+
234
+ investments_df = pd.concat([new_row, investments_df], ignore_index=True)
235
+ save_data(investments_df, INVESTMENTS_CSV)
236
+
237
+ # Tải lại DF và cập nhật bảng
238
+ _, investments_df = update_dataframes()
239
+ return gr.update(visible=False), investments_df.head(10)
240
+
241
+ def delete_investment(investments_df, evt: gr.SelectData):
242
+ """Xóa khoản đầu tư khỏi DataFrame và lưu CSV."""
243
+ if not evt.value:
244
+ return investments_df.head(10)
245
+
246
+ selected_row = investments_df.iloc[evt.index[0]]
247
+ investment_id = selected_row['id']
248
+
249
+ investments_df = investments_df[investments_df['id'] != investment_id]
250
+ save_data(investments_df, INVESTMENTS_CSV)
251
+
252
+ # Tải lại DF và cập nhật bảng
253
+ _, investments_df = update_dataframes()
254
+ return investments_df.head(10)
255
+
256
+ def calculate_investment_report(investments_df):
257
+ """Tính toán báo cáo đầu tư tổng thể."""
258
+ df_user = investments_df[investments_df['user_id'] == MOCK_USER_ID]
259
+
260
+ if df_user.empty:
261
+ return ["0 VND"] * 3 + ["0.00%"], [pd.DataFrame({'Tổng Quan': ['Chưa có dữ liệu đầu tư.']})] * 4
262
+
263
+ total_invested = (df_user['quantity'] * df_user['averagePrice']).sum()
264
+ total_current_value = (df_user['quantity'] * df_user['currentValue']).sum()
265
+ net_return = total_current_value - total_invested
266
+ roi = (net_return / total_invested) * 100 if total_invested > 0 else 0
267
+
268
+ summary_data = {
269
+ 'Chỉ Số': ['Tổng Vốn Đầu Tư', 'Giá Trị Hiện Tại', 'Lãi/Lỗ Ròng', 'ROI Ròng'],
270
+ 'Giá Trị': [
271
+ format_currency(total_invested),
272
+ format_currency(total_current_value),
273
+ format_currency(net_return),
274
+ format_percentage(roi)
275
+ ]
276
+ }
277
+
278
+ # Báo cáo chi tiết ROI theo từng khoản
279
+ df_report = df_user.copy()
280
+ df_report['Lãi/Lỗ (%)'] = ((df_report['currentValue'] - df_report['averagePrice']) / df_report['averagePrice']) * 100
281
+ df_report['Giá Mua TB'] = df_report['averagePrice'].apply(format_currency)
282
+ df_report['Giá Hiện Tại'] = df_report['currentValue'].apply(format_currency)
283
+ df_report['Lãi/Lỗ Ròng'] = (df_report['currentValue'] - df_report['averagePrice']) * df_report['quantity']
284
+ df_report['Lãi/Lỗ Ròng'] = df_report['Lãi/Lỗ Ròng'].apply(format_currency)
285
+
286
+ report_table = df_report[['ticker', 'quantity', 'Giá Mua TB', 'Giá Hiện Tại', 'Lãi/Lỗ Ròng', 'Lãi/Lỗ (%)']].rename(columns={'ticker': 'Mã CK', 'quantity': 'Số Lượng'})
287
+
288
+ return [
289
+ format_currency(total_invested),
290
+ format_currency(total_current_value),
291
+ format_currency(net_return),
292
+ format_percentage(roi),
293
+ ], [pd.DataFrame(summary_data), report_table]
294
+
295
+
296
+ # --- TAB REPORTS (Báo cáo Thu Chi) ---
297
+
298
+ def calculate_report_table(transactions_df, report_type):
299
+ """Tính toán báo cáo thu chi chi tiết theo Tuần, Tháng, Quý, Năm."""
300
+ if transactions_df.empty:
301
+ return pd.DataFrame({'Ngày/Kỳ': ['N/A'], 'Thu Nhập': [0], 'Chi Tiêu': [0], 'Tiết Kiệm Ròng': [0]})
302
+
303
+ df = transactions_df.copy()
304
+ df['date'] = pd.to_datetime(df['date'])
305
+
306
+ # Xử lý các loại báo cáo
307
+ if report_type == 'WEEK':
308
+ df['period'] = df['date'].dt.isocalendar().week.astype(str) + '-' + df['date'].dt.isocalendar().year.astype(str)
309
+ group_col = 'period'
310
+ col_name = 'Tuần-Năm'
311
+ elif report_type == 'MONTH':
312
+ df['period'] = df['date'].dt.to_period('M').astype(str)
313
+ group_col = 'period'
314
+ col_name = 'Tháng-Năm'
315
+ elif report_type == 'QUARTER':
316
+ df['period'] = df['date'].dt.to_period('Q').astype(str)
317
+ group_col = 'period'
318
+ col_name = 'Quý-Năm'
319
+ elif report_type == 'YEAR':
320
+ df['period'] = df['date'].dt.to_period('Y').astype(str)
321
+ group_col = 'period'
322
+ col_name = 'Năm'
323
+ else: # Mặc định là Tháng
324
+ df['period'] = df['date'].dt.to_period('M').astype(str)
325
+ group_col = 'period'
326
+ col_name = 'Tháng-Năm'
327
+
328
+ # Tạo bảng pivot
329
+ pivot_df = df.pivot_table(index=group_col, columns='type', values='amount', aggfunc='sum', fill_value=0)
330
+
331
+ if 'INCOME' not in pivot_df.columns:
332
+ pivot_df['INCOME'] = 0
333
+ if 'EXPENSE' not in pivot_df.columns:
334
+ pivot_df['EXPENSE'] = 0
335
+
336
+ pivot_df['Tiết Kiệm Ròng'] = pivot_df['INCOME'] - pivot_df['EXPENSE']
337
+
338
+ # Định dạng tiền tệ
339
+ pivot_df['Thu Nhập'] = pivot_df['INCOME'].apply(format_currency)
340
+ pivot_df['Chi Tiêu'] = pivot_df['EXPENSE'].apply(format_currency)
341
+ pivot_df['Tiết Kiệm Ròng'] = pivot_df['Tiết Kiệm Ròng'].apply(format_currency)
342
+
343
+ final_report = pivot_df[[ 'Thu Nhập', 'Chi Tiêu', 'Tiết Kiệm Ròng']].reset_index()
344
+ final_report = final_report.rename(columns={group_col: col_name})
345
+
346
+ # Sắp xếp theo kỳ giảm dần
347
+ final_report = final_report.sort_index(ascending=False)
348
+
349
+ return final_report
350
+
351
+ # --- TAB AI VÀ API CALLS ---
352
+
353
+ @lru_cache(maxsize=32)
354
+ def call_gemini_api(user_query, system_prompt, use_search=False, is_json=False):
355
+ """Hàm gọi API Gemini với logic retry và cache."""
356
+ if not GEMINI_API_KEY:
357
+ return {"error": "GEMINI_API_KEY chưa được cấu hình."}
358
+
359
+ payload = {
360
+ "contents": [{"parts": [{"text": user_query}]}],
361
+ "systemInstruction": {"parts": [{"text": system_prompt}]}
362
+ }
363
+ if use_search:
364
+ payload["tools"] = [{"google_search": {}}]
365
+
366
+ if is_json:
367
+ payload["generationConfig"] = {
368
+ "responseMimeType": "application/json",
369
+ "responseSchema": {
370
+ "type": "OBJECT",
371
+ "properties": {
372
+ "suggestions": {"type": "ARRAY", "items": {"type": "STRING"}},
373
+ "actions": {"type": "ARRAY", "items": {"type": "STRING"}}
374
+ }
375
+ }
376
+ }
377
+
378
+ headers = {'Content-Type': 'application/json'}
379
+
380
+ for i in range(3): # 3 lần thử với exponential backoff
381
+ try:
382
+ response = requests.post(f"{GEMINI_API_URL}?key={GEMINI_API_KEY}", headers=headers, json=payload, timeout=20)
383
+ response.raise_for_status()
384
+ result = response.json()
385
+
386
+ text = result.get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '')
387
+
388
+ if is_json:
389
+ return json.loads(text)
390
+
391
+ # Xử lý nguồn dẫn (grounding)
392
+ sources = []
393
+ grounding_metadata = result.get('candidates', [{}])[0].get('groundingMetadata', {})
394
+ if grounding_metadata and grounding_metadata.get('groundingAttributions'):
395
+ sources = [{
396
+ 'uri': attr.get('web', {}).get('uri'),
397
+ 'title': attr.get('web', {}).get('title')
398
+ } for attr in grounding_metadata['groundingAttributions']]
399
+
400
+ return {"text": text, "sources": sources}
401
+
402
+ except requests.exceptions.RequestException as e:
403
+ if i < 2:
404
+ time.sleep(2 ** i) # Backoff
405
+ continue
406
+ return {"error": f"Lỗi kết nối API Gemini: {e}"}
407
+ except json.JSONDecodeError:
408
+ return {"error": "Lỗi phân tích JSON từ phản hồi AI."}
409
+ return {"error": "Đã hết số lần thử kết nối API Gemini."}
410
+
411
+
412
+ def fetch_investment_suggestions():
413
+ """Lấy gợi ý đầu tư vĩ mô từ Gemini."""
414
+ system_prompt = "Act as a leading global financial advisor. Analyze the current macroeconomic situation and future projections (e.g., inflation, interest rates, currency movements) and provide specific, actionable investment suggestions. Your output MUST be a JSON object with two arrays: 'suggestions' (3 high-level themes) and 'actions' (2 specific actions/sectors). DO NOT use any markdown, only raw JSON."
415
+ user_query = "Phân tích tình hình vĩ mô hiện tại và đề xuất 3 chủ đề đầu tư sinh lời tốt hơn và 2 hành động cụ thể trong 6 tháng tới. Trả lời bằng tiếng Việt."
416
+
417
+ result = call_gemini_api(user_query, system_prompt, use_search=True, is_json=True)
418
+
419
+ if result.get("error"):
420
+ return result["error"], ""
421
+
422
+ suggestions_md = "\n".join([f"- {s}" for s in result.get("suggestions", [])])
423
+ actions_md = "\n".join([f"- {a}" for a in result.get("actions", [])])
424
+
425
+ output = f"**3 Chủ Đề Đầu Tư Cấp Cao:**\n{suggestions_md}\n\n**2 Hành Động/Lĩnh Vực Cụ Thể:**\n{actions_md}"
426
+ return "", output
427
+
428
+
429
+ def fetch_expense_suggestions(transactions_df):
430
+ """Lấy gợi ý tiết kiệm từ Gemini dựa trên lịch sử chi tiêu."""
431
+ system_prompt = "Act as a personalized financial coach for a family. Analyze the provided expense history (category and total amount) and give 3 specific, actionable tips on how to save money and 2 related tips on taking advantage of discounts/vouchers (referencing the current market). Your response MUST be in Vietnamese and use markdown bullet points. Do not include a greeting or conclusion. Start with a brief summary of the highest spending categories."
432
+
433
+ # Chuẩn bị lịch sử chi tiêu
434
+ monthly_df = transactions_df[transactions_df['type'] == 'EXPENSE']
435
+ expense_by_category = monthly_df.groupby('category')['amount'].sum().sort_values(ascending=False)
436
+
437
+ expense_history_text = ", ".join([f"{cat}: {format_currency(amount)}" for cat, amount in expense_by_category.items()])
438
+
439
+ user_query = f"Dựa trên lịch sử chi tiêu sau: {expense_history_text}. Hãy đưa ra gợi ý chi tiêu tiết kiệm hơn."
440
+
441
+ result = call_gemini_api(user_query, system_prompt, use_search=False)
442
+
443
+ if result.get("error"):
444
+ return result["error"], ""
445
+
446
+ return "", result["text"]
447
+
448
+
449
+ # --- TAB SHOPPING API CALLS ---
450
+
451
+ def call_accesstrade_api(url, params={}):
452
+ """Hàm gọi API AccessTrade."""
453
+ if not VOUCHER_AUTH_TOKEN:
454
+ return {"error": "VOUCHER_AUTH_TOKEN chưa được cấu hình."}
455
+
456
+ headers = {'Authorization': VOUCHER_AUTH_TOKEN}
457
+
458
+ for i in range(3):
459
+ try:
460
+ response = requests.get(url, headers=headers, params=params, timeout=15)
461
+ response.raise_for_status()
462
+ data = response.json()
463
+ return data.get('data', [])
464
+ except requests.exceptions.RequestException as e:
465
+ if i < 2:
466
+ time.sleep(2 ** i)
467
+ continue
468
+ return {"error": f"Lỗi kết nối API AccessTrade: {e}"}
469
+ return {"error": "Đã hết số lần thử kết nối API AccessTrade."}
470
+
471
+
472
+ def fetch_vouchers():
473
+ """Tải mã giảm giá."""
474
+ vouchers = call_accesstrade_api(VOUCHER_API_URL, params={'limit': 20})
475
+ if isinstance(vouchers, dict) and 'error' in vouchers:
476
+ return gr.update(visible=True, value=vouchers['error']), pd.DataFrame()
477
+
478
+ df = pd.DataFrame(vouchers)
479
+ df['domain'] = df['domain'].fillna('Khác')
480
+ df['coupon_code'] = df['coupons'].apply(lambda x: x[0]['coupon_code'] if x and len(x) > 0 else 'N/A')
481
+ df['coupon_desc'] = df['coupons'].apply(lambda x: x[0]['coupon_desc'] if x and len(x) > 0 else 'Không mô tả')
482
+
483
+ return gr.update(visible=False), df[['image', 'name', 'domain', 'coupon_code', 'coupon_desc', 'end_time', 'aff_link']]
484
+
485
+
486
+ def fetch_products(keywords, sort_field, sort_order):
487
+ """Tải và sắp xếp sản phẩm TikTok Shop."""
488
+ products = call_accesstrade_api(PRODUCTS_API_URL, params={
489
+ 'limit': 50,
490
+ 'title_keywords': keywords,
491
+ 'sort_field': sort_field,
492
+ 'sort_direction': sort_order,
493
+ })
494
+
495
+ if isinstance(products, dict) and 'error' in products:
496
+ return gr.update(visible=True, value=products['error']), pd.DataFrame()
497
+
498
+ df = pd.DataFrame(products)
499
+
500
+ # Xử lý các cột phức tạp và NaN
501
+ df['price'] = df.apply(lambda row: row['sale_price'] if row['sale_price'] else row['price'], axis=1)
502
+ df['price_formatted'] = df['price'].apply(format_currency)
503
+ df['commission_rate'] = df['commission_rate'].fillna(0).astype(float).apply(lambda x: f"{x:.2f}%")
504
+ df['category_name'] = df['categories'].apply(lambda x: x[0]['category_name_show'] if x and len(x) > 0 else 'Khác')
505
+
506
+ return gr.update(visible=False), df[['image', 'name', 'domain', 'price_formatted', 'commission_rate', 'category_name', 'aff_link', 'content']]
507
+
508
+ def fetch_trending_suggestions(keywords):
509
+ """Lấy gợi ý sản phẩm trending từ Gemini."""
510
+ system_prompt = "Act as an e-commerce trend analyst for the Vietnamese market. Based on the user's search query, identify the top 3 currently trending or highly-purchased products/categories related to that query across major e-commerce platforms (Shopee, Lazada, TikTok Shop). Provide 3 specific bullet points describing the trend (e.g., 'focus on eco-friendly materials', 'high demand for dual-function devices'). Your output MUST be in Vietnamese and use markdown bullet points. Do not include a greeting or conclusion."
511
+ user_query = f"Phân tích sản phẩm trending liên quan đến từ khóa tìm kiếm này: {keywords}"
512
+
513
+ result = call_gemini_api(user_query, system_prompt, use_search=True)
514
+
515
+ if result.get("error"):
516
+ return result["error"], ""
517
+
518
+ return "", result["text"]
519
+
520
+ def analyze_product_review(product_df, evt: gr.SelectData):
521
+ """Phân tích đánh giá sản phẩm giả lập bằng Gemini."""
522
+
523
+ if not evt.value:
524
+ return "Vui lòng chọn một sản phẩm trong bảng.", ""
525
+
526
+ selected_row = product_df.iloc[evt.index[0]]
527
+ product_name = selected_row['name']
528
+ product_content = selected_row['content']
529
+
530
+ system_prompt = "Act as a comprehensive product review sentiment analyzer. Based on the product name and description provided, generate 3 specific bullet points for POSITIVE aspects (potential strengths) and 3 bullet points for NEGATIVE aspects (potential weaknesses or common complaints). Then give an overall sentiment score (1 to 5 stars, e.g., '4/5 stars'). The output MUST be in Vietnamese, use markdown bullet points, and follow the structure: POSITIVE: [list]; NEGATIVE: [list]; SENTIMENT: [score]. DO NOT use any other text, greeting, or conclusion."
531
+ user_query = f"Phân tích nhận xét cho sản phẩm này: Tên: {product_name}. Mô tả: {product_content}"
532
+
533
+ result = call_gemini_api(user_query, system_prompt, use_search=False)
534
+
535
+ if result.get("error"):
536
+ return product_name, result["error"]
537
+
538
+ return product_name, result["text"]
539
+
540
+ # ==============================================================================
541
+ # 5. GRADIO INTERFACE BUILDER
542
+ # ==============================================================================
543
+
544
+ with gr.Blocks(title="Ứng Dụng Quản Lý Tài Chính Gia Đình", theme=gr.themes.Soft()) as demo:
545
+
546
+ # Biến trạng thái để lưu DataFrame hiện tại
547
+ transactions_state = gr.State(transactions_df)
548
+ investments_state = gr.State(investments_df)
549
+ product_results_state = gr.State(pd.DataFrame())
550
+
551
+ gr.Markdown("# 💸 FINTECH FAMILY - Quản Lý Thu Chi & Đầu Tư 🚀")
552
+ gr.Markdown("Đây là ứng dụng quản lý tài chính toàn diện, sử dụng CSV để lưu dữ liệu và Gemini AI để gợi ý thông minh.")
553
+
554
+ with gr.Tabs():
555
+
556
+ # --- TAB 1: DASHBOARD (Tổng Quan) ---
557
+ with gr.TabItem("Tổng Quan"):
558
+ gr.Markdown("## Báo Cáo Tổng Hợp Tháng Này")
559
+
560
+ with gr.Row():
561
+ with gr.Column():
562
+ gr.Markdown("### Thu Chi Cá Nhân")
563
+ with gr.Row():
564
+ stat_income = gr.Textbox(label="Tổng Thu", interactive=False)
565
+ stat_expense = gr.Textbox(label="Tổng Chi", interactive=False)
566
+ stat_savings = gr.Textbox(label="Tiết Kiệm Ròng", interactive=False)
567
+ gr.Markdown("### Phân Tích Chi Tiêu Theo Danh Mục")
568
+ expense_category_table = gr.Dataframe(headers=['Danh Mục', 'Số Tiền'], wrap=True, row_headers=False, interactive=False)
569
+
570
+ with gr.Column():
571
+ gr.Markdown("### Đầu Tư & ROI Ròng")
572
+ with gr.Row():
573
+ stat_roi = gr.Textbox(label="ROI Danh Mục Ròng", interactive=False)
574
+
575
+ gr.Markdown("### Tin Tức Thị Trường (Gemini AI)")
576
+ news_error = gr.Markdown(visible=False, label="Lỗi")
577
+ news_output = gr.Markdown("Đang tải tin tức...")
578
+
579
+ # Tải tin tức ngay khi tab được chọn lần đầu
580
+ demo.load(fn=fetch_market_news, outputs=[news_error, news_output], show_progress="minimal")
581
+
582
+ # Nút cập nhật toàn bộ Dashboard
583
+ gr.Button("Cập Nhật Dashboard").click(
584
+ fn=calculate_dashboard_stats,
585
+ inputs=[transactions_state, investments_state],
586
+ outputs=[stat_income, stat_expense, stat_savings, stat_roi, expense_category_table]
587
+ )
588
+
589
+ # --- TAB 2: TRANSACTIONS (Thu Chi) ---
590
+ with gr.TabItem("Quản Lý Thu Chi"):
591
+ gr.Markdown("## Ghi Lại Giao Dịch")
592
+
593
+ with gr.Row():
594
+ with gr.Column(scale=1):
595
+ with gr.Box():
596
+ gr.Markdown("### Thêm Giao Dịch Mới")
597
+ with gr.Row():
598
+ type_input = gr.Radio(choices=['INCOME', 'EXPENSE'], label="Loại Giao Dịch", value='EXPENSE')
599
+ category_input = gr.Dropdown(choices=['Ăn uống', 'Nhà ở', 'Di chuyển', 'Lương', 'Khác'], label="Danh Mục", value='Ăn uống')
600
+
601
+ amount_input = gr.Number(label="Số Tiền (VND)", minimum=0)
602
+ date_input = gr.Textbox(label="Ngày (YYYY-MM-DD)", value=datetime.now().strftime('%Y-%m-%d'))
603
+ description_input = gr.Textbox(label="Mô Tả")
604
+
605
+ tx_error_msg = gr.Markdown(visible=False, label="Lỗi", type='error')
606
+
607
+ add_tx_btn = gr.Button("Ghi Lại Giao Dịch", variant="primary")
608
+
609
+ with gr.Column(scale=2):
610
+ gr.Markdown("### Lịch Sử Giao Dịch Gần Đây (Chọn hàng để xóa)")
611
+
612
+ tx_table = gr.Dataframe(
613
+ headers=['ID', 'Loại', 'Danh Mục', 'Số Tiền', 'Ngày', 'Mô Tả'],
614
+ wrap=True,
615
+ interactive=False,
616
+ visible_cols=['type', 'category', 'amount', 'date', 'description']
617
+ )
618
+ gr.Markdown("💡 **Lưu ý:** Dữ liệu được lưu trong file `transactions.csv`.")
619
+
620
+ # Logic: Thêm giao dịch
621
+ add_tx_btn.click(
622
+ fn=add_transaction,
623
+ inputs=[transactions_state, type_input, category_input, amount_input, date_input, description_input],
624
+ outputs=[tx_error_msg, tx_table]
625
+ ).success(
626
+ fn=lambda df: (df, df.head(10)), # Cập nhật state sau khi thêm
627
+ inputs=[transactions_state],
628
+ outputs=[transactions_state, tx_table]
629
+ )
630
+
631
+ # Logic: Xóa giao dịch
632
+ tx_table.select(
633
+ fn=delete_transaction,
634
+ inputs=[transactions_state],
635
+ outputs=[tx_table]
636
+ ).success(
637
+ fn=lambda df: (df, df.head(10)),
638
+ inputs=[transactions_state],
639
+ outputs=[transactions_state, tx_table]
640
+ )
641
+
642
+ # --- TAB 3: INVESTMENTS (Đầu Tư) ---
643
+ with gr.TabItem("Quản Lý Đầu Tư"):
644
+ gr.Markdown("## Quản Lý Danh Mục Đầu Tư")
645
+
646
+ with gr.Row():
647
+ with gr.Column(scale=1):
648
+ with gr.Box():
649
+ gr.Markdown("### Thêm Khoản Đầu Tư Mới")
650
+ ticker_input = gr.Textbox(label="Mã Cổ Phiếu/Ticker", placeholder="Ví dụ: VNM, BTC")
651
+ quantity_input = gr.Number(label="Số Lượng", minimum=0)
652
+ avg_price_input = gr.Number(label="Giá Mua Trung Bình", minimum=0)
653
+ current_value_input = gr.Number(label="Giá Hiện Tại (Cập nhật thủ công)", minimum=0)
654
+ purchase_date_input = gr.Textbox(label="Ngày Mua (YYYY-MM-DD)", value=datetime.now().strftime('%Y-%m-%d'))
655
+
656
+ inv_error_msg = gr.Markdown(visible=False, label="Lỗi", type='error')
657
+
658
+ add_inv_btn = gr.Button("Thêm Khoản Đầu Tư", variant="primary")
659
+
660
+ with gr.Column(scale=2):
661
+ gr.Markdown("### Danh Mục Hiện Tại (Chọn hàng để xóa)")
662
+ inv_table = gr.Dataframe(
663
+ headers=['ID', 'Ticker', 'Số Lượng', 'Giá Mua TB', 'Giá Hiện Tại', 'Ngày Mua'],
664
+ wrap=True,
665
+ interactive=False,
666
+ visible_cols=['ticker', 'quantity', 'averagePrice', 'currentValue', 'purchaseDate']
667
+ )
668
+ gr.Markdown("💡 **Lưu ý:** Dữ liệu được lưu trong file `investments.csv`.")
669
+
670
+ # Logic: Thêm đầu tư
671
+ add_inv_btn.click(
672
+ fn=add_investment,
673
+ inputs=[investments_state, ticker_input, quantity_input, avg_price_input, current_value_input, purchase_date_input],
674
+ outputs=[inv_error_msg, inv_table]
675
+ ).success(
676
+ fn=lambda df: (df, df.head(10)),
677
+ inputs=[investments_state],
678
+ outputs=[investments_state, inv_table]
679
+ )
680
+
681
+ # Logic: Xóa đầu tư
682
+ inv_table.select(
683
+ fn=delete_investment,
684
+ inputs=[investments_state],
685
+ outputs=[inv_table]
686
+ ).success(
687
+ fn=lambda df: (df, df.head(10)),
688
+ inputs=[investments_state],
689
+ outputs=[investments_state, inv_table]
690
+ )
691
+
692
+ # Báo cáo đầu tư
693
+ gr.Markdown("## Báo Cáo Tỷ Suất Đầu Tư Ròng")
694
+ with gr.Row():
695
+ inv_total_invested = gr.Textbox(label="Tổng Vốn Đầu Tư", interactive=False)
696
+ inv_current_value = gr.Textbox(label="Giá Trị Hiện Tại", interactive=False)
697
+ inv_net_return = gr.Textbox(label="Lãi/Lỗ Ròng", interactive=False)
698
+ inv_roi = gr.Textbox(label="ROI Ròng (%)", interactive=False)
699
+
700
+ inv_report_table = gr.Dataframe(
701
+ headers=['Mã CK', 'Số Lượng', 'Giá Mua TB', 'Giá Hiện Tại', 'Lãi/Lỗ Ròng', 'Lãi/Lỗ (%)'],
702
+ interactive=False
703
+ )
704
+
705
+ gr.Button("Tính Toán Báo Cáo ROI").click(
706
+ fn=calculate_investment_report,
707
+ inputs=[investments_state],
708
+ outputs=[inv_total_invested, inv_current_value, inv_net_return, inv_roi, inv_report_table]
709
+ )
710
+
711
+ # --- TAB 4: REPORTS (Báo Cáo Thu Chi) ---
712
+ with gr.TabItem("Báo Cáo Thu Chi"):
713
+ gr.Markdown("## Báo Cáo Thống Kê Thu Chi Theo Kỳ")
714
+
715
+ report_period_input = gr.Radio(
716
+ choices=['WEEK', 'MONTH', 'QUARTER', 'YEAR'],
717
+ label="Chọn Kỳ Báo Cáo",
718
+ value='MONTH'
719
+ )
720
+ report_table_output = gr.Dataframe(
721
+ headers=['Kỳ', 'Thu Nhập', 'Chi Tiêu', 'Tiết Kiệm Ròng'],
722
+ interactive=False,
723
+ wrap=True
724
+ )
725
+
726
+ gr.Button("Xem Báo Cáo").click(
727
+ fn=calculate_report_table,
728
+ inputs=[transactions_state, report_period_input],
729
+ outputs=[report_table_output]
730
+ )
731
+
732
+ # --- TAB 5: AI GỢI Ý ---
733
+ with gr.TabItem("AI Gợi Ý"):
734
+ gr.Markdown("## Gemini AI Cố Vấn Tài Chính")
735
+
736
+ with gr.Row():
737
+ with gr.Column():
738
+ gr.Markdown("### 🚀 Đề Xuất Đầu Tư (Dựa trên Vĩ mô)")
739
+ inv_ai_output = gr.Markdown("Chưa có đề xuất.")
740
+ inv_ai_error = gr.Markdown(visible=False, label="Lỗi", type='error')
741
+ gr.Button("Phân Tích & Đề Xuất Đầu Tư").click(
742
+ fn=fetch_investment_suggestions,
743
+ outputs=[inv_ai_error, inv_ai_output]
744
+ )
745
+
746
+ with gr.Column():
747
+ gr.Markdown("### 💰 Gợi Ý Chi Tiêu Tiết Kiệm (Dựa trên Lịch sử)")
748
+ expense_ai_output = gr.Markdown("Chưa có gợi ý.")
749
+ expense_ai_error = gr.Markdown(visible=False, label="Lỗi", type='error')
750
+ gr.Button("Phân Tích & Gợi Ý Ti���t Kiệm").click(
751
+ fn=fetch_expense_suggestions,
752
+ inputs=[transactions_state],
753
+ outputs=[expense_ai_error, expense_ai_output]
754
+ )
755
+
756
+ # --- TAB 6: SHOPPING (Mua Sắm Thông Minh) ---
757
+ with gr.TabItem("Mua Sắm Thông Minh"):
758
+ gr.Markdown("## Tìm Kiếm & So Sánh Sản Phẩm TikTok Shop")
759
+
760
+ with gr.Row():
761
+ search_input = gr.Textbox(label="Từ Khóa Sản Phẩm", value="dép", placeholder="Ví dụ: áo khoác, điện thoại")
762
+ sort_field_input = gr.Radio(choices=['COMMISSION', 'PRICE'], label="Sắp xếp theo", value='PRICE')
763
+ sort_order_input = gr.Radio(choices=['DESC', 'ASC'], label="Thứ tự", value='DESC')
764
+
765
+ search_btn = gr.Button("Tìm Kiếm Sản Phẩm & Xu Hướng", variant="primary")
766
+
767
+ # Sản phẩm Trending AI
768
+ gr.Markdown("### 💡 Sản Phẩm Trending (Gợi ý từ AI)")
769
+ trending_ai_output = gr.Markdown("Nhấn tìm kiếm để AI phân tích xu hướng...")
770
+ trending_ai_error = gr.Markdown(visible=False, label="Lỗi", type='error')
771
+
772
+ # Bảng kết quả sản phẩm
773
+ gr.Markdown("### Kết Quả Tìm Kiếm & So Sánh Giá")
774
+ product_search_error = gr.Markdown(visible=False, label="Lỗi", type='error')
775
+ product_table = gr.Dataframe(
776
+ headers=['Ảnh', 'Tên Sản Phẩm', 'Sàn', 'Giá', 'Hoa Hồng (%)', 'Danh Mục', 'Link'],
777
+ interactive=False,
778
+ visible_cols=['image', 'name', 'domain', 'price_formatted', 'commission_rate', 'category_name', 'aff_link']
779
+ )
780
+
781
+ # Phân tích AI
782
+ gr.Markdown("### Phân Tích Đánh Giá Sản Phẩm")
783
+ with gr.Row():
784
+ product_name_output = gr.Textbox(label="Sản Phẩm Được Chọn", interactive=False)
785
+ review_ai_output = gr.Markdown("Chọn một hàng trong bảng kết quả để phân tích.")
786
+
787
+ # Logic Tìm kiếm
788
+ search_btn.click(
789
+ fn=lambda: [gr.update(visible=True, value="Đang tải sản phẩm..."), gr.update(visible=True, value="Đang phân tích xu hướng...")],
790
+ outputs=[product_search_error, trending_ai_error] # Reset errors
791
+ ).then(
792
+ fn=fetch_products,
793
+ inputs=[search_input, sort_field_input, sort_order_input],
794
+ outputs=[product_search_error, product_table]
795
+ ).then(
796
+ fn=fetch_trending_suggestions,
797
+ inputs=[search_input],
798
+ outputs=[trending_ai_error, trending_ai_output]
799
+ )
800
+
801
+ # Logic Phân tích AI Review
802
+ product_table.select(
803
+ fn=analyze_product_review,
804
+ inputs=[product_table],
805
+ outputs=[product_name_output, review_ai_output]
806
+ )
807
+
808
+ # Logic Voucher (Tải sẵn)
809
+ with gr.Box():
810
+ gr.Markdown("### Mã Giảm Giá & Ưu Đãi Tiết Kiệm")
811
+ voucher_error = gr.Markdown(visible=False, label="Lỗi", type='error')
812
+ voucher_table = gr.Dataframe(interactive=False, visible_cols=['name', 'domain', 'coupon_code', 'coupon_desc', 'end_time', 'aff_link'])
813
+ demo.load(fn=fetch_vouchers, outputs=[voucher_error, voucher_table], show_progress="minimal")
814
+
815
+ # Logic: Cập nhật DataFrames sau khi ứng dụng load lần đầu
816
+ demo.load(fn=update_dataframes, outputs=[transactions_state, investments_state], show_progress="minimal").then(
817
+ fn=calculate_dashboard_stats,
818
+ inputs=[transactions_state, investments_state],
819
+ outputs=[stat_income, stat_expense, stat_savings, stat_roi, expense_category_table]
820
+ )
821
+
822
+ # Chạy ứng dụng
823
+ if __name__ == "__main__":
824
+ demo.launch()