tango-app-api-trax 3.9.39 → 3.9.41
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/index.js +2 -1
- package/package.json +1 -1
- package/src/controllers/internalTrax.controller.js +225 -94
- package/src/controllers/mobileTrax.controller.js +69 -29
- package/src/controllers/trax.controller.js +3 -2
- package/src/controllers/traxDashboard.controllers.js +69 -55
- package/src/hbs/flag.hbs +1 -1
- package/src/hbs/template.hbs +7 -0
- package/src/hbs/visit-checklist.hbs +77 -93
- package/src/logging/activityLogFlusher.js +59 -0
- package/src/logging/activityLogMiddleware.js +45 -0
- package/src/logging/activityLogStore.js +91 -0
- package/src/logging/compressBatches.js +83 -0
- package/src/logging/config.js +24 -0
- package/src/logging/createLoggableService.js +46 -0
- package/src/logging/logExternalCall.js +37 -0
- package/src/routes/internalTraxApi.router.js +1 -0
- package/src/services/app.service.js +15 -9
- package/src/services/approver.service.js +23 -15
- package/src/services/authentication.service.js +9 -3
- package/src/services/camera.service.js +19 -13
- package/src/services/checklist.service.js +35 -27
- package/src/services/checklistAssign.service.js +43 -38
- package/src/services/checklistQuestion.service.js +39 -34
- package/src/services/checklistlog.service.js +39 -34
- package/src/services/clientRequest.service.js +9 -2
- package/src/services/clients.services.js +23 -18
- package/src/services/cluster.service.js +31 -23
- package/src/services/domain.service.js +23 -18
- package/src/services/download.services.js +35 -25
- package/src/services/group.service.js +23 -17
- package/src/services/lenskartEmployeeMapping.service.js +15 -10
- package/src/services/locus.service.js +35 -28
- package/src/services/notification.service.js +35 -26
- package/src/services/otp.service.js +20 -13
- package/src/services/planogram.service.js +9 -2
- package/src/services/processedTaskConfig.service.js +35 -27
- package/src/services/processedTaskList.service.js +32 -26
- package/src/services/processedchecklist.services.js +55 -47
- package/src/services/processedchecklistconfig.services.js +39 -34
- package/src/services/recurringFlagTracker.service.js +39 -32
- package/src/services/runAIFeatures.services.js +32 -27
- package/src/services/runAIRequest.services.js +43 -38
- package/src/services/store.service.js +32 -27
- package/src/services/tagging.service.js +9 -2
- package/src/services/taskConfig.service.js +35 -27
- package/src/services/teams.service.js +35 -24
- package/src/services/ticket.service.js +15 -10
- package/src/services/user.service.js +27 -20
- package/src/services/userAssignedstores.service.js +12 -5
- package/src/utils/visitChecklistPdf.utils.js +449 -21
|
@@ -276,29 +276,10 @@ export const checklistPerformance = async ( req, res ) => {
|
|
|
276
276
|
toDate = new Date( toDate.getTime() - userTimezoneOffset );
|
|
277
277
|
toDate.setUTCHours( 23, 59, 59, 59 );
|
|
278
278
|
let result = {};
|
|
279
|
-
let checklistIdList = [];
|
|
280
279
|
|
|
281
280
|
let limit = parseInt( requestData?.limit ) || 10;
|
|
282
281
|
let skip = limit * ( requestData?.offset ) || 0;
|
|
283
282
|
|
|
284
|
-
const detectionPayload = {
|
|
285
|
-
'fromDate': requestData.fromDate,
|
|
286
|
-
'toDate': requestData.toDate,
|
|
287
|
-
'clientId': requestData.clientId,
|
|
288
|
-
'sortColumnName': requestData.sortColumnName,
|
|
289
|
-
'sortBy': requestData.sortBy,
|
|
290
|
-
'storeId': requestData.storeId,
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
let complianceURL = JSON.parse( process.env.LAMBDAURL ).complianceURL;
|
|
295
|
-
const complianceData = await LamdaServiceCall( complianceURL, detectionPayload );
|
|
296
|
-
if ( complianceData?.data?.length && requestData?.sortColumnName == 'questionCompliance' ) {
|
|
297
|
-
const end = skip + requestData?.limit;
|
|
298
|
-
checklistIdList = complianceData.data.slice( skip, end )?.map( ( ele ) => ele?.sourceCheckList_id );
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
283
|
// Get User Based Checklist //
|
|
303
284
|
// let loginUser = { clientId: requestData.clientId, role: req.user.role, userType: req.user.userType, userEmail: req.user.email };
|
|
304
285
|
// let getUserEmails = await getChecklistUsers( loginUser );
|
|
@@ -314,10 +295,6 @@ export const checklistPerformance = async ( req, res ) => {
|
|
|
314
295
|
{ $or: [ { store_id: { $in: requestData.storeId } }, { store_id: { $eq: '' }, userEmail: { $in: requestData.userEmailes } } ] },
|
|
315
296
|
);
|
|
316
297
|
|
|
317
|
-
if ( requestData?.sortColumnName == 'questionCompliance' ) {
|
|
318
|
-
findAndQuery.push( { sourceCheckList_id: { $in: checklistIdList } } );
|
|
319
|
-
}
|
|
320
|
-
|
|
321
298
|
findQuery.push( { $match: { $and: findAndQuery } } );
|
|
322
299
|
|
|
323
300
|
if ( requestData.searchValue && requestData.searchValue != '' ) {
|
|
@@ -367,6 +344,16 @@ export const checklistPerformance = async ( req, res ) => {
|
|
|
367
344
|
timeFlag: { $sum: '$timeFlag' },
|
|
368
345
|
questionFlagCount: { $sum: '$questionFlag' },
|
|
369
346
|
runAIFlagCount: { $sum: '$runAIFlag' },
|
|
347
|
+
userComplianceCountTotal: {
|
|
348
|
+
$sum: {
|
|
349
|
+
$cond: [
|
|
350
|
+
{ $eq: [ '$checklistStatus', 'submit' ] },
|
|
351
|
+
{ $ifNull: [ '$userComplianceCount', 0 ] },
|
|
352
|
+
0,
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
complianceCount: { $first: '$complianceCount' },
|
|
370
357
|
checkListType: { $last: '$checkListType' },
|
|
371
358
|
redo: { $sum: { $cond: [ { $eq: [ '$redoStatus', true ] }, 1, 0 ] } },
|
|
372
359
|
task: {
|
|
@@ -426,6 +413,41 @@ export const checklistPerformance = async ( req, res ) => {
|
|
|
426
413
|
checkListType: 1,
|
|
427
414
|
redo: 1,
|
|
428
415
|
task: 1,
|
|
416
|
+
questionCompliance: {
|
|
417
|
+
$let: {
|
|
418
|
+
vars: {
|
|
419
|
+
divisor: {
|
|
420
|
+
$cond: [
|
|
421
|
+
{ $gt: [ '$submittedChecklist', 1 ] },
|
|
422
|
+
{ $multiply: [ { $ifNull: [ '$complianceCount', 0 ] }, '$submittedChecklist' ] },
|
|
423
|
+
{ $ifNull: [ '$complianceCount', 0 ] },
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
in: {
|
|
428
|
+
$cond: [
|
|
429
|
+
{ $gt: [ '$$divisor', 0 ] },
|
|
430
|
+
{
|
|
431
|
+
$max: [
|
|
432
|
+
0,
|
|
433
|
+
{
|
|
434
|
+
$round: [
|
|
435
|
+
{
|
|
436
|
+
$multiply: [
|
|
437
|
+
{ $divide: [ '$userComplianceCountTotal', '$$divisor' ] },
|
|
438
|
+
100,
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
0,
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
0,
|
|
447
|
+
],
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
},
|
|
429
451
|
},
|
|
430
452
|
} );
|
|
431
453
|
|
|
@@ -446,18 +468,10 @@ export const checklistPerformance = async ( req, res ) => {
|
|
|
446
468
|
},
|
|
447
469
|
} );
|
|
448
470
|
let getChecklistPerformanceData = await processedchecklistService.aggregate( findQuery );
|
|
449
|
-
if ( !getChecklistPerformanceData[0]
|
|
471
|
+
if ( !getChecklistPerformanceData?.[0]?.data?.length ) {
|
|
450
472
|
return res.sendError( 'no data found', 204 );
|
|
451
473
|
}
|
|
452
474
|
|
|
453
|
-
getChecklistPerformanceData?.[0]?.data.forEach( ( ele ) => {
|
|
454
|
-
let findCompliance;
|
|
455
|
-
if ( complianceData?.data?.length ) {
|
|
456
|
-
findCompliance = complianceData?.data?.find( ( data ) => data.sourceCheckList_id == ele?.sourceCheckList_id );
|
|
457
|
-
}
|
|
458
|
-
ele['questionComplianceRate'] = findCompliance?.compliancePercentage ?? 0;
|
|
459
|
-
} );
|
|
460
|
-
|
|
461
475
|
if ( requestData.export ) {
|
|
462
476
|
const exportdata = [];
|
|
463
477
|
getChecklistPerformanceData[0].data.forEach( ( element ) => {
|
|
@@ -4335,26 +4349,26 @@ function escapeRegex( text ) {
|
|
|
4335
4349
|
// }
|
|
4336
4350
|
// }
|
|
4337
4351
|
|
|
4338
|
-
async function LamdaServiceCall( url, data ) {
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
}
|
|
4352
|
+
// async function LamdaServiceCall( url, data ) {
|
|
4353
|
+
// try {
|
|
4354
|
+
// const requestOptions = {
|
|
4355
|
+
// method: 'POST',
|
|
4356
|
+
// headers: {
|
|
4357
|
+
// 'Content-Type': 'application/json',
|
|
4358
|
+
// },
|
|
4359
|
+
// body: JSON.stringify( data ),
|
|
4360
|
+
// };
|
|
4361
|
+
// console.log( data );
|
|
4362
|
+
// const response = await fetch( url, requestOptions );
|
|
4363
|
+
// if ( !response.ok ) {
|
|
4364
|
+
// throw new Error( `Response status: ${response.status}` );
|
|
4365
|
+
// return false;
|
|
4366
|
+
// }
|
|
4367
|
+
// const json = await response.json();
|
|
4368
|
+
// return json;
|
|
4369
|
+
// } catch ( error ) {
|
|
4370
|
+
// console.log( error );
|
|
4371
|
+
// logger.error( { error: error, message: data, function: 'LamdaServiceCall' } );
|
|
4372
|
+
// return false;
|
|
4373
|
+
// }
|
|
4374
|
+
// }
|
package/src/hbs/flag.hbs
CHANGED
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
<td class="flagText" style="padding-left:30px; line-height: 24px;">No of Flags :</td>
|
|
177
177
|
<td></td>
|
|
178
178
|
<td></td>
|
|
179
|
-
<td class="flagText">{{data.flagCount}}</td>
|
|
179
|
+
<td class="flagText">{{data.flagCount}}{{#eq data.status 'submit'}}(RunAIFlag:{{data.runAIFlag}},QuestionFlag:{{data.questionFlag}}){{/eq}}</td>
|
|
180
180
|
</tr>
|
|
181
181
|
<tr bgcolor="#ffffff" style="border:none;margin-top:3px;">
|
|
182
182
|
<td class="flagText" style="padding-left:30px; line-height: 24px;">Submitted By :</td>
|
package/src/hbs/template.hbs
CHANGED
|
@@ -267,6 +267,13 @@
|
|
|
267
267
|
<img src="{{this}}" alt="test" width="200" height="180">
|
|
268
268
|
{{/each}}
|
|
269
269
|
</div>
|
|
270
|
+
{{#if validationVideo.length}}
|
|
271
|
+
<div class="Reference"><span>Uploaded Video</span><br>
|
|
272
|
+
{{#each validationVideo}}
|
|
273
|
+
<a href="{{this}}" target="_blank" style="text-decoration: underline;color:#0085D2">{{this}}</a><br>
|
|
274
|
+
{{/each}}
|
|
275
|
+
</div>
|
|
276
|
+
{{/if}}
|
|
270
277
|
{{/eq}}
|
|
271
278
|
</td>
|
|
272
279
|
</tr>
|
|
@@ -56,14 +56,16 @@
|
|
|
56
56
|
.dp-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:12px;border-bottom:2px solid #00AEEF}
|
|
57
57
|
.dp-header h2{font-size:18px;font-weight:700;color:#1a1a2e}
|
|
58
58
|
.dp-score{font-size:15px;font-weight:700;color:#000000}
|
|
59
|
-
.q-row{
|
|
60
|
-
.q-num{font-size:12px;font-weight:700;color:#000
|
|
61
|
-
.q-body{flex:1}
|
|
59
|
+
.q-row{position:relative;margin-bottom:14px;padding:12px 12px 12px 40px;border-radius:8px;background:transparent}
|
|
60
|
+
.q-num{position:absolute;left:12px;top:12px;font-size:12px;font-weight:700;color:#000}
|
|
62
61
|
.q-text{font-size:13px;color:#444;margin-bottom:4px}
|
|
63
62
|
.q-ans{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;padding:2px 8px;border-radius:12px}
|
|
64
63
|
.ans-yes{background:#e1f5ee;color:#0f6e56}
|
|
65
64
|
.ans-no{background:#fcebeb;color:#a32d2d}
|
|
66
65
|
.flag-badge{background:#faeeda;color:#854f0b;font-size:10px;font-weight:600;padding:1px 6px;border-radius:8px;margin-left:6px}
|
|
66
|
+
.det-badge{display:inline-block;font-size:10px;font-weight:600;padding:2px 8px;border-radius:10px;white-space:nowrap}
|
|
67
|
+
.det-matched{background:#e1f5ee;color:#0f6e56}
|
|
68
|
+
.det-notmatched{background:#fcebeb;color:#a32d2d}
|
|
67
69
|
.q-answer-list{margin-top:6px}
|
|
68
70
|
.q-answer-item{margin-top:6px;padding:0;background:transparent;border:none;border-radius:0}
|
|
69
71
|
.q-answer-text{font-size:12px;color:#1a1a1a;line-height:1.5}
|
|
@@ -72,9 +74,21 @@
|
|
|
72
74
|
.q-answer-media img,.q-answer-media video,.q-answer-item td img{display:block;width:200px;height:180px;object-fit:cover;border-radius:6px;margin-bottom:6px}
|
|
73
75
|
.img-grid{display:flex;flex-wrap:wrap;gap:8px}
|
|
74
76
|
.img-grid img{margin-bottom:0}
|
|
77
|
+
.answer-media-grid{margin-top:8px;font-size:0}
|
|
78
|
+
.answer-media-cell{display:inline-block;width:calc(50% - 6px);vertical-align:top;margin-bottom:12px;break-inside:avoid}
|
|
79
|
+
.answer-media-cell:nth-child(2n+1){margin-right:12px}
|
|
80
|
+
.answer-media-cell .q-answer-caption{margin-bottom:4px}
|
|
81
|
+
.answer-media-cell img{display:block;width:100%;height:200px;object-fit:fill;border-radius:6px;margin-bottom:0}
|
|
75
82
|
.q-answer-link{font-size:12px;color:#0085D2;text-decoration:underline;word-break:break-all}
|
|
76
83
|
.q-answer-caption{font-size:11px;color:#666;margin-bottom:4px}
|
|
77
84
|
.q-answer-remarks{font-size:11px;color:#666;margin-top:6px;white-space:pre-line}
|
|
85
|
+
/* User verification (Submitted By) */
|
|
86
|
+
.user-verify{margin-top:28px;padding-top:18px;border-top:1px solid #d9d9d9;break-inside:avoid}
|
|
87
|
+
.user-verify-title{font-size:15px;font-weight:700;color:#1a1a1a;margin-bottom:14px}
|
|
88
|
+
.user-verify-photo{display:block;width:200px;height:240px;object-fit:cover;border-radius:8px;margin-bottom:10px}
|
|
89
|
+
.user-verify-name{font-size:14px;color:#1a1a1a;font-weight:600}
|
|
90
|
+
.user-verify-sign-label{font-size:12px;color:#666;margin-top:16px;margin-bottom:6px;font-weight:600}
|
|
91
|
+
.user-verify-signature{display:block;width:220px;height:auto;max-height:110px;object-fit:contain;border:1px solid #eee;border-radius:6px;padding:6px;background:#fff}
|
|
78
92
|
/* Footer */
|
|
79
93
|
.page-footer{position:absolute;bottom:20px;left:40px;right:40px;display:flex;justify-content:space-between;align-items:center;font-size:11px;color:#999;border-top:1px solid #d9d9d9;padding-top:10px}
|
|
80
94
|
.footer-brand{display:flex;align-items:center;gap:8px;font-weight:600;color:#0066CC;font-size:11px}
|
|
@@ -112,8 +126,12 @@
|
|
|
112
126
|
|
|
113
127
|
<div class="cover-summary">
|
|
114
128
|
<div class="cover-sum-row"><span class="cover-sum-label">No. of questions</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{numQuestions}}</span></div>
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
{{#if showFlags}}
|
|
130
|
+
<div class="cover-sum-row"><span class="cover-sum-label">Question Flag</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{numFlags}}</span></div>
|
|
131
|
+
{{/if}}
|
|
132
|
+
{{#if showRunAIFlag}}
|
|
133
|
+
<div class="cover-sum-row"><span class="cover-sum-label">Run AI Flag</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{runAIFlag}}</span></div>
|
|
134
|
+
{{/if}}
|
|
117
135
|
<div class="cover-sum-row"><span class="cover-sum-label">Submitted By</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{submittedBy}}</span></div>
|
|
118
136
|
<div class="cover-sum-row"><span class="cover-sum-label">Country</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{country}}</span></div>
|
|
119
137
|
</div>
|
|
@@ -175,10 +193,10 @@
|
|
|
175
193
|
<div class="q-row">
|
|
176
194
|
<span class="q-num">{{this.qno}}</span>
|
|
177
195
|
<div class="q-body">
|
|
178
|
-
<div class="q-text" style="display:flex;justify-content:space-between">
|
|
196
|
+
<div class="q-text" style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
|
179
197
|
<div>{{this.qname}}</div>
|
|
180
198
|
{{#if this.compliance}}
|
|
181
|
-
<div>Score:{{this.score}}</div>
|
|
199
|
+
<div style="flex-shrink:0">Score:{{this.score}}</div>
|
|
182
200
|
{{/if}}
|
|
183
201
|
</div>
|
|
184
202
|
{{#if this.questionReferenceImage}}
|
|
@@ -204,7 +222,7 @@
|
|
|
204
222
|
<div class="q-answer-item">
|
|
205
223
|
{{#eq this.answerType 'text'}}
|
|
206
224
|
{{#if this.answer}}
|
|
207
|
-
<div class="q-answer-text {{#if this.sopFlag}}flagged{{/if}}">{{this.answer}}</div>
|
|
225
|
+
<div class="q-answer-text {{#if this.sopFlag}}flagged{{/if}}">{{this.answer}}{{#if this.showDetectionStatus}} <span class="det-badge {{#eq this.detectionStatus 'Matched'}}det-matched{{else}}det-notmatched{{/eq}}">{{this.detectionStatus}}</span>{{/if}}</div>
|
|
208
226
|
{{/if}}
|
|
209
227
|
{{/eq}}
|
|
210
228
|
{{#eq this.answerType 'video'}}
|
|
@@ -215,6 +233,20 @@
|
|
|
215
233
|
</div>
|
|
216
234
|
{{/if}}
|
|
217
235
|
{{/eq}}
|
|
236
|
+
{{!-- This answer's own images (reference / uploaded / validation), grouped with its label, two per row --}}
|
|
237
|
+
{{#if this.answerMedia.length}}
|
|
238
|
+
<div class="answer-media-grid">
|
|
239
|
+
{{#each this.answerMedia}}
|
|
240
|
+
<div class="answer-media-cell">
|
|
241
|
+
<div class="q-answer-caption" style="display:flex;justify-content:space-between;align-items:center;gap:6px">
|
|
242
|
+
<span>{{this.label}}</span>
|
|
243
|
+
{{#if this.detectionStatus}}<span class="det-badge {{#eq this.detectionStatus 'Matched'}}det-matched{{else}}det-notmatched{{/eq}}">{{this.detectionStatus}}</span>{{/if}}
|
|
244
|
+
</div>
|
|
245
|
+
<img src="{{this.url}}" alt="{{this.label}}" />
|
|
246
|
+
</div>
|
|
247
|
+
{{/each}}
|
|
248
|
+
</div>
|
|
249
|
+
{{/if}}
|
|
218
250
|
{{#eq this.validationDisplayType 'text'}}
|
|
219
251
|
{{#if this.validationAnswer}}
|
|
220
252
|
<div class="q-answer-text {{#if this.sopFlag}}flagged{{/if}}">validation Answer: {{this.validationAnswer}}</div>
|
|
@@ -227,95 +259,36 @@
|
|
|
227
259
|
{{/if}}
|
|
228
260
|
{{/eq}}
|
|
229
261
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
{{#if this.referenceImage}}
|
|
243
|
-
<div class="q-answer-media">
|
|
244
|
-
<div class="q-answer-caption">Reference Image</div>
|
|
245
|
-
<img src="{{this.referenceImage}}" alt="Reference Image" />
|
|
246
|
-
</div>
|
|
247
|
-
{{else}}
|
|
248
|
-
{{#eq this.answerType 'image'}}
|
|
249
|
-
{{#if this.answer}}
|
|
250
|
-
<div class="q-answer-media">
|
|
251
|
-
<div class="q-answer-caption">Uploaded Image</div>
|
|
252
|
-
<img src="{{this.answer}}" alt="Uploaded Image" />
|
|
253
|
-
</div>
|
|
254
|
-
{{/if}}
|
|
255
|
-
{{/eq}}
|
|
256
|
-
{{/if}}
|
|
257
|
-
{{/if}}
|
|
258
|
-
{{else}}
|
|
259
|
-
{{#eq this.answerType 'image'}}
|
|
260
|
-
{{#if this.answer}}
|
|
261
|
-
<div class="q-answer-media">
|
|
262
|
-
<div class="q-answer-caption">Uploaded Image</div>
|
|
263
|
-
<img src="{{this.answer}}" alt="Uploaded Image" />
|
|
264
|
-
</div>
|
|
265
|
-
{{/if}}
|
|
266
|
-
{{/eq}}
|
|
267
|
-
{{/neq}}
|
|
268
|
-
{{else}}
|
|
269
|
-
{{#eq this.answerType 'image'}}
|
|
270
|
-
{{#if this.answer}}
|
|
271
|
-
<div class="q-answer-media">
|
|
272
|
-
<div class="q-answer-caption">Uploaded Image</div>
|
|
273
|
-
<img src="{{this.answer}}" alt="Uploaded Image" />
|
|
274
|
-
</div>
|
|
275
|
-
{{/if}}
|
|
276
|
-
{{/eq}}
|
|
277
|
-
{{/neq}}
|
|
278
|
-
</td>
|
|
279
|
-
<td style="width:50%;vertical-align:top;padding-left:8px">
|
|
280
|
-
{{#eq this.answerType 'image'}}
|
|
281
|
-
{{#if this.answer}}
|
|
282
|
-
{{#neq ../answerType 'image/video'}}
|
|
283
|
-
{{#neq ../answerType 'multipleImage'}}
|
|
284
|
-
{{#if this.multiReferenceImage.length}}
|
|
285
|
-
<div class="q-answer-caption">Uploaded Image</div>
|
|
286
|
-
<img src="{{this.answer}}" alt="Uploaded Image" />
|
|
287
|
-
{{else}}
|
|
288
|
-
{{#if this.referenceImage}}
|
|
289
|
-
<div class="q-answer-caption">Uploaded Image</div>
|
|
290
|
-
<img src="{{this.answer}}" alt="Uploaded Image" />
|
|
291
|
-
{{/if}}
|
|
292
|
-
{{/if}}
|
|
293
|
-
{{/neq}}
|
|
294
|
-
{{/neq}}
|
|
295
|
-
{{/if}}
|
|
296
|
-
{{/eq}}
|
|
297
|
-
{{#if this.validation}}
|
|
298
|
-
{{#eq this.validationDisplayType 'image'}}
|
|
299
|
-
{{#if this.validationAnswer}}
|
|
300
|
-
<div class="q-answer-caption">Validation Image</div>
|
|
301
|
-
<img src="{{this.validationAnswer}}" alt="Validation Image" />
|
|
302
|
-
{{/if}}
|
|
303
|
-
{{/eq}}
|
|
304
|
-
{{#eq this.validationDisplayType 'multiImage'}}
|
|
305
|
-
{{#if this.validationImage.length}}
|
|
306
|
-
<div class="q-answer-caption">Validation Image</div>
|
|
307
|
-
{{#each this.validationImage}}
|
|
308
|
-
<img src="{{this}}" alt="Validation Image" />
|
|
309
|
-
{{/each}}
|
|
310
|
-
{{/if}}
|
|
311
|
-
{{/eq}}
|
|
312
|
-
{{/if}}
|
|
313
|
-
</td>
|
|
314
|
-
</tr></table>
|
|
262
|
+
{{!-- Validation videos captured with multi-image validation (shown as links) --}}
|
|
263
|
+
{{#eq this.validationDisplayType 'multiImage'}}
|
|
264
|
+
{{#if this.validationVideo.length}}
|
|
265
|
+
<div class="q-answer-media">
|
|
266
|
+
<div class="q-answer-caption">Validation Video</div>
|
|
267
|
+
{{#each this.validationVideo}}
|
|
268
|
+
<a class="q-answer-link" href="{{this}}" target="_blank">{{this}}</a>
|
|
269
|
+
{{/each}}
|
|
270
|
+
</div>
|
|
271
|
+
{{/if}}
|
|
272
|
+
{{/eq}}
|
|
273
|
+
|
|
315
274
|
</div>
|
|
316
275
|
{{/each}}
|
|
317
276
|
</div>
|
|
318
277
|
{{/if}}
|
|
278
|
+
{{!-- All images for this question (reference / uploaded / validation), two per row --}}
|
|
279
|
+
{{#if this.mediaItems.length}}
|
|
280
|
+
<div class="answer-media-grid">
|
|
281
|
+
{{#each this.mediaItems}}
|
|
282
|
+
<div class="answer-media-cell">
|
|
283
|
+
<div class="q-answer-caption" style="display:flex;justify-content:space-between;align-items:center;gap:6px">
|
|
284
|
+
<span>{{this.label}}</span>
|
|
285
|
+
{{#if this.detectionStatus}}<span class="det-badge {{#eq this.detectionStatus 'Matched'}}det-matched{{else}}det-notmatched{{/eq}}">{{this.detectionStatus}}</span>{{/if}}
|
|
286
|
+
</div>
|
|
287
|
+
<img src="{{this.url}}" alt="{{this.label}}" />
|
|
288
|
+
</div>
|
|
289
|
+
{{/each}}
|
|
290
|
+
</div>
|
|
291
|
+
{{/if}}
|
|
319
292
|
{{#if this.remarks}}
|
|
320
293
|
<div class="q-answer-remarks">Remarks: {{this.remarks}}</div>
|
|
321
294
|
{{/if}}
|
|
@@ -324,6 +297,17 @@
|
|
|
324
297
|
{{/each}}
|
|
325
298
|
</div>
|
|
326
299
|
{{/each}}
|
|
300
|
+
{{#if showUserVerification}}
|
|
301
|
+
<div class="user-verify">
|
|
302
|
+
<div class="user-verify-title">Submitted By</div>
|
|
303
|
+
{{#if userImage}}
|
|
304
|
+
<img class="user-verify-photo" src="{{userImage}}" alt="Submitted by photo" />
|
|
305
|
+
{{/if}}
|
|
306
|
+
{{#if submittedBy}}
|
|
307
|
+
<div class="user-verify-name">{{userSignature}}</div>
|
|
308
|
+
{{/if}}
|
|
309
|
+
</div>
|
|
310
|
+
{{/if}}
|
|
327
311
|
</div>
|
|
328
312
|
|
|
329
313
|
</body>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { insertOpenSearchData, logger } from 'tango-app-api-middleware';
|
|
2
|
+
import { loggingConfig } from './config.js';
|
|
3
|
+
import { compressBatches } from './compressBatches.js';
|
|
4
|
+
|
|
5
|
+
export async function flushActivityLog( ctx, status ) {
|
|
6
|
+
if ( !loggingConfig.enabled ) return;
|
|
7
|
+
if ( !ctx ) return;
|
|
8
|
+
|
|
9
|
+
if ( ctx.steps.length === 0 && !ctx.error ) return;
|
|
10
|
+
|
|
11
|
+
const compressedSteps = safeCompress( ctx.steps );
|
|
12
|
+
|
|
13
|
+
const doc = {
|
|
14
|
+
version: loggingConfig.version,
|
|
15
|
+
requestId: ctx.requestId,
|
|
16
|
+
timestamp: new Date( ctx.startTime ).toISOString(),
|
|
17
|
+
duration: Date.now() - ctx.startTime,
|
|
18
|
+
user: ctx.user,
|
|
19
|
+
api: {
|
|
20
|
+
method: ctx.api.method,
|
|
21
|
+
path: ctx.api.path,
|
|
22
|
+
action: ctx.api.action,
|
|
23
|
+
body: stringifyBody( ctx.api.body ),
|
|
24
|
+
},
|
|
25
|
+
response: {
|
|
26
|
+
code: ctx.response?.code ?? null,
|
|
27
|
+
body: stringifyBody( ctx.response?.body ),
|
|
28
|
+
},
|
|
29
|
+
status,
|
|
30
|
+
steps: compressedSteps,
|
|
31
|
+
error: ctx.error,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await insertOpenSearchData( loggingConfig.index, doc );
|
|
36
|
+
} catch ( e ) {
|
|
37
|
+
logger.error( { functionName: 'flushActivityLog', error: e } );
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function safeCompress( steps ) {
|
|
42
|
+
try {
|
|
43
|
+
return compressBatches( steps );
|
|
44
|
+
} catch ( _e ) {
|
|
45
|
+
// Never let compression failure block log delivery — fall back to raw steps.
|
|
46
|
+
return steps;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stringifyBody( body ) {
|
|
51
|
+
if ( body === null || body === undefined ) return null;
|
|
52
|
+
if ( typeof body === 'string' ) return body;
|
|
53
|
+
if ( body instanceof Error ) return body.message || String( body );
|
|
54
|
+
try {
|
|
55
|
+
return JSON.stringify( body );
|
|
56
|
+
} catch ( _e ) {
|
|
57
|
+
return '[unserializable]';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { activityLogStore, createLogContext } from './activityLogStore.js';
|
|
2
|
+
import { flushActivityLog } from './activityLogFlusher.js';
|
|
3
|
+
import { loggingConfig } from './config.js';
|
|
4
|
+
|
|
5
|
+
function refreshUser( ctx, req ) {
|
|
6
|
+
if ( req?.user ) {
|
|
7
|
+
ctx.user = {
|
|
8
|
+
_id: req.user._id,
|
|
9
|
+
userName: req.user.userName,
|
|
10
|
+
email: req.user.email,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function traxActivityLogMiddleware( req, res, next ) {
|
|
16
|
+
if ( !loggingConfig.enabled ) return next();
|
|
17
|
+
|
|
18
|
+
const WRITE_METHODS = [ 'POST', 'PUT', 'PATCH', 'DELETE' ];
|
|
19
|
+
if ( !WRITE_METHODS.includes( req.method ) ) return next();
|
|
20
|
+
|
|
21
|
+
const ctx = createLogContext( req );
|
|
22
|
+
|
|
23
|
+
activityLogStore.run( ctx, () => {
|
|
24
|
+
const originalSendSuccess = res.sendSuccess;
|
|
25
|
+
const originalSendError = res.sendError;
|
|
26
|
+
|
|
27
|
+
res.sendSuccess = ( data ) => {
|
|
28
|
+
refreshUser( ctx, req );
|
|
29
|
+
ctx.response.code = 200;
|
|
30
|
+
ctx.response.body = data;
|
|
31
|
+
originalSendSuccess( data );
|
|
32
|
+
setImmediate( () => flushActivityLog( ctx, 'success' ).catch( () => {} ) );
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
res.sendError = ( message, code ) => {
|
|
36
|
+
refreshUser( ctx, req );
|
|
37
|
+
ctx.response.code = code ?? 500;
|
|
38
|
+
ctx.response.body = message;
|
|
39
|
+
originalSendError( message, code );
|
|
40
|
+
setImmediate( () => flushActivityLog( ctx, 'failed' ).catch( () => {} ) );
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
next();
|
|
44
|
+
} );
|
|
45
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
|
|
4
|
+
export const activityLogStore = new AsyncLocalStorage();
|
|
5
|
+
|
|
6
|
+
const SENSITIVE_KEYS = [ 'password', 'token', 'secret', 'authorization' ];
|
|
7
|
+
|
|
8
|
+
export function createLogContext( req ) {
|
|
9
|
+
return {
|
|
10
|
+
requestId: randomUUID(),
|
|
11
|
+
startTime: Date.now(),
|
|
12
|
+
user: req.user ? {
|
|
13
|
+
_id: req.user._id,
|
|
14
|
+
userName: req.user.userName,
|
|
15
|
+
email: req.user.email,
|
|
16
|
+
} : null,
|
|
17
|
+
api: {
|
|
18
|
+
method: req.method,
|
|
19
|
+
path: req.path,
|
|
20
|
+
action: `${ req.method } ${ req.path }`,
|
|
21
|
+
body: sanitizeBody( req.body ),
|
|
22
|
+
},
|
|
23
|
+
response: {
|
|
24
|
+
code: null,
|
|
25
|
+
body: null,
|
|
26
|
+
},
|
|
27
|
+
steps: [],
|
|
28
|
+
stepCounter: 0,
|
|
29
|
+
error: null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getLogContext() {
|
|
34
|
+
return activityLogStore.getStore();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function pushStep( entry ) {
|
|
38
|
+
const ctx = getLogContext();
|
|
39
|
+
if ( !ctx ) return;
|
|
40
|
+
|
|
41
|
+
const last = ctx.steps[ctx.steps.length - 1];
|
|
42
|
+
if (
|
|
43
|
+
last &&
|
|
44
|
+
last.name === entry.name &&
|
|
45
|
+
last.type === entry.type &&
|
|
46
|
+
last.status === 'success' &&
|
|
47
|
+
entry.status === 'success'
|
|
48
|
+
) {
|
|
49
|
+
last.count = ( last.count || 1 ) + 1;
|
|
50
|
+
last.ms += entry.ms;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ctx.stepCounter++;
|
|
55
|
+
ctx.steps.push( {
|
|
56
|
+
stepIndex: ctx.stepCounter,
|
|
57
|
+
name: entry.name,
|
|
58
|
+
type: entry.type,
|
|
59
|
+
status: entry.status,
|
|
60
|
+
ms: entry.ms,
|
|
61
|
+
} );
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function pushStepFailure( entry, error ) {
|
|
65
|
+
const ctx = getLogContext();
|
|
66
|
+
if ( !ctx ) return;
|
|
67
|
+
ctx.stepCounter++;
|
|
68
|
+
const message = error?.message || String( error );
|
|
69
|
+
ctx.steps.push( {
|
|
70
|
+
stepIndex: ctx.stepCounter,
|
|
71
|
+
name: entry.name,
|
|
72
|
+
type: entry.type,
|
|
73
|
+
status: 'failed',
|
|
74
|
+
ms: entry.ms,
|
|
75
|
+
error: message,
|
|
76
|
+
} );
|
|
77
|
+
ctx.error = {
|
|
78
|
+
message,
|
|
79
|
+
failedAtStepIndex: ctx.stepCounter,
|
|
80
|
+
failedStepName: entry.name,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sanitizeBody( body ) {
|
|
85
|
+
if ( !body || typeof body !== 'object' ) return body;
|
|
86
|
+
const cleaned = { ...body };
|
|
87
|
+
for ( const key of SENSITIVE_KEYS ) {
|
|
88
|
+
if ( cleaned[key] !== undefined ) cleaned[key] = '[REDACTED]';
|
|
89
|
+
}
|
|
90
|
+
return cleaned;
|
|
91
|
+
}
|