diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 81f8a47..5874b95 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -88,6 +88,8 @@ model Tenant { auditEvents AuditEvent[] rules TenantRule[] groupAccesses GroupAccess[] + digestConfig DigestConfig? + digests Digest[] } enum AdminRole { @@ -495,3 +497,34 @@ model TenantRule { @@index([tenantId, isActive]) @@index([tenantId, matchType]) } + +// ============================================================================ +// Digest +// ============================================================================ + +model DigestConfig { + id String @id @default(cuid()) + tenantId String @unique + tenant Tenant @relation(fields: [tenantId], references: [id]) + targetGroupJid String + targetAccountId String + scheduleHour Int @default(20) + scheduleMinute Int @default(0) + isActive Boolean @default(true) + lastSentAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Digest { + id String @id @default(cuid()) + tenantId String + tenant Tenant @relation(fields: [tenantId], references: [id]) + content String + messageIds String[] + digestDate DateTime + sentAt DateTime @default(now()) + + @@unique([tenantId, digestDate]) + @@index([tenantId]) +} diff --git a/apps/api/src/modules/my/my.controller.ts b/apps/api/src/modules/my/my.controller.ts index 66db7ff..b10b846 100644 --- a/apps/api/src/modules/my/my.controller.ts +++ b/apps/api/src/modules/my/my.controller.ts @@ -29,6 +29,11 @@ export class MyController { return this.service.getDashboard(member.sub, member.tenantId); } + @Get('digest') + digests(@CurrentMember() member: MemberJwtPayload) { + return this.service.getDigests(member.tenantId); + } + @Get('profile') profile(@CurrentMember() member: MemberJwtPayload) { return this.service.getProfile(member.sub, member.tenantId); diff --git a/apps/api/src/modules/my/my.service.ts b/apps/api/src/modules/my/my.service.ts index 64bcb11..ede0995 100644 --- a/apps/api/src/modules/my/my.service.ts +++ b/apps/api/src/modules/my/my.service.ts @@ -26,6 +26,22 @@ export class MyService { }; } + async getDigests(tenantId: string) { + const digests = await this.prisma.digest.findMany({ + where: { tenantId }, + orderBy: { digestDate: 'desc' }, + take: 20, + select: { id: true, digestDate: true, content: true, messageIds: true, sentAt: true }, + }); + return digests.map((d) => ({ + id: d.id, + digestDate: d.digestDate.toISOString(), + summary: d.content.slice(0, 300), + messageCount: (d.messageIds as string[]).length, + sentAt: d.sentAt.toISOString(), + })); + } + async getDashboard(userId: string, tenantId: string) { const user = await this.prisma.towerUser.findFirst({ where: { id: userId, tenantId } }); if (!user) throw new NotFoundException('User not found'); diff --git a/apps/web/app/api/my/digest/route.ts b/apps/web/app/api/my/digest/route.ts new file mode 100644 index 0000000..4ef5497 --- /dev/null +++ b/apps/web/app/api/my/digest/route.ts @@ -0,0 +1,9 @@ +import { jsonResponse, memberApiFetch } from '../../../_lib/api'; + +export const dynamic = 'force-dynamic'; + +export async function GET(): Promise { + const res = await memberApiFetch('/my/digest'); + const body = await res.json(); + return jsonResponse(body, res.status); +} diff --git a/apps/web/app/my/digest/page.tsx b/apps/web/app/my/digest/page.tsx new file mode 100644 index 0000000..d0741bb --- /dev/null +++ b/apps/web/app/my/digest/page.tsx @@ -0,0 +1,68 @@ +import { getMemberToken, getApiBaseUrl } from '../../_lib/api'; +import Link from 'next/link'; + +interface DigestItem { + id: string; + digestDate: string; + summary: string; + messageCount: number; + sentAt: string; +} + +async function fetchDigests(token: string): Promise { + const res = await fetch(`${getApiBaseUrl()}/my/digest`, { + headers: { Authorization: `Bearer ${token}` }, + cache: 'no-store', + }).catch(() => null); + if (!res || !res.ok) return []; + return (await res.json()) as DigestItem[]; +} + +export default async function DigestPage() { + const token = await getMemberToken(); + if (!token) return null; + + const digests = await fetchDigests(token); + + return ( +
+
+

Digest history

+ ← Dashboard +
+ + {digests.length === 0 && ( +
+

No digests sent yet.

+

Your chapter admin generates these periodically.

+
+ )} + +
+ {digests.map((d) => ( +
+
+
+

+ {new Date(d.digestDate).toLocaleDateString('en-IN', { + weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', + })} +

+

+ {d.messageCount} message{d.messageCount !== 1 ? 's' : ''} · sent {new Date(d.sentAt).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' })} +

+
+ + Digest + +
+

+ {d.summary} + {d.summary.length >= 300 && } +

+
+ ))} +
+
+ ); +} diff --git a/apps/web/app/my/layout.tsx b/apps/web/app/my/layout.tsx index 949d171..2b6aa77 100644 --- a/apps/web/app/my/layout.tsx +++ b/apps/web/app/my/layout.tsx @@ -4,6 +4,7 @@ import { redirect } from 'next/navigation'; const NAV = [ { href: '/my', label: 'Dashboard' }, + { href: '/my/digest', label: 'Digest' }, { href: '/my/groups', label: 'My Groups' }, { href: '/my/settings', label: 'Settings' }, ];