Files
WhiteNightsAdminPanel/src/components/LinkedItems.tsx
2025-03-19 18:05:29 +03:00

200 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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 [pageNum, setPageNum] = useState<number>(1)
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])
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) {
const requestData =
childResource === 'article'
? {
[`${childResource}_id`]: selectedItemId,
page_num: pageNum,
}
: {
[`${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('')
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 defaultExpanded>
<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(Number(e.target.value))} label={`Выберите ${title}`} fullWidth>
{availableItems.map((item) => (
<MenuItem key={item.id} value={item.id}>
{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>
)}
<Button variant="contained" onClick={linkItem} disabled={!selectedItemId}>
Добавить
</Button>
</Stack>
)}
</Stack>
</AccordionDetails>
</Accordion>
)
}