Skip to content

Commit bf22a60

Browse files
Merge pull request #761 from OlufunbiIK/feat/backend-dashboard-controller
Feat/backend dashboard controller
2 parents 544d48b + 8fc3215 commit bf22a60

9 files changed

Lines changed: 792 additions & 80 deletions

File tree

backend/src/dashboard/dashboard.controller.ts

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ import { UserRole } from '../users/enums/userRoles.enum';
1616
import { CurrentUser } from '../auth/decorators/current.user.decorators';
1717
import { User } from '../users/entities/user.entity';
1818
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
19+
import { GetCurrentUser } from 'src/auth/decorators/getCurrentUser.decorator';
20+
import { MemberDashboardProvider } from './providers/member-dashboard.provide';
1921

2022
@ApiTags('dashboard')
2123
@ApiBearerAuth()
2224
@Controller('dashboard')
2325
export class DashboardController {
2426
constructor(
2527
private readonly dashboardService: DashboardService,
28+
private readonly memberDashboardProvider: MemberDashboardProvider,
2629
private readonly adminAnalyticsProvider: AdminAnalyticsProvider,
2730
) {}
2831

@@ -66,15 +69,68 @@ export class DashboardController {
6669
return { success: true, ...data };
6770
}
6871

69-
@Get('admin/analytics')
72+
// ──────────────────────────────────────────────
73+
// Member endpoints
74+
// ──────────────────────────────────────────────
75+
76+
@Get('member')
7077
@HttpCode(HttpStatus.OK)
71-
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN, UserRole.STAFF)
72-
@UseGuards(JwtAuthGuard, RolesGuard)
73-
async getAdminAnalytics(@Query() query: AnalyticsQueryDto) {
74-
const data = await this.adminAnalyticsProvider.getFullAdminDashboard(
75-
query.from,
76-
query.to,
77-
);
78+
async getMemberDashboard(@GetCurrentUser('id') userId: string) {
79+
const data = await this.memberDashboardProvider.getMemberDashboard(userId);
7880
return { success: true, data };
7981
}
82+
83+
@Get('member/bookings')
84+
@HttpCode(HttpStatus.OK)
85+
async getMemberBookings(
86+
@GetCurrentUser('id') userId: string,
87+
@Query('page') page: string = '1',
88+
@Query('limit') limit: string = '10',
89+
) {
90+
const parsedPage = Math.max(1, parseInt(page, 10) || 1);
91+
const parsedLimit = Math.min(50, Math.max(1, parseInt(limit, 10) || 10));
92+
93+
const data = await this.dashboardService.getMemberBookings(
94+
userId,
95+
parsedPage,
96+
parsedLimit,
97+
);
98+
return { success: true, ...data };
99+
}
100+
101+
@Get('member/payments')
102+
@HttpCode(HttpStatus.OK)
103+
async getMemberPayments(
104+
@GetCurrentUser('id') userId: string,
105+
@Query('page') page: string = '1',
106+
@Query('limit') limit: string = '10',
107+
) {
108+
const parsedPage = Math.max(1, parseInt(page, 10) || 1);
109+
const parsedLimit = Math.min(50, Math.max(1, parseInt(limit, 10) || 10));
110+
111+
const data = await this.dashboardService.getMemberPayments(
112+
userId,
113+
parsedPage,
114+
parsedLimit,
115+
);
116+
return { success: true, ...data };
117+
}
118+
119+
@Get('member/invoices')
120+
@HttpCode(HttpStatus.OK)
121+
async getMemberInvoices(
122+
@GetCurrentUser('id') userId: string,
123+
@Query('page') page: string = '1',
124+
@Query('limit') limit: string = '10',
125+
) {
126+
const parsedPage = Math.max(1, parseInt(page, 10) || 1);
127+
const parsedLimit = Math.min(50, Math.max(1, parseInt(limit, 10) || 10));
128+
129+
const data = await this.dashboardService.getMemberInvoices(
130+
userId,
131+
parsedPage,
132+
parsedLimit,
133+
);
134+
return { success: true, ...data };
135+
}
80136
}

backend/src/dashboard/dashboard.service.ts

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm';
33
import { Repository, MoreThanOrEqual } from 'typeorm';
44
import { User } from '../users/entities/user.entity';
55
import { NewsletterSubscriber } from '../newsletter/entities/newsletter.entity';
6+
import { Booking } from '../bookings/entities/booking.entity';
7+
import { Payment } from '../payments/entities/payment.entity';
8+
import { Invoice } from '../invoices/entities/invoice.entity';
9+
import { PaymentStatus } from '../payments/enums/paymentStatus.enum';
610

711
@Injectable()
812
export class DashboardService {
@@ -11,6 +15,12 @@ export class DashboardService {
1115
private readonly userRepository: Repository<User>,
1216
@InjectRepository(NewsletterSubscriber)
1317
private readonly newsletterRepository: Repository<NewsletterSubscriber>,
18+
@InjectRepository(Booking)
19+
private readonly bookingRepository: Repository<Booking>,
20+
@InjectRepository(Payment)
21+
private readonly paymentRepository: Repository<Payment>,
22+
@InjectRepository(Invoice)
23+
private readonly invoiceRepository: Repository<Invoice>,
1424
) {}
1525

1626
/**
@@ -28,8 +38,11 @@ export class DashboardService {
2838
return {
2939
totalMembers,
3040
verifiedMembers,
31-
activeWorkspaces: 1, // placeholder until workspaces entity exists
32-
deskOccupancy: Math.min(Math.round((verifiedMembers / Math.max(totalMembers, 1)) * 100), 100),
41+
activeWorkspaces: 1,
42+
deskOccupancy: Math.min(
43+
Math.round((verifiedMembers / Math.max(totalMembers, 1)) * 100),
44+
100,
45+
),
3346
};
3447
}
3548

@@ -40,7 +53,14 @@ export class DashboardService {
4053
const recentUsers = await this.userRepository.find({
4154
order: { createdAt: 'DESC' },
4255
take: 10,
43-
select: ['id', 'firstname', 'lastname', 'email', 'createdAt', 'isVerified'],
56+
select: [
57+
'id',
58+
'firstname',
59+
'lastname',
60+
'email',
61+
'createdAt',
62+
'isVerified',
63+
],
4464
});
4565

4666
return recentUsers.map((u) => ({
@@ -71,8 +91,12 @@ export class DashboardService {
7191
newSubscribersThisMonth,
7292
] = await Promise.all([
7393
this.userRepository.count({ where: { isDeleted: false } }),
74-
this.userRepository.count({ where: { isActive: true, isDeleted: false } }),
75-
this.userRepository.count({ where: { isSuspended: true, isDeleted: false } }),
94+
this.userRepository.count({
95+
where: { isActive: true, isDeleted: false },
96+
}),
97+
this.userRepository.count({
98+
where: { isSuspended: true, isDeleted: false },
99+
}),
76100
this.userRepository.count({
77101
where: { createdAt: MoreThanOrEqual(thirtyDaysAgo), isDeleted: false },
78102
}),
@@ -84,7 +108,6 @@ export class DashboardService {
84108
}),
85109
]);
86110

87-
// Registration trend — last 6 months
88111
const registrationTrend = await this.getMonthlyRegistrations(6);
89112

90113
return {
@@ -151,13 +174,71 @@ export class DashboardService {
151174
};
152175
}
153176

177+
async getMemberBookings(userId: string, page: number, limit: number) {
178+
const [data, total] = await this.bookingRepository.findAndCount({
179+
where: { userId },
180+
relations: ['workspace'],
181+
order: { createdAt: 'DESC' },
182+
skip: (page - 1) * limit,
183+
take: limit,
184+
});
185+
186+
return {
187+
data,
188+
meta: {
189+
total,
190+
page,
191+
limit,
192+
totalPages: Math.ceil(total / limit),
193+
},
194+
};
195+
}
196+
197+
async getMemberPayments(userId: string, page: number, limit: number) {
198+
const [data, total] = await this.paymentRepository.findAndCount({
199+
where: { userId, status: PaymentStatus.SUCCESS },
200+
order: { createdAt: 'DESC' },
201+
skip: (page - 1) * limit,
202+
take: limit,
203+
});
204+
205+
return {
206+
data,
207+
meta: {
208+
total,
209+
page,
210+
limit,
211+
totalPages: Math.ceil(total / limit),
212+
},
213+
};
214+
}
215+
216+
async getMemberInvoices(userId: string, page: number, limit: number) {
217+
const [data, total] = await this.invoiceRepository.findAndCount({
218+
where: { userId },
219+
relations: ['booking', 'booking.workspace'],
220+
order: { createdAt: 'DESC' },
221+
skip: (page - 1) * limit,
222+
take: limit,
223+
});
224+
225+
return {
226+
data,
227+
meta: {
228+
total,
229+
page,
230+
limit,
231+
totalPages: Math.ceil(total / limit),
232+
},
233+
};
234+
}
235+
154236
private async getMonthlyRegistrations(months: number) {
155237
const result: { month: string; count: number }[] = [];
156238
const now = new Date();
157239

158240
for (let i = months - 1; i >= 0; i--) {
159241
const start = new Date(now.getFullYear(), now.getMonth() - i, 1);
160-
const end = new Date(now.getFullYear(), now.getMonth() - i + 1, 0, 23, 59, 59);
161242

162243
const count = await this.userRepository.count({
163244
where: {
@@ -166,7 +247,6 @@ export class DashboardService {
166247
},
167248
});
168249

169-
// We need a between query, but MoreThanOrEqual + manual filter works for trend
170250
const monthLabel = start.toLocaleString('en', { month: 'short' });
171251
result.push({ month: monthLabel, count });
172252
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { Repository, In } from 'typeorm';
4+
5+
@Injectable()
6+
export class MemberDashboardProvider {
7+
constructor(
8+
@InjectRepository(Booking)
9+
private readonly bookingRepository: Repository<Booking>,
10+
@InjectRepository(Payment)
11+
private readonly paymentRepository: Repository<Payment>,
12+
@InjectRepository(Invoice)
13+
private readonly invoiceRepository: Repository<Invoice>,
14+
@InjectRepository(WorkspaceLog)
15+
private readonly workspaceLogRepository: Repository<WorkspaceLog>,
16+
) {}
17+
18+
async getMemberStats(userId: string) {
19+
const [activeBookings, totalSpentResult, invoiceCount, lastLog] =
20+
await Promise.all([
21+
this.bookingRepository.count({
22+
where: {
23+
userId,
24+
status: In([BookingStatus.PENDING, BookingStatus.CONFIRMED]),
25+
},
26+
}),
27+
this.paymentRepository
28+
.createQueryBuilder('payment')
29+
.select('SUM(payment.amountKobo)', 'total')
30+
.where('payment.userId = :userId', { userId })
31+
.andWhere('payment.status = :status', {
32+
status: PaymentStatus.SUCCESS,
33+
})
34+
.getRawOne<{ total: string | null }>(),
35+
this.invoiceRepository.count({ where: { userId } }),
36+
this.workspaceLogRepository.findOne({
37+
where: { userId },
38+
order: { checkedInAt: 'DESC' },
39+
}),
40+
]);
41+
42+
return {
43+
activeBookings,
44+
totalSpentKobo: parseInt(totalSpentResult?.total ?? '0', 10) || 0,
45+
invoiceCount,
46+
lastCheckIn: lastLog?.checkedInAt ?? null,
47+
};
48+
}
49+
50+
async getMemberDashboard(userId: string) {
51+
const [stats, recentBookings, recentPayments] = await Promise.all([
52+
this.getMemberStats(userId),
53+
this.bookingRepository.find({
54+
where: { userId },
55+
relations: ['workspace'],
56+
order: { createdAt: 'DESC' },
57+
take: 5,
58+
}),
59+
this.paymentRepository.find({
60+
where: { userId, status: PaymentStatus.SUCCESS },
61+
order: { createdAt: 'DESC' },
62+
take: 5,
63+
}),
64+
]);
65+
66+
return { stats, recentBookings, recentPayments };
67+
}
68+
}

0 commit comments

Comments
 (0)