tango-app-api-infra 3.9.5-vms.76 → 3.9.5-vms.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "tango-app-api-infra",
3
- "version": "3.9.5-vms.76",
3
+ "version": "3.9.5-vms.78",
4
4
  "description": "infra",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "start": "nodemon --exec \"eslint --fix . && node app.js\""
8
+ "start": "nodemon --exec \"eslint --fix . && node index.js\""
9
9
  },
10
10
  "engines": {
11
11
  "node": ">=18.10.0"
@@ -86,6 +86,10 @@ export async function createinternalTicket( req, res ) {
86
86
  mappingInfo: [],
87
87
  };
88
88
  const id = `${inputData.storeId}_${inputData.dateString}_internal_footfall-directory-tagging`;
89
+ let getExistingOne = await getOpenSearchById( openSearch.footfallDirectory, id );
90
+ if ( getExistingOne?.body?._source ) {
91
+ return res.sendError( 'Ticket Already Exists', 500 );
92
+ }
89
93
  const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
90
94
  if ( insertResult && insertResult.statusCode === 201 ) {
91
95
  return res.sendSuccess( 'Ticket raised successfully' );
@@ -380,7 +384,8 @@ export async function tangoReviewTicket( req, res ) {
380
384
  // return;
381
385
 
382
386
  let id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
383
- if ( inputData.ticketType === 'internal' ) {
387
+ let getExistingOne = await getOpenSearchById( openSearch.footfallDirectory, id );
388
+ if ( inputData.ticketType === 'internal' &&!getExistingOne?.body?._source ) {
384
389
  id = `${inputData.storeId}_${inputData.dateString}_internal_footfall-directory-tagging`;
385
390
  }
386
391
 
@@ -718,20 +723,858 @@ export async function ticketSummary1( req, res ) {
718
723
 
719
724
  export async function ticketSummary( req, res ) {
720
725
  try {
726
+ const inputData = req.query;
727
+
721
728
  let result = '';
729
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
722
730
  const userInfo = req.user;
723
731
  const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
732
+ const ticketsApproveFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
733
+
724
734
  // const ticketsApproveFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
725
- if ( req.user.userType == 'tango' ) {
735
+ if ( req?.user?.userType === 'tango' ) {
736
+ switch ( inputData?.tangoType ) {
737
+ case 'store':
738
+ const storeQuery = {
739
+ size: 0,
740
+ query: {
741
+ bool: {
742
+ must: [
743
+ {
744
+ 'range': {
745
+ 'dateString': {
746
+ 'gte': inputData?.fromDate,
747
+ 'lte': inputData?.toDate,
748
+ 'format': 'yyyy-MM-dd',
749
+ },
750
+ },
751
+ },
752
+ {
753
+ nested: {
754
+ path: 'mappingInfo',
755
+ query: {
756
+ bool: {
757
+ must: [
758
+ {
759
+ term: {
760
+ 'mappingInfo.type': 'tangoreview',
761
+ },
762
+ },
763
+ ],
764
+ },
765
+ },
766
+ },
767
+ },
768
+
769
+ ],
770
+ },
771
+ },
772
+ };
773
+
774
+ // Helper function to clone deep and replace mappingInfo.status for openTickets/closed/etc
775
+ function buildStoreQueryWithStatus( baseQuery, statusValue ) {
776
+ let q = JSON.parse( JSON.stringify( baseQuery ) );
777
+ // Remove any previous mappingInfo.status term
778
+ let nested = q.query.bool.must.find( ( m ) => m.nested );
779
+ if ( nested ) {
780
+ // filter out all mappingInfo.status
781
+ nested.nested.query.bool.must = nested?.nested?.query?.bool?.must.filter( ( mustItem ) => {
782
+ return !( mustItem.term && mustItem.term['mappingInfo.status'] );
783
+ } );
784
+ // add desired status
785
+ nested.nested.query.bool.must.push( {
786
+ term: {
787
+ 'mappingInfo.status': statusValue,
788
+ },
789
+ } );
790
+ }
791
+ return q;
792
+ }
793
+
794
+ const buildAggStoreQuery = ( baseQuery, filters = [] ) => {
795
+ const q = JSON.parse( JSON.stringify( baseQuery ) );
796
+
797
+ // locate nested section
798
+ const nested = q.query.bool.must.find( ( m ) => m.nested );
799
+
800
+ if ( nested ) {
801
+ // remove old status filters
802
+ nested.nested.query.bool.must =
803
+ nested.nested.query.bool.must.filter( ( m ) => !( m.term && m.term['mappingInfo.status'] ) );
804
+
805
+ // add new filters
806
+ nested.nested.query.bool.must.push( ...filters );
807
+ }
808
+
809
+ return {
810
+ ...q,
811
+ size: 0,
812
+ aggs: {
813
+ avg_value: {
814
+ avg: {
815
+ script: {
816
+ lang: 'painless',
817
+ source: `
818
+ if (doc['revicedPerc.keyword'].size() == 0) return null;
819
+ String v = doc['revicedPerc.keyword'].value.replace('%','');
820
+ try {
821
+ return Double.parseDouble(v);
822
+ } catch (Exception e) {
823
+ return null;
824
+ }
825
+ `,
826
+ },
827
+ },
828
+ },
829
+ },
830
+ };
831
+ };
832
+
833
+
834
+ // Get OpenSearch connection
835
+
836
+ const baseStoreQuery = JSON.parse( JSON.stringify( storeQuery ) );
837
+
838
+ // Total Tickets (all tickets with mappingInfo.type == tangoreview)
839
+ let totalTickets = 0;
840
+
841
+ let allQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
842
+
843
+ allQuery.size = 0;
844
+ const totalResp = await getOpenSearchData( openSearch.footfallDirectory, allQuery );
845
+ totalTickets = totalResp?.body?.hits?.total?.value || 0;
846
+
847
+ // openTickets: mappingInfo.status: 'Open'
848
+ let openTickets = 0;
849
+
850
+ let otQ = buildStoreQueryWithStatus( baseStoreQuery, 'Open' );
851
+ otQ.size = 0;
852
+ const openResp = await getOpenSearchData( openSearch.footfallDirectory, otQ );
853
+ openTickets = openResp?.body?.hits?.total?.value || 0;
854
+ // logger.info( { msd: '..............2', openResp } );
855
+
856
+
857
+ // openInfraIssues: mappingInfo.status: 'Open Accuracy Issue'
858
+ let openInfraIssues = 0;
859
+
860
+ let oiQ = buildStoreQueryWithStatus( baseStoreQuery, 'Open Accuracy Issue' );
861
+ oiQ.size = 0;
862
+ const infraResp = await getOpenSearchData( openSearch.footfallDirectory, oiQ );
863
+ openInfraIssues = infraResp?.body?.hits?.total?.value || 0;
864
+
865
+ // inprogress: mappingInfo.status: 'in-Progress'
866
+ let inprogress = 0;
867
+
868
+ let ipQ = buildStoreQueryWithStatus( baseStoreQuery, 'In-Progress' );
869
+ ipQ.size = 0;
870
+ const ipResp = await getOpenSearchData( openSearch.footfallDirectory, ipQ );
871
+ inprogress = ipResp?.body?.hits?.total?.value || 0;
872
+
873
+ // closedTickets: mappingInfo.status: 'closed'
874
+ let closedTickets = 0;
875
+
876
+ let clQ = buildStoreQueryWithStatus( baseStoreQuery, 'Closed' );
877
+ clQ.size = 0;
878
+ const clResp = await getOpenSearchData( openSearch.footfallDirectory, clQ );
879
+ closedTickets = clResp?.body?.hits?.total?.value || 0;
880
+ // Average revisedPerc (for all tangoreview)
881
+ let averageAccuracyOverAll = 0;
882
+
883
+ let avgQ = buildAggStoreQuery( baseStoreQuery );
884
+ const avgResp = await getOpenSearchData( openSearch.footfallDirectory, avgQ );
885
+ averageAccuracyOverAll = avgResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
886
+
887
+ // ticketAccuracyAbove: avg of revicedPerc > 85%
888
+ let ticketAccuracyAbove = 0;
889
+
890
+
891
+ // For this, add a filter on revicedPerc >= 85
892
+ let aboveQ = buildAggStoreQuery( baseStoreQuery, [
893
+ {
894
+ script: {
895
+ script: {
896
+ lang: 'painless',
897
+ source: `
898
+ doc['revicedPerc.keyword'].size()!=0 &&
899
+ Integer.parseInt(doc['revicedPerc.keyword'].value.replace('%','')) >= params.num
900
+ `,
901
+ params: { num: 85 },
902
+ },
903
+ },
904
+ },
905
+
906
+
907
+ ] );
908
+ const aboveResp = await getOpenSearchData( openSearch.footfallDirectory, aboveQ );
909
+ ticketAccuracyAbove = aboveResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
910
+
911
+ // ticketAccuracyBelow: avg of revicedPerc < 85%
912
+ let ticketAccuracyBelow = 0;
913
+
914
+ let belowQ = buildAggStoreQuery( baseStoreQuery, [
915
+ {
916
+ script: {
917
+ script: {
918
+ lang: 'painless',
919
+ source: `
920
+ doc['revicedPerc.keyword'].size()!=0 &&
921
+ Integer.parseInt(doc['revicedPerc.keyword'].value.replace('%','')) < params.num
922
+ `,
923
+ params: { num: 85 },
924
+ },
925
+ },
926
+ },
927
+
928
+ ] );
929
+ const belowResp = await getOpenSearchData( openSearch.footfallDirectory, belowQ );
930
+ ticketAccuracyBelow = belowResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
931
+
932
+ // Final result object
933
+ result = {
934
+ totalTickets,
935
+ averageAccuracyOverAll: averageAccuracyOverAll+'%',
936
+ openTickets,
937
+ openInfraIssues,
938
+ inprogress,
939
+ closedTickets,
940
+ ticketAccuracyAbove: ticketAccuracyAbove+'%',
941
+ ticketAccuracyBelow: ticketAccuracyBelow+'%',
942
+ };
943
+ break;
944
+ case 'internal':
945
+ const internalQuery = {
946
+ size: 0,
947
+ query: {
948
+ bool: {
949
+ must: [
950
+ {
951
+ 'range': {
952
+ 'dateString': {
953
+ 'gte': inputData?.fromDate,
954
+ 'lte': inputData?.toDate,
955
+ 'format': 'yyyy-MM-dd',
956
+ },
957
+ },
958
+ },
959
+
960
+ {
961
+ nested: {
962
+ path: 'mappingInfo',
963
+ query: {
964
+ bool: {
965
+ must: [
966
+ {
967
+ terms: {
968
+ 'mappingInfo.type': [ 'tagging', 'review', 'approve', 'tangoreview' ],
969
+ },
970
+ },
971
+ ],
972
+ },
973
+ },
974
+ },
975
+ },
976
+
977
+ ],
978
+ },
979
+ },
980
+ };
981
+
982
+ // Helper function to clone deep and replace mappingInfo.status for openTickets/closed/etc
983
+ function buildInternalQueryWithStatus( baseQuery, statusValue ) {
984
+ let q = JSON.parse( JSON.stringify( baseQuery ) );
985
+ // Remove any previous mappingInfo.status term
986
+ let nested = q.query.bool.must.find( ( m ) => m.nested );
987
+ if ( nested ) {
988
+ // filter out all mappingInfo.status
989
+ nested.nested.query.bool.must = nested?.nested?.query?.bool?.must.filter( ( mustItem ) => {
990
+ return !( mustItem.term && mustItem.term['mappingInfo.status'] );
991
+ } );
992
+ // add desired status
993
+ nested.nested.query.bool.must.push( {
994
+ term: {
995
+ 'mappingInfo.status': statusValue,
996
+ },
997
+ } );
998
+ }
999
+ return q;
1000
+ }
1001
+
1002
+ const buildAggInternalQuery = ( baseQuery, filters = [] ) => {
1003
+ const q = JSON.parse( JSON.stringify( baseQuery ) );
1004
+
1005
+ // locate nested section
1006
+ const nested = q.query.bool.must.find( ( m ) => m.nested );
1007
+
1008
+ if ( nested ) {
1009
+ // remove old status filters
1010
+ nested.nested.query.bool.must =
1011
+ nested.nested.query.bool.must.filter( ( m ) => !( m.term && m.term['mappingInfo.status'] ) );
1012
+
1013
+ // add new filters
1014
+ nested.nested.query.bool.must.push( ...filters );
1015
+ }
1016
+
1017
+ return {
1018
+ ...q,
1019
+ size: 0,
1020
+ aggs: {
1021
+ avg_value: {
1022
+ avg: {
1023
+ script: {
1024
+ lang: 'painless',
1025
+ source: `
1026
+ if (doc['revicedPerc.keyword'].size() == 0) return null;
1027
+ String v = doc['revicedPerc.keyword'].value.replace('%','');
1028
+ try {
1029
+ return Double.parseDouble(v);
1030
+ } catch (Exception e) {
1031
+ return null;
1032
+ }
1033
+ `,
1034
+ },
1035
+ },
1036
+ },
1037
+ },
1038
+ };
1039
+ };
1040
+
1041
+
1042
+ // Get OpenSearch connection
1043
+
1044
+
1045
+ const baseInternalQuery = JSON.parse( JSON.stringify( internalQuery ) );
1046
+
1047
+ // Total Tickets (all tickets with mappingInfo.type == tangoreview)
1048
+ let totalInternalTickets = 0;
1049
+
1050
+ let allInternalQuery = JSON.parse( JSON.stringify( baseInternalQuery ) );
1051
+
1052
+ allInternalQuery.size = 0;
1053
+ const totalInternalResp = await getOpenSearchData( openSearch.footfallDirectory, allInternalQuery );
1054
+ totalInternalTickets = totalInternalResp?.body?.hits?.total?.value || 0;
1055
+
1056
+ // openTickets: mappingInfo.status: 'Open'
1057
+ let openInternalTickets = 0;
1058
+
1059
+ let otQInternal = buildInternalQueryWithStatus( baseInternalQuery, 'Open' );
1060
+ otQInternal.size = 0;
1061
+ const openInternalResp = await getOpenSearchData( openSearch.footfallDirectory, otQInternal );
1062
+ openInternalTickets = openInternalResp?.body?.hits?.total?.value || 0;
1063
+ // logger.info( { msd: '..............2', openResp } );
1064
+
1065
+
1066
+ // openInfraIssues: mappingInfo.status: 'Open Accuracy Issue'
1067
+ let openInternalInfraIssues = 0;
1068
+
1069
+ let oiQinternal = buildInternalQueryWithStatus( baseInternalQuery, 'Open Accuracy Issue' );
1070
+ oiQinternal.size = 0;
1071
+ const infraInternalResp = await getOpenSearchData( openSearch.footfallDirectory, oiQinternal );
1072
+ openInternalInfraIssues = infraInternalResp?.body?.hits?.total?.value || 0;
1073
+
1074
+ // inprogress: mappingInfo.status: 'in-Progress'
1075
+ let inprogressIntrenal = 0;
1076
+
1077
+ let ipQInternal = buildInternalQueryWithStatus( baseInternalQuery, 'In-Progress' );
1078
+ ipQInternal.size = 0;
1079
+ const ipInternalResp = await getOpenSearchData( openSearch.footfallDirectory, ipQInternal );
1080
+ inprogressIntrenal = ipInternalResp?.body?.hits?.total?.value || 0;
1081
+
1082
+ // closedTickets: mappingInfo.status: 'closed'
1083
+ let closedInternalTickets = 0;
1084
+
1085
+ let clQInternal = buildInternalQueryWithStatus( baseInternalQuery, 'Closed' );
1086
+ clQInternal.size = 0;
1087
+ const clInternalResp = await getOpenSearchData( openSearch.footfallDirectory, clQInternal );
1088
+ closedInternalTickets = clInternalResp?.body?.hits?.total?.value || 0;
1089
+ // Average revisedPerc (for all tangoreview)
1090
+ let internalAverageAccuracyOverAll = 0;
1091
+
1092
+ let avgQInternal = buildAggInternalQuery( baseInternalQuery );
1093
+ const avgInternalResp = await getOpenSearchData( openSearch.footfallDirectory, avgQInternal );
1094
+ internalAverageAccuracyOverAll = avgInternalResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1095
+
1096
+ // ticketAccuracyAbove: avg of revicedPerc > 85%
1097
+ let internalTicketAccuracyAbove = 0;
1098
+
1099
+
1100
+ // For this, add a filter on revicedPerc >= 85
1101
+ let aboveQinternal = buildAggInternalQuery( baseInternalQuery, [
1102
+ {
1103
+ script: {
1104
+ script: {
1105
+ lang: 'painless',
1106
+ source: `
1107
+ doc['revicedPerc.keyword'].size()!=0 &&
1108
+ Integer.parseInt(doc['revicedPerc.keyword'].value.replace('%','')) >= params.num
1109
+ `,
1110
+ params: { num: 85 },
1111
+ },
1112
+ },
1113
+ },
1114
+
1115
+
1116
+ ] );
1117
+ const aboveRespInternal = await getOpenSearchData( openSearch.footfallDirectory, aboveQinternal );
1118
+ internalTicketAccuracyAbove = aboveRespInternal?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1119
+
1120
+ // ticketAccuracyBelow: avg of revicedPerc < 85%
1121
+ let internalTicketAccuracyBelow = 0;
1122
+
1123
+ let belowQIneranl = buildAggInternalQuery( baseInternalQuery, [
1124
+ {
1125
+ script: {
1126
+ script: {
1127
+ lang: 'painless',
1128
+ source: `
1129
+ doc['revicedPerc.keyword'].size()!=0 &&
1130
+ Integer.parseInt(doc['revicedPerc.keyword'].value.replace('%','')) < params.num
1131
+ `,
1132
+ params: { num: 85 },
1133
+ },
1134
+ },
1135
+ },
1136
+
1137
+ ] );
1138
+ const belowRespInternal = await getOpenSearchData( openSearch.footfallDirectory, belowQIneranl );
1139
+ internalTicketAccuracyBelow = belowRespInternal?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1140
+
1141
+ // Final result object
1142
+ result = {
1143
+ totalTickets: totalInternalTickets,
1144
+ averageAccuracyOverAll: internalAverageAccuracyOverAll+'%',
1145
+ openTickets: openInternalTickets,
1146
+ openInfraIssues: openInternalInfraIssues,
1147
+ inprogress: inprogressIntrenal,
1148
+ closedTickets: closedInternalTickets,
1149
+ ticketAccuracyAbove: internalTicketAccuracyAbove+'%',
1150
+ ticketAccuracyBelow: internalTicketAccuracyBelow+'%',
1151
+ };
1152
+ break;
1153
+ default: '';
1154
+ }
1155
+ // result = {
1156
+ // totalTickets: 0,
1157
+ // averageAccuracyOverAll: 0,
1158
+ // openTickets: 0,
1159
+ // openInfraIssues: 0,
1160
+ // inprogress: 0,
1161
+ // closedTickets: 0,
1162
+ // ticketAccuracyAbove: '0%',
1163
+ // ticketAccuracyBelow: '0%',
1164
+ // };
1165
+ } else if ( req?.user?.userType === 'client' ) {
1166
+ if ( ticketsFeature && !ticketsApproveFeature ) {
1167
+ const storeQuery = {
1168
+ size: 0,
1169
+ query: {
1170
+ bool: {
1171
+ must: [
1172
+ {
1173
+ 'range': {
1174
+ 'dateString': {
1175
+ 'gte': inputData?.fromDate,
1176
+ 'lte': inputData?.toDate,
1177
+ 'format': 'yyyy-MM-dd',
1178
+ },
1179
+ },
1180
+ },
1181
+ {
1182
+ nested: {
1183
+ path: 'mappingInfo',
1184
+ query: {
1185
+ bool: {
1186
+ must: [
1187
+ {
1188
+ term: {
1189
+ 'mappingInfo.type': 'review',
1190
+ },
1191
+ },
1192
+ ],
1193
+ },
1194
+ },
1195
+ },
1196
+ },
1197
+
1198
+ ],
1199
+ },
1200
+ },
1201
+ };
1202
+
1203
+ // Helper function to clone deep and replace mappingInfo.status for openTickets/closed/etc
1204
+ function buildStoreQueryWithStatus( baseQuery, statusValue ) {
1205
+ let q = JSON.parse( JSON.stringify( baseQuery ) );
1206
+ // Remove any previous mappingInfo.status term
1207
+ let nested = q.query.bool.must.find( ( m ) => m.nested );
1208
+ if ( nested ) {
1209
+ // filter out all mappingInfo.status
1210
+ nested.nested.query.bool.must = nested?.nested?.query?.bool?.must.filter( ( mustItem ) => {
1211
+ return !( mustItem.term && mustItem.term['mappingInfo.status'] );
1212
+ } );
1213
+ // add desired status
1214
+ nested.nested.query.bool.must.push( {
1215
+ term: {
1216
+ 'mappingInfo.status': statusValue,
1217
+ },
1218
+ } );
1219
+ }
1220
+ return q;
1221
+ }
1222
+
1223
+ const buildAggStoreQuery = ( baseQuery, filters = [] ) => {
1224
+ const q = JSON.parse( JSON.stringify( baseQuery ) );
1225
+
1226
+ // locate nested section
1227
+ const nested = q.query.bool.must.find( ( m ) => m.nested );
1228
+
1229
+ if ( nested ) {
1230
+ // remove old status filters
1231
+ nested.nested.query.bool.must =
1232
+ nested.nested.query.bool.must.filter( ( m ) => !( m.term && m.term['mappingInfo.status'] ) );
1233
+
1234
+ // add new filters
1235
+ nested.nested.query.bool.must.push( ...filters );
1236
+ }
1237
+
1238
+ return {
1239
+ ...q,
1240
+ size: 0,
1241
+ aggs: {
1242
+ avg_value: {
1243
+ avg: {
1244
+ script: {
1245
+ lang: 'painless',
1246
+ source: `
1247
+ if (doc['revicedPerc.keyword'].size() == 0) return null;
1248
+ String v = doc['revicedPerc.keyword'].value.replace('%','');
1249
+ try {
1250
+ return Double.parseDouble(v);
1251
+ } catch (Exception e) {
1252
+ return null;
1253
+ }
1254
+ `,
1255
+ },
1256
+ },
1257
+ },
1258
+ },
1259
+ };
1260
+ };
1261
+
1262
+
1263
+ // Get OpenSearch connection
1264
+
1265
+ const baseStoreQuery = JSON.parse( JSON.stringify( storeQuery ) );
1266
+
1267
+ // Total Tickets (all tickets with mappingInfo.type == tangoreview)
1268
+ let totalTickets = 0;
1269
+
1270
+ let allQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
1271
+
1272
+ allQuery.size = 0;
1273
+ const totalResp = await getOpenSearchData( openSearch.footfallDirectory, allQuery );
1274
+ totalTickets = totalResp?.body?.hits?.total?.value || 0;
1275
+
1276
+ // openTickets: mappingInfo.status: 'Open'
1277
+ let openTickets = 0;
1278
+
1279
+ let otQ = buildStoreQueryWithStatus( baseStoreQuery, 'Open' );
1280
+ otQ.size = 0;
1281
+ const openResp = await getOpenSearchData( openSearch.footfallDirectory, otQ );
1282
+ openTickets = openResp?.body?.hits?.total?.value || 0;
1283
+ // logger.info( { msd: '..............2', openResp } );
1284
+
1285
+
1286
+ // openInfraIssues: mappingInfo.status: 'Open Accuracy Issue'
1287
+ let openInfraIssues = 0;
1288
+
1289
+ let oiQ = buildStoreQueryWithStatus( baseStoreQuery, 'Open Accuracy Issue' );
1290
+ oiQ.size = 0;
1291
+ const infraResp = await getOpenSearchData( openSearch.footfallDirectory, oiQ );
1292
+ openInfraIssues = infraResp?.body?.hits?.total?.value || 0;
1293
+
1294
+ // inprogress: mappingInfo.status: 'in-Progress'
1295
+ let inprogress = 0;
1296
+
1297
+ let ipQ = buildStoreQueryWithStatus( baseStoreQuery, 'In-Progress' );
1298
+ ipQ.size = 0;
1299
+ const ipResp = await getOpenSearchData( openSearch.footfallDirectory, ipQ );
1300
+ inprogress = ipResp?.body?.hits?.total?.value || 0;
1301
+
1302
+ // closedTickets: mappingInfo.status: 'closed'
1303
+ let closedTickets = 0;
1304
+
1305
+ let clQ = buildStoreQueryWithStatus( baseStoreQuery, 'Closed' );
1306
+ clQ.size = 0;
1307
+ const clResp = await getOpenSearchData( openSearch.footfallDirectory, clQ );
1308
+ closedTickets = clResp?.body?.hits?.total?.value || 0;
1309
+ // Average revisedPerc (for all tangoreview)
1310
+ let averageAccuracyOverAll = 0;
1311
+
1312
+ let avgQ = buildAggStoreQuery( baseStoreQuery );
1313
+ const avgResp = await getOpenSearchData( openSearch.footfallDirectory, avgQ );
1314
+ averageAccuracyOverAll = avgResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1315
+
1316
+ // ticketAccuracyAbove: avg of revicedPerc > 85%
1317
+ let ticketAccuracyAbove = 0;
1318
+
1319
+
1320
+ // For this, add a filter on revicedPerc >= 85
1321
+ let aboveQ = buildAggStoreQuery( baseStoreQuery, [
1322
+ {
1323
+ script: {
1324
+ script: {
1325
+ lang: 'painless',
1326
+ source: `
1327
+ doc['revicedPerc.keyword'].size()!=0 &&
1328
+ Integer.parseInt(doc['revicedPerc.keyword'].value.replace('%','')) >= params.num
1329
+ `,
1330
+ params: { num: 85 },
1331
+ },
1332
+ },
1333
+ },
1334
+
1335
+
1336
+ ] );
1337
+ const aboveResp = await getOpenSearchData( openSearch.footfallDirectory, aboveQ );
1338
+ ticketAccuracyAbove = aboveResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1339
+
1340
+ // ticketAccuracyBelow: avg of revicedPerc < 85%
1341
+ let ticketAccuracyBelow = 0;
1342
+
1343
+ let belowQ = buildAggStoreQuery( baseStoreQuery, [
1344
+ {
1345
+ script: {
1346
+ script: {
1347
+ lang: 'painless',
1348
+ source: `
1349
+ doc['revicedPerc.keyword'].size()!=0 &&
1350
+ Integer.parseInt(doc['revicedPerc.keyword'].value.replace('%','')) < params.num
1351
+ `,
1352
+ params: { num: 85 },
1353
+ },
1354
+ },
1355
+ },
1356
+
1357
+ ] );
1358
+ const belowResp = await getOpenSearchData( openSearch.footfallDirectory, belowQ );
1359
+ ticketAccuracyBelow = belowResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1360
+
1361
+ // Final result object
1362
+ result = {
1363
+ totalTickets,
1364
+ averageAccuracyOverAll: averageAccuracyOverAll+'%',
1365
+ openTickets,
1366
+ openInfraIssues,
1367
+ inprogress,
1368
+ closedTickets,
1369
+ ticketAccuracyAbove: ticketAccuracyAbove+'%',
1370
+ ticketAccuracyBelow: ticketAccuracyBelow+'%',
1371
+ };
1372
+ }
1373
+ } else if ( ticketsFeature && !ticketsApproveFeature ) {
1374
+ const storeQuery = {
1375
+ size: 0,
1376
+ query: {
1377
+ bool: {
1378
+ must: [
1379
+ {
1380
+ 'range': {
1381
+ 'dateString': {
1382
+ 'gte': inputData?.fromDate,
1383
+ 'lte': inputData?.toDate,
1384
+ 'format': 'yyyy-MM-dd',
1385
+ },
1386
+ },
1387
+ },
1388
+ {
1389
+ nested: {
1390
+ path: 'mappingInfo',
1391
+ query: {
1392
+ bool: {
1393
+ must: [
1394
+ {
1395
+ term: {
1396
+ 'mappingInfo.type': inputData.permissionType === 'review'? 'review':'approve',
1397
+ },
1398
+ },
1399
+ ],
1400
+ },
1401
+ },
1402
+ },
1403
+ },
1404
+
1405
+ ],
1406
+ },
1407
+ },
1408
+ };
1409
+
1410
+ // Helper function to clone deep and replace mappingInfo.status for openTickets/closed/etc
1411
+ function buildStoreQueryWithStatus( baseQuery, statusValue ) {
1412
+ let q = JSON.parse( JSON.stringify( baseQuery ) );
1413
+ // Remove any previous mappingInfo.status term
1414
+ let nested = q.query.bool.must.find( ( m ) => m.nested );
1415
+ if ( nested ) {
1416
+ // filter out all mappingInfo.status
1417
+ nested.nested.query.bool.must = nested?.nested?.query?.bool?.must.filter( ( mustItem ) => {
1418
+ return !( mustItem.term && mustItem.term['mappingInfo.status'] );
1419
+ } );
1420
+ // add desired status
1421
+ nested.nested.query.bool.must.push( {
1422
+ term: {
1423
+ 'mappingInfo.status': statusValue,
1424
+ },
1425
+ } );
1426
+ }
1427
+ return q;
1428
+ }
1429
+
1430
+ const buildAggStoreQuery = ( baseQuery, filters = [] ) => {
1431
+ const q = JSON.parse( JSON.stringify( baseQuery ) );
1432
+
1433
+ // locate nested section
1434
+ const nested = q.query.bool.must.find( ( m ) => m.nested );
1435
+
1436
+ if ( nested ) {
1437
+ // remove old status filters
1438
+ nested.nested.query.bool.must =
1439
+ nested.nested.query.bool.must.filter( ( m ) => !( m.term && m.term['mappingInfo.status'] ) );
1440
+
1441
+ // add new filters
1442
+ nested.nested.query.bool.must.push( ...filters );
1443
+ }
1444
+
1445
+ return {
1446
+ ...q,
1447
+ size: 0,
1448
+ aggs: {
1449
+ avg_value: {
1450
+ avg: {
1451
+ script: {
1452
+ lang: 'painless',
1453
+ source: `
1454
+ if (doc['revicedPerc.keyword'].size() == 0) return null;
1455
+ String v = doc['revicedPerc.keyword'].value.replace('%','');
1456
+ try {
1457
+ return Double.parseDouble(v);
1458
+ } catch (Exception e) {
1459
+ return null;
1460
+ }
1461
+ `,
1462
+ },
1463
+ },
1464
+ },
1465
+ },
1466
+ };
1467
+ };
1468
+
1469
+
1470
+ // Get OpenSearch connection
1471
+
1472
+ const baseStoreQuery = JSON.parse( JSON.stringify( storeQuery ) );
1473
+
1474
+ // Total Tickets (all tickets with mappingInfo.type == tangoreview)
1475
+ let totalTickets = 0;
1476
+
1477
+ let allQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
1478
+
1479
+ allQuery.size = 0;
1480
+ const totalResp = await getOpenSearchData( openSearch.footfallDirectory, allQuery );
1481
+ totalTickets = totalResp?.body?.hits?.total?.value || 0;
1482
+
1483
+ // openTickets: mappingInfo.status: 'Open'
1484
+ let openTickets = 0;
1485
+
1486
+ let otQ = buildStoreQueryWithStatus( baseStoreQuery, 'Open' );
1487
+ otQ.size = 0;
1488
+ const openResp = await getOpenSearchData( openSearch.footfallDirectory, otQ );
1489
+ openTickets = openResp?.body?.hits?.total?.value || 0;
1490
+ // logger.info( { msd: '..............2', openResp } );
1491
+
1492
+
1493
+ // openInfraIssues: mappingInfo.status: 'Open Accuracy Issue'
1494
+ let openInfraIssues = 0;
1495
+
1496
+ let oiQ = buildStoreQueryWithStatus( baseStoreQuery, 'Open Accuracy Issue' );
1497
+ oiQ.size = 0;
1498
+ const infraResp = await getOpenSearchData( openSearch.footfallDirectory, oiQ );
1499
+ openInfraIssues = infraResp?.body?.hits?.total?.value || 0;
1500
+
1501
+ // inprogress: mappingInfo.status: 'in-Progress'
1502
+ let inprogress = 0;
1503
+
1504
+ let ipQ = buildStoreQueryWithStatus( baseStoreQuery, 'In-Progress' );
1505
+ ipQ.size = 0;
1506
+ const ipResp = await getOpenSearchData( openSearch.footfallDirectory, ipQ );
1507
+ inprogress = ipResp?.body?.hits?.total?.value || 0;
1508
+
1509
+ // closedTickets: mappingInfo.status: 'closed'
1510
+ let closedTickets = 0;
1511
+
1512
+ let clQ = buildStoreQueryWithStatus( baseStoreQuery, 'Closed' );
1513
+ clQ.size = 0;
1514
+ const clResp = await getOpenSearchData( openSearch.footfallDirectory, clQ );
1515
+ closedTickets = clResp?.body?.hits?.total?.value || 0;
1516
+ // Average revisedPerc (for all tangoreview)
1517
+ let averageAccuracyOverAll = 0;
1518
+
1519
+ let avgQ = buildAggStoreQuery( baseStoreQuery );
1520
+ const avgResp = await getOpenSearchData( openSearch.footfallDirectory, avgQ );
1521
+ averageAccuracyOverAll = avgResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1522
+
1523
+ // ticketAccuracyAbove: avg of revicedPerc > 85%
1524
+ let ticketAccuracyAbove = 0;
1525
+
1526
+
1527
+ // For this, add a filter on revicedPerc >= 85
1528
+ let aboveQ = buildAggStoreQuery( baseStoreQuery, [
1529
+ {
1530
+ script: {
1531
+ script: {
1532
+ lang: 'painless',
1533
+ source: `
1534
+ doc['revicedPerc.keyword'].size()!=0 &&
1535
+ Integer.parseInt(doc['revicedPerc.keyword'].value.replace('%','')) >= params.num
1536
+ `,
1537
+ params: { num: 85 },
1538
+ },
1539
+ },
1540
+ },
1541
+
1542
+
1543
+ ] );
1544
+ const aboveResp = await getOpenSearchData( openSearch.footfallDirectory, aboveQ );
1545
+ ticketAccuracyAbove = aboveResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1546
+
1547
+ // ticketAccuracyBelow: avg of revicedPerc < 85%
1548
+ let ticketAccuracyBelow = 0;
1549
+
1550
+ let belowQ = buildAggStoreQuery( baseStoreQuery, [
1551
+ {
1552
+ script: {
1553
+ script: {
1554
+ lang: 'painless',
1555
+ source: `
1556
+ doc['revicedPerc.keyword'].size()!=0 &&
1557
+ Integer.parseInt(doc['revicedPerc.keyword'].value.replace('%','')) < params.num
1558
+ `,
1559
+ params: { num: 85 },
1560
+ },
1561
+ },
1562
+ },
1563
+
1564
+ ] );
1565
+ const belowResp = await getOpenSearchData( openSearch.footfallDirectory, belowQ );
1566
+ ticketAccuracyBelow = belowResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1567
+
1568
+ // Final result object
726
1569
  result = {
727
- totalTickets: 0,
728
- averageAccuracyOverAll: 0,
729
- openTickets: 0,
730
- openInfraIssues: 0,
731
- inprogress: 0,
732
- closedTickets: 0,
733
- ticketAccuracyAbove: '0%',
734
- ticketAccuracyBelow: '0%',
1570
+ totalTickets,
1571
+ averageAccuracyOverAll: averageAccuracyOverAll+'%',
1572
+ openTickets,
1573
+ openInfraIssues,
1574
+ inprogress,
1575
+ closedTickets,
1576
+ ticketAccuracyAbove: ticketAccuracyAbove+'%',
1577
+ ticketAccuracyBelow: ticketAccuracyBelow+'%',
735
1578
  };
736
1579
  } else {
737
1580
  result = req.user.role === 'superadmin' ?
@@ -1317,6 +2160,87 @@ export async function ticketList( req, res ) {
1317
2160
  }
1318
2161
  }
1319
2162
 
2163
+ if ( inputData?.filterByStore && inputData?.filterByStore !== '' ) {
2164
+ let percQuery = null;
2165
+ const value = inputData.filterByStore;
2166
+
2167
+ // Helper function: remove trailing '%' and convert to number
2168
+ const percValue = ( val ) => {
2169
+ if ( typeof val === 'string' ) {
2170
+ return parseFloat( val.replace( '%', '' ).trim() );
2171
+ }
2172
+ return parseFloat( val );
2173
+ };
2174
+
2175
+ // Example filter values: "<90", "<=90", ">=90", "50 to 90"
2176
+ if ( /^<=?\d+$/.test( value ) ) {
2177
+ // "<90" or "<=90"
2178
+ const num = percValue( value.replace( /[^\d]/g, '' ) );
2179
+ const op = value.includes( '=' ) ? 'lte' : 'lt';
2180
+ percQuery = {
2181
+ script: {
2182
+ script: {
2183
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) ${op === 'lt' ? '<' : '<='} params.num`,
2184
+ params: { num },
2185
+ },
2186
+ },
2187
+ };
2188
+ } else if ( /^>=?\d+$/.test( value ) ) {
2189
+ // ">=90"
2190
+ const num = percValue( value.replace( /[^\d]/g, '' ) );
2191
+ const op = value.includes( '=' ) ? 'gte' : 'gt';
2192
+ percQuery = {
2193
+ script: {
2194
+ script: {
2195
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) ${op === 'gt' ? '>' : '>='} params.num`,
2196
+ params: { num },
2197
+ },
2198
+ },
2199
+ };
2200
+ } else if ( /^(\d+)\s*to\s*(\d+)$/.test( value ) ) {
2201
+ // "50 to 90"
2202
+ const match = value.match( /^(\d+)\s*to\s*(\d+)$/ );
2203
+ const from = percValue( match[1] );
2204
+ const to = percValue( match[2] );
2205
+ percQuery = {
2206
+ script: {
2207
+ script: {
2208
+ source:
2209
+ `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) >= params.from && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) <= params.to`,
2210
+ params: { from, to },
2211
+ },
2212
+ },
2213
+ };
2214
+ }
2215
+ // fallback: treat as exact match (e.g., "90")
2216
+ if ( !percQuery && /^\d+$/.test( value ) ) {
2217
+ percQuery = {
2218
+ script: {
2219
+ script: {
2220
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) == params.num`,
2221
+ params: { num: percValue( value ) },
2222
+ },
2223
+ },
2224
+ };
2225
+ }
2226
+
2227
+ if ( percQuery ) {
2228
+ searchQuery.query.bool.must.push( {
2229
+ nested: {
2230
+ path: 'mappingInfo',
2231
+ query: {
2232
+ bool: {
2233
+ must: [
2234
+ { term: { 'mappingInfo.type': 'tagging' } },
2235
+ percQuery,
2236
+ ],
2237
+ },
2238
+ },
2239
+ },
2240
+ } );
2241
+ }
2242
+ }
2243
+
1320
2244
  if ( inputData?.filterByReviewer && inputData?.filterByReviewer !== '' ) {
1321
2245
  let percQuery = null;
1322
2246
  const value = inputData.filterByReviewer;
@@ -1624,7 +2548,7 @@ export async function ticketList( req, res ) {
1624
2548
 
1625
2549
 
1626
2550
  );
1627
- } else if ( req?.user?.userType === 'client' && inputData.tangoType !== 'internal' ) {
2551
+ } else if ( req?.user?.userType === 'client' ) {
1628
2552
  searchQuery.query.bool.must.push(
1629
2553
  {
1630
2554
  term: {
@@ -2657,7 +3581,7 @@ export async function getTaggedStores( req, res ) {
2657
3581
  + (doc.containsKey('employeeCount') && !doc['employeeCount'].empty ? doc['employeeCount'].value : 0)
2658
3582
  + (doc.containsKey('junkCount') && !doc['junkCount'].empty ? doc['junkCount'].value : 0);
2659
3583
  `,
2660
- lang: 'painless',
3584
+ lang: 'scripting',
2661
3585
  },
2662
3586
  },
2663
3587
  },
@@ -3067,6 +3991,7 @@ export async function reviewerList( req, res ) {
3067
3991
  export async function openTicketList( req, res ) {
3068
3992
  try {
3069
3993
  const inputData = req.body;
3994
+ logger.info( { inputData } );
3070
3995
  const openSearch = JSON.parse( process.env.OPENSEARCH );
3071
3996
 
3072
3997
  // INSERT_YOUR_CODE
@@ -86,7 +86,8 @@ export const tangoReviewAccuracyClosedTicketValid = {
86
86
 
87
87
  export const ticketSummarySchema = Joi.object().keys( {
88
88
  clientId: Joi.string().required(),
89
-
89
+ tangoType: Joi.string().valid( 'store', 'internal' ).optional(),
90
+ permissionType: Joi.string().valid( 'approve', 'review' ).optional(),
90
91
  fromDate: Joi.string()
91
92
  .pattern( /^\d{4}-\d{2}-\d{2}$/, 'YYYY-MM-DD format' )
92
93
  .required()
@@ -159,6 +160,7 @@ export const ticketListSchema = Joi.object().keys( {
159
160
  tangoType: Joi.string().valid( 'store', 'internal', '' ).optional(),
160
161
  permissionType: Joi.string().valid( 'review', 'approve' ).optional(),
161
162
  filterByStatus: Joi.string().optional().allow( '' ),
163
+ filterByStore: Joi.string().optional().allow( '' ),
162
164
  filterByReviewer: Joi.string().optional().allow( '' ),
163
165
  filterByApprover: Joi.string().optional().allow( '' ),
164
166
  filterByTango: Joi.string().optional().allow( '' ),