Compare commits
3 Commits
bf134a99d5
...
88f9fdd25c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88f9fdd25c | ||
|
|
3321395b7f | ||
|
|
20eef84823 |
@ -31,6 +31,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cookies-next": "^5.1.0",
|
"cookies-next": "^5.1.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"date-fns-tz": "^3.2.0",
|
||||||
"embla-carousel-autoplay": "^8.6.0",
|
"embla-carousel-autoplay": "^8.6.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"json-to-graphql-query": "^2.3.0",
|
"json-to-graphql-query": "^2.3.0",
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@ -71,6 +71,9 @@ importers:
|
|||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
|
date-fns-tz:
|
||||||
|
specifier: ^3.2.0
|
||||||
|
version: 3.2.0(date-fns@4.1.0)
|
||||||
embla-carousel-autoplay:
|
embla-carousel-autoplay:
|
||||||
specifier: ^8.6.0
|
specifier: ^8.6.0
|
||||||
version: 8.6.0(embla-carousel@8.6.0)
|
version: 8.6.0(embla-carousel@8.6.0)
|
||||||
@ -1570,6 +1573,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
date-fns-tz@3.2.0:
|
||||||
|
resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==}
|
||||||
|
peerDependencies:
|
||||||
|
date-fns: ^3.0.0 || ^4.0.0
|
||||||
|
|
||||||
date-fns@4.1.0:
|
date-fns@4.1.0:
|
||||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||||
|
|
||||||
@ -4228,6 +4236,10 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
is-data-view: 1.0.2
|
is-data-view: 1.0.2
|
||||||
|
|
||||||
|
date-fns-tz@3.2.0(date-fns@4.1.0):
|
||||||
|
dependencies:
|
||||||
|
date-fns: 4.1.0
|
||||||
|
|
||||||
date-fns@4.1.0: {}
|
date-fns@4.1.0: {}
|
||||||
|
|
||||||
debug@3.2.7:
|
debug@3.2.7:
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { deleteCookie } from 'cookies-next';
|
import { deleteCookie } from 'cookies-next';
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { Building2, LogOut, Wallet } from 'lucide-react';
|
import { Building2, LogOut, Wallet } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@ -12,6 +11,7 @@ import {
|
|||||||
import { CorporateTransactionRequest } from '@/entities/corporate/model/types/corporate-transactions.type';
|
import { CorporateTransactionRequest } from '@/entities/corporate/model/types/corporate-transactions.type';
|
||||||
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
import { formatDate } from '@/shared/lib/format-date';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -195,7 +195,7 @@ export function CorporateDashboard() {
|
|||||||
renderRow={(transaction, index) => (
|
renderRow={(transaction, index) => (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{format(
|
{formatDate(
|
||||||
new Date(transaction.date_create),
|
new Date(transaction.date_create),
|
||||||
'dd.MM.yyyy HH:mm',
|
'dd.MM.yyyy HH:mm',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { deleteCookie } from 'cookies-next';
|
import { deleteCookie } from 'cookies-next';
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react';
|
import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@ -13,6 +12,7 @@ import { BonusTransactionRequest } from '@/entities/bonus/model/types/bonus-tran
|
|||||||
|
|
||||||
import Loader from '@/shared/components/loader';
|
import Loader from '@/shared/components/loader';
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
import { formatDate } from '@/shared/lib/format-date';
|
||||||
import { Button } from '@/shared/shadcn-ui/button';
|
import { Button } from '@/shared/shadcn-ui/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -190,10 +190,7 @@ export function CustomerDashboard() {
|
|||||||
renderRow={(transaction) => (
|
renderRow={(transaction) => (
|
||||||
<TableRow key={transaction.id}>
|
<TableRow key={transaction.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{format(
|
{formatDate(transaction.date_create, 'dd.MM.yyyy HH:mm')}
|
||||||
new Date(transaction.date_create),
|
|
||||||
'dd.MM.yyyy HH:mm',
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{transaction.station}</TableCell>
|
<TableCell>{transaction.station}</TableCell>
|
||||||
<TableCell>{transaction.product_name}</TableCell>
|
<TableCell>{transaction.product_name}</TableCell>
|
||||||
|
|||||||
@ -156,7 +156,7 @@ export default function LoginPage() {
|
|||||||
<p>
|
<p>
|
||||||
{t('auth.loginIssues')}{' '}
|
{t('auth.loginIssues')}{' '}
|
||||||
<Link
|
<Link
|
||||||
href='mailto:info@oriyo.tj'
|
href={`mailto:${t('auth.loginForm.contactUs.mail')}`}
|
||||||
className='text-red-600 hover:underline'
|
className='text-red-600 hover:underline'
|
||||||
>
|
>
|
||||||
{t('auth.contactLink')}
|
{t('auth.contactLink')}
|
||||||
|
|||||||
9
src/shared/lib/format-date.ts
Normal file
9
src/shared/lib/format-date.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { formatInTimeZone } from 'date-fns-tz';
|
||||||
|
|
||||||
|
export const formatDate = (
|
||||||
|
date: Date | string,
|
||||||
|
formatStr: string = 'dd.MM.yyyy HH:mm',
|
||||||
|
) => {
|
||||||
|
const utcDate = new Date(date);
|
||||||
|
return formatInTimeZone(utcDate, 'UTC', formatStr);
|
||||||
|
};
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
1
src/widgets/transactions-table/index.ts
Normal file
1
src/widgets/transactions-table/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { TransactionsTable } from "./ui/transactions-table";
|
||||||
121
src/widgets/transactions-table/ui/table-pagination.tsx
Normal file
121
src/widgets/transactions-table/ui/table-pagination.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
102
src/widgets/transactions-table/ui/transactions-table-header.tsx
Normal file
102
src/widgets/transactions-table/ui/transactions-table-header.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
123
src/widgets/transactions-table/ui/transactions-table.tsx
Normal file
123
src/widgets/transactions-table/ui/transactions-table.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Briefcase } from 'lucide-react';
|
import { Briefcase } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Jobs } from '@/app/api-utlities/@types/index';
|
import { Jobs } from '@/app/api-utlities/@types/index';
|
||||||
|
|
||||||
@ -16,7 +17,6 @@ import {
|
|||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from '@/shared/shadcn-ui/tabs';
|
} from '@/shared/shadcn-ui/tabs';
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
interface VacanciesSectionProps {
|
interface VacanciesSectionProps {
|
||||||
jobs: Jobs;
|
jobs: Jobs;
|
||||||
@ -131,7 +131,7 @@ const Vacancy = ({ jobTitle, location, tags }: VacancyProps) => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link href='mailto:info@oriyo.tj'>
|
<Link href={`mailto:${t('home.vacancies.vacancy.applyToMail')}`}>
|
||||||
<Button variant='outline' size='sm'>
|
<Button variant='outline' size='sm'>
|
||||||
{t('common.buttons.apply')}
|
{t('common.buttons.apply')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user