From a7fce902c3ec337848bb15befca7c5d88d917943 Mon Sep 17 00:00:00 2001 From: BunyodL Date: Sun, 27 Apr 2025 01:43:05 +0500 Subject: [PATCH] feat: make customer-dashboard page --- package.json | 3 + pnpm-lock.yaml | 61 +++++ .../(dashboard)/corporate-dashboard/page.tsx | 0 .../(dashboard)/customer-dashboard/page.tsx | 110 +++++++++ src/shared/shadcn-ui/calendar.tsx | 70 ++++++ src/shared/shadcn-ui/popover.tsx | 30 +++ src/shared/shadcn-ui/table.tsx | 117 ++++++++++ src/widgets/transactions-table.tsx | 216 ++++++++++++++++++ 8 files changed, 607 insertions(+) create mode 100644 src/app/(dashboard)/corporate-dashboard/page.tsx create mode 100644 src/app/(dashboard)/customer-dashboard/page.tsx create mode 100644 src/shared/shadcn-ui/calendar.tsx create mode 100644 src/shared/shadcn-ui/popover.tsx create mode 100644 src/shared/shadcn-ui/table.tsx create mode 100644 src/widgets/transactions-table.tsx diff --git a/package.json b/package.json index 6ffdbf6..75dd634 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.11", "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-navigation-menu": "^1.2.10", + "@radix-ui/react-popover": "^1.1.11", "@radix-ui/react-select": "^2.2.2", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.8", @@ -24,12 +25,14 @@ "aos": "^2.3.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", "lucide-react": "^0.501.0", "next": "15.3.1", "next-themes": "^0.4.6", "react": "^19.0.0", + "react-day-picker": "8.10.1", "react-dom": "^19.0.0", "react-hook-form": "^7.56.1", "react-redux": "^9.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f6c3d0..430bf8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@radix-ui/react-navigation-menu': specifier: ^1.2.10 version: 1.2.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-popover': + specifier: ^1.1.11 + version: 1.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-select': specifier: ^2.2.2 version: 2.2.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -50,6 +53,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 embla-carousel-autoplay: specifier: ^8.6.0 version: 8.6.0(embla-carousel@8.6.0) @@ -68,6 +74,9 @@ importers: react: specifier: ^19.0.0 version: 19.1.0 + react-day-picker: + specifier: 8.10.1 + version: 8.10.1(date-fns@4.1.0)(react@19.1.0) react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) @@ -681,6 +690,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popover@1.1.11': + resolution: {integrity: sha512-yFMfZkVA5G3GJnBgb2PxrrcLKm1ZLWXrbYVgdyTl//0TYEIHS9LJbnyz7WWcZ0qCq7hIlJZpRtxeSeIG5T5oJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popper@1.2.4': resolution: {integrity: sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==} peerDependencies: @@ -1370,6 +1392,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2261,6 +2286,12 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-day-picker@8.10.1: + resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: @@ -3121,6 +3152,29 @@ snapshots: '@types/react': 19.1.2 '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-popover@1.1.11(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-popper': 1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.0(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.0(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0) + aria-hidden: 1.2.4 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.6.3(@types/react@19.1.2)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + '@radix-ui/react-popper@1.2.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -3814,6 +3868,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + date-fns@4.1.0: {} + debug@3.2.7: dependencies: ms: 2.1.3 @@ -4782,6 +4838,11 @@ snapshots: queue-microtask@1.2.3: {} + react-day-picker@8.10.1(date-fns@4.1.0)(react@19.1.0): + dependencies: + date-fns: 4.1.0 + react: 19.1.0 + react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0 diff --git a/src/app/(dashboard)/corporate-dashboard/page.tsx b/src/app/(dashboard)/corporate-dashboard/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/(dashboard)/customer-dashboard/page.tsx b/src/app/(dashboard)/customer-dashboard/page.tsx new file mode 100644 index 0000000..aa2f7ea --- /dev/null +++ b/src/app/(dashboard)/customer-dashboard/page.tsx @@ -0,0 +1,110 @@ +import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react'; + +import { Button } from '@/shared/shadcn-ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/shared/shadcn-ui/card'; + +import { TransactionsTable } from '@/widgets/transactions-table'; + +// Sample customer data +const customerData = { + firstName: 'Алишер', + lastName: 'Рахмонов', + passportNumber: 'A12345678', + bonusPoints: 1250, + cardNumber: '5678-9012-3456-7890', + expiryDate: '12/2025', + registrationDate: '15.06.2020', +}; + +export default function CustomerDashboard() { + return ( +
+
+
+
+

Личный кабинет

+ +
+ +
+ {/* Bonus Card */} + + + + + Бонусная карта + + + Ваши накопленные бонусы + + + +
+

+ {customerData.bonusPoints} +

+

бонусных баллов

+
+
+
+ + Действует до: 31.12.2023 +
+ +
+
+
+ {/* Customer Card */} + + + + + Информация о клиенте + + + +
+
+
+

ФИО

+

+ {customerData.firstName} {customerData.lastName} +

+
+
+

Дата регистрации

+

+ {customerData.registrationDate} +

+
+
+
+
+

Номер карты

+

{customerData.cardNumber}

+
+
+

Срок действия

+

{customerData.expiryDate}

+
+
+
+
+
+
+ + +
+
+
+ ); +} diff --git a/src/shared/shadcn-ui/calendar.tsx b/src/shared/shadcn-ui/calendar.tsx new file mode 100644 index 0000000..2f4b84e --- /dev/null +++ b/src/shared/shadcn-ui/calendar.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import * as React from 'react'; +import { DayPicker } from 'react-day-picker'; + +import { cn } from '@/shared/lib/utils'; +import { buttonVariants } from '@/shared/shadcn-ui/button'; + +export type CalendarProps = React.ComponentProps; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + ( + + ), + IconRight: ({ className, ...props }) => ( + + ), + }} + {...props} + /> + ); +} +Calendar.displayName = 'Calendar'; + +export { Calendar }; diff --git a/src/shared/shadcn-ui/popover.tsx b/src/shared/shadcn-ui/popover.tsx new file mode 100644 index 0000000..7fdf57d --- /dev/null +++ b/src/shared/shadcn-ui/popover.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { cn } from '@/shared/lib/utils'; +import * as PopoverPrimitive from '@radix-ui/react-popover'; +import * as React from 'react'; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/src/shared/shadcn-ui/table.tsx b/src/shared/shadcn-ui/table.tsx new file mode 100644 index 0000000..d9998b3 --- /dev/null +++ b/src/shared/shadcn-ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; + +import { cn } from '@/shared/lib/utils'; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)); +Table.displayName = 'Table'; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = 'TableHeader'; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = 'TableBody'; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0', + className, + )} + {...props} + /> +)); +TableFooter.displayName = 'TableFooter'; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = 'TableRow'; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableHead.displayName = 'TableHead'; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableCell.displayName = 'TableCell'; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = 'TableCaption'; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/src/widgets/transactions-table.tsx b/src/widgets/transactions-table.tsx new file mode 100644 index 0000000..8cee4b8 --- /dev/null +++ b/src/widgets/transactions-table.tsx @@ -0,0 +1,216 @@ +'use client'; + +import { format, subMonths } from 'date-fns'; +import { ru } from 'date-fns/locale'; +import { CalendarIcon } from 'lucide-react'; +import { useState } from 'react'; + +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, + TableHead, + TableHeader, + TableRow, +} from '@/shared/shadcn-ui/table'; + +// Sample customer data +const customerData = { + firstName: 'Алишер', + lastName: 'Рахмонов', + passportNumber: 'A12345678', + bonusPoints: 1250, + cardNumber: '5678-9012-3456-7890', + expiryDate: '12/2025', + registrationDate: '15.06.2020', +}; + +// Sample transaction data +const generateTransactions = () => { + const stations = [ + 'АЗС Душанбе-Центр', + 'АЗС Душанбе-Запад', + 'АЗС Душанбе-Восток', + 'АЗС Худжанд', + 'АЗС Куляб', + ]; + + const products = [ + { name: 'ДТ', price: 8.5 }, + { name: 'АИ-92', price: 9.2 }, + { name: 'АИ-95', price: 10.5 }, + { name: 'Z-100 Power', price: 11.8 }, + { name: 'Пропан', price: 6.3 }, + ]; + + const transactions = []; + + // Generate 50 random transactions over the last 6 months + for (let i = 0; i < 50; i++) { + const date = subMonths(new Date(), Math.random() * 6); + const station = stations[Math.floor(Math.random() * stations.length)]; + const product = products[Math.floor(Math.random() * products.length)]; + const quantity = Math.floor(Math.random() * 40) + 10; // 10-50 liters + const cost = product.price; + const total = quantity * cost; + + transactions.push({ + id: i + 1, + date, + station, + product: product.name, + quantity, + cost, + total, + }); + } + + // Sort by date (newest first) + return transactions.sort((a, b) => b.date.getTime() - a.date.getTime()); +}; + +const transactions = generateTransactions(); + +export const TransactionsTable = () => { + const [startDate, setStartDate] = useState( + subMonths(new Date(), 1), + ); + const [endDate, setEndDate] = useState(new Date()); + const [filteredTransactions, setFilteredTransactions] = + useState(transactions); + + // Filter transactions by date range + const filterTransactions = () => { + if (!startDate || !endDate) return; + + const filtered = transactions.filter((transaction) => { + const transactionDate = new Date(transaction.date); + return transactionDate >= startDate && transactionDate <= endDate; + }); + + setFilteredTransactions(filtered); + }; + + return ( +
+
+

История операций

+ +
+
+
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+
+ + +
+
+ +
+ + + + Дата + Станция + Продукт + Кол-во (л) + Стоимость + Сумма + + + + {filteredTransactions.length > 0 ? ( + filteredTransactions.map((transaction) => ( + + + {format(transaction.date, 'dd.MM.yyyy')} + + {transaction.station} + {transaction.product} + + {transaction.quantity} + + + {transaction.cost.toFixed(2)} сомони + + + {transaction.total.toFixed(2)} сомони + + + )) + ) : ( + + + Нет операций за выбранный период + + + )} + +
+
+
+ ); +};