diff --git a/package-lock.json b/package-lock.json index 5999405..b59f83f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,15 @@ "name": "peko-avto", "version": "0.1.0", "dependencies": { + "@heroicons/react": "^2.2.0", + "@tanstack/react-table": "^8.21.3", + "classnames": "^2.5.1", "next": "15.3.3", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-hook-form": "^7.57.0", + "tailwind-merge": "^3.3.0", + "yup": "^1.6.1" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -210,6 +216,14 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1182,6 +1196,37 @@ "tailwindcss": "4.1.8" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", @@ -2139,6 +2184,11 @@ "node": ">=18" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -4703,6 +4753,11 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4751,6 +4806,21 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz", + "integrity": "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5337,6 +5407,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz", + "integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz", @@ -5369,6 +5448,11 @@ "node": ">=18" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -5409,6 +5493,11 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -5450,6 +5539,17 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -5731,6 +5831,17 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz", + "integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } } } } diff --git a/package.json b/package.json index 7f7cb6f..e56fbd9 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,25 @@ "lint": "next lint" }, "dependencies": { + "@heroicons/react": "^2.2.0", + "@tanstack/react-table": "^8.21.3", + "classnames": "^2.5.1", + "next": "15.3.3", "react": "^19.0.0", "react-dom": "^19.0.0", - "next": "15.3.3" + "react-hook-form": "^7.57.0", + "tailwind-merge": "^3.3.0", + "yup": "^1.6.1" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.3.3", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/src/app/carrier/page.tsx b/src/app/carrier/page.tsx new file mode 100644 index 0000000..82e1e26 --- /dev/null +++ b/src/app/carrier/page.tsx @@ -0,0 +1,137 @@ +'use client' + +import { Button } from '@/components/Button'; +import { Input } from '@/components/Input'; +import { PageLayout } from '@/components/PageLayout'; +import { Table } from '@/components/Table'; +import { useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +interface Vehicle { + id: string; + name: string; + licensePlate: string; + driver: string; + arrivalDate: string; +} + +type LicensePlateProps = { + number: string; +}; + +const LicensePlate = ({ number }: LicensePlateProps) => { + const regionCode = number.substring(6); + return ( +
+
+ {number.substring(0, 6)} +
+
+ {regionCode} +
+ RUS +
+
+
+
+
+
+
+
+ ); +}; + +type AddCarFormProps = Omit; + +const ADD_CAR_INIT: AddCarFormProps = { + arrivalDate: "", + driver: "", + licensePlate: "", + name: "" +}; + +export default function CarrierDashboard() { + const { register, handleSubmit, watch, reset, formState: { errors } } = useForm({ defaultValues: ADD_CAR_INIT }); + const [isAddOpen, setIsAddOpen] = useState(false); + + const onSubmit: SubmitHandler = (form) => { + console.log('form', form); + }; + + const handleFormClose = () => { + reset(ADD_CAR_INIT); + setIsAddOpen(false); + }; + + const [vehicles, setVehicles] = useState([ + { + id: '1', + name: 'Volvo FH16', + licensePlate: 'А123БВ777', + driver: 'Иванов Иван Иванович', + arrivalDate: '2023-06-15', + }, + { + id: '2', + name: 'MAN TGX', + licensePlate: 'О321РТ777', + driver: 'Петров Петр Петрович', + arrivalDate: '2023-06-16', + }, + ]); + + return ( + +
+

Управление вашим автопарком

+
+ +
+ {isAddOpen && ( +
+

Добавить новую машину

+ +
+ + + + +
+ +
+ + +
+
+ )} + +
+
+

Ваши машины ({vehicles.length})

+ +
+ {vehicles.length === 0 ? ( +
+ У вас пока нет добавленных машин +
+ ) : ( +
+ info.getValue(), header: () => "Автомобиль" }, + { accessorKey: "licensePlate", cell: info => ()} />, header: () => "Гос. номер" }, + { accessorKey: "driver", cell: info => info.getValue(), header: () => "Водитель" }, + { accessorKey: "arrivalDate", cell: info => info.getValue(), header: () => "Дата прибытия" }, + { accessorKey: "delete", cell: info => , header: () => "Удалить" } + ]} + tableData={vehicles} + /> + + )} + + + + ); +}; diff --git a/src/app/contractors/create/page.tsx b/src/app/contractors/create/page.tsx new file mode 100644 index 0000000..b1ec9bf --- /dev/null +++ b/src/app/contractors/create/page.tsx @@ -0,0 +1,378 @@ +'use client' +import { Breadcrumbs, IBreadcrumbProps } from '@/components/Breadcrumbs'; +import { PageLayout } from '@/components/PageLayout'; +import { useState } from 'react'; + +const BREADCRUMBS: IBreadcrumbProps[] = [ + { + label: "Главная", + link: "/", + icon: ( + + + + ), + }, + { + label: "Контрагенты", + link: "/contractors", + icon: ( + + + + ), + }, + { + label: "Создание", + link: "/contractors/create", + icon: ( + + + + ) + } +]; + +export default function CreateContractor() { + const [formData, setFormData] = useState({ + name: '', + type: 'legal', // или 'individual' + inn: '', + kpp: '', + ogrn: '', + address: '', + phone: '', + email: '', + contactPerson: '', + bankDetails: { + account: '', + bank: '', + bik: '', + correspondentAccount: '' + } + }); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + + if (name.includes('.')) { + const [parent, child] = name.split('.'); + // setFormData(prev => ({ + // ...prev, + // [parent]: { + // ...prev[parent as keyof typeof formData], + // [child]: value + // } + // })); + } else { + setFormData(prev => ({ + ...prev, + [name]: value + })); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + + try { + // Здесь будет запрос к вашему API + const response = await fetch('/api/contractors', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }); + + if (!response.ok) { + throw new Error('Ошибка при создании контрагента'); + } + + const data = await response.json(); + // router.push(`/contractors/${data.id}`); + } catch (err) { + setError(err instanceof Error ? err.message : 'Неизвестная ошибка'); + } finally { + setIsLoading(false); + } + }; + + return ( + + +
+ {error &&
{error}
} + +
+
+ {/* Основная информация */} +
+

Основная информация

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + {formData.type === "legal" && ( +
+ + +
+ )} + +
+ + +
+
+ +
+

Контактная информация

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + {/* Банковские реквизиты */} +
+

Банковские реквизиты

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+ + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/app/contractors/page.tsx b/src/app/contractors/page.tsx new file mode 100644 index 0000000..e3d0ca0 --- /dev/null +++ b/src/app/contractors/page.tsx @@ -0,0 +1,243 @@ +'use client' +import { Breadcrumbs, IBreadcrumbProps } from '@/components/Breadcrumbs'; +import { Button } from '@/components/Button'; +import { PageLayout } from '@/components/PageLayout'; +import Link from 'next/link'; +import { useState } from 'react'; + +type Vehicle = { + id: number; + name: string; + licensePlate: string; + capacity: number; + selectedRoute: number | null; +}; + +type Counterparty = { + id: number; + name: string; + inn: string; + vehicles: Vehicle[]; + expanded: boolean; +}; + +type Route = { + id: number; + name: string; +}; + +const CounterpartiesPage = () => { + const [counterparties, setCounterparties] = useState([ + { + id: 1, + name: 'ООО "Транспортные решения"', + inn: '1234567890', + vehicles: [ + { + id: 101, + name: 'ГАЗель NEXT', + licensePlate: 'А123БВ777', + capacity: 1500, + selectedRoute: null, + }, + { + id: 102, + name: 'Volvo FH16', + licensePlate: 'О321РТ777', + capacity: 20000, + selectedRoute: null, + }, + ], + expanded: false, + }, + { + id: 2, + name: 'ИП Смирнов А.В.', + inn: '0987654321', + vehicles: [ + { + id: 201, + name: 'Hyundai HD78', + licensePlate: 'У456КХ777', + capacity: 3500, + selectedRoute: null, + }, + ], + expanded: false, + }, + ]); + + const routes: Route[] = [ + { id: 1, name: 'Москва - Санкт-Петербург' }, + { id: 2, name: 'Москва - Нижний Новгород' }, + { id: 3, name: 'Москва - Казань' }, + { id: 4, name: 'Москва - Ростов-на-Дону' }, + ]; + + const toggleCounterparty = (id: number) => { + setCounterparties(counterparties.map(cp => + cp.id === id ? { ...cp, expanded: !cp.expanded } : cp + )); + }; + + const handleRouteSelect = (counterpartyId: number, vehicleId: number, routeId: number | null) => { + setCounterparties(counterparties.map(cp => { + if (cp.id !== counterpartyId) return cp; + + return { + ...cp, + vehicles: cp.vehicles.map(v => { + if (v.id !== vehicleId) return v; + return { ...v, selectedRoute: routeId }; + }) + }; + })); + }; + + const BREADCRUMBS: IBreadcrumbProps[] = [ + { + label: "Главная", + link: "/", + icon: ( + + + + ), + }, + { + label: "Контрагенты", + link: "/contractors", + icon: ( + + + + ), + }, + ]; + + return ( + +
+ + + + +
+ +
+

+ Список контрагентов +

+
+ +
+ {counterparties.map((counterparty) => ( +
+
toggleCounterparty(counterparty.id)} + > +
+

+ {counterparty.name} +

+

ИНН: {counterparty.inn}

+
+
+ + {counterparty.vehicles.length} авто + + + + +
+
+ + {counterparty.expanded && ( +
+
+ {counterparty.vehicles.map((vehicle) => ( +
+ {/* Информация об автомобиле */} +
+
+

+ Модель +

+

+ {vehicle.name} +

+
+
+

+ Гос. номер +

+

+ {vehicle.licensePlate} +

+
+
+

+ Вместимость (кг) +

+

+ {vehicle.capacity.toLocaleString()} +

+
+
+ +
+ + +
+
+ ))} +
+
+ )} +
+ ))} +
+
+ ); +}; + +export default CounterpartiesPage; \ No newline at end of file diff --git a/src/app/driver-ticket/page.tsx b/src/app/driver-ticket/page.tsx new file mode 100644 index 0000000..a0e699c --- /dev/null +++ b/src/app/driver-ticket/page.tsx @@ -0,0 +1,42 @@ +import { PageLayout } from '@/components/PageLayout'; + +export default function DriverTicket({ driverName = "Иванов А.П.", gateNumber = "12", queueNumber = "5" }) { + return ( + +
+
+ ТАЛОН ВОДИТЕЛЯ +
+ +
+
+
+ Водитель: + {driverName} +
+
+ Gate #: + {gateNumber} +
+
+ + {/* Queue Number */} +
+
{queueNumber}
+
номер в очереди
+
+ + {/* Barcode */} +
+ A{Math.random().toString(36).substring(2, 10).toUpperCase()}B +
+
+ + {/* Footer */} +
+ Талон действителен в течение 2 часов +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index e68abe6..a303818 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,103 +1,249 @@ -import Image from "next/image"; +'use client'; +import { useState, useEffect } from 'react'; +import { BuildingOfficeIcon, ChevronDownIcon, ChevronUpIcon, ClockIcon, MapPinIcon } from '@heroicons/react/24/outline'; +import { XMarkIcon, CheckIcon, TruckIcon } from "@heroicons/react/24/outline"; +import { PageLayout } from '@/components/PageLayout'; + +type EventType = { + id: number; + type: string; + driver?: string; + action?: string; + name?: string; + gate?: number; + time?: string; + queuePos?: number; +}; export default function Home() { - return ( -
-
- -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+ const [activeGate, setActiveGate] = useState(null); + const [events, setEvents] = useState([]); + + // Загрузка данных (заглушка) + useEffect(() => { + const mockEvents: EventType[] = [ + { id: 1, type: 'ticket', driver: 'Иванов А.П.', gate: 12, time: '2 мин назад', queuePos: 5 }, + { id: 2, type: 'contractor', action: 'создан', name: 'ООО "Грузовик+"', time: '5 мин назад' }, + { id: 3, type: 'ticket', driver: 'Петров С.И.', gate: 8, time: '7 мин назад', queuePos: 2 } + ]; + setEvents(mockEvents); + }, []); -
- - - Deploy now - - - Read our docs - + const gates = [5, 8, 12, 15]; // Номера гейтов + + return ( + +
+ {/* Основная карта */} + +
+
+
+ {/* Гейты */} + {gates.map(gate => ( + setActiveGate(gate)} + /> + ))} + + {/* Грузовики */} + + + + {/* Легенда */} +
+
+ Свободен + Занят +
+
+
+
+ +
+
+

+ + Live-активность +

+ +
+ {events.map(event => ( + + ))} +
+ + {/* Статистика */} +
+

Статистика за день

+
+ } label="Талонов" value="24" /> + } label="Контрагентов" value="3" /> +
+
+
+
-
- +
+ + ); +} + +function NewTruckPanel() { + const [isExpanded, setIsExpanded] = useState(false); + + const toggleExpand = () => setIsExpanded(!isExpanded); + + return ( +
+ + + {/* Выдвигающаяся панель с подробной информацией */} +
+
+
+
+ +
+
+

Грузовик #12345

+

Статус: Прибыл на склад

+
+
+ +
+
+
Водитель:
+
Иванов Иван Иванович
+
+
+
Груз:
+
Бытовая техника (20 коробок)
+
+
+
Время прибытия:
+
12:30, 15 июня 2023
+
+
+
Место разгрузки:
+
Склад №3, дверь 2B
+
+
+
+
); } + +function Gate({ number, active, onClick }: { number: number, active: Boolean, onClick: () => void }) { + return ( + + ); +} + +function Truck({ x, y, driver, queuePos }: { x: string, y: string, driver: string, queuePos: number }) { + return ( +
+
+ +
+
+

{driver}

+

Очередь: {queuePos}

+
+
+ ); +} + +function EventCard({ event }: { event: any }) { + return ( +
+
+
+ {event.type === 'ticket' ? ( + + ) : ( + + )} +
+
+

+ {event.type === 'ticket' + ? `Талон для ${event.driver} (гейт ${event.gate})` + : `Добавлен ${event.name}`} +

+

+ + {event.time} +

+
+
+
+ ); +} + +function StatCard({ icon, label, value }: any) { + return ( +
+
+
+ {icon} +
+
+

{label}

+

{value}

+
+
+
+ ); +} + +// Хелперы +function getGatePosition(number: number) { + const positions = { + 5: "top-[10%] left-[20%]", + 8: "top-[10%] right-[20%]", + 12: "bottom-[20%] left-[30%]", + 15: "bottom-[20%] right-[30%]", + }; + // @ts-ignore + return positions[number] || ""; +} \ No newline at end of file diff --git a/src/components/Breadcrumbs/index.tsx b/src/components/Breadcrumbs/index.tsx new file mode 100644 index 0000000..c7bd45d --- /dev/null +++ b/src/components/Breadcrumbs/index.tsx @@ -0,0 +1,28 @@ +import Link from 'next/link'; + +export type IBreadcrumbProps = { + label: string, + link: string, + icon?: React.ReactNode +}; + +type IBreadcrumbsProps = { + breadcrumbs: IBreadcrumbProps[]; +}; + +export const Breadcrumbs = ({ breadcrumbs }: IBreadcrumbsProps) => { + return ( + + ); +}; diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx new file mode 100644 index 0000000..177bd01 --- /dev/null +++ b/src/components/Button/index.tsx @@ -0,0 +1,10 @@ +import { ButtonHTMLAttributes } from 'react'; + +type IButtonProps = { + children: React.ReactNode; +} & ButtonHTMLAttributes; + +export const Button = ({ children, ...props }: IButtonProps) => { + const buttonClsx = "cursor-pointer px-4 py-2 bg-gray-800 hover:bg-gray-700 text-white font-medium rounded-md border border-gray-600 shadow-sm transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 disabled:opacity-50 disabled:cursor-not-allowed" + return ; +}; diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx new file mode 100644 index 0000000..ae4d5e5 --- /dev/null +++ b/src/components/Input/index.tsx @@ -0,0 +1,31 @@ +import classNames from 'classnames'; +import { InputHTMLAttributes } from 'react'; +import { twMerge } from 'tailwind-merge'; + +type IInputProps = { + label?: string; + isError?: Boolean; + errorText?: string; +} & InputHTMLAttributes; + +export const Input = ({ label, isError, errorText, className, id, ...props }: IInputProps) => { + const inputClsx = twMerge( + classNames("w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-1", + { + "border-red-300 text-red-900 placeholder-red-300 focus:ring-red-500 focus:border-red-500": isError, + "border-gray-300 focus:ring-blue-500 focus:border-blue-500": !isError, + } + ), + className + ); + + return ( +
+ + + {isError && errorText &&

{errorText}

} +
+ ); +}; diff --git a/src/components/PageLayout/index.tsx b/src/components/PageLayout/index.tsx new file mode 100644 index 0000000..1fa46f8 --- /dev/null +++ b/src/components/PageLayout/index.tsx @@ -0,0 +1,76 @@ +import Head from 'next/head'; +import { Sidebar } from '../Sidebar'; + +type PageLayoutProps = { + children: React.ReactNode; + pageName?: string; +} + +export const PageLayout = ({ children, pageName }: PageLayoutProps) => { + return ( +
+ + {pageName} + + +
+
+

PEKO-Авто

+ ⚙️ + +
+ + + +
+
+
+ АД +
+
+

Администратор

+

admin@company.com

+
+
+
+
+ +
+
+
+
+

{pageName}

+
+
+ +
+ +
+
+
+
+ +
+
+
{children}
+
+
+
+
+ ); +}; diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx new file mode 100644 index 0000000..0b8f294 --- /dev/null +++ b/src/components/Sidebar/index.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import Link from 'next/link'; + +type ISidebarElemProps = { + label: string; + link: string; + icon: React.ReactNode; +}; + +const SIDEBAR_ELEMS: ISidebarElemProps[] = [ + { + label: "Главная", + link: "/", + icon: ( + + + ) + }, + { + label: "Контрагенты", + link: "/contractors", + icon: ( + + + + ) + } +]; + +export const Sidebar = () => { + return ( + +)}; diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx new file mode 100644 index 0000000..6a4844c --- /dev/null +++ b/src/components/Table/index.tsx @@ -0,0 +1,214 @@ +'use client' +import React from 'react'; + +import { + Column, + Table as TanStackTable, + ExpandedState, + useReactTable, + getCoreRowModel, + getPaginationRowModel, + getFilteredRowModel, + getExpandedRowModel, + ColumnDef, + flexRender, + RowData, +} from '@tanstack/react-table' + +type TableProps = { + tableData: T[]; + columns: ColumnDef[]; +}; + +export function Table({ tableData, columns }: TableProps) { + const [expanded, setExpanded] = React.useState({}) + + const table = useReactTable({ + data: tableData, + columns, + state: { + expanded, + }, + onExpandedChange: setExpanded, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getExpandedRowModel: getExpandedRowModel(), + debugTable: true, + }); + + return ( +
+
+
+ + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( +
+
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ {header.column.getCanFilter() && ( +
+ +
+ )} +
+ )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+
+ + {/* Пагинация */} +
+
+ + + + +
+ +
+ + Страница{' '} + + {table.getState().pagination.pageIndex + 1} из {table.getPageCount()} + + + + + Перейти: + { + const page = e.target.value ? Number(e.target.value) - 1 : 0 + table.setPageIndex(page) + }} + className="w-16 px-2 py-1 border border-gray-300 rounded-md text-sm focus:ring-blue-500 focus:border-blue-500" + /> + + + +
+
+
+ ) +} + +function Filter({ + column, + table, +}: { + column: Column; + table: TanStackTable; +}) { + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id); + + const columnFilterValue = column.getFilterValue(); + + return typeof firstValue === "number" ? ( +
+ + column.setFilterValue((old: [number, number]) => [ + e.target.value, + old?.[1], + ]) + } + placeholder={`Min`} + className="w-24 border shadow rounded" + /> + + column.setFilterValue((old: [number, number]) => [ + old?.[0], + e.target.value, + ]) + } + placeholder={`Max`} + className="w-24 border shadow rounded" + /> +
+ ) : ( + column.setFilterValue(e.target.value)} + placeholder={`Поиск...`} + className="w-36 border shadow rounded" + /> + ); +}