From c88646b64c0f4b4a98300d0c5e92e513d25cea12 Mon Sep 17 00:00:00 2001 From: smanylov Date: Sat, 21 Feb 2026 15:07:37 +0700 Subject: [PATCH] add payments history tanstak-table --- src/app/actions/yooKassa.ts | 40 +- .../components/tanstak-table/TanstakTable.tsx | 28 +- .../hooks/react-query/useUserPaymentsData.ts | 37 ++ src/app/lib/formatDate.ts | 17 + src/app/styles/pages-styles.scss | 7 +- .../payment-tab-transaction-history.tsx | 504 +++++++++++++++++- src/i18n/messages/en.json | 6 +- src/i18n/messages/ru.json | 6 +- 8 files changed, 616 insertions(+), 29 deletions(-) create mode 100644 src/app/hooks/react-query/useUserPaymentsData.ts create mode 100644 src/app/lib/formatDate.ts diff --git a/src/app/actions/yooKassa.ts b/src/app/actions/yooKassa.ts index 8c0cc68..0e95728 100644 --- a/src/app/actions/yooKassa.ts +++ b/src/app/actions/yooKassa.ts @@ -1,7 +1,7 @@ 'use server' import { YooCheckout, ICreatePayment } from '@a2seven/yoo-checkout'; -import { getSessionData } from '@/app/actions/session'; +import { getSessionData, deleteSession } from '@/app/actions/session'; import { API_BASE_URL } from '@/app/actions/definitions'; const checkout = new YooCheckout({ shopId: '1276731', secretKey: 'test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4' }); @@ -77,3 +77,41 @@ export async function yooKasaCreatePayment(amount: number, tariff: number, opera return null } } + +export async function fetchPaymentsHistory() { + const email = await getSessionData('email'); + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 30005, + message_body: { + action: 'user_payments', + email: email + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + const parsed = await response.json(); + if (parsed.message_code === 0) { + return { + payments: parsed.message_body.payments, + error: null + }; + } else { + throw parsed.message_code; + } + } else { + return null + } + } catch (error) { + return null + } +} diff --git a/src/app/components/tanstak-table/TanstakTable.tsx b/src/app/components/tanstak-table/TanstakTable.tsx index b9ef430..df0c9b3 100644 --- a/src/app/components/tanstak-table/TanstakTable.tsx +++ b/src/app/components/tanstak-table/TanstakTable.tsx @@ -14,8 +14,8 @@ import { import { IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation, IconInfo } from '@/app/ui/icons/icons'; import { useTranslations, useLocale } from 'next-intl'; import DropDownList from '@/app/components/DropDownList'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { getUserFilesData, removeUserFile, viewFileInfo } from '@/app/actions/fileEntity'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { getUserFilesData, viewFileInfo } from '@/app/actions/fileEntity'; import ModalWindow from '@/app/components/ModalWindow'; import { toast } from 'sonner'; import { pluralize } from '@/app/lib/pluralize'; @@ -25,7 +25,7 @@ import { FileMonitoringModalWindow } from '@/app/ui/modal-windows/file-monitorin import { FileTypeIcon } from '@/app/components/FileTypeIcon'; import { fetchReferralUserStats } from '@/app/actions/referralsActions'; import { MonitoringDropDown } from '@/app/components/tanstak-table/MonitoringDropDown'; -import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo'; +import {formatDate, formatDateTime} from '@/app/lib/formatDate'; type FileType = 'image' | 'video' | 'audio'; @@ -59,24 +59,6 @@ type ApiResponse = { files?: ApiFile[]; }; -// Форматирование даты из timestamp -const formatDate = (timestamp: number) => { - const date = new Date(timestamp); - const day = date.getDate().toString().padStart(2, '0'); - const month = (date.getMonth() + 1).toString().padStart(2, '0'); - const year = date.getFullYear(); - - return `${day}.${month}.${year}`; -}; - -const formatDateTime = (timestamp: number) => { - const date = new Date(timestamp); - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - - return `${hours}:${minutes}`; -}; - const cutFileName = (fileName: string) => { const MAX_FILE_NAME_LENGTH = 26; const lastDotIndex = fileName.lastIndexOf('.'); @@ -367,6 +349,8 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) { {formatDate(row.original.uploadDate)}
{formatDateTime(row.original.uploadDate)} +
+ {row.original.uploadDate} ) : (
-
@@ -583,7 +567,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) { } return result; - }, [tableData, typeFilter, dateFilter]); //tut + }, [tableData, typeFilter, dateFilter]); useEffect(() => { const currentPageRows = table.getRowModel().rows; diff --git a/src/app/hooks/react-query/useUserPaymentsData.ts b/src/app/hooks/react-query/useUserPaymentsData.ts new file mode 100644 index 0000000..ec6c897 --- /dev/null +++ b/src/app/hooks/react-query/useUserPaymentsData.ts @@ -0,0 +1,37 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchPaymentsHistory } from '@/app/actions/yooKassa'; + +export interface PaymentsData { + amount: number, + cancellationReason: string | null, + createdAt: string, + id: number, + operationType: string, + paymentUuid: string, + status: string, + updatedAt: string, +} + + +export interface UserPaymentsData { + payments: PaymentsData[], + error: string | null +} + +export const useUserPaymentsData = () => { + return useQuery({ + queryKey: ['userPaymentsData'], + queryFn: fetchPaymentsHistory, + select: (data): UserPaymentsData => { + if (!data) { + return { + payments: [], + error: null + } + } + + return data; + }, + retry: false + }); +}; \ No newline at end of file diff --git a/src/app/lib/formatDate.ts b/src/app/lib/formatDate.ts new file mode 100644 index 0000000..e477ccb --- /dev/null +++ b/src/app/lib/formatDate.ts @@ -0,0 +1,17 @@ +// Форматирование даты из timestamp +export const formatDate = (timestamp: number | string) => { + const date = new Date(timestamp); + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear(); + + return `${day}.${month}.${year}`; +}; + +export const formatDateTime = (timestamp: number | string) => { + const date = new Date(timestamp); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + return `${hours}:${minutes}`; +}; \ No newline at end of file diff --git a/src/app/styles/pages-styles.scss b/src/app/styles/pages-styles.scss index 71d0943..d8f612d 100644 --- a/src/app/styles/pages-styles.scss +++ b/src/app/styles/pages-styles.scss @@ -835,8 +835,11 @@ .table-item-status { padding: 2px 10px; border-radius: 15px; - background-color: #dcfce7; - color: #166534; + + &.succeeded { + background-color: #dcfce7; + color: #166534; + } } .actions { diff --git a/src/app/ui/payment/payment-tab-transaction-history.tsx b/src/app/ui/payment/payment-tab-transaction-history.tsx index 3869aa4..e25ca88 100644 --- a/src/app/ui/payment/payment-tab-transaction-history.tsx +++ b/src/app/ui/payment/payment-tab-transaction-history.tsx @@ -1,7 +1,507 @@ +'use client' + +import { useUserPaymentsData, PaymentsData } from '@/app/hooks/react-query/useUserPaymentsData'; +import { useEffect, useMemo, useState } from 'react'; +import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, getFilteredRowModel, ColumnDef, SortingState, ColumnFiltersState } from '@tanstack/react-table'; + +import { useTranslations } from 'next-intl'; +import { IconArrowUp, IconArrowDown, IconFilter, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft } from '@/app/ui/icons/icons'; +import { formatDate, formatDateTime } from '@/app/lib/formatDate'; +import DropDownList from '@/app/components/DropDownList'; + export default function PaymentTabTransactionHistory() { + const { data: userPaymentsData, isLoading, isError, error, } = useUserPaymentsData(); + const t = useTranslations('Global'); + + // Состояния + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [dateFilter, setDateFilter] = useState('all'); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const columns = useMemo[]>( + () => [ + { + accessorKey: 'id', + header: ({ column }) => ( +
+ + ID + + +
+ ), + cell: ({ row }) => { + return ( +
+
+ {row.original.id ? row.original.id : '-'} +
+
+ ) + } + }, + { + accessorKey: 'amount', + header: ({ column }) => ( +
+ + {t('amount')} + + +
+ ), + cell: ({ row }) => ( +
+ {row.original.amount} +
+ ), + }, + { + accessorKey: 'operationType', + header: ({ column }) => ( +
+ + {t('operation-type')} + + +
+ ), + cell: ({ row }) => ( +
+ {row.original.operationType} +
+ ), + }, + { + accessorKey: 'status', + header: ({ column }) => ( +
+ + {t('status')} + + +
+ ), + cell: ({ row }) => { + return ( +
+

+ {t(row.original.status.toLocaleLowerCase())} +

+
+ ) + }, + }, + { + accessorKey: 'date', + header: ({ column }) => ( +
+ + {t('date')} + + +
+ ), + cell: ({ row }) => { + return ( +
+ {row.original.createdAt ? ( + <> + {formatDate(row.original.createdAt)} +
+ {formatDateTime(row.original.createdAt)} + + ) : ( +
-
+ )} +
+ ) + }, + sortingFn: (rowA, rowB) => { + const dateA = new Date(rowA.original.createdAt).getTime(); + const dateB = new Date(rowB.original.createdAt).getTime(); + return dateA - dateB; + }, + enableColumnFilter: false, + }, + ], + [] + ) + + const filteredData = useMemo(() => { + let result = userPaymentsData?.payments; + if (!result) { + return []; + } + + // Фильтр по дате + if (dateFilter !== 'all') { + const now = Date.now(); + const oneDay = 24 * 60 * 60 * 1000; + const sevenDays = 7 * oneDay; + const thirtyDays = 30 * oneDay; + + switch (dateFilter) { + case 'today': + const todayStart = new Date().setHours(0, 0, 0, 0); + result = result.filter(item => { + if (item.createdAt) { + return new Date(item.createdAt).getTime() >= todayStart + } else { + return 0 + } + }); + break; + case 'week': + const weekAgo = now - sevenDays; + result = result.filter(item => { + if (item.createdAt) { + return new Date(item.createdAt).getTime() >= weekAgo; + } + }); + break; + case 'month': + const monthAgo = now - thirtyDays; + result = result.filter(item => { + if (item.createdAt) { + return new Date(item.createdAt).getTime() >= monthAgo; + } + }); + break; + case 'older': + const monthAgo2 = now - thirtyDays; + result = result.filter(item => { + if (item.createdAt) { + return new Date(item.createdAt).getTime() < monthAgo2; + } + }); + break; + } + } + + return result + }, [userPaymentsData?.payments, dateFilter]) + + const table = useReactTable({ + data: filteredData, + columns, + state: { + sorting, + columnFilters, + pagination + }, + autoResetPageIndex: false, + onPaginationChange: setPagination, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + initialState: { + pagination: { + pageSize: 10, + }, + }, + }); + return ( -
- payment-tab-transaction-history +
+
+
+

+ История покупок +

+ + {/* Фильтры */} +
+
+
+
{t('date-filter')}:
+ { + switch (dateFilter) { + case 'all': + return t('all-dates') + case 'week': + return t('for-a-week') + case 'month': + return t('for-a-month') + case 'older': + return t('older-than-a-month') + default: + return t('today') + } + })()} + callBack={setDateFilter} + > +
  • + {t('all-dates')} +
  • +
  • + {t('today')} +
  • +
  • + {t('for-a-week')} +
  • +
  • + {t('for-a-month')} +
  • +
  • + {t('older-than-a-month')} +
  • +
    +
    + +
    + +
    +
    +
    {t('items-per-page')}:
    + + {[5, 10, 20, 50, 100].map(pageSize => ( +
  • + {t('show')} {pageSize} +
  • + ))} +
    +
    +
    + +
    + + {/* Таблица */} +
    + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + )) + ) : ( + + + + )} + +
    + {header.isPlaceholder + ? null + : typeof header.column.columnDef.header === 'function' + ? header.column.columnDef.header(header.getContext()) + : header.column.columnDef.header as string} +
    + {typeof cell.column.columnDef.cell === 'function' + ? cell.column.columnDef.cell(cell.getContext()) + : cell.getValue() as string} +
    + {t('no-data-for-selected-filters')} +
    +
    + + {/* Пагинация */} +
    +
    + + {t('page')}{' '} + + {table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1} + + + + | {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} + +
    + +
    + {table.getCanPreviousPage() && ( + + )} + {table.getCanPreviousPage() && ( + + )} + +
    + {Array.from({ length: Math.min(5, table.getPageCount()) }, (_, i) => { + const pageIndex = Math.max( + 0, + Math.min( + table.getPageCount() - 5, + table.getState().pagination.pageIndex - 2 + ) + ) + i; + + if (pageIndex < table.getPageCount()) { + return ( + + ); + } + return null; + })} +
    + + {table.getCanNextPage() && ( + + )} + {table.getCanNextPage() && ( + + )} +
    +
    +
    +
    ) } \ No newline at end of file diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index cd6e37d..d67839f 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -234,7 +234,11 @@ "without-monitoring" : "Without monitoring", "every-day": "Every day", "every-week": "Every week", - "every-month": "Every day" + "every-month": "Every day", + "amount": "Amount", + "date": "Date", + "operation-type": "Operation type", + "succeeded": "Succeeded" }, "Login-register-form": { "and": "and", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index e3cc5a4..311a094 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -234,7 +234,11 @@ "without-monitoring": "Без мониторинга", "every-day": "Каждый день", "every-week": "Каждую неделю", - "every-month": "Каждый месяц" + "every-month": "Каждый месяц", + "amount": "Сумма", + "date": "Дата", + "operation-type": "Тип операции", + "succeeded": "Успешно" }, "Login-register-form": { "and": "и",