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

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="ДД.ММ.ГГГГ"/>
)
}