feat(control-center): adapt InstallationsPage mobile-first cards, Russian strings, and tokens
This commit is contained in:
parent
5115517403
commit
a2b0a183e2
1 changed files with 113 additions and 46 deletions
|
|
@ -5,10 +5,12 @@ import {
|
|||
Monitor,
|
||||
Globe,
|
||||
AlertTriangle,
|
||||
Radio,
|
||||
} from 'lucide-react';
|
||||
import { useKccStore } from '../store/kccStore';
|
||||
import { StatusBadge } from '../components/StatusBadge';
|
||||
import { InstallationKind, InstallationStatus } from '../contracts';
|
||||
import { RU_PLATFORMS, RU_INSTALLATION_STATUS } from '../locales/ru';
|
||||
|
||||
export const InstallationsPage: React.FC = () => {
|
||||
const installations = useKccStore((state) => state.installations);
|
||||
|
|
@ -18,11 +20,13 @@ export const InstallationsPage: React.FC = () => {
|
|||
const [statusFilter, setStatusFilter] = useState<'all' | InstallationStatus>('all');
|
||||
|
||||
const filtered = installations.filter((item) => {
|
||||
const platformRu = RU_PLATFORMS[item.kind] || '';
|
||||
const matchSearch =
|
||||
!search ||
|
||||
item.deviceName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
item.tenantName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
item.appVersion.includes(search);
|
||||
item.appVersion.includes(search) ||
|
||||
platformRu.toLowerCase().includes(search.toLowerCase());
|
||||
const matchPlatform = platformFilter === 'all' || item.kind === platformFilter;
|
||||
const matchStatus = statusFilter === 'all' || item.status === statusFilter;
|
||||
return matchSearch && matchPlatform && matchStatus;
|
||||
|
|
@ -35,46 +39,50 @@ export const InstallationsPage: React.FC = () => {
|
|||
const getPlatformIcon = (kind: InstallationKind) => {
|
||||
switch (kind) {
|
||||
case 'android':
|
||||
return <Smartphone className="w-4 h-4 text-emerald-400" />;
|
||||
return <Smartphone className="w-4 h-4 text-emerald-500" />;
|
||||
case 'desktop':
|
||||
return <Monitor className="w-4 h-4 text-sky-400" />;
|
||||
return <Monitor className="w-4 h-4 text-sky-500" />;
|
||||
case 'web':
|
||||
return <Globe className="w-4 h-4 text-amber-400" />;
|
||||
return <Globe className="w-4 h-4 text-amber-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 border-b border-zinc-800 pb-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 border-b border-krona-border pb-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-zinc-100 flex items-center gap-2">
|
||||
<Smartphone className="w-5 h-5 text-amber-400" />
|
||||
<h1 className="text-lg md:text-xl font-bold text-krona-primary flex items-center gap-2">
|
||||
<Smartphone className="w-5 h-5 text-amber-500 shrink-0" />
|
||||
<span>Установки и клиентские приложения</span>
|
||||
</h1>
|
||||
<p className="text-xs text-zinc-400 mt-0.5">
|
||||
<p className="text-xs text-krona-secondary mt-0.5">
|
||||
Мониторинг клиентских устройств Web, Desktop и Android (ТСД терминалы сбора данных и кассы)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-zinc-400">
|
||||
Всего установок: <span className="font-bold text-zinc-100">{installations.length}</span> (найдено:{' '}
|
||||
<span className="font-bold text-amber-400">{filtered.length}</span>)
|
||||
<div className="flex items-center gap-2 text-xs text-krona-secondary">
|
||||
<span className="px-2.5 py-1 rounded-md bg-krona-workspace border border-krona-border">
|
||||
Всего: <strong className="text-krona-primary">{installations.length}</strong>
|
||||
</span>
|
||||
<span className="px-2.5 py-1 rounded-md bg-amber-500/10 border border-amber-500/30 text-amber-500 font-semibold">
|
||||
Найдено: <strong>{filtered.length}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Outdated Android Callout (Сценарий 4) */}
|
||||
{/* Outdated Android Callout */}
|
||||
{outdatedAndroidList.length > 0 && (
|
||||
<div className="rounded-xl border border-amber-500/40 bg-amber-950/20 p-4 flex flex-col md:flex-row items-start md:items-center justify-between gap-3 shadow-lg">
|
||||
<div className="rounded-xl border border-amber-500/40 bg-amber-500/10 p-4 flex flex-col md:flex-row items-start md:items-center justify-between gap-3 shadow-xs">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-amber-500/20 text-amber-400 border border-amber-500/30 shrink-0 mt-0.5">
|
||||
<div className="p-2 rounded-lg bg-amber-500/20 text-amber-500 border border-amber-500/30 shrink-0 mt-0.5">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold text-amber-300">
|
||||
<div className="text-sm font-bold text-amber-600 dark:text-amber-300">
|
||||
Обнаружены устаревшие Android-установки ({outdatedAndroidList.length} устройств)
|
||||
</div>
|
||||
<div className="text-xs text-amber-300/80 mt-0.5">
|
||||
<div className="text-xs text-krona-secondary mt-0.5 leading-relaxed">
|
||||
Терминалы сбора данных Zebra, Honeywell и кассы Эвотор работают на устаревших версиях клиента (v1.0.4 – v1.2.0).
|
||||
Рекомендуется запланировать push-обновление APK пакета через MDM канал.
|
||||
</div>
|
||||
|
|
@ -87,7 +95,7 @@ export const InstallationsPage: React.FC = () => {
|
|||
setPlatformFilter('android');
|
||||
setStatusFilter('outdated');
|
||||
}}
|
||||
className="shrink-0 px-3 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold text-xs transition-colors"
|
||||
className="shrink-0 min-h-[44px] px-4 py-2 rounded-lg bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold text-xs transition-colors shadow-xs"
|
||||
>
|
||||
Показать только устаревшие Android
|
||||
</button>
|
||||
|
|
@ -98,20 +106,20 @@ export const InstallationsPage: React.FC = () => {
|
|||
<div className="flex flex-col md:flex-row gap-3 items-stretch md:items-center justify-between">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="w-4 h-4 text-zinc-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<Search className="w-4 h-4 text-krona-secondary absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Поиск по устройству, организации или версии..."
|
||||
className="w-full bg-[#0b1d16] border border-zinc-800 rounded-lg pl-9 pr-3 py-2 text-xs text-zinc-100 placeholder-zinc-500 focus:outline-hidden focus:border-amber-500 transition-colors"
|
||||
className="w-full bg-krona-workspace border border-krona-border rounded-lg pl-9 pr-3 py-2.5 text-xs text-krona-primary placeholder:text-krona-secondary/60 focus:outline-hidden focus:border-amber-500 transition-colors min-h-[44px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Platform & Status Filters */}
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-zinc-500 text-[11px] mr-1">Платформа:</span>
|
||||
<div className="flex items-center gap-1 overflow-x-auto pb-1">
|
||||
<span className="text-krona-secondary text-[11px] mr-1 hidden sm:inline">Платформа:</span>
|
||||
{[
|
||||
{ id: 'all', label: 'Все' },
|
||||
{ id: 'android', label: 'Android' },
|
||||
|
|
@ -121,10 +129,10 @@ export const InstallationsPage: React.FC = () => {
|
|||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setPlatformFilter(tab.id as typeof platformFilter)}
|
||||
className={`px-2.5 py-1.5 rounded-lg font-medium transition-colors ${
|
||||
className={`min-h-[44px] px-3 py-2 rounded-lg font-medium transition-colors ${
|
||||
platformFilter === tab.id
|
||||
? 'bg-amber-500/20 text-amber-300 border border-amber-500/40 font-bold'
|
||||
: 'bg-zinc-900 text-zinc-400 border border-zinc-800 hover:text-zinc-200'
|
||||
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border border-amber-500/40 font-bold shadow-xs'
|
||||
: 'bg-krona-workspace text-krona-secondary border border-krona-border hover:text-krona-primary'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
|
|
@ -132,8 +140,8 @@ export const InstallationsPage: React.FC = () => {
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 pl-2 border-l border-zinc-800">
|
||||
<span className="text-zinc-500 text-[11px] mr-1">Статус:</span>
|
||||
<div className="flex items-center gap-1 pl-2 border-l border-krona-border overflow-x-auto pb-1">
|
||||
<span className="text-krona-secondary text-[11px] mr-1 hidden sm:inline">Статус:</span>
|
||||
{[
|
||||
{ id: 'all', label: 'Все' },
|
||||
{ id: 'outdated', label: 'Устарели' },
|
||||
|
|
@ -143,10 +151,10 @@ export const InstallationsPage: React.FC = () => {
|
|||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setStatusFilter(tab.id as typeof statusFilter)}
|
||||
className={`px-2.5 py-1.5 rounded-lg font-medium transition-colors ${
|
||||
className={`min-h-[44px] px-3 py-2 rounded-lg font-medium transition-colors ${
|
||||
statusFilter === tab.id
|
||||
? 'bg-amber-500/20 text-amber-300 border border-amber-500/40 font-bold'
|
||||
: 'bg-zinc-900 text-zinc-400 border border-zinc-800 hover:text-zinc-200'
|
||||
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border border-amber-500/40 font-bold shadow-xs'
|
||||
: 'bg-krona-workspace text-krona-secondary border border-krona-border hover:text-krona-primary'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
|
|
@ -156,15 +164,70 @@ export const InstallationsPage: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Installations Table */}
|
||||
<div className="rounded-xl border border-zinc-800 bg-[#091a13] overflow-hidden shadow-xl">
|
||||
{/* Mobile Cards View (block lg:hidden) */}
|
||||
<div className="block lg:hidden space-y-3">
|
||||
{filtered.map((item) => {
|
||||
const platformLabel = RU_PLATFORMS[item.kind] || item.kind;
|
||||
const statusLabel = RU_INSTALLATION_STATUS[item.status] || item.status;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.installationId}
|
||||
className="p-4 rounded-xl border border-krona-border bg-krona-surface space-y-3 shadow-xs"
|
||||
>
|
||||
<div className="flex justify-between items-start gap-2 border-b border-krona-border pb-2.5">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
{getPlatformIcon(item.kind)}
|
||||
<span className="font-bold text-sm text-krona-primary">{item.deviceName}</span>
|
||||
</div>
|
||||
<div className="text-xs text-krona-secondary mt-0.5">
|
||||
{item.tenantName}
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={item.status} size="sm" customLabel={statusLabel} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="p-2 rounded-lg bg-krona-workspace border border-krona-border">
|
||||
<span className="text-[10px] text-krona-secondary block">Версия клиента</span>
|
||||
<span className="font-mono font-bold text-amber-500">v{item.appVersion}</span>
|
||||
</div>
|
||||
<div className="p-2 rounded-lg bg-krona-workspace border border-krona-border">
|
||||
<span className="text-[10px] text-krona-secondary block">ОС устройства</span>
|
||||
<span className="font-mono text-krona-primary truncate block">{item.osVersion}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-krona-border flex items-center justify-between text-[11px] text-krona-secondary">
|
||||
<span className="flex items-center gap-1 font-mono">
|
||||
<Radio className="w-3 h-3 text-emerald-500" />
|
||||
{item.ipAddressMasked}
|
||||
</span>
|
||||
<span>
|
||||
Активность: {new Date(item.lastSeenAt).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<div className="p-8 rounded-xl border border-krona-border bg-krona-surface text-center text-xs text-krona-secondary">
|
||||
Установки не найдены по выбранным критериям
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop Installations Table (hidden lg:block) */}
|
||||
<div className="hidden lg:block rounded-xl border border-krona-border bg-krona-surface overflow-hidden shadow-xs">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-800 bg-zinc-950/70 text-zinc-400 font-semibold uppercase tracking-wider text-[11px]">
|
||||
<tr className="border-b border-krona-border bg-krona-workspace text-krona-secondary font-semibold uppercase tracking-wider text-[11px]">
|
||||
<th className="py-3 px-4">Платформа</th>
|
||||
<th className="py-3 px-3">Устройство / Модель</th>
|
||||
<th className="py-3 px-3">Организация (Tenant)</th>
|
||||
<th className="py-3 px-3">Организация</th>
|
||||
<th className="py-3 px-3 font-mono">Версия клиента</th>
|
||||
<th className="py-3 px-3">ОС устройства</th>
|
||||
<th className="py-3 px-3">Статус поддержки</th>
|
||||
|
|
@ -172,23 +235,27 @@ export const InstallationsPage: React.FC = () => {
|
|||
<th className="py-3 px-4">Сегмент IP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-800/60">
|
||||
<tbody className="divide-y divide-krona-border/60">
|
||||
{filtered.map((item) => (
|
||||
<tr key={item.installationId} className="hover:bg-zinc-800/40 transition-colors">
|
||||
<tr key={item.installationId} className="hover:bg-krona-workspace/50 transition-colors">
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-2 font-medium capitalize text-zinc-200">
|
||||
<div className="flex items-center gap-2 font-medium text-krona-primary">
|
||||
{getPlatformIcon(item.kind)}
|
||||
<span>{item.kind}</span>
|
||||
<span>{RU_PLATFORMS[item.kind] || item.kind}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-3 font-semibold text-zinc-100">{item.deviceName}</td>
|
||||
<td className="py-3 px-3 text-zinc-300 font-medium">{item.tenantName}</td>
|
||||
<td className="py-3 px-3 font-mono font-bold text-amber-300">v{item.appVersion}</td>
|
||||
<td className="py-3 px-3 text-zinc-400 font-mono text-[11px]">{item.osVersion}</td>
|
||||
<td className="py-3 px-3 font-semibold text-krona-primary">{item.deviceName}</td>
|
||||
<td className="py-3 px-3 text-krona-secondary font-medium">{item.tenantName}</td>
|
||||
<td className="py-3 px-3 font-mono font-bold text-amber-500">v{item.appVersion}</td>
|
||||
<td className="py-3 px-3 text-krona-secondary font-mono text-[11px]">{item.osVersion}</td>
|
||||
<td className="py-3 px-3">
|
||||
<StatusBadge status={item.status} size="sm" />
|
||||
<StatusBadge
|
||||
status={item.status}
|
||||
size="sm"
|
||||
customLabel={RU_INSTALLATION_STATUS[item.status] || item.status}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-3 px-3 text-zinc-400 text-[11px]">
|
||||
<td className="py-3 px-3 text-krona-secondary text-[11px]">
|
||||
{new Date(item.lastSeenAt).toLocaleString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
|
|
@ -196,13 +263,13 @@ export const InstallationsPage: React.FC = () => {
|
|||
minute: '2-digit',
|
||||
})}
|
||||
</td>
|
||||
<td className="py-3 px-4 font-mono text-zinc-500 text-[11px]">{item.ipAddressMasked}</td>
|
||||
<td className="py-3 px-4 font-mono text-krona-secondary text-[11px]">{item.ipAddressMasked}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={8} className="py-8 text-center text-zinc-500">
|
||||
<td colSpan={8} className="py-8 text-center text-krona-secondary">
|
||||
Установки не найдены по выбранным критериям фильтрации
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
Loading…
Reference in a new issue