integrate LinkedItems into /route pages

This commit is contained in:
maxim 2025-03-19 16:19:23 +03:00
parent 9cb939deac
commit 451e1da308
4 changed files with 62 additions and 302 deletions

View File

@ -104,7 +104,7 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
<AccordionDetails sx={{background: theme.palette.background.paper}}> <AccordionDetails sx={{background: theme.palette.background.paper}}>
<Stack gap={2}> <Stack gap={2}>
<Grid container gap={2}> <Grid container gap={1.25}>
{isLoading ? ( {isLoading ? (
<Typography>Загрузка...</Typography> <Typography>Загрузка...</Typography>
) : linkedItems.length > 0 ? ( ) : linkedItems.length > 0 ? (
@ -112,20 +112,20 @@ export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentI
<Box <Box
key={index} key={index}
sx={{ sx={{
marginBottom: '8px', marginTop: '8px',
padding: '14px', padding: '14px',
borderRadius: 2, borderRadius: 2,
border: `2px solid ${theme.palette.divider}`, border: `2px solid ${theme.palette.divider}`,
}} }}
> >
<Stack gap={0.5}> <Stack gap={0.25}>
{fields.map(({label, data}) => ( {fields.map(({label, data}) => (
<Typography key={String(data)}> <Typography variant="body2" color="textSecondary" key={String(data)}>
<strong>{label}:</strong> {item[data]} <strong>{label}:</strong> {item[data]}
</Typography> </Typography>
))} ))}
{type === 'edit' && ( {type === 'edit' && (
<Button variant="outlined" color="error" onClick={() => deleteItem(item.id)} sx={{mt: 1.5}}> <Button variant="outlined" color="error" size="small" onClick={() => deleteItem(item.id)} sx={{mt: 1.5}}>
Отвязать Отвязать
</Button> </Button>
)} )}

View File

@ -2,6 +2,9 @@ import {Autocomplete, Box, TextField, FormControlLabel, Checkbox, Typography} fr
import {Edit, useAutocomplete} from '@refinedev/mui' import {Edit, useAutocomplete} from '@refinedev/mui'
import {useForm} from '@refinedev/react-hook-form' import {useForm} from '@refinedev/react-hook-form'
import {Controller} from 'react-hook-form' import {Controller} from 'react-hook-form'
import {useParams} from 'react-router'
import {LinkedItems} from '../../components/LinkedItems'
import {StationItem, VehicleItem, stationFields, vehicleFields} from './types'
export const RouteEdit = () => { export const RouteEdit = () => {
const { const {
@ -11,6 +14,8 @@ export const RouteEdit = () => {
formState: {errors}, formState: {errors},
} = useForm({}) } = useForm({})
const {id: routeId} = useParams<{id: string}>()
const {autocompleteProps: carrierAutocompleteProps} = useAutocomplete({ const {autocompleteProps: carrierAutocompleteProps} = useAutocomplete({
resource: 'carrier', resource: 'carrier',
}) })
@ -107,6 +112,14 @@ export const RouteEdit = () => {
)} )}
/> />
</Box> </Box>
{routeId && (
<>
<LinkedItems<StationItem> type="edit" parentId={routeId} parentResource="route" childResource="station" fields={stationFields} title="станции" />
<LinkedItems<VehicleItem> type="edit" parentId={routeId} parentResource="route" childResource="vehicle" fields={vehicleFields} title="транспортные средства" />
</>
)}
</Edit> </Edit>
) )
} }

View File

@ -1,182 +1,14 @@
import {Stack, Typography, Box, Grid2 as Grid, Button, MenuItem, Select, FormControl, InputLabel} from '@mui/material' import {Stack, Typography, Box} from '@mui/material'
import {useShow} from '@refinedev/core' import {useShow} from '@refinedev/core'
import {Show, TextFieldComponent as TextField} from '@refinedev/mui' import {Show, TextFieldComponent as TextField} from '@refinedev/mui'
import {useEffect, useState} from 'react' import {LinkedItems} from '../../components/LinkedItems'
import axios from 'axios' import {StationItem, VehicleItem, stationFields, vehicleFields} from './types'
import {BACKEND_URL, VEHICLE_TYPES} from '../../lib/constants'
type StationItem = {
id: number
name: string
description: string
[key: string]: string | number
}
type VehicleItem = {
id: number
tail_number: number
type: number
[key: string]: string | number
}
export const RouteShow = () => { export const RouteShow = () => {
const {query} = useShow({}) const {query} = useShow({})
const {data, isLoading} = query const {data, isLoading} = query
const record = data?.data const record = data?.data
// Station states
const [stations, setStations] = useState<StationItem[]>([])
const [linkedStations, setLinkedStations] = useState<StationItem[]>([])
const [selectedStationId, setSelectedStationId] = useState<number | ''>('')
const [stationsLoading, setStationsLoading] = useState<boolean>(true)
// Vehicle states
const [vehicles, setVehicles] = useState<VehicleItem[]>([])
const [linkedVehicles, setLinkedVehicles] = useState<VehicleItem[]>([])
const [selectedVehicleId, setSelectedVehicleId] = useState<number | ''>('')
const [vehiclesLoading, setVehiclesLoading] = useState<boolean>(true)
useEffect(() => {
if (record?.id) {
axios
.get(`${BACKEND_URL}/route/${record.id}/station`)
.then((response) => {
setLinkedStations(response?.data || [])
})
.catch(() => {
setLinkedStations([])
})
}
}, [record?.id])
useEffect(() => {
axios
.get(`${BACKEND_URL}/station/`)
.then((response) => {
setStations(response?.data || [])
setStationsLoading(false)
})
.catch(() => {
setStations([])
setStationsLoading(false)
})
}, [])
const availableStations = stations.filter((station) => !linkedStations.some((linked) => linked.id === station.id))
const linkStation = () => {
if (selectedStationId) {
axios
.post(
`${BACKEND_URL}/route/${record?.id}/station`,
{station_id: selectedStationId},
{
headers: {
accept: 'application/json',
'Content-Type': 'application/json',
},
},
)
.then(() => {
axios
.get(`${BACKEND_URL}/route/${record?.id}/station`)
.then((response) => {
setLinkedStations(response?.data || [])
})
.catch(() => {
setLinkedStations([])
})
})
.catch((error) => {
console.error('Error linking station:', error)
})
}
}
const deleteStation = (stationId: number) => {
axios
.delete(`${BACKEND_URL}/route/${record?.id}/station`, {
data: {station_id: stationId},
})
.then(() => {
setLinkedStations((prevStations) => prevStations.filter((item) => item.id !== stationId))
})
.catch((error) => {
console.error('Error deleting station:', error)
})
}
// Vehicle effects
useEffect(() => {
if (record?.id) {
axios
.get(`${BACKEND_URL}/route/${record.id}/vehicle`)
.then((response) => {
setLinkedVehicles(response?.data || [])
})
.catch(() => {
setLinkedVehicles([])
})
}
}, [record?.id])
useEffect(() => {
axios
.get(`${BACKEND_URL}/vehicle/`)
.then((response) => {
setVehicles(response?.data || [])
setVehiclesLoading(false)
})
.catch(() => {
setVehicles([])
setVehiclesLoading(false)
})
}, [])
const availableVehicles = vehicles.filter((vehicle) => !linkedVehicles.some((linked) => linked.id === vehicle.id))
const linkVehicle = () => {
if (selectedVehicleId) {
axios
.post(
`${BACKEND_URL}/route/${record?.id}/vehicle`,
{vehicle_id: selectedVehicleId},
{
headers: {
accept: 'application/json',
'Content-Type': 'application/json',
},
},
)
.then(() => {
axios
.get(`${BACKEND_URL}/route/${record?.id}/vehicle`)
.then((response) => {
setLinkedVehicles(response?.data || [])
})
.catch(() => {
setLinkedVehicles([])
})
})
.catch((error) => {
console.error('Error linking vehicle:', error)
})
}
}
const deleteVehicle = (vehicleId: number) => {
axios
.delete(`${BACKEND_URL}/route/${record?.id}/vehicle`, {
data: {vehicle_id: vehicleId},
})
.then(() => {
setLinkedVehicles((prevVehicles) => prevVehicles.filter((item) => item.id !== vehicleId))
})
.catch((error) => {
console.error('Error deleting vehicle:', error)
})
}
const fields = [ const fields = [
{label: 'Перевозчик', data: 'carrier'}, {label: 'Перевозчик', data: 'carrier'},
{label: 'Номер маршрута', data: 'route_number'}, {label: 'Номер маршрута', data: 'route_number'},
@ -204,16 +36,6 @@ export const RouteShow = () => {
}, },
] ]
const stationFields: Array<{label: string; data: keyof StationItem}> = [
{label: 'Название', data: 'name'},
{label: 'Описание', data: 'description'},
]
const vehicleFields: Array<{label: string; data: keyof VehicleItem}> = [
{label: 'Бортовой номер', data: 'tail_number'},
{label: 'Тип', data: 'type'},
]
return ( return (
<Show isLoading={isLoading}> <Show isLoading={isLoading}>
<Stack gap={4}> <Stack gap={4}>
@ -226,123 +48,13 @@ export const RouteShow = () => {
</Stack> </Stack>
))} ))}
<Stack gap={2}> {record?.id && (
<Typography variant="body1" fontWeight="bold"> <>
Привязанные станции <LinkedItems<StationItem> type="show" parentId={record.id} parentResource="route" childResource="station" fields={stationFields} title="станции" />
</Typography>
<Grid container gap={2}> <LinkedItems<VehicleItem> type="show" parentId={record.id} parentResource="route" childResource="vehicle" fields={vehicleFields} title="транспортные средства" />
{stationsLoading ? ( </>
<Typography>Загрузка станций...</Typography> )}
) : linkedStations.length > 0 ? (
linkedStations.map((station, index) => (
<Box
key={index}
sx={{
marginBottom: '8px',
padding: '14px',
borderRadius: 2,
border: (theme) => `2px solid ${theme.palette.divider}`,
}}
>
<Stack gap={0.5}>
{stationFields.map(({label, data}) => (
<Typography key={data}>
<strong>{label}:</strong> {station[data]}
</Typography>
))}
<Button variant="outlined" color="error" onClick={() => deleteStation(station?.id)} sx={{mt: 1.5}}>
Отвязать
</Button>
</Stack>
</Box>
))
) : (
<Typography>Станции не найдены</Typography>
)}
</Grid>
<Stack gap={2}>
<Typography variant="body1" fontWeight="bold">
Привязать станцию
</Typography>
<FormControl fullWidth>
<InputLabel>Станция</InputLabel>
<Select value={selectedStationId} onChange={(e) => setSelectedStationId(Number(e.target.value))} label="Станция" fullWidth>
{availableStations.map((station) => (
<MenuItem key={station.id} value={station.id}>
{station.name}
</MenuItem>
))}
</Select>
</FormControl>
<Button variant="contained" onClick={linkStation} disabled={!selectedStationId}>
Привязать
</Button>
</Stack>
</Stack>
<Stack gap={2}>
<Typography variant="body1" fontWeight="bold">
Привязанные транспортные средства
</Typography>
<Grid container gap={2}>
{vehiclesLoading ? (
<Typography>Загрузка транспорта...</Typography>
) : linkedVehicles.length > 0 ? (
linkedVehicles.map((vehicle, index) => (
<Box
key={index}
sx={{
marginBottom: '8px',
padding: '14px',
borderRadius: 2,
border: (theme) => `2px solid ${theme.palette.divider}`,
}}
>
<Stack gap={0.5}>
{vehicleFields.map(({label, data}) => (
<Typography key={data}>
<strong>{label}:</strong> {vehicle[data]}
</Typography>
))}
<Button variant="outlined" color="error" onClick={() => deleteVehicle(vehicle?.id)} sx={{mt: 1.5}}>
Отвязать
</Button>
</Stack>
</Box>
))
) : (
<Typography>Транспортные средства не найдены</Typography>
)}
</Grid>
<Stack gap={2}>
<Typography variant="body1" fontWeight="bold">
Привязать транспортное средство
</Typography>
<FormControl fullWidth>
<InputLabel>Транспортное средство</InputLabel>
<Select value={selectedVehicleId} onChange={(e) => setSelectedVehicleId(Number(e.target.value))} label="Транспортное средство" fullWidth>
{availableVehicles.map((vehicle) => (
<MenuItem key={vehicle.id} value={vehicle.id}>
{`${vehicle.tail_number} (${VEHICLE_TYPES.find((type) => type.value === vehicle.type)?.label || vehicle.type})`}
</MenuItem>
))}
</Select>
</FormControl>
<Button variant="contained" onClick={linkVehicle} disabled={!selectedVehicleId}>
Привязать
</Button>
</Stack>
</Stack>
</Stack> </Stack>
</Show> </Show>
) )

35
src/pages/route/types.ts Normal file
View File

@ -0,0 +1,35 @@
import {VEHICLE_TYPES} from '../../lib/constants'
export type StationItem = {
id: number
name: string
description: string
[key: string]: string | number
}
export type VehicleItem = {
id: number
tail_number: number
type: number
[key: string]: string | number
}
export type FieldType<T> = {
label: string
data: keyof T
render?: (value: any) => React.ReactNode
}
export const stationFields: Array<FieldType<StationItem>> = [
{label: 'Название', data: 'name'},
{label: 'Описание', data: 'description'},
]
export const vehicleFields: Array<FieldType<VehicleItem>> = [
{label: 'Бортовой номер', data: 'tail_number'},
{
label: 'Тип',
data: 'type',
render: (value: number) => VEHICLE_TYPES.find((type) => type.value === value)?.label || value,
},
]