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

@@ -46,6 +46,9 @@ export class User {
@Column({ name: 'is_superadmin', default: false })
isSuperadmin: boolean;
@Column({ name: 'is_platform_owner', default: false })
isPlatformOwner: boolean;
@Column({ name: 'last_login_at', type: 'timestamptz', nullable: true })
lastLoginAt: Date;

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@@ -50,13 +50,19 @@ export class UsersService {
const dataSource = this.usersRepository.manager.connection;
return dataSource.query(`
SELECT o.*,
(SELECT COUNT(*) FROM shared.user_organizations WHERE organization_id = o.id) as member_count
(SELECT COUNT(*) FROM shared.user_organizations WHERE organization_id = o.id) as member_count,
(SELECT MAX(lh.logged_in_at) FROM shared.login_history lh WHERE lh.organization_id = o.id) as last_activity
FROM shared.organizations o
ORDER BY o.created_at DESC
`);
}
async setSuperadmin(userId: string, isSuperadmin: boolean): Promise<void> {
// Protect platform owner from having superadmin removed
const user = await this.usersRepository.findOne({ where: { id: userId } });
if (user?.isPlatformOwner) {
throw new ForbiddenException('Cannot modify platform owner superadmin status');
}
await this.usersRepository.update(userId, { isSuperadmin });
}
}