refactor: move transactions-table to a separate folder

This commit is contained in:
BunyodL 2025-05-21 21:52:51 +05:00
parent 20eef84823
commit 3321395b7f
5 changed files with 347 additions and 273 deletions

View File

@ -1,273 +0,0 @@
'use client';
import { format } from 'date-fns';
import { ru } from 'date-fns/locale';
import { CalendarIcon, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import TableLoadingOverlay from '@/shared/components/table-loading-overlay';
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button';
import { Calendar } from '@/shared/shadcn-ui/calendar';
import { Label } from '@/shared/shadcn-ui/label';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/shared/shadcn-ui/popover';
import {
Table,
TableBody,
TableCell,
TableHeader,
TableRow,
} from '@/shared/shadcn-ui/table';
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from './pagination';
export interface TransactionRequest {
start_date?: string;
end_date?: string;
page: number;
limit: number;
}
export interface TransactionsTableProps<T> {
data: {
transactions: T[];
total_pages: number;
total_records: number;
};
isLoading: boolean;
onChange: (request: TransactionRequest) => void;
renderHeaders: () => React.ReactNode;
renderRow: (transaction: T, index: number) => React.ReactNode;
itemsPerPageOptions?: number[];
}
export const TransactionsTable = <T,>({
data,
isLoading,
onChange,
renderHeaders,
renderRow,
itemsPerPageOptions = [5, 10, 20, 50],
}: TransactionsTableProps<T>) => {
const [startDate, setStartDate] = useState<Date | undefined>(
new Date(new Date().setMonth(new Date().getMonth() - 1)),
);
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(itemsPerPageOptions[0]);
const handlePageChange = (page: number) => {
if (page < 1 || page > data.total_pages) return;
setCurrentPage(page);
};
const getPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5; // Maximum number of visible pages
const halfVisible = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, currentPage - halfVisible);
let endPage = Math.min(data.total_pages, currentPage + halfVisible);
if (currentPage <= halfVisible) {
endPage = Math.min(data.total_pages, maxVisiblePages);
} else if (currentPage + halfVisible >= data.total_pages) {
startPage = Math.max(1, data.total_pages - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
};
const filterTransactions = () => {
if (!startDate || !endDate) return;
setCurrentPage(1); // Reset to the first page when applying filters
};
const { t } = useTextController();
useEffect(() => {
onChange({
limit: itemsPerPage,
page: currentPage,
...(startDate ? { start_date: format(startDate, 'yyyy-MM-dd') } : {}),
...(endDate ? { end_date: format(endDate, 'yyyy-MM-dd') } : {}),
});
}, [startDate, endDate, itemsPerPage, currentPage]);
if (!data) return null;
return (
<div className='relative space-y-6'>
<TableLoadingOverlay isLoading={isLoading} />
<div className='flex flex-col items-start justify-between gap-4 md:flex-row md:items-center'>
<h2 className='text-2xl font-bold'>
{t('corporate.transactions.title')}
</h2>
<div className='flex w-full flex-col gap-4 md:w-auto md:flex-row'>
<div className='grid gap-2 sm:grid-cols-2'>
<div className='flex items-center gap-2'>
<Label htmlFor='start-date'>
{t('corporate.transactions.dateFrom')}
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant='outline'
className='w-full justify-start text-left font-normal'
>
<CalendarIcon className='mr-2 h-4 w-4' />
{startDate
? format(startDate, 'PP', { locale: ru })
: 'Выберите дату'}
</Button>
</PopoverTrigger>
<PopoverContent className='w-auto p-0'>
<Calendar
mode='single'
selected={startDate}
onSelect={setStartDate}
initialFocus
/>
</PopoverContent>
<Button variant='ghost' onClick={() => setStartDate(undefined)}>
<X className='mr-2 h-4 w-4' />
</Button>
</Popover>
</div>
<div className='flex items-center gap-2'>
<Label htmlFor='end-date'>
{t('corporate.transactions.dateTo')}
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant='outline'
className='w-full justify-start text-left font-normal'
>
<CalendarIcon className='mr-2 h-4 w-4' />
{endDate
? format(endDate, 'PP', { locale: ru })
: t('corporate.transactions.selectDate')}
</Button>
</PopoverTrigger>
<PopoverContent className='w-auto p-0'>
<Calendar
mode='single'
selected={endDate}
onSelect={setEndDate}
initialFocus
/>
</PopoverContent>
<Button variant='ghost' onClick={() => setEndDate(undefined)}>
<X className='mr-2 h-4 w-4' />
</Button>
</Popover>
</div>
</div>
</div>
</div>
<div className='relative rounded-md border'>
<Table>
<TableHeader>{renderHeaders()}</TableHeader>
<TableBody>
{data.transactions.length > 0 ? (
data.transactions.map((transaction, index) =>
renderRow(transaction, index),
)
) : (
<TableRow>
<TableCell
colSpan={6}
className='py-6 text-center text-gray-500'
>
{t('corporate.transactions.no-data')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{data.transactions.length > 0 && (
<div className='flex flex-col items-center justify-between gap-4 sm:flex-row'>
<div className='text-sm text-gray-500'>
Показано {data.transactions.length} из {data.total_records} операций
</div>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => handlePageChange(currentPage - 1)}
className={
currentPage === 1 ? 'pointer-events-none opacity-50' : ''
}
/>
</PaginationItem>
{getPageNumbers().map((page, index) => (
<PaginationItem key={index}>
<PaginationLink
isActive={currentPage === page}
onClick={() => handlePageChange(page as number)}
>
{page}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
onClick={() => handlePageChange(currentPage + 1)}
className={
currentPage === data.total_pages
? 'pointer-events-none opacity-50'
: ''
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
<div className='flex items-center gap-2'>
<span className='text-sm text-gray-500'>
{t('transactions.entries')}
</span>
<select
className='rounded border p-1 text-sm'
value={itemsPerPage}
onChange={(e) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1); // Reset to first page when changing items per page
}}
>
{itemsPerPageOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
</div>
)}
</div>
);
};

View File

@ -0,0 +1 @@
export { TransactionsTable } from "./ui/transactions-table";

View File

@ -0,0 +1,121 @@
import { ChangeEvent } from 'react';
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from '../../pagination';
interface TablePaginationProps {
currentPage: number;
itemsPerPage: number;
totalPages: number;
totalOperations?: number;
transactionsQuantity: number;
itemsPerPageOptions: number[];
onPageChange: (page: number) => void;
onItemsPerPageChange: (e: ChangeEvent<HTMLSelectElement>) => void;
}
export const TransactionsTablePagination = ({
currentPage,
itemsPerPage,
totalPages,
totalOperations = 0,
itemsPerPageOptions,
transactionsQuantity,
onPageChange,
onItemsPerPageChange,
}: TablePaginationProps) => {
const { t } = useTextController();
const getPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5; // Maximum number of visible pages
const halfVisible = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, currentPage - halfVisible);
let endPage = Math.min(totalPages, currentPage + halfVisible);
if (currentPage <= halfVisible) {
endPage = Math.min(totalPages, maxVisiblePages);
} else if (currentPage + halfVisible >= totalPages) {
startPage = Math.max(1, totalPages - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
};
return (
<>
{transactionsQuantity > 0 && (
<div className='flex flex-col items-center justify-between gap-4 sm:flex-row'>
<div className='text-sm text-gray-500'>
Показано {transactionsQuantity} из {totalOperations} операций
</div>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => onPageChange(currentPage - 1)}
className={
currentPage === 1 ? 'pointer-events-none opacity-50' : ''
}
/>
</PaginationItem>
{getPageNumbers().map((page, index) => (
<PaginationItem key={index}>
<PaginationLink
isActive={currentPage === page}
onClick={() => onPageChange(page as number)}
>
{page}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
onClick={() => onPageChange(currentPage + 1)}
className={
currentPage === totalPages
? 'pointer-events-none opacity-50'
: ''
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
<div className='flex items-center gap-2'>
<span className='text-sm text-gray-500'>
{t('transactions.entries')}
</span>
<select
className='rounded border p-1 text-sm'
value={itemsPerPage}
onChange={onItemsPerPageChange}
>
{itemsPerPageOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
</div>
)}
</>
);
};

View File

@ -0,0 +1,102 @@
import { format } from 'date-fns';
import { ru } from 'date-fns/locale';
import { CalendarIcon, X } from 'lucide-react';
import { Dispatch, SetStateAction } from 'react';
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button';
import { Calendar } from '@/shared/shadcn-ui/calendar';
import { Label } from '@/shared/shadcn-ui/label';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/shared/shadcn-ui/popover';
interface TransactionsTableHeaderProps {
startDate: Date | undefined;
setStartDate: Dispatch<SetStateAction<Date | undefined>>;
endDate: Date | undefined;
setEndDate: Dispatch<SetStateAction<Date | undefined>>;
}
export const TransactionsTableHeader = ({
startDate,
setStartDate,
endDate,
setEndDate,
}: TransactionsTableHeaderProps) => {
const { t } = useTextController();
return (
<div className='flex flex-col items-start justify-between gap-4 md:flex-row md:items-center'>
<h2 className='text-2xl font-bold'>
{t('corporate.transactions.title')}
</h2>
<div className='flex w-full flex-col gap-4 md:w-auto md:flex-row'>
<div className='grid gap-2 sm:grid-cols-2'>
<div className='flex items-center gap-2'>
<Label htmlFor='start-date'>
{t('corporate.transactions.dateFrom')}
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant='outline'
className='w-full justify-start text-left font-normal'
>
<CalendarIcon className='mr-2 h-4 w-4' />
{startDate
? format(startDate, 'PP', { locale: ru })
: 'Выберите дату'}
</Button>
</PopoverTrigger>
<PopoverContent className='w-auto p-0'>
<Calendar
mode='single'
selected={startDate}
onSelect={setStartDate}
initialFocus
/>
</PopoverContent>
<Button variant='ghost' onClick={() => setStartDate(undefined)}>
<X className='mr-2 h-4 w-4' />
</Button>
</Popover>
</div>
<div className='flex items-center gap-2'>
<Label htmlFor='end-date'>
{t('corporate.transactions.dateTo')}
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant='outline'
className='w-full justify-start text-left font-normal'
>
<CalendarIcon className='mr-2 h-4 w-4' />
{endDate
? format(endDate, 'PP', { locale: ru })
: t('corporate.transactions.selectDate')}
</Button>
</PopoverTrigger>
<PopoverContent className='w-auto p-0'>
<Calendar
mode='single'
selected={endDate}
onSelect={setEndDate}
initialFocus
/>
</PopoverContent>
<Button variant='ghost' onClick={() => setEndDate(undefined)}>
<X className='mr-2 h-4 w-4' />
</Button>
</Popover>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,123 @@
import { format } from 'date-fns';
import { ChangeEvent, useEffect, useState } from 'react';
import TableLoadingOverlay from '@/shared/components/table-loading-overlay';
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import {
Table,
TableBody,
TableCell,
TableHeader,
TableRow,
} from '@/shared/shadcn-ui/table';
import { TransactionsTablePagination } from './table-pagination';
import { TransactionsTableHeader } from './transactions-table-header';
export interface TransactionRequest {
start_date?: string;
end_date?: string;
page: number;
limit: number;
}
export interface TransactionsTableProps<T> {
data: {
transactions: T[];
total_pages: number;
total_records: number;
};
isLoading: boolean;
onChange: (request: TransactionRequest) => void;
renderHeaders: () => React.ReactNode;
renderRow: (transaction: T, index: number) => React.ReactNode;
itemsPerPageOptions?: number[];
}
export const TransactionsTable = <T,>({
data,
isLoading,
onChange,
renderHeaders,
renderRow,
itemsPerPageOptions = [5, 10, 20, 50],
}: TransactionsTableProps<T>) => {
const [startDate, setStartDate] = useState<Date | undefined>(
new Date(new Date().setMonth(new Date().getMonth() - 1)),
);
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(itemsPerPageOptions[0]);
const handlePageChange = (page: number) => {
if (page < 1 || page > data.total_pages) return;
setCurrentPage(page);
};
const handleItemsPerPageChange = (e: ChangeEvent<HTMLSelectElement>) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1); // Reset to first page when changing items per page
};
const filterTransactions = () => {
if (!startDate || !endDate) return;
setCurrentPage(1); // Reset to the first page when applying filters
};
const { t } = useTextController();
useEffect(() => {
onChange({
limit: itemsPerPage,
page: currentPage,
...(startDate ? { start_date: format(startDate, 'yyyy-MM-dd') } : {}),
...(endDate ? { end_date: format(endDate, 'yyyy-MM-dd') } : {}),
});
}, [startDate, endDate, itemsPerPage, currentPage]);
if (!data) return null;
return (
<div className='relative space-y-6'>
<TableLoadingOverlay isLoading={isLoading} />
<TransactionsTableHeader
startDate={startDate}
setStartDate={setStartDate}
endDate={endDate}
setEndDate={setEndDate}
/>
<div className='relative rounded-md border'>
<Table>
<TableHeader>{renderHeaders()}</TableHeader>
<TableBody>
{data.transactions.length > 0 ? (
data.transactions.map((transaction, index) =>
renderRow(transaction, index),
)
) : (
<TableRow>
<TableCell
colSpan={6}
className='py-6 text-center text-gray-500'
>
{t('corporate.transactions.no-data')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<TransactionsTablePagination
currentPage={currentPage}
itemsPerPage={itemsPerPage}
itemsPerPageOptions={itemsPerPageOptions}
totalPages={data.total_pages}
transactionsQuantity={data.transactions.length}
totalOperations={data.total_records}
onPageChange={handlePageChange}
onItemsPerPageChange={handleItemsPerPageChange}
/>
</div>
);
};