feat: update appeal widget and check double routes for create snapshot

This commit is contained in:
2026-05-19 13:16:02 +03:00
parent 83ccdef790
commit bf45dcdbfc
3 changed files with 197 additions and 79 deletions

View File

@@ -1,31 +1,47 @@
import { useRef, useEffect } from 'react'
import '../../styles/AppealWidget.css'
import { TouchableLayout } from '../TouchableLayout'
import { useRef, useEffect } from "react";
import "../../styles/AppealWidget.css";
import { TouchableLayout } from "../TouchableLayout";
function AppealWidget({widgetImgPath, widgetLabel, widgetText, style, isOpen}) {
const stopProp = (e) => { e.stopPropagation(); e.preventDefault(); };
const layoutRef = useRef(null);
function AppealWidget({
widgetImgPath,
widgetLabel,
widgetText,
style,
isOpen,
}) {
const stopProp = (e) => {
e.stopPropagation();
e.preventDefault();
};
const layoutRef = useRef(null);
useEffect(() => {
if (isOpen && layoutRef.current) {
const scrollable = layoutRef.current.querySelector('.scrollable');
if (scrollable) scrollable.scrollTop = 0;
}
}, [isOpen]);
useEffect(() => {
if (isOpen && layoutRef.current) {
const scrollable = layoutRef.current.querySelector(".scrollable");
if (scrollable) scrollable.scrollTop = 0;
}
}, [isOpen]);
return (
<div style={style} className='dynamic-widget'
onPointerDown={stopProp}
onPointerMove={stopProp}
onPointerUp={stopProp}
>
{widgetImgPath && <img className='dynamic-widget-image' src={widgetImgPath} />}
<div className='dynamic-widget-label'>{widgetLabel}</div>
<TouchableLayout ref={layoutRef} className="dynamic-widget-text-scroll" maxHeight="calc(100vh - 150px - 100px - 300px)">
<div className='dynamic-widget-text'>{widgetText}</div>
</TouchableLayout>
</div>
);
return (
<div
style={style}
className="dynamic-widget"
onPointerDown={stopProp}
onPointerMove={stopProp}
onPointerUp={stopProp}
>
{widgetImgPath && (
<img className="dynamic-widget-image" src={widgetImgPath} />
)}
<div className="dynamic-widget-label">{widgetLabel}</div>
<TouchableLayout
ref={layoutRef}
className="dynamic-widget-text-scroll"
>
<div className="dynamic-widget-text">{widgetText}</div>
</TouchableLayout>
</div>
);
}
export default AppealWidget
export default AppealWidget;

View File

@@ -5,7 +5,7 @@
flex-direction: column;
align-items: center;
width: 420px;
max-height: calc(100vh - 150px - 100px);
max-height: calc(100vh - 150px - 98px);
border-radius: 10px;
background:
linear-gradient(
@@ -13,49 +13,61 @@
rgba(255, 255, 255, 0) 8.71%,
rgba(255, 255, 255, 0.16) 69.69%
),
var(--carrier-left, #006F3A);
var(--carrier-left, #006f3a);
box-sizing: border-box;
overflow: hidden;
touch-action: none;
}
.dynamic-widget-image {
padding-top: 4px;
margin-left: 4px;
margin-right: 4px;
width: 412px;
border-radius: 6px 6px 0 0;
padding-top: 2px;
margin-left: 2px;
margin-right: 2px;
width: 416px;
border-radius: 10px 10px 0 0;
object-fit: cover;
}
.dynamic-widget-label {
width: 380px;
margin-top: 20px;
margin-bottom: 5px;
width: 100%;
padding: 10px 20px;
box-sizing: border-box;
font-size: 20px;
font-weight: 500;
color: #fff;
border-bottom: 1px solid var(--Glass-stroke, rgba(255, 255, 255, 0.8));
background:
linear-gradient(
180deg,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
),
rgba(var(--carrier-right-menu-rgb, 179, 165, 152), 0.4);
}
.dynamic-widget-text-scroll {
margin-top: 0;
margin-bottom: 25px;
.dynamic-widget-text-scroll.scrollable-container {
flex: 1;
min-height: 0;
align-self: stretch;
margin: 15px 20px;
overflow: hidden;
}
.dynamic-widget-text-scroll .scrollable-viewport {
flex: 1;
min-height: 0;
overflow: hidden;
}
.dynamic-widget-text-scroll .scrollable {
flex: 1;
min-height: 0;
}
.dynamic-widget-text-scroll.scrollable-container {
margin: 0 20px;
}
.dynamic-widget-text-scroll .scrollable-viewport {
padding-bottom: 20px;
box-sizing: border-box;
}
.dynamic-widget-text {
font-size: 14px;
font-weight: 400;
line-height: 190%;
padding-bottom: 10px;
padding-right: 5px;
}

View File

@@ -1,5 +1,12 @@
import { Button, TextField } from "@mui/material";
import { snapshotStore } from "@shared";
import {
Button,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material";
import { snapshotStore, authStore, routeStore } from "@shared";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react";
import { useState } from "react";
@@ -12,6 +19,76 @@ export const SnapshotCreatePage = observer(() => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [duplicateWarningOpen, setDuplicateWarningOpen] = useState(false);
const [duplicateRouteNumbers, setDuplicateRouteNumbers] = useState<string[]>([]);
const canReadRoutes = authStore.canRead("routes");
const startExport = async () => {
try {
setIsLoading(true);
const id = await createSnapshot(name);
await getSnapshotStatus(id);
while (snapshotStore.snapshotStatus?.Status != "done") {
await new Promise((resolve) => setTimeout(resolve, 1000));
await getSnapshotStatus(id);
}
if (snapshotStore.snapshotStatus?.Status === "done") {
toast.success("Экспорт медиа успешно создан");
runInAction(() => {
snapshotStore.snapshotStatus = null;
});
await getStorageInfo();
navigate(-1);
}
} catch (error) {
console.error(error);
toast.error("Ошибка при создании экспорта медиа");
} finally {
setIsLoading(false);
}
};
const handleSave = async () => {
if (!canReadRoutes) {
await startExport();
return;
}
try {
runInAction(() => {
routeStore.routes.loaded = false;
});
await routeStore.getRoutes();
const routes = routeStore.routes.data;
const numberCount = new Map<string, number>();
for (const route of routes) {
const num = (route.route_number ?? "").trim();
if (num) {
numberCount.set(num, (numberCount.get(num) ?? 0) + 1);
}
}
const duplicates = Array.from(numberCount.entries())
.filter(([, count]) => count > 1)
.map(([num]) => num);
if (duplicates.length > 0) {
setDuplicateRouteNumbers(duplicates);
setDuplicateWarningOpen(true);
} else {
await startExport();
}
} catch {
await startExport();
}
};
return (
<div className="w-full h-[400px] flex justify-center items-center">
@@ -40,35 +117,7 @@ export const SnapshotCreatePage = observer(() => {
color="primary"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={async () => {
try {
setIsLoading(true);
const id = await createSnapshot(name);
await getSnapshotStatus(id);
while (snapshotStore.snapshotStatus?.Status != "done") {
await new Promise((resolve) => setTimeout(resolve, 1000));
await getSnapshotStatus(id);
}
if (snapshotStore.snapshotStatus?.Status === "done") {
toast.success("Экспорт медиа успешно создан");
runInAction(() => {
snapshotStore.snapshotStatus = null;
});
await getStorageInfo();
navigate(-1);
}
} catch (error) {
console.error(error);
toast.error("Ошибка при создании экспорта медиа");
} finally {
setIsLoading(false);
}
}}
onClick={handleSave}
disabled={isLoading || !name.trim()}
>
{isLoading ? (
@@ -87,6 +136,47 @@ export const SnapshotCreatePage = observer(() => {
</Button>
</div>
</div>
<Dialog
open={duplicateWarningOpen}
onClose={() => !isLoading && setDuplicateWarningOpen(false)}
maxWidth="sm"
fullWidth
>
<DialogTitle>Найдены повторяющиеся маршруты</DialogTitle>
<DialogContent>
<p className="mb-3">
Обнаружены маршруты с одинаковыми номерами. Это может привести к
некорректным данным в экспорте.
</p>
<ul className="list-disc pl-5">
{duplicateRouteNumbers.map((num) => (
<li key={num}>
Найдены повторяющиеся маршруты под номером {num}
</li>
))}
</ul>
</DialogContent>
<DialogActions>
<Button
onClick={() => setDuplicateWarningOpen(false)}
disabled={isLoading}
>
Отмена
</Button>
<Button
variant="contained"
sx={{ backgroundColor: "#795548", "&:hover": { backgroundColor: "#5D4037" } }}
disabled={isLoading}
onClick={async () => {
setDuplicateWarningOpen(false);
await startExport();
}}
>
Продолжить экспорт
</Button>
</DialogActions>
</Dialog>
</div>
);
});