@@ -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+
151398export const promoteToAdmin = async ( req : AuthenticatedRequest , res : Response ) => {
152399 try {
153400 if ( ! isAdmin ( req . user ?. role ) ) {
0 commit comments