From 9d8fd2cb53d435c51132a7266071077e58f9cdb5 Mon Sep 17 00:00:00 2001 From: nns <278048682+nurdotnet@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:32:29 +0500 Subject: [PATCH] =?UTF-8?q?ui(super-admin):=20hero-=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B8=20KPI=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BD=D0=B0=20=D0=B3=D0=BB=D0=B0=D0=B2=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20+=20=D1=82=D1=80=D0=B8=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=B5=D0=B7=D0=BD=D1=8B=D1=85=20=D1=81=D0=B5=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /super-admin переработан: - Hero на indigo-градиенте: badge «Super Admin Console» + крупный H1 «Системная консоль» + подзаголовок + env+build справа. - 4 KPI карточки с цветным круглым фоном иконки (indigo/sky/ emerald/amber), label uppercase tracking, значение крупно, hint мелко серым; hover-shadow. - Три карточки в ряд (lg:3 cols) вместо дублирующих ссылок-CTA: «Здоровье системы» (заглушки ✅/Activity для скорых проверок), «Активные организации» (топ-3 по объёму, ссылка «все»), «Свежие события» (6 последних audit-записей или empty state с Inbox-иконкой и текстом «Журнал пуст. Здесь появятся события»). - Кнопка «Создать организацию» убрана с главной — оставлена только на /super-admin/organizations. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/pages/SuperAdminDashboardPage.tsx | 204 ++++++++++++------ 1 file changed, 141 insertions(+), 63 deletions(-) diff --git a/src/food-market.web/src/pages/SuperAdminDashboardPage.tsx b/src/food-market.web/src/pages/SuperAdminDashboardPage.tsx index 26e4ee1..5d23ff6 100644 --- a/src/food-market.web/src/pages/SuperAdminDashboardPage.tsx +++ b/src/food-market.web/src/pages/SuperAdminDashboardPage.tsx @@ -1,8 +1,10 @@ import { useQuery } from '@tanstack/react-query' import { Link } from 'react-router-dom' -import { Building2, Users, Package, ShoppingCart, FileText, Plus } from 'lucide-react' +import { + Building2, Users, Package, ShoppingCart, FileClock, + CheckCircle2, AlertCircle, Activity, Inbox, ShieldCheck, +} from 'lucide-react' import { api } from '@/lib/api' -import { PageHeader } from '@/components/PageHeader' interface DashboardStats { totalOrgs: number; activeOrgs: number; archivedOrgs: number @@ -17,23 +19,47 @@ interface AuditRow { const fmt = new Intl.NumberFormat('ru') -function Kpi({ icon: Icon, label, value, hint }: { - icon: React.ComponentType<{ className?: string }>; label: string; value: string | number; hint?: string +function Kpi({ icon: Icon, label, value, hint, accent = 'indigo' }: { + icon: React.ComponentType<{ className?: string }> + label: string; value: string | number; hint?: string + accent?: 'indigo' | 'emerald' | 'amber' | 'sky' }) { + const accents: Record = { + indigo: 'bg-indigo-100 text-indigo-600 dark:bg-indigo-900/40 dark:text-indigo-300', + emerald: 'bg-emerald-100 text-emerald-600 dark:bg-emerald-900/40 dark:text-emerald-300', + amber: 'bg-amber-100 text-amber-600 dark:bg-amber-900/40 dark:text-amber-300', + sky: 'bg-sky-100 text-sky-600 dark:bg-sky-900/40 dark:text-sky-300', + } return ( -
-
-
-
{label}
-
{value}
+
+
+
+ +
+
+
{label}
+
{value}
{hint &&
{hint}
}
-
) } +function HealthRow({ label, ok, hint }: { label: string; ok: boolean | 'unknown'; hint?: string }) { + return ( +
  • +
    + {ok === true && } + {ok === false && } + {ok === 'unknown' && } + {label} +
    + {hint && {hint}} +
  • + ) +} + export function SuperAdminDashboardPage() { const { data } = useQuery({ queryKey: ['/api/super-admin/dashboard'], @@ -43,69 +69,121 @@ export function SuperAdminDashboardPage() { queryKey: ['/api/super-admin/audit-log', 'recent'], queryFn: async () => (await api.get<{ items: AuditRow[] }>('/api/super-admin/audit-log?pageSize=10')).data.items, }) + const orgsTop = useQuery({ + queryKey: ['/api/super-admin/organizations', 'top'], + queryFn: async () => (await api.get<{ items: Array<{ id: string; name: string; productCount: number; employeeCount: number; lastLoginAt: string | null }> }>( + '/api/super-admin/organizations?pageSize=3&archived=false')).data.items, + }) + return (
    - + {/* Hero */} +
    +
    +
    +
    + Super Admin Console +
    +

    Системная консоль

    +

    + Управление всеми организациями, аудит действий, состояние системы. + Tenant-данные доступны через «Открыть организацию» в верхней панели. +

    +
    +
    + stage + build {new Date().toISOString().slice(0, 10)} +
    +
    +
    + + {/* KPI cards */}
    + hint={`${data?.activeOrgs ?? 0} активных, ${data?.archivedOrgs ?? 0} в архиве`} accent="indigo" /> - - -
    -
    - -
    - -
    -
    Организации
    -
    Создание, правка, архивирование
    -
    -
    - - -
    - -
    -
    Журнал действий
    -
    Аудит-лог супер-админа
    -
    -
    - -
    -
    - - Создать организацию - + hint={`${data?.activeUsers ?? 0} активных`} accent="sky" /> + +
    -
    -
    -

    Последние события

    - Весь журнал → -
    - {audit.data?.length === 0 ? ( -
    Пока нет записей.
    - ) : ( + {/* Three side-by-side blocks */} +
    + {/* Здоровье — пока заглушки, реальные проверки в следующей итерации */} +
    +

    + Здоровье системы +

      - {audit.data?.map((r) => ( -
    • - {new Date(r.createdAt).toLocaleString('ru')} - {r.actionType} - - {r.organizationName && «{r.organizationName}»} - {r.description} - -
    • - ))} + + + +
    - )} -
    +
    + + {/* Топ организации по объёму */} +
    +
    +

    + Активные организации +

    + все → +
    + {orgsTop.data?.length === 0 ? ( +
    Нет организаций.
    + ) : ( +
      + {orgsTop.data?.map((o) => ( +
    • +
      + {o.name} + + {fmt.format(o.productCount)} товаров + +
      +
      + {fmt.format(o.employeeCount)} сотр. + {o.lastLoginAt && <> · last login {new Date(o.lastLoginAt).toLocaleDateString('ru')}} +
      +
    • + ))} +
    + )} +
    + + {/* Свежие события */} +
    +
    +

    + Свежие события +

    + журнал → +
    + {audit.data?.length === 0 ? ( +
    + + Журнал действий пуст. Здесь появятся события: создание орг, входы как, архивация. +
    + ) : ( +
      + {audit.data?.slice(0, 6).map((r) => ( +
    • +
      + {r.actionType} + {new Date(r.createdAt).toLocaleString('ru', { dateStyle: 'short', timeStyle: 'short' })} +
      +
      + {r.organizationName && «{r.organizationName}»} + {r.description} +
      +
    • + ))} +
    + )} +
    +
    )