@ -0,0 +1,660 @@
import React , { useState , useEffect , useCallback , useRef } from 'react' ;
import {
Table ,
Button ,
Box ,
Title ,
Group ,
LoadingOverlay ,
Alert ,
Modal ,
TextInput ,
SimpleGrid ,
ActionIcon ,
Tooltip ,
Text ,
Badge ,
MultiSelect ,
InputBase ,
Popover ,
Collapse ,
Center ,
MantineProvider ,
} from '@mantine/core' ;
import { DatePicker } from '@mantine/dates' ;
import { useForm } from '@mantine/form' ;
import { useDisclosure } from '@mantine/hooks' ;
import { IconAlertCircle , IconPencil , IconTrash , IconPlus , IconListDetails , IconCalendar , IconChevronDown , IconChevronUp } from '@tabler/icons-react' ;
import { stringToSnils } from '../utils/format'
// --- ИНТЕРФЕЙСЫ ---
export interface IDriver {
id : number ;
fullname : string ;
snils : string ;
birthday : string ;
iin : string ;
license? : IDriverLicense [ ] ;
}
export interface IDriverLicense {
id : number ;
series_number : string ;
form_date : string ;
to_date : string ;
is_actual : boolean ;
driver_connection_id? : number ;
categories? : ICategoryWithConnection [ ] ;
}
export interface ICategoryWithConnection {
id : number ;
name : string ;
name_short : string ;
driver_license_connection_id? : number ;
}
export interface IDriverLicenseCategory {
id : number ;
name : string ;
name_short : string ;
}
// --- ФУНКЦИИ ДЛЯ РАБОТЫ С API ---
const API_BASE_URL = "https://api.jkhsakha.ru/is/fuel" ;
const handleApiResponse = async ( response : Response ) = > {
if ( ! response . ok ) {
const errorData = await response . json ( ) . catch ( ( ) = > ( { message : response.statusText } ) ) ;
throw new Error ( errorData . detail || errorData . message || 'Ошибка запроса к API' ) ;
}
if ( response . status === 204 ) {
return ;
}
return response . json ( ) ;
}
const fetchDictionary = async ( directory : string ) = > {
const response = await fetch ( ` ${ API_BASE_URL } / ${ directory } /?limit=1000 ` ) ;
return handleApiResponse ( response ) ;
} ;
const createDictionaryItem = async ( directory : string , data : any ) = > {
const response = await fetch ( ` ${ API_BASE_URL } / ${ directory } / ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( data ) ,
} ) ;
return handleApiResponse ( response ) ;
} ;
const updateDictionaryItem = async ( directory : string , id : number , data : any ) = > {
const response = await fetch ( ` ${ API_BASE_URL } / ${ directory } / ${ id } / ` , {
method : 'PATCH' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( data ) ,
} ) ;
return handleApiResponse ( response ) ;
} ;
const deleteDictionaryItem = async ( directory : string , id : number ) = > {
const response = await fetch ( ` ${ API_BASE_URL } / ${ directory } / ${ id } / ` , {
method : 'DELETE' ,
} ) ;
return handleApiResponse ( response ) ;
} ;
// --- УТИЛИТЫ ДЛЯ РАБОТЫ С ДАТАМИ ---
function dateToYYYYMMDD ( date : Date | null ) : string {
if ( ! date ) return "" ;
const year = date . getFullYear ( ) ;
const month = String ( date . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ;
const day = String ( date . getDate ( ) ) . padStart ( 2 , '0' ) ;
return ` ${ year } - ${ month } - ${ day } ` ;
}
function formatToDDMMYYYY ( date : Date ) : string {
if ( ! date ) return '' ;
const day = String ( date . getDate ( ) ) . padStart ( 2 , '0' ) ;
const month = String ( date . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ;
const year = date . getFullYear ( ) ;
return ` ${ day } . ${ month } . ${ year } ` ;
}
function parseYYYYMMDD ( dateString : string ) : Date | null {
if ( ! dateString || ! /^\d{4}-\d{2}-\d{2}$/ . test ( dateString ) ) {
return null ;
}
const [ year , month , day ] = dateString . split ( '-' ) . map ( Number ) ;
const date = new Date ( Date . UTC ( year , month - 1 , day ) ) ;
if ( isNaN ( date . getTime ( ) ) ) {
return null ;
}
return date ;
}
function dateToDDMMYYYY ( dateString : string ) : string {
if ( ! dateString ) return '' ;
try {
const [ year , month , day ] = dateString . split ( '-' ) ;
if ( year && month && day ) {
return ` ${ day } . ${ month } . ${ year } ` ;
}
return '' ;
} catch ( error ) {
return '' ;
}
}
// --- КОМПОНЕНТ МАСКИРОВАННОГО ВВОДА ДАТЫ ---
interface MaskedDateInputProps {
value? : Date | null
onChange : ( value : Date | null ) = > void
label? : string
required? : boolean
placeholder? : string
error? : React.ReactNode
maxDate? : Date
}
function parseInputDDMMYYYY ( input : string ) : Date | null {
const [ dd , mm , yyyy ] = input . split ( "." )
const day = parseInt ( dd , 10 )
const month = parseInt ( mm , 10 ) - 1
const year = parseInt ( yyyy , 10 )
if ( isNaN ( day ) || isNaN ( month ) || isNaN ( year ) ) return null ;
const date = new Date ( year , month , day ) ;
if ( date . getFullYear ( ) === year && date . getMonth ( ) === month && date . getDate ( ) === day ) {
return date ;
}
return null ;
}
const MaskedDateInput = ( { value , onChange , label , required , placeholder = "ДД.MM.ГГГГ" , error , maxDate } : MaskedDateInputProps ) = > {
const [ inputValue , setInputValue ] = useState ( value ? formatToDDMMYYYY ( value ) : "" )
const [ opened , setOpened ] = useState ( false )
const inputRef = useRef < HTMLInputElement > ( null )
useEffect ( ( ) = > {
if ( value ) {
const formatted = formatToDDMMYYYY ( value ) ;
if ( formatted !== inputValue ) {
setInputValue ( formatted )
}
} else {
setInputValue ( '' )
}
} , [ value ] )
const handleChange = ( e : React.ChangeEvent < HTMLInputElement > ) = > {
const raw = e . target . value ;
const digits = raw . replace ( /\D/g , '' ) . slice ( 0 , 8 ) ;
let formatted = digits ;
if ( digits . length > 4 ) {
formatted = ` ${ digits . slice ( 0 , 2 ) } . ${ digits . slice ( 2 , 4 ) } . ${ digits . slice ( 4 ) } ` ;
} else if ( digits . length > 2 ) {
formatted = ` ${ digits . slice ( 0 , 2 ) } . ${ digits . slice ( 2 ) } ` ;
}
setInputValue ( formatted ) ;
if ( digits . length === 8 ) {
const parsed = parseInputDDMMYYYY ( formatted ) ;
if ( parsed ) onChange ( parsed ) ;
}
} ;
const handleDatePick = ( date : Date | null ) = > {
if ( ! date ) return ;
setInputValue ( formatToDDMMYYYY ( date ) ) ;
onChange ( date ) ;
setOpened ( false ) ;
} ;
return (
< InputBase
label = { label }
required = { required }
component = "input"
type = "text"
ref = { inputRef }
value = { inputValue }
onChange = { handleChange }
placeholder = { placeholder }
error = { error }
rightSection = {
< Popover opened = { opened } trapFocus onChange = { setOpened } position = "bottom" shadow = "md" width = "auto" >
< Popover.Target >
< ActionIcon variant = "transparent" onClick = { ( ) = > setOpened ( ( o ) = > ! o ) } >
< IconCalendar size = { 16 } / >
< / ActionIcon >
< / Popover.Target >
< Popover.Dropdown >
< DatePicker locale = "ru" value = { value } onChange = { handleDatePick } defaultLevel = "month" maxDate = { maxDate } / >
< / Popover.Dropdown >
< / Popover >
}
/ >
)
}
// --- ГЛАВНЫЙ КОМПОНЕНТ СТРАНИЦЫ ---
const DriversPageContent = ( ) = > {
const [ drivers , setDrivers ] = useState < IDriver [ ] > ( [ ] ) ;
const [ allCategories , setAllCategories ] = useState < IDriverLicenseCategory [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ error , setError ] = useState < string | null > ( null ) ;
const [ expandedDriverId , setExpandedDriverId ] = useState < number | null > ( null ) ;
const [ modalOpened , { open : openModal , close : closeModal } ] = useDisclosure ( false ) ;
const [ licenseModalOpened , { open : openLicenseModal , close : closeLicenseModal } ] = useDisclosure ( false ) ;
const [ confirmModalOpened , { open : openConfirmModal , close : closeConfirmModal } ] = useDisclosure ( false ) ;
const [ isEditing , setIsEditing ] = useState ( false ) ;
const [ selectedDriver , setSelectedDriver ] = useState < IDriver | null > ( null ) ;
const [ driverForNewLicense , setDriverForNewLicense ] = useState < IDriver | null > ( null ) ;
const [ itemToDelete , setItemToDelete ] = useState < { type : 'driver' | 'license' ; driverId : number ; licenseId? : number } | null > ( null ) ;
const fetchData = useCallback ( async ( ) = > {
setLoading ( true ) ;
setError ( null ) ;
try {
const [ driversData , categoriesData ] = await Promise . all ( [
fetchDictionary ( 'driver' ) ,
fetchDictionary ( 'driver_license_category' )
] ) ;
setDrivers ( driversData . results || driversData ) ;
setAllCategories ( categoriesData . results || categoriesData ) ;
} catch ( err ) {
console . error ( 'Ошибка загрузки данных:' , err ) ;
setError ( 'Н е удалось загрузить данные. Пожалуйста, попробуйте снова.' ) ;
} finally {
setLoading ( false ) ;
}
} , [ ] ) ;
useEffect ( ( ) = > {
fetchData ( ) ;
} , [ fetchData ] ) ;
const handleOpenAddModal = ( ) = > {
setIsEditing ( false ) ;
setSelectedDriver ( null ) ;
openModal ( ) ;
} ;
const handleOpenEditModal = ( driver : IDriver ) = > {
setIsEditing ( true ) ;
setSelectedDriver ( driver ) ;
openModal ( ) ;
} ;
const handleOpenAddLicenseModal = ( driver : IDriver ) = > {
setDriverForNewLicense ( driver ) ;
openLicenseModal ( ) ;
} ;
const handleDeleteClick = ( type : 'driver' | 'license' , driverId : number , licenseId? : number ) = > {
setItemToDelete ( { type , driverId , licenseId } ) ;
openConfirmModal ( ) ;
} ;
const handleConfirmDelete = async ( ) = > {
if ( ! itemToDelete ) return ;
setLoading ( true ) ;
try {
if ( itemToDelete . type === 'driver' ) {
const driverToDelete = drivers . find ( d = > d . id === itemToDelete . driverId ) ;
if ( driverToDelete ? . license ) {
for ( const lic of driverToDelete . license ) {
await performLicenseDeletion ( lic , driverToDelete . id ) ;
}
}
await deleteDictionaryItem ( 'driver' , itemToDelete . driverId ) ;
} else if ( itemToDelete . type === 'license' && itemToDelete . licenseId ) {
const driver = drivers . find ( d = > d . id === itemToDelete . driverId ) ;
const license = driver ? . license ? . find ( l = > l . id === itemToDelete . licenseId ) ;
if ( license && driver ) {
await performLicenseDeletion ( license , driver . id ) ;
}
}
await fetchData ( ) ;
} catch ( err : any ) {
console . error ( ` Ошибка удаления ${ itemToDelete . type } : ` , err ) ;
setError ( err . message || ` Н е удалось удалить элемент.` ) ;
} finally {
closeConfirmModal ( ) ;
setItemToDelete ( null ) ;
setLoading ( false ) ;
}
} ;
const performLicenseDeletion = async ( license : IDriverLicense , driverId : number ) = > {
if ( license . categories ) {
for ( const cat of license . categories ) {
if ( cat . driver_license_connection_id ) {
await deleteDictionaryItem ( 'driver_license_connection' , cat . driver_license_connection_id ) ;
}
}
}
const driver = drivers . find ( d = > d . id === driverId ) ;
const lic = driver ? . license ? . find ( l = > l . id === license . id ) ;
if ( lic ? . driver_connection_id ) {
await deleteDictionaryItem ( 'driver_connection' , lic . driver_connection_id ) ;
}
await deleteDictionaryItem ( 'driver_license' , license . id ) ;
} ;
const rows = drivers . map ( ( driver ) = > {
const isExpanded = expandedDriverId === driver . id ;
return (
< React.Fragment key = { driver . id } >
< tr >
< td >
< Group spacing = "xs" >
< ActionIcon onClick = { ( ) = > setExpandedDriverId ( isExpanded ? null : driver . id ) } >
{ isExpanded ? < IconChevronUp size = { 16 } / > : < IconChevronDown size = { 16 } / > }
< / ActionIcon >
< Text size = "sm" weight = { 500 } > { driver . fullname } < / Text >
< / Group >
< / td >
< td > { stringToSnils ( driver . snils ) } < / td >
< td > { dateToDDMMYYYY ( driver . birthday ) } < / td >
< td > { driver . iin } < / td >
< td >
< Group spacing = "xs" noWrap >
< Tooltip label = "Редактировать" >
< ActionIcon color = "blue" onClick = { ( ) = > handleOpenEditModal ( driver ) } >
< IconPencil size = { 16 } / >
< / ActionIcon >
< / Tooltip >
< Tooltip label = "Удалить" >
< ActionIcon color = "red" onClick = { ( ) = > handleDeleteClick ( 'driver' , driver . id ) } >
< IconTrash size = { 16 } / >
< / ActionIcon >
< / Tooltip >
< / Group >
< / td >
< / tr >
< tr >
< td colSpan = { 5 } style = { { padding : 0 , border : 0 } } >
< Collapse in = { isExpanded } >
< Box p = "md" sx = { ( theme ) = > ( { backgroundColor : theme.colors.gray [ 0 ] } ) } >
{ driver . license && driver . license . length > 0 ? (
driver . license . map ( lic = > (
< Box key = { lic . id } p = "xs" mb = "xs" sx = { ( theme ) = > ( { border : ` 1px solid ${ theme . colors . gray [ 3 ] } ` , borderRadius : theme.radius.sm , backgroundColor : theme.white } ) } >
< Group position = "apart" >
< div >
< Text size = "sm" > < b > В У : < / b > { lic . series_number } < / Text >
< Text size = "xs" > < b > Д а т ы : < / b > { dateToDDMMYYYY ( lic . form_date ) } - { dateToDDMMYYYY ( lic . to_date ) } < / Text >
< Group spacing = "xs" mt = { 4 } >
{ lic . categories ? . map ( cat = > < Badge key = { cat . id } > { cat . name_short } < / Badge > ) }
< / Group >
< / div >
< ActionIcon color = "red" onClick = { ( ) = > handleDeleteClick ( 'license' , driver . id , lic . id ) } >
< IconTrash size = { 16 } / >
< / ActionIcon >
< / Group >
< / Box >
) )
) : (
< Text size = "sm" color = "dimmed" > В о д и т е л ь с к и е у д о с т о в е р е н и я о т с у т с т в у ю т . < / Text >
) }
< Group position = "right" mt = "sm" >
< Button variant = "light" size = "xs" leftIcon = { < IconPlus size = { 14 } / > } onClick = { ( ) = > handleOpenAddLicenseModal ( driver ) } >
Д о б а в и т ь В У
< / Button >
< / Group >
< / Box >
< / Collapse >
< / td >
< / tr >
< / React.Fragment >
)
} ) ;
return (
< Box sx = { { position : 'relative' } } >
< LoadingOverlay visible = { loading } overlayBlur = { 2 } / >
< Group position = "apart" mb = "xl" >
< Title order = { 2 } > В о д и т е л и < / Title >
< Button leftIcon = { < IconPlus size = { 16 } / > } onClick = { handleOpenAddModal } >
Д о б а в и т ь в о д и т е л я
< / Button >
< / Group >
{ error && < Alert icon = { < IconAlertCircle size = "1rem" / > } title = "Ошибка!" color = "red" mb = "lg" withCloseButton onClose = { ( ) = > setError ( null ) } > { error } < / Alert > }
< Box sx = { ( theme ) = > ( { border : ` 1px solid ${ theme . colors . gray [ 3 ] } ` , borderRadius : theme.radius.sm , overflow : 'hidden' } ) } >
< Table striped highlightOnHover verticalSpacing = "sm" >
< thead >
< tr >
< th > Ф И О < / th >
< th > С Н И Л С < / th >
< th > Д а т а р о ж д е н и я < / th >
< th > И Н Н < / th >
< th > Д е й с т в и я < / th >
< / tr >
< / thead >
< tbody >
{ rows . length > 0 ? rows : (
< tr >
< td colSpan = { 5 } >
< Center p = "xl" > < Text color = "dimmed" > В о д и т е л и н е н а й д е н ы . < / Text > < / Center >
< / td >
< / tr >
) }
< / tbody >
< / Table >
< / Box >
< DriverFormModal opened = { modalOpened } onClose = { closeModal } driver = { selectedDriver } isEditing = { isEditing } onSuccess = { fetchData } / >
< AddLicenseFormModal opened = { licenseModalOpened } onClose = { closeLicenseModal } driver = { driverForNewLicense } allCategories = { allCategories } onSuccess = { fetchData } / >
< Modal opened = { confirmModalOpened } onClose = { closeConfirmModal } title = "Подтвердите удаление" centered size = "sm" >
< Text size = "sm" > В ы у в е р е н ы , ч т о х о т и т е у д а л и т ь э т о т э л е м е н т ? Э т о д е й с т в и е н е о б р а т и м о . < / Text >
< Group position = "right" mt = "xl" >
< Button variant = "default" onClick = { closeConfirmModal } > О т м е н а < / Button >
< Button color = "red" onClick = { handleConfirmDelete } > У д а л и т ь < / Button >
< / Group >
< / Modal >
< / Box >
) ;
} ;
// --- КОМПОНЕНТ МОДАЛЬНОЙ ФОРМЫ (РЕДАКТИРОВАНИЕ/ДОБАВЛЕНИЕ ВОДИТЕЛЯ) ---
interface DriverFormModalProps {
opened : boolean ;
onClose : ( ) = > void ;
driver : IDriver | null ;
isEditing : boolean ;
onSuccess : ( ) = > void ;
}
const DriverFormModal = ( { opened , onClose , driver , isEditing , onSuccess } : DriverFormModalProps ) = > {
const [ loading , setLoading ] = useState ( false ) ;
const [ formError , setFormError ] = useState < string | null > ( null ) ;
const form = useForm ( {
initialValues : {
fullname : '' ,
snils : '' ,
birthday : null as Date | null ,
iin : '' ,
} ,
validate : {
fullname : ( value ) = > ( value . trim ( ) . length < 5 ? 'ФИО должно содержать минимум 5 символов' : null ) ,
snils : ( value ) = > ( /^\d{11}$/ . test ( value ) ? null : 'СНИЛС должен состоять из 11 цифр' ) ,
iin : ( value ) = > ( /^\d{12}$/ . test ( value ) ? null : 'ИНН должен состоять из 12 цифр' ) ,
birthday : ( value ) = > ( value ? null : 'Укажите дату рождения' ) ,
} ,
} ) ;
useEffect ( ( ) = > {
if ( opened ) {
if ( isEditing && driver ) {
form . setValues ( {
fullname : driver.fullname ,
snils : driver.snils ,
birthday : parseYYYYMMDD ( driver . birthday ) ,
iin : driver.iin ,
} ) ;
} else {
form . reset ( ) ;
}
}
} , [ isEditing , driver , opened ] ) ;
const handleSubmit = async ( values : typeof form . values ) = > {
setLoading ( true ) ;
setFormError ( null ) ;
try {
const driverPayload = {
fullname : values.fullname ,
snils : values.snils ,
iin : values.iin ,
birthday : dateToYYYYMMDD ( values . birthday ) ,
is_actual : true ,
} ;
if ( isEditing && driver ) {
await updateDictionaryItem ( 'driver' , driver . id , driverPayload ) ;
} else {
await createDictionaryItem ( 'driver' , driverPayload ) ;
}
onSuccess ( ) ;
onClose ( ) ;
} catch ( err : any ) {
console . error ( 'Ошибка сохранения:' , err ) ;
setFormError ( err . message || 'Произошла неизвестная ошибка.' ) ;
} finally {
setLoading ( false ) ;
}
} ;
return (
< Modal opened = { opened } onClose = { onClose } title = { isEditing ? "Редактировать водителя" : "Добавить водителя" } size = "lg" centered >
< Box component = "form" onSubmit = { form . onSubmit ( handleSubmit ) } sx = { { position : 'relative' } } >
< LoadingOverlay visible = { loading } / >
{ formError && < Alert color = "red" mb = "md" > { formError } < / Alert > }
< Title order = { 4 } mb = "md" > Д а н н ы е в о д и т е л я < / Title >
< SimpleGrid cols = { 2 } breakpoints = { [ { maxWidth : 'sm' , cols : 1 } ] } >
< TextInput label = "ФИО" required { ...form.getInputProps ( 'fullname' ) } / >
< TextInput label = "СНИЛС" required { ...form.getInputProps ( 'snils' ) } maxLength = { 11 } / >
< MaskedDateInput label = "Дата рождения" required value = { form . values . birthday } onChange = { ( date ) = > form . setFieldValue ( 'birthday' , date ) } error = { form . errors . birthday } maxDate = { new Date ( ) } / >
< TextInput label = "ИНН" required { ...form.getInputProps ( 'iin' ) } maxLength = { 12 } / >
< / SimpleGrid >
< Group position = "right" mt = "xl" >
< Button variant = "default" onClick = { onClose } > О т м е н а < / Button >
< Button type = "submit" > { isEditing ? 'Сохранить изменения' : 'Добавить водителя' } < / Button >
< / Group >
< / Box >
< / Modal >
) ;
} ;
// --- КОМПОНЕНТ МОДАЛЬНОЙ ФОРМЫ (ДОБАВЛЕНИЕ В У ) ---
interface AddLicenseFormModalProps {
opened : boolean ;
onClose : ( ) = > void ;
driver : IDriver | null ;
allCategories : IDriverLicenseCategory [ ] ;
onSuccess : ( ) = > void ;
}
const AddLicenseFormModal = ( { opened , onClose , driver , allCategories , onSuccess } : AddLicenseFormModalProps ) = > {
const [ loading , setLoading ] = useState ( false ) ;
const [ formError , setFormError ] = useState < string | null > ( null ) ;
const form = useForm ( {
initialValues : {
series_number : '' ,
form_date : null as Date | null ,
to_date : null as Date | null ,
categories : [ ] as string [ ] ,
} ,
validate : {
series_number : ( value ) = > ( value . trim ( ) . length < 6 ? 'Укажите серию и номер В У ' : null ) ,
form_date : ( value ) = > ( value ? null : 'Укажите дату выдачи' ) ,
to_date : ( value ) = > ( value ? null : 'Укажите срок действия' ) ,
categories : ( value ) = > ( value . length === 0 ? 'Выберите хотя бы одну категорию' : null ) ,
} ,
} ) ;
useEffect ( ( ) = > {
if ( ! opened ) {
form . reset ( ) ;
setFormError ( null ) ;
}
} , [ opened ] ) ;
const handleSubmit = async ( values : typeof form . values ) = > {
if ( ! driver ) return ;
setLoading ( true ) ;
setFormError ( null ) ;
try {
const licensePayload = {
series_number : values.series_number ,
form_date : dateToYYYYMMDD ( values . form_date ) ,
to_date : dateToYYYYMMDD ( values . to_date ) ,
is_actual : true ,
} ;
const newLicense = await createDictionaryItem ( 'driver_license' , licensePayload ) ;
await createDictionaryItem ( 'driver_connection' , {
driver_id : driver.id ,
driver_license_id : newLicense.id ,
} ) ;
for ( const categoryId of values . categories ) {
await createDictionaryItem ( 'driver_license_connection' , {
driver_license_id : newLicense.id ,
driver_license_category_id : parseInt ( categoryId , 10 ) ,
} ) ;
}
onSuccess ( ) ;
onClose ( ) ;
} catch ( err : any ) {
console . error ( 'Ошибка добавления В У :' , err ) ;
setFormError ( err . message || 'Произошла неизвестная ошибка при добавлении В У .' ) ;
} finally {
setLoading ( false ) ;
}
} ;
const categoryOptions = allCategories . map ( cat = > ( {
value : String ( cat . id ) ,
label : ` ${ cat . name_short } ( ${ cat . name } ) ` ,
} ) ) ;
return (
< Modal opened = { opened } onClose = { onClose } title = { ` Добавить В У для: ${ driver ? . fullname || '' } ` } centered >
< Box component = "form" onSubmit = { form . onSubmit ( handleSubmit ) } sx = { { position : 'relative' } } >
< LoadingOverlay visible = { loading } / >
{ formError && < Alert color = "red" mb = "md" > { formError } < / Alert > }
< SimpleGrid cols = { 1 } spacing = "md" >
< TextInput label = "Серия и номер" required { ...form.getInputProps ( 'series_number' ) } / >
< MultiSelect label = "Категории" placeholder = "Выберите категории" data = { categoryOptions } searchable required { ...form.getInputProps ( 'categories' ) } / >
< MaskedDateInput label = "Дата выдачи" required value = { form . values . form_date } onChange = { ( date ) = > form . setFieldValue ( 'form_date' , date ) } error = { form . errors . form_date } maxDate = { new Date ( ) } / >
< MaskedDateInput label = "Срок действия до" required value = { form . values . to_date } onChange = { ( date ) = > form . setFieldValue ( 'to_date' , date ) } error = { form . errors . to_date } / >
< / SimpleGrid >
< Group position = "right" mt = "xl" >
< Button variant = "default" onClick = { onClose } > О т м е н а < / Button >
< Button type = "submit" > Д о б а в и т ь В У < / Button >
< / Group >
< / Box >
< / Modal >
) ;
} ;
// --- КОМПОНЕНТ-ОБЕРТКА ДЛЯ СТИЛЕЙ ---
const App = ( ) = > (
< MantineProvider withGlobalStyles withNormalizeCSS >
< DriversPageContent / >
< / MantineProvider >
) ;
export default App ;