This commit is contained in:
23 changed files with 5879 additions and 0 deletions

24
.gitignore vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

57
package.json Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

24
src/App.tsx Normal file
View 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
View 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}`);
};

View 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;

View 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;

View 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
View 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>
);
}

View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

26
tsconfig.app.json Normal file
View 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
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View 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
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})