Skip to content

Commit 37b4bb1

Browse files
Merge branch 'main' into org_announcements
2 parents bbd8081 + c96cf59 commit 37b4bb1

File tree

45 files changed

+7304
-422
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+7304
-422
lines changed

backend/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ app.use('/api/blogs', blogRoutes);
6363
app.use('/api/notifications', inAppNotificationRoutes);
6464
app.use('/api/email-notifications', notificationRoutes);
6565
app.use('/api/contact', contactRoutes);
66-
app.use('/api/uploads', fileUploadRoutes);
66+
app.use('/api/files', fileUploadRoutes);
6767
app.use('/api/page-content', pageContentRoutes);
6868
app.use('/api/map', mapRoutes);
6969

backend/controllers/adminController.ts

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,253 @@ export const deleteAdmin = async (req: AuthenticatedRequest, res: Response) => {
148148
* @route POST /api/admins/promote
149149
* @access Admin only
150150
*/
151+
152+
/**
153+
* @desc Get dashboard statistics
154+
* @route GET /api/admins/stats
155+
* @access Admin only
156+
*/
157+
export const getDashboardStats = async (req: AuthenticatedRequest, res: Response) => {
158+
try {
159+
if (!isAdmin(req.user?.role)) {
160+
return res.status(403).json({ error: 'Access denied. Admin privileges required.' });
161+
}
162+
163+
const [
164+
totalOrganizations,
165+
pendingOrganizations,
166+
approvedOrganizations,
167+
totalAnnouncements,
168+
totalBlogs,
169+
totalSurveys,
170+
activeSurveys,
171+
totalEmailSubscribers,
172+
totalAlerts,
173+
recentActivity,
174+
organizationsWithLocation,
175+
upcomingSurveyDeadlines,
176+
recentSurveyResponses,
177+
178+
orgGrowthData,
179+
subscriptionGrowthData,
180+
181+
allSurveys,
182+
] = await Promise.all([
183+
prisma.organization.count(),
184+
prisma.organization.count({ where: { status: 'PENDING' } }),
185+
prisma.organization.count({ where: { status: 'ACTIVE' } }),
186+
187+
prisma.announcements.count(),
188+
prisma.blog.count(),
189+
prisma.survey.count(),
190+
prisma.survey.count({ where: { isActive: true } }),
191+
prisma.emailSubscription.count(),
192+
prisma.alert.count(),
193+
194+
Promise.all([
195+
prisma.organization.findMany({
196+
take: 5,
197+
orderBy: { createdAt: 'desc' },
198+
select: { id: true, name: true, createdAt: true, status: true },
199+
}),
200+
prisma.announcements.findMany({
201+
take: 5,
202+
orderBy: { createdAt: 'desc' },
203+
select: { id: true, title: true, createdAt: true },
204+
}),
205+
prisma.survey.findMany({
206+
take: 5,
207+
orderBy: { createdAt: 'desc' },
208+
select: { id: true, title: true, createdAt: true },
209+
}),
210+
]),
211+
212+
prisma.organization.findMany({
213+
where: {
214+
status: 'ACTIVE',
215+
latitude: { not: null },
216+
longitude: { not: null },
217+
},
218+
select: {
219+
id: true,
220+
name: true,
221+
latitude: true,
222+
longitude: true,
223+
address: true,
224+
city: true,
225+
website: true,
226+
},
227+
}),
228+
229+
prisma.survey.findMany({
230+
where: {
231+
isActive: true,
232+
dueDate: {
233+
gte: new Date(),
234+
lte: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
235+
},
236+
},
237+
take: 5,
238+
orderBy: { dueDate: 'asc' },
239+
select: { id: true, title: true, dueDate: true },
240+
}),
241+
242+
prisma.surveyResponse.findMany({
243+
where: {
244+
submittedDate: {
245+
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
246+
},
247+
},
248+
take: 10,
249+
orderBy: { submittedDate: 'desc' },
250+
select: {
251+
id: true,
252+
submittedDate: true,
253+
survey: { select: { id: true, title: true } },
254+
organization: { select: { id: true, name: true } },
255+
},
256+
}),
257+
258+
prisma.organization.findMany({
259+
where: {
260+
createdAt: {
261+
gte: new Date(Date.now() - 6 * 30 * 24 * 60 * 60 * 1000),
262+
},
263+
},
264+
select: { createdAt: true },
265+
orderBy: { createdAt: 'asc' },
266+
}),
267+
268+
prisma.emailSubscription.findMany({
269+
where: {
270+
createdAt: {
271+
gte: new Date(Date.now() - 6 * 30 * 24 * 60 * 60 * 1000),
272+
},
273+
},
274+
select: { createdAt: true },
275+
orderBy: { createdAt: 'asc' },
276+
}),
277+
278+
prisma.survey.findMany({
279+
where: { isPublished: true },
280+
select: {
281+
id: true,
282+
title: true,
283+
_count: {
284+
select: { surveyResponses: true },
285+
},
286+
},
287+
}),
288+
]);
289+
290+
const aggregateByMonth = (items: { createdAt: Date }[]) => {
291+
const months: Record<string, number> = {};
292+
const now = new Date();
293+
294+
for (let i = 5; i >= 0; i--) {
295+
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
296+
const key = date.toLocaleString('default', { month: 'short', year: '2-digit' });
297+
months[key] = 0;
298+
}
299+
300+
items.forEach(item => {
301+
const date = new Date(item.createdAt);
302+
const key = date.toLocaleString('default', { month: 'short', year: '2-digit' });
303+
if (months[key] !== undefined) {
304+
months[key]++;
305+
}
306+
});
307+
308+
let cumulative = 0;
309+
return Object.entries(months).map(([month, count]) => {
310+
cumulative += count;
311+
return { month, count: cumulative };
312+
});
313+
};
314+
315+
const growthData = {
316+
organizations: aggregateByMonth(orgGrowthData),
317+
subscriptions: aggregateByMonth(subscriptionGrowthData),
318+
};
319+
320+
const totalActiveOrgs = approvedOrganizations;
321+
const surveyResponseRates = allSurveys.map((survey: any) => ({
322+
id: survey.id,
323+
title: survey.title,
324+
totalSent: totalActiveOrgs,
325+
totalResponded: survey._count.surveyResponses,
326+
responseRate:
327+
totalActiveOrgs > 0
328+
? Math.round((survey._count.surveyResponses / totalActiveOrgs) * 100)
329+
: 0,
330+
}));
331+
332+
const [recentOrgs, recentAnnouncements, recentSurveys] = recentActivity;
333+
const formattedActivity = [
334+
...recentOrgs.map((org: any) => ({
335+
id: org.id,
336+
type: 'organization',
337+
title: org.name,
338+
description:
339+
org.status === 'PENDING' ? 'New registration (pending)' : 'Organization registered',
340+
createdAt: org.createdAt,
341+
})),
342+
...recentAnnouncements.map((ann: any) => ({
343+
id: ann.id,
344+
type: 'announcement',
345+
title: ann.title,
346+
description: 'Announcement created',
347+
createdAt: ann.createdAt,
348+
})),
349+
...recentSurveys.map((survey: any) => ({
350+
id: survey.id,
351+
type: 'survey',
352+
title: survey.title,
353+
description: 'Survey created',
354+
createdAt: survey.createdAt,
355+
})),
356+
]
357+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
358+
.slice(0, 10);
359+
360+
res.json({
361+
stats: {
362+
totalOrganizations,
363+
pendingOrganizations,
364+
approvedOrganizations,
365+
totalAnnouncements,
366+
totalBlogs,
367+
totalSurveys,
368+
activeSurveys,
369+
totalEmailSubscribers,
370+
totalAlerts,
371+
},
372+
recentActivity: formattedActivity,
373+
organizationsWithLocation,
374+
actionItems: {
375+
pendingOrganizations,
376+
upcomingSurveyDeadlines: upcomingSurveyDeadlines.map((s: any) => ({
377+
id: s.id,
378+
title: s.title,
379+
endDate: s.dueDate,
380+
})),
381+
recentSurveyResponses: recentSurveyResponses.map((r: any) => ({
382+
id: r.id,
383+
surveyId: r.survey.id,
384+
surveyTitle: r.survey.title,
385+
organizationName: r.organization.name,
386+
submittedDate: r.submittedDate,
387+
})),
388+
},
389+
growthData,
390+
surveyResponseRates,
391+
});
392+
} catch (error) {
393+
console.error('Error fetching dashboard stats:', error);
394+
res.status(500).json({ error: 'Failed to fetch dashboard statistics' });
395+
}
396+
};
397+
151398
export const promoteToAdmin = async (req: AuthenticatedRequest, res: Response) => {
152399
try {
153400
if (!isAdmin(req.user?.role)) {

backend/controllers/blogController.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,26 @@ export const createBlog = async (req: AuthenticatedRequest, res: Response) => {
148148
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
149149
if (!isAdmin(req.user.role)) return res.status(403).json({ error: 'Admin only' });
150150

151-
const { title, content, author, tags, tagIds, featuredImageUrl, isPublished, publishedDate } =
152-
req.body;
153-
154-
console.log('createBlog - Received data:', { title, author, tags, tagIds, isPublished });
151+
const {
152+
title,
153+
content,
154+
author,
155+
tags,
156+
tagIds,
157+
featuredImageUrl,
158+
isPublished,
159+
publishedDate,
160+
attachmentUrls,
161+
} = req.body;
162+
163+
console.log('createBlog - Received data:', {
164+
title,
165+
author,
166+
tags,
167+
tagIds,
168+
isPublished,
169+
attachmentUrls,
170+
});
155171

156172
if (!title || !content || !author) {
157173
return res.status(400).json({ error: 'title, content, and author are required' });
@@ -166,6 +182,7 @@ export const createBlog = async (req: AuthenticatedRequest, res: Response) => {
166182
featuredImageUrl: featuredImageUrl || null,
167183
isPublished: isPublished || false,
168184
publishedDate: isPublished ? publishedDate || new Date() : null,
185+
attachmentUrls: attachmentUrls || [],
169186
},
170187
});
171188

@@ -214,13 +231,14 @@ export const updateBlog = async (req: AuthenticatedRequest, res: Response) => {
214231
});
215232
if (!blogToUpdate) return res.status(404).json({ error: 'Blog not found' });
216233

217-
const { title, content, author, tags, tagIds, featuredImageUrl } = req.body;
234+
const { title, content, author, tags, tagIds, featuredImageUrl, attachmentUrls } = req.body;
218235

219236
const updateData: any = {
220237
...(title && { title }),
221238
...(content && { content }),
222239
...(author && { author }),
223240
...(featuredImageUrl !== undefined && { featuredImageUrl }),
241+
...(attachmentUrls !== undefined && { attachmentUrls }),
224242
};
225243

226244
// Handle tags update if provided (accept both 'tags' and 'tagIds')

0 commit comments

Comments
 (0)