From 3ea0f8c1877b35acefe69182fc888103842cd88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=D1=81=20=D0=A1=D1=83=D0=BB=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Fri, 28 Aug 2026 10:53:34 +0300 Subject: [PATCH] =?UTF-8?q?feat(gps):=20=D1=84=D1=80=D0=BE=D0=BD=D1=82=20?= =?UTF-8?q?=D0=B8=D0=BD=D0=B4=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D1=80=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=82=D0=B5=D1=80=D0=B8=20GPS=20=E2=80=94=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=B9=D0=B4=D0=B6=20=C2=AB=D0=9D=D0=B5=D1=82=20GPS=C2=BB?= =?UTF-8?q?=20=D0=B2=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D0=B0=D1=85,?= =?UTF-8?q?=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D0=BE=D0=B3=D0=B0=20=D0=B2=20=D1=87=D0=B0?= =?UTF-8?q?=D1=81=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/gps/AssetsTab.tsx | 26 ++++++++++++++++++++++++-- client/src/pages/gps/SettingsTab.tsx | 24 +++++++++++++++++++++++- client/src/pages/gps/types.ts | 2 ++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/client/src/pages/gps/AssetsTab.tsx b/client/src/pages/gps/AssetsTab.tsx index d97d871..25a822c 100644 --- a/client/src/pages/gps/AssetsTab.tsx +++ b/client/src/pages/gps/AssetsTab.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -9,7 +9,7 @@ import { Switch } from '@/components/ui/switch'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { Loader2, Pencil, Plus, Trash2, BellRing } from 'lucide-react'; +import { Loader2, Pencil, Plus, Trash2, BellRing, Satellite } from 'lucide-react'; import { apiRequest } from '@/lib/queryClient'; import { useToast } from '@/hooks/use-toast'; import { invalidateGps, useGpsAssetSubscribers, useGpsAssets, useOrgUsers } from '@/hooks/useGps'; @@ -21,6 +21,12 @@ function formatDateTime(value: string | null): string { return new Date(value).toLocaleString('ru-RU'); } +/** Потерян ли GPS: последний фикс старше порога (часов) */ +function isGpsLost(lastGpsFixAt: string | null | undefined, thresholdHours: number): boolean { + if (!lastGpsFixAt) return false; // данных ещё не было — не пугаем + return Date.now() - new Date(lastGpsFixAt).getTime() > thresholdHours * 3600_000; +} + function userLabel(u: { firstName?: string; lastName?: string; email: string; fullName?: string }): string { return u.fullName || `${u.firstName || ''} ${u.lastName || ''}`.trim() || u.email; } @@ -50,6 +56,13 @@ const EMPTY_FORM: AssetFormState = { export function GpsAssetsTab() { const { toast } = useToast(); const { data, isLoading } = useGpsAssets(); + // Порог потери GPS (часов) из настроек организации + const { data: gpsSettings } = useQuery<{ gpsLostHours?: number }>({ + queryKey: ['/api/gps/settings'], + queryFn: async () => (await apiRequest('GET', '/api/gps/settings')).json(), + staleTime: 60_000, + }); + const gpsLostHours = gpsSettings?.gpsLostHours ?? 5; const { data: usersData } = useOrgUsers(); const assets = data?.assets || []; const users = usersData?.users || []; @@ -173,6 +186,15 @@ export function GpsAssetsTab() { style={{ backgroundColor: asset.isOnline ? '#22c55e' : '#9ca3af' }} /> {asset.isOnline ? 'Онлайн' : 'Офлайн'} + {isGpsLost(asset.lastGpsFixAt, gpsLostHours) && ( + + + Нет GPS + + )} {asset.externalId || '—'} diff --git a/client/src/pages/gps/SettingsTab.tsx b/client/src/pages/gps/SettingsTab.tsx index 2a44ee4..c205238 100644 --- a/client/src/pages/gps/SettingsTab.tsx +++ b/client/src/pages/gps/SettingsTab.tsx @@ -15,6 +15,7 @@ interface GpsSettings { jitterMeters: number; maxJumpSpeedKmh: number; tripStopMinutes: number; + gpsLostHours: number; defaultLat: number | null; defaultLng: number | null; defaultZoom: number | null; @@ -43,12 +44,14 @@ export function GpsSettingsTab() { const [jitterMeters, setJitterMeters] = useState('0'); const [maxJumpSpeedKmh, setMaxJumpSpeedKmh] = useState('0'); const [tripStopMinutes, setTripStopMinutes] = useState('10'); + const [gpsLostHours, setGpsLostHours] = useState('5'); useEffect(() => { if (data) { setJitterMeters(String(data.jitterMeters)); setMaxJumpSpeedKmh(String(data.maxJumpSpeedKmh)); setTripStopMinutes(String(data.tripStopMinutes)); + setGpsLostHours(String(data.gpsLostHours)); } }, [data]); @@ -64,6 +67,7 @@ export function GpsSettingsTab() { jitterMeters: Number(jitterMeters), maxJumpSpeedKmh: Number(maxJumpSpeedKmh), tripStopMinutes: Number(tripStopMinutes), + gpsLostHours: Number(gpsLostHours), }); return res.json(); }, @@ -80,6 +84,7 @@ export function GpsSettingsTab() { const jitterValid = isIntInRange(jitterMeters, 1000); const jumpValid = isIntInRange(maxJumpSpeedKmh, 5000); const tripValid = isIntInRange(tripStopMinutes, 120); + const gpsLostValid = /^\d+$/.test(gpsLostHours.trim()) && Number(gpsLostHours) >= 1 && Number(gpsLostHours) <= 72; return (
@@ -129,6 +134,23 @@ export function GpsSettingsTab() { {!jumpValid &&

Целое число от 0 до 5000

}
+
+ + setGpsLostHours(e.target.value)} + /> +

+ Если трекер не видел спутники дольше N часов (шлёт только старый фикс + или позицию по вышкам), на вкладке «Объекты» показывается предупреждение + «Нет GPS» с временем последнего фикса. +

+ {!gpsLostValid &&

Целое число от 1 до 72

} +
+
saveTrackingMutation.mutate()} > {saveTrackingMutation.isPending ? ( diff --git a/client/src/pages/gps/types.ts b/client/src/pages/gps/types.ts index b0b0c72..d7c979b 100644 --- a/client/src/pages/gps/types.ts +++ b/client/src/pages/gps/types.ts @@ -22,6 +22,7 @@ export interface GpsAsset { lastSeenAt: string | null; /** Время последней точки по устройству (может быть старым без GPS-фикса) */ lastRecordedAt?: string | null; + lastGpsFixAt?: string | null; createdAt: string | null; /** Группы объекта (добавляет GET /api/gps/assets) */ groups?: Array<{ id: number; name: string }>; @@ -46,6 +47,7 @@ export interface GpsPosition { lastSeenAt?: string | null; /** Время последней точки по устройству (может быть старым без GPS-фикса) */ lastRecordedAt?: string | null; + lastGpsFixAt?: string | null; } /** Точка трека (GET /api/gps/assets/:id/track) */