import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import {
Alert,
Box,
Button,
Chip,
FormControlLabel,
Grid2 as Grid,
IconButton,
MenuItem,
Switch,
TextField,
ToggleButton,
ToggleButtonGroup,
Tooltip,
Typography,
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import RouteIcon from '@mui/icons-material/RouteOutlined';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import DeleteIcon from '@mui/icons-material/DeleteOutline';
import TimelineIcon from '@mui/icons-material/Timeline';
import PlaceIcon from '@mui/icons-material/PlaceOutlined';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { MEDIA_REQUIREMENTS, MEDIA_REQUIREMENT_LABELS, type GeoJsonLineString, type MediaRequirement } from '@patrol/shared';
import { api } from '../api/client';
import type { Route } from '../api/types';
import { useAuth } from '../auth/AuthContext';
import { SitePicker } from '../lib/SitePicker';
import { useRoutes, useSites } from '../lib/queries';
import { BaseMap, FitTo } from '../map/base';
import { EditorMode, RouteEditor } from '../map/RouteEditor';
import { useConfirm, useNotify } from '../ui/feedback';
import { Card, EmptyState, ErrorState, LoadingState, PageHeader } from '../ui/layout';
import { colors } from '../ui/theme';
type CpForm = {
key: string;
id?: string;
name: string;
location: { type: 'Point'; coordinates: [number, number] };
radiusM: number;
mediaRequired: MediaRequirement;
instruction: string;
};
type Form = { name: string; description: string; expectedMinutes: string; strictOrder: boolean; path: GeoJsonLineString | null; checkpoints: CpForm[] };
const emptyForm = (): Form => ({ name: '', description: '', expectedMinutes: '', strictOrder: false, path: null, checkpoints: [] });
function toForm(r: Route): Form {
return {
name: r.name,
description: r.description || '',
expectedMinutes: r.expectedMinutes ? String(r.expectedMinutes) : '',
strictOrder: r.strictOrder,
path: r.path,
checkpoints: [...r.checkpoints]
.sort((a, b) => a.position - b.position)
.map((c) => ({
key: c.id || crypto.randomUUID(),
id: c.id,
name: c.name,
location: c.location,
radiusM: c.radiusM,
mediaRequired: c.mediaRequired,
instruction: c.instruction || '',
})),
};
}
export function RoutesPage() {
const { routeId } = useParams();
const [params, setParams] = useSearchParams();
const navigate = useNavigate();
const sites = useSites();
const siteId = params.get('site') || sites.data?.[0]?.id || '';
const routes = useRoutes(siteId || undefined);
const { can } = useAuth();
const pickSite = (id: string) => {
setParams({ site: id });
if (routeId) navigate(`/routes?site=${id}`);
};
if (sites.isLoading) return ;
if (sites.isError) return sites.refetch()} />;
if (!sites.data?.length) {
return (
<>
Маршрут рисуется внутри периметра объекта.
>
);
}
const site = sites.data.find((s) => s.id === siteId);
return (
<>
{can('routes.manage') && (
} onClick={() => navigate(`/routes/new?site=${siteId}`)}>
Новый маршрут
)}
>
}
/>
{routes.isLoading && }
{routes.data?.length === 0 && routeId !== 'new' && На объекте пока нет маршрутов.}
{routes.data?.map((r) => (
navigate(`/routes/${r.id}?site=${siteId}`)}
sx={{
p: 1.75,
borderRadius: '12px',
cursor: 'pointer',
bgcolor: '#fff',
border: `1px solid ${routeId === r.id ? colors.brand : colors.line}`,
boxShadow: routeId === r.id ? `0 0 0 3px ${colors.brandSoft}` : 'none',
}}
>
{r.name}
Точек: {r.checkpoints.length} · версия {r.version}
{r.strictOrder ? ' · строгий порядок' : ''}
))}
{routeId ? (
) : (
{can('routes.manage') ? 'или создайте новый для этого объекта' : null}
)}
>
);
}
function RouteForm({ routeId, siteId, perimeter }: { routeId: string; siteId: string; perimeter: import('@patrol/shared').GeoJsonMultiPolygon | null }) {
const isNew = routeId === 'new';
const navigate = useNavigate();
const qc = useQueryClient();
const notify = useNotify();
const confirm = useConfirm();
const { can } = useAuth();
const editable = can('routes.manage');
const route = useQuery({ queryKey: ['route', routeId], queryFn: () => api(`/routes/${routeId}`), enabled: !isNew });
const [form, setForm] = useState