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:
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Post, UseGuards, Req } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { InvestmentPlanningService } from './investment-planning.service';
|
||||
@@ -24,7 +24,7 @@ export class InvestmentPlanningController {
|
||||
|
||||
@Post('recommendations')
|
||||
@ApiOperation({ summary: 'Get AI-powered investment recommendations' })
|
||||
getRecommendations() {
|
||||
return this.service.getAIRecommendations();
|
||||
getRecommendations(@Req() req: any) {
|
||||
return this.service.getAIRecommendations(req.user?.sub, req.user?.orgId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,8 +166,9 @@ export class InvestmentPlanningService {
|
||||
* 4. Call the AI API
|
||||
* 5. Parse and return structured recommendations
|
||||
*/
|
||||
async getAIRecommendations(): Promise<AIResponse> {
|
||||
async getAIRecommendations(userId?: string, orgId?: string): Promise<AIResponse> {
|
||||
this.debug('getAIRecommendations', 'Starting AI recommendation flow');
|
||||
const startTime = Date.now();
|
||||
|
||||
const [snapshot, cdRates, monthlyForecast] = await Promise.all([
|
||||
this.getFinancialSnapshot(),
|
||||
@@ -188,6 +189,7 @@ export class InvestmentPlanningService {
|
||||
|
||||
const messages = this.buildPromptMessages(snapshot, cdRates, monthlyForecast);
|
||||
const aiResponse = await this.callAI(messages);
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
this.debug('final_response', {
|
||||
recommendation_count: aiResponse.recommendations.length,
|
||||
@@ -195,9 +197,33 @@ export class InvestmentPlanningService {
|
||||
risk_notes_count: aiResponse.risk_notes?.length || 0,
|
||||
});
|
||||
|
||||
// Log AI usage to shared.ai_recommendation_log (fire-and-forget)
|
||||
this.logAIUsage(userId, orgId, aiResponse, elapsed).catch(() => {});
|
||||
|
||||
return aiResponse;
|
||||
}
|
||||
|
||||
private async logAIUsage(userId: string | undefined, orgId: string | undefined, response: AIResponse, elapsed: number) {
|
||||
try {
|
||||
const schema = this.tenant.getSchema();
|
||||
await this.dataSource.query(
|
||||
`INSERT INTO shared.ai_recommendation_log
|
||||
(tenant_schema, organization_id, user_id, recommendation_count, response_time_ms, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[
|
||||
schema || null,
|
||||
orgId || null,
|
||||
userId || null,
|
||||
response.recommendations?.length || 0,
|
||||
elapsed,
|
||||
response.recommendations?.length > 0 ? 'success' : 'empty',
|
||||
],
|
||||
);
|
||||
} catch (err) {
|
||||
// Non-critical — don't let logging failure break recommendations
|
||||
}
|
||||
}
|
||||
|
||||
// ── Private: Tenant-Scoped Data Queries ──
|
||||
|
||||
private async getAccountBalances(): Promise<AccountBalance[]> {
|
||||
|
||||
Reference in New Issue
Block a user