fix: Hot bug fix

This commit is contained in:
2025-06-15 20:38:48 +03:00
parent 481385c2f4
commit 32a7cb44d1
24 changed files with 900 additions and 250 deletions

View File

@ -139,7 +139,7 @@ const router = createBrowserRouter([
// City
{ path: "city", element: <CityListPage /> },
{ path: "city/create", element: <CityCreatePage /> },
{ path: "city/:id", element: <CityPreviewPage /> },
// { path: "city/:id", element: <CityPreviewPage /> },
{ path: "city/:id/edit", element: <CityEditPage /> },
// Route
{ path: "route", element: <RouteListPage /> },

View File

@ -12,7 +12,7 @@ import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { carrierStore, cityStore, mediaStore } from "@shared";
import { carrierStore, cityStore, mediaStore, languageStore } from "@shared";
import { useState, useEffect } from "react";
import { ImageUploadCard, LanguageSwitcher } from "@widgets";
import {
@ -23,11 +23,8 @@ import {
export const CarrierCreatePage = observer(() => {
const navigate = useNavigate();
const [fullName, setFullName] = useState("");
const [shortName, setShortName] = useState("");
const [cityId, setCityId] = useState<number | null>(null);
const [slogan, setSlogan] = useState("");
const { createCarrierData, setCreateCarrierData } = carrierStore;
const { language } = languageStore;
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
@ -35,7 +32,7 @@ export const CarrierCreatePage = observer(() => {
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [mediaId, setMediaId] = useState("");
const [activeMenuType, setActiveMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | null
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null);
useEffect(() => {
@ -46,13 +43,7 @@ export const CarrierCreatePage = observer(() => {
const handleCreate = async () => {
try {
setIsLoading(true);
await carrierStore.createCarrier(
fullName,
shortName,
cityId!,
slogan,
selectedMediaId!
);
await carrierStore.createCarrier();
toast.success("Перевозчик успешно создан");
navigate("/carrier");
} catch (error) {
@ -69,6 +60,14 @@ export const CarrierCreatePage = observer(() => {
media_type: number;
}) => {
setSelectedMediaId(media.id);
setCreateCarrierData(
createCarrierData[language].full_name,
createCarrierData[language].short_name,
createCarrierData.city_id,
createCarrierData[language].slogan,
media.id,
language
);
};
const selectedMedia = selectedMediaId
@ -89,19 +88,28 @@ export const CarrierCreatePage = observer(() => {
</div>
<div className="flex flex-col gap-10 w-full items-end">
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
<div className="flex gap-10 items-center mb-5 max-w-[80%] self-start">
<h1 className="text-3xl break-words">Создание перевозчика</h1>
</div>
<FormControl fullWidth>
<InputLabel>Город</InputLabel>
<Select
value={cityId || ""}
value={createCarrierData.city_id || ""}
label="Город"
required
onChange={(e) => setCityId(e.target.value as number)}
onChange={(e) =>
setCreateCarrierData(
createCarrierData[language].full_name,
createCarrierData[language].short_name,
e.target.value as number,
createCarrierData[language].slogan,
selectedMediaId || "",
language
)
}
>
{cityStore.cities.ru.data.map((city) => (
{cityStore.cities["ru"].data.map((city) => (
<MenuItem key={city.id} value={city.id}>
{city.name}
</MenuItem>
@ -112,24 +120,51 @@ export const CarrierCreatePage = observer(() => {
<TextField
fullWidth
label="Полное название"
value={fullName}
value={createCarrierData[language].full_name}
required
onChange={(e) => setFullName(e.target.value)}
onChange={(e) =>
setCreateCarrierData(
e.target.value,
createCarrierData[language].short_name,
createCarrierData.city_id,
createCarrierData[language].slogan,
selectedMediaId || "",
language
)
}
/>
<TextField
fullWidth
label="Короткое название"
value={shortName}
value={createCarrierData[language].short_name}
required
onChange={(e) => setShortName(e.target.value)}
onChange={(e) =>
setCreateCarrierData(
createCarrierData[language].full_name,
e.target.value,
createCarrierData.city_id,
createCarrierData[language].slogan,
selectedMediaId || "",
language
)
}
/>
<TextField
fullWidth
label="Слоган"
value={slogan}
onChange={(e) => setSlogan(e.target.value)}
value={createCarrierData[language].slogan}
onChange={(e) =>
setCreateCarrierData(
createCarrierData[language].full_name,
createCarrierData[language].short_name,
createCarrierData.city_id,
e.target.value,
selectedMediaId || "",
language
)
}
/>
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
@ -144,14 +179,22 @@ export const CarrierCreatePage = observer(() => {
onDeleteImageClick={() => {
setSelectedMediaId(null);
setActiveMenuType(null);
setCreateCarrierData(
createCarrierData[language].full_name,
createCarrierData[language].short_name,
createCarrierData.city_id,
createCarrierData[language].slogan,
"",
language
);
}}
onSelectFileClick={() => {
setActiveMenuType("thumbnail");
setActiveMenuType("image");
setIsSelectMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
setActiveMenuType("image");
}}
/>
</div>
@ -162,7 +205,10 @@ export const CarrierCreatePage = observer(() => {
startIcon={<Save size={20} />}
onClick={handleCreate}
disabled={
isLoading || !fullName || !shortName || !cityId || !selectedMediaId
isLoading ||
!createCarrierData[language].full_name ||
!createCarrierData[language].short_name ||
!createCarrierData.city_id
}
>
{isLoading ? (
@ -177,7 +223,7 @@ export const CarrierCreatePage = observer(() => {
open={isSelectMediaOpen}
onClose={() => setIsSelectMediaOpen(false)}
onSelectMedia={handleMediaSelect}
mediaType={3}
mediaType={1}
/>
<UploadMediaDialog

View File

@ -33,7 +33,7 @@ export const CarrierEditPage = observer(() => {
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [mediaId, setMediaId] = useState("");
const [activeMenuType, setActiveMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | null
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null);
useEffect(() => {
@ -141,7 +141,7 @@ export const CarrierEditPage = observer(() => {
)
}
>
{cityStore.cities[language].data?.map((city) => (
{cityStore.cities["ru"].data?.map((city) => (
<MenuItem key={city.id} value={city.id}>
{city.name}
</MenuItem>
@ -220,12 +220,12 @@ export const CarrierEditPage = observer(() => {
setActiveMenuType(null);
}}
onSelectFileClick={() => {
setActiveMenuType("thumbnail");
setActiveMenuType("image");
setIsSelectMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
setActiveMenuType("image");
}}
/>
</div>
@ -238,9 +238,7 @@ export const CarrierEditPage = observer(() => {
disabled={
isLoading ||
!editCarrierData[language].full_name ||
!editCarrierData[language].short_name ||
!editCarrierData.city_id ||
!editCarrierData.logo
!editCarrierData.city_id
}
>
{isLoading ? (
@ -255,7 +253,7 @@ export const CarrierEditPage = observer(() => {
open={isSelectMediaOpen}
onClose={() => setIsSelectMediaOpen(false)}
onSelectMedia={handleMediaSelect}
mediaType={3}
mediaType={1}
/>
<UploadMediaDialog

View File

@ -1,5 +1,5 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { carrierStore, languageStore } from "@shared";
import { carrierStore, cityStore, languageStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Pencil, Trash2, Minus } from "lucide-react";
@ -8,6 +8,7 @@ import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
export const CarrierListPage = observer(() => {
const { carriers, getCarriers, deleteCarrier } = carrierStore;
const { getCities, cities } = cityStore;
const navigate = useNavigate();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
@ -17,6 +18,9 @@ export const CarrierListPage = observer(() => {
useEffect(() => {
(async () => {
await getCities("ru");
await getCities("en");
await getCities("zh");
await getCarriers(language);
})();
}, [language]);
@ -55,14 +59,15 @@ export const CarrierListPage = observer(() => {
},
},
{
field: "city",
field: "city_id",
headerName: "Город",
flex: 1,
renderCell: (params: GridRenderCellParams) => {
return (
<div className="w-full h-full flex items-center">
{params.value ? (
params.value
cities[language].data.find((city) => city.id == params.value)
?.name
) : (
<Minus size={20} className="text-red-500" />
)}
@ -103,7 +108,7 @@ export const CarrierListPage = observer(() => {
id: carrier.id,
full_name: carrier.full_name,
short_name: carrier.short_name,
city: carrier.city,
city_id: carrier.city_id,
}));
return (

View File

@ -31,7 +31,7 @@ export const CityCreatePage = observer(() => {
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [mediaId, setMediaId] = useState("");
const [activeMenuType, setActiveMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | null
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null);
const { getCountries } = countryStore;
const { getMedia } = mediaStore;
@ -132,15 +132,9 @@ export const CityCreatePage = observer(() => {
</FormControl>
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
{!selectedMedia && (
<div className="flex items-center gap-2 text-red-500">
<Minus size={20} />
<span className="text-sm">Герб города не выбран</span>
</div>
)}
<ImageUploadCard
title="Герб города"
imageKey="thumbnail"
imageKey="image"
imageUrl={selectedMedia?.id}
onImageClick={() => {
setIsPreviewMediaOpen(true);
@ -156,12 +150,22 @@ export const CityCreatePage = observer(() => {
setActiveMenuType(null);
}}
onSelectFileClick={() => {
setActiveMenuType("thumbnail");
setActiveMenuType("image");
setIsSelectMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
setActiveMenuType("image");
}}
setHardcodeType={(type) => {
setActiveMenuType(
type as
| "thumbnail"
| "watermark_lu"
| "watermark_rd"
| "image"
| null
);
}}
/>
</div>
@ -185,14 +189,16 @@ export const CityCreatePage = observer(() => {
open={isSelectMediaOpen}
onClose={() => setIsSelectMediaOpen(false)}
onSelectMedia={handleMediaSelect}
mediaType={3} // Тип медиа для иконок
mediaType={1} // Тип медиа для иконок
/>
<UploadMediaDialog
open={isUploadMediaOpen}
onClose={() => setIsUploadMediaOpen(false)}
afterUpload={handleMediaSelect}
hardcodeType={activeMenuType}
hardcodeType={
activeMenuType as "thumbnail" | "watermark_lu" | "watermark_rd" | null
}
/>
<PreviewMediaDialog

View File

@ -35,7 +35,7 @@ export const CityEditPage = observer(() => {
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
const [mediaId, setMediaId] = useState("");
const [activeMenuType, setActiveMenuType] = useState<
"thumbnail" | "watermark_lu" | "watermark_rd" | null
"thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null
>(null);
const { language } = languageStore;
const { id } = useParams();
@ -151,7 +151,7 @@ export const CityEditPage = observer(() => {
<div className="w-full flex flex-col gap-4 max-w-[300px] mx-auto">
<ImageUploadCard
title="Герб города"
imageKey="thumbnail"
imageKey="image"
imageUrl={selectedMedia?.id}
onImageClick={() => {
setIsPreviewMediaOpen(true);
@ -167,12 +167,22 @@ export const CityEditPage = observer(() => {
setActiveMenuType(null);
}}
onSelectFileClick={() => {
setActiveMenuType("thumbnail");
setActiveMenuType("image");
setIsSelectMediaOpen(true);
}}
setUploadMediaOpen={() => {
setIsUploadMediaOpen(true);
setActiveMenuType("thumbnail");
setActiveMenuType("image");
}}
setHardcodeType={(type) => {
setActiveMenuType(
type as
| "thumbnail"
| "watermark_lu"
| "watermark_rd"
| "image"
| null
);
}}
/>
</div>
@ -198,14 +208,21 @@ export const CityEditPage = observer(() => {
open={isSelectMediaOpen}
onClose={() => setIsSelectMediaOpen(false)}
onSelectMedia={handleMediaSelect}
mediaType={3} // Тип медиа для иконок
mediaType={1} // Тип медиа для иконок
/>
<UploadMediaDialog
open={isUploadMediaOpen}
onClose={() => setIsUploadMediaOpen(false)}
afterUpload={handleMediaSelect}
hardcodeType={activeMenuType}
hardcodeType={
activeMenuType as
| "thumbnail"
| "watermark_lu"
| "watermark_rd"
| "image"
| null
}
/>
<PreviewMediaDialog

View File

@ -11,7 +11,9 @@ export const CityListPage = observer(() => {
const { cities, getCities, deleteCity } = cityStore;
const navigate = useNavigate();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState<number | null>(null);
const [ids, setIds] = useState<number[]>([]);
const { language } = languageStore;
useEffect(() => {
@ -57,18 +59,18 @@ export const CityListPage = observer(() => {
align: "center",
headerAlign: "center",
width: 200,
renderCell: (params: GridRenderCellParams) => {
return (
<div className="flex h-full gap-7 justify-center items-center">
<button onClick={() => navigate(`/city/${params.row.id}/edit`)}>
<Pencil size={20} className="text-blue-500" />
</button>
<button onClick={() => navigate(`/city/${params.row.id}`)}>
{/* <button onClick={() => navigate(`/city/${params.row.id}`)}>
<Eye size={20} className="text-green-500" />
</button>
</button> */}
<button
onClick={() => {
onClick={(e) => {
e.stopPropagation();
setIsDeleteModalOpen(true);
setRowId(params.row.id);
}}
@ -96,11 +98,29 @@ export const CityListPage = observer(() => {
<h1 className="text-2xl">Города</h1>
<CreateButton label="Создать город" path="/city/create" />
</div>
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 0 }}
>
<button
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
onClick={() => setIsBulkDeleteModalOpen(true)}
>
<Trash2 size={20} className="text-white" /> Удалить выбранные (
{ids.length})
</button>
</div>
<DataGrid
rows={rows}
columns={columns}
hideFooterPagination
hideFooter
checkboxSelection
onRowSelectionModelChange={(newSelection) => {
setIds(Array.from(newSelection.ids as unknown as number[]));
}}
/>
</div>
@ -119,6 +139,20 @@ export const CityListPage = observer(() => {
setRowId(null);
}}
/>
<DeleteModal
open={isBulkDeleteModalOpen}
onDelete={async () => {
await Promise.all(ids.map((id) => deleteCity(id.toString())));
toast.success("Города успешно удалены");
getCities(language);
setIsBulkDeleteModalOpen(false);
setIds([]);
}}
onCancel={() => {
setIsBulkDeleteModalOpen(false);
}}
/>
</>
);
});

View File

@ -10,7 +10,9 @@ export const CountryListPage = observer(() => {
const { countries, getCountries, deleteCountry } = countryStore;
const navigate = useNavigate();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState<string | null>(null);
const [ids, setIds] = useState<number[]>([]);
const { language } = languageStore;
useEffect(() => {
@ -52,7 +54,8 @@ export const CountryListPage = observer(() => {
<Eye size={20} className="text-green-500" />
</button> */}
<button
onClick={() => {
onClick={(e) => {
e.stopPropagation();
setIsDeleteModalOpen(true);
setRowId(params.row.code);
}}
@ -80,14 +83,37 @@ export const CountryListPage = observer(() => {
<h1 className="text-2xl">Страны</h1>
<CreateButton label="Создать страну" path="/country/create" />
</div>
<DataGrid rows={rows} columns={columns} hideFooter />
<div
className="flex justify-end mb-5 duration-300"
style={{ opacity: ids.length > 0 ? 1 : 0 }}
>
<button
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
onClick={() => setIsBulkDeleteModalOpen(true)}
>
<Trash2 size={20} className="text-white" /> Удалить выбранные (
{ids.length})
</button>
</div>
<DataGrid
rows={rows}
columns={columns}
hideFooter
checkboxSelection
onRowSelectionModelChange={(newSelection) => {
console.log(newSelection);
setIds(Array.from(newSelection.ids as unknown as number[]));
}}
/>
</div>
<DeleteModal
open={isDeleteModalOpen}
onDelete={async () => {
if (!rowId) return;
await deleteCountry(rowId, language);
await deleteCountry(rowId);
setRowId(null);
setIsDeleteModalOpen(false);
}}
@ -96,6 +122,19 @@ export const CountryListPage = observer(() => {
setIsDeleteModalOpen(false);
}}
/>
<DeleteModal
open={isBulkDeleteModalOpen}
onDelete={async () => {
await Promise.all(ids.map((id) => deleteCountry(id.toString())));
getCountries(language);
setIsBulkDeleteModalOpen(false);
setIds([]);
}}
onCancel={() => {
setIsBulkDeleteModalOpen(false);
}}
/>
</>
);
});

View File

@ -132,6 +132,8 @@ class MapStore {
data = {
route_number: properties.name || "Новый маршрут",
path: geometry.coordinates,
center_latitude: geometry.coordinates[0][1],
center_longitude: geometry.coordinates[0][0],
};
} else if (featureType === "sight") {
data = {
@ -192,17 +194,27 @@ class MapStore {
oldData = this.sights.find((f) => f.id === numericId);
}
console.log(oldData);
console.log(data);
const response = await languageInstance("ru").patch(
`/${featureType}/${numericId}`,
{
...oldData,
latitude: data.latitude,
longitude: data.longitude,
}
);
let response;
if (featureType !== "route") {
response = await languageInstance("ru").patch(
`/${featureType}/${numericId}`,
{
...oldData,
latitude: data.latitude,
longitude: data.longitude,
}
);
} else {
response = await languageInstance("ru").patch(
`/${featureType}/${numericId}`,
{
...oldData,
path: data.path,
center_latitude: data.path[0][1],
center_longitude: data.path[0][0],
}
);
}
if (featureType === "route") {
const index = this.routes.findIndex((f) => f.id === numericId);
@ -1078,7 +1090,10 @@ class MapService {
);
if (!featureAtPixel) {
if (!ctrlKey) this.unselect();
if (ctrlKey) {
// При ctrl + клик вне сущности сбрасываем выбор
this.setSelectedIds(new Set());
}
return;
}
@ -1086,11 +1101,16 @@ class MapService {
if (featureId === undefined) return;
if (ctrlKey) {
// При ctrl + клик на сущность добавляем/удаляем её из выбора
const newSet = new Set(this.selectedIds);
if (newSet.has(featureId)) newSet.delete(featureId);
else newSet.add(featureId);
if (newSet.has(featureId)) {
newSet.delete(featureId);
} else {
newSet.add(featureId);
}
this.setSelectedIds(newSet);
} else {
// При обычном клике на сущность выбираем только её
this.setSelectedIds(new Set([featureId]));
}
}
@ -1153,14 +1173,12 @@ class MapService {
mapStore
.deleteFeature(recourse, numericId)
.then(() => {
toast.success("Объект успешно удален");
if (stateBeforeDelete)
this.addStateToHistory("delete", stateBeforeDelete);
this.vectorSource.removeFeature(feature);
this.unselect();
})
.catch((err) => {
toast.error("Ошибка при удалении объекта");
console.error("Delete failed:", err);
});
}
@ -1196,7 +1214,6 @@ class MapService {
}
})
.catch((err) => {
toast.error("Произошла ошибка при массовом удалении");
console.error("Bulk delete failed:", err);
});
}
@ -1307,12 +1324,9 @@ class MapService {
try {
await mapStore.updateFeature(featureType, featureGeoJSON);
toast.success(`"${feature.get("name")}" успешно обновлен.`);
} catch (error) {
console.error("Failed to update feature:", error);
toast.error(
`Не удалось обновить "${feature.get("name")}". Отмена изменений...`
);
this.undo();
}
}
@ -1338,7 +1352,6 @@ class MapService {
featureType === "route"
? createdFeatureData.route_number
: createdFeatureData.name;
toast.success(`"${newName}" создано.`);
const newFeatureId = `${featureType}-${createdFeatureData.id}`;
feature.setId(newFeatureId);
@ -1507,15 +1520,20 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
);
const handleCheckboxChange = useCallback(
// @ts-ignore
(id) => {
(id: string | number | undefined) => {
if (id === undefined) return;
const newSet = new Set(selectedIds);
if (newSet.has(id)) newSet.delete(id);
else newSet.add(id);
if (newSet.has(id)) {
newSet.delete(id);
} else {
newSet.add(id);
}
setSelectedIds(newSet);
if (mapService) {
mapService.setSelectedIds(newSet);
}
},
[selectedIds, setSelectedIds]
[selectedIds, setSelectedIds, mapService]
);
const handleBulkDelete = useCallback(() => {
@ -1990,13 +2008,11 @@ export const MapPage: React.FC = () => {
);
const handleMapClick = useCallback(
(event: any) => {
if (!mapServiceInstance || isLassoActive) return;
const ctrlKey =
event.originalEvent.ctrlKey || event.originalEvent.metaKey;
mapServiceInstance.handleMapClick(event, ctrlKey);
(event: MapBrowserEvent<any>) => {
if (!mapServiceInstance) return;
mapServiceInstance.handleMapClick(event, event.originalEvent.ctrlKey);
},
[mapServiceInstance, isLassoActive]
[mapServiceInstance]
);
useEffect(() => {

View File

@ -35,11 +35,49 @@ export const RouteCreatePage = observer(() => {
const [centerLng, setCenterLng] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
useEffect(() => {
carrierStore.getCarriers(language);
articlesStore.getArticleList();
}, [language]);
const validateCoordinates = (value: string) => {
try {
const lines = value.trim().split("\n");
const coordinates = lines.map((line) => {
const [lat, lon] = line
.trim()
.split(/[\s,]+/)
.map(Number);
return [lat, lon];
});
if (coordinates.length === 0) {
return "Введите хотя бы одну пару координат";
}
if (
!coordinates.every(
(point) => Array.isArray(point) && point.length === 2
)
) {
return "Каждая строка должна содержать две координаты";
}
if (
!coordinates.every((point) =>
point.every((coord) => !isNaN(coord) && typeof coord === "number")
)
) {
return "Координаты должны быть числами";
}
return true;
} catch {
return "Неверный формат координат";
}
};
const handleCreateRoute = async () => {
try {
setIsLoading(true);
@ -52,16 +90,24 @@ export const RouteCreatePage = observer(() => {
const center_latitude = centerLat ? Number(centerLat) : undefined;
const center_longitude = centerLng ? Number(centerLng) : undefined;
const route_direction = direction === "forward";
const validationResult = validateCoordinates(routeCoords);
if (validationResult !== true) {
toast.error(validationResult);
return;
}
// Координаты маршрута как массив массивов чисел
const path = routeCoords
.trim()
.split("\n")
.map((line) =>
line
.split(" ")
.map((coord) => Number(coord.trim()))
.filter((n) => !isNaN(n))
)
.filter((arr) => arr.length === 2);
.map((line) => {
const [lat, lon] = line
.trim()
.split(/[\s,]+/)
.map(Number);
return [lat, lon];
});
// Собираем объект маршрута
const newRoute: Partial<Route> = {
@ -141,9 +187,33 @@ export const RouteCreatePage = observer(() => {
className="w-full"
label="Координаты маршрута"
multiline
minRows={3}
minRows={4}
value={routeCoords}
onChange={(e) => setRouteCoords(e.target.value)}
onChange={(e) => {
const newValue = e.target.value;
setRouteCoords(newValue);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const lines = routeCoords.split("\n");
const lastLine = lines[lines.length - 1];
// Если мы на последней строке и она не пустая
if (lastLine && lastLine.trim()) {
e.preventDefault();
const newValue = routeCoords + "\n";
setRouteCoords(newValue);
}
}
}}
error={validateCoordinates(routeCoords) !== true}
helperText={
typeof validateCoordinates(routeCoords) === "string"
? validateCoordinates(routeCoords)
: "Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)"
}
placeholder="55.7558 37.6173
55.7539 37.6208"
/>
<TextField
className="w-full"

View File

@ -27,6 +27,8 @@ export const RouteEditPage = observer(() => {
const { editRouteData } = routeStore;
const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
const [coordinates, setCoordinates] = useState<string>("");
useEffect(() => {
const fetchData = async () => {
const response = await routeStore.getRoute(Number(id));
@ -37,6 +39,15 @@ export const RouteEditPage = observer(() => {
fetchData();
}, [id, language]);
useEffect(() => {
if (editRouteData.path && editRouteData.path.length > 0) {
const formattedPath = editRouteData.path
.map((coords) => coords.join(" "))
.join("\n");
setCoordinates(formattedPath);
}
}, [editRouteData.path]);
const handleSave = async () => {
setIsLoading(true);
await routeStore.editRoute(Number(id));
@ -44,6 +55,43 @@ export const RouteEditPage = observer(() => {
setIsLoading(false);
};
const validateCoordinates = (value: string) => {
try {
const lines = value.trim().split("\n");
const coordinates = lines.map((line) => {
const [lat, lon] = line
.trim()
.split(/[\s,]+/)
.map(Number);
return [lat, lon];
});
if (coordinates.length === 0) {
return "Введите хотя бы одну пару координат";
}
if (
!coordinates.every(
(point) => Array.isArray(point) && point.length === 2
)
) {
return "Каждая строка должна содержать две координаты";
}
if (
!coordinates.every((point) =>
point.every((coord) => !isNaN(coord) && typeof coord === "number")
)
) {
return "Координаты должны быть числами";
}
return true;
} catch {
return "Неверный формат координат";
}
};
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
@ -105,15 +153,46 @@ export const RouteEditPage = observer(() => {
className="w-full"
label="Координаты маршрута"
multiline
minRows={3}
value={editRouteData.path.map((p) => p.join(" ")).join("\n") || ""}
onChange={(e) =>
routeStore.setEditRouteData({
path: e.target.value
.split("\n")
.map((line) => line.split(" ").map(Number)),
})
minRows={4}
value={coordinates}
onChange={(e) => {
const newValue = e.target.value;
setCoordinates(newValue);
const validationResult = validateCoordinates(newValue);
if (validationResult === true) {
const lines = newValue.trim().split("\n");
const path = lines.map((line) => {
const [lat, lon] = line
.trim()
.split(/[\s,]+/)
.map(Number);
return [lat, lon];
});
routeStore.setEditRouteData({ path });
}
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const lines = coordinates.split("\n");
const lastLine = lines[lines.length - 1];
// Если мы на последней строке и она не пустая
if (lastLine && lastLine.trim()) {
e.preventDefault();
const newValue = coordinates + "\n";
setCoordinates(newValue);
}
}
}}
error={validateCoordinates(coordinates) !== true}
helperText={
typeof validateCoordinates(coordinates) === "string"
? validateCoordinates(coordinates)
: "Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)"
}
placeholder="55.7558 37.6173
55.7539 37.6208"
/>
<TextField
className="w-full"

View File

@ -1,5 +1,5 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { languageStore, routeStore } from "@shared";
import { carrierStore, languageStore, routeStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Map, Pencil, Trash2, Minus } from "lucide-react";
@ -9,6 +9,7 @@ import { LanguageSwitcher } from "@widgets";
export const RouteListPage = observer(() => {
const { routes, getRoutes, deleteRoute } = routeStore;
const { carriers, getCarriers } = carrierStore;
const navigate = useNavigate();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
@ -17,19 +18,27 @@ export const RouteListPage = observer(() => {
const { language } = languageStore;
useEffect(() => {
getRoutes();
const fetchData = async () => {
await getCarriers("ru");
await getCarriers("en");
await getCarriers("zh");
await getRoutes();
};
fetchData();
}, [language]);
const columns: GridColDef[] = [
{
field: "carrier",
field: "carrier_id",
headerName: "Перевозчик",
width: 250,
renderCell: (params: GridRenderCellParams) => {
return (
<div className="w-full h-full flex items-center">
{params.value ? (
params.value
carriers[language].data.find(
(carrier) => carrier.id == params.value
)?.short_name
) : (
<Minus size={20} className="text-red-500" />
)}
@ -105,7 +114,7 @@ export const RouteListPage = observer(() => {
const rows = routes.data.map((route) => ({
id: route.id,
carrier: route.carrier,
carrier_id: route.carrier_id,
route_number: route.route_number,
route_direction: route.route_direction ? "Прямой" : "Обратный",
}));

View File

@ -8,33 +8,62 @@ import {
InputLabel,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react";
import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { stationsStore } from "@shared";
import { useState } from "react";
import { stationsStore, languageStore, cityStore } from "@shared";
import { useEffect, useState } from "react";
import { LanguageSwitcher } from "@widgets";
export const StationCreatePage = observer(() => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [systemName, setSystemName] = useState("");
const [direction, setDirection] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
const {
createStationData,
setCreateCommonData,
createStation,
setLanguageCreateStationData,
} = stationsStore;
const { cities, getCities } = cityStore;
const [coordinates, setCoordinates] = useState<string>("");
useEffect(() => {
if (
createStationData.common.latitude !== 0 ||
createStationData.common.longitude !== 0
) {
setCoordinates(
`${createStationData.common.latitude}, ${createStationData.common.longitude}`
);
}
}, [createStationData.common.latitude, createStationData.common.longitude]);
const handleCreate = async () => {
try {
setIsLoading(true);
await stationsStore.createStation(name, systemName, direction);
await createStation();
toast.success("Станция успешно создана");
navigate("/station");
} catch (error) {
console.error("Error creating station:", error);
toast.error("Ошибка при создании станции");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
const fetchCities = async () => {
await getCities("ru");
await getCities("en");
await getCities("zh");
};
fetchCities();
}, []);
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
@ -47,44 +76,123 @@ export const StationCreatePage = observer(() => {
Назад
</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">{name}</h1>
<h1 className="text-3xl break-words">Создание станции</h1>
</div>
<TextField
className="w-full"
fullWidth
label="Название"
value={createStationData[language].name || ""}
required
value={name}
onChange={(e) => setName(e.target.value)}
/>
<TextField
className="w-full"
label="Системное название"
required
value={systemName}
onChange={(e) => setSystemName(e.target.value)}
onChange={(e) =>
setLanguageCreateStationData(language, {
name: e.target.value,
})
}
/>
<FormControl fullWidth>
<InputLabel>Направление</InputLabel>
<InputLabel id="direction-label">Прямой/обратный маршрут</InputLabel>
<Select
value={direction}
label="Направление"
onChange={(e) => setDirection(e.target.value)}
required
labelId="direction-label"
value={createStationData.common.direction ? "Прямой" : "Обратный"}
label="Прямой/обратный маршрут"
onChange={(e) =>
setCreateCommonData({
direction: e.target.value === "Прямой",
})
}
>
<MenuItem value="forward">Прямое</MenuItem>
<MenuItem value="backward">Обратное</MenuItem>
<MenuItem value="Прямой">Прямой</MenuItem>
<MenuItem value="Обратный">Обратный</MenuItem>
</Select>
</FormControl>
<TextField
fullWidth
label="Описание"
value={createStationData[language].description || ""}
onChange={(e) =>
setLanguageCreateStationData(language, {
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 = cities["ru"].data.find(
(city) => city.id === e.target.value
);
setCreateCommonData({
city_id: e.target.value as number,
city: selectedCity?.name || "",
});
}}
>
{cities["ru"].data.map((city) => (
<MenuItem key={city.id} value={city.id}>
{city.name}
</MenuItem>
))}
</Select>
</FormControl>
<Button
variant="contained"
color="primary"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleCreate}
disabled={isLoading || !name || !systemName || !direction}
disabled={isLoading || !createStationData[language]?.name}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />

View File

@ -29,6 +29,18 @@ export const StationEditPage = observer(() => {
setLanguageEditStationData,
} = stationsStore;
const { cities, getCities } = cityStore;
const [coordinates, setCoordinates] = useState<string>("");
useEffect(() => {
if (
editStationData.common.latitude !== 0 ||
editStationData.common.longitude !== 0
) {
setCoordinates(
`${editStationData.common.latitude}, ${editStationData.common.longitude}`
);
}
}, [editStationData.common.latitude, editStationData.common.longitude]);
const handleEdit = async () => {
try {
@ -71,7 +83,7 @@ export const StationEditPage = observer(() => {
</div>
<div className="flex flex-col gap-10 w-full items-end">
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
<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
@ -128,16 +140,33 @@ export const StationEditPage = observer(() => {
<TextField
fullWidth
label="Координаты"
value={`${editStationData.common.latitude} ${editStationData.common.longitude}`}
value={coordinates}
onChange={(e) => {
const [latitude, longitude] = e.target.value.split(" ").map(Number);
if (!isNaN(latitude) && !isNaN(longitude)) {
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: latitude,
longitude: longitude,
latitude: lat,
longitude: lon,
});
} else {
setEditCommonData({
latitude: 0,
longitude: 0,
});
}
}}
placeholder="Введите координаты в формате: широта долгота (можно использовать запятые или пробелы)"
/>
<FormControl fullWidth>
@ -146,7 +175,7 @@ export const StationEditPage = observer(() => {
value={editStationData.common.city_id || ""}
label="Город"
onChange={(e) => {
const selectedCity = cities[language].data.find(
const selectedCity = cities["ru"].data.find(
(city) => city.id === e.target.value
);
setEditCommonData({
@ -155,7 +184,7 @@ export const StationEditPage = observer(() => {
});
}}
>
{cities[language].data.map((city) => (
{cities["ru"].data.map((city) => (
<MenuItem key={city.id} value={city.id}>
{city.name}
</MenuItem>

View File

@ -21,7 +21,7 @@ export const StationPreviewPage = observer(() => {
}, [id, language]);
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<Paper className="w-full p-3 py-5 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex justify-between items-center">
<button

View File

@ -9,7 +9,7 @@ export const MEDIA_TYPE_LABELS = {
};
export const MEDIA_TYPE_VALUES = {
photo: 1,
image: 1,
video: 2,
icon: 3,
thumbnail: 3,

View File

@ -132,7 +132,7 @@ export const PreviewMediaDialog = observer(
sx={{ width: "50%" }}
/>
<Box className="flex gap-4">
<Box className="flex gap-4 h-[40vh]">
<Paper
elevation={2}
sx={{
@ -142,7 +142,6 @@ export const PreviewMediaDialog = observer(
alignItems: "center",
justifyContent: "center",
}}
className="max-h-[40vh]"
>
<MediaViewer
media={{
@ -150,7 +149,6 @@ export const PreviewMediaDialog = observer(
media_type: media.media_type,
filename: media.filename,
}}
fullHeight
/>
</Paper>

View File

@ -102,7 +102,6 @@ export const SelectMediaDialog = observer(
filteredMedia = filteredMedia.filter(
(mediaItem) => mediaItem.media_type === mediaType
);
console.log(filteredMedia);
}
return (
@ -163,7 +162,13 @@ export const SelectMediaDialog = observer(
},
}}
>
<ListItemText primary={mediaItem.media_name} />
<ListItemText
primary={
mediaItem.media_name
? mediaItem.media_name
: mediaItem.filename
}
/>
</ListItemButton>
)
)

View File

@ -31,7 +31,7 @@ interface UploadMediaDialogProps {
media_type: number;
}) => void;
afterUploadSight?: (id: string) => void;
hardcodeType?: "thumbnail" | "watermark_lu" | "watermark_rd" | null;
hardcodeType?: "thumbnail" | "watermark_lu" | "watermark_rd" | "image" | null;
}
export const UploadMediaDialog = observer(

View File

@ -64,7 +64,7 @@ class CarrierStore {
getCarriers = async (language: Language) => {
if (this.carriers[language as keyof Carriers].loaded) return;
const response = await authInstance.get("/carrier");
const response = await languageInstance(language).get("/carrier");
runInAction(() => {
this.carriers[language as keyof Carriers].data = response.data;
@ -108,46 +108,94 @@ class CarrierStore {
return this.carrier[id];
};
createCarrier = async (
createCarrierData = {
city_id: 0,
logo: "",
ru: {
full_name: "",
short_name: "",
slogan: "",
},
en: {
full_name: "",
short_name: "",
slogan: "",
},
zh: {
full_name: "",
short_name: "",
slogan: "",
},
};
setCreateCarrierData = (
fullName: string,
shortName: string,
cityId: number,
slogan: string,
logoId: string
logoId: string,
language: Language
) => {
const { language } = languageStore;
const cityName =
cityStore.cities[language].data.find((city) => city.id === cityId)
?.name || "";
const response = await languageInstance(language).post("/carrier", {
this.createCarrierData.city_id = cityId;
this.createCarrierData.logo = logoId;
this.createCarrierData[language] = {
full_name: fullName,
short_name: shortName,
slogan: slogan,
};
};
createCarrier = async () => {
const { language } = languageStore;
const cityName =
cityStore.cities[language].data.find(
(city) => city.id === this.createCarrierData.city_id
)?.name || "";
const payload = {
full_name: this.createCarrierData[language].full_name,
short_name: this.createCarrierData[language].short_name,
city: cityName,
city_id: cityId,
slogan,
logo: logoId,
});
city_id: this.createCarrierData.city_id,
slogan: this.createCarrierData[language].slogan,
...(this.createCarrierData.logo
? { logo: this.createCarrierData.logo }
: {}),
};
const response = await languageInstance(language).post("/carrier", payload);
const carrierId = response.data.id;
runInAction(() => {
this.carriers[language].data.push(response.data);
});
// Create translations for other languages
for (const lang of ["ru", "en", "zh"].filter((l) => l !== language)) {
await languageInstance(lang as Language).patch(`/carrier/${carrierId}`, {
full_name: fullName,
short_name: shortName,
const patchPayload = {
// @ts-ignore
full_name: this.createCarrierData[lang as any].full_name as string,
// @ts-ignore
short_name: this.createCarrierData[lang as any].short_name as string,
city: cityName,
city_id: cityId,
slogan,
logo: logoId,
city_id: this.createCarrierData.city_id,
// @ts-ignore
slogan: this.createCarrierData[lang as any].slogan as string,
...(this.createCarrierData.logo
? { logo: this.createCarrierData.logo }
: {}),
};
await languageInstance(lang as Language).patch(
`/carrier/${carrierId}`,
patchPayload
);
runInAction(() => {
this.carriers[lang as keyof Carriers].data.push(response.data);
});
}
runInAction(() => {
for (const language of ["ru", "en", "zh"] as const) {
this.carriers[language].data.push(response.data);
}
});
};
editCarrierData = {
@ -206,31 +254,29 @@ class CarrierStore {
};
editCarrier = async (id: number) => {
const { language } = languageStore;
const cityName =
cityStore.cities[languageStore.language].data.find(
cityStore.cities[language].data.find(
(city) => city.id === this.editCarrierData.city_id
)?.name || "";
for (const language of ["ru", "en", "zh"] as const) {
const response = await languageInstance(language).patch(
`/carrier/${id}`,
{
...this.editCarrierData[language],
city: cityName,
logo: this.editCarrierData.logo,
}
);
for (const lang of ["ru", "en", "zh"] as const) {
const response = await languageInstance(lang).patch(`/carrier/${id}`, {
...this.editCarrierData[lang],
city: cityName,
city_id: this.editCarrierData.city_id,
logo: this.editCarrierData.logo,
});
runInAction(() => {
if (this.carrier[id]) {
this.carrier[id][language] = response.data;
}
for (const language of ["ru", "en", "zh"] as const) {
this.carriers[language].data = this.carriers[language].data.map(
(carrier: Carrier) =>
carrier.id === id ? { ...carrier, ...response.data } : carrier
);
this.carrier[id][lang] = response.data;
}
this.carriers[lang].data = this.carriers[lang].data.map(
(carrier: Carrier) =>
carrier.id === id ? { ...carrier, ...response.data } : carrier
);
});
}
};

View File

@ -85,7 +85,7 @@ class CityStore {
return;
}
const response = await authInstance.get(`/city`);
const response = await languageInstance(language).get(`/city`);
runInAction(() => {
this.cities[language].data = response.data;
@ -98,7 +98,7 @@ class CityStore {
return;
}
const response = await authInstance.get(`/city/${code}`);
const response = await languageInstance(language).get(`/city/${code}`);
runInAction(() => {
if (!this.city[code]) {
@ -170,15 +170,20 @@ class CityStore {
try {
// Create city in primary language
const cityResponse = await languageInstance(language).post("/city", {
const cityPayload = {
name,
country:
countryStore.countries[language as keyof CashedCountries]?.data.find(
(c) => c.code === country_code
)?.name || "",
country_code,
arms: arms || "",
});
...(arms ? { arms } : {}),
};
const cityResponse = await languageInstance(language).post(
"/city",
cityPayload
);
const cityId = cityResponse.data.id;
@ -194,14 +199,16 @@ class CityStore {
(c) => c.code === country_code
)?.name || "";
const patchPayload = {
name: secondaryName || "",
country: countryName,
country_code: country_code || "",
...(arms ? { arms } : {}),
};
const patchResponse = await languageInstance(secondaryLanguage).patch(
`/city/${cityId}`,
{
name: secondaryName || "",
country: countryName,
country_code: country_code || "",
arms: arms || "",
}
patchPayload
);
runInAction(() => {

View File

@ -91,14 +91,16 @@ class CountryStore {
return response.data;
};
deleteCountry = async (code: string, language: keyof CashedCountries) => {
deleteCountry = async (code: string) => {
await authInstance.delete(`/country/${code}`);
runInAction(() => {
this.countries[language].data = this.countries[language].data.filter(
(country) => country.code !== code
);
this.countries[language].loaded = true;
for (const lang of ["ru", "en", "zh"]) {
this.countries[lang as keyof CashedCountries].data = this.countries[
lang as keyof CashedCountries
].data.filter((country) => country.code !== code);
}
this.country[code] = {
ru: null,
en: null,

View File

@ -64,6 +64,10 @@ type Station = {
};
};
type CreateStationData = {
[key in Language]: StationLanguageData;
} & { common: StationCommonData };
class StationsStore {
stations: Station[] = [];
station: Station | null = null;
@ -139,6 +143,51 @@ class StationsStore {
},
};
createStationData: CreateStationData = {
ru: {
name: "",
system_name: "",
description: "",
address: "",
loaded: false,
},
en: {
name: "",
system_name: "",
description: "",
address: "",
loaded: false,
},
zh: {
name: "",
system_name: "",
description: "",
address: "",
loaded: false,
},
common: {
city: "",
city_id: 0,
direction: false,
icon: "",
latitude: 0,
longitude: 0,
offset_x: 0,
offset_y: 0,
transfers: {
bus: "",
metro_blue: "",
metro_green: "",
metro_orange: "",
metro_purple: "",
metro_red: "",
train: "",
tram: "",
trolleybus: "",
},
},
};
constructor() {
makeAutoObservable(this);
}
@ -172,9 +221,6 @@ class StationsStore {
};
getEditStation = async (id: number) => {
if (this.editStationData.ru.loaded) {
return;
}
const ruResponse = await languageInstance("ru").get(`/station/${id}`);
const enResponse = await languageInstance("en").get(`/station/${id}`);
const zhResponse = await languageInstance("zh").get(`/station/${id}`);
@ -336,35 +382,125 @@ class StationsStore {
});
};
createStation = async (
name: string,
systemName: string,
direction: string
setCreateCommonData = (data: Partial<StationCommonData>) => {
this.createStationData.common = {
...this.createStationData.common,
...data,
};
};
setLanguageCreateStationData = (
language: Language,
data: Partial<StationLanguageData>
) => {
const response = await authInstance.post("/station", {
station_name: name,
system_name: systemName,
direction,
this.createStationData[language] = {
...this.createStationData[language],
...data,
};
};
createStation = async () => {
const { language } = languageStore;
let commonDataPayload: Partial<StationCommonData> = {
city_id: this.createStationData.common.city_id,
direction: this.createStationData.common.direction,
icon: this.createStationData.common.icon,
latitude: this.createStationData.common.latitude,
longitude: this.createStationData.common.longitude,
offset_x: this.createStationData.common.offset_x,
offset_y: this.createStationData.common.offset_y,
transfers: this.createStationData.common.transfers,
city: this.createStationData.common.city,
};
if (this.createStationData.common.icon === "") {
delete commonDataPayload.icon;
}
// First create station in Russian
const { name, description, address } = this.createStationData[language];
const response = await languageInstance(language).post("/station", {
name: name || "",
system_name: name || "", // system_name is often derived from name
description: description || "",
address: address || "",
...commonDataPayload,
});
runInAction(() => {
this.stations.push(response.data);
const newStation = response.data as Station;
if (!this.stationPreview[newStation.id]) {
this.stationPreview[newStation.id] = {
ru: { loaded: false, data: newStation },
en: { loaded: false, data: newStation },
zh: { loaded: false, data: newStation },
};
}
this.stationPreview[newStation.id]["ru"] = {
loaded: true,
data: newStation,
};
this.stationPreview[newStation.id]["en"] = {
loaded: true,
data: newStation,
this.stationLists[language].data.push(response.data);
});
const stationId = response.data.id;
// Then update for other languages
for (const lang of ["ru", "en", "zh"].filter(
(lang) => lang !== language
) as Language[]) {
const { name, description, address } = this.createStationData[lang];
const response = await languageInstance(lang).patch(
`/station/${stationId}`,
{
name: name || "",
system_name: name || "", // system_name is often derived from name
description: description || "",
address: address || "",
...commonDataPayload,
}
);
runInAction(() => {
this.stationLists[lang].data.push(response.data);
});
}
runInAction(() => {
this.createStationData = {
ru: {
name: "",
system_name: "",
description: "",
address: "",
loaded: false,
},
en: {
name: "",
system_name: "",
description: "",
address: "",
loaded: false,
},
zh: {
name: "",
system_name: "",
description: "",
address: "",
loaded: false,
},
common: {
city: "",
city_id: 0,
direction: false,
icon: "",
latitude: 0,
longitude: 0,
offset_x: 0,
offset_y: 0,
transfers: {
bus: "",
metro_blue: "",
metro_green: "",
metro_orange: "",
metro_purple: "",
metro_red: "",
train: "",
tram: "",
trolleybus: "",
},
},
};
});
return response.data;
};
// Reset editStationData when navigating away or after saving

View File

@ -5,7 +5,7 @@ import { editSightStore } from "@shared";
import { toast } from "react-toastify";
interface ImageUploadCardProps {
title: string;
imageKey?: "thumbnail" | "watermark_lu" | "watermark_rd";
imageKey?: "thumbnail" | "watermark_lu" | "watermark_rd" | "image";
imageUrl: string | null | undefined;
onImageClick: () => void;
onDeleteImageClick: () => void;