136 lines
5.4 KiB
TypeScript
136 lines
5.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Loader2, Save, SlidersHorizontal } from 'lucide-react';
|
||
import { apiRequest } from '@/lib/queryClient';
|
||
import { useToast } from '@/hooks/use-toast';
|
||
|
||
interface GpsSettings {
|
||
success: boolean;
|
||
jitterMeters: number;
|
||
maxJumpSpeedKmh: number;
|
||
}
|
||
|
||
/**
|
||
* Таб «Настройки» GPS-модуля.
|
||
* Секция «Трекинг»: фильтры входящих точек — дрожание позиции (джиттер)
|
||
* и максимальный скачок скорости (LBS-прыжки). Сюда же позже добавятся
|
||
* другие настройки модуля — структура секций-Card под это готова.
|
||
*/
|
||
export function GpsSettingsTab() {
|
||
const { toast } = useToast();
|
||
const { data, isLoading } = useQuery<GpsSettings>({
|
||
queryKey: ['/api/gps/settings'],
|
||
});
|
||
|
||
const [jitterMeters, setJitterMeters] = useState('0');
|
||
const [maxJumpSpeedKmh, setMaxJumpSpeedKmh] = useState('0');
|
||
|
||
useEffect(() => {
|
||
if (data) {
|
||
setJitterMeters(String(data.jitterMeters));
|
||
setMaxJumpSpeedKmh(String(data.maxJumpSpeedKmh));
|
||
}
|
||
}, [data]);
|
||
|
||
const saveMutation = useMutation({
|
||
mutationFn: async () => {
|
||
// Оба поля сохраняются одним PUT (непереданные на сервере сохраняются)
|
||
const res = await apiRequest('PUT', '/api/gps/settings', {
|
||
jitterMeters: Number(jitterMeters),
|
||
maxJumpSpeedKmh: Number(maxJumpSpeedKmh),
|
||
});
|
||
return res.json();
|
||
},
|
||
onSuccess: () => {
|
||
toast({ title: 'Настройки сохранены' });
|
||
},
|
||
onError: (err: any) => {
|
||
toast({
|
||
title: 'Ошибка',
|
||
description: err?.message || 'Не удалось сохранить настройки',
|
||
variant: 'destructive',
|
||
});
|
||
},
|
||
});
|
||
|
||
const isIntInRange = (v: string, max: number) => /^\d+$/.test(v.trim()) && Number(v) >= 0 && Number(v) <= max;
|
||
const jitterValid = isIntInRange(jitterMeters, 1000);
|
||
const jumpValid = isIntInRange(maxJumpSpeedKmh, 5000);
|
||
|
||
return (
|
||
<div className="max-w-2xl space-y-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base flex items-center gap-2">
|
||
<SlidersHorizontal className="w-4 h-4" />
|
||
Трекинг
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
{isLoading ? (
|
||
<div className="flex items-center justify-center h-16">
|
||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="space-y-1">
|
||
<Label className="text-xs">Фильтр дрожания позиции, метров</Label>
|
||
<Input
|
||
type="text"
|
||
inputMode="numeric"
|
||
className="h-8 w-32"
|
||
value={jitterMeters}
|
||
onChange={(e) => setJitterMeters(e.target.value)}
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
0 = выключен. Точки ближе N метров при скорости < 5 км/ч не записываются — убирает
|
||
«кучу мелких перемещений» у стоящего объекта. Действует на новые точки.
|
||
</p>
|
||
{!jitterValid && (
|
||
<p className="text-xs text-destructive">Целое число от 0 до 1000</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<Label className="text-xs">Максимальный скачок, км/ч</Label>
|
||
<Input
|
||
type="text"
|
||
inputMode="numeric"
|
||
className="h-8 w-32"
|
||
value={maxJumpSpeedKmh}
|
||
onChange={(e) => setMaxJumpSpeedKmh(e.target.value)}
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
0 = выключен. Точки, требующие перемещения быстрее N км/ч от предыдущей,
|
||
отбрасываются — отсекает LBS-прыжки на вышки при потере GPS. Действует на новые точки.
|
||
</p>
|
||
{!jumpValid && (
|
||
<p className="text-xs text-destructive">Целое число от 0 до 5000</p>
|
||
)}
|
||
</div>
|
||
|
||
<Button
|
||
size="sm"
|
||
className="h-8"
|
||
disabled={saveMutation.isPending || !jitterValid || !jumpValid}
|
||
onClick={() => saveMutation.mutate()}
|
||
>
|
||
{saveMutation.isPending ? (
|
||
<Loader2 className="w-3.5 h-3.5 mr-1 animate-spin" />
|
||
) : (
|
||
<Save className="w-3.5 h-3.5 mr-1" />
|
||
)}
|
||
Сохранить
|
||
</Button>
|
||
</>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|