diff --git a/client/src/components/gps/GpsMap.tsx b/client/src/components/gps/GpsMap.tsx
index 37e5be3..6b98d53 100644
--- a/client/src/components/gps/GpsMap.tsx
+++ b/client/src/components/gps/GpsMap.tsx
@@ -24,6 +24,8 @@ export interface GpsMapMarker {
/** vehicle/person — кружок с иконкой; dot — маленькая точка (старт/финиш/вершина/точка трека) */
kind: 'vehicle' | 'person' | 'dot';
label?: string;
+ /** Вторая строка подписи (заказчик) */
+ label2?: string;
/** Приглушённый маркер для offline-объектов */
offline?: boolean;
/** Примерная позиция (LBS по вышкам, нет GPS): полупрозрачный маркер с пунктирной обводкой */
diff --git a/client/src/components/gps/marker-html.ts b/client/src/components/gps/marker-html.ts
index 62ca25b..fa09507 100644
--- a/client/src/components/gps/marker-html.ts
+++ b/client/src/components/gps/marker-html.ts
@@ -43,9 +43,9 @@ const LABEL_HEIGHT = 18; // подпись под маркером (16px + от
export function gpsMarkerLayout(marker: GpsMapMarker): GpsMarkerLayout {
if (marker.kind === 'dot') {
- return { width: 24, height: 16 + (marker.label ? LABEL_HEIGHT : 0), anchorX: 12, anchorY: 8 };
+ return { width: 24, height: 16 + (marker.label ? LABEL_HEIGHT : 0) + (marker.label2 ? LABEL_HEIGHT : 0), anchorX: 12, anchorY: 8 };
}
- return { width: 120, height: 28 + (marker.label ? LABEL_HEIGHT : 0), anchorX: 60, anchorY: 14 };
+ return { width: 120, height: 28 + (marker.label ? LABEL_HEIGHT : 0) + (marker.label2 ? LABEL_HEIGHT : 0), anchorX: 60, anchorY: 14 };
}
/**
@@ -61,9 +61,13 @@ export function gpsMarkerHtml(marker: GpsMapMarker): string {
const title = escapeHtml(marker.label || '');
// Подпись выходит за ширину маркера (для dot 24px) — overflow виден по умолчанию,
// max-width не ставим, иначе «Старт»/«Финиш» обрезались до «С…»/«Ф…»
- const label = marker.label
- ? `
${title}
`
- : '';
+ const title2 = escapeHtml(marker.label2 || '');
+ // Подпись: одна или две строки (объект / заказчик). Выходит за ширину маркера,
+ // overflow виден по умолчанию, max-width не ставим
+ const label =
+ marker.label || marker.label2
+ ? `${title}${marker.label2 ? `
${title2}
` : ''}
`
+ : '';
let body: string;
if (marker.kind === 'dot') {
diff --git a/client/src/pages/gps/MapTab.tsx b/client/src/pages/gps/MapTab.tsx
index 1fc5359..f304bdd 100644
--- a/client/src/pages/gps/MapTab.tsx
+++ b/client/src/pages/gps/MapTab.tsx
@@ -131,13 +131,43 @@ export function GpsMapTab({ onShowRoute }: { onShowRoute: (assetId: number) => v
// === Слой задач формы: выбранная форма + её geo-поле → задачи на карте ===
const [, setLocation] = useLocation();
- const [tasksLayer, setTasksLayer] = useState(false);
- // Отображение названий объектов на карте (по умолчанию скрыты — длинные
- // заголовки перекрывают друг друга; включаем кнопкой «Названия»)
- const [showLabels, setShowLabels] = useState(false);
- const [mapFormId, setMapFormId] = useState(null);
- const [mapFieldId, setMapFieldId] = useState(null);
+ // Настройки слоя сохраняются в localStorage — иначе при переходе между
+ // табами компонент размонтируется и выбор формы/поля слетает
+ const LAYER_STORAGE_KEY = 'gps-tasks-layer';
+ const [layerPref, setLayerPref] = useState<{ layer: boolean; formId: number | null; fieldId: number | null; labels: boolean }>(() => {
+ try {
+ const raw = JSON.parse(localStorage.getItem(LAYER_STORAGE_KEY) || 'null');
+ if (raw && typeof raw === 'object') {
+ return {
+ layer: raw.layer === true,
+ formId: typeof raw.formId === 'number' ? raw.formId : null,
+ fieldId: typeof raw.fieldId === 'number' ? raw.fieldId : null,
+ labels: raw.labels === true,
+ };
+ }
+ } catch { /* ignore */ }
+ return { layer: false, formId: null, fieldId: null, labels: false };
+ });
+ const tasksLayer = layerPref.layer;
+ const mapFormId = layerPref.formId;
+ const mapFieldId = layerPref.fieldId;
+ const showLabels = layerPref.labels;
+ const setLayerPrefPart = (part: Partial) => {
+ setLayerPref((prev) => {
+ const next = { ...prev, ...part };
+ try { localStorage.setItem(LAYER_STORAGE_KEY, JSON.stringify(next)); } catch { /* ignore */ }
+ return next;
+ });
+ };
const [createAt, setCreateAt] = useState<{ lat: number; lng: number } | null>(null);
+ useEffect(() => {
+ if (mapFormId !== null) setLayerPrefPart({ formId: mapFormId });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [mapFormId]);
+ useEffect(() => {
+ if (mapFieldId !== null) setLayerPrefPart({ fieldId: mapFieldId });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [mapFieldId]);
const { data: formsData } = useQuery<{ success: boolean; forms: Array<{ id: number; name: string }> }>({
queryKey: ['/api/forms'],
@@ -156,7 +186,7 @@ export function GpsMapTab({ onShowRoute }: { onShowRoute: (assetId: number) => v
// geo-поле по умолчанию — первое (при смене формы)
useEffect(() => {
if (mapFieldId === null || !geoFields.some((f) => f.id === mapFieldId)) {
- setMapFieldId(geoFields[0]?.id ?? null);
+ setLayerPrefPart({ fieldId: geoFields[0]?.id ?? null });
}
}, [geoFields, mapFieldId]);
@@ -186,13 +216,14 @@ export function GpsMapTab({ onShowRoute }: { onShowRoute: (assetId: number) => v
return { lat, lng };
};
- // Короткое название для подписи: «объект · заказчик» (первая и последняя
- // части заголовка через «;»), длину ограничиваем, иначе подписи перекрывают карту
- const shortTitle = (title: string): string => {
+ // Двухстрочная подпись: первая строка — объект, вторая — заказчик
+ // (первая и последняя части заголовка через «;»), длину ограничиваем
+ const shortTitle = (title: string): { line1: string; line2?: string } => {
const parts = title.split(';').map((x) => x.trim()).filter(Boolean);
- let text = parts.length > 1 ? `${parts[0]} · ${parts[parts.length - 1]}` : (parts[0] ?? title);
- if (text.length > 40) text = `${text.slice(0, 39)}…`;
- return text;
+ const line1 = parts[0] ?? title;
+ const line2 = parts.length > 1 ? parts[parts.length - 1] : undefined;
+ const cut = (t: string) => (t.length > 34 ? `${t.slice(0, 33)}…` : t);
+ return { line1: cut(line1), line2: line2 ? cut(line2) : undefined };
};
const taskMarkers: GpsMapMarker[] = useMemo(() => {
@@ -207,8 +238,10 @@ export function GpsMapTab({ onShowRoute }: { onShowRoute: (assetId: number) => v
lng: geo.lng,
kind: 'dot',
color: '#8b5cf6',
- // Подпись — только при включённом режиме «Названия»
- label: showLabels ? shortTitle(t.title) : undefined,
+ // Подпись — только при включённом режиме «Названия», в две строки
+ ...(showLabels
+ ? (() => { const st = shortTitle(t.title); return { label: st.line1, ...(st.line2 ? { label2: st.line2 } : {}) }; })()
+ : {}),
noCluster: true,
zIndex: 40,
onClick: rulerMode ? undefined : () => setLocation(`/forms/${mapFormId}/tasks/${t.id}`),
@@ -317,7 +350,7 @@ export function GpsMapTab({ onShowRoute }: { onShowRoute: (assetId: number) => v
size="sm"
variant={tasksLayer ? 'default' : 'outline'}
className="h-7 text-xs"
- onClick={() => setTasksLayer((v) => !v)}
+ onClick={() => setLayerPrefPart({ layer: !tasksLayer })}
>
Объекты
@@ -326,7 +359,7 @@ export function GpsMapTab({ onShowRoute }: { onShowRoute: (assetId: number) => v
<>