main
This commit is contained in:
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="ДД.ММ.ГГГГ"/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user