fix: Fix Map page

This commit is contained in:
2025-06-12 22:50:43 +03:00
parent 27cb644242
commit 300ff262ce
41 changed files with 2216 additions and 1055 deletions

View File

@@ -0,0 +1,62 @@
export const CarrierSvg = () => {
return (
<svg
fill="#000000"
height="26px"
width="26px"
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 489.785 489.785"
>
<g id="XMLID_196_">
<path
id="XMLID_203_"
d="M409.772,379.327l-81.359-124.975c-5.884-9.054-15.925-13.119-25.987-13.119
c-2.082,0-6.392,0.05-11.051,0.115c-0.363-0.61-0.742-1.215-1.355-1.627l-20.492-13.609c-2.364-1.569-5.434-1.486-7.701,0.182
l-16.948,12.508l-16.959-12.508c-2.285-1.668-5.337-1.751-7.72-0.182l-20.455,13.609c-0.578,0.377-0.945,0.907-1.282,1.461
c-4.828,0.031-9.327,0.057-11.222,0.057c-10.016,0-20.011,4.119-25.859,13.113L80.022,379.327
c-8.65,13.267-5.149,31.008,7.896,39.992l18.06,12.449c10.887-25.926,28.868-48.094,51.45-64.279l4.657-7.162v3.861
c16.364-10.811,34.941-18.477,54.885-22.234c-5.926-13.152-10.899-28.819-14.546-43.586c-4.249-17.232-6.741-33.201-6.741-42.245
c0-3.351,0.433-6.579,1.09-9.727l14.8,48.975c0.766,2.565,2.984,4.417,5.641,4.73c0.268,0.03,0.529,0.046,0.784,0.046
c2.365,0,4.602-1.25,5.818-3.34l11.538-19.873l3.246,3.235c-7.768,10.276-10.82,39.199-12.005,60.314
c5.994-0.734,12.066-1.222,18.254-1.222c6.201,0,12.292,0.497,18.304,1.23c-1.186-21.114-4.237-50.037-12.024-60.322l3.246-3.255
l11.574,19.892c1.216,2.09,3.422,3.34,5.805,3.34c0.255,0,0.522-0.016,0.779-0.046c2.655-0.314,4.874-2.166,5.659-4.73
l14.791-48.872c0.634,3.116,1.051,6.313,1.051,9.624c0,16.806-8.425,57.342-21.276,85.831
c19.981,3.768,38.588,11.453,54.953,22.291v-3.899l4.735,7.256c22.504,16.193,40.436,38.324,51.293,64.206l18.139-12.488
C414.919,410.335,418.403,392.594,409.772,379.327z M219.962,276.685l-8.613-28.53l12.388-8.24l12.322,9.088L219.962,276.685z
M269.783,276.685l-16.079-27.683l12.31-9.088l12.401,8.24L269.783,276.685z"
/>
<path
id="XMLID_202_"
d="M202.716,424.721l14.705,19.349c8.151-4.914,17.598-7.607,27.427-7.607c9.848,0,19.313,2.692,27.464,7.615
l14.705-19.363c-11.465-10.799-26.346-16.721-42.15-16.721C229.055,407.994,214.156,413.925,202.716,424.721z"
/>
<path
id="XMLID_201_"
d="M176.693,160.576c0.499,25.456,14.96,47.266,36.03,58.591c9.622,5.18,20.473,8.384,32.174,8.384
c11.683,0,22.503-3.198,32.114-8.368c21.063-11.311,35.579-33.117,36.06-58.582c-17.379,12.075-41.896,19.923-68.174,19.923
S194.096,172.676,176.693,160.576z"
/>
<path
id="XMLID_200_"
d="M174.741,100.132l-0.225,20.205c0.037,15.991,31.524,36.82,70.38,36.82
c38.855,0,70.314-20.829,70.331-36.82l-0.207-20.195c10.224-2.662,18.158-6.617,23.239-12.301
c3.981-4.434,6.267-9.902,6.267-16.783C344.528,39.883,299.879,0,244.897,0c-55.031,0-99.631,39.883-99.631,71.058
c0,6.881,2.273,12.34,6.236,16.783C156.585,93.524,164.529,97.479,174.741,100.132z"
/>
<path
id="XMLID_197_"
d="M244.848,356.925c-73.255,0-132.858,59.605-132.858,132.86h33.47c0-0.048,0-0.114,0-0.161v-0.031
c1.088-6.557,6.711-11.334,13.313-11.334c0.115,0,0.243,0.01,0.37,0.01l51.707,1.341c-0.973,3.247-1.648,6.619-1.648,10.176h71.322
c0-3.557-0.669-6.929-1.66-10.176l51.724-1.341c0.109,0,0.219-0.01,0.353-0.01c6.595,0,12.243,4.777,13.324,11.334v0.031
c0,0.047,0,0.113,0,0.161h33.44C377.706,416.53,318.122,356.925,244.848,356.925z M302.201,433.91l-27.562,36.317
c-6.389-9.687-17.325-16.104-29.792-16.104c-12.437,0-23.385,6.411-29.762,16.098l-27.555-36.3
c-4.699-6.194-4.11-14.923,1.392-20.424c15.452-15.443,35.689-23.166,55.943-23.166c20.249,0,40.484,7.723,55.961,23.179
C306.322,419.007,306.901,427.719,302.201,433.91z"
/>
</g>
</svg>
);
};

View File

@@ -7,22 +7,24 @@ import {
Users,
Earth,
Landmark,
BusFront,
GitBranch,
Car,
Table,
Notebook,
Split,
Newspaper,
PersonStanding,
Cpu,
BookImage,
} from "lucide-react";
import { CarrierSvg } from "./CarrierSvg";
export const DRAWER_WIDTH = 300;
interface NavigationItem {
id: string;
label: string;
icon: LucideIcon;
icon?: LucideIcon | React.ReactNode;
path?: string;
onClick?: () => void;
nestedItems?: NavigationItem[];
@@ -34,43 +36,6 @@ export const NAVIGATION_ITEMS: {
secondary: NavigationItem[];
} = {
primary: [
{
id: "countries",
label: "Страны",
icon: Earth,
path: "/country",
},
{
id: "cities",
label: "Города",
icon: Building2,
path: "/city",
},
{
id: "carriers",
label: "Перевозчики",
icon: BusFront,
path: "/carrier",
},
{
id: "snapshots",
label: "Снапшоты",
icon: GitBranch,
path: "/snapshot",
},
{
id: "map",
label: "Карта",
icon: Map,
path: "/map",
},
{
id: "devices",
label: "Устройства",
icon: Cpu,
path: "/devices",
},
{
id: "all",
label: "Все сущности",
@@ -106,15 +71,58 @@ export const NAVIGATION_ITEMS: {
icon: Split,
path: "/route",
},
{
id: "reference",
label: "Справочник",
icon: Notebook,
nestedItems: [
{
id: "countries",
label: "Страны",
icon: Earth,
path: "/country",
},
{
id: "cities",
label: "Города",
icon: Building2,
path: "/city",
},
{
id: "carriers",
label: "Перевозчики",
// @ts-ignore
icon: CarrierSvg,
path: "/carrier",
},
],
},
],
},
{
id: "vehicles",
label: "Транспорт",
icon: Car,
path: "/vehicle",
id: "snapshots",
label: "Снапшоты",
icon: GitBranch,
path: "/snapshot",
},
{
id: "map",
label: "Карта",
icon: Map,
path: "/map",
},
{
id: "devices",
label: "Устройства",
icon: Cpu,
path: "/devices",
},
// {
// id: "vehicles",
// label: "Транспорт",
// icon: Car,
// path: "/vehicle",
// },
{
id: "users",
label: "Пользователи",

View File

@@ -120,7 +120,6 @@ export const PreviewMediaDialog = observer(
disabled={isLoading}
/>
</Box>
<TextField
fullWidth
label="Тип медиа"
@@ -133,7 +132,7 @@ export const PreviewMediaDialog = observer(
sx={{ width: "50%" }}
/>
<Box className="flex gap-4 h-full">
<Box className="flex gap-4">
<Paper
elevation={2}
sx={{
@@ -142,8 +141,8 @@ export const PreviewMediaDialog = observer(
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: 400,
}}
className="max-h-[40vh]"
>
<MediaViewer
media={{
@@ -151,6 +150,7 @@ export const PreviewMediaDialog = observer(
media_type: media.media_type,
filename: media.filename,
}}
fullHeight
/>
</Paper>

View File

@@ -188,7 +188,7 @@ export const UploadMediaDialog = observer(
</Select>
</FormControl>
<Box className="flex gap-4 h-full">
<Box className="flex gap-4 h-[40vh]">
<Paper
elevation={2}
sx={{
@@ -197,7 +197,7 @@ export const UploadMediaDialog = observer(
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: 400,
height: "100%",
}}
>
{/* <MediaViewer

View File

@@ -1,4 +1,10 @@
import { authInstance, editSightStore, Language, languageStore } from "@shared";
import {
authInstance,
editSightStore,
Language,
languageStore,
languageInstance,
} from "@shared";
import { computed, makeAutoObservable, runInAction } from "mobx";
export type Article = {
@@ -6,6 +12,18 @@ export type Article = {
heading: string;
body: string;
service_name: string;
ru?: {
heading: string;
body: string;
};
en?: {
heading: string;
body: string;
};
zh?: {
heading: string;
body: string;
};
};
type Media = {
@@ -99,13 +117,25 @@ class ArticlesStore {
this.articleLoading = false;
};
getArticle = async (id: number) => {
getArticle = async (id: number, language?: Language) => {
this.articleLoading = true;
const response = await authInstance.get(`/article/${id}`);
runInAction(() => {
this.articleData = response.data;
});
if (language) {
const response = await languageInstance(language).get(`/article/${id}`);
runInAction(() => {
if (!this.articleData) {
this.articleData = { id, heading: "", body: "", service_name: "" };
}
this.articleData[language] = {
heading: response.data.heading,
body: response.data.body,
};
});
} else {
const response = await authInstance.get(`/article/${id}`);
runInAction(() => {
this.articleData = response.data;
});
}
this.articleLoading = false;
};
@@ -137,6 +167,20 @@ class ArticlesStore {
}
return null;
});
deleteArticles = async (ids: number[]) => {
for (const id of ids) {
await authInstance.delete(`/article/${id}`);
}
for (const id of ["ru", "en", "zh"] as Language[]) {
runInAction(() => {
this.articleList[id].data = this.articleList[id].data.filter(
(article) => !ids.includes(article.id)
);
});
}
};
}
export const articlesStore = new ArticlesStore();

View File

@@ -1,4 +1,4 @@
import { authInstance } from "@shared";
import { authInstance, cityStore, languageStore } from "@shared";
import { makeAutoObservable, runInAction } from "mobx";
export type Carrier = {
@@ -9,9 +9,9 @@ export type Carrier = {
city: string;
city_id: number;
logo: string;
main_color: string;
left_color: string;
right_color: string;
// main_color: string;
// left_color: string;
// right_color: string;
};
type Carriers = {
@@ -68,9 +68,9 @@ class CarrierStore {
city: "",
city_id: 0,
logo: "",
main_color: "",
left_color: "",
right_color: "",
// main_color: "",
// left_color: "",
// right_color: "",
};
}
this.carrier[id] = response.data;
@@ -81,22 +81,22 @@ class CarrierStore {
createCarrier = async (
fullName: string,
shortName: string,
city: string,
cityId: number,
main_color: string,
left_color: string,
right_color: string,
// main_color: string,
// left_color: string,
// right_color: string,
slogan: string,
logoId: string
) => {
const response = await authInstance.post("/carrier", {
full_name: fullName,
short_name: shortName,
city,
city: "",
city_id: cityId,
main_color,
left_color,
right_color,
// main_color,
// left_color,
// right_color,
slogan,
logo: logoId,
});
@@ -108,11 +108,11 @@ class CarrierStore {
editCarrierData = {
full_name: "",
short_name: "",
city: "",
city_id: 0,
main_color: "",
left_color: "",
right_color: "",
// main_color: "",
// left_color: "",
// right_color: "",
slogan: "",
logo: "",
};
@@ -120,32 +120,35 @@ class CarrierStore {
setEditCarrierData = (
fullName: string,
shortName: string,
city: string,
cityId: number,
main_color: string,
left_color: string,
right_color: string,
// main_color: string,
// left_color: string,
// right_color: string,
slogan: string,
logoId: string
) => {
this.editCarrierData = {
full_name: fullName,
short_name: shortName,
city,
city_id: cityId,
main_color: main_color,
left_color: left_color,
right_color: right_color,
// main_color: main_color,
// left_color: left_color,
// right_color: right_color,
slogan: slogan,
logo: logoId,
};
};
editCarrier = async (id: number) => {
const response = await authInstance.patch(
`/carrier/${id}`,
this.editCarrierData
);
const { language } = languageStore;
const response = await authInstance.patch(`/carrier/${id}`, {
...this.editCarrierData,
city: cityStore.cities[language].data.find(
(city) => city.id === this.editCarrierData.city_id
)?.name,
});
runInAction(() => {
this.carriers.data = this.carriers.data.map((carrier) =>

View File

@@ -4,6 +4,7 @@ import {
Language,
languageStore,
countryStore,
CashedCountries,
} from "@shared";
import { makeAutoObservable, runInAction } from "mobx";
@@ -16,9 +17,18 @@ export type City = {
};
export type CashedCities = {
ru: City[];
en: City[];
zh: City[];
ru: {
data: City[];
loaded: boolean;
};
en: {
data: City[];
loaded: boolean;
};
zh: {
data: City[];
loaded: boolean;
};
};
export type CashedCity = {
@@ -29,9 +39,18 @@ export type CashedCity = {
class CityStore {
cities: CashedCities = {
ru: [],
en: [],
zh: [],
ru: {
data: [],
loaded: false,
},
en: {
data: [],
loaded: false,
},
zh: {
data: [],
loaded: false,
},
};
city: Record<string, CashedCity> = {};
@@ -40,25 +59,37 @@ class CityStore {
makeAutoObservable(this);
}
ruCities: City[] = [];
ruCities: {
data: City[];
loaded: boolean;
} = {
data: [],
loaded: false,
};
getRuCities = async () => {
if (this.ruCities.loaded) {
return;
}
const response = await languageInstance("ru").get(`/city`);
runInAction(() => {
this.ruCities = response.data;
this.ruCities.data = response.data;
this.ruCities.loaded = true;
});
};
getCities = async (language: keyof CashedCities) => {
if (this.cities[language] && this.cities[language].length > 0) {
if (this.cities[language].loaded) {
return;
}
const response = await authInstance.get(`/city`);
runInAction(() => {
this.cities[language] = response.data;
this.cities[language].data = response.data;
this.cities[language].loaded = true;
});
};
@@ -83,19 +114,22 @@ class CityStore {
return response.data;
};
deleteCity = async (code: string, language: keyof CashedCities) => {
deleteCity = async (code: string) => {
await authInstance.delete(`/city/${code}`);
runInAction(() => {
this.cities[language] = this.cities[language].filter(
(city) => city.country_code !== code
);
this.city[code][language] = null;
for (const secondaryLanguage of ["ru", "en", "zh"] as Language[]) {
this.cities[secondaryLanguage].data = this.cities[
secondaryLanguage
].data.filter((city) => city.id !== Number(code));
if (this.city[code]) {
this.city[code][secondaryLanguage] = null;
}
}
});
};
createCityData = {
country: "",
country_code: "",
arms: "",
ru: {
@@ -111,14 +145,12 @@ class CityStore {
setCreateCityData = (
name: string,
country: string,
country_code: string,
arms: string,
language: keyof CashedCities
) => {
this.createCityData = {
...this.createCityData,
country: country,
country_code: country_code,
arms: arms,
[language]: {
@@ -127,73 +159,84 @@ class CityStore {
};
};
createCity = async () => {
const { language } = languageStore;
const { country, country_code, arms } = this.createCityData;
const { name } = this.createCityData[language as keyof CashedCities];
async createCity() {
const language = languageStore.language as Language;
const { country_code, arms } = this.createCityData;
const { name } = this.createCityData[language];
if (name && country && country_code && arms) {
const cityResponse = await languageInstance(language as Language).post(
"/city",
{
name: name,
country: country,
country_code: country_code,
arms: arms,
}
);
if (!name || !country_code) {
return;
}
runInAction(() => {
this.cities[language as keyof CashedCities] = [
...this.cities[language as keyof CashedCities],
cityResponse.data,
];
try {
// Create city in primary language
const cityResponse = await languageInstance(language).post("/city", {
name,
country:
countryStore.countries[language as keyof CashedCountries]?.data.find(
(c) => c.code === country_code
)?.name || "",
country_code,
arms: arms || "",
});
for (const secondaryLanguage of ["ru", "en", "zh"].filter(
const cityId = cityResponse.data.id;
// Create/update other language versions
for (const secondaryLanguage of (["ru", "en", "zh"] as Language[]).filter(
(l) => l !== language
)) {
const { name } =
this.createCityData[secondaryLanguage as keyof CashedCities];
const { name: secondaryName } = this.createCityData[secondaryLanguage];
const patchResponse = await languageInstance(
secondaryLanguage as Language
).patch(`/city/${cityResponse.data.id}`, {
name: name,
country: country,
country_code: country_code,
arms: arms,
});
// Get country name in secondary language
const countryName =
countryStore.countries[secondaryLanguage]?.data.find(
(c) => c.code === country_code
)?.name || "";
const patchResponse = await languageInstance(secondaryLanguage).patch(
`/city/${cityId}`,
{
name: secondaryName || "",
country: countryName,
country_code: country_code || "",
arms: arms || "",
}
);
runInAction(() => {
this.cities[secondaryLanguage as keyof CashedCities] = [
...this.cities[secondaryLanguage as keyof CashedCities],
this.cities[secondaryLanguage].data = [
...this.cities[secondaryLanguage].data,
patchResponse.data,
];
});
}
}
runInAction(() => {
this.createCityData = {
country: "",
country_code: "",
arms: "",
ru: {
name: "",
},
en: {
name: "",
},
zh: {
name: "",
},
};
});
};
// Update primary language data
runInAction(() => {
this.cities[language].data = [
...this.cities[language].data,
cityResponse.data,
];
});
// Reset form data
runInAction(() => {
this.createCityData = {
country_code: "",
arms: "",
ru: { name: "" },
en: { name: "" },
zh: { name: "" },
};
});
} catch (error) {
console.error("Error creating city:", error);
throw error;
}
}
editCityData = {
country: "",
country_code: "",
arms: "",
ru: {
@@ -209,14 +252,12 @@ class CityStore {
setEditCityData = (
name: string,
country: string,
country_code: string,
arms: string,
language: keyof CashedCities
) => {
this.editCityData = {
...this.editCityData,
country: country,
country_code: country_code,
arms: arms,
@@ -232,7 +273,7 @@ class CityStore {
const { name } = this.editCityData[language as keyof CashedCities];
const { countries } = countryStore;
const country = countries[language as keyof CashedCities].find(
const country = countries[language as keyof CashedCities].data.find(
(country) => country.code === country_code
);
@@ -255,9 +296,9 @@ class CityStore {
}
if (this.cities[language as keyof CashedCities]) {
this.cities[language as keyof CashedCities] = this.cities[
this.cities[language as keyof CashedCities].data = this.cities[
language as keyof CashedCities
].map((city) =>
].data.map((city) =>
city.id === Number(code)
? {
id: city.id,

View File

@@ -12,9 +12,18 @@ export type Country = {
};
export type CashedCountries = {
ru: Country[];
en: Country[];
zh: Country[];
ru: {
data: Country[];
loaded: boolean;
};
en: {
data: Country[];
loaded: boolean;
};
zh: {
data: Country[];
loaded: boolean;
};
};
export type CashedCountry = {
@@ -25,9 +34,18 @@ export type CashedCountry = {
class CountryStore {
countries: CashedCountries = {
ru: [],
en: [],
zh: [],
ru: {
data: [],
loaded: false,
},
en: {
data: [],
loaded: false,
},
zh: {
data: [],
loaded: false,
},
};
country: Record<string, CashedCountry> = {};
@@ -37,14 +55,15 @@ class CountryStore {
}
getCountries = async (language: keyof CashedCountries) => {
if (this.countries[language] && this.countries[language].length > 0) {
if (this.countries[language].loaded) {
return;
}
const response = await authInstance.get(`/country`);
const response = await languageInstance(language).get(`/country`);
runInAction(() => {
this.countries[language] = response.data;
this.countries[language].data = response.data;
this.countries[language].loaded = true;
});
};
@@ -76,10 +95,15 @@ class CountryStore {
await authInstance.delete(`/country/${code}`);
runInAction(() => {
this.countries[language] = this.countries[language].filter(
this.countries[language].data = this.countries[language].data.filter(
(country) => country.code !== code
);
this.country[code][language] = null;
this.countries[language].loaded = true;
this.country[code] = {
ru: null,
en: null,
zh: null,
};
});
};
@@ -121,8 +145,8 @@ class CountryStore {
});
runInAction(() => {
this.countries[language as keyof CashedCountries] = [
...this.countries[language as keyof CashedCountries],
this.countries[language as keyof CashedCountries].data = [
...this.countries[language as keyof CashedCountries].data,
{ code: code, name: name },
];
});
@@ -142,8 +166,8 @@ class CountryStore {
);
}
runInAction(() => {
this.countries[secondaryLanguage as keyof CashedCountries] = [
...this.countries[secondaryLanguage as keyof CashedCountries],
this.countries[secondaryLanguage as keyof CashedCountries].data = [
...this.countries[secondaryLanguage as keyof CashedCountries].data,
{ code: code, name: name },
];
});
@@ -204,11 +228,10 @@ class CountryStore {
};
}
if (this.countries[language as keyof CashedCountries]) {
this.countries[language as keyof CashedCountries] = this.countries[
language as keyof CashedCountries
].map((country) =>
country.code === code ? { code, name } : country
);
this.countries[language as keyof CashedCountries].data =
this.countries[language as keyof CashedCountries].data.map(
(country) => (country.code === code ? { code, name } : country)
);
}
});
}