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

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

View File

@ -19,4 +19,8 @@ export const updateDictionaryItem = async (directory: string, id: number, data:
export const deleteDictionaryItem = async (directory: string, id: number) => {
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
to_date: string
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 {

View File

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