ui(super-admin): SaaS-метрики на главной системной консоли (placeholders)

Food Market — SaaS для розницы, SuperAdmin это владелец платформы,
а не сотрудник магазина. Операционные метрики магазинов («Товаров
29540», «Приёмок за месяц») для него бесполезны — это для tenant
dashboard'а конкретной орги.

KPI блок на /super-admin переработан под кабинет SaaS-провайдера:

Top row (4 карточки):
- Организаций (как было — клиентская база)
- Платящих клиентов — placeholder, accent emerald, muted
- MRR (₸ / мес) — placeholder, accent violet, muted
- Должники — placeholder, accent rose, muted

Second row (2 карточки):
- Пользователей (всего/активных)
- Регистраций за 30 дней — реальное значение
  (COUNT Organizations WHERE CreatedAt >= now-30d)

Заглушки получили проп muted=true: фон чуть серее (slate-50/60),
значение «—» более бледным slate-400, иконка остаётся полноцветной
чтобы было видно «здесь будут данные после Phase 4». В hint —
«Скоро · после внедрения биллинга».

API: DashboardStats потерял TotalProducts/TotalSuppliesThisMonth,
получил RegistrationsLast30Days.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-26 15:47:13 +05:00
parent 7c161a138b
commit 21c2ca89fe
2 changed files with 37 additions and 12 deletions

View file

@ -29,11 +29,14 @@ public async Task<ActionResult<SetupStatusDto>> GetSetupStatus(CancellationToken
public record DashboardStats(
int TotalOrgs, int ActiveOrgs, int ArchivedOrgs,
int TotalUsers, int ActiveUsers,
int TotalProducts, int TotalSuppliesThisMonth);
int RegistrationsLast30Days);
[HttpGet("dashboard")]
public async Task<ActionResult<DashboardStats>> Dashboard(CancellationToken ct)
{
// Метрики SuperAdmin'а — кабинет SaaS-владельца, не операционные
// показатели магазинов. Биллинговые KPI (MRR, должники, платящие)
// считаем на UI как заглушки — отдельный модуль подписки в Phase 4+.
var monthAgo = DateTime.UtcNow.AddDays(-30);
return new DashboardStats(
TotalOrgs: await _db.Organizations.IgnoreQueryFilters().CountAsync(ct),
@ -41,8 +44,7 @@ public async Task<ActionResult<DashboardStats>> Dashboard(CancellationToken ct)
ArchivedOrgs: await _db.Organizations.IgnoreQueryFilters().CountAsync(o => o.IsArchived, ct),
TotalUsers: await _db.Users.CountAsync(ct),
ActiveUsers: await _db.Users.CountAsync(u => u.IsActive, ct),
TotalProducts: await _db.Products.IgnoreQueryFilters().CountAsync(ct),
TotalSuppliesThisMonth: await _db.Supplies.IgnoreQueryFilters().CountAsync(s => s.Date >= monthAgo, ct));
RegistrationsLast30Days: await _db.Organizations.IgnoreQueryFilters().CountAsync(o => o.CreatedAt >= monthAgo, ct));
}
public record AuditRow(

View file

@ -1,15 +1,17 @@
import { useQuery } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import {
Building2, Users, Package, ShoppingCart, FileClock,
Building2, Users, FileClock,
CheckCircle2, AlertCircle, Activity, Inbox, ShieldCheck,
CreditCard, TrendingUp, AlertTriangle, UserPlus,
} from 'lucide-react'
import { api } from '@/lib/api'
import { cn } from '@/lib/utils'
interface DashboardStats {
totalOrgs: number; activeOrgs: number; archivedOrgs: number
totalUsers: number; activeUsers: number
totalProducts: number; totalSuppliesThisMonth: number
registrationsLast30Days: number
}
interface AuditRow {
@ -19,26 +21,36 @@ interface AuditRow {
const fmt = new Intl.NumberFormat('ru')
function Kpi({ icon: Icon, label, value, hint, accent = 'indigo' }: {
function Kpi({ icon: Icon, label, value, hint, accent = 'indigo', muted = false }: {
icon: React.ComponentType<{ className?: string }>
label: string; value: string | number; hint?: string
accent?: 'indigo' | 'emerald' | 'amber' | 'sky'
accent?: 'indigo' | 'emerald' | 'amber' | 'sky' | 'rose' | 'violet'
/** «Скоро» заглушка под будущий биллинг. Тонировка фона сильнее,
* но иконка остаётся цветной чтобы было ясно «здесь будет данные». */
muted?: boolean
}) {
const accents: Record<string, string> = {
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',
rose: 'bg-rose-100 text-rose-600 dark:bg-rose-900/40 dark:text-rose-300',
violet: 'bg-violet-100 text-violet-600 dark:bg-violet-900/40 dark:text-violet-300',
}
return (
<div className="rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-4 transition hover:shadow-sm hover:border-slate-300 dark:hover:border-slate-700">
<div className={cn(
'rounded-xl border bg-white dark:bg-slate-900 p-4 transition hover:shadow-sm',
muted
? 'border-slate-200/70 dark:border-slate-800/70 bg-slate-50/60 dark:bg-slate-900/60'
: 'border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700',
)}>
<div className="flex items-start gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${accents[accent]}`}>
<Icon className="w-5 h-5" />
</div>
<div className="min-w-0">
<div className="text-[11px] uppercase tracking-wide text-slate-500">{label}</div>
<div className="text-2xl font-bold mt-0.5 leading-tight">{value}</div>
<div className={cn('text-2xl font-bold mt-0.5 leading-tight', muted && 'text-slate-400')}>{value}</div>
{hint && <div className="text-xs text-slate-400 mt-0.5">{hint}</div>}
</div>
</div>
@ -98,14 +110,25 @@ export function SuperAdminDashboardPage() {
</div>
</section>
{/* KPI cards */}
{/* KPI: SaaS-владелец платформы. Главный ряд клиентская база
* и биллинг (последние 3 заглушки до Phase 4 «Подписки»).
* Второй ряд инфраструктура учёток. Операционные метрики
* магазинов (товары/приёмки) выкинуты это для tenant-дашборда. */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 sm:gap-4">
<Kpi icon={Building2} label="Организаций" value={fmt.format(data?.totalOrgs ?? 0)}
hint={`${data?.activeOrgs ?? 0} активных, ${data?.archivedOrgs ?? 0} в архиве`} accent="indigo" />
<Kpi icon={CreditCard} label="Платящих клиентов" value="—"
hint="Скоро · после внедрения биллинга" accent="emerald" muted />
<Kpi icon={TrendingUp} label="MRR (₸ / мес)" value="—"
hint="Скоро · после внедрения биллинга" accent="violet" muted />
<Kpi icon={AlertTriangle} label="Должники" value="—"
hint="Скоро · после внедрения биллинга" accent="rose" muted />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 sm:gap-4">
<Kpi icon={Users} label="Пользователей" value={fmt.format(data?.totalUsers ?? 0)}
hint={`${data?.activeUsers ?? 0} активных`} accent="sky" />
<Kpi icon={Package} label="Товаров (всего)" value={fmt.format(data?.totalProducts ?? 0)} accent="emerald" />
<Kpi icon={ShoppingCart} label="Приёмок за месяц" value={fmt.format(data?.totalSuppliesThisMonth ?? 0)} accent="amber" />
<Kpi icon={UserPlus} label="Регистраций за 30 дней" value={fmt.format(data?.registrationsLast30Days ?? 0)}
hint="Новые организации" accent="amber" />
</div>
{/* Three side-by-side blocks */}