Commit 6636c8d4 authored by ThinhNC's avatar ThinhNC

fix(forecast): show horizon-dependent projected balance

parent 845ec367
...@@ -70,7 +70,7 @@ File này lưu trữ các quyết định thiết kế dài hạn và trạng th ...@@ -70,7 +70,7 @@ File này lưu trữ các quyết định thiết kế dài hạn và trạng th
- Module mục tiêu tiết kiệm đã có frontend tại `/saving-goals``/saving-goals/:id`, kết nối Saving Goal API qua TanStack Query, gồm danh sách/chi tiết, tạo/sửa, pause/resume, archive/restore, tìm kiếm, lọc trạng thái/thời hạn, sắp xếp, phân trang và quản lý đầy đủ tạo/sửa/xóa lịch sử đóng góp; có validation, loading/error/empty/confirmation và bản dịch Việt/Anh. - Module mục tiêu tiết kiệm đã có frontend tại `/saving-goals``/saving-goals/:id`, kết nối Saving Goal API qua TanStack Query, gồm danh sách/chi tiết, tạo/sửa, pause/resume, archive/restore, tìm kiếm, lọc trạng thái/thời hạn, sắp xếp, phân trang và quản lý đầy đủ tạo/sửa/xóa lịch sử đóng góp; có validation, loading/error/empty/confirmation và bản dịch Việt/Anh.
- Module báo cáo tài chính đã có frontend tại `/reports`, kết nối đầy đủ Report API qua TanStack Query, gồm lọc ngày/tuần/tháng/năm/khoảng tùy chọn, ví và tiền tệ; KPI tổng quan, dòng tiền, cơ cấu chi tiêu, hiệu suất ngân sách, mục tiêu tiết kiệm, loading/error/empty state, responsive và bản dịch Việt/Anh. Khối tóm tắt AI đã tích hợp AI Insights, hiển thị nhận định cùng các điểm nổi bật và điều hướng sang phân tích chi tiết với phạm vi báo cáo được giữ nguyên. - Module báo cáo tài chính đã có frontend tại `/reports`, kết nối đầy đủ Report API qua TanStack Query, gồm lọc ngày/tuần/tháng/năm/khoảng tùy chọn, ví và tiền tệ; KPI tổng quan, dòng tiền, cơ cấu chi tiêu, hiệu suất ngân sách, mục tiêu tiết kiệm, loading/error/empty state, responsive và bản dịch Việt/Anh. Khối tóm tắt AI đã tích hợp AI Insights, hiển thị nhận định cùng các điểm nổi bật và điều hướng sang phân tích chi tiết với phạm vi báo cáo được giữ nguyên.
- Module thông báo và nhắc nhở đã có frontend tại `/notifications`, kết nối Notification/Reminder API qua TanStack Query, gồm badge chưa đọc toàn cục, danh sách/lọc/phân trang, đánh dấu đọc một/tất cả, xóa, điều hướng tới nguồn, CRUD và bật/tắt lịch nhắc, tùy chọn loại cảnh báo/kênh nhận; có polling gần thời gian thực, loading/error/empty state, xác nhận thao tác và bản dịch Việt/Anh. - Module thông báo và nhắc nhở đã có frontend tại `/notifications`, kết nối Notification/Reminder API qua TanStack Query, gồm badge chưa đọc toàn cục, danh sách/lọc/phân trang, đánh dấu đọc một/tất cả, xóa, điều hướng tới nguồn, CRUD và bật/tắt lịch nhắc, tùy chọn loại cảnh báo/kênh nhận; có polling gần thời gian thực, loading/error/empty state, xác nhận thao tác và bản dịch Việt/Anh.
- Module dự báo dòng tiền và ngân sách (Upgrade 1) đã có frontend tại `/forecast`, kết nối Forecast API (`/forecast/runway`, `/forecast/budget-depletion`) qua TanStack Query; gồm chọn khoảng dự báo (14, 30, 60, 90 ngày), thước đo số ngày an toàn tài chính (runway) với exponential burn velocity ($\lambda=0.04$), phân tích nguy cơ vỡ ngân sách, hạn mức chi tối đa mỗi ngày, Claymorphism skeleton loading, error/retry state, và bản dịch Việt/Anh. - Module dự báo dòng tiền và ngân sách (Upgrade 1) đã có frontend tại `/forecast`, kết nối Forecast API (`/forecast/runway`, `/forecast/budget-depletion`) qua TanStack Query; gồm chọn khoảng dự báo (14, 30, 60, 90 ngày), số dư dự kiến cuối khoảng và chênh lệch so với hiện tại, thước đo số ngày an toàn tài chính (runway) với exponential burn velocity ($\lambda=0.04$), phân tích nguy cơ vỡ ngân sách, hạn mức chi tối đa mỗi ngày, Claymorphism skeleton loading, error/retry state, và bản dịch Việt/Anh.
- Module mô phỏng kịch bản tài chính What-if (Upgrade 2) đã có frontend tại `/simulations`, kết nối Simulation API (`/simulations/presets`, `/simulations/run`) qua TanStack Query; gồm áp dụng kịch bản mẫu có sẵn, thanh trượt thời gian 3-36 tháng đúng contract Backend, cấu hình biến động dòng tiền tùy chỉnh (tăng/giảm thu nhập, chi tiêu), bảng so sánh số dư baseline/simulated, cảnh báo nguy cơ thâm hụt, tác động đến mục tiêu tiết kiệm, diễn biến trajectory từng tháng, skeleton loading và bản dịch Việt/Anh. - Module mô phỏng kịch bản tài chính What-if (Upgrade 2) đã có frontend tại `/simulations`, kết nối Simulation API (`/simulations/presets`, `/simulations/run`) qua TanStack Query; gồm áp dụng kịch bản mẫu có sẵn, thanh trượt thời gian 3-36 tháng đúng contract Backend, cấu hình biến động dòng tiền tùy chỉnh (tăng/giảm thu nhập, chi tiêu), bảng so sánh số dư baseline/simulated, cảnh báo nguy cơ thâm hụt, tác động đến mục tiêu tiết kiệm, diễn biến trajectory từng tháng, skeleton loading và bản dịch Việt/Anh.
- Module phát hiện chi tiêu bất thường (Upgrade 3) đã có frontend tại `/anomalies`, kết nối Anomaly API (`/anomalies/recent`, `/anomalies/evaluate`) qua TanStack Query; gồm công cụ kiểm tra nhanh giao dịch trước khi chi (Pre-Purchase Evaluator) dựa trên Modified Z-Score & MAD, danh sách lịch sử giao dịch bị gắn cờ với mức độ nghiêm trọng và mã lý do, skeleton loading và bản dịch Việt/Anh. - Module phát hiện chi tiêu bất thường (Upgrade 3) đã có frontend tại `/anomalies`, kết nối Anomaly API (`/anomalies/recent`, `/anomalies/evaluate`) qua TanStack Query; gồm công cụ kiểm tra nhanh giao dịch trước khi chi (Pre-Purchase Evaluator) dựa trên Modified Z-Score & MAD, danh sách lịch sử giao dịch bị gắn cờ với mức độ nghiêm trọng và mã lý do, skeleton loading và bản dịch Việt/Anh.
- Module tự động nhận diện dịch vụ định kỳ (Upgrade 4) đã có frontend tại `/subscriptions`, kết nối Subscription API (`/subscriptions/discover`, `/subscriptions/convert-to-reminder`) qua TanStack Query; gồm danh sách các gói cước định kỳ phát hiện được với chu kỳ và độ chắc chắn, cảnh báo tăng giá cước (price drift), nút 1-click chuyển đổi thành nhắc nhở tự động, skeleton loading và bản dịch Việt/Anh. - Module tự động nhận diện dịch vụ định kỳ (Upgrade 4) đã có frontend tại `/subscriptions`, kết nối Subscription API (`/subscriptions/discover`, `/subscriptions/convert-to-reminder`) qua TanStack Query; gồm danh sách các gói cước định kỳ phát hiện được với chu kỳ và độ chắc chắn, cảnh báo tăng giá cước (price drift), nút 1-click chuyển đổi thành nhắc nhở tự động, skeleton loading và bản dịch Việt/Anh.
......
...@@ -1409,6 +1409,8 @@ ...@@ -1409,6 +1409,8 @@
"sustainable": "Healthy Cash Flow", "sustainable": "Healthy Cash Flow",
"currentBalance": "Current Total Balance", "currentBalance": "Current Total Balance",
"dailyBurnRate": "Average Net Daily Cash Flow", "dailyBurnRate": "Average Net Daily Cash Flow",
"projectedBalance": "Projected balance after {{days}} days",
"balanceChange": "{{amount}} vs. current balance",
"day": "day", "day": "day",
"depletionAlert": "Projected Balance Depletion Date", "depletionAlert": "Projected Balance Depletion Date",
"historySummary": "Based on {{transactions}} transactions across the last {{days}} days.", "historySummary": "Based on {{transactions}} transactions across the last {{days}} days.",
......
...@@ -1502,14 +1502,16 @@ ...@@ -1502,14 +1502,16 @@
"pageTitle": "Dự báo Dòng tiền & Ngân sách", "pageTitle": "Dự báo Dòng tiền & Ngân sách",
"selectHorizon": "Khoảng thời gian dự báo", "selectHorizon": "Khoảng thời gian dự báo",
"horizonOption": "{{days}} ngày", "horizonOption": "{{days}} ngày",
"runwayTitle": "Thời gian An toàn Tài chính (Runway)", "runwayTitle": "Thời gian An toàn Tài chính",
"days": "ngày", "days": "ngày",
"runwayDays": "{{days}} ngày", "runwayDays": "{{days}} ngày",
"daysLeft": "Còn {{days}} ngày", "daysLeft": "Còn {{days}} ngày",
"unlimitedRunway": "Bền vững dài hạn (Dòng tiền dương)", "unlimitedRunway": "Bền vững dài hạn",
"sustainable": "Dòng tiền tốt", "sustainable": "Dòng tiền tốt",
"currentBalance": "Tổng số dư hiện tại", "currentBalance": "Tổng số dư hiện tại",
"dailyBurnRate": "Dòng tiền ròng bình quân mỗi ngày", "dailyBurnRate": "Dòng tiền ròng bình quân mỗi ngày",
"projectedBalance": "Số dư dự kiến sau {{days}} ngày",
"balanceChange": "{{amount}} so với hiện tại",
"day": "ngày", "day": "ngày",
"depletionAlert": "Dự kiến cạn kiệt số dư vào ngày", "depletionAlert": "Dự kiến cạn kiệt số dư vào ngày",
"historySummary": "Dự báo dựa trên {{transactions}} giao dịch trong {{days}} ngày gần nhất.", "historySummary": "Dự báo dựa trên {{transactions}} giao dịch trong {{days}} ngày gần nhất.",
......
...@@ -10,6 +10,11 @@ interface ForecastRunwayCardProps { ...@@ -10,6 +10,11 @@ interface ForecastRunwayCardProps {
data: ForecastRunway; data: ForecastRunway;
} }
const safeNumber = (value: string): number => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
};
export const ForecastRunwayCard: React.FC<ForecastRunwayCardProps> = ({ data }) => { export const ForecastRunwayCard: React.FC<ForecastRunwayCardProps> = ({ data }) => {
const { t, formatCurrency, formatNumber, intlLocale } = useI18n(); const { t, formatCurrency, formatNumber, intlLocale } = useI18n();
...@@ -18,8 +23,15 @@ export const ForecastRunwayCard: React.FC<ForecastRunwayCardProps> = ({ data }) ...@@ -18,8 +23,15 @@ export const ForecastRunwayCard: React.FC<ForecastRunwayCardProps> = ({ data })
? t("forecast.runwayDays", { days: data.metrics.runwayDays ?? 0 }) ? t("forecast.runwayDays", { days: data.metrics.runwayDays ?? 0 })
: t("forecast.unlimitedRunway"); : t("forecast.unlimitedRunway");
const burnRate = Number(data.metrics.netDailyBurnRate); const burnRate = safeNumber(data.metrics.netDailyBurnRate);
const currentBal = parseFloat(data.currentBalance); const currentBal = safeNumber(data.currentBalance);
const projectedBalance = safeNumber(data.metrics.projectedEndBalance);
const projectedChange = projectedBalance - currentBal;
const projectedChangeTone = projectedChange > 0
? "text-clay-income"
: projectedChange < 0
? "text-clay-expense"
: "text-clay-text-muted";
let badgeType: "income" | "warning" | "expense" = "income"; let badgeType: "income" | "warning" | "expense" = "income";
if (!isFinite) { if (!isFinite) {
...@@ -69,6 +81,23 @@ export const ForecastRunwayCard: React.FC<ForecastRunwayCardProps> = ({ data }) ...@@ -69,6 +81,23 @@ export const ForecastRunwayCard: React.FC<ForecastRunwayCardProps> = ({ data })
</div> </div>
</div> </div>
{/* Horizon-dependent projected balance */}
<div className="rounded-clay-sm border border-clay-primary/20 bg-clay-primary/10 p-3.5">
<span className="block text-[11px] font-medium text-clay-text-muted">
{t("forecast.projectedBalance", { days: data.horizonDays })}
</span>
<div className="mt-0.5 flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
<p className="font-baloo text-lg font-bold text-clay-primary">
{formatCurrency(projectedBalance, data.currency)}
</p>
<p className={`text-xs font-bold ${projectedChangeTone}`}>
{t("forecast.balanceChange", {
amount: `${projectedChange > 0 ? "+" : ""}${formatCurrency(projectedChange, data.currency)}`,
})}
</p>
</div>
</div>
{/* Projected Depletion Date */} {/* Projected Depletion Date */}
{data.metrics.depletionDate && ( {data.metrics.depletionDate && (
<div className="bg-clay-expense-soft border border-clay-expense/30 p-3.5 rounded-clay-sm"> <div className="bg-clay-expense-soft border border-clay-expense/30 p-3.5 rounded-clay-sm">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment