Add comprehensive platform administration panel

- Database: Add login_history, ai_recommendation_log tables; is_platform_owner
  column on users; subscription fields on organizations (payment_date,
  confirmation_number, renewal_date)
- Backend: New AdminAnalyticsService with platform metrics, tenant detail, and
  health score calculations (0-100 based on activity, budget, transactions,
  members, AI usage)
- Backend: Login/org-switch now records to login_history; AI recommendations
  logged to ai_recommendation_log; platform owner protected from superadmin toggle
- Frontend: 4-tab admin panel (Dashboard, Organizations, Users, Tenant Health)
  with tenant detail drawer, subscription management, health scoring visualization
- Platform owner account (admin@hoaledgeriq.com) auto-redirects to admin panel
- Seed data includes platform owner account and sample login history

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 08:51:39 -05:00
parent 0bd30a0eb8
commit a32d4cc179
20 changed files with 3183 additions and 317 deletions

View File

@@ -55,8 +55,14 @@ function SuperAdminRoute({ children }: { children: React.ReactNode }) {
function AuthRoute({ children }: { children: React.ReactNode }) {
const token = useAuthStore((s) => s.token);
const user = useAuthStore((s) => s.user);
const currentOrg = useAuthStore((s) => s.currentOrg);
const organizations = useAuthStore((s) => s.organizations);
if (token && currentOrg) return <Navigate to="/" replace />;
// Platform owner / superadmin with no org memberships → admin panel
if (token && user?.isSuperadmin && (!organizations || organizations.length === 0)) {
return <Navigate to="/admin" replace />;
}
if (token && !currentOrg) return <Navigate to="/select-org" replace />;
return <>{children}</>;
}

View File

@@ -17,6 +17,7 @@ import {
IconChartAreaLine,
IconClipboardCheck,
IconSparkles,
IconHeartRateMonitor,
} from '@tabler/icons-react';
import { useAuthStore } from '../../stores/authStore';
@@ -87,12 +88,44 @@ export function Sidebar({ onNavigate }: SidebarProps) {
const navigate = useNavigate();
const location = useLocation();
const user = useAuthStore((s) => s.user);
const currentOrg = useAuthStore((s) => s.currentOrg);
const organizations = useAuthStore((s) => s.organizations);
const isAdminOnly = location.pathname.startsWith('/admin') && !currentOrg;
const go = (path: string) => {
navigate(path);
onNavigate?.();
};
// When on admin route with no org selected, show admin-only sidebar
if (isAdminOnly && user?.isSuperadmin) {
return (
<ScrollArea p="sm">
<Text size="xs" c="dimmed" fw={700} tt="uppercase" px="sm" pb={4}>
Platform Administration
</Text>
<NavLink
label="Admin Panel"
leftSection={<IconCrown size={18} />}
active={location.pathname === '/admin'}
onClick={() => go('/admin')}
color="red"
/>
{organizations && organizations.length > 0 && (
<>
<Divider my="sm" />
<NavLink
label="Switch to Tenant"
leftSection={<IconBuildingBank size={18} />}
onClick={() => go('/select-org')}
variant="subtle"
/>
</>
)}
</ScrollArea>
);
}
return (
<ScrollArea p="sm">
{navSections.map((section, sIdx) => (

File diff suppressed because it is too large Load Diff

View File

@@ -38,8 +38,11 @@ export function LoginPage() {
try {
const { data } = await api.post('/auth/login', values);
setAuth(data.accessToken, data.user, data.organizations);
// Always go through org selection to ensure correct JWT with orgSchema
if (data.organizations.length >= 1) {
// Platform owner / superadmin with no orgs → admin panel
if (data.user?.isSuperadmin && data.organizations.length === 0) {
navigate('/admin');
} else if (data.organizations.length >= 1) {
// Always go through org selection to ensure correct JWT with orgSchema
navigate('/select-org');
} else {
navigate('/');

View File

@@ -14,6 +14,7 @@ interface User {
firstName: string;
lastName: string;
isSuperadmin?: boolean;
isPlatformOwner?: boolean;
}
interface AuthState {