From 7f6599d3f89b6aa48d7f611674ce6905893aa50e Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sat, 1 Aug 2026 10:43:37 +0000 Subject: [PATCH] =?UTF-8?q?feat(cc-053):=20BriefingController=20=E2=80=94?= =?UTF-8?q?=20aggregate=20daily=20briefing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/briefing/briefing.controller.ts | 119 ++++++++++++++++++++++++++-- 1 file changed, 114 insertions(+), 5 deletions(-) diff --git a/src/briefing/briefing.controller.ts b/src/briefing/briefing.controller.ts index 9d861cf..8e73cd7 100644 --- a/src/briefing/briefing.controller.ts +++ b/src/briefing/briefing.controller.ts @@ -1,14 +1,123 @@ import { Controller, Get } from '@nestjs/common'; +import { and, desc, eq, gt, gte, isNull, isNotNull, lt, lte, inArray, or } from 'drizzle-orm'; import { CurrentSession } from '../auth/session.decorator'; import type { AuthenticatedSession } from '../auth/tenant.guard'; -import { BriefingService } from './briefing.service'; +import { db } from '../db/client'; +import { goals, tasks, decisions, opportunities, financialSignals, notifications } from '../db/schema'; @Controller('briefing') export class BriefingController { - constructor(private readonly briefingService: BriefingService) {} + @Get() + async today(@CurrentSession() session: AuthenticatedSession) { + const now = new Date(); + const todayEnd = new Date(now); + todayEnd.setHours(23, 59, 59, 999); + const sevenDays = new Date(now.getTime() + 7 * 86400_000); + const tid = session.tenantId; - @Get('today') - today(@CurrentSession() session: AuthenticatedSession) { - return this.briefingService.today(session); + const [ + atRiskGoals, + overdueTasks, + todayTasks, + pendingDecisions, + criticalSignals, + expiringOpps, + recentNotifications, + ] = await Promise.all([ + // Goals at risk + db.query.goals.findMany({ + where: and(eq(goals.tenantId, tid), isNull(goals.deletedAt), eq(goals.status, 'at_risk')), + orderBy: [desc(goals.createdAt)], + limit: 5, + }), + // Overdue tasks + db.query.tasks.findMany({ + where: and( + eq(tasks.tenantId, tid), + isNull(tasks.deletedAt), + inArray(tasks.status, ['open', 'in_progress', 'blocked'] as any[]), + lt(tasks.dueAt, now), + ), + orderBy: [desc(tasks.priority)], + limit: 10, + }), + // Tasks due today + db.query.tasks.findMany({ + where: and( + eq(tasks.tenantId, tid), + isNull(tasks.deletedAt), + inArray(tasks.status, ['open', 'in_progress'] as any[]), + gte(tasks.dueAt, now), + lte(tasks.dueAt, todayEnd), + ), + orderBy: [desc(tasks.priority)], + limit: 10, + }), + // Decisions pending outcome review + db.query.decisions.findMany({ + where: and( + eq(decisions.tenantId, tid), + isNotNull(decisions.reviewDueAt), + lte(decisions.reviewDueAt, now), + isNull(decisions.outcomeReview), + ), + orderBy: [desc(decisions.reviewDueAt)], + limit: 5, + }), + // Critical financial signals (last 48h) + db.query.financialSignals.findMany({ + where: and( + or(isNull(financialSignals.tenantId), eq(financialSignals.tenantId, tid)), + eq(financialSignals.severity, 'critical'), + gte(financialSignals.publishedAt, new Date(now.getTime() - 48 * 3600_000)), + or(isNull(financialSignals.expiresAt), gt(financialSignals.expiresAt, now)), + ), + orderBy: [desc(financialSignals.publishedAt)], + limit: 5, + }), + // Opportunities expiring in 7 days + db.query.opportunities.findMany({ + where: and( + eq(opportunities.tenantId, tid), + isNotNull(opportunities.expiresAt), + lte(opportunities.expiresAt, sevenDays), + gt(opportunities.expiresAt, now), + ), + orderBy: [desc(opportunities.expiresAt)], + limit: 5, + }), + // Unread notifications (last 24h, max 10) + db.query.notifications.findMany({ + where: and( + eq(notifications.tenantId, tid), + eq(notifications.recipientUserId, session.userId), + inArray(notifications.status, ['PENDING', 'DELIVERED'] as any[]), + gte(notifications.createdAt, new Date(now.getTime() - 24 * 3600_000)), + ), + orderBy: [desc(notifications.createdAt)], + limit: 10, + }), + ]); + + const score = Math.max( + 0, + 100 + - atRiskGoals.length * 15 + - Math.min(overdueTasks.length * 5, 25) + - pendingDecisions.length * 10 + - criticalSignals.length * 10, + ); + + return { + generatedAt: now.toISOString(), + score, + atRiskGoals, + overdueTasks, + todayTasks, + pendingDecisions, + criticalSignals, + expiringOpportunities: expiringOpps, + notifications: recentNotifications, + }; } }