refactoring

This commit is contained in:
2025-09-02 09:32:46 +09:00
parent c5afd523fb
commit 398425d022
4 changed files with 225 additions and 195 deletions

View File

@ -0,0 +1,76 @@
import { useState } from "react";
import { IDriver, IDriverLicense } from "../../interfaces/Driver";
import { ActionIcon, Button, Flex, Table, Transition } from "@mantine/core";
import { stringToSnils } from "../../utils/format";
import { dateToDDMMYYYY } from "../../utils/date";
import { IconEdit, IconLinkPlus, IconPlus, IconTrash } from "@tabler/icons-react";
import DriverLicense from "./DriverLicense";
interface IDriverProps {
driver: IDriver,
handleEditDriver: (driver: IDriver) => void,
handleDeleteDriver: (id: number) => void,
handleEditLicense: (license: IDriverLicense) => void,
handleDeleteLicense: (id: number) => void
}
const Driver = ({ driver, handleEditDriver, handleDeleteDriver, handleEditLicense, handleDeleteLicense }: IDriverProps) => {
const [opened, setOpened] = useState(false)
return (
<>
<Table.Tr style={{ cursor: 'pointer' }} onClick={() => setOpened(!opened)}>
<Table.Td>{driver.fullname}</Table.Td>
<Table.Td>{stringToSnils(driver.snils)}</Table.Td>
<Table.Td>{dateToDDMMYYYY(driver.birthday)}</Table.Td>
<Table.Td>{driver.iin}</Table.Td>
<Table.Td>
<ActionIcon color="blue" onClick={(e) => { e.stopPropagation(); handleEditDriver(driver); }} style={{ marginRight: "10px" }}>
<IconEdit />
</ActionIcon>
<ActionIcon color="red" onClick={(e) => { e.stopPropagation(); handleDeleteDriver(driver.id); }}>
<IconTrash />
</ActionIcon>
</Table.Td>
</Table.Tr>
<Transition
mounted={opened}
transition={{
in: { opacity: 1, transform: 'scaleY(1)' },
out: { opacity: 0, transform: 'scaleY(0)' },
common: { transformOrigin: 'top' },
transitionProperty: 'transform, opacity',
}}
duration={200}
timingFunction='ease'
keepMounted
>
{(transitionStyle) => (
<Table.Tr style={{ ...transitionStyle }}>
<Table.Td colSpan={5}>
<Flex justify='space-evenly'>
<Button leftSection={<IconPlus />} onClick={() => handleEditLicense({} as IDriverLicense)}>
Добавить водительские права
</Button>
<Button leftSection={<IconLinkPlus />}>Привязать существующие права</Button>
</Flex>
</Table.Td>
</Table.Tr>
)}
</Transition>
{opened && driver.license && Array.isArray(driver.license) && driver.license.map((license: IDriverLicense, index) => (
<Table.Tr>
<DriverLicense
key={`licId-${license.id}-${index}`}
license={license}
handleDeleteLicense={handleDeleteLicense}
handleEditLicense={handleEditLicense}
/>
</Table.Tr>
))}
</>
)
}
export default Driver

View File

@ -0,0 +1,29 @@
import { ActionIcon, Table } from "@mantine/core"
import { IDriverLicense } from "../../interfaces/Driver"
import { dateToDDMMYYYY } from "../../utils/date"
import { IconEdit, IconTrash } from "@tabler/icons-react"
const DriverLicense = ({ license, handleDeleteLicense, handleEditLicense }: {
license: IDriverLicense,
handleDeleteLicense: (id: number) => void,
handleEditLicense: (license: IDriverLicense) => void
}) => {
return (
<>
<Table.Td>{license.series_number}</Table.Td>
<Table.Td>{dateToDDMMYYYY(license.form_date)}</Table.Td>
<Table.Td>{dateToDDMMYYYY(license.to_date)}</Table.Td>
<Table.Td>{license.categories?.map((c) => c.name_short).join(", ")}</Table.Td>
<Table.Td>
<ActionIcon color="blue" onClick={() => handleEditLicense(license)} style={{ marginRight: "10px" }}>
<IconEdit />
</ActionIcon>
<ActionIcon color="red" onClick={() => handleDeleteLicense(license.id)}>
<IconTrash />
</ActionIcon>
</Table.Td>
</>
)
}
export default DriverLicense

View File

@ -27,4 +27,19 @@ export interface IDriverLicenseCategory {
id: number id: number
name: string name: string
name_short: string name_short: string
}
export interface IDriverConnection {
driver_id: number
driver_license_id: number
}
export interface IDriverLicenseConnection {
id: number
}
export interface IDriverLicenseCategoryConnection {
id: number
driver_license_id: number
driver_license_category_id: number
} }

View File

@ -1,103 +1,13 @@
import 'dayjs/locale/ru' import 'dayjs/locale/ru'
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { Button, TextInput, Table, ActionIcon, Modal, Checkbox, Tabs, Flex, Transition, Select, MultiSelect } from "@mantine/core" import { Button, TextInput, Table, Modal, Checkbox, Tabs, Flex, Select, MultiSelect } from "@mantine/core"
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { IconEdit, IconLinkPlus, IconPlus, IconTrash } from "@tabler/icons-react"
import { fetchDictionary, createDictionaryItem, updateDictionaryItem, deleteDictionaryItem } from "../api/api" import { fetchDictionary, createDictionaryItem, updateDictionaryItem, deleteDictionaryItem } from "../api/api"
import MaskedDateInput from '../components/MaskedDateInput' import MaskedDateInput from '../components/MaskedDateInput'
import { IDriver, IDriverLicense, IDriverLicenseCategory } from '../interfaces/Driver' import { IDriver, IDriverLicense, IDriverLicenseCategory } from '../interfaces/Driver'
import { dateToDDMMYYYY, dateToYYYYMMDD } from '../utils/date' import { dateToYYYYMMDD } from '../utils/date'
import { stringToSnils } from '../utils/format' import DriverLicense from '../components/Forms/DriverLicense'
import Driver from '../components/Forms/Driver'
const DriverLicense = ({ license, handleDeleteLicense, handleEditLicense }: {
license: IDriverLicense,
handleDeleteLicense: (id: number) => void,
handleEditLicense: (license: IDriverLicense) => void
}) => {
return (
<>
<Table.Td>{license.series_number}</Table.Td>
<Table.Td>{dateToDDMMYYYY(license.form_date)}</Table.Td>
<Table.Td>{dateToDDMMYYYY(license.to_date)}</Table.Td>
<Table.Td>{license.categories?.map((c) => c.name_short).join(", ")}</Table.Td>
<Table.Td>
<ActionIcon color="blue" onClick={() => handleEditLicense(license)} style={{ marginRight: "10px" }}>
<IconEdit />
</ActionIcon>
<ActionIcon color="red" onClick={() => handleDeleteLicense(license.id)}>
<IconTrash />
</ActionIcon>
</Table.Td>
</>
)
}
interface IDriverProps {
driver: IDriver,
handleEditDriver: (driver: IDriver) => void,
handleDeleteDriver: (id: number) => void,
handleEditLicense: (license: IDriverLicense) => void,
handleDeleteLicense: (id: number) => void
}
const Driver = ({ driver, handleEditDriver, handleDeleteDriver, handleEditLicense, handleDeleteLicense }: IDriverProps) => {
const [opened, setOpened] = useState(false)
return (
<>
<Table.Tr style={{ cursor: 'pointer' }} onClick={() => setOpened(!opened)}>
<Table.Td>{driver.fullname}</Table.Td>
<Table.Td>{stringToSnils(driver.snils)}</Table.Td>
<Table.Td>{dateToDDMMYYYY(driver.birthday)}</Table.Td>
<Table.Td>{driver.iin}</Table.Td>
<Table.Td>
<ActionIcon color="blue" onClick={(e) => { e.stopPropagation(); handleEditDriver(driver); }} style={{ marginRight: "10px" }}>
<IconEdit />
</ActionIcon>
<ActionIcon color="red" onClick={(e) => { e.stopPropagation(); handleDeleteDriver(driver.id); }}>
<IconTrash />
</ActionIcon>
</Table.Td>
</Table.Tr>
<Transition
mounted={opened}
transition={{
in: { opacity: 1, transform: 'scaleY(1)' },
out: { opacity: 0, transform: 'scaleY(0)' },
common: { transformOrigin: 'top' },
transitionProperty: 'transform, opacity',
}}
duration={200}
timingFunction='ease'
keepMounted
>
{(transitionStyle) => (
<Table.Tr style={{ ...transitionStyle }}>
<Table.Td colSpan={5}>
<Flex justify='space-evenly'>
<Button leftSection={<IconPlus />} onClick={() => handleEditLicense({} as IDriverLicense)}>
Добавить водительские права
</Button>
<Button leftSection={<IconLinkPlus />}>Привязать существующие права</Button>
</Flex>
</Table.Td>
</Table.Tr>
)}
</Transition>
{opened && driver.license && Array.isArray(driver.license) && driver.license.map((license: IDriverLicense) => (
<Table.Tr>
<DriverLicense
key={license.id}
license={license}
handleDeleteLicense={handleDeleteLicense}
handleEditLicense={handleEditLicense}
/>
</Table.Tr>
))}
</>
)
}
const DriverForm = () => { const DriverForm = () => {
const [drivers, setDrivers] = useState<IDriver[]>([]) const [drivers, setDrivers] = useState<IDriver[]>([])
@ -181,97 +91,97 @@ const DriverForm = () => {
setModalOpened(true) setModalOpened(true)
} }
const handleAddLicense = async (values: any) => { const handleAddLicense = async (values: any) => {
try { try {
const licenseData = { const licenseData = {
series_number: values.series_number, series_number: values.series_number,
form_date: dateToYYYYMMDD(values.form_date), form_date: dateToYYYYMMDD(values.form_date),
to_date: dateToYYYYMMDD(values.to_date), to_date: dateToYYYYMMDD(values.to_date),
is_actual: values.is_actual, is_actual: values.is_actual,
}; };
if (selectedLicense) { if (selectedLicense) {
// 1. Обновляем данные прав // 1. Обновляем данные прав
const updatedLicense = await updateDictionaryItem("driver_license", selectedLicense.id, licenseData); const updatedLicense = await updateDictionaryItem("driver_license", selectedLicense.id, licenseData);
// 2. Для редактирования просто создаем новые связи (старые должны удаляться клиентом) // 2. Для редактирования просто создаем новые связи (старые должны удаляться клиентом)
await Promise.all( await Promise.all(
values.categories.map((categoryId: string) => values.categories.map((categoryId: string) =>
createDictionaryItem("driver_license_connection", { createDictionaryItem("driver_license_connection", {
driver_license_id: selectedLicense.id, driver_license_id: selectedLicense.id,
driver_license_category_id: parseInt(categoryId), driver_license_category_id: parseInt(categoryId),
}) })
) )
); );
// 3. Обновляем связь с водителем // 3. Обновляем связь с водителем
await createDictionaryItem("driver_connection", { await createDictionaryItem("driver_connection", {
driver_id: parseInt(values.driverId), driver_id: parseInt(values.driverId),
driver_license_id: selectedLicense.id, driver_license_id: selectedLicense.id,
}); });
} else { } else {
// 1. Создаем новые права // 1. Создаем новые права
const newLicense = await createDictionaryItem("driver_license", licenseData); const newLicense = await createDictionaryItem("driver_license", licenseData);
// 2. Создаем связи категорий // 2. Создаем связи категорий
await Promise.all( await Promise.all(
values.categories.map((categoryId: string) => values.categories.map((categoryId: string) =>
createDictionaryItem("driver_license_connection", { createDictionaryItem("driver_license_connection", {
driver_license_id: newLicense.id, driver_license_id: newLicense.id,
driver_license_category_id: parseInt(categoryId), driver_license_category_id: parseInt(categoryId),
}) })
) )
); );
// 3. Создаем связь с водителем // 3. Создаем связь с водителем
await createDictionaryItem("driver_connection", { await createDictionaryItem("driver_connection", {
driver_id: parseInt(values.driverId), driver_id: parseInt(values.driverId),
driver_license_id: newLicense.id, driver_license_id: newLicense.id,
}); });
}
// Обновляем данные
const updatedDrivers = await fetchDictionary("driver");
setDrivers(updatedDrivers);
setLicenseModalOpened(false);
licenseForm.reset();
setSelectedLicense(null);
} catch (error) {
console.error("Error saving license:", error);
} }
};
// Обновляем данные const handleEditLicense = (license: IDriverLicense) => {
const updatedDrivers = await fetchDictionary("driver"); // Находим водителя по имеющимся данным (без запроса к API)
setDrivers(updatedDrivers); const driverWithLicense = drivers.find(d =>
d.license?.some((l: IDriverLicense) => l.id === license.id)
);
setLicenseModalOpened(false); licenseForm.setValues({
licenseForm.reset(); series_number: license.series_number,
setSelectedLicense(null); form_date: license.form_date,
} catch (error) { to_date: license.to_date,
console.error("Error saving license:", error); is_actual: license.is_actual,
} categories: license.categories?.map(c => c.id.toString()) || [],
}; driverId: driverWithLicense?.id.toString() || "",
});
setSelectedLicense(license);
setLicenseModalOpened(true);
};
const handleEditLicense = (license: IDriverLicense) => { const handleDeleteLicense = async (licenseId: number) => {
// Находим водителя по имеющимся данным (без запроса к API) try {
const driverWithLicense = drivers.find(d => // Удаляем только сами права (связи должны удаляться автоматически на сервере)
d.license?.some((l: IDriverLicense) => l.id === license.id) await deleteDictionaryItem("driver_license", licenseId);
);
licenseForm.setValues({ // Обновляем данные
series_number: license.series_number, const updatedDrivers = await fetchDictionary("driver");
form_date: license.form_date, setDrivers(updatedDrivers);
to_date: license.to_date, } catch (error) {
is_actual: license.is_actual, console.error("Error deleting license:", error);
categories: license.categories?.map(c => c.id.toString()) || [], }
driverId: driverWithLicense?.id.toString() || "", };
});
setSelectedLicense(license);
setLicenseModalOpened(true);
};
const handleDeleteLicense = async (licenseId: number) => {
try {
// Удаляем только сами права (связи должны удаляться автоматически на сервере)
await deleteDictionaryItem("driver_license", licenseId);
// Обновляем данные
const updatedDrivers = await fetchDictionary("driver");
setDrivers(updatedDrivers);
} catch (error) {
console.error("Error deleting license:", error);
}
};
return ( return (
<div> <div>
@ -297,11 +207,11 @@ const handleDeleteLicense = async (licenseId: number) => {
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{drivers.map((driver) => ( {drivers.map((driver, index) => (
<Driver <Driver
key={driver.id} key={`drvId-${driver.id}-${index}`}
driver={driver} driver={driver}
handleEditDriver={handleEditDriver} handleEditDriver={handleEditDriver}
handleDeleteDriver={handleDeleteDriver} handleDeleteDriver={handleDeleteDriver}
handleEditLicense={handleEditLicense} handleEditLicense={handleEditLicense}
handleDeleteLicense={handleDeleteLicense} handleDeleteLicense={handleDeleteLicense}
@ -328,12 +238,12 @@ const handleDeleteLicense = async (licenseId: number) => {
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{drivers.flatMap(driver => {drivers.flatMap(driver =>
driver.license?.map((license: IDriverLicense) => ( driver.license?.map((license: IDriverLicense, index) => (
<Table.Tr key={license.id}> <Table.Tr key={`licId-${license.id}-${index}`}>
<Table.Td>{driver.fullname}</Table.Td> <Table.Td>{driver.fullname}</Table.Td>
<DriverLicense <DriverLicense
license={license} license={license}
handleDeleteLicense={handleDeleteLicense} handleDeleteLicense={handleDeleteLicense}
handleEditLicense={handleEditLicense} handleEditLicense={handleEditLicense}
/> />
@ -362,13 +272,13 @@ const handleDeleteLicense = async (licenseId: number) => {
</form> </form>
</Modal> </Modal>
<Modal <Modal
opened={licenseModalOpened} opened={licenseModalOpened}
onClose={() => { onClose={() => {
setLicenseModalOpened(false) setLicenseModalOpened(false)
licenseForm.reset() licenseForm.reset()
setSelectedLicense(null) setSelectedLicense(null)
}} }}
title={selectedLicense ? "Редактировать водительские права" : "Добавить водительские права"} title={selectedLicense ? "Редактировать водительские права" : "Добавить водительские права"}
size="md" size="md"
> >
@ -383,10 +293,10 @@ const handleDeleteLicense = async (licenseId: number) => {
}))} }))}
required required
/> />
<TextInput <TextInput
label="Серия и номер" label="Серия и номер"
{...licenseForm.getInputProps("series_number")} {...licenseForm.getInputProps("series_number")}
required required
mt="sm" mt="sm"
/> />
<Flex gap="sm" mt="sm" justify="center"> <Flex gap="sm" mt="sm" justify="center">
@ -401,9 +311,9 @@ const handleDeleteLicense = async (licenseId: number) => {
{...licenseForm.getInputProps("to_date")} {...licenseForm.getInputProps("to_date")}
/> />
</Flex> </Flex>
<Checkbox <Checkbox
label="Действующее" label="Действующее"
{...licenseForm.getInputProps("is_actual", { type: 'checkbox' })} {...licenseForm.getInputProps("is_actual", { type: 'checkbox' })}
mt="sm" mt="sm"
/> />
<MultiSelect <MultiSelect