feat(control-center): adapt AnnouncementsPage and IncidentsPage mobile-first styling

This commit is contained in:
ochenstarik 2026-09-11 09:38:37 +07:00
parent b81206a2bb
commit b8739a5096
2 changed files with 231 additions and 137 deletions

View file

@ -3,9 +3,13 @@ import {
BellRing,
Send,
Plus,
X,
Users,
CheckCircle2,
} from 'lucide-react';
import { useKccStore, useOperatorRole } from '../store/kccStore';
import { StatusBadge } from '../components/StatusBadge';
import { RU_ANNOUNCEMENT_PRIORITY } from '../locales/ru';
export const AnnouncementsPage: React.FC = () => {
const announcements = useKccStore((state) => state.announcements);
@ -74,15 +78,15 @@ export const AnnouncementsPage: React.FC = () => {
};
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">
<BellRing 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">
<BellRing 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">
Таргетированные сервисные рассылки, уведомления об обновлениях модулей и плановом ТО
</p>
</div>
@ -91,102 +95,105 @@ export const AnnouncementsPage: React.FC = () => {
type="button"
disabled={!permissions.canCreateAnnouncements}
onClick={() => setIsCreating(!isCreating)}
className="px-3 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold text-xs flex items-center gap-1.5 transition-colors disabled:opacity-40"
className="min-h-[44px] px-4 py-2.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold text-xs flex items-center justify-center gap-2 transition-colors disabled:opacity-40 shadow-xs shrink-0"
>
<Plus className="w-4 h-4" />
<span>{isCreating ? 'Закрыть редактор' : 'Создать объявление'}</span>
{isCreating ? <X className="w-4 h-4" /> : <Plus className="w-4 h-4" />}
<span>{isCreating ? 'Закрыть редактор' : 'Создать оповещение'}</span>
</button>
</div>
{/* New Announcement Form (Сценарий 7) */}
{/* New Announcement Form (Modal or Expandable) */}
{isCreating && (
<form
onSubmit={handleSendAnnouncement}
className="rounded-xl border border-amber-500/40 bg-[#0a1c15] p-5 space-y-4 shadow-xl animate-in fade-in slide-in-from-top-2"
className="rounded-xl border border-amber-500/40 bg-krona-surface p-4 md:p-5 space-y-4 shadow-lg animate-in fade-in slide-in-from-top-2"
>
<div className="flex justify-between items-center border-b border-zinc-800 pb-3">
<div className="flex items-center gap-2 text-sm font-bold text-zinc-100">
<Send className="w-4 h-4 text-amber-400" />
<div className="flex justify-between items-center border-b border-krona-border pb-3">
<div className="flex items-center gap-2 text-sm font-bold text-krona-primary">
<Send className="w-4 h-4 text-amber-500" />
<span>Конструктор операторского оповещения</span>
</div>
<span className="text-xs text-amber-300/80 font-mono">DEMO: без внешней отправки SMS/Email</span>
<span className="text-[11px] text-amber-600 dark:text-amber-300 font-mono">
ДЕМО: In-App доставка
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
{/* Title */}
<div className="space-y-1.5">
<label className="font-semibold text-zinc-300">Заголовок оповещения:</label>
<label className="font-semibold text-krona-primary">Заголовок оповещения:</label>
<input
type="text"
required
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Например: Доступен модуль Omnichannel 3.0"
className="w-full bg-zinc-950 border border-zinc-700 rounded-lg p-2.5 text-xs text-zinc-100 placeholder-zinc-500 focus:outline-hidden focus:border-amber-500"
placeholder="Например: Доступно обновление платформы v3.0"
className="w-full bg-krona-workspace border border-krona-border rounded-lg px-3 py-2.5 text-xs text-krona-primary placeholder:text-krona-secondary/60 focus:outline-hidden focus:border-amber-500 min-h-[44px]"
/>
</div>
{/* Priority */}
<div className="space-y-1.5">
<label className="font-semibold text-zinc-300">Приоритет доставки:</label>
<label className="font-semibold text-krona-primary">Приоритет доставки:</label>
<select
value={priority}
onChange={(e) => setPriority(e.target.value as typeof priority)}
className="w-full bg-zinc-950 border border-zinc-700 rounded-lg p-2.5 text-xs text-zinc-100 focus:outline-hidden"
className="w-full bg-krona-workspace border border-krona-border rounded-lg px-3 py-2.5 text-xs text-krona-primary focus:outline-hidden min-h-[44px]"
>
<option value="normal">Обычный (Информационный баннер)</option>
<option value="important">Важный (Уведомление в шапке)</option>
<option value="critical">Критический (Блокирующий модальный диалог)</option>
<option value="critical">Критический (Блокирующий диалог)</option>
</select>
</div>
</div>
{/* Audience selection */}
<div className="space-y-2 text-xs">
<label className="font-semibold text-zinc-300">Целевая аудитория тенантов:</label>
<div className="grid grid-cols-3 gap-2">
<label className="font-semibold text-krona-primary">Целевая аудитория организаций:</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<button
type="button"
onClick={() => setAudienceType('all')}
className={`p-2 rounded-lg border text-xs font-medium transition-colors ${
className={`min-h-[44px] p-2.5 rounded-lg border text-xs font-medium transition-colors flex items-center justify-center gap-1.5 ${
audienceType === 'all'
? 'bg-amber-500/20 text-amber-300 border-amber-500/50 font-bold'
: 'bg-zinc-950 text-zinc-400 border-zinc-800'
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border-amber-500/50 font-bold'
: 'bg-krona-workspace text-krona-secondary border-krona-border hover:text-krona-primary'
}`}
>
Все организации ({organizations.length})
<Users className="w-3.5 h-3.5" />
<span>Все ({organizations.length})</span>
</button>
<button
type="button"
onClick={() => setAudienceType('module')}
className={`p-2 rounded-lg border text-xs font-medium transition-colors ${
className={`min-h-[44px] p-2.5 rounded-lg border text-xs font-medium transition-colors flex items-center justify-center gap-1.5 ${
audienceType === 'module'
? 'bg-amber-500/20 text-amber-300 border-amber-500/50 font-bold'
: 'bg-zinc-950 text-zinc-400 border-zinc-800'
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border-amber-500/50 font-bold'
: 'bg-krona-workspace text-krona-secondary border-krona-border hover:text-krona-primary'
}`}
>
По установленному модулю
<span>По модулю</span>
</button>
<button
type="button"
onClick={() => setAudienceType('specific')}
className={`p-2 rounded-lg border text-xs font-medium transition-colors ${
className={`min-h-[44px] p-2.5 rounded-lg border text-xs font-medium transition-colors flex items-center justify-center gap-1.5 ${
audienceType === 'specific'
? 'bg-amber-500/20 text-amber-300 border-amber-500/50 font-bold'
: 'bg-zinc-950 text-zinc-400 border-zinc-800'
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border-amber-500/50 font-bold'
: 'bg-krona-workspace text-krona-secondary border-krona-border hover:text-krona-primary'
}`}
>
Выборочные организации ({selectedTenants.length})
<span>Выборочные ({selectedTenants.length})</span>
</button>
</div>
{audienceType === 'module' && (
<div className="pt-2 flex items-center gap-2">
<span className="text-zinc-400 text-xs">Выберите модуль:</span>
<div className="pt-2 flex flex-col sm:flex-row sm:items-center gap-2">
<span className="text-krona-secondary text-xs">Выберите модуль:</span>
<select
value={selectedModuleId}
onChange={(e) => setSelectedModuleId(e.target.value)}
className="bg-zinc-900 border border-zinc-700 rounded-md px-2.5 py-1 text-xs text-zinc-200"
className="bg-krona-workspace border border-krona-border rounded-lg px-3 py-2 text-xs text-krona-primary min-h-[44px]"
>
{modules.map((m) => (
<option key={m.moduleId} value={m.moduleId}>
@ -200,73 +207,94 @@ export const AnnouncementsPage: React.FC = () => {
{/* Body */}
<div className="space-y-1.5 text-xs">
<label className="font-semibold text-zinc-300">Текст сообщения:</label>
<label className="font-semibold text-krona-primary">Текст сообщения:</label>
<textarea
required
rows={3}
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="Введите текст объявления для операторов и пользователей организаций..."
className="w-full bg-zinc-950 border border-zinc-700 rounded-lg p-2.5 text-xs text-zinc-100 placeholder-zinc-500 focus:outline-hidden focus:border-amber-500 resize-none"
className="w-full bg-krona-workspace border border-krona-border rounded-lg p-3 text-xs text-krona-primary placeholder:text-krona-secondary/60 focus:outline-hidden focus:border-amber-500 resize-none"
/>
</div>
{/* Actions */}
<div className="flex justify-end gap-2 pt-2 border-t border-zinc-800">
<div className="flex flex-col-reverse sm:flex-row justify-end gap-2 pt-2 border-t border-krona-border">
<button
type="button"
onClick={() => setIsCreating(false)}
className="px-3 py-1.5 rounded-lg border border-zinc-700 text-zinc-400 hover:text-zinc-200 text-xs font-medium"
className="min-h-[44px] px-4 py-2.5 rounded-lg border border-krona-border text-krona-secondary hover:text-krona-primary text-xs font-medium transition-colors"
>
Отмена
</button>
<button
type="submit"
className="px-4 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold text-xs flex items-center gap-1.5 shadow-md shadow-amber-900/30"
className="min-h-[44px] px-5 py-2.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-zinc-950 font-bold text-xs flex items-center justify-center gap-2 shadow-xs"
>
<Send className="w-3.5 h-3.5" />
<span>Опубликовать (Reason + Audit)</span>
<Send className="w-4 h-4" />
<span>Опубликовать с подтверждением</span>
</button>
</div>
</form>
)}
{/* Announcements List */}
{/* Announcements List - Mobile-First Cards */}
<div className="space-y-3">
{announcements.map((anc) => (
<div key={anc.announcementId} className="rounded-xl border border-zinc-800 bg-[#091a13] p-5 space-y-3 shadow-lg">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-zinc-850 pb-3">
{announcements.map((anc) => {
const priorityLabel = RU_ANNOUNCEMENT_PRIORITY[anc.priority] || anc.priority;
const isCritical = anc.priority === 'critical';
const isImportant = anc.priority === 'important';
return (
<div
key={anc.announcementId}
className="rounded-xl border border-krona-border bg-krona-surface p-4 md:p-5 space-y-3 shadow-xs"
>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-krona-border pb-3">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="font-bold text-sm text-zinc-100">{anc.title}</span>
<StatusBadge status={anc.status === 'completed' ? 'healthy' : 'warning'} size="sm" customLabel={anc.status.toUpperCase()} />
<span className="text-[10px] uppercase font-bold text-amber-400 px-1.5 py-0.5 rounded-xs bg-amber-500/10 border border-amber-500/30">
{anc.priority}
<div className="flex flex-wrap items-center gap-2 mb-1">
<span className="font-bold text-sm text-krona-primary">{anc.title}</span>
<StatusBadge
status={anc.status === 'completed' ? 'healthy' : 'warning'}
size="sm"
customLabel={anc.status === 'completed' ? 'Доставлено' : 'Отправка'}
/>
<span
className={`text-[10px] font-bold px-2 py-0.5 rounded-md border ${
isCritical
? 'bg-rose-500/15 text-rose-600 dark:text-rose-300 border-rose-500/40'
: isImportant
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border-amber-500/40'
: 'bg-krona-workspace text-krona-secondary border-krona-border'
}`}
>
{priorityLabel}
</span>
</div>
<div className="text-[11px] text-zinc-400">
Автор: <span className="text-zinc-200">{anc.author}</span> • Опубликовано:{' '}
{new Date(anc.createdAt).toLocaleString('ru-RU')}
<div className="text-[11px] text-krona-secondary">
Автор: <span className="text-krona-primary font-medium">{anc.author}</span> • Опубликовано:{' '}
{new Date(anc.createdAt).toLocaleDateString('ru-RU')}
</div>
</div>
{/* Delivery statistics */}
<div className="flex items-center gap-3 text-xs shrink-0 font-mono">
<div className="text-right">
<div className="text-[10px] text-zinc-500">Доставлено</div>
<div className="font-bold text-emerald-400">{anc.stats.delivered} / {anc.stats.totalTargeted}</div>
<div className="text-left sm:text-right">
<div className="text-[10px] text-krona-secondary">Доставлено</div>
<div className="font-bold text-emerald-600 dark:text-emerald-400 flex items-center gap-1">
<CheckCircle2 className="w-3 h-3 inline" />
<span>{anc.stats.delivered} / {anc.stats.totalTargeted}</span>
</div>
<div className="text-right">
<div className="text-[10px] text-zinc-500">Каналы</div>
<div className="text-zinc-300 font-semibold">{anc.channels.join(', ')}</div>
</div>
</div>
</div>
<p className="text-xs text-zinc-300 leading-relaxed">{anc.body}</p>
<p className="text-xs text-krona-secondary leading-relaxed">{anc.body}</p>
</div>
))}
);
})}
</div>
</div>
);
};

View file

@ -3,16 +3,20 @@ import {
AlertTriangle,
Search,
ChevronRight,
ShieldAlert,
Server,
Building2,
Package,
} from 'lucide-react';
import { useKccStore } from '../store/kccStore';
import { StatusBadge } from '../components/StatusBadge';
import { Incident } from '../contracts';
import { RU_INCIDENT_SEVERITY, RU_INCIDENT_STATUS } from '../locales/ru';
export const IncidentsPage: React.FC = () => {
const incidents = useKccStore((state) => state.incidents);
const openInspector = useKccStore((state) => state.openInspector);
const [statusFilter, setStatusFilter] = useState<'all' | Incident['status']>('all');
const [statusFilter, setStatusFilter] = useState<'all' | 'open' | 'mitigating' | 'resolved'>('all');
const [search, setSearch] = useState('');
const filtered = incidents.filter((inc) => {
@ -21,57 +25,81 @@ export const IncidentsPage: React.FC = () => {
inc.incidentNumber.toLowerCase().includes(search.toLowerCase()) ||
inc.summary.toLowerCase().includes(search.toLowerCase()) ||
inc.description.toLowerCase().includes(search.toLowerCase());
const matchStatus = statusFilter === 'all' || inc.status === statusFilter;
let matchStatus = true;
if (statusFilter === 'open') {
matchStatus = inc.status === 'investigating';
} else if (statusFilter === 'mitigating') {
matchStatus = inc.status === 'identified' || inc.status === 'monitoring';
} else if (statusFilter === 'resolved') {
matchStatus = inc.status === 'resolved';
}
return matchSearch && matchStatus;
});
const unresolvedCount = incidents.filter((i) => i.status !== 'resolved').length;
const criticalCount = incidents.filter(
(i) => i.status !== 'resolved' && (i.severity === 'P0_CRITICAL' || i.severity === 'P1_MAJOR')
).length;
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">
<AlertTriangle className="w-5 h-5 text-rose-400" />
<span>Инциденты платформы (Incidents Desk)</span>
<h1 className="text-lg md:text-xl font-bold text-krona-primary flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-rose-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">
Оперативное реагирование на сбои серверных узлов, деградацию модулей и инфраструктурные аварии
</p>
</div>
<div className="text-xs text-zinc-400">
Всего инцидентов: <span className="font-bold text-zinc-100">{incidents.length}</span> (открыто:{' '}
<span className="font-bold text-rose-400">{incidents.filter((i) => i.status !== 'resolved').length}</span>)
<div className="flex flex-wrap 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">{incidents.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>{unresolvedCount}</strong>
</span>
{criticalCount > 0 && (
<span className="px-2.5 py-1 rounded-md bg-rose-500/10 border border-rose-500/30 text-rose-500 font-bold">
Критических: {criticalCount}
</span>
)}
</div>
</div>
{/* Filter and Search Bar */}
<div className="flex flex-col md:flex-row gap-3 items-stretch md:items-center justify-between">
<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>
<div className="flex items-center gap-1 overflow-x-auto pb-1 text-xs">
{/* Status Filter Tabs with min 44px touch targets */}
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 text-xs -mx-1 px-1">
{[
{ id: 'all', label: 'Все инциденты' },
{ id: 'open', label: 'Открытые' },
{ id: 'mitigating', label: 'В процессе митигации' },
{ id: 'resolved', label: 'Разрешенные' },
{ id: 'mitigating', label: 'В процессе' },
{ id: 'resolved', label: 'Устранённые' },
].map((tab) => (
<button
key={tab.id}
onClick={() => setStatusFilter(tab.id as typeof statusFilter)}
className={`px-3 py-1.5 rounded-lg font-medium transition-colors ${
className={`min-h-[44px] px-3.5 py-2 rounded-lg font-medium transition-colors whitespace-nowrap ${
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}
@ -80,58 +108,96 @@ export const IncidentsPage: React.FC = () => {
</div>
</div>
{/* Incidents List */}
{/* Incidents List - Mobile-First Cards */}
<div className="space-y-3">
{filtered.map((inc) => (
{filtered.map((inc) => {
const isCritical = inc.severity === 'P0_CRITICAL';
const isMajor = inc.severity === 'P1_MAJOR';
const severityInfo = RU_INCIDENT_SEVERITY[inc.severity] || {
label: inc.severity,
short: inc.severity,
};
const statusText = RU_INCIDENT_STATUS[inc.status] || inc.status;
return (
<div
key={inc.incidentId}
onClick={() => openInspector('incident', inc.incidentId)}
className="p-5 rounded-xl border border-zinc-800 bg-[#091a13] hover:border-zinc-700 cursor-pointer transition-colors space-y-3 shadow-lg group"
className={`p-4 md:p-5 rounded-xl border bg-krona-surface transition-all cursor-pointer space-y-3 shadow-xs hover:border-amber-500/50 group ${
isCritical
? 'border-rose-500/40 bg-rose-500/5'
: isMajor
? 'border-amber-500/30'
: 'border-krona-border'
}`}
>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-zinc-850 pb-3">
<div className="flex items-center gap-2.5">
<span className="font-mono font-bold text-sm text-zinc-100 group-hover:text-amber-300 transition-colors">
{/* Card Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-krona-border pb-3">
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono font-bold text-sm text-krona-primary group-hover:text-amber-500 transition-colors">
{inc.incidentNumber}
</span>
<StatusBadge status={inc.status} size="sm" />
<span className="text-[10px] uppercase font-extrabold px-2 py-0.5 rounded-xs bg-rose-950/80 text-rose-300 border border-rose-800/60 font-mono">
{inc.severity}
<StatusBadge status={inc.status} size="sm" customLabel={statusText} />
<span
className={`text-[11px] font-bold px-2 py-0.5 rounded-md border font-mono ${
isCritical
? 'bg-rose-500/15 text-rose-600 dark:text-rose-300 border-rose-500/40'
: isMajor
? 'bg-amber-500/15 text-amber-600 dark:text-amber-300 border-amber-500/40'
: 'bg-sky-500/15 text-sky-600 dark:text-sky-300 border-sky-500/40'
}`}
>
{severityInfo.short}
</span>
</div>
<div className="flex items-center gap-3 text-xs text-zinc-400">
<span>Ответственный: <strong className="text-zinc-200">{inc.ownerName}</strong></span>
<div className="flex items-center gap-2 text-xs text-krona-secondary">
<span>Ответственный: <strong className="text-krona-primary font-medium">{inc.ownerName}</strong></span>
<span>•</span>
<span>{new Date(inc.openedAt).toLocaleString('ru-RU')}</span>
<span>{new Date(inc.openedAt).toLocaleDateString('ru-RU')}</span>
</div>
</div>
{/* Summary and Description */}
<div className="space-y-1">
<h3 className="font-bold text-sm text-zinc-100">{inc.summary}</h3>
<p className="text-xs text-zinc-300 leading-relaxed">{inc.description}</p>
<h3 className="font-bold text-sm text-krona-primary">{inc.summary}</h3>
<p className="text-xs text-krona-secondary leading-relaxed line-clamp-2 md:line-clamp-none">
{inc.description}
</p>
</div>
<div className="pt-2 border-t border-zinc-850 flex items-center justify-between text-xs text-zinc-400">
<div className="flex gap-4">
<span>Затронуто тенантов: <strong className="text-zinc-200">{inc.affectedTenantIds.length}</strong></span>
<span>Узлов: <strong className="text-zinc-200">{inc.affectedServerIds.length}</strong></span>
<span>Модулей: <strong className="text-zinc-200">{inc.affectedModuleIds.length}</strong></span>
{/* Card Footer: Impact Metrics & Inspector Trigger */}
<div className="pt-2 border-t border-krona-border flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs text-krona-secondary">
<div className="flex flex-wrap items-center gap-3 md:gap-5">
<span className="inline-flex items-center gap-1">
<Building2 className="w-3.5 h-3.5 text-krona-secondary" />
Организаций: <strong className="text-krona-primary font-medium">{inc.affectedTenantIds.length}</strong>
</span>
<span className="inline-flex items-center gap-1">
<Server className="w-3.5 h-3.5 text-krona-secondary" />
Узлов: <strong className="text-krona-primary font-medium">{inc.affectedServerIds.length}</strong>
</span>
<span className="inline-flex items-center gap-1">
<Package className="w-3.5 h-3.5 text-krona-secondary" />
Модулей: <strong className="text-krona-primary font-medium">{inc.affectedModuleIds.length}</strong>
</span>
</div>
<button
type="button"
className="inline-flex items-center gap-1 text-xs text-amber-400 hover:text-amber-300 font-medium"
>
<div className="inline-flex items-center gap-1 text-xs font-semibold text-amber-500 hover:text-amber-600 transition-colors min-h-[44px]">
<span>Детали расследования</span>
<ChevronRight className="w-3.5 h-3.5" />
</button>
<ChevronRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
</div>
</div>
))}
</div>
);
})}
{filtered.length === 0 && (
<div className="p-8 rounded-xl border border-zinc-800 bg-[#091a13] text-center text-xs text-zinc-500">
Инциденты не найдены
<div className="p-8 rounded-xl border border-krona-border bg-krona-surface text-center space-y-2">
<ShieldAlert className="w-8 h-8 text-krona-secondary mx-auto opacity-50" />
<p className="text-xs text-krona-secondary">
Инциденты не найдены по выбранным критериям фильтрации
</p>
</div>
)}
</div>