Commit 59607176 authored by ThinhNC's avatar ThinhNC

feat(zalo-bot): add conversational fast-entry guidance and deep link transaction handling

- Add Fast-Entry instructional card with chat syntax examples in NotificationSettings

- Add i18n translations for fastEntry in vi.json and en.json

- Support Zalo Bot deep link (?screen=transaction-edit&id=...&source=zalo_bot) in TransactionsPage

- Update frontend memory.md
parent 42aea742
...@@ -33,6 +33,7 @@ File này lưu trữ các quyết định thiết kế dài hạn và trạng th ...@@ -33,6 +33,7 @@ File này lưu trữ các quyết định thiết kế dài hạn và trạng th
- **Báo cáo tài chính**: Trang `/reports` dùng trực tiếp bốn Report API (`overview`, `cash-flow`, `spending-by-category`, `budget-performance`) qua TanStack Query và luôn giữ số liệu tách theo tiền tệ; không cộng gộp các loại tiền tệ khác nhau. Bộ lọc khoảng ngày tùy chọn gửi mốc kết thúc dạng exclusive sang Backend nhưng hiển thị ngày kết thúc dạng inclusive cho người dùng. Biểu đồ dùng SVG/CSS với semantic Clay tokens, không phụ thuộc thư viện chart. Biểu đồ Xu hướng dòng tiền hiển thị số tiền thu/chi gọn gàng trực tiếp tại các điểm nút trên đường cong (không dùng cột số tiền bên trái để tối ưu diện tích vẽ đồ thị), tự động tách vị trí trên/dưới khi có giao dịch cả thu và chi; trục hoành chia mốc chuẩn: Tuần chia 7 ngày từ Thứ 2 đến CN, Năm chia 12 tháng (T1-T12), hỗ trợ chạm vào điểm để highlight và xem chi tiết. Thẻ Tổng quan ngân sách phải thể hiện số liệu tổng hợp (đã chi, còn lại, hạn mức và số lượng theo trạng thái), còn Hiệu suất ngân sách trình bày từng ngân sách; tổng quan chỉ ưu tiên summary `OVERALL` khi `budgetCount > 0`, nếu không fallback sang `CATEGORY`, và hiển thị `—` thay vì `0%` khi không có ngân sách hợp lệ. Khối AI tại báo cáo phải hiển thị trọn bản tóm tắt cùng tối đa ba điểm đáng chú ý, sau đó mới dẫn sang Trợ lý AI để phân tích sâu; khi điều hướng phải giữ `dateFrom`/`dateTo`/`currency` của kỳ báo cáo và nút quay lại phải trở về báo cáo. - **Báo cáo tài chính**: Trang `/reports` dùng trực tiếp bốn Report API (`overview`, `cash-flow`, `spending-by-category`, `budget-performance`) qua TanStack Query và luôn giữ số liệu tách theo tiền tệ; không cộng gộp các loại tiền tệ khác nhau. Bộ lọc khoảng ngày tùy chọn gửi mốc kết thúc dạng exclusive sang Backend nhưng hiển thị ngày kết thúc dạng inclusive cho người dùng. Biểu đồ dùng SVG/CSS với semantic Clay tokens, không phụ thuộc thư viện chart. Biểu đồ Xu hướng dòng tiền hiển thị số tiền thu/chi gọn gàng trực tiếp tại các điểm nút trên đường cong (không dùng cột số tiền bên trái để tối ưu diện tích vẽ đồ thị), tự động tách vị trí trên/dưới khi có giao dịch cả thu và chi; trục hoành chia mốc chuẩn: Tuần chia 7 ngày từ Thứ 2 đến CN, Năm chia 12 tháng (T1-T12), hỗ trợ chạm vào điểm để highlight và xem chi tiết. Thẻ Tổng quan ngân sách phải thể hiện số liệu tổng hợp (đã chi, còn lại, hạn mức và số lượng theo trạng thái), còn Hiệu suất ngân sách trình bày từng ngân sách; tổng quan chỉ ưu tiên summary `OVERALL` khi `budgetCount > 0`, nếu không fallback sang `CATEGORY`, và hiển thị `—` thay vì `0%` khi không có ngân sách hợp lệ. Khối AI tại báo cáo phải hiển thị trọn bản tóm tắt cùng tối đa ba điểm đáng chú ý, sau đó mới dẫn sang Trợ lý AI để phân tích sâu; khi điều hướng phải giữ `dateFrom`/`dateTo`/`currency` của kỳ báo cáo và nút quay lại phải trở về báo cáo.
- Các bảng/thẻ tổng hợp trên `/reports` liên kết tới module tương ứng: số dư và từng ví tới Ví, thu/chi, dòng tiền và chi tiêu theo danh mục tới Giao dịch (chọn danh mục cụ thể sẽ lọc theo đúng danh mục đó, loại chi tiêu và khoảng ngày của kỳ báo cáo), ngân sách tới chi tiết ngân sách, mục tiêu tới danh sách mục tiêu. Chỉ bật trạng thái tương tác khi người dùng có quyền đọc route đích; route đích nhận marker `fromReports` để nút quay lại trở về đúng trang báo cáo. Thẻ Tổng thu/Tổng chi và Chi tiêu theo danh mục mở danh sách giao dịch với các bộ lọc tương ứng và tự mở bảng lọc để người dùng nhận biết trạng thái đang áp dụng. - Các bảng/thẻ tổng hợp trên `/reports` liên kết tới module tương ứng: số dư và từng ví tới Ví, thu/chi, dòng tiền và chi tiêu theo danh mục tới Giao dịch (chọn danh mục cụ thể sẽ lọc theo đúng danh mục đó, loại chi tiêu và khoảng ngày của kỳ báo cáo), ngân sách tới chi tiết ngân sách, mục tiêu tới danh sách mục tiêu. Chỉ bật trạng thái tương tác khi người dùng có quyền đọc route đích; route đích nhận marker `fromReports` để nút quay lại trở về đúng trang báo cáo. Thẻ Tổng thu/Tổng chi và Chi tiêu theo danh mục mở danh sách giao dịch với các bộ lọc tương ứng và tự mở bảng lọc để người dùng nhận biết trạng thái đang áp dụng.
- **Thông báo & nhắc nhở**: Trang `/notifications` là trung tâm chung cho inbox thông báo, lịch nhắc và tùy chọn cảnh báo. Unread count dùng query key riêng và được hiển thị bằng badge chuông toàn cục sau đăng nhập. Cơ chế short polling 30 giây đã được thay thế hoàn toàn bằng Server-Sent Events (SSE) qua hook `useNotificationSSE` (kết nối tới `/notifications/stream` với token/cookie) kết hợp `NotificationRealtimeListener` trong `Layout`; khi có thông báo mới hoặc unread count thay đổi, server tự động đẩy event tới client để cập nhật badge và hiển thị popup tức thì; refetch khi focus và mọi mutation invalidate namespace liên quan. Reminder datetime luôn dùng Vietnamese wall time rồi chuyển sang UTC ISO bằng helper business-time. Với cảnh báo tự động do Backend hiện tạo bằng tiếng Anh, frontend nhận diện đúng mẫu theo `type`, dùng metadata để dựng title/message theo locale; nội dung người dùng nhập, legacy không khớp mẫu và AI tương lai được giữ nguyên. Notification `data` giữ kiểu mở `Record<string, unknown>` để tiếp nhận metadata cảnh báo/AI mà UI không phụ thuộc cấu trúc riêng. Điều hướng từ nút xem chi tiết gắn `fromNotifications` trong route state; các trang đích hệ thống dùng history back khi có marker này và giữ fallback back cố định cho luồng truy cập trực tiếp. Thông báo giao dịch bất thường phải truyền `transactionId` (fallback lần lượt từ `data`, `sourceId`, URL) để trang Giao dịch gọi chi tiết và tự mở đúng giao dịch, không chỉ chuyển tới danh sách chung. - **Thông báo & nhắc nhở**: Trang `/notifications` là trung tâm chung cho inbox thông báo, lịch nhắc và tùy chọn cảnh báo. Unread count dùng query key riêng và được hiển thị bằng badge chuông toàn cục sau đăng nhập. Cơ chế short polling 30 giây đã được thay thế hoàn toàn bằng Server-Sent Events (SSE) qua hook `useNotificationSSE` (kết nối tới `/notifications/stream` với token/cookie) kết hợp `NotificationRealtimeListener` trong `Layout`; khi có thông báo mới hoặc unread count thay đổi, server tự động đẩy event tới client để cập nhật badge và hiển thị popup tức thì; refetch khi focus và mọi mutation invalidate namespace liên quan. Reminder datetime luôn dùng Vietnamese wall time rồi chuyển sang UTC ISO bằng helper business-time. Với cảnh báo tự động do Backend hiện tạo bằng tiếng Anh, frontend nhận diện đúng mẫu theo `type`, dùng metadata để dựng title/message theo locale; nội dung người dùng nhập, legacy không khớp mẫu và AI tương lai được giữ nguyên. Notification `data` giữ kiểu mở `Record<string, unknown>` để tiếp nhận metadata cảnh báo/AI mà UI không phụ thuộc cấu trúc riêng. Điều hướng từ nút xem chi tiết gắn `fromNotifications` trong route state; các trang đích hệ thống dùng history back khi có marker này và giữ fallback back cố định cho luồng truy cập trực tiếp. Thông báo giao dịch bất thường phải truyền `transactionId` (fallback lần lượt từ `data`, `sourceId`, URL) để trang Giao dịch gọi chi tiết và tự mở đúng giao dịch, không chỉ chuyển tới danh sách chung.
- **Conversational Fast-Entry (Ghi Chép Hội thoại Zalo Bot)**: `NotificationSettings.tsx` hiển thị panel hướng dẫn Fast-Entry ngay bên dưới trạng thái "Đã liên kết" với các ví dụ câu chat mẫu (code monospace). Panel này chỉ xuất hiện khi `isZaloLinked = true`. i18n keys nằm ở `notification.fastEntry.*`. Deep Link từ Bot trỏ vào `/transactions?screen=transaction-edit&id=...&source=zalo_bot`; trang Transactions phải xử lý query param `source=zalo_bot` và `id` để tự mở modal sửa giao dịch tương ứng.
- **Giao dịch tự động định kỳ**: Trang `/recurring-transactions` quản lý lịch thu/chi tự động qua - **Giao dịch tự động định kỳ**: Trang `/recurring-transactions` quản lý lịch thu/chi tự động qua
TanStack Query, gồm tạo/sửa, pause/resume, xóa mềm, preview ngày chạy và lịch sử occurrence. TanStack Query, gồm tạo/sửa, pause/resume, xóa mềm, preview ngày chạy và lịch sử occurrence.
Hỗ trợ cài đặt thông báo nhắc trước hạn thanh toán (`remindDaysBefore`: cùng ngày, 1, 2, 3, 5, 7, 14 ngày) Hỗ trợ cài đặt thông báo nhắc trước hạn thanh toán (`remindDaysBefore`: cùng ngày, 1, 2, 3, 5, 7, 14 ngày)
......
...@@ -1322,6 +1322,11 @@ ...@@ -1322,6 +1322,11 @@
"copyUrl": "Copy", "copyUrl": "Copy",
"copiedUrl": "Copied!", "copiedUrl": "Copied!",
"openBot": "Open Bot Finwise in Zalo" "openBot": "Open Bot Finwise in Zalo"
},
"fastEntry": {
"title": "Instant Chat Entry",
"desc": "Send natural messages to FinWise Bot to log transactions instantly — no need to open the app!",
"undoHint": "After bot confirms, type 'undo' or 'edit to X' within 15 minutes to make corrections."
} }
}, },
"reminder": { "reminder": {
......
...@@ -1386,6 +1386,11 @@ ...@@ -1386,6 +1386,11 @@
"copyUrl": "Sao chép", "copyUrl": "Sao chép",
"copiedUrl": "Đã sao chép!", "copiedUrl": "Đã sao chép!",
"openBot": "Mở Bot Finwise trong Zalo" "openBot": "Mở Bot Finwise trong Zalo"
},
"fastEntry": {
"title": "Ghi Chép Nhanh qua Zalo Chat",
"desc": "Nhắn tin tự nhiên cho Bot FinWise để ghi ngay vào sổ — không cần mở App!",
"undoHint": "Sau khi bot xác nhận, gõ 'hoàn tác' hoặc 'sửa thành Xk' trong vòng 15 phút để điều chỉnh."
} }
}, },
"reminder": { "reminder": {
......
...@@ -304,8 +304,9 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett ...@@ -304,8 +304,9 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
)} )}
{/* TH3: Đã liên kết -> Hiện nút Hủy liên kết */} {/* TH3: Đã liên kết -> Hiện nút Hủy liên kết + hướng dẫn Fast-Entry */}
{isZaloLinked && ( {isZaloLinked && (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-3 pt-1"> <div className="flex items-center justify-between gap-3 pt-1">
<span className="clay-caption text-xs text-green-700 font-semibold"> <span className="clay-caption text-xs text-green-700 font-semibold">
{t("notification.zaloBotLink.successLinked")} {t("notification.zaloBotLink.successLinked")}
...@@ -319,6 +320,40 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett ...@@ -319,6 +320,40 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
{t("notification.zaloBotLink.unlink")} {t("notification.zaloBotLink.unlink")}
</button> </button>
</div> </div>
{/* Panel hướng dẫn Fast-Entry */}
<div className="rounded-clay-md border border-emerald-200 bg-emerald-50/60 p-4 shadow-clay-pressed">
<div className="flex items-center gap-2 mb-2">
<span className="text-emerald-600 text-base">⚡</span>
<span className="font-nunito text-sm font-bold text-emerald-800">
{t("notification.fastEntry.title")}
</span>
</div>
<p className="clay-caption text-xs text-emerald-700 mb-3">
{t("notification.fastEntry.desc")}
</p>
<div className="flex flex-col gap-1.5">
{[
{ icon: "☕", msg: "Cà phê sáng 35k ví tiền mặt" },
{ icon: "🍜", msg: "Ăn trưa 50k" },
{ icon: "💰", msg: "Vừa nhận lương 20tr ví VCB" },
{ icon: "↩️", msg: "hoàn tác" },
{ icon: "✏️", msg: "sửa thành 40k" },
].map(({ icon, msg }) => (
<div
key={msg}
className="flex items-start gap-2 rounded-clay-sm bg-white/70 px-2.5 py-1.5 border border-emerald-100"
>
<span className="text-sm">{icon}</span>
<code className="font-mono text-xs text-emerald-900 select-all break-all">{msg}</code>
</div>
))}
</div>
<p className="clay-caption text-[10px] text-emerald-600 mt-2.5 italic">
{t("notification.fastEntry.undoHint")}
</p>
</div>
</div>
)} )}
{/* Mục nhập thủ công (Dành cho quản trị viên hoặc fallback) */} {/* Mục nhập thủ công (Dành cho quản trị viên hoặc fallback) */}
......
...@@ -100,6 +100,25 @@ const TransactionsPage: React.FC = () => { ...@@ -100,6 +100,25 @@ const TransactionsPage: React.FC = () => {
const openedNotificationTransactionRef = useRef<string>(); const openedNotificationTransactionRef = useRef<string>();
const notifiedDetailErrorRef = useRef(false); const notifiedDetailErrorRef = useRef(false);
// Hỗ trợ mở trực tiếp giao dịch từ Deep Link Zalo Bot (?screen=transaction-edit&id=...&source=zalo_bot)
const deepLinkInfo = useMemo(() => {
if (typeof window === "undefined") return { id: undefined, screen: undefined, isZaloBot: false };
const searchStr =
window.location.search ||
(window.location.hash.includes("?") ? window.location.hash.split("?")[1] : "");
const params = new URLSearchParams(searchStr);
const id = params.get("id") || undefined;
const screen = params.get("screen") || undefined;
const source = params.get("source") || undefined;
return {
id,
screen,
isZaloBot: source === "zalo_bot",
};
}, []);
const activeTargetTxId = notificationTransactionId || deepLinkInfo.id;
// Search & Filter state // Search & Filter state
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const deferredSearch = useDeferredValue(search.trim()); const deferredSearch = useDeferredValue(search.trim());
...@@ -206,23 +225,28 @@ const TransactionsPage: React.FC = () => { ...@@ -206,23 +225,28 @@ const TransactionsPage: React.FC = () => {
// Main Transactions Query // Main Transactions Query
const transactionsQuery = useTransactions(query); const transactionsQuery = useTransactions(query);
const notificationTransactionQuery = useTransaction(notificationTransactionId || ""); const targetTransactionQuery = useTransaction(activeTargetTxId || "");
useEffect(() => { useEffect(() => {
const transaction = notificationTransactionQuery.data?.data; const transaction = targetTransactionQuery.data?.data;
if (!transaction || openedNotificationTransactionRef.current === transaction.id) return; if (!transaction || openedNotificationTransactionRef.current === transaction.id) return;
openedNotificationTransactionRef.current = transaction.id; openedNotificationTransactionRef.current = transaction.id;
setSelectedTransaction(transaction); setSelectedTransaction(transaction);
if (deepLinkInfo.screen === "transaction-edit" || deepLinkInfo.isZaloBot) {
setIsEditOpen(true);
} else {
setIsDetailOpen(true); setIsDetailOpen(true);
}, [notificationTransactionQuery.data?.data]); }
}, [targetTransactionQuery.data?.data, deepLinkInfo.screen, deepLinkInfo.isZaloBot]);
useEffect(() => { useEffect(() => {
if (!notificationTransactionQuery.isError || notifiedDetailErrorRef.current) return; if (!targetTransactionQuery.isError || notifiedDetailErrorRef.current) return;
notifiedDetailErrorRef.current = true; notifiedDetailErrorRef.current = true;
openSnackbar({ type: "error", text: t("transaction.loadFailed") }); openSnackbar({ type: "error", text: t("transaction.loadFailed") });
}, [notificationTransactionQuery.isError, openSnackbar, t]); }, [targetTransactionQuery.isError, openSnackbar, t]);
// Mutations // Mutations
const createMutation = useCreateTransaction(); const createMutation = useCreateTransaction();
......
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