feat(cc-053): BriefingController — aggregate daily briefing
This commit is contained in:
parent
835b4a6574
commit
7f6599d3f8
1 changed files with 114 additions and 5 deletions
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue