Files
WhiteNightsAdminPanel/src/pages/Station/StationEditPage/index.tsx
2026-05-05 15:07:18 +03:00

404 lines
11 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 {
Button,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Box,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "react-toastify";
import {
stationsStore,
languageStore,
cityStore,
authStore,
mediaStore,
isMediaIdEmpty,
LoadingSpinner,
selectedCityStore,
SelectMediaDialog,
PreviewMediaDialog,
UploadMediaDialog,
} from "@shared";
import { useEffect, useState } from "react";
import {
ImageUploadCard,
LanguageSwitcher,
SaveWithoutCityAgree,
DeleteModal,
} from "@widgets";
import { LinkedSights } from "../LinkedSights";
export const StationEditPage = observer(() => {
const navigate = useNavigate();
useEffect(() => {
selectedCityStore.setIsLocked(true);
return () => selectedCityStore.setIsLocked(false);
}, []);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(true);
const { language } = languageStore;
const { id } = useParams();
const {
editStationData,
getEditStation,
setEditCommonData,
editStation,
setLanguageEditStationData,
} = stationsStore;
const { getCities } = cityStore;
const canReadCities = authStore.canRead("cities");
const [coordinates, setCoordinates] = useState<string>("");
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [mediaId, setMediaId] = useState("");
const [isDeleteIconModalOpen, setIsDeleteIconModalOpen] = useState(false);
const [activeMenuType, setActiveMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null);
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
useEffect(() => {
languageStore.setLanguage("ru");
}, []);
useEffect(() => {
if (
editStationData.common.latitude !== 0 ||
editStationData.common.longitude !== 0
) {
setCoordinates(
`${editStationData.common.latitude}, ${editStationData.common.longitude}`
);
}
}, [editStationData.common.latitude, editStationData.common.longitude]);
const executeEdit = async () => {
try {
setIsLoading(true);
await editStation(Number(id));
toast.success("Остановка успешно обновлена");
} catch (error) {
console.error("Error updating station:", error);
toast.error("Ошибка при обновлении остановки");
} finally {
setIsLoading(false);
}
};
const handleEdit = async () => {
const isCityMissing = !editStationData.common.city_id;
const isNameMissing =
!editStationData.ru.name ||
!editStationData.en.name ||
!editStationData.zh.name;
if (isCityMissing || isNameMissing) {
setIsSaveWarningOpen(true);
return;
}
await executeEdit();
};
const handleConfirmEdit = async () => {
setIsSaveWarningOpen(false);
await executeEdit();
};
const handleCancelEdit = () => {
setIsSaveWarningOpen(false);
};
const handleMediaSelect = (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
setEditCommonData({ icon: media.id });
};
const selectedMedia =
editStationData.common.icon && !isMediaIdEmpty(editStationData.common.icon)
? mediaStore.media.find((m) => m.id === editStationData.common.icon)
: null;
const effectiveIconUrl = isMediaIdEmpty(editStationData.common.icon)
? null
: selectedMedia?.id ?? editStationData.common.icon;
useEffect(() => {
const fetchAndSetStationData = async () => {
if (!id) {
setIsLoadingData(false);
return;
}
setIsLoadingData(true);
try {
const stationId = Number(id);
await getEditStation(stationId);
if (!authStore.me) {
await authStore.getMeAction().catch(() => undefined);
}
if (authStore.canRead("cities")) {
await getCities("ru");
} else {
await authStore.fetchMeCities().catch(() => undefined);
}
await mediaStore.getMedia();
} finally {
setIsLoadingData(false);
}
};
fetchAndSetStationData();
}, [id]);
const baseCities = canReadCities
? cityStore.cities["ru"].data
: authStore.meCities["ru"].map((city) => ({
id: city.city_id,
name: city.name,
country: "",
country_code: "",
arms: "",
}));
const availableCities =
editStationData.common.city_id &&
!baseCities.some((city) => city.id === editStationData.common.city_id)
? [
{
id: editStationData.common.city_id,
name: editStationData.common.city || `Город ${editStationData.common.city_id}`,
country: "",
country_code: "",
arms: "",
},
...baseCities,
]
: baseCities;
if (isLoadingData) {
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "60vh",
}}
>
<LoadingSpinner message="Загрузка данных остановки..." />
</Box>
);
}
return (
<Box className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex items-center gap-4">
<button
className="flex items-center gap-2"
onClick={() => navigate(-1)}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<div className="flex flex-col gap-10 w-full items-end">
<div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
<h1 className="text-3xl break-words">{editStationData.ru.name}</h1>
</div>
<TextField
fullWidth
label="Название"
value={editStationData[language].name || ""}
required
onChange={(e) =>
setLanguageEditStationData(language, {
name: e.target.value,
})
}
/>
<TextField
fullWidth
label="Описание"
value={editStationData.common.description || ""}
onChange={(e) =>
setEditCommonData({
description: e.target.value,
})
}
/>
{/* <TextField
fullWidth
label="Адрес"
value={editStationData[language].address || ""}
onChange={(e) =>
setLanguageEditStationData(language, {
address: e.target.value,
})
}
/> */}
<TextField
fullWidth
label="Координаты"
value={coordinates}
onChange={(e) => {
const newValue = e.target.value;
setCoordinates(newValue);
const input = newValue.replace(/,/g, " ").trim();
const [latStr, lonStr] = input.split(/\s+/);
const lat = parseFloat(latStr);
const lon = parseFloat(lonStr);
const isValidLat = !isNaN(lat);
const isValidLon = !isNaN(lon);
if (isValidLat && isValidLon) {
setEditCommonData({
latitude: lat,
longitude: lon,
});
} else {
setEditCommonData({
latitude: 0,
longitude: 0,
});
}
}}
placeholder="Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)"
/>
<FormControl fullWidth>
<InputLabel>Город</InputLabel>
<Select
value={editStationData.common.city_id || ""}
label="Город"
onChange={(e) => {
const selectedCity = availableCities.find(
(city) => city.id === e.target.value
);
setEditCommonData({
city_id: e.target.value as number,
city: selectedCity?.name || "",
});
}}
>
{availableCities.map((city) => (
<MenuItem key={city.id} value={city.id}>
{city.name}
</MenuItem>
))}
</Select>
</FormControl>
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
<ImageUploadCard
title="Иконка остановки"
imageKey="thumbnail"
imageUrl={effectiveIconUrl}
onImageClick={() => {
setIsPreviewMediaOpen(true);
setMediaId(effectiveIconUrl ?? "");
}}
onDeleteImageClick={() => {
setIsDeleteIconModalOpen(true);
}}
onSelectFileClick={() => {
setActiveMenuType("image");
setIsSelectMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("image");
}}
/>
</div>
{id && (
<LinkedSights
parentId={Number(id)}
fields={[{ label: "Название", data: "name" }]}
type="edit"
/>
)}
<Button
variant="contained"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleEdit}
disabled={isLoading}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
) : (
"Сохранить"
)}
</Button>
</div>
<SelectMediaDialog
open={isSelectMediaOpen}
onClose={() => setIsSelectMediaOpen(false)}
onSelectMedia={handleMediaSelect}
mediaType={1}
/>
<UploadMediaDialog
open={isUploadMediaOpen}
onClose={() => setIsUploadMediaOpen(false)}
contextObjectName={editStationData[language].name || "Остановка"}
contextType="station"
afterUpload={handleMediaSelect}
hardcodeType={activeMenuType}
/>
<PreviewMediaDialog
open={isPreviewMediaOpen}
onClose={() => setIsPreviewMediaOpen(false)}
mediaId={mediaId}
/>
<DeleteModal
open={isDeleteIconModalOpen}
onDelete={() => {
setEditCommonData({ icon: "" });
setIsDeleteIconModalOpen(false);
}}
onCancel={() => setIsDeleteIconModalOpen(false)}
edit
/>
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
{isSaveWarningOpen && (
<SaveWithoutCityAgree
blocker={{
proceed: handleConfirmEdit,
reset: handleCancelEdit,
}}
/>
)}
</Box>
);
});