@@ -7,94 +7,103 @@ const isValidDate = (date) => {
77 return ! Number . isNaN ( parsedDate . getTime ( ) ) ;
88} ;
99
10- module . exports = ( ) => ( {
11- getOPTStatusBreakdown : async ( req , res , next ) => {
12- try {
13- const { startDate, endDate, role } = req . query ;
14- const query = { } ;
10+ const parseDateParam = ( dateStr , fieldName , res ) => {
11+ if ( ! isValidDate ( dateStr ) ) {
12+ res . status ( 400 ) . json ( { message : `Invalid ${ fieldName } . Use ISO format (YYYY-MM-DD).` } ) ;
13+ return null ;
14+ }
15+ const parsed = new Date ( dateStr ) ;
16+ if ( Number . isNaN ( parsed . getTime ( ) ) ) {
17+ res . status ( 400 ) . json ( { message : `Invalid ${ fieldName } . Use ISO format (YYYY-MM-DD).` } ) ;
18+ return null ;
19+ }
20+ return parsed ;
21+ } ;
1522
16- let start ;
17- let end ;
23+ const validateDateRange = ( start , end , startDate , endDate , res ) => {
24+ if ( start && end && start > end ) {
25+ res . status ( 400 ) . json ( { message : 'startDate cannot be greater than endDate.' } ) ;
26+ return false ;
27+ }
28+ if ( startDate && endDate && start . toDateString ( ) === end . toDateString ( ) ) {
29+ res . status ( 400 ) . json ( { message : 'startDate and endDate cannot be the same.' } ) ;
30+ return false ;
31+ }
32+ return true ;
33+ } ;
1834
19- if ( startDate ) {
20- if ( ! isValidDate ( startDate ) ) {
21- return res . status ( 400 ) . json ( {
22- message : 'Invalid startDate. Use ISO format (YYYY-MM-DD).' ,
23- } ) ;
24- }
25- start = new Date ( startDate ) ;
26- if ( Number . isNaN ( start . getTime ( ) ) ) {
27- return res . status ( 400 ) . json ( {
28- message : 'Invalid startDate. Use ISO format (YYYY-MM-DD).' ,
29- } ) ;
30- }
31- }
35+ const sanitizeRole = ( role ) => {
36+ if ( ! role ) return null ;
37+ const str = String ( role ) ;
38+ return str . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, '' ) ;
39+ } ;
3240
33- if ( endDate ) {
34- if ( ! isValidDate ( endDate ) ) {
35- return res . status ( 400 ) . json ( {
36- message : 'Invalid endDate. Use ISO format (YYYY-MM-DD).' ,
37- } ) ;
38- }
39- end = new Date ( endDate ) ;
40- if ( Number . isNaN ( end . getTime ( ) ) ) {
41- return res . status ( 400 ) . json ( {
42- message : 'Invalid endDate. Use ISO format (YYYY-MM-DD).' ,
43- } ) ;
44- }
45- }
41+ const buildQuery = ( start , end , role ) => {
42+ const query = { } ;
43+ if ( start || end ) {
44+ query . applicationDate = { } ;
45+ if ( start ) query . applicationDate . $gte = start ;
46+ if ( end ) query . applicationDate . $lte = end ;
47+ }
48+ const safeRole = sanitizeRole ( role ) ;
49+ if ( safeRole ) {
50+ query . role = safeRole ;
51+ }
52+ return query ;
53+ } ;
4654
47- if ( start && end && start > end ) {
48- return res . status ( 400 ) . json ( {
49- message : 'startDate cannot be greater than endDate.' ,
50- } ) ;
51- }
55+ const computeBreakdown = ( result ) => {
56+ const totalCandidates = result . length ;
57+ const breakDownMap = { } ;
58+ result . forEach ( ( { optStatus } ) => {
59+ breakDownMap [ optStatus ] = ( breakDownMap [ optStatus ] || 0 ) + 1 ;
60+ } ) ;
61+ const breakDown = Object . entries ( breakDownMap ) . map ( ( [ optStatus , count ] ) => ( {
62+ optStatus,
63+ count,
64+ percentage : Number ( ( ( count / totalCandidates ) * 100 ) . toFixed ( 2 ) ) ,
65+ } ) ) ;
66+ return { totalCandidates, breakDown } ;
67+ } ;
5268
53- if ( startDate && endDate && start . toDateString ( ) === end . toDateString ( ) ) {
54- return res . status ( 400 ) . json ( {
55- message : 'startDate and endDate cannot be the same.' ,
56- } ) ;
57- }
69+ module . exports = function optAnalyticsController ( ) {
70+ return {
71+ getOPTStatusBreakdown : async function getOPTStatusBreakdown ( req , res , next ) {
72+ try {
73+ const { startDate , endDate , role } = req . query ;
5874
59- if ( start || end ) {
60- query . applicationDate = { } ;
61- if ( start ) query . applicationDate . $gte = start ;
62- if ( end ) query . applicationDate . $lte = end ;
63- }
75+ let start ;
76+ let end ;
6477
65- if ( role ) {
66- query . role = role ;
67- }
78+ if ( startDate ) {
79+ start = parseDateParam ( startDate , 'startDate' , res ) ;
80+ if ( start === null ) return null ;
81+ }
6882
69- const result = await CandidateOPTStatus . find ( query ) ;
83+ if ( endDate ) {
84+ end = parseDateParam ( endDate , 'endDate' , res ) ;
85+ if ( end === null ) return null ;
86+ }
7087
71- if ( ! result . length ) {
72- return res . json ( {
73- totalCandidates : 0 ,
74- breakDown : [ ] ,
75- message : 'No records found for the given filters.' ,
76- } ) ;
77- }
88+ if ( ! validateDateRange ( start , end , startDate , endDate , res ) ) return null ;
7889
79- const totalCandidates = result . length ;
80- const breakDownMap = { } ;
90+ const query = buildQuery ( start , end , role ) ;
8191
82- result . forEach ( ( { optStatus } ) => {
83- breakDownMap [ optStatus ] = ( breakDownMap [ optStatus ] || 0 ) + 1 ;
84- } ) ;
92+ const result = await CandidateOPTStatus . find ( query ) ;
8593
86- const breakDown = Object . entries ( breakDownMap ) . map ( ( [ optStatus , count ] ) => ( {
87- optStatus,
88- count,
89- percentage : Number ( ( ( count / totalCandidates ) * 100 ) . toFixed ( 2 ) ) ,
90- } ) ) ;
94+ if ( ! result . length ) {
95+ return res . json ( {
96+ totalCandidates : 0 ,
97+ breakDown : [ ] ,
98+ message : 'No records found for the given filters.' ,
99+ } ) ;
100+ }
91101
92- return res . json ( {
93- totalCandidates,
94- breakDown,
95- } ) ;
96- } catch ( err ) {
97- next ( err ) ;
98- }
99- } ,
100- } ) ;
102+ return res . json ( computeBreakdown ( result ) ) ;
103+ } catch ( err ) {
104+ next ( err ) ;
105+ }
106+ return null ;
107+ } ,
108+ } ;
109+ } ;
0 commit comments