233 lines
7.7 KiB
TypeScript
233 lines
7.7 KiB
TypeScript
import {useState, useEffect} from 'react'
|
||
import {Stack, Typography, Button, MenuItem, Select, FormControl, InputLabel, Grid, Box, Accordion, AccordionSummary, AccordionDetails, useTheme, TextField} 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 ExtraFieldConfig = {
|
||
type: 'number'
|
||
label: string
|
||
minValue: number
|
||
maxValue: (linkedItems: any[]) => number
|
||
}
|
||
|
||
type LinkedItemsProps<T> = {
|
||
parentId: string | number
|
||
parentResource: string
|
||
childResource: string
|
||
fields: Field<T>[]
|
||
title: string
|
||
type: 'show' | 'edit'
|
||
extraField?: ExtraFieldConfig
|
||
}
|
||
|
||
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 | null>(null)
|
||
const [pageNum, setPageNum] = useState<number>(1)
|
||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||
const [mediaOrder, setMediaOrder] = useState<number>(1)
|
||
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])
|
||
|
||
useEffect(() => {
|
||
if (childResource === 'article' && parentResource === 'sight') {
|
||
setPageNum(linkedItems.length + 1)
|
||
}
|
||
}, [linkedItems, childResource, parentResource])
|
||
|
||
const availableItems = items.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||
|
||
const linkItem = () => {
|
||
if (selectedItemId !== null) {
|
||
const requestData =
|
||
childResource === 'article'
|
||
? {
|
||
[`${childResource}_id`]: selectedItemId,
|
||
page_num: pageNum,
|
||
}
|
||
: childResource === 'media'
|
||
? {
|
||
[`${childResource}_id`]: selectedItemId,
|
||
media_order: mediaOrder,
|
||
}
|
||
: {
|
||
[`${childResource}_id`]: selectedItemId,
|
||
}
|
||
|
||
axios
|
||
.post(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`, requestData)
|
||
.then(() => {
|
||
axios.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`).then((response) => {
|
||
setLinkedItems(response?.data || [])
|
||
setSelectedItemId(null)
|
||
if (childResource === 'article') {
|
||
setPageNum(pageNum + 1)
|
||
}
|
||
})
|
||
})
|
||
.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>
|
||
<AccordionSummary
|
||
expandIcon={<ExpandMoreIcon />}
|
||
sx={{
|
||
background: theme.palette.background.paper,
|
||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||
}}
|
||
>
|
||
<Typography variant="subtitle1" fontWeight="bold">
|
||
Привязанные {title}
|
||
</Typography>
|
||
</AccordionSummary>
|
||
|
||
<AccordionDetails sx={{background: theme.palette.background.paper}}>
|
||
<Stack gap={2}>
|
||
<Grid container gap={1.25}>
|
||
{isLoading ? (
|
||
<Typography>Загрузка...</Typography>
|
||
) : linkedItems.length > 0 ? (
|
||
linkedItems.map((item, index) => (
|
||
<Box
|
||
key={index}
|
||
sx={{
|
||
marginTop: '8px',
|
||
padding: '14px',
|
||
borderRadius: 2,
|
||
border: `2px solid ${theme.palette.divider}`,
|
||
}}
|
||
>
|
||
<Stack gap={0.25}>
|
||
{fields.map(({label, data}) => (
|
||
<Typography variant="body2" color="textSecondary" key={String(data)}>
|
||
<strong>{label}:</strong> {item[data]}
|
||
</Typography>
|
||
))}
|
||
{type === 'edit' && (
|
||
<Button variant="outlined" color="error" size="small" onClick={() => deleteItem(item.id)} sx={{mt: 1.5}}>
|
||
Отвязать
|
||
</Button>
|
||
)}
|
||
</Stack>
|
||
</Box>
|
||
))
|
||
) : (
|
||
<Typography>{title} не найдены</Typography>
|
||
)}
|
||
</Grid>
|
||
|
||
{type === 'edit' && (
|
||
<Stack gap={2}>
|
||
<Typography variant="subtitle1">Добавить {title}</Typography>
|
||
<FormControl fullWidth>
|
||
<InputLabel>Выберите {title}</InputLabel>
|
||
<Select value={selectedItemId || ''} onChange={(e) => setSelectedItemId(e.target.value as number)} label={`Выберите ${title}`}>
|
||
{availableItems.map((item) => (
|
||
<MenuItem key={item.id} value={item.id}>
|
||
{/* {fields.map((field) => item[field.data]).join(' - ')} */}
|
||
{item[fields[0].data]}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
</FormControl>
|
||
|
||
{childResource === 'article' && (
|
||
<FormControl fullWidth>
|
||
<TextField
|
||
type="number"
|
||
label="Номер страницы"
|
||
name="page_num"
|
||
value={pageNum}
|
||
onChange={(e) => {
|
||
const newValue = Number(e.target.value)
|
||
const minValue = linkedItems.length + 1 // page number on articles lenght
|
||
setPageNum(newValue < minValue ? minValue : newValue)
|
||
}}
|
||
fullWidth
|
||
InputLabelProps={{shrink: true}}
|
||
/>
|
||
</FormControl>
|
||
)}
|
||
|
||
{childResource === 'media' && type === 'edit' && (
|
||
<FormControl fullWidth>
|
||
<TextField
|
||
type="number"
|
||
label="Порядок отображения медиа"
|
||
value={mediaOrder}
|
||
onChange={(e) => {
|
||
const newValue = Number(e.target.value)
|
||
const maxValue = linkedItems.length + 1
|
||
const value = Math.max(1, Math.min(newValue, maxValue))
|
||
setMediaOrder(value)
|
||
}}
|
||
fullWidth
|
||
InputLabelProps={{shrink: true}}
|
||
/>
|
||
</FormControl>
|
||
)}
|
||
|
||
<Button variant="contained" onClick={linkItem} disabled={!selectedItemId}>
|
||
Добавить
|
||
</Button>
|
||
</Stack>
|
||
)}
|
||
</Stack>
|
||
</AccordionDetails>
|
||
</Accordion>
|
||
)
|
||
}
|