344 lines
9.7 KiB
TypeScript
344 lines
9.7 KiB
TypeScript
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 } from "react-router-dom";
|
||
import { toast } from "react-toastify";
|
||
import {
|
||
stationsStore,
|
||
languageStore,
|
||
cityStore,
|
||
authStore,
|
||
mediaStore,
|
||
isMediaIdEmpty,
|
||
useSelectedCity,
|
||
SelectMediaDialog,
|
||
UploadMediaDialog,
|
||
PreviewMediaDialog,
|
||
} from "@shared";
|
||
import { useEffect, useState } from "react";
|
||
import {
|
||
ImageUploadCard,
|
||
LanguageSwitcher,
|
||
SaveWithoutCityAgree,
|
||
} from "@widgets";
|
||
|
||
export const StationCreatePage = observer(() => {
|
||
const navigate = useNavigate();
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const { language } = languageStore;
|
||
const {
|
||
createStationData,
|
||
setCreateCommonData,
|
||
createStation,
|
||
setLanguageCreateStationData,
|
||
} = stationsStore;
|
||
const { getCities } = cityStore;
|
||
const canReadCities = authStore.canRead("cities");
|
||
const { selectedCityId, selectedCity } = useSelectedCity();
|
||
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 [activeMenuType, setActiveMenuType] = useState<
|
||
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
|
||
>(null);
|
||
|
||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (
|
||
createStationData.common.latitude !== 0 ||
|
||
createStationData.common.longitude !== 0
|
||
) {
|
||
setCoordinates(
|
||
`${createStationData.common.latitude}, ${createStationData.common.longitude}`
|
||
);
|
||
}
|
||
}, [createStationData.common.latitude, createStationData.common.longitude]);
|
||
|
||
const executeCreate = async () => {
|
||
try {
|
||
setIsLoading(true);
|
||
await createStation();
|
||
toast.success("Остановка успешно создана");
|
||
navigate("/station");
|
||
} catch (error) {
|
||
console.error("Error creating station:", error);
|
||
toast.error("Ошибка при создании станции");
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleCreate = async () => {
|
||
const isCityMissing = !createStationData.common.city_id;
|
||
|
||
const isNameMissing =
|
||
!createStationData.ru.name ||
|
||
!createStationData.en.name ||
|
||
!createStationData.zh.name;
|
||
|
||
if (isCityMissing || isNameMissing) {
|
||
setIsSaveWarningOpen(true);
|
||
return;
|
||
}
|
||
|
||
await executeCreate();
|
||
};
|
||
|
||
const handleConfirmCreate = async () => {
|
||
setIsSaveWarningOpen(false);
|
||
await executeCreate();
|
||
};
|
||
|
||
const handleCancelCreate = () => {
|
||
setIsSaveWarningOpen(false);
|
||
};
|
||
|
||
useEffect(() => {
|
||
const fetchCities = async () => {
|
||
if (!authStore.me) {
|
||
await authStore.getMeAction().catch(() => undefined);
|
||
}
|
||
if (authStore.canRead("cities")) {
|
||
await getCities("ru");
|
||
return;
|
||
}
|
||
await authStore.fetchMeCities().catch(() => undefined);
|
||
};
|
||
|
||
fetchCities();
|
||
mediaStore.getMedia();
|
||
}, []);
|
||
|
||
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 =
|
||
selectedCity?.id && !baseCities.some((city) => city.id === selectedCity.id)
|
||
? [selectedCity, ...baseCities]
|
||
: baseCities;
|
||
|
||
const handleMediaSelect = (media: {
|
||
id: string;
|
||
filename: string;
|
||
media_name?: string;
|
||
media_type: number;
|
||
}) => {
|
||
setCreateCommonData({ icon: media.id });
|
||
};
|
||
|
||
const selectedMedia =
|
||
createStationData.common.icon &&
|
||
!isMediaIdEmpty(createStationData.common.icon)
|
||
? mediaStore.media.find((m) => m.id === createStationData.common.icon)
|
||
: null;
|
||
const effectiveIconUrl = isMediaIdEmpty(createStationData.common.icon)
|
||
? null
|
||
: selectedMedia?.id ?? createStationData.common.icon;
|
||
|
||
useEffect(() => {
|
||
if (selectedCityId && selectedCity && !createStationData.common.city_id) {
|
||
setCreateCommonData({
|
||
city_id: selectedCityId,
|
||
city: selectedCity.name,
|
||
});
|
||
}
|
||
}, [selectedCityId, selectedCity, createStationData.common.city_id]);
|
||
|
||
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">Создание остановки</h1>
|
||
</div>
|
||
<TextField
|
||
fullWidth
|
||
label="Название"
|
||
value={createStationData[language].name || ""}
|
||
required
|
||
onChange={(e) =>
|
||
setLanguageCreateStationData(language, {
|
||
name: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
|
||
<TextField
|
||
fullWidth
|
||
label="Описание"
|
||
value={createStationData.common.description || ""}
|
||
onChange={(e) =>
|
||
setCreateCommonData({
|
||
description: e.target.value,
|
||
})
|
||
}
|
||
/>
|
||
|
||
{/* <TextField
|
||
fullWidth
|
||
label="Адрес"
|
||
value={createStationData[language].address || ""}
|
||
onChange={(e) =>
|
||
setLanguageCreateStationData(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) {
|
||
setCreateCommonData({
|
||
latitude: lat,
|
||
longitude: lon,
|
||
});
|
||
} else {
|
||
setCreateCommonData({
|
||
latitude: 0,
|
||
longitude: 0,
|
||
});
|
||
}
|
||
}}
|
||
placeholder="Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)"
|
||
/>
|
||
|
||
<FormControl fullWidth>
|
||
<InputLabel>Город</InputLabel>
|
||
<Select
|
||
value={createStationData.common.city_id || ""}
|
||
label="Город"
|
||
onChange={(e) => {
|
||
const selectedCity = availableCities.find(
|
||
(city) => city.id === e.target.value
|
||
);
|
||
setCreateCommonData({
|
||
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={() => {
|
||
setCreateCommonData({ icon: "" });
|
||
setActiveMenuType(null);
|
||
}}
|
||
onSelectFileClick={() => {
|
||
setActiveMenuType("image");
|
||
setIsSelectMediaOpen(true);
|
||
}}
|
||
setUploadMediaOpen={() => {
|
||
setIsUploadMediaOpen(true);
|
||
setActiveMenuType("image");
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<Button
|
||
variant="contained"
|
||
className="w-min flex gap-2 items-center"
|
||
startIcon={<Save size={20} />}
|
||
onClick={handleCreate}
|
||
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={createStationData[language].name || "Остановка"}
|
||
contextType="station"
|
||
afterUpload={handleMediaSelect}
|
||
hardcodeType={activeMenuType}
|
||
/>
|
||
|
||
<PreviewMediaDialog
|
||
open={isPreviewMediaOpen}
|
||
onClose={() => setIsPreviewMediaOpen(false)}
|
||
mediaId={mediaId}
|
||
/>
|
||
|
||
{isSaveWarningOpen && (
|
||
<SaveWithoutCityAgree
|
||
blocker={{
|
||
proceed: handleConfirmCreate,
|
||
reset: handleCancelCreate,
|
||
}}
|
||
/>
|
||
)}
|
||
</Box>
|
||
);
|
||
});
|