tango-app-api-trax 3.8.18 → 3.8.20
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
|
@@ -35,6 +35,8 @@ import fs from 'fs';
|
|
|
35
35
|
import path from 'path';
|
|
36
36
|
import { fileURLToPath as toPath } from 'url';
|
|
37
37
|
import * as cameraService from '../services/camera.service.js';
|
|
38
|
+
import * as recurringFlagTracker from '../services/recurringFlagTracker.service.js';
|
|
39
|
+
import ExcelJS from 'exceljs';
|
|
38
40
|
|
|
39
41
|
const __ctrlDir = path.dirname( toPath( import.meta.url ) );
|
|
40
42
|
const tangoyeLogoSvg = fs.readFileSync(
|
|
@@ -4224,3 +4226,283 @@ export async function getEyetestStream( req, res ) {
|
|
|
4224
4226
|
return res.sendError( e, 500 );
|
|
4225
4227
|
}
|
|
4226
4228
|
}
|
|
4229
|
+
|
|
4230
|
+
function buildRecurringFlagExcel( rows ) {
|
|
4231
|
+
const workbook = new ExcelJS.Workbook();
|
|
4232
|
+
const sheet = workbook.addWorksheet( 'Recurring Flags' );
|
|
4233
|
+
sheet.columns = [
|
|
4234
|
+
{ header: 'Store Name', key: 'storeName', width: 25 },
|
|
4235
|
+
{ header: 'Checklist Name', key: 'checklistName', width: 30 },
|
|
4236
|
+
{ header: 'Section', key: 'sectionName', width: 25 },
|
|
4237
|
+
{ header: 'Question', key: 'questionName', width: 40 },
|
|
4238
|
+
{ header: 'Last Submitted By', key: 'lastSubmittedBy', width: 25 },
|
|
4239
|
+
{ header: 'Last Submission Date', key: 'lastSubmissionDate', width: 22 },
|
|
4240
|
+
{ header: 'Recurring Days', key: 'days', width: 16 },
|
|
4241
|
+
];
|
|
4242
|
+
sheet.getRow( 1 ).font = { bold: true };
|
|
4243
|
+
rows.forEach( ( r ) => sheet.addRow( r ) );
|
|
4244
|
+
return workbook.xlsx.writeBuffer();
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
export async function recurringFlagAlert( req, res ) {
|
|
4248
|
+
try {
|
|
4249
|
+
const checklistDetails = await CLconfig.find( {
|
|
4250
|
+
publish: true,
|
|
4251
|
+
$expr: {
|
|
4252
|
+
$gt: [
|
|
4253
|
+
{ $size: { $cond: [ { $isArray: '$recurringFlag.users' }, '$recurringFlag.users', [] ] } },
|
|
4254
|
+
0,
|
|
4255
|
+
],
|
|
4256
|
+
},
|
|
4257
|
+
}, { _id: 1, checkListName: 1, recurringFlag: 1, approver: 1, client_id: 1 } );
|
|
4258
|
+
|
|
4259
|
+
if ( !checklistDetails.length ) {
|
|
4260
|
+
return res.sendSuccess( 'No checklists configured for recurring flag' );
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
// Pending triggers will be grouped per recipient email at the end.
|
|
4264
|
+
const triggers = [];
|
|
4265
|
+
const trackerIdsToReset = [];
|
|
4266
|
+
|
|
4267
|
+
await Promise.all( checklistDetails.map( async ( cl ) => {
|
|
4268
|
+
const threshold = cl?.recurringFlag?.threshold || 3;
|
|
4269
|
+
const notifyType = cl?.recurringFlag?.notifyType || [];
|
|
4270
|
+
const users = cl?.recurringFlag?.users || [];
|
|
4271
|
+
|
|
4272
|
+
let recipients = [];
|
|
4273
|
+
if ( notifyType.includes( 'approver' ) && Array.isArray( cl.approver ) ) {
|
|
4274
|
+
recipients = cl.approver.map( ( a ) => a?.value ).filter( Boolean );
|
|
4275
|
+
}
|
|
4276
|
+
recipients = [ ...recipients, ...users.map( ( u ) => u?.value ).filter( Boolean ) ];
|
|
4277
|
+
recipients = [ ...new Set( recipients ) ];
|
|
4278
|
+
|
|
4279
|
+
if ( !recipients.length ) return;
|
|
4280
|
+
|
|
4281
|
+
// Read tracker rows that have hit threshold and have not yet been emailed for the current streak.
|
|
4282
|
+
// Submit-time updates already maintained consecutiveCount + lastFlaggedDate per (store, section, qno).
|
|
4283
|
+
const trackerRows = await recurringFlagTracker.find( {
|
|
4284
|
+
sourceCheckList_id: cl._id,
|
|
4285
|
+
consecutiveCount: { $gte: threshold },
|
|
4286
|
+
$expr: { $ne: [ { $ifNull: [ '$lastEmailDate', '' ] }, { $ifNull: [ '$lastFlaggedDate', '' ] } ] },
|
|
4287
|
+
} );
|
|
4288
|
+
|
|
4289
|
+
for ( const t of trackerRows ) {
|
|
4290
|
+
for ( const recipient of recipients ) {
|
|
4291
|
+
triggers.push( {
|
|
4292
|
+
recipient,
|
|
4293
|
+
clientId: t.client_id,
|
|
4294
|
+
storeId: t.store_id,
|
|
4295
|
+
storeName: t.storeName,
|
|
4296
|
+
checklistId: cl._id.toString(),
|
|
4297
|
+
checklistName: cl.checkListName?.trim() || t.checkListName || '',
|
|
4298
|
+
sectionName: t.sectionName,
|
|
4299
|
+
qno: t.qno,
|
|
4300
|
+
qname: t.qname,
|
|
4301
|
+
days: t.consecutiveCount,
|
|
4302
|
+
lastSubmittedBy: t.lastSubmittedBy || '--',
|
|
4303
|
+
lastSubmissionDate: t.lastSubmissionDate || t.lastFlaggedDate || '',
|
|
4304
|
+
} );
|
|
4305
|
+
}
|
|
4306
|
+
trackerIdsToReset.push( { _id: t._id, lastFlaggedDate: t.lastFlaggedDate } );
|
|
4307
|
+
}
|
|
4308
|
+
} ) );
|
|
4309
|
+
|
|
4310
|
+
if ( !triggers.length ) {
|
|
4311
|
+
return res.sendSuccess( 'No recurring flags reached threshold' );
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
// Group triggers by recipient.
|
|
4315
|
+
const byRecipient = new Map();
|
|
4316
|
+
for ( const t of triggers ) {
|
|
4317
|
+
if ( !byRecipient.has( t.recipient ) ) byRecipient.set( t.recipient, [] );
|
|
4318
|
+
byRecipient.get( t.recipient ).push( t );
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
const flagDomain = `${JSON.parse( process.env.URL ).domain}/manage/trax/flags?date=${dayjs().format( 'YYYY-MM-DD' )}`;
|
|
4322
|
+
const fileContent = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/recurringFlag.hbs', 'utf8' );
|
|
4323
|
+
const compiled = handlebars.compile( fileContent );
|
|
4324
|
+
|
|
4325
|
+
const sentSummary = [];
|
|
4326
|
+
|
|
4327
|
+
await Promise.all( [ ...byRecipient.entries() ].map( async ( [ recipient, items ] ) => {
|
|
4328
|
+
const stores = new Set( items.map( ( i ) => i.storeId ) );
|
|
4329
|
+
const checklists = new Set( items.map( ( i ) => i.checklistId ) );
|
|
4330
|
+
const isMultiStore = stores.size > 1;
|
|
4331
|
+
const isMultiChecklist = !isMultiStore && checklists.size > 1;
|
|
4332
|
+
// Threshold for the message line — when grouping spans multiple checklists/stores, take min threshold seen.
|
|
4333
|
+
const thresholdShown = items.reduce( ( acc, it ) => Math.min( acc, it.days ), items[0].days );
|
|
4334
|
+
|
|
4335
|
+
const rows = items.map( ( i ) => ( {
|
|
4336
|
+
storeName: i.storeName,
|
|
4337
|
+
checklistName: i.checklistName,
|
|
4338
|
+
sectionName: i.sectionName,
|
|
4339
|
+
questionName: i.qname,
|
|
4340
|
+
lastSubmittedBy: i.lastSubmittedBy,
|
|
4341
|
+
lastSubmissionDate: i.lastSubmissionDate,
|
|
4342
|
+
days: i.days,
|
|
4343
|
+
} ) );
|
|
4344
|
+
|
|
4345
|
+
const ATTACHMENT_THRESHOLD = 10;
|
|
4346
|
+
const hasAttachment = ( isMultiStore || isMultiChecklist ) && rows.length > ATTACHMENT_THRESHOLD;
|
|
4347
|
+
const displayRows = hasAttachment ? rows.slice( 0, ATTACHMENT_THRESHOLD ) : rows;
|
|
4348
|
+
|
|
4349
|
+
const data = {
|
|
4350
|
+
threshold: thresholdShown,
|
|
4351
|
+
isMultiStore,
|
|
4352
|
+
isMultiChecklist,
|
|
4353
|
+
showTable: isMultiStore || isMultiChecklist,
|
|
4354
|
+
hasAttachment,
|
|
4355
|
+
domain: flagDomain,
|
|
4356
|
+
rows: displayRows,
|
|
4357
|
+
};
|
|
4358
|
+
|
|
4359
|
+
if ( isMultiStore ) {
|
|
4360
|
+
data.highlights = {
|
|
4361
|
+
totalStores: stores.size,
|
|
4362
|
+
totalChecklists: checklists.size,
|
|
4363
|
+
totalFlags: items.length,
|
|
4364
|
+
};
|
|
4365
|
+
} else if ( isMultiChecklist ) {
|
|
4366
|
+
data.storeName = items[0].storeName;
|
|
4367
|
+
} else {
|
|
4368
|
+
const single = items[0];
|
|
4369
|
+
data.storeName = single.storeName;
|
|
4370
|
+
data.checklistName = single.checklistName;
|
|
4371
|
+
data.questionName = single.qname;
|
|
4372
|
+
data.lastSubmittedBy = single.lastSubmittedBy;
|
|
4373
|
+
data.lastSubmissionDate = single.lastSubmissionDate;
|
|
4374
|
+
data.days = single.days;
|
|
4375
|
+
data.daysPlural = single.days > 1;
|
|
4376
|
+
data.flagCount = 1;
|
|
4377
|
+
data.flagCountPlural = false;
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
const html = compiled( { data } );
|
|
4381
|
+
|
|
4382
|
+
const params = {
|
|
4383
|
+
toEmail: recipient,
|
|
4384
|
+
mailSubject: 'TangoEye | Recurring Flags Detected',
|
|
4385
|
+
htmlBody: html,
|
|
4386
|
+
attachment: '',
|
|
4387
|
+
sourceEmail: JSON.parse( process.env.SES ).adminEmail,
|
|
4388
|
+
};
|
|
4389
|
+
|
|
4390
|
+
if ( hasAttachment ) {
|
|
4391
|
+
try {
|
|
4392
|
+
const buf = await buildRecurringFlagExcel( rows );
|
|
4393
|
+
params.attachment = {
|
|
4394
|
+
filename: 'Recurring-Flags-Summary.xlsx',
|
|
4395
|
+
content: Buffer.from( buf ),
|
|
4396
|
+
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
4397
|
+
};
|
|
4398
|
+
} catch ( e ) {
|
|
4399
|
+
logger.error( { functionName: 'recurringFlagAlert.buildExcel', error: e } );
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
sendEmailWithSES( params.toEmail, params.mailSubject, params.htmlBody, params.attachment, params.sourceEmail );
|
|
4404
|
+
sentSummary.push( { recipient, count: items.length, mode: isMultiStore ? 'multi-store' : ( isMultiChecklist ? 'multi-checklist' : 'single' ) } );
|
|
4405
|
+
} ) );
|
|
4406
|
+
|
|
4407
|
+
// Reset counters and stamp lastEmailDate for all triggered tracker rows so the next streak starts fresh.
|
|
4408
|
+
if ( trackerIdsToReset.length ) {
|
|
4409
|
+
const resetOps = trackerIdsToReset.map( ( { _id, lastFlaggedDate } ) => ( {
|
|
4410
|
+
updateOne: {
|
|
4411
|
+
filter: { _id },
|
|
4412
|
+
update: { $set: { consecutiveCount: 0, lastEmailDate: lastFlaggedDate } },
|
|
4413
|
+
},
|
|
4414
|
+
} ) );
|
|
4415
|
+
await recurringFlagTracker.bulkWrite( resetOps );
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
return res.sendSuccess( { message: 'Recurring flag emails dispatched', sent: sentSummary } );
|
|
4419
|
+
} catch ( e ) {
|
|
4420
|
+
logger.error( { functionName: 'recurringFlagAlert', error: e } );
|
|
4421
|
+
return res.sendError( e, 500 );
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
export async function updateStoreLatLong( req, res ) {
|
|
4426
|
+
try {
|
|
4427
|
+
const defaultStores = [
|
|
4428
|
+
{ 'storeName': 'OFLBTM', 'lat': 12.9059052, 'long': 77.6057203 },
|
|
4429
|
+
{ 'storeName': 'OFLRRN', 'lat': 12.9096852, 'long': 77.5135813 },
|
|
4430
|
+
{ 'storeName': 'OFLGUN', 'lat': 12.9286282, 'long': 77.738055 },
|
|
4431
|
+
{ 'storeName': 'OFLJPN', 'lat': 12.8915891, 'long': 77.5776564 },
|
|
4432
|
+
{ 'storeName': 'OFLAECS', 'lat': 12.9638564, 'long': 77.7125467 },
|
|
4433
|
+
{ 'storeName': 'Seegehalli', 'lat': 13.0083064, 'long': 77.7588426 },
|
|
4434
|
+
{ 'storeName': 'OFLJPNS', 'lat': 12.8695409, 'long': 77.5820004 },
|
|
4435
|
+
{ 'storeName': 'OFLHAR', 'lat': 12.9136122, 'long': 77.6649999 },
|
|
4436
|
+
{ 'storeName': 'OFLKSNR', 'lat': 13.0051196, 'long': 77.6601987 },
|
|
4437
|
+
{ 'storeName': 'Hosa Road', 'lat': 12.8792369, 'long': 77.6721843 },
|
|
4438
|
+
{ 'storeName': 'OFLDEV', 'lat': 12.8942057, 'long': 77.6019696 },
|
|
4439
|
+
{ 'storeName': 'OFLAYN', 'lat': 12.958161, 'long': 77.570055 },
|
|
4440
|
+
{ 'storeName': 'OFLKAG', 'lat': 12.9845196, 'long': 77.6758945 },
|
|
4441
|
+
{ 'storeName': 'OFLBVR', 'lat': 12.9858428, 'long': 77.5424725 },
|
|
4442
|
+
{ 'storeName': 'OFLMTK', 'lat': 13.0279313, 'long': 77.5587934 },
|
|
4443
|
+
{ 'storeName': 'OFLMAG', 'lat': 12.9837929, 'long': 77.5325324 },
|
|
4444
|
+
{ 'storeName': 'OFLBEL', 'lat': 13.0370029, 'long': 77.5622911 },
|
|
4445
|
+
{ 'storeName': 'OFLKDU', 'lat': 12.8835726, 'long': 77.517709 },
|
|
4446
|
+
{ 'storeName': 'OFLAMLI', 'lat': 13.0685032, 'long': 77.5972815 },
|
|
4447
|
+
{ 'storeName': 'OFLSAN', 'lat': 19.0603798, 'long': 73.0041633 },
|
|
4448
|
+
{ 'storeName': 'OFLLOK', 'lat': 19.1471544, 'long': 72.5405682 },
|
|
4449
|
+
{ 'storeName': 'OFLTSTG', 'lat': 19.2471119, 'long': 72.9769107 },
|
|
4450
|
+
{ 'storeName': 'OFLKHGRS', 'lat': 19.0583611, 'long': 73.0584353 },
|
|
4451
|
+
{ 'storeName': 'OFLMAL', 'lat': 12.9673877, 'long': 77.499519 },
|
|
4452
|
+
{ 'storeName': 'OFLBAG', 'lat': 13.1218427, 'long': 77.6234015 },
|
|
4453
|
+
{ 'storeName': 'OFLYEL', 'lat': 13.114551, 'long': 77.5401312 },
|
|
4454
|
+
{ 'storeName': 'OFLTCP', 'lat': 13.0240695, 'long': 77.6928401 },
|
|
4455
|
+
{ 'storeName': 'OFLECTN', 'lat': 12.8185795, 'long': 77.6520784 },
|
|
4456
|
+
{ 'storeName': 'OFLHBL', 'lat': 13.0559812, 'long': 77.593941 },
|
|
4457
|
+
{ 'storeName': 'OFLBWR', 'lat': 12.9691559, 'long': 77.739599 },
|
|
4458
|
+
{ 'storeName': 'OFLHSR', 'lat': 12.9183666, 'long': 77.5793797 },
|
|
4459
|
+
];
|
|
4460
|
+
|
|
4461
|
+
const list = Array.isArray( req.body?.stores ) && req.body.stores.length ? req.body.stores : defaultStores;
|
|
4462
|
+
|
|
4463
|
+
const updated = [];
|
|
4464
|
+
const unchanged = [];
|
|
4465
|
+
const notFound = [];
|
|
4466
|
+
const invalid = [];
|
|
4467
|
+
|
|
4468
|
+
for ( const item of list ) {
|
|
4469
|
+
const storeName = item?.storeName;
|
|
4470
|
+
const latitude = parseFloat( item?.lat );
|
|
4471
|
+
const longitude = parseFloat( item?.long );
|
|
4472
|
+
|
|
4473
|
+
if ( !storeName || Number.isNaN( latitude ) || Number.isNaN( longitude ) ) {
|
|
4474
|
+
invalid.push( storeName || '<missing>' );
|
|
4475
|
+
continue;
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
const result = await storeService.updateOne(
|
|
4479
|
+
{ storeName, clientId: '467' },
|
|
4480
|
+
{ 'storeProfile.latitude': latitude, 'storeProfile.longitude': longitude },
|
|
4481
|
+
);
|
|
4482
|
+
|
|
4483
|
+
if ( result.matchedCount === 0 ) {
|
|
4484
|
+
notFound.push( storeName );
|
|
4485
|
+
} else if ( result.modifiedCount === 0 ) {
|
|
4486
|
+
unchanged.push( storeName );
|
|
4487
|
+
} else {
|
|
4488
|
+
updated.push( storeName );
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
logger.info( {
|
|
4493
|
+
functionName: 'updateStoreLatLong',
|
|
4494
|
+
summary: { updated: updated.length, unchanged: unchanged.length, notFound: notFound.length, invalid: invalid.length },
|
|
4495
|
+
} );
|
|
4496
|
+
|
|
4497
|
+
return res.sendSuccess( {
|
|
4498
|
+
message: 'Store lat/long update complete',
|
|
4499
|
+
updated,
|
|
4500
|
+
unchanged,
|
|
4501
|
+
notFound,
|
|
4502
|
+
invalid,
|
|
4503
|
+
} );
|
|
4504
|
+
} catch ( e ) {
|
|
4505
|
+
logger.error( { functionName: 'updateStoreLatLong', error: e } );
|
|
4506
|
+
return res.sendError( e, 500 );
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logger, signedUrl, fileUpload, getOtp, sendEmailWithSES, getUuid, insertOpenSearchData, listFileByPath, getObject, deleteFiles } from 'tango-app-api-middleware';
|
|
1
|
+
import { logger, signedUrl, fileUpload, getOtp, sendEmailWithSES, getUuid, insertOpenSearchData, listFileByPath, getObject, deleteFiles, sendMessageToQueue } from 'tango-app-api-middleware';
|
|
2
2
|
import * as processedchecklist from '../services/processedchecklist.services.js';
|
|
3
3
|
import * as processedtask from '../services/processedTaskList.service.js';
|
|
4
4
|
import * as PCLconfig from '../services/processedchecklistconfig.services.js';
|
|
@@ -26,6 +26,7 @@ dayjs.extend( customParseFormat );
|
|
|
26
26
|
dayjs.extend( timeZone );
|
|
27
27
|
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore.js';
|
|
28
28
|
import * as cameraService from '../services/camera.service.js';
|
|
29
|
+
import * as recurringFlagTracker from '../services/recurringFlagTracker.service.js';
|
|
29
30
|
dayjs.extend( isSameOrBefore );
|
|
30
31
|
import puppeteer from 'puppeteer';
|
|
31
32
|
import handlebars from './handlebar-helper.js';
|
|
@@ -2034,6 +2035,89 @@ function QuestionFlag( req, res ) {
|
|
|
2034
2035
|
}
|
|
2035
2036
|
}
|
|
2036
2037
|
|
|
2038
|
+
async function updateRecurringFlagTracker( processedChecklist, questionAnswers, currentDateTime ) {
|
|
2039
|
+
try {
|
|
2040
|
+
if ( !processedChecklist?.store_id || !processedChecklist?.sourceCheckList_id ) return;
|
|
2041
|
+
|
|
2042
|
+
const today = currentDateTime ? currentDateTime.format( 'YYYY-MM-DD' ) : dayjs().format( 'YYYY-MM-DD' );
|
|
2043
|
+
const yesterday = dayjs( today, 'YYYY-MM-DD' ).subtract( 1, 'day' ).format( 'YYYY-MM-DD' );
|
|
2044
|
+
|
|
2045
|
+
const trackerKeyBase = {
|
|
2046
|
+
client_id: processedChecklist.client_id,
|
|
2047
|
+
sourceCheckList_id: processedChecklist.sourceCheckList_id,
|
|
2048
|
+
store_id: processedChecklist.store_id,
|
|
2049
|
+
};
|
|
2050
|
+
|
|
2051
|
+
const existingRows = await recurringFlagTracker.find( trackerKeyBase, { section_id: 1, qno: 1, consecutiveCount: 1, lastFlaggedDate: 1 } );
|
|
2052
|
+
const existingMap = new Map();
|
|
2053
|
+
for ( const r of existingRows ) {
|
|
2054
|
+
existingMap.set( `${r.section_id || ''}::${r.qno || ''}`, r );
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
const ops = [];
|
|
2058
|
+
|
|
2059
|
+
( questionAnswers || [] ).forEach( ( section ) => {
|
|
2060
|
+
( section?.questions || [] ).forEach( ( question ) => {
|
|
2061
|
+
const sectionId = section?.section_id || '';
|
|
2062
|
+
const qno = question?.qno || '';
|
|
2063
|
+
if ( qno === '' ) return;
|
|
2064
|
+
|
|
2065
|
+
const flaggedNow = Array.isArray( question?.userAnswer ) && question.userAnswer.some( ( a ) => a?.sopFlag === true );
|
|
2066
|
+
const key = `${sectionId}::${qno}`;
|
|
2067
|
+
const existing = existingMap.get( key );
|
|
2068
|
+
|
|
2069
|
+
if ( flaggedNow ) {
|
|
2070
|
+
let newCount;
|
|
2071
|
+
if ( !existing ) {
|
|
2072
|
+
newCount = 1;
|
|
2073
|
+
} else if ( existing.lastFlaggedDate === today ) {
|
|
2074
|
+
// Multi-submit on same day: already counted, no-op.
|
|
2075
|
+
return;
|
|
2076
|
+
} else if ( existing.lastFlaggedDate === yesterday ) {
|
|
2077
|
+
newCount = ( existing.consecutiveCount || 0 ) + 1;
|
|
2078
|
+
} else {
|
|
2079
|
+
// Gap broke the streak — fresh start.
|
|
2080
|
+
newCount = 1;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
ops.push( {
|
|
2084
|
+
updateOne: {
|
|
2085
|
+
filter: { ...trackerKeyBase, section_id: sectionId, qno },
|
|
2086
|
+
update: {
|
|
2087
|
+
$set: {
|
|
2088
|
+
checkListName: processedChecklist.checkListName,
|
|
2089
|
+
storeName: processedChecklist.storeName,
|
|
2090
|
+
sectionName: section?.sectionName || '',
|
|
2091
|
+
qname: question?.qname || '',
|
|
2092
|
+
consecutiveCount: newCount,
|
|
2093
|
+
lastFlaggedDate: today,
|
|
2094
|
+
lastSubmittedBy: processedChecklist.userName || processedChecklist.userEmail || '--',
|
|
2095
|
+
lastSubmissionDate: currentDateTime ? currentDateTime.format( 'hh:mm A, DD MMM YYYY' ) : today,
|
|
2096
|
+
},
|
|
2097
|
+
},
|
|
2098
|
+
upsert: true,
|
|
2099
|
+
},
|
|
2100
|
+
} );
|
|
2101
|
+
} else if ( existing && ( existing.consecutiveCount || 0 ) > 0 ) {
|
|
2102
|
+
// Question answered correctly today — break the streak.
|
|
2103
|
+
ops.push( {
|
|
2104
|
+
updateOne: {
|
|
2105
|
+
filter: { ...trackerKeyBase, section_id: sectionId, qno },
|
|
2106
|
+
update: { $set: { consecutiveCount: 0, lastFlaggedDate: today } },
|
|
2107
|
+
},
|
|
2108
|
+
} );
|
|
2109
|
+
}
|
|
2110
|
+
} );
|
|
2111
|
+
} );
|
|
2112
|
+
|
|
2113
|
+
if ( ops.length ) {
|
|
2114
|
+
await recurringFlagTracker.bulkWrite( ops );
|
|
2115
|
+
}
|
|
2116
|
+
} catch ( error ) {
|
|
2117
|
+
logger.error( { function: 'updateRecurringFlagTracker', error } );
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2037
2121
|
export async function submitChecklist( req, res ) {
|
|
2038
2122
|
try {
|
|
2039
2123
|
let requestData = req.body;
|
|
@@ -2169,6 +2253,7 @@ export async function submitChecklist( req, res ) {
|
|
|
2169
2253
|
// };
|
|
2170
2254
|
// await detectionService.create( detectionData );
|
|
2171
2255
|
if ( requestData.submittype == 'submit' ) {
|
|
2256
|
+
updateRecurringFlagTracker( getchecklist[0], requestData.questionAnswers, currentDateTime );
|
|
2172
2257
|
updateOpenSearch( req.user, requestData );
|
|
2173
2258
|
let openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
2174
2259
|
// console.log( 'openSearch', openSearch );
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
|
7
|
+
<title>Recurring Flags Detected</title>
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
9
|
+
<style type="text/css">
|
|
10
|
+
body { font-family: "Inter", sans-serif !important; }
|
|
11
|
+
body, table, td, a { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; }
|
|
12
|
+
table, td { mso-table-rspace: 0pt; mso-table-lspace: 0pt; }
|
|
13
|
+
img { -ms-interpolation-mode: bicubic; }
|
|
14
|
+
a[x-apple-data-detectors] {
|
|
15
|
+
font-family: "inherit" !important;
|
|
16
|
+
font-size: inherit !important;
|
|
17
|
+
font-weight: inherit !important;
|
|
18
|
+
line-height: inherit !important;
|
|
19
|
+
color: inherit !important;
|
|
20
|
+
text-decoration: none !important;
|
|
21
|
+
}
|
|
22
|
+
body { width: 100% !important; height: 100% !important; padding: 0 !important; margin: 0 !important; }
|
|
23
|
+
table { border-collapse: collapse !important; }
|
|
24
|
+
a { color: #1a82e2; }
|
|
25
|
+
img { height: auto; line-height: 100%; text-decoration: none; border: 0; outline: none; }
|
|
26
|
+
.flagText { font-size: 16px; font-weight: 600; line-height: 24px; text-align: left; }
|
|
27
|
+
.rfTable { width: 100%; border-collapse: collapse; }
|
|
28
|
+
.rfTable th, .rfTable td {
|
|
29
|
+
border: 1px solid #E2E8F0;
|
|
30
|
+
padding: 8px 12px;
|
|
31
|
+
font-size: 13px;
|
|
32
|
+
text-align: left;
|
|
33
|
+
color: #202B3C;
|
|
34
|
+
}
|
|
35
|
+
.rfTable th { background-color: #F1F5F9; font-weight: 600; }
|
|
36
|
+
.highlight { font-size: 14px; color: #202B3C; line-height: 22px; }
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
|
|
40
|
+
<body style="background-color: #dbe5ea;">
|
|
41
|
+
<div class="preheader" style="display: none; max-width: 0; max-height: 0; overflow: hidden; font-size: 1px; line-height: 1px; color: #fff; opacity: 0;">
|
|
42
|
+
Recurring Flags Detected
|
|
43
|
+
</div>
|
|
44
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-left:10px;padding-right:10px">
|
|
45
|
+
<tr>
|
|
46
|
+
<td bgcolor="#dbe5ea" style="padding:32px 10px 0 10px">
|
|
47
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 680px;" align="center">
|
|
48
|
+
<tr>
|
|
49
|
+
<td style="margin-top: 0px;margin-bottom: 0px;font-size: 16px;line-height: 24px;background-color: #ffffff;padding-left: 18px;padding-right: 24px;padding-top: 24px;padding-bottom: 24px;">
|
|
50
|
+
<p style="margin-top: 0px;margin-bottom: 0px;">
|
|
51
|
+
<a href="https://tangoeye.ai/" style="text-decoration: none;outline: none;color: #ffffff;">
|
|
52
|
+
<img src="https://devtangoretail-api.tangoeye.ai/logo.png" width="200" height="100" alt="TangoEye"
|
|
53
|
+
style="-ms-interpolation-mode: bicubic;vertical-align: middle;border: 0;line-height: 100%;height: auto;outline: none;text-decoration: none;">
|
|
54
|
+
</a>
|
|
55
|
+
</p>
|
|
56
|
+
</td>
|
|
57
|
+
</tr>
|
|
58
|
+
<tr>
|
|
59
|
+
<td align="left" bgcolor="#ffffff" style="padding-left: 30px;padding-right: 24px;font-size: 14px; line-height: 24px;">
|
|
60
|
+
<p style="width: 624px;height: 0px;border: 1px solid #CBD5E1;flex: none;order: 1;flex-grow: 0;"></p>
|
|
61
|
+
</td>
|
|
62
|
+
</tr>
|
|
63
|
+
|
|
64
|
+
{{!-- Intro line --}}
|
|
65
|
+
<tr>
|
|
66
|
+
<td style="background-color: #ffffff;padding-left: 30px;padding-right: 24px;padding-bottom: 10px;">
|
|
67
|
+
<div style="margin-top: 0px;margin-bottom: 0px;font-size: 16px;line-height: 28px;color: #82899a;">
|
|
68
|
+
<span style="font-weight: 400;color: #121A26;line-height: 140%;">
|
|
69
|
+
Hi,<br/>
|
|
70
|
+
{{#if data.isMultiStore}}
|
|
71
|
+
Recurring flags has been detected across multiple stores and across multiple checklists on recent submissions, exceeding the configured threshold of {{data.threshold}} occurrences.
|
|
72
|
+
{{else if data.isMultiChecklist}}
|
|
73
|
+
A Recurring flags has been identified for store <b>{{data.storeName}}</b> across multiple checklists on recent submissions, exceeding the configured threshold of {{data.threshold}} occurrences.
|
|
74
|
+
{{else}}
|
|
75
|
+
A recurring flag has been identified for store <b>{{data.storeName}}</b>.where a question has been flagged multiple times in recent {{data.checklistName}} submissions, exceeding the configured threshold of {{data.threshold}} occurrences.
|
|
76
|
+
{{/if}}
|
|
77
|
+
</span>
|
|
78
|
+
</div>
|
|
79
|
+
</td>
|
|
80
|
+
</tr>
|
|
81
|
+
|
|
82
|
+
{{!-- Multi-store: Key Highlights --}}
|
|
83
|
+
{{#if data.isMultiStore}}
|
|
84
|
+
<tr>
|
|
85
|
+
<td style="background-color: #ffffff;padding-left: 30px;padding-right: 24px;padding-bottom: 10px;">
|
|
86
|
+
<div class="highlight">
|
|
87
|
+
<b>Key Highlights:</b>
|
|
88
|
+
<ul style="margin:8px 0 0 0;padding-left:18px;">
|
|
89
|
+
<li>Total Stores with Recurring Flags: {{data.highlights.totalStores}}</li>
|
|
90
|
+
<li>Total Checklists with Recurring Flags: {{data.highlights.totalChecklists}}</li>
|
|
91
|
+
<li>Total Recurring Flags: {{data.highlights.totalFlags}}</li>
|
|
92
|
+
</ul>
|
|
93
|
+
</div>
|
|
94
|
+
</td>
|
|
95
|
+
</tr>
|
|
96
|
+
{{/if}}
|
|
97
|
+
|
|
98
|
+
{{!-- Section header for details --}}
|
|
99
|
+
<tr>
|
|
100
|
+
<td style="background-color: #ffffff;padding-left: 30px;padding-right: 24px;padding-bottom: 10px;">
|
|
101
|
+
<div style="font-size: 16px;line-height: 28px;color: #121A26;font-weight:600;">
|
|
102
|
+
{{#if data.isMultiStore}}
|
|
103
|
+
Details of the Recurring Flags are as follows:
|
|
104
|
+
{{else if data.isMultiChecklist}}
|
|
105
|
+
Details of the Recurring Flags from Store {{data.storeName}} (last {{data.threshold}} days):
|
|
106
|
+
{{else}}
|
|
107
|
+
Details of the Recurring Flags from checklist are as follows:
|
|
108
|
+
{{/if}}
|
|
109
|
+
</div>
|
|
110
|
+
</td>
|
|
111
|
+
</tr>
|
|
112
|
+
</table>
|
|
113
|
+
</td>
|
|
114
|
+
</tr>
|
|
115
|
+
|
|
116
|
+
{{!-- ============ Single store + single checklist body ============ --}}
|
|
117
|
+
{{#unless data.isMultiChecklist}}{{#unless data.isMultiStore}}
|
|
118
|
+
<tr>
|
|
119
|
+
<td align="center" bgcolor="#dbe5ea" style="padding:0 10px 0 10px">
|
|
120
|
+
<table align="center" bgcolor="#ffffff" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 680px;">
|
|
121
|
+
<tr bgcolor="#ffffff">
|
|
122
|
+
<td class="flagText" style="padding-left:30px;line-height: 24px;color:#000000">Store Name :</td>
|
|
123
|
+
<td></td><td></td>
|
|
124
|
+
<td class="flagText">{{data.storeName}}</td>
|
|
125
|
+
</tr>
|
|
126
|
+
<tr bgcolor="#ffffff">
|
|
127
|
+
<td class="flagText" style="padding-left:30px;line-height: 24px;">Checklist Name :</td>
|
|
128
|
+
<td></td><td></td>
|
|
129
|
+
<td class="flagText">{{data.checklistName}}</td>
|
|
130
|
+
</tr>
|
|
131
|
+
<tr bgcolor="#ffffff">
|
|
132
|
+
<td class="flagText" style="padding-left:30px;line-height: 24px;">Last Submitted By :</td>
|
|
133
|
+
<td></td><td></td>
|
|
134
|
+
<td class="flagText">{{data.lastSubmittedBy}}</td>
|
|
135
|
+
</tr>
|
|
136
|
+
<tr bgcolor="#ffffff">
|
|
137
|
+
<td class="flagText" style="padding-left:30px;line-height: 24px;">Last Submission Date :</td>
|
|
138
|
+
<td></td><td></td>
|
|
139
|
+
<td class="flagText">{{data.lastSubmissionDate}}</td>
|
|
140
|
+
</tr>
|
|
141
|
+
<tr bgcolor="#ffffff">
|
|
142
|
+
<td class="flagText" style="padding-left:30px;line-height: 24px;">No of Flags :</td>
|
|
143
|
+
<td></td><td></td>
|
|
144
|
+
<td class="flagText">{{data.flagCount}} Question Flag{{#if data.flagCountPlural}}s{{/if}} - "{{data.questionName}}" has flagged for {{data.days}} day{{#if data.daysPlural}}s{{/if}} in a Row</td>
|
|
145
|
+
</tr>
|
|
146
|
+
</table>
|
|
147
|
+
</td>
|
|
148
|
+
</tr>
|
|
149
|
+
{{/unless}}{{/unless}}
|
|
150
|
+
|
|
151
|
+
{{!-- ============ Tabular body for multi-checklist or multi-store ============ --}}
|
|
152
|
+
{{#if data.showTable}}
|
|
153
|
+
<tr>
|
|
154
|
+
<td align="center" bgcolor="#dbe5ea" style="padding:0 10px 0 10px">
|
|
155
|
+
<table align="center" bgcolor="#ffffff" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 680px;">
|
|
156
|
+
<tr>
|
|
157
|
+
<td style="padding:10px 30px 10px 30px;background-color:#ffffff;">
|
|
158
|
+
<table class="rfTable">
|
|
159
|
+
<thead>
|
|
160
|
+
<tr>
|
|
161
|
+
{{#if data.isMultiStore}}<th>Store Name</th>{{/if}}
|
|
162
|
+
<th>Checklist Name</th>
|
|
163
|
+
<th>Question</th>
|
|
164
|
+
<th>Last Submitted By</th>
|
|
165
|
+
<th>Last Submission Date</th>
|
|
166
|
+
<th>Total Recurring Flags</th>
|
|
167
|
+
</tr>
|
|
168
|
+
</thead>
|
|
169
|
+
<tbody>
|
|
170
|
+
{{#each data.rows}}
|
|
171
|
+
<tr>
|
|
172
|
+
{{#if ../data.isMultiStore}}<td>{{this.storeName}}</td>{{/if}}
|
|
173
|
+
<td>{{this.checklistName}}</td>
|
|
174
|
+
<td>{{this.questionName}}</td>
|
|
175
|
+
<td>{{this.lastSubmittedBy}}</td>
|
|
176
|
+
<td>{{this.lastSubmissionDate}}</td>
|
|
177
|
+
<td>{{this.days}}</td>
|
|
178
|
+
</tr>
|
|
179
|
+
{{/each}}
|
|
180
|
+
</tbody>
|
|
181
|
+
</table>
|
|
182
|
+
</td>
|
|
183
|
+
</tr>
|
|
184
|
+
</table>
|
|
185
|
+
</td>
|
|
186
|
+
</tr>
|
|
187
|
+
{{/if}}
|
|
188
|
+
|
|
189
|
+
{{!-- View Flags button --}}
|
|
190
|
+
<tr>
|
|
191
|
+
<td align="center" bgcolor="#dbe5ea" style="padding:0 10px 0 10px">
|
|
192
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 680px;">
|
|
193
|
+
<tr>
|
|
194
|
+
<td bgcolor="#ffffff" style="padding: 20px;padding-top:15px;padding-left:30px;">
|
|
195
|
+
<table border="0" cellpadding="0" cellspacing="0">
|
|
196
|
+
<tr>
|
|
197
|
+
<td align="center" bgcolor="#00A3FF" style="border-radius: 6px;height:50px;">
|
|
198
|
+
<a href="{{data.domain}}" target="_blank"
|
|
199
|
+
style="display: inline-block; padding: 16px 36px;font-size: 16px; color: #ffffff; text-decoration: none; border-radius: 6px;">
|
|
200
|
+
View Flags
|
|
201
|
+
</a>
|
|
202
|
+
</td>
|
|
203
|
+
</tr>
|
|
204
|
+
</table>
|
|
205
|
+
</td>
|
|
206
|
+
</tr>
|
|
207
|
+
</table>
|
|
208
|
+
</td>
|
|
209
|
+
</tr>
|
|
210
|
+
|
|
211
|
+
{{!-- Attachment hint when full data exceeds inline limit --}}
|
|
212
|
+
{{#if data.hasAttachment}}
|
|
213
|
+
<tr>
|
|
214
|
+
<td align="center" bgcolor="#dbe5ea" style="padding:0 10px 0 10px">
|
|
215
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 680px;">
|
|
216
|
+
<tr>
|
|
217
|
+
<td bgcolor="#ffffff" style="padding:0 30px 15px 30px;font-size:13px;color:#202B3C;">
|
|
218
|
+
Refer to the attached file for a detailed breakdown of the Full report
|
|
219
|
+
</td>
|
|
220
|
+
</tr>
|
|
221
|
+
</table>
|
|
222
|
+
</td>
|
|
223
|
+
</tr>
|
|
224
|
+
{{/if}}
|
|
225
|
+
|
|
226
|
+
<tr>
|
|
227
|
+
<td align="center" bgcolor="#dbe5ea" style="padding:0 10px 32px 10px;">
|
|
228
|
+
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 680px;">
|
|
229
|
+
<tr>
|
|
230
|
+
<td style="background-color: #ffffff;padding-left: 30px;padding-right: 24px;padding-top:5px">
|
|
231
|
+
<div style="font-size: 12px;color: #202B3C;font-weight: 400;line-height: 150%;">
|
|
232
|
+
<p>If you have any questions or need assistance, please reach out to us at support@tangotech.co.in.</p>
|
|
233
|
+
</div>
|
|
234
|
+
</td>
|
|
235
|
+
</tr>
|
|
236
|
+
<tr>
|
|
237
|
+
<td style="background-color: #ffffff;padding-left: 30px;padding-right: 24px;padding-bottom:15px">
|
|
238
|
+
<div style="font-size: 12px;color: #202B3C;font-weight: 400;line-height: 150%;">
|
|
239
|
+
<p>© Tango Eye. All rights reserved.</p>
|
|
240
|
+
</div>
|
|
241
|
+
</td>
|
|
242
|
+
</tr>
|
|
243
|
+
</table>
|
|
244
|
+
</td>
|
|
245
|
+
</tr>
|
|
246
|
+
</table>
|
|
247
|
+
</body>
|
|
248
|
+
</html>
|
|
@@ -37,12 +37,14 @@ internalTraxRouter
|
|
|
37
37
|
.post( '/getSubmissionDetails', isAllowedInternalAPIHandler, internalController.checklistTaskSubmissionDetails )
|
|
38
38
|
.post( '/posblock', isAllowedInternalAPIHandler, internalController.getStoreTaskDetails )
|
|
39
39
|
.post( '/runAIFlag', isAllowedInternalAPIHandler, internalController.runAIFlag )
|
|
40
|
+
.post( '/recurringFlag', isAllowedInternalAPIHandler, internalController.recurringFlagAlert )
|
|
40
41
|
.post( '/downloadInsertPdf', isAllowedInternalAPIHandler, internalController.downloadInsertPdf )
|
|
41
42
|
.get( '/checklistAutoMailList', isAllowedInternalAPIHandler, internalController.checklistAutoMailList )
|
|
42
43
|
.post( '/sendAIEmailList', isAllowedInternalAPIHandler, internalController.sendAIEmailList )
|
|
43
44
|
.post( '/liveAiPushNotificationAlert', isAllowedInternalAPIHandler, internalController.liveAiPushNotificationAlert )
|
|
44
45
|
.get( '/aiChecklistDetails', isAllowedInternalAPIHandler, internalController.aiChecklistDetails )
|
|
45
46
|
.get( '/getEyetestStream', isAllowedInternalAPIHandler, internalController.getEyetestStream )
|
|
47
|
+
.post( '/updateStoreLatLong', isAllowedInternalAPIHandler, internalController.updateStoreLatLong )
|
|
46
48
|
;
|
|
47
49
|
|
|
48
50
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import model from 'tango-api-schema';
|
|
2
|
+
|
|
3
|
+
export const findOne = async ( query={}, field={} ) => {
|
|
4
|
+
return model.recurringFlagTrackerModel.findOne( query, field );
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const find = async ( query={}, field={} ) => {
|
|
8
|
+
return model.recurringFlagTrackerModel.find( query, field );
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const create = async ( document = {} ) => {
|
|
12
|
+
return model.recurringFlagTrackerModel.create( document );
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const updateOne = async ( query = {}, record={} ) => {
|
|
16
|
+
return model.recurringFlagTrackerModel.updateOne( query, { $set: record }, { upsert: true } );
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const updateMany = async ( query = {}, record={} ) => {
|
|
20
|
+
return model.recurringFlagTrackerModel.updateMany( query, { $set: record } );
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const deleteOne = async ( query = {} ) => {
|
|
24
|
+
return model.recurringFlagTrackerModel.deleteOne( query );
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const aggregate = async ( query = [] ) => {
|
|
28
|
+
return model.recurringFlagTrackerModel.aggregate( query );
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const bulkWrite = async ( ops = [] ) => {
|
|
32
|
+
return model.recurringFlagTrackerModel.bulkWrite( ops );
|
|
33
|
+
};
|