Spaces:
Sleeping
Sleeping
Create app.py
Browse files
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()
|