diff --git a/nginx.conf b/nginx.conf index b73e383..7a058f7 100644 --- a/nginx.conf +++ b/nginx.conf @@ -66,13 +66,13 @@ server { add_header Cache-Control "no-store, no-cache, must-revalidate"; } - location ~ ^/(foods|orders|customers|schedule|payment_methods|discounts|announcements|reports|comments|roles|admins|shipment_methods|coupons|reviews|pagers|notifications)/ { + location ~ ^/(foods|orders|customers|schedule|payment_methods|discounts|announcements|reports|comments|roles|admins|shipment_methods|coupons|reviews|pagers|notifications|wallet)/ { try_files $uri $uri/ /index.html; add_header Cache-Control "no-store, no-cache, must-revalidate"; } # Restaurant menu preview (same-origin iframe; strips X-Frame-Options from upstream) - location ~ ^/(?!auth|dashboard|setting|statistics|foods|orders|customers|schedule|payment_methods|discounts|announcements|reports|comments|roles|admins|shipment_methods|coupons|reviews|pagers|notifications|assets|icons|_next)([a-zA-Z0-9][a-zA-Z0-9_-]*)(/.*)?$ { + location ~ ^/(?!auth|dashboard|setting|statistics|foods|orders|customers|schedule|payment_methods|discounts|announcements|reports|comments|roles|admins|shipment_methods|coupons|reviews|pagers|notifications|wallet|assets|icons|_next)([a-zA-Z0-9][a-zA-Z0-9_-]*)(/.*)?$ { proxy_pass https://dmenu.danakcorp.com/$1$2$is_args$args; proxy_ssl_server_name on; proxy_set_header Host dmenu.danakcorp.com; diff --git a/src/config/Pages.ts b/src/config/Pages.ts index b8a9a7c..ec5c6e7 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -96,4 +96,7 @@ export const Pages = { statistics: { list: "/statistics", }, + wallet: { + transactions: "/wallet/transactions", + }, }; diff --git a/src/config/menuPreview.ts b/src/config/menuPreview.ts index dfd4c84..3fa5dd2 100644 --- a/src/config/menuPreview.ts +++ b/src/config/menuPreview.ts @@ -23,6 +23,7 @@ export const MENU_PREVIEW_RESERVED_SEGMENTS = new Set([ "pagers", "notifications", "sliders", + "wallet", "assets", "menu-preview", ]); diff --git a/src/langs/fa.json b/src/langs/fa.json index 38bd46b..ed2ad67 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -620,6 +620,21 @@ "invoice_issued": "صورتحساب صادر شد", "cancel": "انصراف", "balance": "موجودی کیف پول", + "balance_after": "موجودی پس از تراکنش", + "type_credit": "واریز", + "type_debit": "برداشت", + "reason": "دلیل", + "reason_deposit": "شارژ کیف پول", + "reason_sms_send": "ارسال پیامک", + "transactions_tab": "تراکنش‌ها", + "charge_requests_tab": "درخواست‌های شارژ", + "invoice": "صورتحساب", + "view_invoice": "مشاهده صورتحساب", + "charge_status_pending": "در انتظار پرداخت", + "charge_status_paid": "پرداخت شده", + "charge_status_success": "پرداخت موفق", + "charge_status_failed": "ناموفق", + "charge_status_cancelled": "لغو شده", "increese_wallet": "افزایش موجودی حساب اعتباری", "online_pay": "پرداخت آنلاین", "card_to_card": "کارت به کارت", diff --git a/src/pages/wallet/Transactions.tsx b/src/pages/wallet/Transactions.tsx new file mode 100644 index 0000000..53d3cbc --- /dev/null +++ b/src/pages/wallet/Transactions.tsx @@ -0,0 +1,88 @@ +import Button from "@/components/Button"; +import Tabs from "@/components/Tabs"; +import { EmptyWallet, ReceiptItem } from "iconsax-react"; +import { type FC, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { formatPrice } from "@/helpers/func"; +import ChargeModal from "./components/ChargeModal"; +import ChargeRequestsTab from "./components/ChargeRequestsTab"; +import TransactionsTab from "./components/TransactionsTab"; +import { WalletTabEnum } from "./enum/Enum"; +import { useGetWalletBalance } from "./hooks/useWalletData"; +import MoonLoader from "react-spinners/MoonLoader"; + +const WalletTransactions: FC = () => { + const { t } = useTranslation("global"); + const [activeTab, setActiveTab] = useState(WalletTabEnum.TRANSACTIONS); + const [showChargeModal, setShowChargeModal] = useState(false); + const { data: balanceData, isLoading: isBalanceLoading } = useGetWalletBalance(); + + const balance = balanceData?.data?.balance ?? 0; + + return ( +
+
+

{t("transaction.transaction")}

+ +
+ +
+ +
+
{t("wallet.balance")}
+
+ {isBalanceLoading ? ( + + ) : ( + formatPrice(balance) + )} +
+
+
+ +
+ + ), + label: t("wallet.transactions_tab"), + value: WalletTabEnum.TRANSACTIONS, + }, + { + icon: ( + + ), + label: t("wallet.charge_requests_tab"), + value: WalletTabEnum.CHARGE_REQUESTS, + }, + ]} + onChange={(value) => setActiveTab(value as WalletTabEnum)} + active={activeTab} + /> +
+ + {activeTab === WalletTabEnum.TRANSACTIONS && } + {activeTab === WalletTabEnum.CHARGE_REQUESTS && } + + setShowChargeModal(false)} + /> +
+ ); +}; + +export default WalletTransactions; diff --git a/src/pages/wallet/components/ChargeRequestTableColumns.tsx b/src/pages/wallet/components/ChargeRequestTableColumns.tsx new file mode 100644 index 0000000..d0e4f4d --- /dev/null +++ b/src/pages/wallet/components/ChargeRequestTableColumns.tsx @@ -0,0 +1,79 @@ +import type { ColumnType } from "@/components/types/TableTypes"; +import StatusWithText from "@/components/StatusWithText"; +import { formatOptionalDate, formatPrice } from "@/helpers/func"; +import type { TFunction } from "i18next"; +import type { ChargeRequest } from "../types/Types"; + +const getStatusVariant = (status?: string | null): "success" | "error" | "warning" => { + if (!status) return "warning"; + + const normalized = status.toLowerCase(); + + if (["success", "successful", "paid", "completed", "approved"].includes(normalized)) { + return "success"; + } + + if (["failed", "unsuccessful", "rejected", "cancelled", "canceled"].includes(normalized)) { + return "error"; + } + + return "warning"; +}; + +const getStatusLabel = (status: string | null | undefined, t: TFunction) => { + if (!status) return "-"; + + const key = `wallet.charge_status_${status.toLowerCase()}`; + const translated = t(key); + return translated !== key ? translated : status; +}; + +const openInvoice = (invoiceId: string) => { + const invoiceBaseUrl = import.meta.env.VITE_INVOICE_URL; + if (invoiceId && invoiceBaseUrl) { + window.open(`${invoiceBaseUrl}${invoiceId}`, "_blank"); + } +}; + +export const getChargeRequestTableColumns = ( + t: TFunction +): ColumnType[] => { + return [ + { + key: "amount", + title: t("transaction.amount"), + render: (item: ChargeRequest) => formatPrice(item.amount), + }, + { + key: "status", + title: t("transaction.status"), + render: (item: ChargeRequest) => ( + + ), + }, + { + key: "invoiceId", + title: t("wallet.invoice"), + render: (item: ChargeRequest) => + item.invoiceId ? ( + + ) : ( + "-" + ), + }, + { + key: "createdAt", + title: t("transaction.date"), + render: (item: ChargeRequest) => formatOptionalDate(item.createdAt), + }, + ]; +}; diff --git a/src/pages/wallet/components/ChargeRequestsTab.tsx b/src/pages/wallet/components/ChargeRequestsTab.tsx new file mode 100644 index 0000000..8caa36f --- /dev/null +++ b/src/pages/wallet/components/ChargeRequestsTab.tsx @@ -0,0 +1,55 @@ +import Filters from "@/components/Filters"; +import Table from "@/components/Table"; +import type { RowDataType } from "@/components/types/TableTypes"; +import { type FC } from "react"; +import { useTranslation } from "react-i18next"; +import { getChargeRequestTableColumns } from "./ChargeRequestTableColumns"; +import { useGetChargeRequests } from "../hooks/useWalletData"; +import { useChargeRequestFilters } from "../hooks/useChargeRequestFilters"; +import type { ChargeRequest } from "../types/Types"; + +const ChargeRequestsTab: FC = () => { + const { t } = useTranslation("global"); + const { + filters, + currentPage, + apiParams, + handleFiltersChange, + handlePageChange, + limit, + } = useChargeRequestFilters(); + + const { data: chargeRequestsData, isLoading } = useGetChargeRequests(apiParams); + + const chargeRequests = chargeRequestsData?.data || []; + const totalPages = + chargeRequestsData?.meta?.totalPages || + Math.max(1, Math.ceil(chargeRequests.length / limit) || 1); + const columns = getChargeRequestTableColumns(t); + + return ( + <> +
+ +
+ + + columns={columns} + data={chargeRequests as (ChargeRequest & RowDataType)[]} + isloading={isLoading} + pagination={{ + currentPage, + totalPages, + onPageChange: handlePageChange, + }} + /> + + ); +}; + +export default ChargeRequestsTab; diff --git a/src/pages/wallet/components/TransactionTableColumns.tsx b/src/pages/wallet/components/TransactionTableColumns.tsx new file mode 100644 index 0000000..e2f4b32 --- /dev/null +++ b/src/pages/wallet/components/TransactionTableColumns.tsx @@ -0,0 +1,60 @@ +import type { ColumnType } from "@/components/types/TableTypes"; +import StatusWithText from "@/components/StatusWithText"; +import { formatOptionalDate, formatPrice } from "@/helpers/func"; +import type { TFunction } from "i18next"; +import type { WalletTransaction } from "../types/Types"; + +const getTypeLabel = (type: string, t: TFunction) => { + if (type === "credit") return t("wallet.type_credit"); + if (type === "debit") return t("wallet.type_debit"); + return type || "-"; +}; + +const getTypeVariant = (type: string): "success" | "error" | "warning" => { + if (type === "credit") return "success"; + if (type === "debit") return "error"; + return "warning"; +}; + +const getReasonLabel = (reason: string, t: TFunction) => { + const key = `wallet.reason_${reason}`; + const translated = t(key); + return translated !== key ? translated : reason || "-"; +}; + +export const getTransactionTableColumns = ( + t: TFunction +): ColumnType[] => { + return [ + { + key: "type", + title: t("transaction.transaction2"), + render: (item: WalletTransaction) => ( + + ), + }, + { + key: "amount", + title: t("transaction.amount"), + render: (item: WalletTransaction) => formatPrice(item.amount), + }, + { + key: "balance", + title: t("wallet.balance_after"), + render: (item: WalletTransaction) => formatPrice(item.balance), + }, + { + key: "reason", + title: t("wallet.reason"), + render: (item: WalletTransaction) => getReasonLabel(item.reason, t), + }, + { + key: "createdAt", + title: t("transaction.date"), + render: (item: WalletTransaction) => formatOptionalDate(item.createdAt), + }, + ]; +}; diff --git a/src/pages/wallet/components/TransactionsTab.tsx b/src/pages/wallet/components/TransactionsTab.tsx new file mode 100644 index 0000000..4185180 --- /dev/null +++ b/src/pages/wallet/components/TransactionsTab.tsx @@ -0,0 +1,55 @@ +import Filters from "@/components/Filters"; +import Table from "@/components/Table"; +import type { RowDataType } from "@/components/types/TableTypes"; +import { type FC } from "react"; +import { useTranslation } from "react-i18next"; +import { getTransactionTableColumns } from "./TransactionTableColumns"; +import { useGetWalletTransactions } from "../hooks/useWalletData"; +import { useWalletTransactionFilters } from "../hooks/useWalletTransactionFilters"; +import type { WalletTransaction } from "../types/Types"; + +const TransactionsTab: FC = () => { + const { t } = useTranslation("global"); + const { + filters, + currentPage, + apiParams, + handleFiltersChange, + handlePageChange, + limit, + } = useWalletTransactionFilters(); + + const { data: transactionsData, isLoading } = useGetWalletTransactions(apiParams); + + const transactions = transactionsData?.data || []; + const totalPages = + transactionsData?.meta?.totalPages || + Math.max(1, Math.ceil(transactions.length / limit) || 1); + const columns = getTransactionTableColumns(t); + + return ( + <> +
+ +
+ + + columns={columns} + data={transactions as (WalletTransaction & RowDataType)[]} + isloading={isLoading} + pagination={{ + currentPage, + totalPages, + onPageChange: handlePageChange, + }} + /> + + ); +}; + +export default TransactionsTab; diff --git a/src/pages/wallet/components/WalletHeader.tsx b/src/pages/wallet/components/WalletHeader.tsx index a47938f..8e9e90f 100644 --- a/src/pages/wallet/components/WalletHeader.tsx +++ b/src/pages/wallet/components/WalletHeader.tsx @@ -1,6 +1,8 @@ import { type FC, useState } from "react"; import { EmptyWallet } from "iconsax-react"; import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { Pages } from "@/config/Pages"; import { formatPrice } from "@/helpers/func"; import { useGetWalletBalance } from "../hooks/useWalletData"; import ChargeModal from "./ChargeModal"; @@ -8,6 +10,7 @@ import MoonLoader from "react-spinners/MoonLoader"; const WalletHeader: FC = () => { const { t } = useTranslation("global"); + const navigate = useNavigate(); const [showChargeModal, setShowChargeModal] = useState(false); const { data, isLoading } = useGetWalletBalance(); @@ -16,14 +19,20 @@ const WalletHeader: FC = () => { return ( <>
- -
- {isLoading ? ( - - ) : ( - formatPrice(balance) - )} -
+