krona-front/src/features/control-center/pages/OrganizationsPage.tsx

176 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import {
Building2,
Search,
ChevronRight,
} from 'lucide-react';
import { useKccStore } from '../store/kccStore';
import { StatusBadge } from '../components/StatusBadge';
import { MoneyDisplay } from '../components/MoneyDisplay';
import { PlatformStatus } from '../contracts';
export const OrganizationsPage: React.FC = () => {
const organizations = useKccStore((state) => state.organizations);
const openInspector = useKccStore((state) => state.openInspector);
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | PlatformStatus>('all');
const filtered = organizations.filter((org) => {
const matchSearch =
!search ||
org.displayName.toLowerCase().includes(search.toLowerCase()) ||
org.inn.includes(search) ||
org.domain.toLowerCase().includes(search.toLowerCase());
const matchStatus = statusFilter === 'all' || org.platformStatus === statusFilter;
return matchSearch && matchStatus;
});
return (
<div className="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>
<h1 className="text-xl font-bold text-zinc-100 flex items-center gap-2">
<Building2 className="w-5 h-5 text-amber-400" />
<span>Организации платформы (Tenants)</span>
</h1>
<p className="text-xs text-zinc-400 mt-0.5">
Реестр клиентских организаций, управление статусами, тарифами и 360-анализ тенантов
</p>
</div>
<div className="text-xs text-zinc-400">
Всего организаций: <span className="font-bold text-zinc-100">{organizations.length}</span> (найдено:{' '}
<span className="font-bold text-amber-400">{filtered.length}</span>)
</div>
</div>
{/* Filter and Search Bar */}
<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" />
<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"
/>
</div>
{/* Status Filters */}
<div className="flex items-center gap-1 overflow-x-auto pb-1 text-xs">
{[
{ id: 'all', label: 'Все' },
{ id: 'active', label: 'Active' },
{ id: 'trial', label: 'Trial' },
{ id: 'grace', label: 'Grace' },
{ id: 'suspended', label: 'Suspended' },
].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 ${
statusFilter === tab.id
? 'bg-amber-500/20 text-amber-300 border border-amber-500/40 font-semibold'
: 'bg-zinc-900/60 text-zinc-400 border border-zinc-800 hover:text-zinc-200 hover:bg-zinc-800'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* Organizations Table */}
<div className="rounded-xl border border-zinc-800 bg-[#091a13] overflow-hidden shadow-xl">
<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]">
<th className="py-3 px-4">Организация / ИНН</th>
<th className="py-3 px-3">Статус</th>
<th className="py-3 px-3">Тарифный план</th>
<th className="py-3 px-3 text-center">Польз. / Точки</th>
<th className="py-3 px-3 text-center">Модули</th>
<th className="py-3 px-3 text-right">MRR Доход</th>
<th className="py-3 px-3 text-right">Задолженность</th>
<th className="py-3 px-3">Активность</th>
<th className="py-3 px-4 text-right">Действие</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-800/60">
{filtered.map((org) => (
<tr
key={org.tenantId}
onClick={() => openInspector('organization', org.tenantId)}
className="hover:bg-zinc-800/40 cursor-pointer transition-colors group"
>
<td className="py-3 px-4">
<div className="font-bold text-zinc-100 group-hover:text-amber-300 transition-colors">
{org.displayName}
</div>
<div className="text-[11px] text-zinc-400 font-mono mt-0.5">
ИНН {org.inn} • {org.domain}
</div>
</td>
<td className="py-3 px-3">
<StatusBadge status={org.platformStatus} size="sm" />
</td>
<td className="py-3 px-3 font-medium text-zinc-300">{org.tariffPlan}</td>
<td className="py-3 px-3 text-center tabular-nums">
<span className="font-bold text-zinc-100">{org.userCount}</span>
<span className="text-zinc-500 text-[10px]"> / {org.branchCount}</span>
</td>
<td className="py-3 px-3 text-center font-bold text-zinc-200 tabular-nums">
{org.installedModulesCount}
</td>
<td className="py-3 px-3 text-right">
<MoneyDisplay minorUnits={org.mrrMinorUnits} className="text-amber-400 font-bold" />
</td>
<td className="py-3 px-3 text-right">
<MoneyDisplay
minorUnits={org.outstandingDebtMinorUnits}
className={org.outstandingDebtMinorUnits > 0 ? 'text-rose-400 font-bold' : 'text-zinc-400'}
/>
</td>
<td className="py-3 px-3 text-zinc-400 text-[11px]">
{new Date(org.lastActiveAt).toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
})}
</td>
<td className="py-3 px-4 text-right">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
openInspector('organization', org.tenantId);
}}
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-zinc-900 border border-zinc-700 text-zinc-300 hover:text-amber-300 hover:border-amber-500/40 text-[11px] font-medium transition-colors"
>
<span>360° Карточка</span>
<ChevronRight className="w-3 h-3" />
</button>
</td>
</tr>
))}
{filtered.length === 0 && (
<tr>
<td colSpan={9} className="py-8 text-center text-zinc-500">
Организации не найдены по заданным критериям фильтрации
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
};