init commit
This commit is contained in:
parent
89f5721441
commit
80ef365a6b
113
package-lock.json
generated
113
package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
package.json
16
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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
137
src/app/carrier/page.tsx
Normal file
137
src/app/carrier/page.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="flex items-center border-4 border-black w-[220px] h-[50px] bg-white rounded-md overflow-hidden shadow-md">
|
||||
<div className="flex items-center justify-center text-black text-3xl font-bold px-2 w-3/4">
|
||||
{number.substring(0, 6)}
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-1/4 border-l-4 border-black text-black">
|
||||
<span className="text-xl font-bold">{regionCode}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs">RUS</span>
|
||||
<div className="w-5 h-[14px] border border-black box-border">
|
||||
<div className="w-full h-1 bg-white"></div>
|
||||
<div className="w-full h-1 bg-blue-600"></div>
|
||||
<div className="w-full h-1 bg-red-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type AddCarFormProps = Omit<Vehicle, "id">;
|
||||
|
||||
const ADD_CAR_INIT: AddCarFormProps = {
|
||||
arrivalDate: "",
|
||||
driver: "",
|
||||
licensePlate: "",
|
||||
name: ""
|
||||
};
|
||||
|
||||
export default function CarrierDashboard() {
|
||||
const { register, handleSubmit, watch, reset, formState: { errors } } = useForm<AddCarFormProps>({ defaultValues: ADD_CAR_INIT });
|
||||
const [isAddOpen, setIsAddOpen] = useState<Boolean>(false);
|
||||
|
||||
const onSubmit: SubmitHandler<AddCarFormProps> = (form) => {
|
||||
console.log('form', form);
|
||||
};
|
||||
|
||||
const handleFormClose = () => {
|
||||
reset(ADD_CAR_INIT);
|
||||
setIsAddOpen(false);
|
||||
};
|
||||
|
||||
const [vehicles, setVehicles] = useState<Vehicle[]>([
|
||||
{
|
||||
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 (
|
||||
<PageLayout pageName="Личный кабинет перевозчика">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-black mb-4">Управление вашим автопарком</h2>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4">
|
||||
{isAddOpen && (
|
||||
<div className="mb-8 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<h2 className="text-lg font-medium text-gray-800 mb-4">Добавить новую машину</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<Input id="name" label='Название машины' type="text" placeholder='Например: Volvo FH16' {...register("name", { required: "Это обязательное поле" })} />
|
||||
<Input id="licensePlate" label='Гос. номер' type="text" placeholder='Например: А777РТ777' {...register("licensePlate", { required: "Это обязательное поле" })} />
|
||||
<Input id="driver" label='Водитель' type="text" placeholder='Например: Денисов В. А.' {...register("driver", { required: "Это обязательное поле" })} />
|
||||
<Input id="arrivalDate" label='Дата прибытия' type="date" {...register("arrivalDate", { required: "Это обязательное поле" })} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button onClick={handleFormClose} className="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium rounded-md shadow-sm transition-colors duration-200">Отмена</button>
|
||||
<button onClick={handleSubmit(onSubmit)} className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md shadow-sm transition-colors duration-200">
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className='flex justify-between'>
|
||||
<h2 className="text-lg font-medium text-gray-800 mb-4">Ваши машины ({vehicles.length})</h2>
|
||||
<Button onClick={() => setIsAddOpen((prev) => !prev)}>+ Добавить машину</Button>
|
||||
</div>
|
||||
{vehicles.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
У вас пока нет добавленных машин
|
||||
</div>
|
||||
) : (
|
||||
<div className="shadow">
|
||||
<Table
|
||||
columns={[
|
||||
{ accessorKey: "name", cell: info => info.getValue(), header: () => "Автомобиль" },
|
||||
{ accessorKey: "licensePlate", cell: info => <LicensePlate number={info.getValue<string>()} />, header: () => "Гос. номер" },
|
||||
{ accessorKey: "driver", cell: info => info.getValue(), header: () => "Водитель" },
|
||||
{ accessorKey: "arrivalDate", cell: info => info.getValue(), header: () => "Дата прибытия" },
|
||||
{ accessorKey: "delete", cell: info => <Button>Удалить</Button>, header: () => "Удалить" }
|
||||
]}
|
||||
tableData={vehicles}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
378
src/app/contractors/create/page.tsx
Normal file
378
src/app/contractors/create/page.tsx
Normal file
|
|
@ -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: (
|
||||
<svg className="w-3 h-3 mr-2.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Контрагенты",
|
||||
link: "/contractors",
|
||||
icon: (
|
||||
<svg className="w-3 h-3 text-gray-400 mx-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Создание",
|
||||
link: "/contractors/create",
|
||||
icon: (
|
||||
<svg className="w-3 h-3 text-gray-400 mx-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
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<HTMLInputElement | HTMLSelectElement>) => {
|
||||
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 (
|
||||
<PageLayout pageName='Добавление контрагента'>
|
||||
<Breadcrumbs breadcrumbs={BREADCRUMBS} />
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
{error && <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Основная информация */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Основная информация</h2>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Наименование *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="type"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Тип контрагента *
|
||||
</label>
|
||||
<select
|
||||
id="type"
|
||||
name="type"
|
||||
value={formData.type}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
>
|
||||
<option value="legal">Юридическое лицо</option>
|
||||
<option value="individual">Физическое лицо/ИП</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="inn"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
ИНН *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="inn"
|
||||
name="inn"
|
||||
value={formData.inn}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.type === "legal" && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="kpp"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
КПП
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="kpp"
|
||||
name="kpp"
|
||||
value={formData.kpp}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="ogrn"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{formData.type === "legal" ? "ОГРН" : "ОГРНИП"}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="ogrn"
|
||||
name="ogrn"
|
||||
value={formData.ogrn}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Контактная информация</h2>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="address"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Адрес
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="address"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Телефон
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="contactPerson"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Контактное лицо
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="contactPerson"
|
||||
name="contactPerson"
|
||||
value={formData.contactPerson}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Банковские реквизиты */}
|
||||
<div className="space-y-4 md:col-span-2">
|
||||
<h2 className="text-xl font-semibold">Банковские реквизиты</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="bankDetails.account"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Расчетный счет
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="bankDetails.account"
|
||||
name="bankDetails.account"
|
||||
value={formData.bankDetails.account}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="bankDetails.bank"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Банк
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="bankDetails.bank"
|
||||
name="bankDetails.bank"
|
||||
value={formData.bankDetails.bank}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="bankDetails.bik"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
БИК
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="bankDetails.bik"
|
||||
name="bankDetails.bik"
|
||||
value={formData.bankDetails.bik}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="bankDetails.correspondentAccount"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Корр. счет
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="bankDetails.correspondentAccount"
|
||||
name="bankDetails.correspondentAccount"
|
||||
value={formData.bankDetails.correspondentAccount}
|
||||
onChange={handleChange}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
// onClick={() => router.push('/contractors')}
|
||||
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? "Сохранение..." : "Создать контрагента"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
243
src/app/contractors/page.tsx
Normal file
243
src/app/contractors/page.tsx
Normal file
|
|
@ -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<Counterparty[]>([
|
||||
{
|
||||
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: (
|
||||
<svg className="w-3 h-3 mr-2.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Контрагенты",
|
||||
link: "/contractors",
|
||||
icon: (
|
||||
<svg className="w-3 h-3 text-gray-400 mx-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageLayout pageName="Управление контрагентами">
|
||||
<div className="flex justify-between">
|
||||
<Breadcrumbs breadcrumbs={BREADCRUMBS} />
|
||||
<Link href="contractors/create">
|
||||
<Button>Добавить контрагента</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-medium text-gray-900">
|
||||
Список контрагентов
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-200">
|
||||
{counterparties.map((counterparty) => (
|
||||
<div
|
||||
key={counterparty.id}
|
||||
className="p-6 transition-colors duration-150"
|
||||
>
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => toggleCounterparty(counterparty.id)}
|
||||
>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
{counterparty.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">ИНН: {counterparty.inn}</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm text-gray-500 mr-2">
|
||||
{counterparty.vehicles.length} авто
|
||||
</span>
|
||||
<svg
|
||||
className={`w-5 h-5 text-gray-500 transform transition-transform duration-200 ${
|
||||
counterparty.expanded ? "rotate-180" : ""
|
||||
}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{counterparty.expanded && (
|
||||
<div className="mt-4 pl-4 border-l-2 border-gray-200">
|
||||
<div className="space-y-4">
|
||||
{counterparty.vehicles.map((vehicle) => (
|
||||
<div key={vehicle.id} className="bg-gray-50 p-4 rounded-lg">
|
||||
{/* Информация об автомобиле */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500">
|
||||
Модель
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-gray-900">
|
||||
{vehicle.name}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500">
|
||||
Гос. номер
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-gray-900">
|
||||
{vehicle.licensePlate}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-500">
|
||||
Вместимость (кг)
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-gray-900">
|
||||
{vehicle.capacity.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<label
|
||||
htmlFor={`route-select-${vehicle.id}`}
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Назначить маршрут
|
||||
</label>
|
||||
<select
|
||||
id={`route-select-${vehicle.id}`}
|
||||
className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
|
||||
value={vehicle.selectedRoute || ""}
|
||||
onChange={(e) =>
|
||||
handleRouteSelect(
|
||||
counterparty.id,
|
||||
vehicle.id,
|
||||
e.target.value ? parseInt(e.target.value) : null
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">Не выбран</option>
|
||||
{routes.map((route) => (
|
||||
<option key={route.id} value={route.id}>
|
||||
{route.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CounterpartiesPage;
|
||||
42
src/app/driver-ticket/page.tsx
Normal file
42
src/app/driver-ticket/page.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { PageLayout } from '@/components/PageLayout';
|
||||
|
||||
export default function DriverTicket({ driverName = "Иванов А.П.", gateNumber = "12", queueNumber = "5" }) {
|
||||
return (
|
||||
<PageLayout pageName='Талон водителя'>
|
||||
<div className="w-full max-w-xs bg-white rounded-lg overflow-hidden shadow-lg border-t-4 border-gray-800">
|
||||
<div className="bg-gray-800 text-white py-3 px-4 text-center font-bold text-lg tracking-wider">
|
||||
ТАЛОН ВОДИТЕЛЯ
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center py-2 border-b border-dashed border-gray-200">
|
||||
<span className="text-gray-600 font-bold text-sm">Водитель:</span>
|
||||
<span className="text-gray-800 font-bold">{driverName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-dashed border-gray-200">
|
||||
<span className="text-gray-600 font-bold text-sm">Gate #:</span>
|
||||
<span className="text-gray-800 font-bold">{gateNumber}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Queue Number */}
|
||||
<div className="text-center mt-5">
|
||||
<div className="text-5xl font-bold text-red-600 leading-none">{queueNumber}</div>
|
||||
<div className="text-gray-600 text-sm mt-1">номер в очереди</div>
|
||||
</div>
|
||||
|
||||
{/* Barcode */}
|
||||
<div className="mt-6 text-center py-2 bg-gray-50 font-barcode text-4xl">
|
||||
A{Math.random().toString(36).substring(2, 10).toUpperCase()}B
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-xs text-center text-gray-500 py-2 bg-gray-50">
|
||||
Талон действителен в течение 2 часов
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
334
src/app/page.tsx
334
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 (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
const [activeGate, setActiveGate] = useState<number | null>(null);
|
||||
const [events, setEvents] = useState<EventType[]>([]);
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
// Загрузка данных (заглушка)
|
||||
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);
|
||||
}, []);
|
||||
|
||||
const gates = [5, 8, 12, 15]; // Номера гейтов
|
||||
|
||||
return (
|
||||
<PageLayout pageName='Главная страница'>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Основная карта */}
|
||||
<NewTruckPanel />
|
||||
<div className='flex gap-2'>
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
<div className="relative h-[80vh] bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl border border-gray-200">
|
||||
{/* Гейты */}
|
||||
{gates.map(gate => (
|
||||
<Gate
|
||||
key={gate}
|
||||
number={gate}
|
||||
active={activeGate === gate}
|
||||
onClick={() => setActiveGate(gate)}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
))}
|
||||
|
||||
{/* Грузовики */}
|
||||
<Truck x="20%" y="30%" driver="Иванов А.П." queuePos={5} />
|
||||
<Truck x="60%" y="50%" driver="Петров С.И." queuePos={2} />
|
||||
|
||||
{/* Легенда */}
|
||||
<div className="absolute bottom-4 left-4 bg-white/80 p-2 rounded-lg shadow-sm">
|
||||
<div className="flex items-center space-x-3 text-sm">
|
||||
<span className="flex items-center"><span className="w-3 h-3 bg-green-500 rounded-full mr-1"></span> Свободен</span>
|
||||
<span className="flex items-center"><span className="w-3 h-3 bg-red-500 rounded-full mr-1"></span> Занят</span>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-96 bg-white border-l border-gray-200 overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<h2 className="text-xl font-semibold flex items-center">
|
||||
<ClockIcon className="h-5 w-5 mr-2 text-amber-500" />
|
||||
Live-активность
|
||||
</h2>
|
||||
|
||||
<div className="mt-6 space-y-4">
|
||||
{events.map(event => (
|
||||
<EventCard key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Статистика */}
|
||||
<div className="mt-8">
|
||||
<h3 className="font-medium text-gray-700 mb-3">Статистика за день</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<StatCard icon={<TruckIcon className="h-5 w-5" />} label="Талонов" value="24" />
|
||||
<StatCard icon={<BuildingOfficeIcon className="h-5 w-5" />} label="Контрагентов" value="3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function NewTruckPanel() {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const toggleExpand = () => setIsExpanded(!isExpanded);
|
||||
|
||||
return (
|
||||
<div className="max-w-[350px] bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
|
||||
<button
|
||||
onClick={toggleExpand}
|
||||
className="w-full hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
<div className="px-4 py-3 sm:px-6 lg:px-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0 p-2 bg-green-100 rounded-lg">
|
||||
<TruckIcon className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-left">Прибыл новый грузовик</h3>
|
||||
<p className="text-xs text-gray-500">Нажмите, чтобы посмотреть подробнее</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
{isExpanded ? (
|
||||
<ChevronUpIcon className="h-5 w-5 text-gray-400" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-5 w-5 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Выдвигающаяся панель с подробной информацией */}
|
||||
<div className={`transition-all duration-300 ease-in-out overflow-hidden ${isExpanded ? "max-h-96" : "max-h-0"}`}>
|
||||
<div className="p-4 border-t border-gray-200 bg-gray-50">
|
||||
<div className="flex items-center space-x-4 mb-4">
|
||||
<div className="flex-shrink-0 p-3 bg-green-100 rounded-lg">
|
||||
<TruckIcon className="h-8 w-8 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-md font-semibold">Грузовик #12345</h4>
|
||||
<p className="text-sm text-gray-500">Статус: Прибыл на склад</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="col-span-1 text-gray-500">Водитель:</div>
|
||||
<div className="col-span-2">Иванов Иван Иванович</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="col-span-1 text-gray-500">Груз:</div>
|
||||
<div className="col-span-2">Бытовая техника (20 коробок)</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="col-span-1 text-gray-500">Время прибытия:</div>
|
||||
<div className="col-span-2">12:30, 15 июня 2023</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="col-span-1 text-gray-500">Место разгрузки:</div>
|
||||
<div className="col-span-2">Склад №3, дверь 2B</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Gate({ number, active, onClick }: { number: number, active: Boolean, onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`absolute ${getGatePosition(number)} w-24 h-24 rounded-lg border-4 flex items-center justify-center transition-all
|
||||
${active ? 'border-blue-500 bg-blue-50' : 'border-gray-300 bg-white'}`}
|
||||
>
|
||||
<span className="text-xl font-bold">#{number}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Truck({ x, y, driver, queuePos }: { x: string, y: string, driver: string, queuePos: number }) {
|
||||
return (
|
||||
<div
|
||||
className="absolute cursor-pointer group"
|
||||
style={{ left: x, top: y }}
|
||||
>
|
||||
<div className="bg-orange-500 text-white p-2 rounded-lg shadow-md transform -rotate-6 group-hover:rotate-0 transition">
|
||||
<TruckIcon className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="opacity-0 group-hover:opacity-100 transition bg-white shadow-lg rounded-lg p-2 mt-1 w-40">
|
||||
<p className="font-medium truncate">{driver}</p>
|
||||
<p className="text-sm">Очередь: <span className="font-bold">{queuePos}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EventCard({ event }: { event: any }) {
|
||||
return (
|
||||
<div className="p-3 bg-gray-50 rounded-lg border border-gray-200 hover:border-blue-300 transition">
|
||||
<div className="flex items-start">
|
||||
<div className={`p-2 rounded-lg mr-3 ${
|
||||
event.type === 'ticket' ? 'bg-blue-100 text-blue-600' : 'bg-purple-100 text-purple-600'
|
||||
}`}>
|
||||
{event.type === 'ticket' ? (
|
||||
<TruckIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<BuildingOfficeIcon className="h-5 w-5" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{event.type === 'ticket'
|
||||
? `Талон для ${event.driver} (гейт ${event.gate})`
|
||||
: `Добавлен ${event.name}`}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 flex items-center mt-1">
|
||||
<ClockIcon className="h-3 w-3 mr-1" />
|
||||
{event.time}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ icon, label, value }: any) {
|
||||
return (
|
||||
<div className="bg-gray-50 p-3 rounded-lg border border-gray-200">
|
||||
<div className="flex items-center">
|
||||
<div className="p-2 bg-white rounded-lg mr-3 shadow-sm">
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">{label}</p>
|
||||
<p className="text-xl font-bold">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Хелперы
|
||||
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] || "";
|
||||
}
|
||||
28
src/components/Breadcrumbs/index.tsx
Normal file
28
src/components/Breadcrumbs/index.tsx
Normal file
|
|
@ -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 (
|
||||
<nav className="flex mb-6" aria-label="Breadcrumb">
|
||||
<ol className="inline-flex items-center gap-2">
|
||||
{breadcrumbs.map(({ icon, label, link }) =>
|
||||
<li key={label} className="inline-flex items-center">
|
||||
<Link href={link} className="inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600">
|
||||
{icon}
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
10
src/components/Button/index.tsx
Normal file
10
src/components/Button/index.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { ButtonHTMLAttributes } from 'react';
|
||||
|
||||
type IButtonProps = {
|
||||
children: React.ReactNode;
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
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 <button {...props} className={buttonClsx}>{children}</button>;
|
||||
};
|
||||
31
src/components/Input/index.tsx
Normal file
31
src/components/Input/index.tsx
Normal file
|
|
@ -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<HTMLInputElement>;
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{label}
|
||||
</label>
|
||||
<input {...props} id={id} className={inputClsx} aria-invalid={isError ? "true" : "false"} aria-describedby={isError && errorText ? `${id}-error` : undefined} />
|
||||
{isError && errorText && <p id={`${id}-error`} className="mt-2 text-sm text-red-600">{errorText}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
76
src/components/PageLayout/index.tsx
Normal file
76
src/components/PageLayout/index.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="flex h-screen bg-gray-50">
|
||||
<Head>
|
||||
<title>{pageName}</title>
|
||||
</Head>
|
||||
|
||||
<div className={`bg-gray-800 text-white transition-all duration-300 flex flex-col`}>
|
||||
<div className="p-4 flex items-center justify-between border-b border-gray-700">
|
||||
<h1 className="text-xl font-bold">PEKO-Авто</h1>
|
||||
<span className="text-2xl">⚙️</span>
|
||||
<button className="text-gray-400 hover:text-white">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Sidebar />
|
||||
|
||||
<div className="p-4 border-t border-gray-700">
|
||||
<div className="flex items-center">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-600 flex items-center justify-center">
|
||||
<span className="text-sm">АД</span>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium">Администратор</p>
|
||||
<p className="text-xs text-gray-400">admin@company.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<header className="bg-white shadow-sm z-10">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{pageName}</h2>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<button className="p-1 rounded-full text-gray-500 hover:text-gray-600 hover:bg-gray-100">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button className="flex items-center space-x-2 focus:outline-none">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white">
|
||||
<span>АД</span>
|
||||
</div>
|
||||
<span className="hidden md:inline-block font-medium">
|
||||
Администратор
|
||||
</span>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-6 bg-gray-50">
|
||||
<div className="mx-auto">
|
||||
<div className="bg-white shadow rounded-lg p-6">{children}</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
44
src/components/Sidebar/index.tsx
Normal file
44
src/components/Sidebar/index.tsx
Normal file
|
|
@ -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: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>)
|
||||
},
|
||||
{
|
||||
label: "Контрагенты",
|
||||
link: "/contractors",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
export const Sidebar = () => {
|
||||
return (
|
||||
<nav className="flex-1 overflow-y-auto">
|
||||
<ul className="space-y-1 p-2">
|
||||
{SIDEBAR_ELEMS.map(({ icon, label, link }) => (
|
||||
<li key={label}>
|
||||
<Link href={link} className="flex items-center p-2 rounded hover:bg-gray-700">
|
||||
{icon}
|
||||
<span className="ml-3">{label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
)};
|
||||
214
src/components/Table/index.tsx
Normal file
214
src/components/Table/index.tsx
Normal file
|
|
@ -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<T extends RowData> = {
|
||||
tableData: T[];
|
||||
columns: ColumnDef<T>[];
|
||||
};
|
||||
|
||||
export function Table<T extends RowData>({ tableData, columns }: TableProps<T>) {
|
||||
const [expanded, setExpanded] = React.useState<ExpandedState>({})
|
||||
|
||||
const table = useReactTable<T>({
|
||||
data: tableData,
|
||||
columns,
|
||||
state: {
|
||||
expanded,
|
||||
},
|
||||
onExpandedChange: setExpanded,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getExpandedRowModel: getExpandedRowModel(),
|
||||
debugTable: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white rounded-lg shadow-sm border border-gray-200">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="flex items-center">
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</div>
|
||||
{header.column.getCanFilter() && (
|
||||
<div className="mt-1">
|
||||
<Filter column={header.column} table={table} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{table.getRowModel().rows.map(row => (
|
||||
<tr key={row.id} className="hover:bg-gray-50 transition-colors">
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Пагинация */}
|
||||
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
{'<<'}
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
{'<'}
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
{'>'}
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
{'>>'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-gray-700">
|
||||
<span className="flex items-center gap-1">
|
||||
Страница{' '}
|
||||
<strong>
|
||||
{table.getState().pagination.pageIndex + 1} из {table.getPageCount()}
|
||||
</strong>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-1">
|
||||
Перейти:
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={table.getPageCount()}
|
||||
defaultValue={table.getState().pagination.pageIndex + 1}
|
||||
onChange={e => {
|
||||
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"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<select
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={e => {
|
||||
table.setPageSize(Number(e.target.value))
|
||||
}}
|
||||
className="px-2 py-1 border border-gray-300 rounded-md text-sm focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Показать {pageSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Filter({
|
||||
column,
|
||||
table,
|
||||
}: {
|
||||
column: Column<any, any>;
|
||||
table: TanStackTable<any>;
|
||||
}) {
|
||||
const firstValue = table
|
||||
.getPreFilteredRowModel()
|
||||
.flatRows[0]?.getValue(column.id);
|
||||
|
||||
const columnFilterValue = column.getFilterValue();
|
||||
|
||||
return typeof firstValue === "number" ? (
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
value={(columnFilterValue as [number, number])?.[0] ?? ""}
|
||||
onChange={(e) =>
|
||||
column.setFilterValue((old: [number, number]) => [
|
||||
e.target.value,
|
||||
old?.[1],
|
||||
])
|
||||
}
|
||||
placeholder={`Min`}
|
||||
className="w-24 border shadow rounded"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={(columnFilterValue as [number, number])?.[1] ?? ""}
|
||||
onChange={(e) =>
|
||||
column.setFilterValue((old: [number, number]) => [
|
||||
old?.[0],
|
||||
e.target.value,
|
||||
])
|
||||
}
|
||||
placeholder={`Max`}
|
||||
className="w-24 border shadow rounded"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={(columnFilterValue ?? "") as string}
|
||||
onChange={(e) => column.setFilterValue(e.target.value)}
|
||||
placeholder={`Поиск...`}
|
||||
className="w-36 border shadow rounded"
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user