feat(cc-053): BriefingController — aggregate daily briefing

This commit is contained in:
admin-valentin 2026-08-01 10:43:37 +00:00
parent 835b4a6574
commit 7f6599d3f8

View file

@ -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,
};
}
}