feat: member portal Sprint 1 — dashboard with MemberShell layout and TowerUser profile fields

- Add avatar, hometown, currentLocation, interests, language, digestPreference, directoryVisible fields to TowerUser
- Migration: 20260617200000_add_tower_user_profile_fields
- GET /my/dashboard API endpoint with stats (groups, messages, consents)
- BFF route /api/my/dashboard
- MemberShell 3-column layout (220px nav + flex-1 main + 280px rail)
- Dashboard page: hero card, stat cards, group list, interests chips
- Sidebar returns null for /my paths (MemberShell owns the nav)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 15:44:58 +05:30
parent 622737fdca
commit 566690bd04
8 changed files with 283 additions and 75 deletions
+5
View File
@@ -24,6 +24,11 @@ class OptInDto {
export class MyController {
constructor(private readonly service: MyService) {}
@Get('dashboard')
dashboard(@CurrentMember() member: MemberJwtPayload) {
return this.service.getDashboard(member.sub, member.tenantId);
}
@Get('profile')
profile(@CurrentMember() member: MemberJwtPayload) {
return this.service.getProfile(member.sub, member.tenantId);
+40
View File
@@ -26,6 +26,46 @@ export class MyService {
};
}
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');
const [activeConsents, messagesCount] = await Promise.all([
this.prisma.consentRecord.findMany({
where: { userId, tenantId, status: 'GRANTED' },
include: { group: { select: { id: true, name: true } } },
}),
this.prisma.message.count({ where: { senderTowerUserId: userId, tenantId } }),
]);
return {
profile: {
id: user.id,
jid: user.jid,
displayName: user.displayName,
avatar: user.avatar,
hometown: user.hometown,
currentLocation: user.currentLocation,
interests: user.interests,
language: user.language,
digestPreference: user.digestPreference,
directoryVisible: user.directoryVisible,
createdAt: user.createdAt.toISOString(),
},
stats: {
groupsJoined: activeConsents.length,
messagesContributed: messagesCount,
activeConsents: activeConsents.length,
},
groups: activeConsents.map((c) => ({
id: c.group.id,
name: c.group.name,
joinedAt: c.effectiveAt.toISOString(),
consentStatus: c.status,
})),
};
}
async listGroups(userId: string, tenantId: string): Promise<MemberGroupSummary[]> {
const consents = await this.prisma.consentRecord.findMany({
where: { userId, tenantId },