форма добавления водительских прав

This commit is contained in:
3 changed files with 262 additions and 118 deletions

View File

@ -20,3 +20,7 @@ export const updateDictionaryItem = async (directory: string, id: number, data:
export const deleteDictionaryItem = async (directory: string, id: number) => { export const deleteDictionaryItem = async (directory: string, id: number) => {
await axios.delete(`${API_BASE_URL}/${directory}/${id}`); await axios.delete(`${API_BASE_URL}/${directory}/${id}`);
}; };
export const deleteDictionaryItemByName = async (directory: string, id: number) => {
await axios.delete(`${API_BASE_URL}/${directory}/id_license=${id}`);
};

View File

@ -12,7 +12,14 @@ export interface IDriverLicense {
form_date: string form_date: string
to_date: string to_date: string
is_actual: boolean is_actual: boolean
categories: IDriverLicenseCategory[] connectionId?: number; // ID связи с водителем
categories?: Array<{
id: number;
name: string;
name_short: string;
connectionId: number; // ID связи с категорией
}>
driverId: number
} }
export interface IDriverLicenseCategory { export interface IDriverLicenseCategory {

View File

@ -1,31 +1,46 @@
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 } from "@mantine/core" import { Button, TextInput, Table, ActionIcon, Modal, Checkbox, Tabs, Flex, Transition, 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 { 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 } from '../interfaces/Driver' import { IDriver, IDriverLicense, IDriverLicenseCategory } from '../interfaces/Driver'
import { dateToDDMMYYYY, dateToYYYYMMDD } from '../utils/date' import { dateToDDMMYYYY, dateToYYYYMMDD } from '../utils/date'
import { stringToSnils } from '../utils/format' import { stringToSnils } from '../utils/format'
const DriverLicense = ({ license, handleDeleteLicense }: { license: IDriverLicense, handleDeleteLicense: any }) => { const DriverLicense = ({ license, handleDeleteLicense, handleEditLicense }: {
license: IDriverLicense,
handleDeleteLicense: (id: number) => void,
handleEditLicense: (license: IDriverLicense) => void
}) => {
return ( return (
<Table.Tr> <>
<Table.Td>{license.series_number}</Table.Td> <Table.Td>{license.series_number}</Table.Td>
<Table.Td>{license.form_date}</Table.Td> <Table.Td>{dateToDDMMYYYY(license.form_date)}</Table.Td>
<Table.Td>{license.to_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>{license.categories?.map((c) => c.name_short).join(", ")}</Table.Td>
<Table.Td> <Table.Td>
<ActionIcon color="blue" onClick={() => handleEditLicense(license)} style={{ marginRight: "10px" }}>
<IconEdit />
</ActionIcon>
<ActionIcon color="red" onClick={() => handleDeleteLicense(license.id)}> <ActionIcon color="red" onClick={() => handleDeleteLicense(license.id)}>
<IconTrash /> <IconTrash />
</ActionIcon> </ActionIcon>
</Table.Td> </Table.Td>
</Table.Tr> </>
) )
} }
const Driver = ({ driver, handleEditDriver, handleDeleteDriver }: { driver: any, handleEditDriver: any, handleDeleteDriver: any }) => { 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) const [opened, setOpened] = useState(false)
return ( return (
@ -36,10 +51,10 @@ const Driver = ({ driver, handleEditDriver, handleDeleteDriver }: { driver: any,
<Table.Td>{dateToDDMMYYYY(driver.birthday)}</Table.Td> <Table.Td>{dateToDDMMYYYY(driver.birthday)}</Table.Td>
<Table.Td>{driver.iin}</Table.Td> <Table.Td>{driver.iin}</Table.Td>
<Table.Td> <Table.Td>
<ActionIcon color="blue" onClick={() => handleEditDriver(driver)} style={{ marginRight: "10px" }}> <ActionIcon color="blue" onClick={(e) => { e.stopPropagation(); handleEditDriver(driver); }} style={{ marginRight: "10px" }}>
<IconEdit /> <IconEdit />
</ActionIcon> </ActionIcon>
<ActionIcon color="red" onClick={() => handleDeleteDriver(driver.id)}> <ActionIcon color="red" onClick={(e) => { e.stopPropagation(); handleDeleteDriver(driver.id); }}>
<IconTrash /> <IconTrash />
</ActionIcon> </ActionIcon>
</Table.Td> </Table.Td>
@ -61,17 +76,23 @@ const Driver = ({ driver, handleEditDriver, handleDeleteDriver }: { driver: any,
<Table.Tr style={{ ...transitionStyle }}> <Table.Tr style={{ ...transitionStyle }}>
<Table.Td colSpan={5}> <Table.Td colSpan={5}>
<Flex justify='space-evenly'> <Flex justify='space-evenly'>
<Button leftSection={<IconPlus />}>Добавить водительские права</Button> <Button leftSection={<IconPlus />} onClick={() => handleEditLicense({} as IDriverLicense)}>
<Button leftSection={<IconLinkPlus />}>Привязать водительские права</Button> Добавить водительские права
</Button>
<Button leftSection={<IconLinkPlus />}>Привязать существующие права</Button>
</Flex> </Flex>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
)} )}
</Transition> </Transition>
{opened && driver.license && Array.isArray(driver.license) && driver.license.map((license: IDriverLicense) => (
{driver.license && Array.isArray(driver.license) && driver.license.map((dr: IDriver) => (
<Table.Tr> <Table.Tr>
<Table.Td>{JSON.stringify(dr)}</Table.Td> <DriverLicense
key={license.id}
license={license}
handleDeleteLicense={handleDeleteLicense}
handleEditLicense={handleEditLicense}
/>
</Table.Tr> </Table.Tr>
))} ))}
</> </>
@ -79,23 +100,27 @@ const Driver = ({ driver, handleEditDriver, handleDeleteDriver }: { driver: any,
} }
const DriverForm = () => { const DriverForm = () => {
const [drivers, setDrivers] = useState<any[]>([]) const [drivers, setDrivers] = useState<IDriver[]>([])
const [licenseCategories, setLicenseCategories] = useState<{ value: string; label: string }[]>([]) const [licenseCategories, setLicenseCategories] = useState<IDriverLicenseCategory[]>([])
const [selectedDriver, setSelectedDriver] = useState<any | null>(null) const [selectedDriver, setSelectedDriver] = useState<IDriver | null>(null)
const [selectedLicense, setSelectedLicense] = useState<any | null>(null) const [selectedLicense, setSelectedLicense] = useState<IDriverLicense | null>(null)
const [modalOpened, setModalOpened] = useState(false) const [modalOpened, setModalOpened] = useState(false)
const [licenseModalOpened, setLicenseModalOpened] = useState(false) const [licenseModalOpened, setLicenseModalOpened] = useState(false)
useEffect(() => { useEffect(() => {
fetchDictionary("driver_license_category").then((data) => { const fetchData = async () => {
const options = data.map((item: any) => ({ try {
value: String(item.id), const [categories, driversData] = await Promise.all([
label: item.name_short, // Берем короткое название (например, "B", "C", "D") fetchDictionary("driver_license_category"),
})); fetchDictionary("driver")
setLicenseCategories(options); ]);
}); setLicenseCategories(categories)
setDrivers(driversData)
fetchDictionary("driver").then(setDrivers) } catch (error) {
console.error("Error fetching data:", error)
}
}
fetchData()
}, []) }, [])
const driverForm = useForm<Partial<IDriver>>({ const driverForm = useForm<Partial<IDriver>>({
@ -105,113 +130,163 @@ const DriverForm = () => {
birthday: "", birthday: "",
iin: "", iin: "",
}, },
}); })
const licenseForm = useForm({ const licenseForm = useForm({
initialValues: { initialValues: {
driverId: "",
series_number: "", series_number: "",
form_date: "", form_date: "",
to_date: "", to_date: "",
categories: [], is_actual: true,
frontPhoto: null, categories: [] as string[],
backPhoto: null, driverId: "",
}, },
}); })
const handleAddDriver = async (values: any) => {
const date = new Date(Date.parse(values.birthday))
const handleAddDriver = async (values: Partial<IDriver>) => {
try {
const date = new Date(Date.parse(values.birthday || ""))
const newValues = { const newValues = {
fullname: values.fullname, ...values,
snils: values.snils,
birthday: dateToYYYYMMDD(date), birthday: dateToYYYYMMDD(date),
iin: values.iin
} }
if (selectedDriver) { if (selectedDriver) {
const updatedDriver = await updateDictionaryItem("driver", selectedDriver.id, newValues); const updatedDriver = await updateDictionaryItem("driver", selectedDriver.id, newValues)
setDrivers((prev) => prev.map((d) => (d.id === selectedDriver.id ? updatedDriver : d))); setDrivers(prev => prev.map(d => d.id === selectedDriver.id ? updatedDriver : d))
} else { } else {
const newDriver = await createDictionaryItem("driver", newValues); const newDriver = await createDictionaryItem("driver", newValues)
setDrivers((prev) => [...prev, newDriver]); setDrivers(prev => [...prev, newDriver])
}
setModalOpened(false)
driverForm.reset()
setSelectedDriver(null)
} catch (error) {
console.error("Error saving driver:", error)
}
} }
setModalOpened(false);
driverForm.reset();
setSelectedDriver(null);
};
const handleDeleteDriver = async (id: number) => { const handleDeleteDriver = async (id: number) => {
try {
await deleteDictionaryItem("driver", id) await deleteDictionaryItem("driver", id)
setDrivers((prev) => prev.filter((driver) => driver.id !== id)) setDrivers(prev => prev.filter(driver => driver.id !== id))
}; } catch (error) {
console.error("Error deleting driver:", error)
}
}
const handleEditDriver = (driver: any) => { const handleEditDriver = (driver: IDriver) => {
driverForm.setValues(driver) driverForm.setValues(driver)
setSelectedDriver(driver) setSelectedDriver(driver)
setModalOpened(true) setModalOpened(true)
} }
const handleAddLicense = async (values: any) => { const handleAddLicense = async (values: any) => {
try {
const licenseData = {
series_number: values.series_number,
form_date: dateToYYYYMMDD(values.form_date),
to_date: dateToYYYYMMDD(values.to_date),
is_actual: values.is_actual,
};
if (selectedLicense) { if (selectedLicense) {
const updatedLicense = await updateDictionaryItem("driver_license", selectedLicense.id, values); // 1. Обновляем данные прав
setDrivers((prev) => const updatedLicense = await updateDictionaryItem("driver_license", selectedLicense.id, licenseData);
prev.map((driver) =>
driver.id === values.driverId // 2. Для редактирования просто создаем новые связи (старые должны удаляться клиентом)
? { await Promise.all(
...driver, values.categories.map((categoryId: string) =>
license: driver.license.map((lic: IDriverLicense) => (lic.id === selectedLicense.id ? updatedLicense : lic)), createDictionaryItem("driver_license_connection", {
} driver_license_id: selectedLicense.id,
: driver driver_license_category_id: parseInt(categoryId),
})
) )
); );
} 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) => // 3. Обновляем связь с водителем
prev.map((driver) => await createDictionaryItem("driver_connection", {
driver.id === values.driverId driver_id: parseInt(values.driverId),
? { ...driver, license: [...(driver.license || []), newLicense] } driver_license_id: selectedLicense.id,
: driver });
) } else {
// 1. Создаем новые права
const newLicense = await createDictionaryItem("driver_license", licenseData);
// 2. Создаем связи категорий
await Promise.all(
values.categories.map((categoryId: string) =>
createDictionaryItem("driver_license_connection", {
driver_license_id: newLicense.id,
driver_license_category_id: parseInt(categoryId),
})
) )
);
// 3. Создаем связь с водителем
await createDictionaryItem("driver_connection", {
driver_id: parseInt(values.driverId),
driver_license_id: newLicense.id,
});
} }
// Обновляем данные
const updatedDrivers = await fetchDictionary("driver");
setDrivers(updatedDrivers);
setLicenseModalOpened(false); setLicenseModalOpened(false);
licenseForm.reset(); licenseForm.reset();
setSelectedLicense(null); setSelectedLicense(null);
} catch (error) {
console.error("Error saving license:", error);
}
};
const handleEditLicense = (license: IDriverLicense) => {
// Находим водителя по имеющимся данным (без запроса к API)
const driverWithLicense = drivers.find(d =>
d.license?.some((l: IDriverLicense) => l.id === license.id)
);
licenseForm.setValues({
series_number: license.series_number,
form_date: license.form_date,
to_date: license.to_date,
is_actual: license.is_actual,
categories: license.categories?.map(c => c.id.toString()) || [],
driverId: driverWithLicense?.id.toString() || "",
});
setSelectedLicense(license);
setLicenseModalOpened(true);
}; };
const handleDeleteLicense = async (licenseId: number) => { const handleDeleteLicense = async (licenseId: number) => {
try {
// Удаляем только сами права (связи должны удаляться автоматически на сервере)
await deleteDictionaryItem("driver_license", licenseId); await deleteDictionaryItem("driver_license", licenseId);
setDrivers((prev) =>
prev.map((driver) => // Обновляем данные
({ ...driver, license: driver.license.filter((lic: IDriverLicense) => lic.id !== licenseId) }) const updatedDrivers = await fetchDictionary("driver");
) setDrivers(updatedDrivers);
); } catch (error) {
console.error("Error deleting license:", error);
}
}; };
return ( return (
<div> <div>
<Tabs defaultValue='drivers'> <Tabs defaultValue='drivers'>
<Tabs.List> <Tabs.List>
<Tabs.Tab value='drivers'> <Tabs.Tab value='drivers'>Водители</Tabs.Tab>
Водители <Tabs.Tab value='licenses'>Водительские права</Tabs.Tab>
</Tabs.Tab>
<Tabs.Tab value='licenses'>
Водительские права
</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel value='drivers'> <Tabs.Panel value='drivers'>
<h2>Водители</h2> <h2>Водители</h2>
<Button onClick={() => setModalOpened(true)}>Добавить водителя</Button> <Button onClick={() => { setSelectedDriver(null); driverForm.reset(); setModalOpened(true); }}>
<Table striped highlightOnHover withColumnBorders mt="md"> Добавить водителя
</Button>
<Table highlightOnHover withColumnBorders mt="md">
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
<Table.Th>ФИО</Table.Th> <Table.Th>ФИО</Table.Th>
@ -223,7 +298,14 @@ const DriverForm = () => {
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{drivers.map((driver) => ( {drivers.map((driver) => (
<Driver key={driver.id} driver={driver} handleEditDriver={handleEditDriver} handleDeleteDriver={handleDeleteDriver} /> <Driver
key={driver.id}
driver={driver}
handleEditDriver={handleEditDriver}
handleDeleteDriver={handleDeleteDriver}
handleEditLicense={handleEditLicense}
handleDeleteLicense={handleDeleteLicense}
/>
))} ))}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
@ -231,8 +313,10 @@ const DriverForm = () => {
<Tabs.Panel value='licenses'> <Tabs.Panel value='licenses'>
<h2>Водительские права</h2> <h2>Водительские права</h2>
<Button onClick={() => setLicenseModalOpened(true)}>Добавить права</Button> <Button onClick={() => { setSelectedLicense(null); licenseForm.reset(); setLicenseModalOpened(true); }}>
<Table striped highlightOnHover withColumnBorders mt="md"> Добавить права
</Button>
<Table highlightOnHover withColumnBorders mt="md">
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
<Table.Th>ФИО</Table.Th> <Table.Th>ФИО</Table.Th>
@ -244,9 +328,16 @@ const DriverForm = () => {
</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 => (
<DriverLicense license={license} handleDeleteLicense={handleDeleteLicense} /> <Table.Tr key={license.id}>
<Table.Td>{driver.fullname}</Table.Td>
<DriverLicense
license={license}
handleDeleteLicense={handleDeleteLicense}
handleEditLicense={handleEditLicense}
/>
</Table.Tr>
)) ))
)} )}
</Table.Tbody> </Table.Tbody>
@ -254,7 +345,7 @@ const DriverForm = () => {
</Tabs.Panel> </Tabs.Panel>
</Tabs> </Tabs>
<Modal opened={modalOpened} onClose={() => setModalOpened(false)} title="Добавить / Редактировать водителя"> <Modal opened={modalOpened} onClose={() => setModalOpened(false)} title={selectedDriver ? "Редактировать водителя" : "Добавить водителя"}>
<form onSubmit={driverForm.onSubmit(handleAddDriver)}> <form onSubmit={driverForm.onSubmit(handleAddDriver)}>
<TextInput label="ФИО" {...driverForm.getInputProps("fullname")} required /> <TextInput label="ФИО" {...driverForm.getInputProps("fullname")} required />
<TextInput label="СНИЛС" {...driverForm.getInputProps("snils")} required /> <TextInput label="СНИЛС" {...driverForm.getInputProps("snils")} required />
@ -271,27 +362,69 @@ const DriverForm = () => {
</form> </form>
</Modal> </Modal>
<Modal opened={licenseModalOpened} onClose={() => setLicenseModalOpened(false)} title="Добавить / Редактировать водительские права"> <Modal
opened={licenseModalOpened}
onClose={() => {
setLicenseModalOpened(false)
licenseForm.reset()
setSelectedLicense(null)
}}
title={selectedLicense ? "Редактировать водительские права" : "Добавить водительские права"}
size="md"
>
<form onSubmit={licenseForm.onSubmit(handleAddLicense)}> <form onSubmit={licenseForm.onSubmit(handleAddLicense)}>
<TextInput label="Серия и номер" {...licenseForm.getInputProps("series_number")} required /> <Select
label="Водитель"
value={licenseForm.values.driverId}
onChange={(value) => licenseForm.setFieldValue('driverId', value || '')}
data={drivers.map(driver => ({
value: driver.id.toString(),
label: driver.fullname,
}))}
required
/>
<TextInput
label="Серия и номер"
{...licenseForm.getInputProps("series_number")}
required
mt="sm"
/>
<Flex gap="sm" mt="sm" justify="center">
<MaskedDateInput <MaskedDateInput
label="Дата выдачи" label="Дата выдачи"
required required
flex={1}
{...licenseForm.getInputProps("form_date")} {...licenseForm.getInputProps("form_date")}
/> />
<MaskedDateInput <MaskedDateInput
label="Срок действия" label="Срок действия"
required required
flex={1}
{...licenseForm.getInputProps("to_date")} {...licenseForm.getInputProps("to_date")}
/> />
<Checkbox defaultChecked label="Действующее" {...licenseForm.getInputProps("is_actual")} style={{ marginTop: "10px" }} /> </Flex>
<Checkbox
label="Действующее"
{...licenseForm.getInputProps("is_actual", { type: 'checkbox' })}
mt="sm"
/>
<MultiSelect
label="Категории"
data={licenseCategories.map(category => ({
value: category.id.toString(),
label: `${category.name_short} (${category.name})`,
}))}
{...licenseForm.getInputProps("categories")}
required
mt="sm"
/>
<Button fullWidth mt="md" type="submit"> <Button fullWidth mt="md" type="submit">
Сохранить Сохранить
</Button> </Button>
</form> </form>
</Modal> </Modal>
</div> </div>
); )
}; }
export default DriverForm; export default DriverForm