From 28295dcc58708025d2f5b05be7dc36bb27b91c66 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sun, 24 May 2026 10:17:16 +0330 Subject: [PATCH] add payment page --- Dockerfile | 8 + src/config/Paths.tsx | 3 + src/pages/payment/List.tsx | 289 ++++++++++++++++++++ src/pages/payment/enum/PaymentEnum.ts | 12 + src/pages/payment/hooks/usePaymentData.ts | 40 +++ src/pages/payment/service/PaymentService.ts | 26 ++ src/pages/payment/types/Types.ts | 51 ++++ src/router/MainRouter.tsx | 2 + src/shared/Sidebar.tsx | 9 + 9 files changed, 440 insertions(+) create mode 100644 src/pages/payment/List.tsx create mode 100644 src/pages/payment/enum/PaymentEnum.ts create mode 100644 src/pages/payment/hooks/usePaymentData.ts create mode 100644 src/pages/payment/service/PaymentService.ts create mode 100644 src/pages/payment/types/Types.ts diff --git a/Dockerfile b/Dockerfile index 44b2d4c..5941bac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,14 @@ WORKDIR /build COPY package*.json ./ RUN npm install COPY . ./ + +ARG VITE_API_BASE_URL +ARG VITE_TOKEN_NAME +ARG VITE_REFRESH_TOKEN_NAME +ENV VITE_API_BASE_URL=$VITE_API_BASE_URL \ + VITE_TOKEN_NAME=$VITE_TOKEN_NAME \ + VITE_REFRESH_TOKEN_NAME=$VITE_REFRESH_TOKEN_NAME + RUN npm run build FROM nginx:stable-alpine AS production-stage diff --git a/src/config/Paths.tsx b/src/config/Paths.tsx index c88ec1c..cf3f56c 100644 --- a/src/config/Paths.tsx +++ b/src/config/Paths.tsx @@ -63,6 +63,9 @@ export const Paths = { detail: '/invoice/', update: '/invoice/update/', }, + payments: { + list: '/payments', + }, newOrder: '/new-order', orderDetails: '/order/', tickets: { diff --git a/src/pages/payment/List.tsx b/src/pages/payment/List.tsx new file mode 100644 index 0000000..dee6f4d --- /dev/null +++ b/src/pages/payment/List.tsx @@ -0,0 +1,289 @@ +import { type FC, useMemo, useState } from 'react'; +import moment from 'moment-jalaali'; +import { Link } from 'react-router-dom'; +import { Eye } from 'iconsax-react'; +import Tabs from '@/components/Tabs'; +import Table from '@/components/Table'; +import Filters, { type FilterValues } from '@/components/Filters'; +import ModalConfrim from '@/components/ModalConfrim'; +import type { ColumnType } from '@/components/types/TableTypes'; +import { Paths } from '@/config/Paths'; +import { toast } from '@/shared/toast'; +import { extractErrorMessage } from '@/config/func'; +import { + useConfirmCashPayment, + useDenyCashPayment, + useGetPayments, + useVerifyOnlinePayment, +} from './hooks/usePaymentData'; +import { + PaymentMethodEnum, + PaymentStatusEnum, +} from './enum/PaymentEnum'; +import type { PaymentType } from './types/Types'; + +type StatusTab = '' | PaymentStatusEnum; + +const methodLabels: Record = { + [PaymentMethodEnum.Online]: 'آنلاین', + [PaymentMethodEnum.Cash]: 'نقدی', + [PaymentMethodEnum.Credit]: 'اعتبار', +}; + +const statusLabels: Record = { + [PaymentStatusEnum.Pending]: 'در انتظار', + [PaymentStatusEnum.Paid]: 'پرداخت شده', + [PaymentStatusEnum.Failed]: 'ناموفق', + [PaymentStatusEnum.Refunded]: 'بازگشت داده شده', +}; + +const statusBadgeClass: Record = { + [PaymentStatusEnum.Pending]: 'bg-amber-100 text-amber-800', + [PaymentStatusEnum.Paid]: 'bg-green-100 text-green-800', + [PaymentStatusEnum.Failed]: 'bg-red-100 text-red-800', + [PaymentStatusEnum.Refunded]: 'bg-gray-100 text-gray-800', +}; + +const formatAmount = (amount: number) => + Number(amount).toLocaleString('fa-IR') + ' تومان'; + +const getCustomerName = (item: PaymentType) => { + const user = item.invoice?.user; + if (!user) return '-'; + const name = [user.firstName, user.lastName].filter(Boolean).join(' '); + return name || user.phone || '-'; +}; + +const PaymentsList: FC = () => { + const [page, setPage] = useState(1); + const [activeTab, setActiveTab] = useState(''); + const [filters, setFilters] = useState({}); + const [confirmAction, setConfirmAction] = useState<{ + type: 'cash-confirm' | 'cash-deny' | 'online-verify'; + payment: PaymentType; + } | null>(null); + + const statuses = useMemo(() => { + if (!activeTab) return undefined; + return [activeTab]; + }, [activeTab]); + + const search = (filters.search ?? '').trim() || undefined; + + const { data, isLoading } = useGetPayments({ + page, + limit: 10, + search, + statuses, + }); + + const confirmCash = useConfirmCashPayment(); + const denyCash = useDenyCashPayment(); + const verifyOnline = useVerifyOnlinePayment(); + + const payments = data?.data ?? []; + const meta = data?.meta; + + const isActionLoading = + confirmCash.isPending || denyCash.isPending || verifyOnline.isPending; + + const handleConfirmAction = () => { + if (!confirmAction) return; + + const { type, payment } = confirmAction; + const onSuccess = () => { + toast('عملیات با موفقیت انجام شد', 'success'); + setConfirmAction(null); + }; + const onError = (error: unknown) => { + toast(extractErrorMessage(error), 'error'); + }; + + if (type === 'cash-confirm') { + confirmCash.mutate(payment.id, { onSuccess, onError }); + return; + } + if (type === 'cash-deny') { + denyCash.mutate(payment.id, { onSuccess, onError }); + return; + } + if (type === 'online-verify' && payment.token) { + verifyOnline.mutate(payment.token, { onSuccess, onError }); + } + }; + + const getConfirmLabel = () => { + if (!confirmAction) return ''; + switch (confirmAction.type) { + case 'cash-confirm': + return 'آیا از تایید این پرداخت نقدی اطمینان دارید؟'; + case 'cash-deny': + return 'آیا از رد این پرداخت نقدی اطمینان دارید؟'; + case 'online-verify': + return 'آیا از تایید این پرداخت آنلاین اطمینان دارید؟'; + default: + return ''; + } + }; + + const columns: ColumnType[] = [ + { + title: 'شماره پیش‌فاکتور', + key: 'invoiceNumber', + render: (item) => item.invoice?.invoiceNumber ?? '-', + }, + { + title: 'مشتری', + key: 'customer', + render: (item) => getCustomerName(item), + }, + { + title: 'مبلغ', + key: 'amount', + render: (item) => formatAmount(item.amount), + }, + { + title: 'روش پرداخت', + key: 'method', + render: (item) => methodLabels[item.method] ?? item.method, + }, + { + title: 'وضعیت', + key: 'status', + render: (item) => ( + + {statusLabels[item.status] ?? item.status} + + ), + }, + { + title: 'تاریخ', + key: 'createdAt', + render: (item) => + item.createdAt + ? moment(item.createdAt).format('jYYYY/jMM/jDD HH:mm') + : '-', + }, + { + title: 'عملیات', + key: 'actions', + render: (item) => { + const isPending = item.status === PaymentStatusEnum.Pending; + const isCash = item.method === PaymentMethodEnum.Cash; + const isOnline = item.method === PaymentMethodEnum.Online; + + return ( +
+ {item.invoice?.id && ( + + + + )} + {isPending && isCash && ( + <> + + + + )} + {isPending && isOnline && item.token && ( + + )} +
+ ); + }, + }, + ]; + + return ( +
+

پرداخت‌ها

+ +
+ { + setActiveTab(tab as StatusTab); + setPage(1); + }} + /> +
+ +
+ { + setFilters(values); + setPage(1); + }} + /> +
+ +
+ 1 + ? { + currentPage: meta.page, + totalPages: meta.totalPages, + onPageChange: setPage, + } + : undefined + } + /> + + + setConfirmAction(null)} + onConfrim={handleConfirmAction} + isloading={isActionLoading} + label={getConfirmLabel()} + /> + + ); +}; + +export default PaymentsList; diff --git a/src/pages/payment/enum/PaymentEnum.ts b/src/pages/payment/enum/PaymentEnum.ts new file mode 100644 index 0000000..7100f0b --- /dev/null +++ b/src/pages/payment/enum/PaymentEnum.ts @@ -0,0 +1,12 @@ +export enum PaymentStatusEnum { + Pending = 'pending', + Paid = 'paid', + Failed = 'failed', + Refunded = 'refunded', +} + +export enum PaymentMethodEnum { + Online = 'Online', + Cash = 'Cash', + Credit = 'Credit', +} diff --git a/src/pages/payment/hooks/usePaymentData.ts b/src/pages/payment/hooks/usePaymentData.ts new file mode 100644 index 0000000..092252b --- /dev/null +++ b/src/pages/payment/hooks/usePaymentData.ts @@ -0,0 +1,40 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import * as api from '../service/PaymentService'; +import type { GetPaymentsParams } from '../types/Types'; + +export const useGetPayments = (params: GetPaymentsParams) => { + return useQuery({ + queryKey: ['payments', params], + queryFn: () => api.getPayments(params), + }); +}; + +export const useConfirmCashPayment = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (paymentId: string) => api.confirmCashPayment(paymentId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['payments'] }); + }, + }); +}; + +export const useDenyCashPayment = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (paymentId: string) => api.denyCashPayment(paymentId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['payments'] }); + }, + }); +}; + +export const useVerifyOnlinePayment = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (token: string) => api.verifyOnlinePayment(token), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['payments'] }); + }, + }); +}; diff --git a/src/pages/payment/service/PaymentService.ts b/src/pages/payment/service/PaymentService.ts new file mode 100644 index 0000000..b0cd701 --- /dev/null +++ b/src/pages/payment/service/PaymentService.ts @@ -0,0 +1,26 @@ +import axios from '@/config/axios'; +import type { GetPaymentsParams, GetPaymentsResponseType } from '../types/Types'; + +export const getPayments = async ( + params: GetPaymentsParams +): Promise => { + const { data } = await axios.get('/admin/payments', { + params, + }); + return data; +}; + +export const confirmCashPayment = async (paymentId: string) => { + const { data } = await axios.post(`/admin/payments/${paymentId}/cash/verify`); + return data; +}; + +export const denyCashPayment = async (paymentId: string) => { + const { data } = await axios.delete(`/admin/payments/${paymentId}/cash/deny`); + return data; +}; + +export const verifyOnlinePayment = async (token: string) => { + const { data } = await axios.post(`/admin/payments/online/${token}/verify`); + return data; +}; diff --git a/src/pages/payment/types/Types.ts b/src/pages/payment/types/Types.ts new file mode 100644 index 0000000..e9aa4e4 --- /dev/null +++ b/src/pages/payment/types/Types.ts @@ -0,0 +1,51 @@ +import type { PaymentMethodEnum, PaymentStatusEnum } from '../enum/PaymentEnum'; + +export type PaymentUserType = { + id: string; + firstName?: string; + lastName?: string; + phone?: string; +}; + +export type PaymentInvoiceType = { + id: string; + invoiceNumber: number; + user?: PaymentUserType; +}; + +export type PaymentType = { + id: string; + amount: number; + method: PaymentMethodEnum; + status: PaymentStatusEnum; + gateway?: string | null; + token?: string | null; + transactionId?: string | null; + description?: string | null; + createdAt: string; + paidAt?: string | null; + failedAt?: string | null; + invoice?: PaymentInvoiceType; +}; + +export type GetPaymentsParams = { + page?: number; + limit?: number; + search?: string; + statuses?: PaymentStatusEnum[]; + invoiceId?: string; + from?: string; + to?: string; +}; + +export type GetPaymentsResponseType = { + statusCode: number; + success: boolean; + data: PaymentType[]; + meta: { + total: number; + page: number; + limit: number; + totalPages: number; + }; +}; diff --git a/src/router/MainRouter.tsx b/src/router/MainRouter.tsx index 9341164..36d3d00 100644 --- a/src/router/MainRouter.tsx +++ b/src/router/MainRouter.tsx @@ -56,6 +56,7 @@ import ConvertToOrders from "@/pages/order/ConvertToOrders"; import OrderPrint from "@/pages/order/Print"; import TicketList from "@/pages/ticket/TicketList"; import TicketDetail from "@/pages/ticket/Detail"; +import PaymentsList from "@/pages/payment/List"; const MainRouter: FC = () => { return ( @@ -71,6 +72,7 @@ const MainRouter: FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/shared/Sidebar.tsx b/src/shared/Sidebar.tsx index 6df4d0d..f83512f 100644 --- a/src/shared/Sidebar.tsx +++ b/src/shared/Sidebar.tsx @@ -12,6 +12,7 @@ import { People, Printer, Receipt21, + MoneyRecive, ShieldSecurity, Teacher, User, @@ -90,6 +91,14 @@ const SideBar: FC = () => { activeName='view_invoices' /> + } + title={'پرداخت‌ها'} + isActive={isActive('/payments')} + link={Paths.payments.list} + activeName='manage_payments' + /> + } title={'سفارشات'}