Ребренд Мониторинг Орбита -> Железный Егорин; accent green->amber #F59E0B; LoginScreen правая половина = Lightning (WebGL, hue 35 тёплый) под стеклянной формой; левая панель — тёплый градиент. Доступ агентам: репо публичный, push german-токеном. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
132 lines
5.5 KiB
TypeScript
132 lines
5.5 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { Smartphone, Plus, Trash2, Info } from 'lucide-react';
|
||
import { PageHeader } from '@/components/PageHeader';
|
||
import { Card, CardHeader, Badge } from '@/components/ui/primitives';
|
||
import { apiGet, apiPost, apiDelete } from '@/api/client';
|
||
import { useAuth } from '@/core/store';
|
||
|
||
interface Device {
|
||
imei: string;
|
||
name: string;
|
||
}
|
||
|
||
export function DevicesModule() {
|
||
const user = useAuth((s) => s.user);
|
||
const [devices, setDevices] = useState<Device[]>([]);
|
||
const [imei, setImei] = useState('');
|
||
const [name, setName] = useState('');
|
||
const [err, setErr] = useState('');
|
||
const [busy, setBusy] = useState(false);
|
||
|
||
const refresh = () => apiGet<Device[]>('/devices').then(setDevices).catch(() => setDevices([]));
|
||
useEffect(() => {
|
||
refresh();
|
||
}, []);
|
||
|
||
const bind = async () => {
|
||
setErr('');
|
||
if (!/^\d{6,20}$/.test(imei.trim())) {
|
||
setErr('IMEI — только цифры (6–20 знаков)');
|
||
return;
|
||
}
|
||
setBusy(true);
|
||
try {
|
||
await apiPost('/devices/bind', { imei: imei.trim(), name: name.trim() || undefined });
|
||
setImei('');
|
||
setName('');
|
||
await refresh();
|
||
} catch {
|
||
setErr('Не удалось привязать устройство');
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
};
|
||
const remove = async (i: string) => {
|
||
await apiDelete(`/devices/${i}`);
|
||
refresh();
|
||
};
|
||
|
||
return (
|
||
<div>
|
||
<PageHeader
|
||
icon={Smartphone}
|
||
title="Мои устройства"
|
||
desc="Привязка трекеров Navtelecom по IMEI к вашему кабинету"
|
||
actions={<Badge severity="info">кабинет: {user.clientId}</Badge>}
|
||
/>
|
||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||
<Card className="lg:col-span-2">
|
||
<CardHeader title="Привязанные трекеры" desc={`${devices.length} устройств(а)`} />
|
||
<div className="px-4 pb-4">
|
||
<div className="flex flex-col gap-2 sm:flex-row">
|
||
<input
|
||
value={imei}
|
||
onChange={(e) => setImei(e.target.value)}
|
||
placeholder="IMEI трекера"
|
||
inputMode="numeric"
|
||
className="tnum flex-1 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none placeholder:text-fg-muted focus:border-accent"
|
||
/>
|
||
<input
|
||
value={name}
|
||
onChange={(e) => setName(e.target.value)}
|
||
placeholder="Название (опционально)"
|
||
className="flex-1 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-fg outline-none placeholder:text-fg-muted focus:border-accent"
|
||
/>
|
||
<button
|
||
onClick={bind}
|
||
disabled={busy}
|
||
className="inline-flex items-center justify-center gap-1.5 rounded-lg bg-accent px-4 py-2 text-sm font-semibold text-[#06210f] hover:opacity-90 disabled:opacity-50"
|
||
>
|
||
<Plus className="h-4 w-4" /> Привязать
|
||
</button>
|
||
</div>
|
||
{err && <p className="mt-1.5 text-xs text-crit">{err}</p>}
|
||
|
||
<div className="mt-3 divide-y divide-border">
|
||
{devices.length === 0 ? (
|
||
<p className="py-7 text-center text-sm text-fg-muted">
|
||
Устройств пока нет. Привяжите трекер по IMEI — данные появятся после первого сеанса связи.
|
||
</p>
|
||
) : (
|
||
devices.map((d) => (
|
||
<div key={d.imei} className="flex items-center gap-3 py-3">
|
||
<div className="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-accent/15 text-accent">
|
||
<Smartphone className="h-4 w-4" />
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="truncate text-sm font-medium text-fg">{d.name}</div>
|
||
<div className="font-mono text-[11px] text-fg-muted">IMEI {d.imei}</div>
|
||
</div>
|
||
<button onClick={() => remove(d.imei)} className="grid h-8 w-8 shrink-0 place-items-center rounded-lg text-fg-muted hover:bg-muted hover:text-crit" title="Отвязать">
|
||
<Trash2 className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
<Card className="lg:col-span-1">
|
||
<CardHeader title="Как подключить трекер" />
|
||
<div className="space-y-2.5 px-4 pb-4 text-sm text-fg-muted">
|
||
<p className="flex items-start gap-2">
|
||
<Info className="mt-0.5 h-4 w-4 shrink-0 text-info" /> В конфигураторе Navtelecom укажите телематический сервер:
|
||
</p>
|
||
<div className="rounded-lg bg-bg/40 p-3 font-mono text-xs leading-relaxed text-fg">
|
||
домен: gps.servaki.online
|
||
<br />
|
||
порт: 2000
|
||
<br />
|
||
протокол: FLEX
|
||
</div>
|
||
<p>
|
||
После привязки IMEI входящие данные попадут в ваш кабинет (<span className="font-mono">{user.clientId}</span>) и появятся в GPS-мониторинге — только вам.
|
||
</p>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|