ui(super-admin): SaaS-метрики на главной системной консоли (placeholders)
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 47s
CI / Web (React + Vite) (push) Successful in 40s
Docker API / Build + push API (push) Successful in 45s
Docker Web / Build + push Web (push) Successful in 30s
Docker API / Deploy API on stage (push) Successful in 17s
Docker Web / Deploy Web on stage (push) Successful in 12s
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 47s
CI / Web (React + Vite) (push) Successful in 40s
Docker API / Build + push API (push) Successful in 45s
Docker Web / Build + push Web (push) Successful in 30s
Docker API / Deploy API on stage (push) Successful in 17s
Docker Web / Deploy Web on stage (push) Successful in 12s
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:
parent
4dafdc8995
commit
7a21c83d3e
|
|
@ -29,11 +29,14 @@ public async Task<ActionResult<SetupStatusDto>> GetSetupStatus(CancellationToken
|
||||||
public record DashboardStats(
|
public record DashboardStats(
|
||||||
int TotalOrgs, int ActiveOrgs, int ArchivedOrgs,
|
int TotalOrgs, int ActiveOrgs, int ArchivedOrgs,
|
||||||
int TotalUsers, int ActiveUsers,
|
int TotalUsers, int ActiveUsers,
|
||||||
int TotalProducts, int TotalSuppliesThisMonth);
|
int RegistrationsLast30Days);
|
||||||
|
|
||||||
[HttpGet("dashboard")]
|
[HttpGet("dashboard")]
|
||||||
public async Task<ActionResult<DashboardStats>> Dashboard(CancellationToken ct)
|
public async Task<ActionResult<DashboardStats>> Dashboard(CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
// Метрики SuperAdmin'а — кабинет SaaS-владельца, не операционные
|
||||||
|
// показатели магазинов. Биллинговые KPI (MRR, должники, платящие)
|
||||||
|
// считаем на UI как заглушки — отдельный модуль подписки в Phase 4+.
|
||||||
var monthAgo = DateTime.UtcNow.AddDays(-30);
|
var monthAgo = DateTime.UtcNow.AddDays(-30);
|
||||||
return new DashboardStats(
|
return new DashboardStats(
|
||||||
TotalOrgs: await _db.Organizations.IgnoreQueryFilters().CountAsync(ct),
|
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),
|
ArchivedOrgs: await _db.Organizations.IgnoreQueryFilters().CountAsync(o => o.IsArchived, ct),
|
||||||
TotalUsers: await _db.Users.CountAsync(ct),
|
TotalUsers: await _db.Users.CountAsync(ct),
|
||||||
ActiveUsers: await _db.Users.CountAsync(u => u.IsActive, ct),
|
ActiveUsers: await _db.Users.CountAsync(u => u.IsActive, ct),
|
||||||
TotalProducts: await _db.Products.IgnoreQueryFilters().CountAsync(ct),
|
RegistrationsLast30Days: await _db.Organizations.IgnoreQueryFilters().CountAsync(o => o.CreatedAt >= monthAgo, ct));
|
||||||
TotalSuppliesThisMonth: await _db.Supplies.IgnoreQueryFilters().CountAsync(s => s.Date >= monthAgo, ct));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record AuditRow(
|
public record AuditRow(
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
Building2, Users, Package, ShoppingCart, FileClock,
|
Building2, Users, FileClock,
|
||||||
CheckCircle2, AlertCircle, Activity, Inbox, ShieldCheck,
|
CheckCircle2, AlertCircle, Activity, Inbox, ShieldCheck,
|
||||||
|
CreditCard, TrendingUp, AlertTriangle, UserPlus,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface DashboardStats {
|
interface DashboardStats {
|
||||||
totalOrgs: number; activeOrgs: number; archivedOrgs: number
|
totalOrgs: number; activeOrgs: number; archivedOrgs: number
|
||||||
totalUsers: number; activeUsers: number
|
totalUsers: number; activeUsers: number
|
||||||
totalProducts: number; totalSuppliesThisMonth: number
|
registrationsLast30Days: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuditRow {
|
interface AuditRow {
|
||||||
|
|
@ -19,26 +21,36 @@ interface AuditRow {
|
||||||
|
|
||||||
const fmt = new Intl.NumberFormat('ru')
|
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 }>
|
icon: React.ComponentType<{ className?: string }>
|
||||||
label: string; value: string | number; hint?: 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> = {
|
const accents: Record<string, string> = {
|
||||||
indigo: 'bg-indigo-100 text-indigo-600 dark:bg-indigo-900/40 dark:text-indigo-300',
|
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',
|
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',
|
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',
|
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 (
|
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="flex items-start gap-3">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${accents[accent]}`}>
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${accents[accent]}`}>
|
||||||
<Icon className="w-5 h-5" />
|
<Icon className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-[11px] uppercase tracking-wide text-slate-500">{label}</div>
|
<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>}
|
{hint && <div className="text-xs text-slate-400 mt-0.5">{hint}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -98,14 +110,25 @@ export function SuperAdminDashboardPage() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* KPI cards */}
|
{/* KPI: SaaS-владелец платформы. Главный ряд — клиентская база
|
||||||
|
* и биллинг (последние 3 — заглушки до Phase 4 «Подписки»).
|
||||||
|
* Второй ряд — инфраструктура учёток. Операционные метрики
|
||||||
|
* магазинов (товары/приёмки) выкинуты — это для tenant-дашборда. */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 sm:gap-4">
|
<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)}
|
<Kpi icon={Building2} label="Организаций" value={fmt.format(data?.totalOrgs ?? 0)}
|
||||||
hint={`${data?.activeOrgs ?? 0} активных, ${data?.archivedOrgs ?? 0} в архиве`} accent="indigo" />
|
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)}
|
<Kpi icon={Users} label="Пользователей" value={fmt.format(data?.totalUsers ?? 0)}
|
||||||
hint={`${data?.activeUsers ?? 0} активных`} accent="sky" />
|
hint={`${data?.activeUsers ?? 0} активных`} accent="sky" />
|
||||||
<Kpi icon={Package} label="Товаров (всего)" value={fmt.format(data?.totalProducts ?? 0)} accent="emerald" />
|
<Kpi icon={UserPlus} label="Регистраций за 30 дней" value={fmt.format(data?.registrationsLast30Days ?? 0)}
|
||||||
<Kpi icon={ShoppingCart} label="Приёмок за месяц" value={fmt.format(data?.totalSuppliesThisMonth ?? 0)} accent="amber" />
|
hint="Новые организации" accent="amber" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Three side-by-side blocks */}
|
{/* Three side-by-side blocks */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue