Compare commits

...

3 Commits

Author SHA1 Message Date
BunyodL
88f9fdd25c fix: format-date without localizing 2025-05-21 22:06:33 +05:00
BunyodL
3321395b7f refactor: move transactions-table to a separate folder 2025-05-21 21:52:51 +05:00
BunyodL
20eef84823 fix: make mails dynamic 2025-05-21 20:59:02 +05:00
12 changed files with 376 additions and 283 deletions

View File

@ -31,6 +31,7 @@
"clsx": "^2.1.1",
"cookies-next": "^5.1.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"json-to-graphql-query": "^2.3.0",

12
pnpm-lock.yaml generated
View File

@ -71,6 +71,9 @@ importers:
date-fns:
specifier: ^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:
specifier: ^8.6.0
version: 8.6.0(embla-carousel@8.6.0)
@ -1570,6 +1573,11 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
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:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
@ -4228,6 +4236,10 @@ snapshots:
es-errors: 1.3.0
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: {}
debug@3.2.7:

View File

@ -1,7 +1,6 @@
'use client';
import { deleteCookie } from 'cookies-next';
import { format } from 'date-fns';
import { Building2, LogOut, Wallet } from 'lucide-react';
import { useEffect, useState } from 'react';
@ -12,6 +11,7 @@ import {
import { CorporateTransactionRequest } from '@/entities/corporate/model/types/corporate-transactions.type';
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { formatDate } from '@/shared/lib/format-date';
import { Button } from '@/shared/shadcn-ui/button';
import {
Card,
@ -195,7 +195,7 @@ export function CorporateDashboard() {
renderRow={(transaction, index) => (
<TableRow key={index}>
<TableCell>
{format(
{formatDate(
new Date(transaction.date_create),
'dd.MM.yyyy HH:mm',
)}

View File

@ -1,7 +1,6 @@
'use client';
import { deleteCookie } from 'cookies-next';
import { format } from 'date-fns';
import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-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 { useTextController } from '@/shared/language/hooks/use-text-controller';
import { formatDate } from '@/shared/lib/format-date';
import { Button } from '@/shared/shadcn-ui/button';
import {
Card,
@ -190,10 +190,7 @@ export function CustomerDashboard() {
renderRow={(transaction) => (
<TableRow key={transaction.id}>
<TableCell>
{format(
new Date(transaction.date_create),
'dd.MM.yyyy HH:mm',
)}
{formatDate(transaction.date_create, 'dd.MM.yyyy HH:mm')}
</TableCell>
<TableCell>{transaction.station}</TableCell>
<TableCell>{transaction.product_name}</TableCell>

View File

@ -156,7 +156,7 @@ export default function LoginPage() {
<p>
{t('auth.loginIssues')}{' '}
<Link
href='mailto:info@oriyo.tj'
href={`mailto:${t('auth.loginForm.contactUs.mail')}`}
className='text-red-600 hover:underline'
>
{t('auth.contactLink')}

View 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);
};

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>
);
};

View File

@ -1,6 +1,7 @@
'use client';
import { Briefcase } from 'lucide-react';
import Link from 'next/link';
import { Jobs } from '@/app/api-utlities/@types/index';
@ -16,7 +17,6 @@ import {
TabsList,
TabsTrigger,
} from '@/shared/shadcn-ui/tabs';
import Link from 'next/link';
interface VacanciesSectionProps {
jobs: Jobs;
@ -131,7 +131,7 @@ const Vacancy = ({ jobTitle, location, tags }: VacancyProps) => {
})}
</div>
</div>
<Link href='mailto:info@oriyo.tj'>
<Link href={`mailto:${t('home.vacancies.vacancy.applyToMail')}`}>
<Button variant='outline' size='sm'>
{t('common.buttons.apply')}
</Button>