Commit 4f565cd8 authored by ThinhNC's avatar ThinhNC

Merge branch 'feat/transaction-calculator-and-auth-enhancements' into 'develop'

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

See merge request !34
parents 254876b8 6db4e922
......@@ -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
- Đã 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.
- 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ý 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ý 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.
......
......@@ -327,3 +327,31 @@ export const CopyIcon: React.FC<IconProps> = ({ size = 16, ...props }) => (
</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 @@
"success": "Signed in successfully!",
"failed": "Sign-in failed",
"failedDetail": "Sign-in failed. Please check your details and try again.",
"or": "Or",
"account": "Account",
"accountPlaceholder": "Email or phone number",
"feature": {
"wallet": "Manage Wallets",
"report": "Reports",
......@@ -908,6 +911,11 @@
"edit": "Edit transaction",
"detail": "Transaction Details",
"amount": "Amount",
"calculator": "Calculator",
"calculatorTitle": "Transaction Calculator",
"calcDone": "Done",
"calcClear": "Clear All",
"calcDelete": "Delete",
"type": "Transaction Type",
"wallet": "Wallet",
"category": "Category",
......
......@@ -158,9 +158,12 @@
"submit": "Đăng Nhập",
"noAccount": "Chưa có tài khoản?",
"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",
"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": {
"wallet": "Quản lý ví",
"report": "Báo cáo",
......@@ -942,6 +945,11 @@
"edit": "Chỉnh sửa giao dịch",
"detail": "Chi tiết giao dịch",
"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",
"wallet": "Ví sử dụng",
"category": "Danh mục",
......
This diff is collapsed.
......@@ -7,6 +7,8 @@ import { LocalizedDateInput } from "@/components/shared/LocalizedDateInput";
import { Input } from "@/components/ui/Input";
import { Modal } from "@/components/ui/Modal";
import { Select } from "@/components/ui/Select";
import { CalculatorIcon } from "@/components/ui/icons";
import { TransactionCalculatorModal } from "./TransactionCalculatorModal";
import { TranslationFunction, useI18n } from "@/i18n";
import { useWallets } from "@/hooks/use-wallets";
import { useCategoryTree } from "@/hooks/use-categories";
......@@ -137,6 +139,7 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({
const [selectedFilePreviewUrl, setSelectedFilePreviewUrl] = useState<string | null>(null);
const [fileError, setFileError] = useState<string | null>(null);
const [deleteCurrentReceipt, setDeleteCurrentReceipt] = useState(false);
const [isCalculatorOpen, setIsCalculatorOpen] = useState(false);
const walletsQuery = useWallets({
includeArchived: false,
......@@ -359,7 +362,8 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({
};
return (
<Modal
<>
<Modal
isOpen={isOpen}
onClose={onClose}
title={transaction ? t("transaction.edit") : t("transaction.create")}
......@@ -386,34 +390,58 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({
}
>
<form id={formId} className="flex flex-col gap-4" onSubmit={handleSubmit(submitForm)}>
<div className="grid grid-cols-2 gap-3">
<Select
label={t("transaction.type")}
options={[
{ value: "EXPENSE", label: t("transaction.typeExpense") },
{ value: "INCOME", label: t("transaction.typeIncome") },
]}
disabled={isSubmitting}
{...register("type")}
/>
<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 className="flex items-end gap-2.5">
{/* Loại giao dịch */}
<div className="flex-1 min-w-0">
<Select
label={t("transaction.type")}
options={[
{ value: "EXPENSE", label: t("transaction.typeExpense") },
{ value: "INCOME", label: t("transaction.typeIncome") },
]}
disabled={isSubmitting}
{...register("type")}
/>
</div>
{/* Nút Máy tính (sau Loại giao dịch, trước Số tiền) */}
<div className="flex flex-col gap-2 shrink-0">
<span className="font-nunito font-semibold text-sm px-1 invisible select-none" aria-hidden="true">
&nbsp;
</span>
<button
id="btn-transaction-calculator"
type="button"
onClick={() => setIsCalculatorOpen(true)}
disabled={isSubmitting}
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 className="grid grid-cols-2 gap-3">
......@@ -603,5 +631,18 @@ export const TransactionFormModal: React.FC<TransactionFormModalProps> = ({
</div>
</form>
</Modal>
<TransactionCalculatorModal
isOpen={isCalculatorOpen}
onClose={() => setIsCalculatorOpen(false)}
initialAmount={watch("amount")}
onApply={(calculatedAmount) => {
setValue("amount", calculatedAmount, {
shouldValidate: true,
shouldDirty: true,
});
}}
/>
</>
);
};
......@@ -6,7 +6,7 @@
"noImplicitAny": false,
"preserveConstEnums": true,
"jsx": "react-jsx",
"lib": ["dom", "es5", "es6", "es7", "es2017"],
"lib": ["dom", "es5", "es6", "es7", "es2017", "es2018"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": 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