Files
egorin/src/modules/devices/DevicesModule.tsx
T
GermanandClaude Opus 4.8 b1c52ab748 Железный Егорин — форк Орбиты: тёплый стиль (amber), логин с odyssey WebGL-молнией справа, энергомониторинг+панель
Ребренд Мониторинг Орбита -> Железный Егорин; accent green->amber #F59E0B; LoginScreen правая половина = Lightning (WebGL, hue 35 тёплый) под стеклянной формой; левая панель — тёплый градиент.
Доступ агентам: репо публичный, push german-токеном.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 23:57:16 +03:00

132 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}