diff --git a/src/components/LinkedItems.tsx b/src/components/LinkedItems.tsx new file mode 100644 index 0000000..af380db --- /dev/null +++ b/src/components/LinkedItems.tsx @@ -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 = { + label: string + data: keyof T +} + +type LinkedItemsProps = { + parentId: string | number + parentResource: string + childResource: string + fields: Field[] + title: string + type: 'show' | 'edit' +} + +export const LinkedItems = ({parentId, parentResource, childResource, fields, title, type}: LinkedItemsProps) => { + const [items, setItems] = useState([]) + const [linkedItems, setLinkedItems] = useState([]) + const [selectedItemId, setSelectedItemId] = useState('') + const [isLoading, setIsLoading] = useState(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 ( + + } + sx={{ + background: theme.palette.background.paper, + borderBottom: `1px solid ${theme.palette.divider}`, + }} + > + + {type === 'show' ? `Привязанные ${title}` : title} + + + + + + + {isLoading ? ( + Загрузка... + ) : linkedItems.length > 0 ? ( + linkedItems.map((item, index) => ( + + + {fields.map(({label, data}) => ( + + {label}: {item[data]} + + ))} + {type === 'edit' && ( + + )} + + + )) + ) : ( + {title} не найдены + )} + + + {type === 'edit' && ( + <> + + {title} + + + + + + + )} + + + + ) +} diff --git a/src/pages/station/edit.tsx b/src/pages/station/edit.tsx index dfaa150..c4d63c3 100644 --- a/src/pages/station/edit.tsx +++ b/src/pages/station/edit.tsx @@ -1,6 +1,9 @@ import {Box, TextField} from '@mui/material' import {Edit} from '@refinedev/mui' import {useForm} from '@refinedev/react-hook-form' +import {useParams} from 'react-router' +import {LinkedItems} from '../../components/LinkedItems' +import {type SightItem, sightFields} from './types' export const StationEdit = () => { const { @@ -9,6 +12,8 @@ export const StationEdit = () => { formState: {errors}, } = useForm({}) + const {id: stationId} = useParams<{id: string}>() + return ( @@ -67,6 +72,17 @@ export const StationEdit = () => { name="longitude" /> + + {stationId && ( + + type="edit" // display and manage + parentId={stationId} + parentResource="station" + childResource="sight" + fields={sightFields} + title="достопримечательности" + /> + )} ) } diff --git a/src/pages/station/show.tsx b/src/pages/station/show.tsx index 78a8960..2d1286c 100644 --- a/src/pages/station/show.tsx +++ b/src/pages/station/show.tsx @@ -1,123 +1,18 @@ -import {Stack, Typography, Box, Grid2 as Grid, Button, MenuItem, Select, FormControl, InputLabel} from '@mui/material' import {useShow} from '@refinedev/core' import {Show, TextFieldComponent as TextField} from '@refinedev/mui' - -import {useEffect, useState} from 'react' -import axios from 'axios' - -import {BACKEND_URL} from '../../lib/constants' - -type SightItem = { - id: number - name: string - latitude: number - longitude: number - city_id: number - city: string - [key: string]: string | number -} +import {Stack, Typography} from '@mui/material' +import {LinkedItems} from '../../components/LinkedItems' +import {type SightItem, sightFields, stationFields} from './types' export const StationShow = () => { const {query} = useShow({}) const {data, isLoading} = query const record = data?.data - const [sights, setSights] = useState([]) - const [linkedSights, setLinkedSights] = useState([]) - const [selectedSightId, setSelectedSightId] = useState('') - const [sightsLoading, setSightsLoading] = useState(true) - - useEffect(() => { - if (record?.id) { - axios - .get(`${BACKEND_URL}/station/${record.id}/sight`) - .then((response) => { - setLinkedSights(response?.data || []) - }) - .catch(() => { - setLinkedSights([]) - }) - } - }, [record?.id]) - - useEffect(() => { - axios - .get(`${BACKEND_URL}/sight/`) // without "/" throws CORS error - .then((response) => { - setSights(response?.data || []) - setSightsLoading(false) - }) - .catch(() => { - setSights([]) - setSightsLoading(false) - }) - }, []) - - const availableSights = sights.filter((sight) => !linkedSights.some((linked) => linked.id === sight.id)) - - const linkSight = () => { - if (selectedSightId) { - axios - .post( - `${BACKEND_URL}/station/${record?.id}/sight`, - {sight_id: selectedSightId}, - { - headers: { - accept: 'application/json', - 'Content-Type': 'application/json', - }, - }, - ) - .then(() => { - axios - .get(`${BACKEND_URL}/station/${record?.id}/sight`) - .then((response) => { - setLinkedSights(response?.data || []) - }) - .catch(() => { - setLinkedSights([]) - }) - }) - .catch((error) => { - console.error('Error linking sight:', error) - }) - } - } - - const deleteSight = (sightId: number) => { - axios - .delete(`${BACKEND_URL}/station/${record?.id}/sight`, { - data: {sight_id: sightId}, - }) - .then(() => { - setLinkedSights((prevSights) => prevSights.filter((item) => item.id !== sightId)) - }) - .catch((error) => { - console.error('Error deleting sight:', error) - }) - } - - const fields = [ - // {label: 'ID', data: 'id'}, - {label: 'Название', data: 'name'}, - // {label: 'Широта', data: 'latitude'}, #* - // {label: 'Долгота', data: 'longitude'}, #* - {label: 'Описание', data: 'description'}, - ] - - const sightFields: Array<{label: string; data: keyof SightItem}> = [ - // {label: 'ID', data: 'id'}, - {label: 'Название', data: 'name'}, - // {label: 'Широта', data: 'latitude'}, #* - // {label: 'Долгота', data: 'longitude'}, #* - // {label: 'ID города', data: 'city_id'}, - {label: 'Город', data: 'city'}, - ] - return ( - {fields.map(({label, data}) => ( + {stationFields.map(({label, data}) => ( {label} @@ -126,64 +21,16 @@ export const StationShow = () => { ))} - - - Привязанные достопримечательности - - - - {sightsLoading ? ( - Загрузка достопримечательностей... - ) : linkedSights.length > 0 ? ( - linkedSights.map((sight, index) => ( - `2px solid ${theme.palette.divider}`, - }} - > - - {sightFields.map(({label, data}) => ( - - {label}: {sight[data]} - - ))} - - - - - )) - ) : ( - Достопримечательности не найдены - )} - - - - - Привязать достопримечательность - - - - Достопримечательность - - - - - - + {record?.id && ( + + type="show" // only display + parentId={record.id} + parentResource="station" + childResource="sight" + fields={sightFields} + title="достопримечательности" + /> + )} ) diff --git a/src/pages/station/types.ts b/src/pages/station/types.ts new file mode 100644 index 0000000..ac1e19f --- /dev/null +++ b/src/pages/station/types.ts @@ -0,0 +1,43 @@ +import React from 'react' + +export type StationItem = { + id: number + name: string + description: string + latitude: number + longitude: number + [key: string]: string | number +} + +export type SightItem = { + id: number + name: string + latitude: number + longitude: number + city_id: number + city: string + [key: string]: string | number +} + +export type FieldType = { + label: string + data: keyof T + render?: (value: any) => React.ReactNode +} + +export const stationFields: Array> = [ + // {label: 'ID', data: 'id'}, + {label: 'Название', data: 'name'}, + // {label: 'Широта', data: 'latitude'}, + // {label: 'Долгота', data: 'longitude'}, + {label: 'Описание', data: 'description'}, +] + +export const sightFields: Array> = [ + // {label: 'ID', data: 'id'}, + {label: 'Название', data: 'name'}, + // {label: 'Широта', data: 'latitude'}, + // {label: 'Долгота', data: 'longitude'}, + // {label: 'ID города', data: 'city_id'}, + {label: 'Город', data: 'city'}, +] \ No newline at end of file