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",
|
"name": "peko-avto",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
"react": "^19.0.0",
|
"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": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|
@ -210,6 +216,14 @@
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
|
@ -1182,6 +1196,37 @@
|
||||||
"tailwindcss": "4.1.8"
|
"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": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
|
||||||
|
|
@ -2139,6 +2184,11 @@
|
||||||
"node": ">=18"
|
"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": {
|
"node_modules/client-only": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
|
|
@ -4703,6 +4753,11 @@
|
||||||
"react-is": "^16.13.1"
|
"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": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
@ -4751,6 +4806,21 @@
|
||||||
"react": "^19.1.0"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
|
@ -5337,6 +5407,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.8",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz",
|
||||||
|
|
@ -5369,6 +5448,11 @@
|
||||||
"node": ">=18"
|
"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": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||||
|
|
@ -5409,6 +5493,11 @@
|
||||||
"node": ">=8.0"
|
"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": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||||
|
|
@ -5450,6 +5539,17 @@
|
||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/typed-array-buffer": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||||
|
|
@ -5731,6 +5831,17 @@
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": "^19.0.0",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@tailwindcss/postcss": "^4",
|
|
||||||
"tailwindcss": "^4",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.3",
|
"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>
|
||||||
|
);
|
||||||
|
};
|
||||||
338
src/app/page.tsx
338
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() {
|
export default function Home() {
|
||||||
return (
|
const [activeGate, setActiveGate] = useState<number | null>(null);
|
||||||
<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)]">
|
const [events, setEvents] = useState<EventType[]>([]);
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
|
||||||
<Image
|
// Загрузка данных (заглушка)
|
||||||
className="dark:invert"
|
useEffect(() => {
|
||||||
src="/next.svg"
|
const mockEvents: EventType[] = [
|
||||||
alt="Next.js logo"
|
{ id: 1, type: 'ticket', driver: 'Иванов А.П.', gate: 12, time: '2 мин назад', queuePos: 5 },
|
||||||
width={180}
|
{ id: 2, type: 'contractor', action: 'создан', name: 'ООО "Грузовик+"', time: '5 мин назад' },
|
||||||
height={38}
|
{ id: 3, type: 'ticket', driver: 'Петров С.И.', gate: 8, time: '7 мин назад', queuePos: 2 }
|
||||||
priority
|
];
|
||||||
/>
|
setEvents(mockEvents);
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
const gates = [5, 8, 12, 15]; // Номера гейтов
|
||||||
<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"
|
return (
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<PageLayout pageName='Главная страница'>
|
||||||
target="_blank"
|
<div className="flex flex-col gap-2">
|
||||||
rel="noopener noreferrer"
|
{/* Основная карта */}
|
||||||
>
|
<NewTruckPanel />
|
||||||
<Image
|
<div className='flex gap-2'>
|
||||||
className="dark:invert"
|
<div className="flex-1 relative overflow-hidden">
|
||||||
src="/vercel.svg"
|
<div className="relative h-[80vh] bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl border border-gray-200">
|
||||||
alt="Vercel logomark"
|
{/* Гейты */}
|
||||||
width={20}
|
{gates.map(gate => (
|
||||||
height={20}
|
<Gate
|
||||||
/>
|
key={gate}
|
||||||
Deploy now
|
number={gate}
|
||||||
</a>
|
active={activeGate === gate}
|
||||||
<a
|
onClick={() => setActiveGate(gate)}
|
||||||
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"
|
{/* Грузовики */}
|
||||||
>
|
<Truck x="20%" y="30%" driver="Иванов А.П." queuePos={5} />
|
||||||
Read our docs
|
<Truck x="60%" y="50%" driver="Петров С.И." queuePos={2} />
|
||||||
</a>
|
|
||||||
|
{/* Легенда */}
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
</main>
|
</div>
|
||||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
</PageLayout>
|
||||||
<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"
|
function NewTruckPanel() {
|
||||||
rel="noopener noreferrer"
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
>
|
|
||||||
<Image
|
const toggleExpand = () => setIsExpanded(!isExpanded);
|
||||||
aria-hidden
|
|
||||||
src="/file.svg"
|
return (
|
||||||
alt="File icon"
|
<div className="max-w-[350px] bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
|
||||||
width={16}
|
<button
|
||||||
height={16}
|
onClick={toggleExpand}
|
||||||
/>
|
className="w-full hover:bg-gray-50 transition-colors"
|
||||||
Learn
|
>
|
||||||
</a>
|
<div className="px-4 py-3 sm:px-6 lg:px-3">
|
||||||
<a
|
<div className="flex items-center justify-between">
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
<div className="flex items-center space-x-3">
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<div className="flex-shrink-0 p-2 bg-green-100 rounded-lg">
|
||||||
target="_blank"
|
<TruckIcon className="h-6 w-6 text-green-600" />
|
||||||
rel="noopener noreferrer"
|
</div>
|
||||||
>
|
<div>
|
||||||
<Image
|
<h3 className="text-sm font-medium text-left">Прибыл новый грузовик</h3>
|
||||||
aria-hidden
|
<p className="text-xs text-gray-500">Нажмите, чтобы посмотреть подробнее</p>
|
||||||
src="/window.svg"
|
</div>
|
||||||
alt="Window icon"
|
</div>
|
||||||
width={16}
|
<div className="ml-2">
|
||||||
height={16}
|
{isExpanded ? (
|
||||||
/>
|
<ChevronUpIcon className="h-5 w-5 text-gray-400" />
|
||||||
Examples
|
) : (
|
||||||
</a>
|
<ChevronDownIcon className="h-5 w-5 text-gray-400" />
|
||||||
<a
|
)}
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
</div>
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
</div>
|
||||||
target="_blank"
|
</div>
|
||||||
rel="noopener noreferrer"
|
</button>
|
||||||
>
|
|
||||||
<Image
|
{/* Выдвигающаяся панель с подробной информацией */}
|
||||||
aria-hidden
|
<div className={`transition-all duration-300 ease-in-out overflow-hidden ${isExpanded ? "max-h-96" : "max-h-0"}`}>
|
||||||
src="/globe.svg"
|
<div className="p-4 border-t border-gray-200 bg-gray-50">
|
||||||
alt="Globe icon"
|
<div className="flex items-center space-x-4 mb-4">
|
||||||
width={16}
|
<div className="flex-shrink-0 p-3 bg-green-100 rounded-lg">
|
||||||
height={16}
|
<TruckIcon className="h-8 w-8 text-green-600" />
|
||||||
/>
|
</div>
|
||||||
Go to nextjs.org →
|
<div>
|
||||||
</a>
|
<h4 className="text-md font-semibold">Грузовик #12345</h4>
|
||||||
</footer>
|
<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>
|
</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