176 lines
8.1 KiB
TypeScript
176 lines
8.1 KiB
TypeScript
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>
|
||
);
|
||
};
|