feat(gps): вкладка «Инструкции» со справочником настройки трекеров + открыты порты протоколов (Teltonika, GPS103, H02, Suntech)

This commit is contained in:
2026-08-24 22:15:34 +03:00
parent a6a42fdb15
commit 7fe636b56c
4 changed files with 572 additions and 2 deletions

View File

@@ -0,0 +1,212 @@
import { useMemo, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ChevronDown, ChevronRight, Copy, Info, Search } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useToast } from '@/hooks/use-toast';
import { resolveGuideHost, TRACKER_GUIDES, type TrackerGuideItem } from './tracker-guides';
/**
* Таб «Инструкции»: справочник по настройке GPS-трекеров.
* Слева — поиск и дерево категорий/устройств, справа — карточка выбранного
* устройства с шагами настройки, SMS-командами и примечаниями.
* Адрес сервера ({host}) подставляется из window.location.hostname.
*/
export function GpsGuidesTab() {
const { toast } = useToast();
const [search, setSearch] = useState('');
// Раскрытые категории дерева (при поиске раскрываются все с совпадениями)
const [expanded, setExpanded] = useState<Set<string>>(() => new Set([TRACKER_GUIDES[0]?.id].filter(Boolean) as string[]));
// По умолчанию выбран первый элемент первой категории
const [selectedId, setSelectedId] = useState<string>(TRACKER_GUIDES[0]?.items[0]?.id ?? '');
const query = search.trim().toLowerCase();
// Фильтрация дерева: поиск по названию устройства, категории и протоколу
const filtered = useMemo(() => {
if (!query) return TRACKER_GUIDES.map((c) => ({ ...c, matchedItems: c.items }));
return TRACKER_GUIDES.map((category) => {
const categoryMatch = category.name.toLowerCase().includes(query);
const matchedItems = category.items.filter(
(item) =>
categoryMatch ||
item.name.toLowerCase().includes(query) ||
item.protocol.toLowerCase().includes(query)
);
return { ...category, matchedItems };
}).filter((c) => c.matchedItems.length > 0);
}, [query]);
const isExpanded = (categoryId: string) => (query ? true : expanded.has(categoryId));
const toggleCategory = (categoryId: string) => {
setExpanded((prev) => {
const next = new Set(prev);
if (next.has(categoryId)) next.delete(categoryId);
else next.add(categoryId);
return next;
});
};
const selected: TrackerGuideItem | undefined = useMemo(
() => TRACKER_GUIDES.flatMap((c) => c.items).find((i) => i.id === selectedId),
[selectedId]
);
const copyCommand = async (command: string) => {
const text = resolveGuideHost(command);
try {
await navigator.clipboard.writeText(text);
toast({ title: 'Скопировано', description: text });
} catch {
toast({ title: 'Не удалось скопировать', description: text, variant: 'destructive' });
}
};
return (
<div className="space-y-4">
{/* Общий шаг для всех устройств */}
<Alert>
<Info className="h-4 w-4" />
<AlertTitle>Сначала создайте объект в CRM</AlertTitle>
<AlertDescription className="text-xs">
/gps → Объекты → Добавить: в поле «IMEI / ID трекера» укажите идентификатор устройства
(для GT06 — IMEI с наклейки; для Traccar Client — придуманный ID). В командах ниже адрес
сервера уже подставлен — можно использовать доменное имя или IP сервера.
</AlertDescription>
</Alert>
<div className="grid grid-cols-1 md:grid-cols-[280px_1fr] gap-4">
{/* Дерево устройств + поиск */}
<Card>
<CardContent className="p-3 space-y-2">
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
className="h-8 pl-7 text-xs"
placeholder="Поиск по названию, бренду, протоколу…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
{filtered.length === 0 ? (
<p className="text-xs text-muted-foreground py-4 text-center">Ничего не найдено</p>
) : (
<div className="space-y-1">
{filtered.map((category) => (
<div key={category.id}>
<button
type="button"
className="w-full flex items-center gap-1 px-1.5 py-1.5 rounded text-xs font-medium text-foreground hover:bg-muted/50 transition-colors text-left"
onClick={() => toggleCategory(category.id)}
>
{isExpanded(category.id) ? (
<ChevronDown className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
) : (
<ChevronRight className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
)}
<span className="truncate">{category.name}</span>
</button>
{isExpanded(category.id) && (
<div className="ml-4 space-y-0.5">
{category.matchedItems.map((item) => (
<button
key={item.id}
type="button"
onClick={() => setSelectedId(item.id)}
className={cn(
'w-full text-left px-2 py-1 rounded text-xs transition-colors truncate',
selectedId === item.id
? 'bg-muted text-foreground font-medium'
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'
)}
>
{item.name}
</button>
))}
</div>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Карточка выбранного устройства */}
<Card>
{selected ? (
<>
<CardHeader className="pb-3">
<div className="flex items-center gap-2 flex-wrap">
<CardTitle className="text-base">{selected.name}</CardTitle>
<Badge variant="outline" className="text-[10px]">
Порт {selected.port}
</Badge>
<Badge variant="secondary" className="text-[10px]">
{selected.protocol}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="text-xs font-semibold mb-2">Шаги настройки</h4>
<ol className="list-decimal list-inside space-y-1.5 text-xs text-muted-foreground">
{selected.steps.map((step, idx) => (
<li key={idx}>{resolveGuideHost(step)}</li>
))}
</ol>
</div>
{selected.sms && selected.sms.length > 0 && (
<div>
<h4 className="text-xs font-semibold mb-2">SMS-команды</h4>
<div className="space-y-2">
{selected.sms.map((sms, idx) => (
<div key={idx} className="border border-border rounded p-2 space-y-1">
<div className="flex items-center gap-2">
<code className="flex-1 text-xs font-mono bg-muted rounded px-1.5 py-1 break-all">
{resolveGuideHost(sms.command)}
</code>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 shrink-0"
title="Скопировать команду"
onClick={() => copyCommand(sms.command)}
>
<Copy className="w-3.5 h-3.5" />
</Button>
</div>
<p className="text-[11px] text-muted-foreground">{resolveGuideHost(sms.description)}</p>
</div>
))}
</div>
</div>
)}
{selected.notes && selected.notes.length > 0 && (
<div>
<h4 className="text-xs font-semibold mb-1.5">Примечания</h4>
<ul className="list-disc list-inside space-y-1 text-[11px] text-muted-foreground">
{selected.notes.map((note, idx) => (
<li key={idx}>{resolveGuideHost(note)}</li>
))}
</ul>
</div>
)}
</CardContent>
</>
) : (
<CardContent className="p-8 text-center text-sm text-muted-foreground">
Выберите устройство в дереве слева
</CardContent>
)}
</Card>
</div>
</div>
);
}

View File

@@ -1,12 +1,13 @@
import { useLocation } from 'wouter';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Map, Truck, Hexagon, Route, Folder } from 'lucide-react';
import { Map, Truck, Hexagon, Route, Folder, BookOpen } from 'lucide-react';
import Layout from '@/components/Layout';
import { GpsMapTab } from './MapTab';
import { GpsAssetsTab } from './AssetsTab';
import { GpsGeozonesTab } from './GeozonesTab';
import { GpsRoutesTab } from './RoutesTab';
import { GpsGroupsTab } from './GroupsTab';
import { GpsGuidesTab } from './GuidesTab';
const TABS = [
{ value: 'map', label: 'Карта', icon: Map },
@@ -14,6 +15,7 @@ const TABS = [
{ value: 'geozones', label: 'Геозоны', icon: Hexagon },
{ value: 'routes', label: 'Маршруты', icon: Route },
{ value: 'groups', label: 'Группы', icon: Folder },
{ value: 'guides', label: 'Инструкции', icon: BookOpen },
];
/**
@@ -57,6 +59,9 @@ export default function GpsPage() {
<TabsContent value="groups">
<GpsGroupsTab />
</TabsContent>
<TabsContent value="guides">
<GpsGuidesTab />
</TabsContent>
</Tabs>
</div>
</Layout>

View File

@@ -0,0 +1,344 @@
/**
* Статический справочник по настройке GPS-трекеров (вкладка «Инструкции»).
* Дерево: категории → устройства/протоколы. Порты соответствуют реально
* открытым на сервере портам Traccar (5023 GT06, 5027 Teltonika, 5001 GPS103,
* 5013 H02, 5036 Suntech, 5055 OsmAnd/HTTP).
*
* В текстах и SMS-командах плейсхолдер {host} заменяется в UI на
* window.location.hostname (для iistwin.ru резолвится в IP сервера).
*/
export interface TrackerGuideSms {
/** Текст SMS-команды (с плейсхолдером {host}) */
command: string;
description: string;
}
export interface TrackerGuideItem {
id: string;
/** Название устройства/протокола */
name: string;
/** Порт сервера Traccar */
port: number;
/** Протокол Traccar */
protocol: string;
/** Шаги настройки (нумерованный список) */
steps: string[];
/** SMS-команды (опционально) */
sms?: TrackerGuideSms[];
/** Примечания (muted-текст) */
notes?: string[];
}
export interface TrackerGuideCategory {
id: string;
name: string;
description?: string;
items: TrackerGuideItem[];
}
const APN_NOTE =
'APN оператора: internet.mts.ru (МТС), internet.beeline.ru (Билайн), internet.tele2.ru (Tele2), internet.megafon.ru (МегаФон)';
export const TRACKER_GUIDES: TrackerGuideCategory[] = [
{
id: 'gt06',
name: 'Автомобильные трекеры (GT06)',
description: 'Самый массовый протокол китайских трекеров — Concox, Coban, Jimi и клоны',
items: [
{
id: 'concox-gt06n',
name: 'Concox GT06N / GT06E',
port: 5023,
protocol: 'GT06',
steps: [
'Вставьте в трекер SIM-карту с отключённым PIN-кодом и включённым мобильным интернетом',
'Узнайте IMEI устройства (наклейка на корпусе или команда check123456)',
'Настройте трекер SMS-командами ниже: APN → сервер → включение GPRS',
'В CRM создайте объект (/gps → Объекты → Добавить) и укажите IMEI в поле «IMEI / ID трекера»',
'Подайте питание на трекер — через 1–2 минуты объект появится на карте',
],
sms: [
{ command: 'apn123456 {apn}', description: 'Установить APN оператора (например: apn123456 internet.mts.ru)' },
{ command: 'adminip123456 {host} 5023', description: 'Сервер и порт (вместо {host} — адрес сервера или IP)' },
{ command: 'gprs123456', description: 'Включить передачу по GPRS' },
{ command: 'check123456', description: 'Проверка настроек: ответ содержит IMEI, APN, сервер' },
],
notes: [
'123456 — заводской SMS-пароль по умолчанию; если меняли — подставьте свой',
APN_NOTE,
],
},
{
id: 'coban-gps303',
name: 'Coban GPS303',
port: 5023,
protocol: 'GT06',
steps: [
'Вставьте SIM-карту без PIN-кода',
'Отправьте SMS-команды настройки APN и сервера',
'Создайте объект в CRM с IMEI трекера',
'Проверьте статус командой check123456 — в ответе должен быть адрес сервера',
],
sms: [
{ command: 'apn123456 {apn}', description: 'Установить APN оператора' },
{ command: 'adminip123456 {host} 5023', description: 'Сервер и порт' },
{ command: 'gprs123456', description: 'Включить GPRS-режим' },
{ command: 'check123456', description: 'Проверка текущих настроек' },
],
notes: [
'У Coban встречаются прошивки с другим набором команд — если apn123456 не принимается, попробуйте формат APN,123456,{apn}#',
APN_NOTE,
],
},
{
id: 'jimi-jm01',
name: 'Jimi / Concox JM01',
port: 5023,
protocol: 'GT06',
steps: [
'Вставьте SIM-карту, включите трекер',
'Настройте APN и сервер SMS-командами',
'Создайте объект в CRM с IMEI трекера',
],
sms: [
{ command: 'apn123456 {apn}', description: 'Установить APN оператора' },
{ command: 'adminip123456 {host} 5023', description: 'Сервер и порт' },
{ command: 'gprs123456', description: 'Включить GPRS' },
{ command: 'check123456', description: 'Проверка настроек' },
],
notes: ['JM01 — видеотрекер; по GT06 передаются координаты, видео настраивается отдельно', APN_NOTE],
},
{
id: 'gt06-generic',
name: 'Любой GT06-совместимый трекер',
port: 5023,
protocol: 'GT06',
steps: [
'Узнайте IMEI устройства',
'По инструкции вендора задайте APN и адрес сервера {host}, порт 5023',
'Включите передачу данных по GPRS/TCP',
'Создайте объект в CRM с этим IMEI',
],
notes: [
'GT06 — протокол сотен безымянных китайских трекеров; SMS-команды у каждого вендора свои, смотрите документацию устройства',
'Частые варианты команд сервера: adminip123456, IP123456, pw,123456,ip,...# (формат H02)',
APN_NOTE,
],
},
],
},
{
id: 'teltonika',
name: 'Teltonika',
description: 'Профессиональные трекеры, настройка через Teltonika Configurator',
items: [
{
id: 'teltonika-fmb920',
name: 'Teltonika FMB920',
port: 5027,
protocol: 'Teltonika (FMB)',
steps: [
'Подключите трекер к ПК и откройте Teltonika Configurator',
'Перейдите на вкладку GPRS',
'Domain: {host} (или IP сервера), Port: 5027, Protocol: TCP',
'APN: APN вашего оператора (internet.mts.ru / internet.beeline.ru / ...)',
'Сохраните конфигурацию в устройство (Save to device)',
'Создайте объект в CRM с IMEI трекера',
],
notes: [APN_NOTE, 'Записи сначала накапливаются во флэш-памяти и выгружаются пачками — первая точка может появиться с задержкой'],
},
{
id: 'teltonika-fmb125',
name: 'Teltonika FMB125',
port: 5027,
protocol: 'Teltonika (FMB)',
steps: [
'Откройте Teltonika Configurator, вкладка GPRS',
'Domain: {host}, Port: 5027, Protocol: TCP',
'Укажите APN оператора и сохраните в устройство',
'Создайте объект в CRM с IMEI трекера',
],
notes: [APN_NOTE],
},
{
id: 'teltonika-fmc130',
name: 'Teltonika FMC130',
port: 5027,
protocol: 'Teltonika (FMB)',
steps: [
'Откройте Teltonika Configurator, вкладка GPRS',
'Domain: {host}, Port: 5027, Protocol: TCP',
'Укажите APN оператора и сохраните в устройство',
'Создайте объект в CRM с IMEI трекера',
],
notes: [APN_NOTE, 'FMC130 — 4G/LTE: убедитесь, что SIM поддерживает LTE-данные'],
},
],
},
{
id: 'gps103',
name: 'GPS103 / TK-серия',
description: 'TK-102/103, Xexun и совместимые',
items: [
{
id: 'tk103',
name: 'TK-102 / TK-103',
port: 5001,
protocol: 'GPS103',
steps: [
'Вставьте SIM-карту без PIN-кода',
'Отправьте SMS-команды: begin → APN → сервер',
'Создайте объект в CRM с IMEI трекера',
],
sms: [
{ command: 'begin123456', description: 'Сброс к заводским настройкам' },
{ command: 'apn123456 {apn}', description: 'Установить APN оператора' },
{ command: 'adminip123456 {host} 5001', description: 'Сервер и порт' },
],
notes: ['123456 — пароль по умолчанию', APN_NOTE],
},
{
id: 'xexun',
name: 'Xexun (TK-совместимые)',
port: 5001,
protocol: 'GPS103',
steps: [
'Настройте APN и сервер SMS-командами',
'Создайте объект в CRM с IMEI трекера',
],
sms: [
{ command: 'begin123456', description: 'Сброс к заводским настройкам' },
{ command: 'apn123456 {apn}', description: 'Установить APN оператора' },
{ command: 'adminip123456 {host} 5001', description: 'Сервер и порт' },
],
notes: [APN_NOTE],
},
],
},
{
id: 'h02',
name: 'H02-совместимые',
description: 'Китайские трекеры и часы с протоколом H02',
items: [
{
id: 'h02-generic',
name: 'H02-совместимый трекер / часы',
port: 5013,
protocol: 'H02',
steps: [
'Вставьте SIM-карту с мобильным интернетом',
'Отправьте SMS с адресом сервера в формате ниже',
'Создайте объект в CRM с IMEI устройства',
],
sms: [
{ command: 'pw,123456,ip,{host},5013#', description: 'Сервер и порт (пароль 123456)' },
],
notes: [
'Формат SMS зависит от прошивки: у части устройств команда выглядит как pw,123456,ip,{host},5013# — у других иначе, сверяйтесь с документацией',
APN_NOTE,
],
},
],
},
{
id: 'suntech',
name: 'Suntech',
description: 'ST300/ST310 и совместимые',
items: [
{
id: 'suntech-st300',
name: 'Suntech ST300 / ST310',
port: 5036,
protocol: 'Suntech',
steps: [
'Откройте Suntech-конфигуратор (или настройте по SMS по документации вендора)',
'Укажите сервер: {host}, порт 5036',
'Укажите APN оператора',
'Создайте объект в CRM с идентификатором устройства',
],
notes: [APN_NOTE, 'Идентификатор Suntech — Device ID из конфигуратора (обычно совпадает с IMEI)'],
},
],
},
{
id: 'smartphone',
name: 'Смартфон как трекер',
description: 'Приложения, шлющие координаты по HTTP (порт 5055, протокол OsmAnd)',
items: [
{
id: 'traccar-client',
name: 'Traccar Client (Android / iOS)',
port: 5055,
protocol: 'OsmAnd (HTTP)',
steps: [
'Установите Traccar Client из магазина приложений',
'Server URL: http://{host}:5055',
'Device identifier: IMEI/ID из карточки объекта в CRM (сначала создайте объект и задайте ID)',
'Частота отправки (Frequency): 60 секунд',
'Включите сервис в приложении — точки пойдут на карту',
],
notes: [
'Рекомендуемый способ для смартфона: работает в фоне',
'Device identifier придумываете сами (например, ivan-phone) — он же указывается в поле «IMEI / ID трекера» объекта в CRM',
],
},
{
id: 'gpslogger',
name: 'GPSLogger (Android)',
port: 5055,
protocol: 'OsmAnd (HTTP)',
steps: [
'Установите GPSLogger, откройте настройки отправки (Logging → Custom URL)',
'Custom URL: http://{host}:5055/?id=<ID>&lat=%LAT&lon=%LON&speed=%SPD&bearing=%BRR&timestamp=%TIMESTAMP',
'<ID> — идентификатор объекта в CRM (поле «IMEI / ID трекера»)',
'Включите запись трека в приложении',
],
notes: ['Бесплатный, open source; работает в фоне'],
},
{
id: 'owntracks',
name: 'OwnTracks',
port: 5055,
protocol: 'OsmAnd (HTTP)',
steps: [
'Переведите OwnTracks в режим HTTP',
'URL: http://{host}:5055/?id=<ID>',
'<ID> — идентификатор объекта в CRM',
],
notes: [
'Совместим частично (шлёт не все поля OsmAnd-формата) — приоритет у Traccar Client',
],
},
],
},
{
id: 'pwa',
name: 'Без устройства (PWA)',
description: 'Встроенный трекинг прямо из браузера/PWA — ничего устанавливать не нужно',
items: [
{
id: 'pwa-tracking',
name: 'Встроенный трекинг (профиль пользователя)',
port: 443,
protocol: 'Внутренний (HTTPS, POST /api/gps/my-position)',
steps: [
'Откройте свой профиль: /profile → вкладка «GPS»',
'Включите переключатель «Отслеживать мою геолокацию»',
'Разрешите браузеру доступ к геолокации',
'Объект появится на карте автоматически (создаётся сам при первой точке)',
],
notes: [
'Координаты отправляются, пока открыто приложение (не чаще раза в 60 сек)',
'Для фонового отслеживания используйте Traccar Client (раздел «Смартфон как трекер»)',
],
},
],
},
];
/** Подстановка адреса сервера вместо плейсхолдера {host} */
export function resolveGuideHost(text: string): string {
const host = typeof window !== 'undefined' ? window.location.hostname : 'iistwin.ru';
return text.replaceAll('{host}', host);
}