main
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
50
README.md
Normal file
50
README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import react from 'eslint-plugin-react'
|
||||
|
||||
export default tseslint.config({
|
||||
// Set the react version
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
// Add the react plugin
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended rules
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/fuel.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Топливо и Транспорт</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4933
package-lock.json
generated
Normal file
4933
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
package.json
Normal file
57
package.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "fuel_react",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/carousel": "^7.15.3",
|
||||
"@mantine/charts": "^7.15.3",
|
||||
"@mantine/code-highlight": "^7.15.3",
|
||||
"@mantine/core": "^7.15.3",
|
||||
"@mantine/dates": "^7.15.3",
|
||||
"@mantine/dropzone": "^7.15.3",
|
||||
"@mantine/form": "^7.15.3",
|
||||
"@mantine/hooks": "^7.15.3",
|
||||
"@mantine/modals": "^7.15.3",
|
||||
"@mantine/notifications": "^7.15.3",
|
||||
"@mantine/nprogress": "^7.15.3",
|
||||
"@mantine/spotlight": "^7.15.3",
|
||||
"@mantine/tiptap": "^7.15.3",
|
||||
"@tabler/icons-react": "^3.28.1",
|
||||
"@tiptap/extension-link": "^2.11.2",
|
||||
"@tiptap/pm": "^2.11.2",
|
||||
"@tiptap/react": "^2.11.2",
|
||||
"@tiptap/starter-kit": "^2.11.2",
|
||||
"axios": "^1.7.9",
|
||||
"dayjs": "^1.11.13",
|
||||
"embla-carousel-react": "^7.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-imask": "^7.6.1",
|
||||
"react-router-dom": "^7.1.1",
|
||||
"recharts": "^2.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.14.0",
|
||||
"postcss": "^8.5.0",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.18.2",
|
||||
"vite": "^6.0.5"
|
||||
}
|
||||
}
|
||||
14
postcss.config.cjs
Normal file
14
postcss.config.cjs
Normal file
@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
BIN
public/fuel.png
Normal file
BIN
public/fuel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
24
src/App.tsx
Normal file
24
src/App.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { Layout } from "./components/Layout";
|
||||
import Dictionaries from "./pages/Dictionaries";
|
||||
import DriverForm from "./pages/DriverForm";
|
||||
import 'dayjs/locale/ru'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<MantineProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<div>a</div>} />
|
||||
<Route path="/dictionaries" element={<Dictionaries />} />
|
||||
<Route path="/drivers" element={<DriverForm />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
22
src/api/api.ts
Normal file
22
src/api/api.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import axios from "axios";
|
||||
|
||||
const API_BASE_URL = "https://api.jkhsakha.ru/is/fuel";
|
||||
|
||||
export const fetchDictionary = async (directory: string) => {
|
||||
const response = await axios.get(`${API_BASE_URL}/${directory}?limit=100`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const createDictionaryItem = async (directory: string, data: any) => {
|
||||
const response = await axios.post(`${API_BASE_URL}/${directory}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateDictionaryItem = async (directory: string, id: number, data: any) => {
|
||||
const response = await axios.patch(`${API_BASE_URL}/${directory}/${id}`, data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const deleteDictionaryItem = async (directory: string, id: number) => {
|
||||
await axios.delete(`${API_BASE_URL}/${directory}/${id}`);
|
||||
};
|
||||
67
src/components/DictionaryModal.tsx
Normal file
67
src/components/DictionaryModal.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Modal, Button, TextInput, Select } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { fetchDictionary } from "../api/api"; // Импорт API запроса
|
||||
|
||||
interface Props {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (values: any) => void;
|
||||
initialValues: any;
|
||||
fields: { name: string; label: string; type: "text" | "select"; options?: any[] }[];
|
||||
}
|
||||
|
||||
const DictionaryModal: React.FC<Props> = ({ opened, onClose, onSubmit, initialValues, fields }) => {
|
||||
const form = useForm({ initialValues });
|
||||
const [fuelTypes, setFuelTypes] = useState<{ value: string; label: string }[]>([]);
|
||||
|
||||
// Загружаем виды топлива при открытии модального окна
|
||||
useEffect(() => {
|
||||
if (opened) {
|
||||
fetchDictionary("fuel_types").then((data) => {
|
||||
const options = data.map((item: any) => ({
|
||||
value: String(item.id), // Преобразуем ID в строку
|
||||
label: item.name,
|
||||
}));
|
||||
setFuelTypes(options);
|
||||
});
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
// Обновляем форму при изменении initialValues
|
||||
useEffect(() => {
|
||||
const updatedValues = { ...initialValues };
|
||||
|
||||
// Приводим id_fuel_type к строке, если он есть
|
||||
if (updatedValues.id_fuel_type !== undefined) {
|
||||
updatedValues.id_fuel_type = String(updatedValues.id_fuel_type);
|
||||
}
|
||||
|
||||
form.setValues(updatedValues);
|
||||
}, [initialValues]);
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title="Добавить/Редактировать" centered>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
{fields.map((field) =>
|
||||
field.type === "text" ? (
|
||||
<TextInput key={field.name} label={field.label} {...form.getInputProps(field.name)} />
|
||||
) : (
|
||||
<Select
|
||||
key={field.name}
|
||||
label={field.label}
|
||||
data={field.name === "id_fuel_type" ? fuelTypes : field.options || []} // Используем API-данные для "Вид топлива"
|
||||
value={form.values[field.name] || ""} // Указываем явно значение
|
||||
{...form.getInputProps(field.name)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Button fullWidth mt="md" type="submit">
|
||||
Сохранить
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DictionaryModal;
|
||||
17
src/components/DictionaryPage.tsx
Normal file
17
src/components/DictionaryPage.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import {TextInput, Button}from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
const DictionaryPage = () => {
|
||||
const [inputValue, setInputValue] = useState("")
|
||||
const saveDataHandler = (value: String) => {
|
||||
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<TextInput label="Введите название" value={inputValue}
|
||||
onChange={(event) => setInputValue(event.currentTarget.value)}/>
|
||||
<Button onClick={() => saveDataHandler(inputValue)}>Сохранить</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default DictionaryPage;
|
||||
125
src/components/DictionaryTable.tsx
Normal file
125
src/components/DictionaryTable.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Table, ActionIcon, Text, Modal, Button, Pagination } from "@mantine/core";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { fetchDictionary } from "../api/api";
|
||||
|
||||
interface Props {
|
||||
data: any[];
|
||||
onEdit: (item: any) => void;
|
||||
onDelete: (id: number) => void;
|
||||
}
|
||||
|
||||
const columnNames: Record<string, string> = {
|
||||
name: "Наименование",
|
||||
name_short: "Аббревиатура",
|
||||
id_fuel_type: "Вид топлива",
|
||||
};
|
||||
|
||||
const DictionaryTable: React.FC<Props> = ({ data, onEdit, onDelete }) => {
|
||||
const [deleteModalOpened, setDeleteModalOpened] = useState(false);
|
||||
const [selectedItem, setSelectedItem] = useState<any | null>(null);
|
||||
const [fuelTypesMap, setFuelTypesMap] = useState<Map<string, string>>(new Map());
|
||||
|
||||
// Пагинация
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 10;
|
||||
|
||||
// Загружаем виды топлива
|
||||
useEffect(() => {
|
||||
fetchDictionary("fuel_types").then((fuelTypes) => {
|
||||
const map = new Map(fuelTypes.map((fuel: any) => [String(fuel.id), fuel.name]));
|
||||
setFuelTypesMap(map);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const openDeleteModal = (item: any) => {
|
||||
setSelectedItem(item);
|
||||
setDeleteModalOpened(true);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedItem) {
|
||||
onDelete(selectedItem.id);
|
||||
setDeleteModalOpened(false);
|
||||
setSelectedItem(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Фильтрация данных для текущей страницы
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const paginatedData = data.slice(startIndex, startIndex + itemsPerPage);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table striped highlightOnHover withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{data.length > 0 ? (
|
||||
Object.keys(data[0]).map((key) => (
|
||||
<Table.Th key={key} style={{ textAlign: "left" }}>
|
||||
{columnNames[key] || key}
|
||||
</Table.Th>
|
||||
))
|
||||
) : (
|
||||
<Table.Th></Table.Th>
|
||||
)}
|
||||
{data.length > 0 && <Table.Th style={{ textAlign: "left" }}>Действия</Table.Th>}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{paginatedData.length > 0 ? (
|
||||
paginatedData.map((item) => (
|
||||
<Table.Tr key={item.id}>
|
||||
{Object.keys(item).map((key, idx) => (
|
||||
<Table.Td key={idx}>
|
||||
{key === "id_fuel_type" ? fuelTypesMap.get(String(item[key])) || "Неизвестно" : String(item[key])}
|
||||
</Table.Td>
|
||||
))}
|
||||
<Table.Td>
|
||||
<ActionIcon color="blue" onClick={() => onEdit(item)} style={{ marginRight: "10px" }}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
<ActionIcon color="red" onClick={() => openDeleteModal(item)}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={data.length > 0 ? Object.keys(data[0]).length + 1 : 1} style={{ textAlign: "center" }}>
|
||||
<Text c="dimmed">Нет записей</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
|
||||
{/* Пагинация */}
|
||||
{data.length > itemsPerPage && (
|
||||
<Pagination
|
||||
total={Math.ceil(data.length / itemsPerPage)}
|
||||
value={currentPage}
|
||||
onChange={setCurrentPage}
|
||||
mt="md"
|
||||
position="center"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Модальное окно подтверждения удаления */}
|
||||
<Modal opened={deleteModalOpened} onClose={() => setDeleteModalOpened(false)} title="Подтверждение удаления" centered>
|
||||
<Text>Вы уверены, что хотите удалить <b>{selectedItem?.name || "элемент"}</b>?</Text>
|
||||
<div style={{ marginTop: "20px", display: "flex", justifyContent: "flex-end" }}>
|
||||
<Button variant="outline" onClick={() => setDeleteModalOpened(false)} style={{ marginRight: "10px" }}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button color="red" onClick={handleDelete}>
|
||||
Удалить
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DictionaryTable;
|
||||
57
src/components/Layout.tsx
Normal file
57
src/components/Layout.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { AppShell, Burger, Group, Text, NavLink } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { Outlet, useNavigate, useLocation } from "react-router-dom";
|
||||
import { IconHome2, IconGauge, IconActivity, IconUser } from "@tabler/icons-react";
|
||||
|
||||
export function Layout() {
|
||||
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
|
||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header>
|
||||
<Group h="100%" px="md">
|
||||
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
||||
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
|
||||
<img src="/fuel.png" width={50} />
|
||||
<Text size="xl" fw={900} variant="gradient" gradient={{ from: "blue", to: "cyan", deg: 90 }}>
|
||||
Топливо и Транспорт
|
||||
</Text>
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
<AppShell.Navbar p="md">
|
||||
<NavLink
|
||||
onClick={() => navigate("/")}
|
||||
label="Главная"
|
||||
leftSection={<IconHome2 size="1rem" stroke={1.5} />}
|
||||
active={location.pathname === "/"}
|
||||
/>
|
||||
<NavLink
|
||||
onClick={() => navigate("/dictionaries")}
|
||||
label="Справочники"
|
||||
leftSection={<IconGauge size="1rem" stroke={1.5} />}
|
||||
active={location.pathname === "/dictionaries"}
|
||||
/>
|
||||
<NavLink
|
||||
onClick={() => navigate("/drivers")}
|
||||
label="Водители"
|
||||
leftSection={<IconUser size="1rem" stroke={1.5} />}
|
||||
active={location.pathname === "/drivers"}
|
||||
/>
|
||||
</AppShell.Navbar>
|
||||
<AppShell.Main>
|
||||
<Outlet />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
10
src/components/MaskedDateInput.tsx
Normal file
10
src/components/MaskedDateInput.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { InputBase } from "@mantine/core"
|
||||
import { IMaskInput } from 'react-imask';
|
||||
|
||||
const MaskedDateInput = () => {
|
||||
|
||||
|
||||
return (
|
||||
<InputBase component={IMaskInput} mask="00.00.0000" placeholder="ДД.ММ.ГГГГ"/>
|
||||
)
|
||||
}
|
||||
11
src/main.tsx
Normal file
11
src/main.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
103
src/pages/Dictionaries.tsx
Normal file
103
src/pages/Dictionaries.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button, Select } from "@mantine/core";
|
||||
import DictionaryTable from "../components/DictionaryTable";
|
||||
import DictionaryModal from "../components/DictionaryModal";
|
||||
import { fetchDictionary, createDictionaryItem, updateDictionaryItem, deleteDictionaryItem } from "../api/api";
|
||||
|
||||
const dictionaryOptions = [
|
||||
{ value: "fuel_types", label: "Вид топлива" },
|
||||
{ value: "fuel", label: "Наименование топлива" },
|
||||
{ value: "unit", label: "Единица измерения" },
|
||||
{ value: "vehicle_class", label: "Категория транспорта" },
|
||||
{ value: "vehicle_type", label: "Тип транспорта" },
|
||||
{ value: "vehicle_activity", label: "Вид деятельности транспорта" },
|
||||
{ value: "driver_license_category", label: "Категория водительских удостоверений" },
|
||||
];
|
||||
|
||||
const fieldMappings: Record<string, any[]> = {
|
||||
fuel_types: [{ name: "name", label: "Наименование", type: "text" }],
|
||||
fuel: [
|
||||
{ name: "name", label: "Наименование", type: "text" },
|
||||
{ name: "id_fuel_type", label: "Вид топлива", type: "select", options: [] },
|
||||
],
|
||||
unit: [
|
||||
{ name: "name", label: "Наименование", type: "text" },
|
||||
{ name: "name_short", label: "Аббревиатура", type: "text" },
|
||||
],
|
||||
vehicle_class: [{ name: "name", label: "Наименование", type: "text" }],
|
||||
vehicle_type: [{ name: "name", label: "Наименование", type: "text" }],
|
||||
vehicle_activity: [{ name: "name", label: "Наименование", type: "text" }],
|
||||
driver_license_category: [
|
||||
{ name: "name", label: "Наименование", type: "text" },
|
||||
{ name: "name_short", label: "Аббревиатура", type: "text" },
|
||||
],
|
||||
};
|
||||
|
||||
const Dictionaries = () => {
|
||||
const [selectedDictionary, setSelectedDictionary] = useState("fuel_types");
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [modalOpened, setModalOpened] = useState(false);
|
||||
const [currentItem, setCurrentItem] = useState<any | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDictionary(selectedDictionary).then(setData);
|
||||
}, [selectedDictionary]);
|
||||
|
||||
const handleAdd = async (values: any) => {
|
||||
const newItem = await createDictionaryItem(selectedDictionary, values);
|
||||
setData((prev) => [...prev, newItem]); // Обновляем локальное состояние
|
||||
setModalOpened(false);
|
||||
};
|
||||
|
||||
const handleEdit = async (values: any) => {
|
||||
if (!currentItem) return;
|
||||
const updatedItem = await updateDictionaryItem(selectedDictionary, currentItem.id, values);
|
||||
setData((prev) => prev.map((item) => (item.id === currentItem.id ? updatedItem : item))); // Обновляем локальное состояние
|
||||
setModalOpened(false);
|
||||
setCurrentItem(null);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await deleteDictionaryItem(selectedDictionary, id);
|
||||
setData((prev) => prev.filter((item) => item.id !== id)); // Обновляем локальное состояние
|
||||
};
|
||||
|
||||
const getEmptyInitialValues = () => {
|
||||
return fieldMappings[selectedDictionary]?.reduce((acc, field) => {
|
||||
acc[field.name] = "";
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Справочники</h3>
|
||||
<Select
|
||||
data={dictionaryOptions}
|
||||
value={selectedDictionary}
|
||||
onChange={setSelectedDictionary}
|
||||
allowDeselect={false}
|
||||
searchable
|
||||
nothingFoundMessage="Не найдено..."
|
||||
maxDropdownHeight={300}
|
||||
/><br/>
|
||||
<Button onClick={() => { setCurrentItem(null); setModalOpened(true); }}>Добавить</Button><br/><br/>
|
||||
|
||||
<DictionaryTable
|
||||
data={data}
|
||||
onEdit={(item) => { setCurrentItem(item); setModalOpened(true); }}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<DictionaryModal
|
||||
opened={modalOpened}
|
||||
onClose={() => setModalOpened(false)}
|
||||
onSubmit={currentItem ? handleEdit : handleAdd}
|
||||
initialValues={currentItem || getEmptyInitialValues()}
|
||||
fields={fieldMappings[selectedDictionary] || []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dictionaries;
|
||||
259
src/pages/DriverForm.tsx
Normal file
259
src/pages/DriverForm.tsx
Normal file
@ -0,0 +1,259 @@
|
||||
import 'dayjs/locale/ru';
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Button,
|
||||
TextInput,
|
||||
Table,
|
||||
ActionIcon,
|
||||
Modal,
|
||||
} from "@mantine/core";
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import {
|
||||
fetchDictionary,
|
||||
createDictionaryItem,
|
||||
updateDictionaryItem,
|
||||
deleteDictionaryItem,
|
||||
} from "../api/api";
|
||||
|
||||
const DriverForm = () => {
|
||||
const [drivers, setDrivers] = useState<any[]>([]);
|
||||
const [licenseCategories, setLicenseCategories] = useState<{ value: string; label: string }[]>([]);
|
||||
const [selectedDriver, setSelectedDriver] = useState<any | null>(null);
|
||||
const [selectedLicense, setSelectedLicense] = useState<any | null>(null);
|
||||
const [modalOpened, setModalOpened] = useState(false);
|
||||
const [licenseModalOpened, setLicenseModalOpened] = useState(false);
|
||||
const [dateValue, setDateValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchDictionary("driver_license_category").then((data) => {
|
||||
const options = data.map((item: any) => ({
|
||||
value: String(item.id),
|
||||
label: item.name_short, // Берем короткое название (например, "B", "C", "D")
|
||||
}));
|
||||
setLicenseCategories(options);
|
||||
});
|
||||
|
||||
fetchDictionary("driver").then(setDrivers);
|
||||
}, []);
|
||||
|
||||
const driverForm = useForm({
|
||||
initialValues: {
|
||||
fullname: "Попов Спиридон Семенович",
|
||||
snils: "11122233344",
|
||||
birthday: null,
|
||||
iin: "123456789123",
|
||||
},
|
||||
});
|
||||
|
||||
const licenseForm = useForm({
|
||||
initialValues: {
|
||||
driverId: "",
|
||||
series_number: "",
|
||||
form_date: "",
|
||||
to_date: "",
|
||||
categories: [],
|
||||
frontPhoto: null,
|
||||
backPhoto: null,
|
||||
},
|
||||
});
|
||||
|
||||
function dateToYYYYMMDD(date) {
|
||||
if (!(date instanceof Date) || isNaN(date)) {
|
||||
return ""; // Handle invalid Date objects
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
const handleAddDriver = async (values: any) => {
|
||||
const date = new Date(Date.parse(values.birthday))
|
||||
|
||||
const newValues = {
|
||||
fullname: values.fullname,
|
||||
snils: values.snils,
|
||||
birthday: dateToYYYYMMDD(date),
|
||||
iin: values.iin
|
||||
}
|
||||
|
||||
if (selectedDriver) {
|
||||
const updatedDriver = await updateDictionaryItem("driver", selectedDriver.id, newValues);
|
||||
setDrivers((prev) => prev.map((d) => (d.id === selectedDriver.id ? updatedDriver : d)));
|
||||
} else {
|
||||
const newDriver = await createDictionaryItem("driver", newValues);
|
||||
setDrivers((prev) => [...prev, newDriver]);
|
||||
}
|
||||
setModalOpened(false);
|
||||
driverForm.reset();
|
||||
setSelectedDriver(null);
|
||||
};
|
||||
|
||||
const handleDeleteDriver = async (id: number) => {
|
||||
await deleteDictionaryItem("driver", id);
|
||||
setDrivers((prev) => prev.filter((driver) => driver.id !== id));
|
||||
};
|
||||
|
||||
const handleEditDriver = (driver: any) => {
|
||||
driverForm.setValues(driver);
|
||||
setSelectedDriver(driver);
|
||||
setModalOpened(true);
|
||||
};
|
||||
|
||||
const handleAddLicense = async (values: any) => {
|
||||
if (selectedLicense) {
|
||||
const updatedLicense = await updateDictionaryItem("driver_license", selectedLicense.id, values);
|
||||
setDrivers((prev) =>
|
||||
prev.map((driver) =>
|
||||
driver.id === values.driverId
|
||||
? {
|
||||
...driver,
|
||||
license: driver.license.map((lic) => (lic.id === selectedLicense.id ? updatedLicense : lic)),
|
||||
}
|
||||
: driver
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const newLicense = await createDictionaryItem("driver_license", values);
|
||||
const updatedDriver = await updateDictionaryItem("driver_connection", {
|
||||
driver_id: values.driverId,
|
||||
driver_license_id: newLicense.id,
|
||||
});
|
||||
|
||||
setDrivers((prev) =>
|
||||
prev.map((driver) =>
|
||||
driver.id === values.driverId
|
||||
? { ...driver, license: [...(driver.license || []), newLicense] }
|
||||
: driver
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
setLicenseModalOpened(false);
|
||||
licenseForm.reset();
|
||||
setSelectedLicense(null);
|
||||
};
|
||||
|
||||
const handleDeleteLicense = async (driverId: number, licenseId: number) => {
|
||||
await deleteDictionaryItem("driver_license", licenseId);
|
||||
setDrivers((prev) =>
|
||||
prev.map((driver) =>
|
||||
driver.id === driverId ? { ...driver, license: driver.license.filter((lic) => lic.id !== licenseId) } : driver
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
function parseDDMMYYYY(dateString: string): Date | null {
|
||||
const [day, month, year] = dateString.split('.').map(Number);
|
||||
|
||||
if (
|
||||
isNaN(day) ||
|
||||
isNaN(month) ||
|
||||
isNaN(year) ||
|
||||
month < 1 ||
|
||||
month > 12 ||
|
||||
year < 1000 || // Prevent extremely early dates
|
||||
year > 9999
|
||||
) {
|
||||
return null; // Invalid input
|
||||
}
|
||||
|
||||
return new Date(year, month - 1, day); // Month is 0-indexed in Date
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Водители</h2>
|
||||
<Button onClick={() => setModalOpened(true)}>Добавить водителя</Button>
|
||||
<Table striped highlightOnHover withColumnBorders mt="md">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>ФИО</Table.Th>
|
||||
<Table.Th>СНИЛС</Table.Th>
|
||||
<Table.Th>Дата рождения</Table.Th>
|
||||
<Table.Th>ИНН</Table.Th>
|
||||
<Table.Th>Действия</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{drivers.map((driver) => (
|
||||
<Table.Tr key={driver.id}>
|
||||
<Table.Td>{driver.fullname}</Table.Td>
|
||||
<Table.Td>{driver.snils}</Table.Td>
|
||||
<Table.Td>{driver.birthday}</Table.Td>
|
||||
<Table.Td>{driver.iin}</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon color="blue" onClick={() => handleEditDriver(driver)} style={{ marginRight: "10px" }}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
<ActionIcon color="red" onClick={() => handleDeleteDriver(driver.id)}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
|
||||
<h2>Водительские права</h2>
|
||||
<Button onClick={() => setLicenseModalOpened(true)}>Добавить права</Button>
|
||||
<Table striped highlightOnHover withColumnBorders mt="md">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>ФИО</Table.Th>
|
||||
<Table.Th>Серия и номер</Table.Th>
|
||||
<Table.Th>Дата выдачи</Table.Th>
|
||||
<Table.Th>Срок действия</Table.Th>
|
||||
<Table.Th>Категории</Table.Th>
|
||||
<Table.Th>Действия</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{drivers.flatMap((driver) =>
|
||||
driver.license?.map((license) => (
|
||||
<Table.Tr key={license.id}>
|
||||
<Table.Td>{driver.fullname}</Table.Td>
|
||||
<Table.Td>{license.series_number}</Table.Td>
|
||||
<Table.Td>{license.form_date}</Table.Td>
|
||||
<Table.Td>{license.to_date}</Table.Td>
|
||||
<Table.Td>{license.categories?.map((c) => c.name_short).join(", ")}</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon color="red" onClick={() => handleDeleteLicense(driver.id, license.id)}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
|
||||
<Modal opened={modalOpened} onClose={() => setModalOpened(false)} title="Добавить/Редактировать водителя">
|
||||
<form onSubmit={driverForm.onSubmit(handleAddDriver)}>
|
||||
<TextInput label="ФИО" {...driverForm.getInputProps("fullname")} required />
|
||||
<TextInput label="СНИЛС" {...driverForm.getInputProps("snils")} required />
|
||||
<TextInput label="ИНН" {...driverForm.getInputProps("iin")} required />
|
||||
<DateInput
|
||||
label="Дата рождения"
|
||||
placeholder="ДД.MM.ГГГГ"
|
||||
defaultLevel='month'
|
||||
valueFormat='DD.MM.YYYY'
|
||||
required
|
||||
dateParser={parseDDMMYYYY}
|
||||
locale="ru"
|
||||
{...driverForm.getInputProps("birthday")}
|
||||
/>
|
||||
<Button fullWidth mt="md" type="submit">
|
||||
Сохранить
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DriverForm;
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
26
tsconfig.app.json
Normal file
26
tsconfig.app.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
tsconfig.node.json
Normal file
24
tsconfig.node.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user