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 { 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 { CurrentSession } from '../auth/session.decorator';
|
||||||
import type { AuthenticatedSession } from '../auth/tenant.guard';
|
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')
|
@Controller('briefing')
|
||||||
export class BriefingController {
|
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')
|
const [
|
||||||
today(@CurrentSession() session: AuthenticatedSession) {
|
atRiskGoals,
|
||||||
return this.briefingService.today(session);
|
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