Commit 6db4e922 authored by ThinhNC's avatar ThinhNC

feat(transactions): add transaction calculator modal and restore dual account login

parent d8cbd2b9
...@@ -63,12 +63,12 @@ File này lưu trữ các quyết định thiết kế dài hạn và trạng th ...@@ -63,12 +63,12 @@ File này lưu trữ các quyết định thiết kế dài hạn và trạng th
## Trạng thái đã biết ## Trạng thái đã biết
- Đã cấu hình React 18, Vite 5, ZMP SDK/UI, Tailwind và bộ base components Claymorphism. - Đã cấu hình React 18, Vite 5, ZMP SDK/UI, Tailwind và bộ base components Claymorphism.
- Đã có các màn hình đăng nhập, đăng ký, quên mật khẩu, đặt lại mật khẩu, trang chủ, hồ sơ và Style Guide. - Đã có các màn hình đăng nhập, đăng ký, quên mật khẩu, đặt lại mật khẩu, trang chủ, hồ sơ và Style Guide; màn hình đăng nhập kết hợp form tài khoản/mật khẩu với validation Zod, ghi nhớ email và nút đăng nhập Zalo SDK 1-click theo chuẩn Claymorphism.
- Đã kết nối frontend với API xác thực, React Query và Zustand auth store. - Đã kết nối frontend với API xác thực, React Query và Zustand auth store.
- Dev server dùng cổng 13579 cho nội dung app và cổng 13580 cho khung mô phỏng; `index.html` tiếp tục nằm ở root để tránh iframe trả 404 hoặc màn hình đen. - Dev server dùng cổng 13579 cho nội dung app và cổng 13580 cho khung mô phỏng; `index.html` tiếp tục nằm ở root để tránh iframe trả 404 hoặc màn hình đen.
- Module quản lý ví đã có frontend tại `/wallets`, kết nối đầy đủ Wallet API qua TanStack Query, gồm danh sách/chi tiết, tạo/sửa, đặt mặc định, lưu trữ/khôi phục, tìm kiếm, sắp xếp và phân trang. `DELETE /wallets/:id` được thể hiện trong UI là lưu trữ mềm, đúng quy tắc Backend bảo toàn lịch sử. - Module quản lý ví đã có frontend tại `/wallets`, kết nối đầy đủ Wallet API qua TanStack Query, gồm danh sách/chi tiết, tạo/sửa, đặt mặc định, lưu trữ/khôi phục, tìm kiếm, sắp xếp và phân trang. `DELETE /wallets/:id` được thể hiện trong UI là lưu trữ mềm, đúng quy tắc Backend bảo toàn lịch sử.
- Module quản lý danh mục đã có frontend tại `/categories`, kết nối Category API qua TanStack Query, gồm cây cha/con, tạo/sửa, tìm kiếm, lọc loại/nguồn/trạng thái, sắp xếp, lưu trữ và khôi phục; toàn bộ nội dung có bản dịch Việt/Anh. - Module quản lý danh mục đã có frontend tại `/categories`, kết nối Category API qua TanStack Query, gồm cây cha/con, tạo/sửa, tìm kiếm, lọc loại/nguồn/trạng thái, sắp xếp, lưu trữ và khôi phục; toàn bộ nội dung có bản dịch Việt/Anh.
- Module quản lý giao dịch đã có frontend tại `/transactions`, kết nối Transaction API qua TanStack Query, gồm danh sách lịch sử theo ngày, xem/tạo/sửa/xóa giao dịch, lọc nâng cao, sắp xếp, phân trang và đính kèm hóa đơn. - Module quản lý giao dịch đã có frontend tại `/transactions`, kết nối Transaction API qua TanStack Query, gồm danh sách lịch sử theo ngày, xem/tạo/sửa/xóa giao dịch, lọc nâng cao, sắp xếp, phân trang, đính kèm hóa đơn và tích hợp nút máy tính tính toán số tiền nhanh (+, −, ×, ÷, 000) ngay giữa Loại giao dịch và Số tiền.
- Module quản lý chuyển tiền đã có frontend tại `/transfers`, dùng contract `GET/POST /transfers``DELETE /transfers/:id` qua TanStack Query; gồm form chọn ví nguồn/đích, validation số dư và hai ví khác nhau, xác nhận trước khi chuyển/xóa, lịch sử có tìm kiếm/lọc/sắp xếp/phân trang và đầy đủ loading/error/empty state. Sau thao tác tạo hoặc xóa, frontend làm mới cache transfer, wallet và transaction để lấy lại số dư do Backend tính toán. - Module quản lý chuyển tiền đã có frontend tại `/transfers`, dùng contract `GET/POST /transfers``DELETE /transfers/:id` qua TanStack Query; gồm form chọn ví nguồn/đích, validation số dư và hai ví khác nhau, xác nhận trước khi chuyển/xóa, lịch sử có tìm kiếm/lọc/sắp xếp/phân trang và đầy đủ loading/error/empty state. Sau thao tác tạo hoặc xóa, frontend làm mới cache transfer, wallet và transaction để lấy lại số dư do Backend tính toán.
- Module quản lý ngân sách đã có frontend tại `/budgets``/budgets/:id`, kết nối Budget API qua TanStack Query, gồm danh sách/chi tiết, tạo/sửa, archive/restore, tìm kiếm, lọc loại/chu kỳ/danh mục/thời điểm, sắp xếp, phân trang và cảnh báo trực quan theo tỷ lệ sử dụng; đầy đủ validation, loading/error/empty/confirmation và bản dịch Việt/Anh. - Module quản lý ngân sách đã có frontend tại `/budgets``/budgets/:id`, kết nối Budget API qua TanStack Query, gồm danh sách/chi tiết, tạo/sửa, archive/restore, tìm kiếm, lọc loại/chu kỳ/danh mục/thời điểm, sắp xếp, phân trang và cảnh báo trực quan theo tỷ lệ sử dụng; đầy đủ 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 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.
......
...@@ -327,3 +327,31 @@ export const CopyIcon: React.FC<IconProps> = ({ size = 16, ...props }) => ( ...@@ -327,3 +327,31 @@ export const CopyIcon: React.FC<IconProps> = ({ size = 16, ...props }) => (
</svg> </svg>
); );
// Calculator
export const CalculatorIcon: React.FC<IconProps> = ({ size = 24, ...props }) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
filter="url(#clay-3d-shadow)"
{...props}
>
<rect x="4" y="2" width="16" height="20" rx="4" fill="url(#clay-grad-primary)" stroke="rgb(var(--color-clay-primary-dark))" />
<rect x="7" y="5" width="10" height="4" rx="1.5" fill="rgb(var(--color-clay-surface))" stroke="rgb(var(--color-clay-highlight))" strokeWidth="1" />
<circle cx="8.5" cy="12" r="1" fill="rgb(var(--color-clay-text))" stroke="none" />
<circle cx="12" cy="12" r="1" fill="rgb(var(--color-clay-text))" stroke="none" />
<circle cx="15.5" cy="12" r="1" fill="rgb(var(--color-clay-text))" stroke="none" />
<circle cx="8.5" cy="15.5" r="1" fill="rgb(var(--color-clay-text))" stroke="none" />
<circle cx="12" cy="15.5" r="1" fill="rgb(var(--color-clay-text))" stroke="none" />
<circle cx="15.5" cy="15.5" r="1" fill="rgb(var(--color-clay-text))" stroke="none" />
<circle cx="8.5" cy="18.5" r="1" fill="rgb(var(--color-clay-text))" stroke="none" />
<rect x="11" y="17.7" width="5.5" height="1.6" rx="0.8" fill="rgb(var(--color-clay-expense))" stroke="none" />
</svg>
);
...@@ -161,6 +161,9 @@ ...@@ -161,6 +161,9 @@
"success": "Signed in successfully!", "success": "Signed in successfully!",
"failed": "Sign-in failed", "failed": "Sign-in failed",
"failedDetail": "Sign-in failed. Please check your details and try again.", "failedDetail": "Sign-in failed. Please check your details and try again.",
"or": "Or",
"account": "Account",
"accountPlaceholder": "Email or phone number",
"feature": { "feature": {
"wallet": "Manage Wallets", "wallet": "Manage Wallets",
"report": "Reports", "report": "Reports",
...@@ -908,6 +911,11 @@ ...@@ -908,6 +911,11 @@
"edit": "Edit transaction", "edit": "Edit transaction",
"detail": "Transaction Details", "detail": "Transaction Details",
"amount": "Amount", "amount": "Amount",
"calculator": "Calculator",
"calculatorTitle": "Transaction Calculator",
"calcDone": "Done",
"calcClear": "Clear All",
"calcDelete": "Delete",
"type": "Transaction Type", "type": "Transaction Type",
"wallet": "Wallet", "wallet": "Wallet",
"category": "Category", "category": "Category",
......
...@@ -158,9 +158,12 @@ ...@@ -158,9 +158,12 @@
"submit": "Đăng Nhập", "submit": "Đăng Nhập",
"noAccount": "Chưa có tài khoản?", "noAccount": "Chưa có tài khoản?",
"registerNow": "Đăng ký ngay", "registerNow": "Đăng ký ngay",
"success": "Đăng nhập thành công! 🎉", "success": "Đăng nhập thành công!",
"failed": "Đăng nhập thất bại", "failed": "Đăng nhập thất bại",
"failedDetail": "Đăng nhập thất bại, vui lòng kiểm tra lại thông tin.", "failedDetail": "Đăng nhập thất bại, vui lòng kiểm tra lại thông tin.",
"or": "Hoặc",
"account": "Tài khoản",
"accountPlaceholder": "Email hoặc số điện thoại",
"feature": { "feature": {
"wallet": "Quản lý ví", "wallet": "Quản lý ví",
"report": "Báo cáo", "report": "Báo cáo",
...@@ -942,6 +945,11 @@ ...@@ -942,6 +945,11 @@
"edit": "Chỉnh sửa giao dịch", "edit": "Chỉnh sửa giao dịch",
"detail": "Chi tiết giao dịch", "detail": "Chi tiết giao dịch",
"amount": "Số tiền", "amount": "Số tiền",
"calculator": "Máy tính",
"calculatorTitle": "Máy tính giao dịch",
"calcDone": "Xong",
"calcClear": "Xóa tất cả",
"calcDelete": "Xóa lùi",
"type": "Loại giao dịch", "type": "Loại giao dịch",
"wallet": "Ví sử dụng", "wallet": "Ví sử dụng",
"category": "Danh mục", "category": "Danh mục",
......
This diff is collapsed.
...@@ -7,6 +7,8 @@ import { LocalizedDateInput } from "@/components/shared/LocalizedDateInput"; ...@@ -7,6 +7,8 @@ import { LocalizedDateInput } from "@/components/shared/LocalizedDateInput";
import { Input } from "@/components/ui/Input"; import { Input } from "@/components/ui/Input";
import { Modal } from "@/components/ui/Modal"; import { Modal } from "@/components/ui/Modal";
import { Select } from "@/components/ui/Select"; import { Select } from "@/components/ui/Select";
import { CalculatorIcon } from "@/components/ui/icons";
import { TransactionCalculatorModal } from "./TransactionCalculatorModal";
import { TranslationFunction, useI18n } from "@/i18n"; import { TranslationFunction, useI18n } from "@/i18n";
import { useWallets } from "@/hooks/use-wallets"; import { useWallets } from "@/hooks/use-wallets";
import { useCategoryTree } from "@/hooks/use-categories"; import { useCategoryTree } from "@/hooks/use-categories";
...@@ -137,6 +139,7 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({ ...@@ -137,6 +139,7 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({
const [selectedFilePreviewUrl, setSelectedFilePreviewUrl] = useState<string | null>(null); const [selectedFilePreviewUrl, setSelectedFilePreviewUrl] = useState<string | null>(null);
const [fileError, setFileError] = useState<string | null>(null); const [fileError, setFileError] = useState<string | null>(null);
const [deleteCurrentReceipt, setDeleteCurrentReceipt] = useState(false); const [deleteCurrentReceipt, setDeleteCurrentReceipt] = useState(false);
const [isCalculatorOpen, setIsCalculatorOpen] = useState(false);
const walletsQuery = useWallets({ const walletsQuery = useWallets({
includeArchived: false, includeArchived: false,
...@@ -359,7 +362,8 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({ ...@@ -359,7 +362,8 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({
}; };
return ( return (
<Modal <>
<Modal
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
title={transaction ? t("transaction.edit") : t("transaction.create")} title={transaction ? t("transaction.edit") : t("transaction.create")}
...@@ -386,34 +390,58 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({ ...@@ -386,34 +390,58 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({
} }
> >
<form id={formId} className="flex flex-col gap-4" onSubmit={handleSubmit(submitForm)}> <form id={formId} className="flex flex-col gap-4" onSubmit={handleSubmit(submitForm)}>
<div className="grid grid-cols-2 gap-3"> <div className="flex items-end gap-2.5">
<Select {/* Loại giao dịch */}
label={t("transaction.type")} <div className="flex-1 min-w-0">
options={[ <Select
{ value: "EXPENSE", label: t("transaction.typeExpense") }, label={t("transaction.type")}
{ value: "INCOME", label: t("transaction.typeIncome") }, options={[
]} { value: "EXPENSE", label: t("transaction.typeExpense") },
disabled={isSubmitting} { value: "INCOME", label: t("transaction.typeIncome") },
{...register("type")} ]}
/> disabled={isSubmitting}
{...register("type")}
<Controller />
name="amount" </div>
control={control}
render={({ field }) => ( {/* Nút Máy tính (sau Loại giao dịch, trước Số tiền) */}
<Input <div className="flex flex-col gap-2 shrink-0">
{...field} <span className="font-nunito font-semibold text-sm px-1 invisible select-none" aria-hidden="true">
label={t("transaction.amount")} &nbsp;
inputMode="decimal" </span>
placeholder={t("transaction.amountPlaceholder")} <button
error={errors.amount?.message} id="btn-transaction-calculator"
disabled={isSubmitting} type="button"
className="text-right tabular-nums font-semibold" onClick={() => setIsCalculatorOpen(true)}
value={formatAmountInput(field.value, intlLocale)} disabled={isSubmitting}
onChange={(event) => field.onChange(parseAmountInput(event.target.value, intlLocale))} title={t("transaction.calculator") || "Máy tính"}
/> aria-label={t("transaction.calculator") || "Máy tính"}
)} className="flex h-[46px] w-[46px] items-center justify-center rounded-clay-sm bg-clay-surface text-clay-primary border border-clay-highlight/60 shadow-clay-raised transition-all duration-200 ease-in-out hover:shadow-clay-hover hover:scale-105 active:shadow-clay-pressed active:scale-95 disabled:opacity-50 disabled:pointer-events-none"
/> >
<CalculatorIcon size={22} />
</button>
</div>
{/* Số tiền */}
<div className="flex-[1.2] min-w-0">
<Controller
name="amount"
control={control}
render={({ field }) => (
<Input
{...field}
label={t("transaction.amount")}
inputMode="decimal"
placeholder={t("transaction.amountPlaceholder")}
error={errors.amount?.message}
disabled={isSubmitting}
className="text-right tabular-nums font-semibold"
value={formatAmountInput(field.value, intlLocale)}
onChange={(event) => field.onChange(parseAmountInput(event.target.value, intlLocale))}
/>
)}
/>
</div>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
...@@ -603,5 +631,18 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({ ...@@ -603,5 +631,18 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({
</div> </div>
</form> </form>
</Modal> </Modal>
<TransactionCalculatorModal
isOpen={isCalculatorOpen}
onClose={() => setIsCalculatorOpen(false)}
initialAmount={watch("amount")}
onApply={(calculatedAmount) => {
setValue("amount", calculatedAmount, {
shouldValidate: true,
shouldDirty: true,
});
}}
/>
</>
); );
}; };
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
"noImplicitAny": false, "noImplicitAny": false,
"preserveConstEnums": true, "preserveConstEnums": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"lib": ["dom", "es5", "es6", "es7", "es2017"], "lib": ["dom", "es5", "es6", "es7", "es2017", "es2018"],
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowJs": true, "allowJs": true,
......
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