hotfix admin panel

This commit is contained in:
Микаэл Оганесян
2025-09-27 22:29:13 -07:00
parent 34ba3c1db0
commit b25df42960
14 changed files with 461 additions and 582 deletions

View File

@@ -73,7 +73,8 @@ if (typeof document !== "undefined") {
import { languageInstance } from "@shared";
import { makeAutoObservable } from "mobx";
import { stationsStore, routeStore, sightsStore } from "@shared";
import { stationsStore, routeStore, sightsStore, menuStore } from "@shared";
import { Token } from "@mui/icons-material";
// Функция для сброса кешей карты
export const clearMapCaches = () => {
@@ -101,6 +102,7 @@ interface ApiStation {
name: string;
latitude: number;
longitude: number;
created_at?: string;
}
interface ApiSight {
@@ -109,8 +111,12 @@ interface ApiSight {
description: string;
latitude: number;
longitude: number;
created_at?: string;
}
export type SortType = "name_asc" | "name_desc" | "date_asc" | "date_desc";
class MapStore {
constructor() {
makeAutoObservable(this);
@@ -120,6 +126,61 @@ class MapStore {
stations: ApiStation[] = [];
sights: ApiSight[] = [];
// НОВЫЕ ПОЛЯ ДЛЯ СОРТИРОВКИ
stationSort: SortType = "name_asc";
sightSort: SortType = "name_asc";
// НОВЫЕ МЕТОДЫ-СЕТТЕРЫ
setStationSort = (sortType: SortType) => {
this.stationSort = sortType;
};
setSightSort = (sortType: SortType) => {
this.sightSort = sortType;
};
// ПРИВАТНЫЙ МЕТОД ДЛЯ ОБЩЕЙ ЛОГИКИ СОРТИРОВКИ
private sortFeatures<T extends ApiStation | ApiSight>(
features: T[],
sortType: SortType
): T[] {
const sorted = [...features];
switch (sortType) {
case "name_asc":
return sorted.sort((a, b) => a.name.localeCompare(b.name));
case "name_desc":
return sorted.sort((a, b) => b.name.localeCompare(a.name));
case "date_asc":
return sorted.sort((a, b) => {
if ('created_at' in a && 'created_at' in b && a.created_at && b.created_at) {
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
}
// Фоллбэк: сортировка по ID, если дата недоступна
return a.id - b.id;
});
case "date_desc":
return sorted.sort((a, b) => {
if ('created_at' in a && 'created_at' in b && a.created_at && b.created_at) {
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
}
// Фоллбэк: сортировка по ID, если дата недоступна
return b.id - a.id;
});
default:
return sorted;
}
}
// НОВЫЕ ГЕТТЕРЫ, ВОЗВРАЩАЮЩИЕ ОТСОРТИРОВАННЫЕ СПИСКИ
get sortedStations(): ApiStation[] {
return this.sortFeatures(this.stations, this.stationSort);
}
get sortedSights(): ApiSight[] {
return this.sortFeatures(this.sights, this.sightSort);
}
getRoutes = async () => {
const response = await languageInstance("ru").get("/route");
const routesIds = response.data.map((route: any) => route.id);
@@ -2116,6 +2177,8 @@ const MapControls: React.FC<MapControlsProps> = ({
);
};
import {observer} from "mobx-react-lite";
// --- MAP SIGHTBAR COMPONENT ---
interface MapSightbarProps {
mapService: MapService | null;
@@ -2126,8 +2189,7 @@ interface MapSightbarProps {
activeSection: string | null;
setActiveSection: (section: string | null) => void;
}
const MapSightbar: React.FC<MapSightbarProps> = ({
const MapSightbar: React.FC<MapSightbarProps> = observer(({
mapService,
mapFeatures,
selectedFeature,
@@ -2138,50 +2200,50 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
}) => {
const navigate = useNavigate();
const [searchQuery, setSearchQuery] = useState("");
const [stationSort, setStationSort] = useState<SortType>("name_asc");
const [sightSort, setSightSort] = useState<SortType>("name_asc");
const actualFeatures = useMemo(() => {
return mapFeatures.filter((feature) => !feature.get("isProxy"));
}, [mapFeatures]);
const { isOpen } = menuStore;
const actualFeatures = useMemo(
() => mapFeatures.filter((f) => !f.get("isProxy")),
[mapFeatures]
);
const filteredFeatures = useMemo(() => {
if (!searchQuery.trim()) return actualFeatures;
return actualFeatures.filter((feature) =>
((feature.get("name") as string) || "")
return actualFeatures.filter((f) =>
((f.get("name") as string) || "")
.toLowerCase()
.includes(searchQuery.toLowerCase())
);
}, [actualFeatures, searchQuery]);
const handleFeatureClick = useCallback(
(id: string | number | undefined) => {
if (!id || !mapService) return;
(id: string | number) => {
if (!mapService) return;
mapService.selectFeature(id);
},
[mapService]
);
const handleDeleteFeature = useCallback(
// @ts-ignore
(id, recourse) => {
if (
mapService &&
window.confirm("Вы действительно хотите удалить этот объект?")
) {
mapService.deleteFeature(id, recourse);
(id: string | number, resource: string) => {
if (!mapService) return;
if (window.confirm("Вы действительно хотите удалить этот объект?")) {
mapService.deleteFeature(id, resource);
}
},
[mapService]
);
const handleCheckboxChange = useCallback(
(id: string | number | undefined) => {
if (!id || !mapService) return;
(id: string | number) => {
if (!mapService) 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);
mapService.setSelectedIds(newSet);
},
@@ -2200,69 +2262,68 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
}
}, [mapService, selectedIds, setSelectedIds]);
// @ts-ignore
const handleEditFeature = useCallback(
// @ts-ignore
(featureType, fullId) => {
if (!featureType || !fullId) return;
(featureType: string, fullId: string | number) => {
const numericId = String(fullId).split("-")[1];
if (numericId) navigate(`/${featureType}/${numericId}/edit`);
if (!featureType || !numericId) return;
navigate(`/${featureType}/${numericId}/edit`);
},
[navigate]
);
const sortFeatures = (
features: Feature<Geometry>[],
currentSelectedIds: Set<string | number>,
currentSelectedFeature: Feature<Geometry> | null
) => {
const selectedId = currentSelectedFeature?.getId();
return [...features].sort((a, b) => {
const aId = a.getId();
const bId = b.getId();
if (selectedId) {
if (aId === selectedId) return -1;
if (bId === selectedId) return 1;
}
const aIsChecked = aId !== undefined && currentSelectedIds.has(aId);
const bIsChecked = bId !== undefined && currentSelectedIds.has(bId);
if (aIsChecked && !bIsChecked) return -1;
if (!aIsChecked && bIsChecked) return 1;
const aNumericId = aId ? parseInt(String(aId).split("-")[1], 10) : 0;
const bNumericId = bId ? parseInt(String(bId).split("-")[1], 10) : 0;
if (
!isNaN(aNumericId) &&
!isNaN(bNumericId) &&
aNumericId !== bNumericId
) {
return aNumericId - bNumericId;
}
const aName = (a.get("name") as string) || "";
const bName = (b.get("name") as string) || "";
return aName.localeCompare(bName, "ru");
});
const sortFeaturesByType = <T extends Feature<Geometry>>(
features: T[],
sortType: SortType
): T[] => {
const sorted = [...features];
switch (sortType) {
case "name_asc":
return sorted.sort((a, b) =>
((a.get("name") as string) || "").localeCompare(
((b.get("name") as string) || "")
)
);
case "name_desc":
return sorted.sort((a, b) =>
((b.get("name") as string) || "").localeCompare(
((a.get("name") as string) || "")
)
);
case "date_asc":
return sorted.sort((a, b) => {
const aDate = a.get("created_at")
? new Date(a.get("created_at"))
: new Date(0);
const bDate = b.get("created_at")
? new Date(b.get("created_at"))
: new Date(0);
return aDate.getTime() - bDate.getTime();
});
case "date_desc":
return sorted.sort((a, b) => {
const aDate = a.get("created_at")
? new Date(a.get("created_at"))
: new Date(0);
const bDate = b.get("created_at")
? new Date(b.get("created_at"))
: new Date(0);
return bDate.getTime() - aDate.getTime();
});
default:
return sorted;
}
};
const toggleSection = (id: string) =>
setActiveSection(activeSection === id ? null : id);
const stations = filteredFeatures.filter(
(f) => f.get("featureType") === "station"
);
const lines = filteredFeatures.filter(
(f) => f.get("featureType") === "route"
);
const lines = filteredFeatures.filter((f) => f.get("featureType") === "route");
const sights = filteredFeatures.filter(
(f) => f.get("featureType") === "sight"
);
const sortedStations = sortFeatures(stations, selectedIds, selectedFeature);
const sortedLines = sortFeatures(lines, selectedIds, selectedFeature);
const sortedSights = sortFeatures(sights, selectedIds, selectedFeature);
const sortedStations = sortFeaturesByType(stations, stationSort);
const sortedSights = sortFeaturesByType(sights, sightSort);
const renderFeatureList = (
features: Feature<Geometry>[],
@@ -2273,9 +2334,11 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
{features.length > 0 ? (
features.map((feature) => {
const fId = feature.getId();
if (fId === undefined) return null; // TypeScript-safe
const fName = (feature.get("name") as string) || "Без названия";
const isSelected = selectedFeature?.getId() === fId;
const isChecked = fId !== undefined && selectedIds.has(fId);
const isChecked = selectedIds.has(fId);
return (
<div
key={String(fId)}
@@ -2316,11 +2379,8 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
size={16}
/>
<span
className={`font-medium truncate ${
isSelected
? "text-orange-600"
: "group-hover:text-blue-600"
}`}
className={`font-medium whitespace-nowrap overflow-x-auto block
scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent`}
title={fName}
>
{fName}
@@ -2331,7 +2391,8 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
<button
onClick={(e) => {
e.stopPropagation();
handleEditFeature(feature.get("featureType"), fId);
const featureTypeVal = feature.get("featureType");
if (featureTypeVal) handleEditFeature(featureTypeVal, fId);
}}
className="p-1 rounded-full text-gray-400 hover:text-blue-600 hover:bg-blue-100 transition-colors"
title="Редактировать детали"
@@ -2358,32 +2419,69 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
</div>
);
const toggleSection = (id: string) =>
setActiveSection(activeSection === id ? null : id);
const sections = [
{
id: "layers",
title: `Остановки (${sortedStations.length})`,
icon: <Bus size={20} />,
count: sortedStations.length,
content: renderFeatureList(sortedStations, "station", MapPin),
content: (
<>
<div className="flex items-center space-x-2 mb-2">
<label>Сортировка:</label>
<select
value={stationSort}
onChange={(e) => setStationSort(e.target.value as SortType)}
className="border rounded px-2 py-1 text-sm"
>
<option value="name_asc">Имя </option>
<option value="name_desc">Имя </option>
</select>
</div>
{renderFeatureList(sortedStations, "station", MapPin)}
</>
),
},
{
id: "lines",
title: `Маршруты (${sortedLines.length})`,
title: `Маршруты (${lines.length})`,
icon: <RouteIcon size={20} />,
count: sortedLines.length,
content: renderFeatureList(sortedLines, "route", ArrowRightLeft),
count: lines.length,
content: renderFeatureList(lines, "route", ArrowRightLeft),
},
{
id: "sights",
title: `Достопримечательности (${sortedSights.length})`,
icon: <Landmark size={20} />,
count: sortedSights.length,
content: renderFeatureList(sortedSights, "sight", Landmark),
content: (
<>
<div className="flex items-center space-x-2 mb-2">
<label>Сортировка:</label>
<select
value={sightSort}
onChange={(e) => setSightSort(e.target.value as SortType)}
className="border rounded px-2 py-1 text-sm"
>
<option value="name_asc">Имя </option>
<option value="name_desc">Имя </option>
</select>
</div>
{renderFeatureList(sortedSights, "sight", Landmark)}
</>
),
},
];
React.useEffect(() => {
console.log("isOpen changed:", isOpen);
}, [isOpen]);
return (
<div className="w-[360px] relative bg-gray-50 shadow-lg flex flex-col border-l border-gray-200 h-[90vh]">
<div className={`${isOpen ? "w-[360px]" : "w-[590px]"} transition-all duration-300 ease-in-out relative bg-gray-50 shadow-lg flex flex-col border-l border-gray-200 h-[90vh]`}>
<div className="p-4 bg-gray-700 text-white">
<h2 className="text-lg font-semibold">Панель управления</h2>
</div>
@@ -2396,8 +2494,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex-1 flex flex-col min-h-0">
<div className="flex-1 flex flex-col min-h-0 overflow-y-auto">
{filteredFeatures.length === 0 && searchQuery ? (
<div className="p-4 text-center text-gray-500">
Ничего не найдено.
@@ -2452,7 +2549,6 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
)
)}
</div>
{selectedIds.size > 0 && (
<div className="p-3 border-t border-gray-200 bg-white flex-shrink-0">
<button
@@ -2466,7 +2562,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
)}
</div>
);
};
});
// --- MAP PAGE COMPONENT ---
export const MapPage: React.FC = () => {
const mapRef = useRef<HTMLDivElement | null>(null);