transactions

This commit is contained in:
hamid zarghami
2025-12-14 14:14:52 +03:30
parent 8e49baab30
commit 1797063f34
6 changed files with 400 additions and 33 deletions
@@ -0,0 +1,272 @@
'use client'
import * as React from 'react'
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
} from '@tanstack/react-table'
import { Button } from '@/components/ui/button'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table'
import clsx from 'clsx'
import { ArrowLeft2, ArrowRight2 } from 'iconsax-react'
import { ef } from '@/lib/helpers/utfNumbers'
import { Transaction } from '../types/Types'
interface TransactionsTableProps {
transactions: Transaction[]
}
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('fa-IR', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
};
const getStatusText = (status: string): string => {
const statusMap: Record<string, string> = {
'SUCCESS': 'موفق',
'FAILED': 'ناموفق',
'PENDING': 'در انتظار',
'CANCELLED': 'لغو شده',
};
return statusMap[status] || status;
};
const getStatusColor = (status: string): string => {
const colorMap: Record<string, string> = {
'SUCCESS': 'bg-[#00BA4B]',
'FAILED': 'bg-red-500',
'PENDING': 'bg-yellow-500',
'CANCELLED': 'bg-gray-500',
};
return colorMap[status] || 'bg-transparent border border-primary';
};
export const columns: ColumnDef<Transaction>[] = [
{
accessorKey: 'order.orderNumber',
header: () => <div className='text-right pr-6'>شماره سفارش</div>,
cell: ({ row }) => {
const orderNumber = row.original.order?.orderNumber;
return (
<div className='text-right pr-6'>{ef(orderNumber?.toString() || '-')}</div>
)
}
},
{
accessorKey: 'type',
header: () => <div className='text-center'>نوع تراکنش</div>,
cell: ({ row }) => {
const type = row.original.order?.paymentMethod?.description || '-';
return <div className='text-center'>{type}</div>
}
},
{
accessorKey: 'amount',
header: () => <div className='text-center'>مبلغ</div>,
cell: ({ row }) => {
const amount = row.original.amount;
const formatted = Math.abs(amount).toLocaleString('fa-IR');
return <div className='text-center'>{ef(formatted)} ریال</div>
}
},
{
accessorKey: 'createdAt',
header: () => <div className='text-center'>تاریخ</div>,
cell: ({ row }) => {
const date = formatDate(row.original.createdAt);
return <div className='text-center'>{ef(date)}</div>
}
},
{
accessorKey: 'status',
header: () => <div className='text-center'>وضعیت</div>,
cell: ({ row }) => {
const status = row.original.status;
const statusText = getStatusText(status);
const statusColor = getStatusColor(status);
return (
<div className='text-right flex items-center justify-center gap-x-2'>
<span
className={clsx(
'size-2 rounded-full',
statusColor
)}
></span>
{statusText}
</div>
)
}
}
]
export function TransactionsTable({ transactions }: TransactionsTableProps) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({})
const [rowSelection, setRowSelection] = React.useState({})
const table = useReactTable({
data: transactions,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection
},
initialState: {
pagination: {
pageSize: 8
}
}
})
return (
<div className='w-full'>
<div className='rounded-3xl min-h-[507px] w-full'>
<Table className='w-[727px] md:w-full'>
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
<TableRow
className='dark:border-neutral-400 border-[#EAEDF5] h-14'
key={headerGroup.id}
>
{headerGroup.headers.map(header => {
return (
<TableHead
key={header.id}
className='font-light text-xs text-disabled-text text-center'
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map(row => (
<TableRow
key={row.id}
className='text-center w-full border-b-[#EAEDF5] dark:border-b-background text-xs odd:bg-[#E9EBF433] bg-border dark:odd:bg-background/80 even:bg-container border-0 border-b! h-14 border-r-4! border-r-transparent active:border-r-primary dark:active:border-r-foreground'
>
{row.getVisibleCells().map(cell => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className='h-24 text-center'
>
تراکنشی یافت نشد.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className='flex items-center justify-end space-x-2 pt-4 px-7 pb-11 select-none'>
<div className='flex-1 text-sm text-disabled-text'>
نمایش:{' '}
<span className='text-primary dark:text-foreground'>
{ef(table.getRowModel().rows.length)}
</span>{' '}
از {ef(table.getFilteredRowModel().rows.length)}
</div>
<div className='space-x-2 justify-center flex'>
<Button
variant='ghost'
size='sm'
className='size-6'
onClick={() => table.previousPage()}
hidden={!table.getCanPreviousPage()}
>
<ArrowRight2 size={12} className='stroke-[#A8ABBF]' />
</Button>
<Button
variant='secondary'
size='sm'
className='dark:bg-disabled bg-[#EAECF5] text-xs size-6'
onClick={() => table.previousPage()}
hidden={!table.getCanPreviousPage()}
>
{ef(table.getState().pagination.pageIndex)}
</Button>
<Button
variant='default'
className='text-white dark:text-foreground text-xs size-6'
size='sm'
hidden={!table.getCanPreviousPage() && !table.getCanNextPage()}
>
{ef(table.getState().pagination.pageIndex + 1)}
</Button>
<Button
variant='secondary'
size='sm'
className='dark:bg-disabled bg-[#EAECF5] text-xs size-6'
onClick={() => table.nextPage()}
hidden={!table.getCanNextPage()}
>
{ef(table.getState().pagination.pageIndex + 2)}
</Button>
<Button
variant='ghost'
size='sm'
className='size-6'
onClick={() => table.nextPage()}
hidden={!table.getCanNextPage()}
>
<ArrowLeft2 size={12} className='stroke-[#A8ABBF]' />
</Button>
</div>
</div>
</div>
)
}
@@ -20,3 +20,10 @@ export const useConvertScoreToWallet = () => {
mutationFn: api.convertScoreToWallet, mutationFn: api.convertScoreToWallet,
}); });
}; };
export const useGetTransactions = () => {
return useQuery({
queryKey: ["transactions"],
queryFn: api.getTransactions,
});
};
+49 -32
View File
@@ -1,17 +1,55 @@
import { DataTableDemo } from '@/components/ui/datatable' 'use client';
import { ef } from '@/lib/helpers/utfNumbers' import { ef } from '@/lib/helpers/utfNumbers'
import { import {
ArrowLeft,
ArrowRight,
MoneyRecive, MoneyRecive,
MoneySend, MoneySend,
Wallet Wallet
} from 'iconsax-react' } from 'iconsax-react'
import React from 'react' import React, { useMemo } from 'react'
import { useGetTransactions, useGetUserWallet } from './hooks/useTransactionData';
import { Transaction } from './types/Types';
import { TransactionsTable } from './components/TransactionsTable';
import LoadingOverlay from '@/components/overlays/LoadingOverlay';
function TransactionsReviewIndex() { function TransactionsReviewIndex() {
const { data: transactionsResponse, isLoading: isLoadingTransactions } = useGetTransactions();
const { data: walletResponse, isLoading: isLoadingWallet } = useGetUserWallet();
const transactions = transactionsResponse?.data || [];
const walletAmount = walletResponse?.data?.wallet || 0;
const stats = useMemo(() => {
if (!transactions.length) {
return {
totalDeposits: 0,
totalWithdrawals: 0,
};
}
const deposits = transactions
.filter((t: Transaction) => t.status === 'SUCCESS' && t.amount > 0)
.reduce((sum: number, t: Transaction) => sum + t.amount, 0);
const withdrawals = transactions
.filter((t: Transaction) => t.status === 'SUCCESS' && t.amount < 0)
.reduce((sum: number, t: Transaction) => sum + Math.abs(t.amount), 0);
return {
totalDeposits: deposits,
totalWithdrawals: withdrawals,
};
}, [transactions]);
const formatPrice = (amount: number) => {
return ef(amount.toLocaleString('fa-IR'));
};
const isLoading = isLoadingTransactions || isLoadingWallet;
return ( return (
<section className='flex flex-col h-full pb-10 pt-6 gap-4'> <section className='flex flex-col h-full pb-10 pt-6 gap-4 relative'>
{isLoading && <LoadingOverlay />}
<h1 className='font-medium text-base'>لیست تراکنش ها</h1> <h1 className='font-medium text-base'>لیست تراکنش ها</h1>
<div className='grid grid-cols-3 gap-2 h-28'> <div className='grid grid-cols-3 gap-2 h-28'>
@@ -24,17 +62,10 @@ function TransactionsReviewIndex() {
</div> </div>
<span className='text-xs2 pt-1'>مجموع واریز ها</span> <span className='text-xs2 pt-1'>مجموع واریز ها</span>
<span className='text-xs font-medium mt-1 mb-0.5'> <span className='text-xs font-medium mt-1 mb-0.5'>
{ef('140,000,000 ریال')} {formatPrice(stats.totalDeposits)} ریال
</span>
<span className='text-xs2 text-green-600 mt-0.5 bg-green-100 dark:bg-transparent dark:border dark:border-green-400 w-min px-1 py-px items-center justify-between rounded-md flex gap-0.5'>
<ArrowRight
size={10}
className='stroke-green-600 mb-0.5 -rotate-z-45'
/>
<span className='mt-0.5'>{ef('30%')}</span>
</span> </span>
</div> </div>
<div className='w-full h-full flex flex-col items-center justify-between p-2.5 text-center bg-container rounded-xl'> <div className='w-full h-full flex flex-col items-center justify-center p-2.5 text-center bg-container rounded-xl'>
<div className='size-8 bg-background rounded-full grid items-center'> <div className='size-8 bg-background rounded-full grid items-center'>
<MoneyRecive <MoneyRecive
className='stroke-primary justify-self-center dark:stroke-neutral-200' className='stroke-primary justify-self-center dark:stroke-neutral-200'
@@ -43,17 +74,10 @@ function TransactionsReviewIndex() {
</div> </div>
<span className='text-xs2 pt-1'>مجموع برداشت ها</span> <span className='text-xs2 pt-1'>مجموع برداشت ها</span>
<span className='text-xs font-medium mt-1 mb-0.5'> <span className='text-xs font-medium mt-1 mb-0.5'>
{ef('140,000,000 ریال')} {formatPrice(stats.totalWithdrawals)} ریال
</span>
<span className='text-xs2 text-green-600 mt-0.5 bg-green-100 dark:bg-transparent dark:border dark:border-green-400 w-min px-1 py-px items-center justify-between rounded-md flex gap-0.5'>
<ArrowRight
size={10}
className='stroke-green-600 mb-0.5 -rotate-z-45'
/>
<span className='mt-0.5'>{ef('30%')}</span>
</span> </span>
</div> </div>
<div className='w-full h-full flex flex-col items-center justify-between p-2.5 text-center bg-container rounded-xl'> <div className='w-full h-full flex flex-col items-center justify-center p-2.5 text-center bg-container rounded-xl'>
<div className='size-8 bg-background rounded-full grid items-center'> <div className='size-8 bg-background rounded-full grid items-center'>
<Wallet <Wallet
className='stroke-primary justify-self-center dark:stroke-neutral-200' className='stroke-primary justify-self-center dark:stroke-neutral-200'
@@ -62,20 +86,13 @@ function TransactionsReviewIndex() {
</div> </div>
<span className='text-xs2 pt-1'>موجودی کیف پول</span> <span className='text-xs2 pt-1'>موجودی کیف پول</span>
<span className='text-xs font-medium mt-1 mb-0.5'> <span className='text-xs font-medium mt-1 mb-0.5'>
{ef('140,000,000 ریال')} {formatPrice(walletAmount)} ریال
</span>
<span className='text-xs2 text-red-600 mt-0.5 bg-red-100 dark:bg-transparent dark:border dark:border-red-400 w-min px-1 py-px items-center justify-between rounded-md flex gap-1'>
<ArrowLeft
size={10}
className='stroke-red-600 mb-0.5 -rotate-z-45'
/>
<span className='mt-0.5'>{ef('30%')}</span>
</span> </span>
</div> </div>
</div> </div>
<div className='w-full h-full bg-container rounded-3xl mt-0'> <div className='w-full h-full bg-container rounded-3xl mt-0'>
<DataTableDemo /> <TransactionsTable transactions={transactions} />
</div> </div>
</section> </section>
) )
@@ -1,5 +1,9 @@
import { api } from "@/config/axios"; import { api } from "@/config/axios";
import { CouponsResponse, WalletResponse } from "../types/Types"; import {
CouponsResponse,
WalletResponse,
TransactionsResponse,
} from "../types/Types";
export const getMyCoupons = async (): Promise<CouponsResponse> => { export const getMyCoupons = async (): Promise<CouponsResponse> => {
const { data } = await api.get<CouponsResponse>("/public/coupons/me"); const { data } = await api.get<CouponsResponse>("/public/coupons/me");
@@ -15,3 +19,8 @@ export const convertScoreToWallet = async () => {
const { data } = await api.post("/public/user/convert-score-to-wallet"); const { data } = await api.post("/public/user/convert-score-to-wallet");
return data; return data;
}; };
export const getTransactions = async (): Promise<TransactionsResponse> => {
const { data } = await api.get<TransactionsResponse>("/public/payments");
return data;
};
@@ -38,3 +38,63 @@ export interface Wallet {
} }
export type WalletResponse = BaseResponse<Wallet>; export type WalletResponse = BaseResponse<Wallet>;
export interface PaymentMethod {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
restaurant: string;
title: string;
method: string;
gateway: string | null;
description: string;
enabled: boolean;
order: number;
merchantId: string | null;
}
export interface TransactionOrder {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
user: string;
restaurant: string;
deliveryMethod: string;
address: string | null;
paymentMethod: PaymentMethod;
orderNumber: number;
couponDiscount: number;
couponDetail: unknown | null;
itemsDiscount: number;
totalDiscount: number;
subTotal: number;
tax: number;
deliveryFee: number;
total: number;
totalItems: number;
description: string | null;
tableNumber: string;
status: string;
paymentStatus: string;
}
export interface Transaction {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
order: TransactionOrder;
amount: number;
authority: string | null;
gateway: string | null;
refId: string | null;
status: string;
cardPan: string | null;
verifyResponse: unknown | null;
paidAt: string | null;
failedAt: string | null;
}
export type TransactionsResponse = BaseResponse<Transaction[]>;
+2
View File
@@ -69,6 +69,8 @@ const StepOtp = ({ phone, slug, code }: Props) => {
window.location.href = getRedirectPath() window.location.href = getRedirectPath()
}, },
onError: (error) => { onError: (error) => {
clear()
window.location.href = getRedirectPath()
toast(extractErrorMessage(error), 'error') toast(extractErrorMessage(error), 'error')
} }
}) })