init LinkedItems
component for show, edit
pages
This commit is contained in:
163
src/components/LinkedItems.tsx
Normal file
163
src/components/LinkedItems.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
import {useState, useEffect} from 'react'
|
||||
import {Stack, Typography, Button, MenuItem, Select, FormControl, InputLabel, Grid, Box, Accordion, AccordionSummary, AccordionDetails, useTheme} from '@mui/material'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import axios from 'axios'
|
||||
import {BACKEND_URL} from '../lib/constants'
|
||||
|
||||
type Field<T> = {
|
||||
label: string
|
||||
data: keyof T
|
||||
}
|
||||
|
||||
type LinkedItemsProps<T> = {
|
||||
parentId: string | number
|
||||
parentResource: string
|
||||
childResource: string
|
||||
fields: Field<T>[]
|
||||
title: string
|
||||
type: 'show' | 'edit'
|
||||
}
|
||||
|
||||
export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentId, parentResource, childResource, fields, title, type}: LinkedItemsProps<T>) => {
|
||||
const [items, setItems] = useState<T[]>([])
|
||||
const [linkedItems, setLinkedItems] = useState<T[]>([])
|
||||
const [selectedItemId, setSelectedItemId] = useState<number | ''>('')
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
if (parentId) {
|
||||
axios
|
||||
.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`)
|
||||
.then((response) => {
|
||||
setLinkedItems(response?.data || [])
|
||||
})
|
||||
.catch(() => {
|
||||
setLinkedItems([])
|
||||
})
|
||||
}
|
||||
}, [parentId, parentResource, childResource])
|
||||
|
||||
useEffect(() => {
|
||||
if (type === 'edit') {
|
||||
axios
|
||||
.get(`${BACKEND_URL}/${childResource}/`)
|
||||
.then((response) => {
|
||||
setItems(response?.data || [])
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch(() => {
|
||||
setItems([])
|
||||
setIsLoading(false)
|
||||
})
|
||||
} else {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [childResource, type])
|
||||
|
||||
const availableItems = items.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||||
|
||||
const linkItem = () => {
|
||||
if (selectedItemId) {
|
||||
axios
|
||||
.post(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`, {
|
||||
[`${childResource}_id`]: selectedItemId,
|
||||
})
|
||||
.then(() => {
|
||||
axios.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`).then((response) => {
|
||||
setLinkedItems(response?.data || [])
|
||||
setSelectedItemId('')
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error linking item:', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const deleteItem = (itemId: number) => {
|
||||
axios
|
||||
.delete(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`, {
|
||||
data: {[`${childResource}_id`]: itemId},
|
||||
})
|
||||
.then(() => {
|
||||
setLinkedItems((prev) => prev.filter((item) => item.id !== itemId))
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error unlinking item:', error)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
sx={{
|
||||
background: theme.palette.background.paper,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight="bold">
|
||||
{type === 'show' ? `Привязанные ${title}` : title}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
|
||||
<AccordionDetails sx={{background: theme.palette.background.paper}}>
|
||||
<Stack gap={2}>
|
||||
<Grid container gap={2}>
|
||||
{isLoading ? (
|
||||
<Typography>Загрузка...</Typography>
|
||||
) : linkedItems.length > 0 ? (
|
||||
linkedItems.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
marginBottom: '8px',
|
||||
padding: '14px',
|
||||
borderRadius: 2,
|
||||
border: `2px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Stack gap={0.5}>
|
||||
{fields.map(({label, data}) => (
|
||||
<Typography key={String(data)}>
|
||||
<strong>{label}:</strong> {item[data]}
|
||||
</Typography>
|
||||
))}
|
||||
{type === 'edit' && (
|
||||
<Button variant="outlined" color="error" onClick={() => deleteItem(item.id)} sx={{mt: 1.5}}>
|
||||
Отвязать
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Typography>{title} не найдены</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{type === 'edit' && (
|
||||
<>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{title}</InputLabel>
|
||||
|
||||
<Select value={selectedItemId} onChange={(e) => setSelectedItemId(Number(e.target.value))} label={title} fullWidth>
|
||||
{availableItems.map((item) => (
|
||||
<MenuItem key={item.id} value={item.id}>
|
||||
{item[fields[0].data]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Button variant="contained" onClick={linkItem} disabled={!selectedItemId}>
|
||||
Добавить
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user