feat: Redesign route direction with media_order input

This commit is contained in:
Илья Куприец 2025-05-20 16:59:57 +03:00
parent 7c363f1730
commit 8a443882b5
4 changed files with 244 additions and 184 deletions

View File

@ -106,17 +106,19 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>(
</AccordionDetails>
</Accordion>
{!props.dontRecurse &&
{!props.dontRecurse && (
<>
<ArticleEditModal />
<StationEditModal />
</>
}
)}
</>
);
}
};
export const LinkedItemsContents = <T extends { id: number; [key: string]: any }>({
export const LinkedItemsContents = <
T extends { id: number; [key: string]: any }
>({
parentId,
parentResource,
childResource,
@ -128,7 +130,7 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
onUpdate,
disableCreation = false,
updatedLinkedItems,
refresh
refresh,
}: LinkedItemsProps<T>) => {
const { language } = languageStore;
const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
@ -154,7 +156,7 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
}, [childResource, availableItems]);
useEffect(() => {
if(!updatedLinkedItems?.length) return;
if (!updatedLinkedItems?.length) return;
setLinkedItems(updatedLinkedItems);
}, [updatedLinkedItems]);
@ -173,7 +175,7 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
setLinkedItems(reorderedItems);
if(parentResource === "sight" && childResource === "article") {
if (parentResource === "sight" && childResource === "article") {
axiosInstance.post(
`${import.meta.env.VITE_KRBL_API}/sight/${parentId}/article/order`,
{
@ -358,11 +360,17 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
: "default",
}}
onClick={() => {
if (childResource === "article" && type==="edit") {
if (
childResource === "article" &&
type === "edit"
) {
setArticleModalOpenAction(true);
setArticleIdAction(item.id);
}
if (childResource === "station" && type==="edit") {
if (
childResource === "station" &&
type === "edit"
) {
setStationModalOpenAction(true);
setStationIdAction(item.id);
setRouteIdAction(Number(parentId));
@ -434,25 +442,15 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
<Autocomplete
fullWidth
value={
availableItems?.find(
(item) => item.id === selectedItemId
) || null
}
onChange={(_, newValue) =>
setSelectedItemId(newValue?.id || null)
availableItems?.find((item) => item.id === selectedItemId) || null
}
onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
options={availableItems}
getOptionLabel={(item) => String(item[fields[0].data])}
renderInput={(params) => (
<TextField
{...params}
label={`Выберите ${title}`}
fullWidth
/>
<TextField {...params} label={`Выберите ${title}`} fullWidth />
)}
isOptionEqualToValue={(option, value) =>
option.id === value?.id
}
isOptionEqualToValue={(option, value) => option.id === value?.id}
filterOptions={(options, { inputValue }) => {
const searchWords = inputValue
.toLowerCase()
@ -495,17 +493,32 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
{childResource === "media" && (
<FormControl fullWidth>
<TextField
type="number"
type="text"
label="Порядок отображения медиа"
value={mediaOrder}
onChange={(e) => {
const newValue = Number(e.target.value);
const rawValue = e.target.value;
const numericValue = Number(rawValue);
const maxValue = linkedItems.length + 1;
const value = Math.max(1, Math.min(newValue, maxValue));
setMediaOrder(value);
if (isNaN(numericValue)) {
return;
} else {
let newValue = numericValue;
if (newValue < 10 && newValue > 0) {
setMediaOrder(numericValue);
}
if (newValue > maxValue) {
newValue = maxValue;
}
setMediaOrder(newValue);
}
}}
fullWidth
slotProps={{inputLabel: {shrink: true}}}
InputLabelProps={{ shrink: true }}
/>
</FormControl>
)}
@ -513,7 +526,9 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
<Button
variant="contained"
onClick={linkItem}
disabled={!selectedItemId}
disabled={
!selectedItemId || (childResource == "media" && mediaOrder == 0)
}
sx={{ alignSelf: "flex-start" }}
>
Добавить

View File

@ -8,6 +8,7 @@ import {
} from "@mui/material";
import { Create, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { useState } from "react";
import { Controller } from "react-hook-form";
export const RouteCreate = () => {
@ -16,6 +17,7 @@ export const RouteCreate = () => {
refineCore: { formLoading },
register,
control,
setValue,
formState: { errors },
} = useForm({
refineCoreProps: {
@ -23,6 +25,16 @@ export const RouteCreate = () => {
},
});
const directions = [
{
label: "Прямой",
value: true,
},
{
label: "Обратный",
value: false,
},
];
const { autocompleteProps: carrierAutocompleteProps } = useAutocomplete({
resource: "carrier",
onSearch: (value) => [
@ -34,22 +46,25 @@ export const RouteCreate = () => {
],
});
const { autocompleteProps: governorAppealAutocompleteProps } = useAutocomplete({
resource: "article",
const { autocompleteProps: governorAppealAutocompleteProps } =
useAutocomplete({
resource: "article",
onSearch: (value) => [
{
field: "heading",
operator: "contains",
value,
},
{
field: "media_type",
operator: "contains",
value,
},
]
});
onSearch: (value) => [
{
field: "heading",
operator: "contains",
value,
},
{
field: "media_type",
operator: "contains",
value,
},
],
});
const [routeDirection, setRouteDirection] = useState(false);
return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
@ -111,37 +126,12 @@ export const RouteCreate = () => {
helperText={(errors as any)?.route_number?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="text"
label={"Номер маршрута *"}
name="route_number"
/>
<Controller
name="route_direction" // boolean
control={control}
defaultValue={false}
render={({ field }: { field: any }) => (
<FormControlLabel
label="Прямой маршрут? *"
control={
<Checkbox
{...field}
checked={field.value}
onChange={(e) => field.onChange(e.target.checked)}
/>
}
/>
)}
/>
<Typography
variant="caption"
color="textSecondary"
sx={{ mt: 0, mb: 1 }}
>
(Прямой / Обратный)
</Typography>
<TextField
{...register("path", {
required: "Это поле является обязательным",
@ -191,7 +181,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.path?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="text"
label={"Координаты маршрута *"}
name="path"
@ -209,9 +199,9 @@ export const RouteCreate = () => {
helperText={(errors as any)?.route_sys_number?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Системный номер маршрута *"}
label={"Номер маршрута в Говорящем Городе *"}
name="route_sys_number"
/>
@ -256,7 +246,44 @@ export const RouteCreate = () => {
)}
/>
)}
/>
/>
<input
type="hidden"
{...register("route_direction", {
value: routeDirection,
})}
/>
<Autocomplete
options={directions}
defaultValue={directions.find((el) => el.value == false)}
onChange={(_, element) => {
if (element) {
setValue("route_direction", element.value);
setRouteDirection(element.value);
}
}}
renderInput={(params) => (
<TextField
{...params}
label="Прямой/обратный маршрут"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
<Typography
variant="caption"
color="textSecondary"
sx={{ mt: 0, mb: 1 }}
>
{routeDirection ? "Прямой" : "Обратный"}
</Typography>
<TextField
{...register("scale_min", {
@ -267,7 +294,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.scale_min?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Масштаб (мин)"}
name="scale_min"
@ -282,7 +309,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.scale_max?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Масштаб (макс)"}
name="scale_max"
@ -297,7 +324,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.rotate?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Поворот"}
name="rotate"
@ -312,7 +339,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.center_latitude?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Центр. широта"}
name="center_latitude"
@ -327,7 +354,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.center_longitude?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Центр. долгота"}
name="center_longitude"

View File

@ -18,7 +18,7 @@ import {
stationFields,
vehicleFields,
} from "./types";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { META_LANGUAGE, languageStore } from "@stores";
import { observer } from "mobx-react-lite";
import { LanguageSelector } from "@ui";
@ -32,11 +32,25 @@ export const RouteEdit = observer(() => {
formState: { errors },
refineCore: { queryResult },
setValue,
getValues,
watch,
} = useForm({
refineCoreProps: META_LANGUAGE(language)
refineCoreProps: META_LANGUAGE(language),
});
const routeDirectionFromServer = watch("route_direction");
const [routeDirection, setRouteDirection] = useState(false);
const navigate = useNavigate();
const directions = [
{
label: "Прямой",
value: true,
},
{
label: "Обратный",
value: false,
},
];
const { id: routeId } = useParams<{ id: string }>();
@ -59,35 +73,40 @@ export const RouteEdit = observer(() => {
value,
},
],
...META_LANGUAGE(language)
...META_LANGUAGE(language),
});
const { autocompleteProps: governorAppealAutocompleteProps } = useAutocomplete({
resource: "article",
const { autocompleteProps: governorAppealAutocompleteProps } =
useAutocomplete({
resource: "article",
onSearch: (value) => [
{
field: "heading",
operator: "contains",
value,
},
{
field: "media_type",
operator: "contains",
value,
},
],
...META_LANGUAGE(language)
});
onSearch: (value) => [
{
field: "heading",
operator: "contains",
value,
},
{
field: "media_type",
operator: "contains",
value,
},
],
...META_LANGUAGE(language),
});
useEffect(() => {
if (routeDirectionFromServer) {
setRouteDirection(routeDirectionFromServer);
}
}, [routeDirectionFromServer]);
return (
<Edit saveButtonProps={saveButtonProps}>
<Box
sx={{display: "flex", flexDirection: "column", gap:1}}
>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column"}}
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<LanguageSelector />
@ -144,37 +163,36 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.route_number?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="text"
label={"Номер маршрута"}
name="route_number"
/>
<Controller
name="route_direction" // boolean
control={control}
defaultValue={false}
render={({ field }: { field: any }) => (
<FormControlLabel
label="Прямой маршрут?"
control={
<Checkbox
{...field}
checked={field.value}
onChange={(e) => field.onChange(e.target.checked)}
/>
}
<input type="hidden" {...register("route_direction")} />
<Autocomplete
options={directions}
value={directions.find((el) => el.value == routeDirection)}
onChange={(_, element) => {
if (element) {
setValue("route_direction", element.value);
setRouteDirection(element.value);
}
}}
renderInput={(params) => (
<TextField
{...params}
label="Прямой/обратный маршрут"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
<Typography
variant="caption"
color="textSecondary"
sx={{ mt: 0, mb: 1 }}
>
(Прямой / Обратный)
</Typography>
<TextField
{...register("path", {
required: "Это поле является обязательным",
@ -198,7 +216,8 @@ export const RouteEdit = observer(() => {
return "Введите хотя бы одну пару координат";
if (
!value.every(
(point: unknown) => Array.isArray(point) && point.length === 2
(point: unknown) =>
Array.isArray(point) && point.length === 2
)
) {
return "Каждая строка должна содержать две координаты";
@ -220,12 +239,12 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.path?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="text"
label={"Координаты маршрута *"}
name="path"
placeholder="55.7558 37.6173
55.7539 37.6208"
55.7539 37.6208"
multiline
rows={4}
sx={{
@ -241,53 +260,53 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.route_sys_number?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Системный номер маршрута *"}
label={"Номер маршрута в Говорящем Городе *"}
name="route_sys_number"
/>
<Controller
control={control}
name="governor_appeal"
defaultValue={null}
render={({ field }) => (
<Autocomplete
{...governorAppealAutocompleteProps}
value={
governorAppealAutocompleteProps.options.find(
(option) => option.id === field.value
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.heading : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.heading
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Обращение губернатора"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
)}
control={control}
name="governor_appeal"
defaultValue={null}
render={({ field }) => (
<Autocomplete
{...governorAppealAutocompleteProps}
value={
governorAppealAutocompleteProps.options.find(
(option) => option.id === field.value
) ?? null
}
onChange={(_, value) => {
field.onChange(value?.id ?? "");
}}
getOptionLabel={(item) => {
return item ? item.heading : "";
}}
isOptionEqualToValue={(option, value) => {
return option.id === value?.id;
}}
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.heading
.toLowerCase()
.includes(inputValue.toLowerCase())
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Обращение губернатора"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
)}
/>
<TextField
@ -299,7 +318,7 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.scale_min?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Масштаб (мин)"}
name="scale_min"
@ -314,7 +333,7 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.scale_max?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Масштаб (макс)"}
name="scale_max"
@ -329,7 +348,7 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.rotate?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Поворот"}
name="rotate"
@ -344,7 +363,7 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.center_latitude?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Центр. широта"}
name="center_latitude"
@ -359,12 +378,11 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.center_longitude?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
slotProps={{ inputLabel: { shrink: true } }}
type="number"
label={"Центр. долгота"}
name="center_longitude"
/>
</Box>
{routeId && (
@ -390,9 +408,9 @@ export const RouteEdit = observer(() => {
</>
)}
<Box sx={{ display: 'flex', justifyContent: 'flex-start' }}>
<Button
variant="contained"
<Box sx={{ display: "flex", justifyContent: "flex-start" }}>
<Button
variant="contained"
color="primary"
onClick={() => navigate(`/route-preview/${routeId}`)}
>

View File

@ -9,7 +9,7 @@ import {
} from "@refinedev/mui";
import { Button, Typography } from "@mui/material";
import React from "react";
import MapIcon from '@mui/icons-material/Map';
import MapIcon from "@mui/icons-material/Map";
import { localeText } from "../../locales/ru/localeText";
import { useLink } from "@refinedev/core";
@ -18,10 +18,10 @@ import { languageStore, META_LANGUAGE } from "@stores";
export const RouteList = observer(() => {
const Link = useLink();
const { language } = languageStore;
const { language } = languageStore;
const { dataGridProps } = useDataGrid({
resource: "route/",
meta: META_LANGUAGE(language)
meta: META_LANGUAGE(language),
});
const columns = React.useMemo<GridColDef[]>(
@ -64,7 +64,7 @@ export const RouteList = observer(() => {
},
{
field: "route_sys_number",
headerName: "Системный номер маршрута",
headerName: "Номер маршрута в Говорящем Городе",
type: "string",
minWidth: 120,
display: "flex",
@ -156,7 +156,7 @@ export const RouteList = observer(() => {
<>
<EditButton hideText recordItemId={row.id} />
<Link to={`/route-preview/${row.id}`}>
<Button sx={{minWidth: 0}} >
<Button sx={{ minWidth: 0 }}>
<MapIcon fontSize="small" />
</Button>
</Link>