tango-app-api-trax 3.9.34 → 3.9.36
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
|
@@ -18,7 +18,7 @@ import timeZone from 'dayjs/plugin/timezone.js';
|
|
|
18
18
|
import utc from 'dayjs/plugin/utc.js';
|
|
19
19
|
import { logger } from 'tango-app-api-middleware';
|
|
20
20
|
import mongoose from 'mongoose';
|
|
21
|
-
import { sendPushNotification, sendAiPushNotification, sendEmailWithSES, signedUrl, fileUpload
|
|
21
|
+
import { sendPushNotification, sendAiPushNotification, sendEmailWithSES, signedUrl, fileUpload } from 'tango-app-api-middleware';
|
|
22
22
|
// import * as planoService from '../services/planogram.service.js';
|
|
23
23
|
import * as clusterServices from '../services/cluster.service.js';
|
|
24
24
|
import * as teamsServices from '../services/teams.service.js';
|
|
@@ -1703,6 +1703,85 @@ export async function saleUpdateCollection( req, res ) {
|
|
|
1703
1703
|
}
|
|
1704
1704
|
};
|
|
1705
1705
|
|
|
1706
|
+
/**
|
|
1707
|
+
* One-off migration: in checklistquestionsconfigs, when a question's answerType
|
|
1708
|
+
* is 'multiplechoicesingle' and one of its answers has validationType
|
|
1709
|
+
* 'Capture Image', replace it with 'Capture Multiple Image with description'.
|
|
1710
|
+
*
|
|
1711
|
+
* Document shape: { question: [ { answerType, answers: [ { validationType } ] } ] }
|
|
1712
|
+
* Defaults to a dry run; pass apply:true (body) or ?apply=true to write changes.
|
|
1713
|
+
* @param {Object} req Express request — body.apply / query.apply toggles writing
|
|
1714
|
+
* @param {Object} res Express response
|
|
1715
|
+
* @return {Promise<void>}
|
|
1716
|
+
*/
|
|
1717
|
+
export async function updateMultipleChoiceSingleValidationType( req, res ) {
|
|
1718
|
+
try {
|
|
1719
|
+
const TARGET_ANSWER_TYPE = 'multiplechoicesingle';
|
|
1720
|
+
const FROM_VALIDATION_TYPE = 'Capture Image';
|
|
1721
|
+
const TO_VALIDATION_TYPE = 'Capture Multiple Image with description';
|
|
1722
|
+
|
|
1723
|
+
const apply = req.body?.apply === true || req.query?.apply === 'true';
|
|
1724
|
+
|
|
1725
|
+
// Only load docs that actually contain a matching question + answer.
|
|
1726
|
+
const query = {
|
|
1727
|
+
question: {
|
|
1728
|
+
$elemMatch: {
|
|
1729
|
+
answerType: TARGET_ANSWER_TYPE,
|
|
1730
|
+
answers: { $elemMatch: { validationType: FROM_VALIDATION_TYPE } },
|
|
1731
|
+
},
|
|
1732
|
+
},
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
const docs = await CLquestions.find( query );
|
|
1736
|
+
|
|
1737
|
+
let documentsUpdated = 0;
|
|
1738
|
+
let answersUpdated = 0;
|
|
1739
|
+
const updatedIds = [];
|
|
1740
|
+
|
|
1741
|
+
for ( const doc of docs ) {
|
|
1742
|
+
const questions = Array.isArray( doc.question ) ? doc.question : [];
|
|
1743
|
+
let changedInDoc = 0;
|
|
1744
|
+
|
|
1745
|
+
for ( const q of questions ) {
|
|
1746
|
+
if ( !q || q.answerType !== TARGET_ANSWER_TYPE ) continue;
|
|
1747
|
+
|
|
1748
|
+
const answers = Array.isArray( q.answers ) ? q.answers : [];
|
|
1749
|
+
for ( const a of answers ) {
|
|
1750
|
+
if ( a && a.validationType === FROM_VALIDATION_TYPE ) {
|
|
1751
|
+
a.validationType = TO_VALIDATION_TYPE;
|
|
1752
|
+
changedInDoc++;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if ( changedInDoc > 0 ) {
|
|
1758
|
+
documentsUpdated++;
|
|
1759
|
+
answersUpdated += changedInDoc;
|
|
1760
|
+
updatedIds.push( doc._id );
|
|
1761
|
+
|
|
1762
|
+
// `question` is a Mixed array, so write the whole rebuilt array.
|
|
1763
|
+
if ( apply ) {
|
|
1764
|
+
await CLquestions.updateMany( { _id: doc._id }, { question: questions } );
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
return res.sendSuccess( {
|
|
1770
|
+
message: apply ?
|
|
1771
|
+
'Validation type updated successfully' :
|
|
1772
|
+
'Dry run — pass apply:true to write these changes',
|
|
1773
|
+
apply,
|
|
1774
|
+
matchedDocuments: docs.length,
|
|
1775
|
+
documentsUpdated,
|
|
1776
|
+
answersUpdated,
|
|
1777
|
+
updatedIds,
|
|
1778
|
+
} );
|
|
1779
|
+
} catch ( error ) {
|
|
1780
|
+
logger.error( { function: 'updateMultipleChoiceSingleValidationType', error: error } );
|
|
1781
|
+
return res.sendError( error, 500 );
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1706
1785
|
export async function getUserStoreList( req, res ) {
|
|
1707
1786
|
try {
|
|
1708
1787
|
let details = [];
|
|
@@ -3601,7 +3680,7 @@ export async function runAIFlag( req, res ) {
|
|
|
3601
3680
|
runAIFlag: store?.runAIFlag,
|
|
3602
3681
|
questionFlag: store?.questionFlag,
|
|
3603
3682
|
checklistName: store.checkListName?.trim(),
|
|
3604
|
-
submittedBy: store?.userName,
|
|
3683
|
+
submittedBy: store.timeFlag ? '--' : store?.userName,
|
|
3605
3684
|
time: store?.submitTime_string ?? '--',
|
|
3606
3685
|
domain: `${JSON.parse( process.env.URL ).domain}/manage/trax/flags?date=${dayjs().format( 'YYYY-MM-DD' )}`,
|
|
3607
3686
|
status: store.checklistStatus,
|
|
@@ -3667,31 +3746,36 @@ export const downloadInsertPdf = async ( req, res ) => {
|
|
|
3667
3746
|
const safeName = ( str ) =>
|
|
3668
3747
|
( str || '' ).toString().replace( /[<>:"/\\|?*]+/g, '_' );
|
|
3669
3748
|
|
|
3670
|
-
const query = {
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
};
|
|
3749
|
+
// const query = {
|
|
3750
|
+
// query: {
|
|
3751
|
+
// bool: {
|
|
3752
|
+
// must: [
|
|
3753
|
+
// {
|
|
3754
|
+
// term: {
|
|
3755
|
+
// _id: req.body.checklistId,
|
|
3756
|
+
// },
|
|
3757
|
+
// },
|
|
3758
|
+
// ],
|
|
3759
|
+
// },
|
|
3760
|
+
// },
|
|
3761
|
+
// };
|
|
3762
|
+
|
|
3683
3763
|
|
|
3684
3764
|
// 1) Launch browser page + fetch OpenSearch data in parallel
|
|
3685
3765
|
const [ browser, aiDetails ] = await Promise.all( [
|
|
3686
3766
|
getBrowserInstance(),
|
|
3687
|
-
getOpenSearchData( JSON.parse( process.env.OPENSEARCH ).traxIndex, query ),
|
|
3767
|
+
// getOpenSearchData( JSON.parse( process.env.OPENSEARCH ).traxIndex, query ),
|
|
3768
|
+
processedchecklist.findOne( { _id: req.body.checklistId } ),
|
|
3688
3769
|
] );
|
|
3770
|
+
console.log( aiDetails );
|
|
3689
3771
|
|
|
3690
|
-
if (
|
|
3772
|
+
if ( !aiDetails ) {
|
|
3691
3773
|
return res.sendError( 'Checklist not found', 404 );
|
|
3692
3774
|
}
|
|
3693
3775
|
|
|
3694
|
-
const doc = { ...aiDetails
|
|
3776
|
+
const doc = { ...aiDetails?.toObject() };
|
|
3777
|
+
|
|
3778
|
+
console.log( doc );
|
|
3695
3779
|
|
|
3696
3780
|
// 2) Fetch brandInfo + compliance data in parallel
|
|
3697
3781
|
const complianceURL = JSON.parse( process.env.LAMBDAURL ).complianceHistory;
|
|
@@ -428,14 +428,19 @@ export const checklistPerformance = async ( req, res ) => {
|
|
|
428
428
|
$cond: [
|
|
429
429
|
{ $gt: [ '$$divisor', 0 ] },
|
|
430
430
|
{
|
|
431
|
-
$
|
|
431
|
+
$max: [
|
|
432
|
+
0,
|
|
432
433
|
{
|
|
433
|
-
$
|
|
434
|
-
{
|
|
435
|
-
|
|
434
|
+
$round: [
|
|
435
|
+
{
|
|
436
|
+
$multiply: [
|
|
437
|
+
{ $divide: [ '$userComplianceCountTotal', '$$divisor' ] },
|
|
438
|
+
100,
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
0,
|
|
436
442
|
],
|
|
437
443
|
},
|
|
438
|
-
0,
|
|
439
444
|
],
|
|
440
445
|
},
|
|
441
446
|
0,
|
|
@@ -463,7 +468,7 @@ export const checklistPerformance = async ( req, res ) => {
|
|
|
463
468
|
},
|
|
464
469
|
} );
|
|
465
470
|
let getChecklistPerformanceData = await processedchecklistService.aggregate( findQuery );
|
|
466
|
-
if ( !getChecklistPerformanceData[0].
|
|
471
|
+
if ( !getChecklistPerformanceData[0].count[0].total ) {
|
|
467
472
|
return res.sendError( 'no data found', 204 );
|
|
468
473
|
}
|
|
469
474
|
|
|
@@ -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,6 +74,11 @@
|
|
|
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}
|
|
@@ -182,10 +189,10 @@
|
|
|
182
189
|
<div class="q-row">
|
|
183
190
|
<span class="q-num">{{this.qno}}</span>
|
|
184
191
|
<div class="q-body">
|
|
185
|
-
<div class="q-text" style="display:flex;justify-content:space-between">
|
|
192
|
+
<div class="q-text" style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
|
186
193
|
<div>{{this.qname}}</div>
|
|
187
194
|
{{#if this.compliance}}
|
|
188
|
-
<div>Score:{{this.score}}</div>
|
|
195
|
+
<div style="flex-shrink:0">Score:{{this.score}}</div>
|
|
189
196
|
{{/if}}
|
|
190
197
|
</div>
|
|
191
198
|
{{#if this.questionReferenceImage}}
|
|
@@ -211,7 +218,7 @@
|
|
|
211
218
|
<div class="q-answer-item">
|
|
212
219
|
{{#eq this.answerType 'text'}}
|
|
213
220
|
{{#if this.answer}}
|
|
214
|
-
<div class="q-answer-text {{#if this.sopFlag}}flagged{{/if}}">{{this.answer}}</div>
|
|
221
|
+
<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>
|
|
215
222
|
{{/if}}
|
|
216
223
|
{{/eq}}
|
|
217
224
|
{{#eq this.answerType 'video'}}
|
|
@@ -222,6 +229,20 @@
|
|
|
222
229
|
</div>
|
|
223
230
|
{{/if}}
|
|
224
231
|
{{/eq}}
|
|
232
|
+
{{!-- This answer's own images (reference / uploaded / validation), grouped with its label, two per row --}}
|
|
233
|
+
{{#if this.answerMedia.length}}
|
|
234
|
+
<div class="answer-media-grid">
|
|
235
|
+
{{#each this.answerMedia}}
|
|
236
|
+
<div class="answer-media-cell">
|
|
237
|
+
<div class="q-answer-caption" style="display:flex;justify-content:space-between;align-items:center;gap:6px">
|
|
238
|
+
<span>{{this.label}}</span>
|
|
239
|
+
{{#if this.detectionStatus}}<span class="det-badge {{#eq this.detectionStatus 'Matched'}}det-matched{{else}}det-notmatched{{/eq}}">{{this.detectionStatus}}</span>{{/if}}
|
|
240
|
+
</div>
|
|
241
|
+
<img src="{{this.url}}" alt="{{this.label}}" />
|
|
242
|
+
</div>
|
|
243
|
+
{{/each}}
|
|
244
|
+
</div>
|
|
245
|
+
{{/if}}
|
|
225
246
|
{{#eq this.validationDisplayType 'text'}}
|
|
226
247
|
{{#if this.validationAnswer}}
|
|
227
248
|
<div class="q-answer-text {{#if this.sopFlag}}flagged{{/if}}">validation Answer: {{this.validationAnswer}}</div>
|
|
@@ -234,127 +255,36 @@
|
|
|
234
255
|
{{/if}}
|
|
235
256
|
{{/eq}}
|
|
236
257
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
{{#if this.referenceImage}}
|
|
250
|
-
<div class="q-answer-media">
|
|
251
|
-
<div class="q-answer-caption">Reference Image</div>
|
|
252
|
-
<img src="{{this.referenceImage}}" alt="Reference Image" />
|
|
253
|
-
</div>
|
|
254
|
-
{{else}}
|
|
255
|
-
{{#eq this.answerType 'image'}}
|
|
256
|
-
{{#if this.answer}}
|
|
257
|
-
<div class="q-answer-media">
|
|
258
|
-
<div class="q-answer-caption">Uploaded Image</div>
|
|
259
|
-
<img src="{{this.answer}}" alt="Uploaded Image" />
|
|
260
|
-
</div>
|
|
261
|
-
{{/if}}
|
|
262
|
-
{{/eq}}
|
|
263
|
-
{{/if}}
|
|
264
|
-
{{/if}}
|
|
265
|
-
{{else}}
|
|
266
|
-
{{#eq this.answerType 'image'}}
|
|
267
|
-
{{#if this.answer}}
|
|
268
|
-
<div class="q-answer-media">
|
|
269
|
-
<div class="q-answer-caption">Uploaded Image</div>
|
|
270
|
-
<img src="{{this.answer}}" alt="Uploaded Image" />
|
|
271
|
-
</div>
|
|
272
|
-
{{/if}}
|
|
273
|
-
{{/eq}}
|
|
274
|
-
{{/neq}}
|
|
275
|
-
{{else}}
|
|
276
|
-
{{#eq this.answerType 'image'}}
|
|
277
|
-
{{#if this.answer}}
|
|
278
|
-
<div class="q-answer-media">
|
|
279
|
-
<div class="q-answer-caption">Uploaded Image</div>
|
|
280
|
-
<img src="{{this.answer}}" alt="Uploaded Image" />
|
|
281
|
-
</div>
|
|
282
|
-
{{/if}}
|
|
283
|
-
{{/eq}}
|
|
284
|
-
{{/neq}}
|
|
285
|
-
{{#unless this.hasReferenceImage}}
|
|
286
|
-
{{#if this.validation}}
|
|
287
|
-
{{#eq this.validationDisplayType 'image'}}
|
|
288
|
-
{{#if this.validationAnswer}}
|
|
289
|
-
<div class="q-answer-caption">Validation Image</div>
|
|
290
|
-
<img src="{{this.validationAnswer}}" alt="Validation Image" />
|
|
291
|
-
{{/if}}
|
|
292
|
-
{{/eq}}
|
|
293
|
-
{{#eq this.validationDisplayType 'multiImage'}}
|
|
294
|
-
{{#if this.validationImage.length}}
|
|
295
|
-
<div class="q-answer-caption">Validation Image</div>
|
|
296
|
-
{{#each this.validationImage}}
|
|
297
|
-
<img src="{{this}}" alt="Validation Image" />
|
|
298
|
-
{{/each}}
|
|
299
|
-
{{/if}}
|
|
300
|
-
{{#if this.validationVideo.length}}
|
|
301
|
-
<div class="q-answer-caption">Validation Video</div>
|
|
302
|
-
{{#each this.validationVideo}}
|
|
303
|
-
<a class="q-answer-link" href="{{this}}" target="_blank">{{this}}</a>
|
|
304
|
-
{{/each}}
|
|
305
|
-
{{/if}}
|
|
306
|
-
{{/eq}}
|
|
307
|
-
{{/if}}
|
|
308
|
-
{{/unless}}
|
|
309
|
-
</td>
|
|
310
|
-
<td style="width:50%;vertical-align:top;padding-left:8px">
|
|
311
|
-
{{#eq this.answerType 'image'}}
|
|
312
|
-
{{#if this.answer}}
|
|
313
|
-
{{#neq ../answerType 'image/video'}}
|
|
314
|
-
{{#neq ../answerType 'multipleImage'}}
|
|
315
|
-
{{#if this.multiReferenceImage.length}}
|
|
316
|
-
<div class="q-answer-caption">Uploaded Image</div>
|
|
317
|
-
<img src="{{this.answer}}" alt="Uploaded Image" />
|
|
318
|
-
{{else}}
|
|
319
|
-
{{#if this.referenceImage}}
|
|
320
|
-
<div class="q-answer-caption">Uploaded Image</div>
|
|
321
|
-
<img src="{{this.answer}}" alt="Uploaded Image" />
|
|
322
|
-
{{/if}}
|
|
323
|
-
{{/if}}
|
|
324
|
-
{{/neq}}
|
|
325
|
-
{{/neq}}
|
|
326
|
-
{{/if}}
|
|
327
|
-
{{/eq}}
|
|
328
|
-
{{#if this.hasReferenceImage}}
|
|
329
|
-
{{#if this.validation}}
|
|
330
|
-
{{#eq this.validationDisplayType 'image'}}
|
|
331
|
-
{{#if this.validationAnswer}}
|
|
332
|
-
<div class="q-answer-caption">Validation Image</div>
|
|
333
|
-
<img src="{{this.validationAnswer}}" alt="Validation Image" />
|
|
334
|
-
{{/if}}
|
|
335
|
-
{{/eq}}
|
|
336
|
-
{{#eq this.validationDisplayType 'multiImage'}}
|
|
337
|
-
{{#if this.validationImage.length}}
|
|
338
|
-
<div class="q-answer-caption">Validation Image</div>
|
|
339
|
-
{{#each this.validationImage}}
|
|
340
|
-
<img src="{{this}}" alt="Validation Image" />
|
|
341
|
-
{{/each}}
|
|
342
|
-
{{/if}}
|
|
343
|
-
{{#if this.validationVideo.length}}
|
|
344
|
-
<div class="q-answer-caption">Validation Video</div>
|
|
345
|
-
{{#each this.validationVideo}}
|
|
346
|
-
<a class="q-answer-link" href="{{this}}" target="_blank">{{this}}</a>
|
|
347
|
-
{{/each}}
|
|
348
|
-
{{/if}}
|
|
349
|
-
{{/eq}}
|
|
350
|
-
{{/if}}
|
|
351
|
-
{{/if}}
|
|
352
|
-
</td>
|
|
353
|
-
</tr></table>
|
|
258
|
+
{{!-- Validation videos captured with multi-image validation (shown as links) --}}
|
|
259
|
+
{{#eq this.validationDisplayType 'multiImage'}}
|
|
260
|
+
{{#if this.validationVideo.length}}
|
|
261
|
+
<div class="q-answer-media">
|
|
262
|
+
<div class="q-answer-caption">Validation Video</div>
|
|
263
|
+
{{#each this.validationVideo}}
|
|
264
|
+
<a class="q-answer-link" href="{{this}}" target="_blank">{{this}}</a>
|
|
265
|
+
{{/each}}
|
|
266
|
+
</div>
|
|
267
|
+
{{/if}}
|
|
268
|
+
{{/eq}}
|
|
269
|
+
|
|
354
270
|
</div>
|
|
355
271
|
{{/each}}
|
|
356
272
|
</div>
|
|
357
273
|
{{/if}}
|
|
274
|
+
{{!-- All images for this question (reference / uploaded / validation), two per row --}}
|
|
275
|
+
{{#if this.mediaItems.length}}
|
|
276
|
+
<div class="answer-media-grid">
|
|
277
|
+
{{#each this.mediaItems}}
|
|
278
|
+
<div class="answer-media-cell">
|
|
279
|
+
<div class="q-answer-caption" style="display:flex;justify-content:space-between;align-items:center;gap:6px">
|
|
280
|
+
<span>{{this.label}}</span>
|
|
281
|
+
{{#if this.detectionStatus}}<span class="det-badge {{#eq this.detectionStatus 'Matched'}}det-matched{{else}}det-notmatched{{/eq}}">{{this.detectionStatus}}</span>{{/if}}
|
|
282
|
+
</div>
|
|
283
|
+
<img src="{{this.url}}" alt="{{this.label}}" />
|
|
284
|
+
</div>
|
|
285
|
+
{{/each}}
|
|
286
|
+
</div>
|
|
287
|
+
{{/if}}
|
|
358
288
|
{{#if this.remarks}}
|
|
359
289
|
<div class="q-answer-remarks">Remarks: {{this.remarks}}</div>
|
|
360
290
|
{{/if}}
|
|
@@ -370,7 +300,7 @@
|
|
|
370
300
|
<img class="user-verify-photo" src="{{userImage}}" alt="Submitted by photo" />
|
|
371
301
|
{{/if}}
|
|
372
302
|
{{#if submittedBy}}
|
|
373
|
-
<div class="user-verify-name">{{
|
|
303
|
+
<div class="user-verify-name">{{userSignature}}</div>
|
|
374
304
|
{{/if}}
|
|
375
305
|
</div>
|
|
376
306
|
{{/if}}
|
|
@@ -13,6 +13,7 @@ internalTraxRouter
|
|
|
13
13
|
.get( '/list', isAllowedInternalAPIHandler, internalController.list )
|
|
14
14
|
.post( '/updateAomDetails', isAllowedInternalAPIHandler, internalController.aomupdateCollection )
|
|
15
15
|
.post( '/updateSaleManagerDetails', isAllowedInternalAPIHandler, internalController.saleUpdateCollection )
|
|
16
|
+
.post( '/updateValidationType', isAllowedInternalAPIHandler, internalController.updateMultipleChoiceSingleValidationType )
|
|
16
17
|
.get( '/profile', isAllowedMobileSessionHandler, internalController.getUserStoreList )
|
|
17
18
|
.post( '/pushNotification', isAllowedInternalAPIHandler, internalController.pushNotification )
|
|
18
19
|
.post( '/taskPushNotification', isAllowedInternalAPIHandler, internalController.taskPushNotification )
|
|
@@ -146,6 +146,54 @@ function flattenImageRefs( arr ) {
|
|
|
146
146
|
.filter( Boolean );
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Builds the ordered media list (reference → uploaded → validation) for a SINGLE
|
|
151
|
+
* answer entry, deduped within that entry. Used to render each answer's images
|
|
152
|
+
* inline next to its own text/label, instead of pooling every answer's images
|
|
153
|
+
* into one question-level grid (which decoupled labels from their images).
|
|
154
|
+
* @param {Object} ua a single entry produced by buildQuestionAnswerEntries
|
|
155
|
+
* @return {Array} list of { type, label, url, detectionStatus } for this entry, in render order
|
|
156
|
+
*/
|
|
157
|
+
function buildEntryMediaItems( ua = {} ) {
|
|
158
|
+
const items = [];
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
|
|
161
|
+
const push = ( type, label, url, detectionStatus = '' ) => {
|
|
162
|
+
if ( !url || typeof url !== 'string' ) return;
|
|
163
|
+
if ( seen.has( url ) ) return;
|
|
164
|
+
seen.add( url );
|
|
165
|
+
items.push( { type, label, url, detectionStatus } );
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Each uploaded/validation image shows its OWN runAI verdict (per-image, never
|
|
169
|
+
// aggregated). References are not AI-checked, so they carry no badge.
|
|
170
|
+
const statusByUrl = buildAnswerImageStatus( ua );
|
|
171
|
+
const statusFor = ( url ) => statusByUrl.get( url ) || '';
|
|
172
|
+
|
|
173
|
+
if ( ua.hasReferenceImage ) {
|
|
174
|
+
if ( Array.isArray( ua.multiReferenceImage ) && ua.multiReferenceImage.length ) {
|
|
175
|
+
ua.multiReferenceImage.forEach( ( url ) => push( 'reference', 'Reference Image', url ) );
|
|
176
|
+
} else if ( ua.referenceImage ) {
|
|
177
|
+
push( 'reference', 'Reference Image', ua.referenceImage );
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if ( ua.answerType === 'image' && ua.answer ) {
|
|
182
|
+
push( 'uploaded', 'Uploaded Image', ua.answer, statusFor( ua.answer ) );
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if ( ua.validation ) {
|
|
186
|
+
if ( ua.validationDisplayType === 'image' && ua.validationAnswer ) {
|
|
187
|
+
push( 'validation', 'Validation Image', ua.validationAnswer, statusFor( ua.validationAnswer ) );
|
|
188
|
+
}
|
|
189
|
+
if ( ua.validationDisplayType === 'multiImage' && Array.isArray( ua.validationImage ) ) {
|
|
190
|
+
ua.validationImage.forEach( ( url ) => push( 'validation', 'Validation Image', url, statusFor( url ) ) );
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return items;
|
|
195
|
+
}
|
|
196
|
+
|
|
149
197
|
function buildQuestionAnswerEntries( question ) {
|
|
150
198
|
const rawUserAnswers = getSourceUserAnswers( question );
|
|
151
199
|
const uniqueUserAnswers = [];
|
|
@@ -190,7 +238,21 @@ function buildQuestionAnswerEntries( question ) {
|
|
|
190
238
|
question?.answerType !== 'multipleImage' &&
|
|
191
239
|
Boolean( referenceImage || multiReferenceImage.length );
|
|
192
240
|
|
|
193
|
-
|
|
241
|
+
// runAI verdicts for this answer, in source order. Used to tag each uploaded /
|
|
242
|
+
// validation image with its OWN verdict (by answerImage filename, else by
|
|
243
|
+
// position) so multi-image answers don't all collapse to one aggregated status.
|
|
244
|
+
const isPerAnswerType = PER_ANSWER_DETECTION_TYPES.includes( question?.answerType );
|
|
245
|
+
const runAIValues = getRunAIMatchValues( userAnswer?.runAIData );
|
|
246
|
+
const imageStatus = buildImageStatusMap( userAnswer?.runAIData );
|
|
247
|
+
|
|
248
|
+
// Choice answers (yes/no, multiple choice, dropdown) take this answer's own
|
|
249
|
+
// verdict directly — never aggregated. Other types keep the aggregate only as
|
|
250
|
+
// a last-resort fallback.
|
|
251
|
+
const detectionStatus = isPerAnswerType ?
|
|
252
|
+
verdictFromValue( runAIValues[0] ) :
|
|
253
|
+
getDetectionStatus( userAnswer?.runAIData );
|
|
254
|
+
|
|
255
|
+
const entry = {
|
|
194
256
|
answer: userAnswer?.answer || '',
|
|
195
257
|
answerType: getMediaDisplayType( question?.answerType, userAnswer ),
|
|
196
258
|
referenceImage,
|
|
@@ -198,6 +260,9 @@ function buildQuestionAnswerEntries( question ) {
|
|
|
198
260
|
multiReferenceImage,
|
|
199
261
|
remarks: userAnswer?.remarks || '',
|
|
200
262
|
sopFlag: userAnswer?.sopFlag ?? matchedAnswer?.sopFlag ?? false,
|
|
263
|
+
detectionStatus,
|
|
264
|
+
imageStatus,
|
|
265
|
+
runAIValues,
|
|
201
266
|
validation,
|
|
202
267
|
validationType,
|
|
203
268
|
validationAnswer,
|
|
@@ -205,7 +270,90 @@ function buildQuestionAnswerEntries( question ) {
|
|
|
205
270
|
validationVideo,
|
|
206
271
|
validationDisplayType: getValidationDisplayType( validationType ),
|
|
207
272
|
};
|
|
273
|
+
|
|
274
|
+
// Text-label answers (e.g. "Attach the image of Cat") carry their own
|
|
275
|
+
// reference/validation images. Group those images with this entry so the
|
|
276
|
+
// label and its images render together, instead of pooling every answer's
|
|
277
|
+
// images into one question-level grid. Image/video answers keep flowing
|
|
278
|
+
// into the pooled grid (handled by buildQuestionMediaItems).
|
|
279
|
+
entry.answerMedia = entry.answerType === 'text' ? buildEntryMediaItems( entry ) : [];
|
|
280
|
+
|
|
281
|
+
// Choice answers with no media show their verdict next to the answer text;
|
|
282
|
+
// when they carry validation media the badge sits on that media instead.
|
|
283
|
+
entry.showDetectionStatus = isPerAnswerType && Boolean( detectionStatus ) && !entry.answerMedia.length;
|
|
284
|
+
|
|
285
|
+
return entry;
|
|
286
|
+
} );
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Flattens a question's user answers into one ordered, deduped image list for the
|
|
292
|
+
* 2-per-row answer media grid. Order: reference images first, then uploaded images,
|
|
293
|
+
* then validation images. Videos are shown as links elsewhere, so only images are
|
|
294
|
+
* collected here. When a question has no reference image, the uploaded images simply
|
|
295
|
+
* fill the grid from the start (taking the reference slot).
|
|
296
|
+
* @param {Array} userAnswers entries produced by buildQuestionAnswerEntries
|
|
297
|
+
* @param {string} [forcedStatus] when set (image / image-video), every uploaded &
|
|
298
|
+
* validation image shows this aggregated verdict instead of its own per-image one
|
|
299
|
+
* @return {Array} list of { type, label, url, detectionStatus } media items in render order
|
|
300
|
+
*/
|
|
301
|
+
function buildQuestionMediaItems( userAnswers = [], forcedStatus = '' ) {
|
|
302
|
+
const items = [];
|
|
303
|
+
const seen = new Set();
|
|
304
|
+
|
|
305
|
+
const push = ( type, label, url, detectionStatus = '' ) => {
|
|
306
|
+
if ( !url || typeof url !== 'string' ) return;
|
|
307
|
+
if ( seen.has( url ) ) return;
|
|
308
|
+
seen.add( url );
|
|
309
|
+
items.push( { type, label, url, detectionStatus } );
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Entries that render their media inline with the answer (answerMedia) are
|
|
313
|
+
// excluded here so their images are not duplicated in the pooled grid.
|
|
314
|
+
const pool = userAnswers.filter( ( ua ) => !( ua.answerMedia && ua.answerMedia.length ) );
|
|
315
|
+
|
|
316
|
+
// Resolve each image's status per answer (per-image; forcedStatus aggregates
|
|
317
|
+
// image / image-video). Built per answer so positional matching stays aligned.
|
|
318
|
+
const statusByUrl = new Map();
|
|
319
|
+
pool.forEach( ( ua ) => {
|
|
320
|
+
buildAnswerImageStatus( ua, forcedStatus ).forEach( ( status, url ) => statusByUrl.set( url, status ) );
|
|
321
|
+
} );
|
|
322
|
+
const statusFor = ( url ) => statusByUrl.get( url ) || '';
|
|
323
|
+
|
|
324
|
+
// 1. Reference images first (deduped, so a single shared reference shows once).
|
|
325
|
+
// Skipped for image/video & multipleImage upload questions (hasReferenceImage
|
|
326
|
+
// is false), which have no comparison reference — only uploaded answers.
|
|
327
|
+
pool.forEach( ( ua ) => {
|
|
328
|
+
if ( !ua.hasReferenceImage ) return;
|
|
329
|
+
if ( Array.isArray( ua.multiReferenceImage ) && ua.multiReferenceImage.length ) {
|
|
330
|
+
ua.multiReferenceImage.forEach( ( url ) => push( 'reference', 'Reference Image', url ) );
|
|
331
|
+
} else if ( ua.referenceImage ) {
|
|
332
|
+
push( 'reference', 'Reference Image', ua.referenceImage );
|
|
333
|
+
}
|
|
334
|
+
} );
|
|
335
|
+
|
|
336
|
+
// 2. Uploaded images — tagged with each image's own detection status (from runAIData).
|
|
337
|
+
pool.forEach( ( ua ) => {
|
|
338
|
+
if ( ua.answerType === 'image' && ua.answer ) {
|
|
339
|
+
push( 'uploaded', 'Uploaded Image', ua.answer, statusFor( ua.answer ) );
|
|
340
|
+
}
|
|
341
|
+
} );
|
|
342
|
+
|
|
343
|
+
// 3. Validation images (single 'Capture Image' and multi 'Capture Multiple Image').
|
|
344
|
+
pool.forEach( ( ua ) => {
|
|
345
|
+
if ( !ua.validation ) return;
|
|
346
|
+
|
|
347
|
+
if ( ua.validationDisplayType === 'image' && ua.validationAnswer ) {
|
|
348
|
+
push( 'validation', 'Validation Image', ua.validationAnswer, statusFor( ua.validationAnswer ) );
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if ( ua.validationDisplayType === 'multiImage' && Array.isArray( ua.validationImage ) ) {
|
|
352
|
+
ua.validationImage.forEach( ( url ) => push( 'validation', 'Validation Image', url, statusFor( url ) ) );
|
|
353
|
+
}
|
|
208
354
|
} );
|
|
355
|
+
|
|
356
|
+
return items;
|
|
209
357
|
}
|
|
210
358
|
|
|
211
359
|
|
|
@@ -236,6 +384,125 @@ function getRunAIMatchValues( runAIData ) {
|
|
|
236
384
|
}
|
|
237
385
|
|
|
238
386
|
|
|
387
|
+
// Answer types whose runAI verdict is aggregated across all uploaded images:
|
|
388
|
+
// if any image is not matched, the whole question reads 'Not Matched'. Every
|
|
389
|
+
// other type shows each image's own verdict.
|
|
390
|
+
const AGGREGATE_DETECTION_TYPES = [ 'image', 'image/video' ];
|
|
391
|
+
|
|
392
|
+
// Choice answers: each selected answer carries its OWN runAI verdict and shows it
|
|
393
|
+
// directly — never aggregated ('.every') across the question's other answers.
|
|
394
|
+
const PER_ANSWER_DETECTION_TYPES = [ 'yes/no', 'multiplechoicesingle', 'multiplechoicemultiple', 'dropdown' ];
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Maps a single runAI value to a display status.
|
|
399
|
+
* @param {string|boolean} value
|
|
400
|
+
* @return {string} 'Matched' | 'Not Matched' | ''
|
|
401
|
+
*/
|
|
402
|
+
function verdictFromValue( value ) {
|
|
403
|
+
if ( value === 'True' || value === true ) return 'Matched';
|
|
404
|
+
if ( value === 'False' || value === false ) return 'Not Matched';
|
|
405
|
+
return '';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The filename of an image URL/path, ignoring any query string. Used to match an
|
|
411
|
+
* uploaded/validation image against its runAIData entry (whose answerImage may
|
|
412
|
+
* carry a different prefix / signed-URL query), regardless of CDN resolution.
|
|
413
|
+
* @param {string} url
|
|
414
|
+
* @return {string}
|
|
415
|
+
*/
|
|
416
|
+
function imageBasename( url ) {
|
|
417
|
+
return String( url || '' ).split( '?' )[0].split( '/' ).pop();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Builds a per-image detection status map from the nested runAIData shape
|
|
423
|
+
* ([ { answerImage, results: [ { featureName, value } ] } ]), keyed by the
|
|
424
|
+
* answerImage's filename. The flat shape has no per-image key, so it yields an
|
|
425
|
+
* empty map and callers fall back to the answer-level status.
|
|
426
|
+
* @param {Array} runAIData
|
|
427
|
+
* @return {Object} { [filename]: 'Matched' | 'Not Matched' }
|
|
428
|
+
*/
|
|
429
|
+
function buildImageStatusMap( runAIData ) {
|
|
430
|
+
const map = {};
|
|
431
|
+
if ( !Array.isArray( runAIData ) ) return map;
|
|
432
|
+
|
|
433
|
+
runAIData.forEach( ( item ) => {
|
|
434
|
+
if ( !item || !item.answerImage ) return;
|
|
435
|
+
const results = Array.isArray( item.results ) ? item.results : [ item ];
|
|
436
|
+
const values = results
|
|
437
|
+
.filter( ( r ) => r?.featureName === 'Matched/Not Matched' )
|
|
438
|
+
.map( ( r ) => r.value );
|
|
439
|
+
if ( !values.length ) return;
|
|
440
|
+
const matched = values.every( ( v ) => v === 'True' || v === true );
|
|
441
|
+
map[imageBasename( item.answerImage )] = matched ? 'Matched' : 'Not Matched';
|
|
442
|
+
} );
|
|
443
|
+
|
|
444
|
+
return map;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Resolves the detection status for each checked image (uploaded + validation) of a
|
|
450
|
+
* single answer, returned as a Map of url → 'Matched' | 'Not Matched' | ''.
|
|
451
|
+
*
|
|
452
|
+
* Each image gets its OWN verdict — never aggregated across the answer's other images:
|
|
453
|
+
* 1. matched explicitly by its answerImage filename (nested runAIData), else
|
|
454
|
+
* 2. matched positionally — the Nth checked image takes the Nth runAI verdict
|
|
455
|
+
* (works for the flat runAIData shape and when filenames don't line up).
|
|
456
|
+
* For image / image-video, forcedStatus (the question-level aggregate) overrides all.
|
|
457
|
+
* @param {Object} ua a single entry produced by buildQuestionAnswerEntries
|
|
458
|
+
* @param {string} [forcedStatus] aggregated status applied to every image (image/image-video)
|
|
459
|
+
* @return {Map<string,string>} url → status
|
|
460
|
+
*/
|
|
461
|
+
function buildAnswerImageStatus( ua = {}, forcedStatus = '' ) {
|
|
462
|
+
const statusByUrl = new Map();
|
|
463
|
+
const byName = ua.imageStatus || {};
|
|
464
|
+
const values = Array.isArray( ua.runAIValues ) ? ua.runAIValues : [];
|
|
465
|
+
let idx = 0;
|
|
466
|
+
|
|
467
|
+
const assign = ( url ) => {
|
|
468
|
+
if ( !url || typeof url !== 'string' || statusByUrl.has( url ) ) return;
|
|
469
|
+
let status = forcedStatus;
|
|
470
|
+
if ( !status ) {
|
|
471
|
+
const key = imageBasename( url );
|
|
472
|
+
status = ( key in byName ) ? byName[key] : verdictFromValue( values[idx] );
|
|
473
|
+
idx++;
|
|
474
|
+
}
|
|
475
|
+
statusByUrl.set( url, status );
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
if ( ua.answerType === 'image' && ua.answer ) assign( ua.answer );
|
|
479
|
+
|
|
480
|
+
if ( ua.validation ) {
|
|
481
|
+
if ( ua.validationDisplayType === 'image' && ua.validationAnswer ) assign( ua.validationAnswer );
|
|
482
|
+
if ( ua.validationDisplayType === 'multiImage' && Array.isArray( ua.validationImage ) ) {
|
|
483
|
+
ua.validationImage.forEach( assign );
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return statusByUrl;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Resolves a 'Matched' / 'Not Matched' detection status from runAIData. Handles both
|
|
493
|
+
* runAIData shapes (per-image nested results, and flat). Any non-match verdict makes
|
|
494
|
+
* the whole status 'Not Matched'. Returns '' when there is no runAI verdict to report.
|
|
495
|
+
* @param {Array} runAIData
|
|
496
|
+
* @return {string} 'Matched' | 'Not Matched' | ''
|
|
497
|
+
*/
|
|
498
|
+
function getDetectionStatus( runAIData ) {
|
|
499
|
+
const values = getRunAIMatchValues( runAIData );
|
|
500
|
+
if ( !values.length ) return '';
|
|
501
|
+
const allMatched = values.every( ( v ) => v === 'True' || v === true );
|
|
502
|
+
return allMatched ? 'Matched' : 'Not Matched';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
|
|
239
506
|
/**
|
|
240
507
|
|
|
241
508
|
* Map section array { sectionName, questions[] } (view API or processed checklist) to scores / template sections.
|
|
@@ -257,6 +524,8 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
257
524
|
|
|
258
525
|
const flags = [];
|
|
259
526
|
|
|
527
|
+
let hasSopFlag = false;
|
|
528
|
+
|
|
260
529
|
|
|
261
530
|
( questionAnswer || [] ).forEach( ( section, sectionIdx ) => {
|
|
262
531
|
let sectionScore = 0;
|
|
@@ -278,6 +547,10 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
278
547
|
|
|
279
548
|
const ua = userAnswersWithRef[0];
|
|
280
549
|
|
|
550
|
+
if ( !hasSopFlag && userAnswersWithRef.some( ( entry ) => entry.sopFlag === true ) ) {
|
|
551
|
+
hasSopFlag = true;
|
|
552
|
+
}
|
|
553
|
+
|
|
281
554
|
|
|
282
555
|
const max = q.compliance ? Math.max( ...q?.answers.map( ( o ) => o?.complianceScore ?? Math.max( o?.matchedCount ?? 0, o?.notMatchedCount ?? 0 ) ) ) : 0;
|
|
283
556
|
|
|
@@ -301,6 +574,22 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
301
574
|
}
|
|
302
575
|
|
|
303
576
|
|
|
577
|
+
// image / image-video aggregate their runAI verdict across all uploaded images
|
|
578
|
+
// (any 'Not Matched' → whole question 'Not Matched'). Every other type keeps
|
|
579
|
+
// each image's own verdict, so this stays '' for them.
|
|
580
|
+
let aggregateDetectionStatus = '';
|
|
581
|
+
if ( AGGREGATE_DETECTION_TYPES.includes( q.answerType ) ) {
|
|
582
|
+
const aiAnswers = q.answerType === 'image/video' ?
|
|
583
|
+
( q.userAnswer || [] ).filter( ( a ) => a?.answerType === 'image' ) :
|
|
584
|
+
( q.userAnswer || [] );
|
|
585
|
+
const matchValues = aiAnswers.flatMap( ( a ) => getRunAIMatchValues( a?.runAIData ) );
|
|
586
|
+
if ( matchValues.length ) {
|
|
587
|
+
aggregateDetectionStatus = matchValues.every( ( v ) => v === 'True' || v === true ) ?
|
|
588
|
+
'Matched' : 'Not Matched';
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
|
|
304
593
|
sectionScore += score;
|
|
305
594
|
|
|
306
595
|
sectionMax += max;
|
|
@@ -358,6 +647,8 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
358
647
|
|
|
359
648
|
answerDisplay: isYes ? 'Yes' : ( isNo ? 'No' : ( answerText && answerText.startsWith( 'http' ) ? 'Image' : ( answerText || '—' ) ) ),
|
|
360
649
|
|
|
650
|
+
mediaItems: buildQuestionMediaItems( userAnswersWithRef, aggregateDetectionStatus ),
|
|
651
|
+
|
|
361
652
|
userAnswer: userAnswersWithRef.length ? userAnswersWithRef : [ {
|
|
362
653
|
answer: '',
|
|
363
654
|
answerType: 'text',
|
|
@@ -381,15 +672,15 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
381
672
|
|
|
382
673
|
sectionName: section.sectionName || `Section ${sectionIdx + 1}`,
|
|
383
674
|
|
|
384
|
-
targetScore: sectionMax,
|
|
675
|
+
targetScore: sectionMax ? sectionMax : '--',
|
|
385
676
|
|
|
386
|
-
actualScore: sectionScore,
|
|
677
|
+
actualScore: sectionMax ? sectionScore : '--',
|
|
387
678
|
|
|
388
679
|
questionsCount,
|
|
389
680
|
|
|
390
681
|
passedCount,
|
|
391
682
|
|
|
392
|
-
percentage: sectionMax > 0 ? Math.round( ( sectionScore / sectionMax ) * 100 ) :
|
|
683
|
+
percentage: sectionMax > 0 ? Math.round( ( sectionScore / sectionMax ) * 100 ) : '--',
|
|
393
684
|
|
|
394
685
|
} );
|
|
395
686
|
|
|
@@ -415,7 +706,7 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
415
706
|
const numQuestions = questionAnswer.reduce( ( sum, s ) => sum + ( s.questions?.length || 0 ), 0 );
|
|
416
707
|
|
|
417
708
|
|
|
418
|
-
return { totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions };
|
|
709
|
+
return { totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions, hasSopFlag };
|
|
419
710
|
}
|
|
420
711
|
|
|
421
712
|
|
|
@@ -437,7 +728,7 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
|
|
|
437
728
|
|
|
438
729
|
const {
|
|
439
730
|
|
|
440
|
-
totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions,
|
|
731
|
+
totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions, hasSopFlag,
|
|
441
732
|
|
|
442
733
|
} = mapSectionsFromQuestionAnswer( questionAnswer );
|
|
443
734
|
|
|
@@ -551,7 +842,11 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
|
|
|
551
842
|
|
|
552
843
|
numFlags: typeof doc.questionFlag === 'number' ? doc.questionFlag : flags.length,
|
|
553
844
|
|
|
554
|
-
|
|
845
|
+
showFlags: hasSopFlag,
|
|
846
|
+
|
|
847
|
+
runAIFlag: typeof doc.runAIFlag === 'number' ? doc.runAIFlag : 0,
|
|
848
|
+
|
|
849
|
+
showRunAIFlag: ( typeof doc.runAIQuestionCount === 'number' ? doc.runAIQuestionCount : 0 ) > 0,
|
|
555
850
|
|
|
556
851
|
submittedBy: doc.userName || '--',
|
|
557
852
|
|
|
@@ -618,7 +913,7 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
|
|
|
618
913
|
|
|
619
914
|
const {
|
|
620
915
|
|
|
621
|
-
totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions,
|
|
916
|
+
totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions, hasSopFlag,
|
|
622
917
|
|
|
623
918
|
} = mapSectionsFromQuestionAnswer( questionAnswer );
|
|
624
919
|
|
|
@@ -704,7 +999,11 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
|
|
|
704
999
|
|
|
705
1000
|
numFlags: checklistAnswer?.flagCount ?? flags.length,
|
|
706
1001
|
|
|
707
|
-
|
|
1002
|
+
showFlags: hasSopFlag,
|
|
1003
|
+
|
|
1004
|
+
runAIFlag: checklistAnswer?.runAIFlag ?? checklistInfo?.runAIFlag ?? checklistAnswer?.aiBreachedCount ?? 0,
|
|
1005
|
+
|
|
1006
|
+
showRunAIFlag: ( checklistInfo?.runAIQuestionCount ?? checklistAnswer?.runAIQuestionCount ?? 0 ) > 0,
|
|
708
1007
|
|
|
709
1008
|
submittedBy: checklistInfo?.submittedBy || storeProfile?.userName || '--',
|
|
710
1009
|
|
|
@@ -847,11 +1146,6 @@ export function createImageCache() {
|
|
|
847
1146
|
}
|
|
848
1147
|
|
|
849
1148
|
|
|
850
|
-
if ( resolvedData.userSignature && resolvedData.userSignature.startsWith( 'http' ) ) {
|
|
851
|
-
urls.add( resolvedData.userSignature );
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
|
|
855
1149
|
const collectFromSection = ( section ) => {
|
|
856
1150
|
section.questions?.forEach( ( q ) => {
|
|
857
1151
|
if ( q.questionReferenceImage && q.questionReferenceImage.startsWith( 'http' ) ) urls.add( q.questionReferenceImage );
|
|
@@ -875,6 +1169,14 @@ export function createImageCache() {
|
|
|
875
1169
|
if ( typeof u === 'string' && u.startsWith( 'http' ) ) urls.add( u );
|
|
876
1170
|
} );
|
|
877
1171
|
}
|
|
1172
|
+
|
|
1173
|
+
ua.answerMedia?.forEach( ( m ) => {
|
|
1174
|
+
if ( typeof m.url === 'string' && m.url.startsWith( 'http' ) ) urls.add( m.url );
|
|
1175
|
+
} );
|
|
1176
|
+
} );
|
|
1177
|
+
|
|
1178
|
+
q.mediaItems?.forEach( ( m ) => {
|
|
1179
|
+
if ( typeof m.url === 'string' && m.url.startsWith( 'http' ) ) urls.add( m.url );
|
|
878
1180
|
} );
|
|
879
1181
|
} );
|
|
880
1182
|
};
|
|
@@ -917,11 +1219,6 @@ export function createImageCache() {
|
|
|
917
1219
|
}
|
|
918
1220
|
|
|
919
1221
|
|
|
920
|
-
if ( resolvedData.userSignature ) {
|
|
921
|
-
resolvedData.userSignature = cache.get( resolvedData.userSignature ) || resolvedData.userSignature;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
|
|
925
1222
|
const replaceInSection = ( section ) => {
|
|
926
1223
|
section.questions?.forEach( ( q ) => {
|
|
927
1224
|
if ( q.questionReferenceImage && cache.has( q.questionReferenceImage ) ) q.questionReferenceImage = cache.get( q.questionReferenceImage );
|
|
@@ -943,7 +1240,19 @@ export function createImageCache() {
|
|
|
943
1240
|
if ( ua.validationDisplayType === 'multiImage' && ua.validationImage?.length ) {
|
|
944
1241
|
ua.validationImage = ua.validationImage.map( ( u ) => ( cache.has( u ) ? cache.get( u ) : u ) );
|
|
945
1242
|
}
|
|
1243
|
+
|
|
1244
|
+
if ( ua.answerMedia?.length ) {
|
|
1245
|
+
ua.answerMedia.forEach( ( m ) => {
|
|
1246
|
+
if ( cache.has( m.url ) ) m.url = cache.get( m.url );
|
|
1247
|
+
} );
|
|
1248
|
+
}
|
|
946
1249
|
} );
|
|
1250
|
+
|
|
1251
|
+
if ( q.mediaItems?.length ) {
|
|
1252
|
+
q.mediaItems.forEach( ( m ) => {
|
|
1253
|
+
if ( cache.has( m.url ) ) m.url = cache.get( m.url );
|
|
1254
|
+
} );
|
|
1255
|
+
}
|
|
947
1256
|
} );
|
|
948
1257
|
};
|
|
949
1258
|
|
|
@@ -1024,7 +1333,19 @@ export function resolveTemplateUrls( templateData, baseUrl = 'https://d1r0hc2ssk
|
|
|
1024
1333
|
if ( ua.answer && ua.answerType !== 'text' ) {
|
|
1025
1334
|
ua.answer = resolveUrl( ua.answer );
|
|
1026
1335
|
}
|
|
1336
|
+
|
|
1337
|
+
if ( ua.answerMedia?.length ) {
|
|
1338
|
+
ua.answerMedia.forEach( ( m ) => {
|
|
1339
|
+
m.url = resolveUrl( m.url );
|
|
1340
|
+
} );
|
|
1341
|
+
}
|
|
1027
1342
|
} );
|
|
1343
|
+
|
|
1344
|
+
if ( q.mediaItems?.length ) {
|
|
1345
|
+
q.mediaItems.forEach( ( m ) => {
|
|
1346
|
+
m.url = resolveUrl( m.url );
|
|
1347
|
+
} );
|
|
1348
|
+
}
|
|
1028
1349
|
} );
|
|
1029
1350
|
};
|
|
1030
1351
|
|
|
@@ -1046,10 +1367,6 @@ export function resolveTemplateUrls( templateData, baseUrl = 'https://d1r0hc2ssk
|
|
|
1046
1367
|
resolvedData.userImage = resolveUrl( resolvedData.userImage );
|
|
1047
1368
|
}
|
|
1048
1369
|
|
|
1049
|
-
if ( resolvedData?.userSignature && !resolvedData.userSignature.startsWith( 'http' ) && !resolvedData.userSignature.startsWith( 'data:' ) ) {
|
|
1050
|
-
resolvedData.userSignature = resolveUrl( resolvedData.userSignature );
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
1370
|
|
|
1054
1371
|
return resolvedData;
|
|
1055
1372
|
}
|
|
@@ -1168,7 +1485,19 @@ export async function generateVisitChecklistPDF( templateData, baseUrl = 'https:
|
|
|
1168
1485
|
if ( ua.answer && ua.answerType !== 'text' ) {
|
|
1169
1486
|
ua.answer = resolveUrl( ua.answer );
|
|
1170
1487
|
}
|
|
1488
|
+
|
|
1489
|
+
if ( ua.answerMedia?.length ) {
|
|
1490
|
+
ua.answerMedia.forEach( ( m ) => {
|
|
1491
|
+
m.url = resolveUrl( m.url );
|
|
1492
|
+
} );
|
|
1493
|
+
}
|
|
1171
1494
|
} );
|
|
1495
|
+
|
|
1496
|
+
if ( q.mediaItems?.length ) {
|
|
1497
|
+
q.mediaItems.forEach( ( m ) => {
|
|
1498
|
+
m.url = resolveUrl( m.url );
|
|
1499
|
+
} );
|
|
1500
|
+
}
|
|
1172
1501
|
} );
|
|
1173
1502
|
};
|
|
1174
1503
|
|
|
@@ -1186,10 +1515,6 @@ export async function generateVisitChecklistPDF( templateData, baseUrl = 'https:
|
|
|
1186
1515
|
resolvedData.userImage = resolveUrl( resolvedData.userImage );
|
|
1187
1516
|
}
|
|
1188
1517
|
|
|
1189
|
-
if ( resolvedData.userSignature && !resolvedData.userSignature.startsWith( 'http' ) && !resolvedData.userSignature.startsWith( 'data:' ) ) {
|
|
1190
|
-
resolvedData.userSignature = resolveUrl( resolvedData.userSignature );
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
1518
|
|
|
1194
1519
|
const html = template( resolvedData );
|
|
1195
1520
|
|