transactions
This commit is contained in:
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetTransactions = () => {
|
||||
return useQuery({
|
||||
queryKey: ["transactions"],
|
||||
queryFn: api.getTransactions,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,17 +1,55 @@
|
||||
import { DataTableDemo } from '@/components/ui/datatable'
|
||||
'use client';
|
||||
import { ef } from '@/lib/helpers/utfNumbers'
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
MoneyRecive,
|
||||
MoneySend,
|
||||
Wallet
|
||||
} 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() {
|
||||
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 (
|
||||
<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>
|
||||
|
||||
<div className='grid grid-cols-3 gap-2 h-28'>
|
||||
@@ -24,17 +62,10 @@ function TransactionsReviewIndex() {
|
||||
</div>
|
||||
<span className='text-xs2 pt-1'>مجموع واریز ها</span>
|
||||
<span className='text-xs font-medium mt-1 mb-0.5'>
|
||||
{ef('140,000,000 ریال')}
|
||||
</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>
|
||||
{formatPrice(stats.totalDeposits)} ریال
|
||||
</span>
|
||||
</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'>
|
||||
<MoneyRecive
|
||||
className='stroke-primary justify-self-center dark:stroke-neutral-200'
|
||||
@@ -43,17 +74,10 @@ function TransactionsReviewIndex() {
|
||||
</div>
|
||||
<span className='text-xs2 pt-1'>مجموع برداشت ها</span>
|
||||
<span className='text-xs font-medium mt-1 mb-0.5'>
|
||||
{ef('140,000,000 ریال')}
|
||||
</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>
|
||||
{formatPrice(stats.totalWithdrawals)} ریال
|
||||
</span>
|
||||
</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'>
|
||||
<Wallet
|
||||
className='stroke-primary justify-self-center dark:stroke-neutral-200'
|
||||
@@ -62,20 +86,13 @@ function TransactionsReviewIndex() {
|
||||
</div>
|
||||
<span className='text-xs2 pt-1'>موجودی کیف پول</span>
|
||||
<span className='text-xs font-medium mt-1 mb-0.5'>
|
||||
{ef('140,000,000 ریال')}
|
||||
</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>
|
||||
{formatPrice(walletAmount)} ریال
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full h-full bg-container rounded-3xl mt-0'>
|
||||
<DataTableDemo />
|
||||
<TransactionsTable transactions={transactions} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { api } from "@/config/axios";
|
||||
import { CouponsResponse, WalletResponse } from "../types/Types";
|
||||
import {
|
||||
CouponsResponse,
|
||||
WalletResponse,
|
||||
TransactionsResponse,
|
||||
} from "../types/Types";
|
||||
|
||||
export const getMyCoupons = async (): Promise<CouponsResponse> => {
|
||||
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");
|
||||
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 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[]>;
|
||||
|
||||
@@ -69,6 +69,8 @@ const StepOtp = ({ phone, slug, code }: Props) => {
|
||||
window.location.href = getRedirectPath()
|
||||
},
|
||||
onError: (error) => {
|
||||
clear()
|
||||
window.location.href = getRedirectPath()
|
||||
toast(extractErrorMessage(error), 'error')
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user