tango-app-api-audio-analytics 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tango-app-api-audio-analytics",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "audioAnalytics",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -18,6 +18,7 @@
18
18
  "cors": "^2.8.6",
19
19
  "dotenv": "^17.3.1",
20
20
  "express": "^5.2.1",
21
+ "json2csv": "^6.0.0-alpha.2",
21
22
  "mongodb": "^6.21.0",
22
23
  "nodemon": "^3.1.14",
23
24
  "tango-api-schema": "^2.5.62",
@@ -224,3 +224,79 @@ export const callExternalStreamAPI = async ( req, res ) => {
224
224
  } );
225
225
  }
226
226
  };
227
+
228
+ // ======================= CHAT STREAM API =======================
229
+
230
+ const CHAT_STREAM_API = 'http://13.232.38.210:8000/api/chat/stream';
231
+
232
+ /**
233
+ * Chat Stream API - Streams response from external AI chat API
234
+ * Accepts user data and configuration, returns streaming response
235
+ * @param {Object} req - Express request object
236
+ * @param {Object} res - Express response object
237
+ */
238
+ export async function chatStream( req, res ) {
239
+ try {
240
+ /* eslint-disable camelcase */
241
+ const {
242
+ userId,
243
+ sessionId,
244
+ sessionDate,
245
+ sessionTimezone,
246
+ message,
247
+ config,
248
+ } = req.body;
249
+
250
+ logger.info( { message: 'Received chat stream request', userId, sessionId, message } );
251
+
252
+ // Build payload for external API
253
+ const payload = {
254
+ userId,
255
+ sessionId,
256
+ sessionDate,
257
+ sessionTimezone,
258
+ message,
259
+ config,
260
+ };
261
+ /* eslint-enable camelcase */
262
+
263
+ logger.info( { message: 'Calling external chat stream API', payload } );
264
+
265
+ // Set SSE headers
266
+ res.setHeader( 'Content-Type', 'text/event-stream' );
267
+ res.setHeader( 'Cache-Control', 'no-cache' );
268
+ res.setHeader( 'Connection', 'keep-alive' );
269
+ res.setHeader( 'X-Accel-Buffering', 'no' );
270
+
271
+ // Make the external API call with streaming
272
+ const response = await fetch( CHAT_STREAM_API, {
273
+ method: 'POST',
274
+ headers: {
275
+ 'Content-Type': 'application/json',
276
+ 'Accept': 'text/event-stream',
277
+ },
278
+ body: JSON.stringify( payload ),
279
+ } );
280
+
281
+ if ( !response.ok ) {
282
+ logger.error( `External Chat Stream API error: ${response.status} ${response.statusText}` );
283
+ res.write( `data: ${JSON.stringify( { error: `External API error: ${response.statusText}` } )}\n\n` );
284
+ res.end();
285
+ return;
286
+ }
287
+
288
+ // Stream the response from external API to client
289
+ for await ( const chunk of response.body ) {
290
+ const text = new TextDecoder().decode( chunk );
291
+ res.write( text );
292
+ }
293
+
294
+ res.end();
295
+ logger.info( { message: 'Chat stream completed successfully' } );
296
+ } catch ( error ) {
297
+ logger.error( { error: error, function: 'chatStream' } );
298
+ res.write( `data: ${JSON.stringify( { error: error.message } )}\n\n` );
299
+ res.end();
300
+ }
301
+ }
302
+
@@ -1,5 +1,66 @@
1
1
  // import { sampleCohortData } from '../models/cohortAnalysis.model.js';
2
2
  import { logger } from 'tango-app-api-middleware';
3
+ import { getCohortAnalysisSummaryCard } from '../services/conversation.service.js';
4
+
5
+ /**
6
+ * Get Cohort Analysis Summary Card
7
+ * POST /cohort-analysis-card
8
+ * @param {Object} req - Express request object
9
+ * @param {Object} req.body - Request body with analysis parameters
10
+ * @param {string} req.body.startDate - Start date for analysis (YYYY-MM-DD)
11
+ * @param {string} req.body.endDate - End date for analysis (YYYY-MM-DD)
12
+ * @param {string[]} req.body.storeId - Array of store IDs for filtering
13
+ * @param {string[]} req.body.cohortType - Array of cohort types to filter
14
+ * @param {string[]} req.body.clientId - Array of client IDs
15
+ * @param {Object} res - Express response object
16
+ * @return {void} Returns JSON response with cohort analysis summary card data
17
+ */
18
+ export const getCohortAnalysisCard = async ( req, res ) => {
19
+ try {
20
+ const { startDate, endDate, storeId, cohortType, clientId } = req.body;
21
+
22
+ // Validation
23
+ if ( !startDate || !endDate || !storeId || !cohortType || !clientId ) {
24
+ return res.status( 400 ).json( {
25
+ status: 'error',
26
+ message: 'Missing required parameters: startDate, endDate, storeId, cohortType, clientId',
27
+ code: 'MISSING_PARAMETERS',
28
+ timestamp: new Date().toISOString(),
29
+ } );
30
+ }
31
+
32
+ logger.info( {
33
+ message: 'Fetching cohort analysis summary card',
34
+ startDate,
35
+ endDate,
36
+ storeId,
37
+ cohortType,
38
+ } );
39
+
40
+ // Get summary card data from Lambda
41
+ const summaryCard = await getCohortAnalysisSummaryCard( {
42
+ startDate,
43
+ endDate,
44
+ storeId,
45
+ cohortType,
46
+ clientId,
47
+ } );
48
+
49
+ return res.status( 200 ).json( {
50
+ status: 'success',
51
+ data: summaryCard,
52
+ timestamp: new Date().toISOString(),
53
+ } );
54
+ } catch ( error ) {
55
+ logger.error( { error, message: 'Error fetching cohort analysis summary card', body: req.body } );
56
+ return res.status( 500 ).json( {
57
+ status: 'error',
58
+ message: error.message || 'Internal server error',
59
+ code: 'INTERNAL_ERROR',
60
+ timestamp: new Date().toISOString(),
61
+ } );
62
+ }
63
+ };
3
64
 
4
65
  /**
5
66
  * Get Complete Cohort Analysis
@@ -1,5 +1,13 @@
1
1
  // import { sampleConversationData, sampleConversationsList } from '../models/conversationAnalysis.model.js';
2
2
  import { logger } from 'tango-app-api-middleware';
3
+ import {
4
+ getConversationsListFromLambda,
5
+ exportConversationsFromLambda,
6
+ getConversationDetailsFromLambda,
7
+ exportConversationsToCSV,
8
+ filterConversationsBySearch,
9
+ sortConversations,
10
+ } from '../services/conversation.service.js';
3
11
 
4
12
  /**
5
13
  * Get Conversation Analysis List
@@ -8,13 +16,31 @@ import { logger } from 'tango-app-api-middleware';
8
16
  * @param {Object} req.body - Request body with query parameters
9
17
  * @param {string} req.body.startDate - Start date for filtering
10
18
  * @param {string} req.body.endDate - End date for filtering
11
- * @param {string} req.body.storeId - Store ID for filtering
19
+ * @param {string[]} req.body.storeId - Array of store IDs for filtering
20
+ * @param {string[]} req.body.clientId - Optional array of client IDs
21
+ * @param {boolean} req.body.isAI - Filter for AI conversations
22
+ * @param {string} req.body.analyticsType - Type of analytics (audio, text, all)
23
+ * @param {string} req.body.searchValue - Search term for filtering
24
+ * @param {number} req.body.limit - Pagination limit
25
+ * @param {number} req.body.offset - Pagination offset
26
+ * @param {boolean} req.body.isExport - Flag to export as CSV
12
27
  * @param {Object} res - Express response object
13
- * @return {void} Returns JSON response with conversations list
28
+ * @return {void} Returns JSON response with conversations list or CSV file
14
29
  */
15
30
  export const getConversationsList = async ( req, res ) => {
16
31
  try {
17
- const { startDate, endDate, storeId } = req.body;
32
+ const {
33
+ startDate,
34
+ endDate,
35
+ storeId,
36
+ clientId,
37
+ isAI,
38
+ analyticsType,
39
+ searchValue,
40
+ limit,
41
+ offset,
42
+ isExport,
43
+ } = req.body;
18
44
 
19
45
  // Validation
20
46
  if ( !startDate || !endDate || !storeId ) {
@@ -26,21 +52,101 @@ export const getConversationsList = async ( req, res ) => {
26
52
  } );
27
53
  }
28
54
 
29
- logger.info( `Fetching conversations for store ${storeId} from ${startDate} to ${endDate}` );
55
+ logger.info( {
56
+ message: 'Fetching conversations list',
57
+ startDate,
58
+ endDate,
59
+ storeId,
60
+ isExport,
61
+ } );
62
+
63
+ let conversations;
64
+ let totalCount;
65
+
66
+ // If export is requested, call the export Lambda function
67
+ if ( isExport ) {
68
+ try {
69
+ const exportResponse = await exportConversationsFromLambda( {
70
+ startDate,
71
+ endDate,
72
+ storeId,
73
+ clientId,
74
+ isAI,
75
+ analyticsType,
76
+ searchValue,
77
+ } );
78
+
79
+ conversations = exportResponse.conversations || [];
80
+ totalCount = exportResponse.totalCount || conversations.length;
81
+
82
+ logger.info( {
83
+ message: 'Export Lambda response received',
84
+ recordCount: conversations.length,
85
+ } );
86
+
87
+ // Apply search filter if provided
88
+ if ( searchValue ) {
89
+ conversations = filterConversationsBySearch( conversations, searchValue );
90
+ }
91
+
92
+ // Apply sorting
93
+ conversations = sortConversations( conversations, 'date', 'desc' );
94
+
95
+ // Convert to CSV
96
+ const csv = await exportConversationsToCSV( conversations );
97
+ res.setHeader( 'Content-Type', 'text/csv' );
98
+ res.setHeader( 'Content-Disposition', 'attachment; filename=conversations.csv' );
99
+ return res.send( csv );
100
+ } catch ( error ) {
101
+ logger.error( { error, message: 'Error in export Lambda call', body: req.body } );
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ // Call Lambda to get conversations (non-export)
107
+ const lambdaResponse = await getConversationsListFromLambda( {
108
+ startDate,
109
+ endDate,
110
+ storeId,
111
+ clientId,
112
+ isAI,
113
+ analyticsType,
114
+ searchValue,
115
+ limit,
116
+ offset,
117
+ } );
118
+
119
+ conversations = lambdaResponse.conversations || [];
120
+ totalCount = lambdaResponse.totalCount || conversations.length;
121
+
122
+ // Apply search filter if provided
123
+ if ( searchValue ) {
124
+ conversations = filterConversationsBySearch( conversations, searchValue );
125
+ }
126
+
127
+ // Apply sorting
128
+ conversations = sortConversations( conversations, 'date', 'desc' );
30
129
 
130
+ // Return paginated JSON response
31
131
  return res.status( 200 ).json( {
32
132
  status: 'success',
33
133
  data: {
34
- totalConversations: 0,
35
- conversations: 'sampleConversationsList',
134
+ totalConversations: totalCount,
135
+ conversations,
136
+ pagination: {
137
+ limit,
138
+ offset,
139
+ total: totalCount,
140
+ hasMore: ( offset + limit ) < totalCount,
141
+ },
36
142
  },
37
143
  timestamp: new Date().toISOString(),
38
144
  } );
39
145
  } catch ( error ) {
40
- logger.error( `Error fetching conversations: ${error.message}` );
146
+ logger.error( { error, message: 'Error fetching conversations list', body: req.body } );
41
147
  return res.status( 500 ).json( {
42
148
  status: 'error',
43
- message: 'Internal server error',
149
+ message: error.message || 'Internal server error',
44
150
  code: 'INTERNAL_ERROR',
45
151
  timestamp: new Date().toISOString(),
46
152
  } );
@@ -66,24 +172,43 @@ export const getConversationDetails = async ( req, res ) => {
66
172
  if ( !conversationId || !storeId ) {
67
173
  return res.status( 400 ).json( {
68
174
  status: 'error',
69
- message: 'Missing required parameters',
175
+ message: 'Missing required parameters: conversationId, storeId',
70
176
  code: 'MISSING_PARAMETERS',
71
177
  timestamp: new Date().toISOString(),
72
178
  } );
73
179
  }
74
180
 
75
- logger.info( `Fetching conversation ${conversationId}` );
181
+ logger.info( {
182
+ message: 'Fetching conversation details',
183
+ conversationId,
184
+ storeId,
185
+ } );
186
+
187
+ // Call Lambda to get conversation details
188
+ const conversationData = await getConversationDetailsFromLambda( {
189
+ conversationId,
190
+ storeId,
191
+ } );
192
+
193
+ if ( !conversationData ) {
194
+ return res.status( 404 ).json( {
195
+ status: 'error',
196
+ message: 'Conversation not found',
197
+ code: 'NOT_FOUND',
198
+ timestamp: new Date().toISOString(),
199
+ } );
200
+ }
76
201
 
77
202
  return res.status( 200 ).json( {
78
203
  status: 'success',
79
- data: 'sampleConversationData',
204
+ data: conversationData,
80
205
  timestamp: new Date().toISOString(),
81
206
  } );
82
207
  } catch ( error ) {
83
- logger.error( `Error fetching conversation: ${error.message}` );
208
+ logger.error( { error, message: 'Error fetching conversation details', params: req.params } );
84
209
  return res.status( 500 ).json( {
85
210
  status: 'error',
86
- message: 'Internal server error',
211
+ message: error.message || 'Internal server error',
87
212
  code: 'INTERNAL_ERROR',
88
213
  timestamp: new Date().toISOString(),
89
214
  } );
@@ -497,6 +497,53 @@ export class ExternalStreamAPIRequestDTO {
497
497
  }
498
498
  }
499
499
 
500
+ // ======================= JOI VALIDATION SCHEMAS =======================
501
+
502
+ /**
503
+ * Cohort Analysis Card Schema
504
+ */
505
+ export const cohortAnalysisCardValid = joi.object( {
506
+ startDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'Start date in YYYY-MM-DD format' ),
507
+ endDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'End date in YYYY-MM-DD format' ),
508
+ storeId: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of store IDs' ),
509
+ cohortType: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of cohort types' ),
510
+ clientId: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of client IDs' ),
511
+ } ).strict();
512
+
513
+ /**
514
+ * Conversations List Schema
515
+ */
516
+ export const conversationsListValid = joi.object( {
517
+ startDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'Start date in YYYY-MM-DD format' ),
518
+ endDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'End date in YYYY-MM-DD format' ),
519
+ storeId: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of store IDs' ),
520
+ clientId: joi.array().items( joi.string() ).optional().description( 'Array of client IDs' ),
521
+ isAI: joi.boolean().optional().description( 'Filter for AI-generated conversations' ),
522
+ analyticsType: joi.string().optional().valid( 'audio', 'text', 'all' ).description( 'Type of analytics' ),
523
+ searchValue: joi.string().optional().description( 'Search term' ),
524
+ isExport: joi.boolean().optional().description( 'Flag to export as CSV' ),
525
+ limit: joi.number().integer().min( 1 ).max( 1000 ).optional().default( 10 ).description( 'Pagination limit' ),
526
+ offset: joi.number().integer().min( 0 ).optional().default( 0 ).description( 'Pagination offset' ),
527
+ } ).strict();
528
+
529
+ /**
530
+ * Conversation Details Schema
531
+ */
532
+ export const conversationDetailsValid = joi.object( {
533
+ storeId: joi.string().required().description( 'Store ID' ),
534
+ } ).strict();
535
+
536
+ /**
537
+ * CSV Export Schema
538
+ */
539
+ export const csvExportValid = joi.object( {
540
+ startDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'Start date in YYYY-MM-DD format' ),
541
+ endDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'End date in YYYY-MM-DD format' ),
542
+ storeId: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of store IDs' ),
543
+ clientId: joi.array().items( joi.string() ).optional().description( 'Array of client IDs' ),
544
+ columns: joi.array().items( joi.string() ).optional().description( 'Columns to include in export' ),
545
+ } ).strict();
546
+
500
547
 
501
548
  const cohortContextNonNumericSchema = joi.object( {
502
549
  contextName: joi.string().required(),
@@ -586,3 +633,30 @@ const listCohortsByClientSchema = joi.object( {
586
633
  export const listCohortsByClientValid = {
587
634
  query: listCohortsByClientSchema,
588
635
  };
636
+
637
+ // ======================= CHAT STREAM API SCHEMA =======================
638
+
639
+ /**
640
+ * Chat Stream Request Schema
641
+ * For streaming AI chat API
642
+ */
643
+ const chatStreamSchema = joi.object( {
644
+
645
+ userId: joi.string().required().description( 'User ID' ),
646
+ sessionId: joi.string().required().description( 'Session ID' ),
647
+ sessionDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'Session date in YYYY-MM-DD format' ),
648
+ sessionTimezone: joi.string().required().description( 'Session timezone (e.g., Asia/Kolkata)' ),
649
+ message: joi.string().required().min( 1 ).description( 'User message/prompt' ),
650
+ config: joi.object( {
651
+ selectedStores: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of selected store IDs' ),
652
+ dateRange: joi.object( {
653
+ start: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'Start date in YYYY-MM-DD format' ),
654
+ end: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'End date in YYYY-MM-DD format' ),
655
+ } ).required().description( 'Date range object' ),
656
+ selectedProducts: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of selected products' ),
657
+ } ).required().description( 'Configuration object' ),
658
+ } ).strict();
659
+
660
+ export const chatStreamValid = {
661
+ body: chatStreamSchema,
662
+ };
@@ -2,7 +2,10 @@
2
2
  import express from 'express';
3
3
  import { validate } from 'tango-app-api-middleware';
4
4
  import { createCohortValid, updateCohortValid, getCohortValid, listCohortsByClientValid } from '../dtos/audioAnalytics.dtos.js';
5
- import { createCohort, updateCohort, getCohort, listCohortsByClient } from '../controllers/audioAnalytics.controller.js';
5
+ import { cohortAnalysisCardValid, conversationsListValid, conversationDetailsValid, chatStreamValid } from '../dtos/audioAnalytics.dtos.js';
6
+ import { createCohort, updateCohort, getCohort, listCohortsByClient, chatStream } from '../controllers/audioAnalytics.controller.js';
7
+ import { getCohortAnalysisCard } from '../controllers/cohortAnalytics.controller.js';
8
+ import { getConversationsList, getConversationDetails } from '../controllers/conversationAnalytics.controller.js';
6
9
 
7
10
  export const audioAnalyticsrouter = express.Router(); ;
8
11
 
@@ -12,10 +15,21 @@ audioAnalyticsrouter.get( '/test', ( req, res ) => {
12
15
  return res.json( 'Hello, world!' );
13
16
  } );
14
17
 
18
+ // Cohort Management Routes
15
19
  audioAnalyticsrouter.post( '/create-cohort', validate( createCohortValid ), createCohort );
16
20
  audioAnalyticsrouter.post( '/update-cohort', validate( updateCohortValid ), updateCohort );
17
21
  audioAnalyticsrouter.get( '/get-cohort/:cohortId', validate( getCohortValid ), getCohort );
18
22
  audioAnalyticsrouter.get( '/list-cohorts', validate( listCohortsByClientValid ), listCohortsByClient );
19
23
 
24
+ // Cohort Analytics Routes
25
+ audioAnalyticsrouter.post( '/cohort-analysis-card', validate( cohortAnalysisCardValid ), getCohortAnalysisCard );
26
+
27
+ // Conversation Analytics Routes
28
+ audioAnalyticsrouter.post( '/conversations/list', validate( conversationsListValid ), getConversationsList );
29
+ audioAnalyticsrouter.post( '/conversations/:conversationId', validate( conversationDetailsValid ), getConversationDetails );
30
+
31
+ // Chat Stream API Route
32
+ audioAnalyticsrouter.post( '/chat/stream', validate( chatStreamValid ), chatStream );
33
+
20
34
 
21
35
  export default audioAnalyticsrouter;
@@ -0,0 +1,240 @@
1
+ import { logger } from 'tango-app-api-middleware';
2
+ import axios from 'axios';
3
+ import { Parser } from 'json2csv';
4
+
5
+ const LAMBDA_ENDPOINT = JSON.parse( process.env.URL ) || 'http://lambda-api:8000';
6
+
7
+ /**
8
+ * Call the Lambda function to get cohort analysis
9
+ * @param {Object} params - Request parameters
10
+ * @param {string} params.startDate - Start date (YYYY-MM-DD)
11
+ * @param {string} params.endDate - End date (YYYY-MM-DD)
12
+ * @param {string[]} params.storeId - Array of store IDs
13
+ * @param {string[]} params.cohortType - Array of cohort types
14
+ * @param {string[]} params.clientId - Array of client IDs
15
+ * @return {Promise<Object>} Response from Lambda
16
+ */
17
+ export async function getCohortAnalysisFromLambda( params ) {
18
+ try {
19
+ logger.info( { message: 'Calling Lambda for cohort analysis', params } );
20
+
21
+ const response = await axios.post( `${LAMBDA_ENDPOINT.cohortAnalysisCard}/cohort-analysis`, {
22
+ startDate: params.startDate,
23
+ endDate: params.endDate,
24
+ storeId: params.storeId,
25
+ cohortType: params.cohortType,
26
+ clientId: params.clientId,
27
+ }, {
28
+ timeout: 30000,
29
+ } );
30
+
31
+ logger.info( { message: 'Lambda response received', data: response.data } );
32
+ return response.data;
33
+ } catch ( error ) {
34
+ logger.error( { error, message: 'Error calling Lambda for cohort analysis', params } );
35
+ throw new Error( `Failed to fetch cohort analysis: ${error.message}` );
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Call Lambda to get conversations list
41
+ * @param {Object} params - Request parameters
42
+ * @param {string} params.startDate - Start date (YYYY-MM-DD)
43
+ * @param {string} params.endDate - End date (YYYY-MM-DD)
44
+ * @param {string[]} params.storeId - Array of store IDs
45
+ * @param {string[]} params.clientId - Array of client IDs
46
+ * @param {boolean} params.isAI - Filter for AI conversations
47
+ * @param {string} params.analyticsType - Type of analytics
48
+ * @param {string} params.searchValue - Search term
49
+ * @param {number} params.limit - Pagination limit
50
+ * @param {number} params.offset - Pagination offset
51
+ * @return {Promise<Object>} Response from Lambda
52
+ */
53
+ export async function getConversationsListFromLambda( params ) {
54
+ try {
55
+ logger.info( { message: 'Calling Lambda for conversations list', params } );
56
+
57
+ const response = await axios.post( `${LAMBDA_ENDPOINT.cohortConversationList}/conversations/list`, {
58
+ startDate: params.startDate,
59
+ endDate: params.endDate,
60
+ storeId: params.storeId,
61
+ clientId: params.clientId,
62
+ isAI: params.isAI,
63
+ analyticsType: params.analyticsType,
64
+ searchValue: params.searchValue,
65
+ limit: params.limit,
66
+ offset: params.offset,
67
+ }, {
68
+ timeout: 30000,
69
+ } );
70
+
71
+ logger.info( { message: 'Lambda response received for conversations list', totalCount: response.data?.totalCount } );
72
+ return response.data;
73
+ } catch ( error ) {
74
+ logger.error( { error, message: 'Error calling Lambda for conversations list', params } );
75
+ throw new Error( `Failed to fetch conversations list: ${error.message}` );
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Call Lambda to get conversations for export
81
+ * @param {Object} params - Request parameters
82
+ * @param {string} params.startDate - Start date (YYYY-MM-DD)
83
+ * @param {string} params.endDate - End date (YYYY-MM-DD)
84
+ * @param {string[]} params.storeId - Array of store IDs
85
+ * @param {string[]} params.clientId - Array of client IDs
86
+ * @param {boolean} params.isAI - Filter for AI conversations
87
+ * @param {string} params.analyticsType - Type of analytics
88
+ * @param {string} params.searchValue - Search term
89
+ * @return {Promise<Object>} Response from Lambda with export data
90
+ */
91
+ export async function exportConversationsFromLambda( params ) {
92
+ try {
93
+ logger.info( { message: 'Calling Lambda for conversations export', params } );
94
+
95
+ const response = await axios.post( `${LAMBDA_ENDPOINT.cohortConversationExport}/conversations/export`, {
96
+ startDate: params.startDate,
97
+ endDate: params.endDate,
98
+ storeId: params.storeId,
99
+ clientId: params.clientId,
100
+ isAI: params.isAI,
101
+ analyticsType: params.analyticsType,
102
+ searchValue: params.searchValue,
103
+ }, {
104
+ timeout: 30000,
105
+ } );
106
+
107
+ logger.info( { message: 'Lambda response received for conversations export', recordCount: response.data?.recordCount } );
108
+ return response.data;
109
+ } catch ( error ) {
110
+ logger.error( { error, message: 'Error calling Lambda for conversations export', params } );
111
+ throw new Error( `Failed to export conversations: ${error.message}` );
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Call Lambda to get conversation details
117
+ * @param {Object} params - Request parameters
118
+ * @param {string} params.conversationId - Conversation ID
119
+ * @param {string} params.storeId - Store ID
120
+ * @return {Promise<Object>} Response from Lambda
121
+ */
122
+ export async function getConversationDetailsFromLambda( params ) {
123
+ try {
124
+ logger.info( { message: 'Calling Lambda for conversation details', params } );
125
+
126
+ const response = await axios.post( `${LAMBDA_ENDPOINT.cohortConversationDetail}/conversations/${params.conversationId}`, {
127
+ storeId: params.storeId,
128
+ }, {
129
+ timeout: 30000,
130
+ } );
131
+
132
+ logger.info( { message: 'Lambda response received for conversation details', conversationId: params.conversationId } );
133
+ return response.data;
134
+ } catch ( error ) {
135
+ logger.error( { error, message: 'Error calling Lambda for conversation details', params } );
136
+ throw new Error( `Failed to fetch conversation details: ${error.message}` );
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Export conversations list to CSV
142
+ * @param {Array} conversations - Array of conversation objects
143
+ * @param {Array} columns - Columns to include in export
144
+ * @return {Promise<string>} CSV string
145
+ */
146
+ export async function exportConversationsToCSV( conversations, columns = null ) {
147
+ try {
148
+ if ( !conversations || conversations.length === 0 ) {
149
+ logger.warn( 'No conversations to export' );
150
+ return '';
151
+ }
152
+
153
+ // If no columns specified, use all keys from first object
154
+ const fields = columns || Object.keys( conversations[0] );
155
+
156
+ const parser = new Parser( { fields } );
157
+ const csv = parser.parse( conversations );
158
+
159
+ logger.info( { message: 'Conversations exported to CSV', recordCount: conversations.length } );
160
+ return csv;
161
+ } catch ( error ) {
162
+ logger.error( { error, message: 'Error exporting conversations to CSV' } );
163
+ throw new Error( `Failed to export to CSV: ${error.message}` );
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Get cohort analysis summary card data
169
+ * @param {Object} params - Request parameters
170
+ * @return {Promise<Object>} Summary card data from Lambda
171
+ */
172
+ export async function getCohortAnalysisSummaryCard( params ) {
173
+ try {
174
+ const lambdaResponse = await getCohortAnalysisFromLambda( params );
175
+
176
+ // Process response to create summary card
177
+ const summaryCard = {
178
+ totalConversations: lambdaResponse.totalConversations || 0,
179
+ totalDuration: lambdaResponse.totalDuration || 0,
180
+ averageRating: lambdaResponse.averageRating || 0,
181
+ topCohorts: lambdaResponse.topCohorts || [],
182
+ trendData: lambdaResponse.trendData || [],
183
+ keyInsights: lambdaResponse.keyInsights || [],
184
+ timestamp: new Date().toISOString(),
185
+ };
186
+
187
+ return summaryCard;
188
+ } catch ( error ) {
189
+ logger.error( { error, message: 'Error getting cohort analysis summary card' } );
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Process and filter conversations based on search criteria
196
+ * @param {Array} conversations - Array of conversation objects
197
+ * @param {string} searchValue - Search term
198
+ * @return {Array} Filtered conversations
199
+ */
200
+ export function filterConversationsBySearch( conversations, searchValue ) {
201
+ if ( !searchValue || searchValue.trim() === '' ) {
202
+ return conversations;
203
+ }
204
+
205
+ const searchLower = searchValue.toLowerCase();
206
+ return conversations.filter( ( conv ) => {
207
+ return (
208
+ ( conv.storeName && conv.storeName.toLowerCase().includes( searchLower ) ) ||
209
+ ( conv.storeId && conv.storeId.toLowerCase().includes( searchLower ) ) ||
210
+ ( conv.summary && conv.summary.toLowerCase().includes( searchLower ) ) ||
211
+ ( conv.audioDescription && conv.audioDescription.toLowerCase().includes( searchLower ) )
212
+ );
213
+ } );
214
+ }
215
+
216
+ /**
217
+ * Sort conversations by specified field
218
+ * @param {Array} conversations - Array of conversation objects
219
+ * @param {string} sortBy - Field to sort by
220
+ * @param {string} order - Sort order ('asc' or 'desc')
221
+ * @return {Array} Sorted conversations
222
+ */
223
+ export function sortConversations( conversations, sortBy = 'date', order = 'desc' ) {
224
+ const sorted = [ ...conversations ];
225
+ sorted.sort( ( a, b ) => {
226
+ const aVal = a[sortBy];
227
+ const bVal = b[sortBy];
228
+
229
+ if ( aVal === undefined || aVal === null ) return 1;
230
+ if ( bVal === undefined || bVal === null ) return -1;
231
+
232
+ if ( typeof aVal === 'string' ) {
233
+ return order === 'desc' ? bVal.localeCompare( aVal ) : aVal.localeCompare( bVal );
234
+ }
235
+
236
+ return order === 'desc' ? bVal - aVal : aVal - bVal;
237
+ } );
238
+
239
+ return sorted;
240
+ }
@@ -0,0 +1,160 @@
1
+ import { Parser } from 'json2csv';
2
+ import { logger } from 'tango-app-api-middleware';
3
+
4
+ /**
5
+ * Convert data array to CSV format
6
+ * @param {Array} data - Array of objects to convert to CSV
7
+ * @param {Array} fields - Fields to include in CSV (if null, uses all fields from first object)
8
+ * @param {Object} options - Additional options for Parser
9
+ * @return {Promise<string>} CSV string
10
+ */
11
+ export async function convertToCSV( data, fields = null, options = {} ) {
12
+ try {
13
+ if ( !data || data.length === 0 ) {
14
+ logger.warn( 'No data provided for CSV export' );
15
+ return '';
16
+ }
17
+
18
+ const parserOptions = {
19
+ fields: fields || Object.keys( data[0] ),
20
+ ...options,
21
+ };
22
+
23
+ const parser = new Parser( parserOptions );
24
+ const csv = parser.parse( data );
25
+
26
+ logger.info( { message: 'Data converted to CSV', recordCount: data.length } );
27
+ return csv;
28
+ } catch ( error ) {
29
+ logger.error( { error, message: 'Error converting data to CSV' } );
30
+ throw new Error( `Failed to convert to CSV: ${error.message}` );
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Export conversations to CSV
36
+ * @param {Array} conversations - Array of conversation objects
37
+ * @param {Array} columns - Columns to include in export
38
+ * @return {Promise<string>} CSV string
39
+ */
40
+ export async function exportConversationsToCSV( conversations, columns = null ) {
41
+ const defaultFields = columns || [
42
+ 'conversationId',
43
+ 'storeId',
44
+ 'storeName',
45
+ 'date',
46
+ 'time',
47
+ 'duration',
48
+ 'audioDescription',
49
+ 'summary',
50
+ ];
51
+
52
+ return convertToCSV( conversations, defaultFields, {
53
+ header: true,
54
+ flatten: false,
55
+ } );
56
+ }
57
+
58
+ /**
59
+ * Export cohort analysis data to CSV
60
+ * @param {Array} data - Array of cohort analysis objects
61
+ * @param {Array} columns - Columns to include in export
62
+ * @return {Promise<string>} CSV string
63
+ */
64
+ export async function exportCohortAnalysisToCSV( data, columns = null ) {
65
+ const defaultFields = columns || [
66
+ 'cohortId',
67
+ 'cohortName',
68
+ 'cohortType',
69
+ 'storeId',
70
+ 'totalConversations',
71
+ 'averageRating',
72
+ 'conversionRate',
73
+ 'date',
74
+ ];
75
+
76
+ return convertToCSV( data, defaultFields, {
77
+ header: true,
78
+ flatten: false,
79
+ } );
80
+ }
81
+
82
+ /**
83
+ * Export dashboard metrics to CSV
84
+ * @param {Array} metrics - Array of metric objects
85
+ * @param {Array} columns - Columns to include in export
86
+ * @return {Promise<string>} CSV string
87
+ */
88
+ export async function exportMetricsToCSV( metrics, columns = null ) {
89
+ const defaultFields = columns || [
90
+ 'metricName',
91
+ 'value',
92
+ 'target',
93
+ 'variance',
94
+ 'status',
95
+ 'date',
96
+ ];
97
+
98
+ return convertToCSV( metrics, defaultFields, {
99
+ header: true,
100
+ flatten: false,
101
+ } );
102
+ }
103
+
104
+ /**
105
+ * Generate CSV file blob
106
+ * @param {string} csvString - CSV string content
107
+ * @param {string} filename - Name of the file
108
+ * @return {Blob} File blob for download
109
+ */
110
+ export function generateCSVBlob( csvString, filename = 'export.csv' ) {
111
+ const blob = new Blob( [ csvString ], { type: 'text/csv;charset=utf-8;' } );
112
+ const link = document.createElement( 'a' );
113
+ const url = URL.createObjectURL( blob );
114
+
115
+ link.setAttribute( 'href', url );
116
+ link.setAttribute( 'download', filename );
117
+ link.style.visibility = 'hidden';
118
+
119
+ document.body.appendChild( link );
120
+ link.click();
121
+ document.body.removeChild( link );
122
+
123
+ return blob;
124
+ }
125
+
126
+ /**
127
+ * Flatten nested objects for CSV export
128
+ * @param {Object} obj - Object to flatten
129
+ * @param {string} prefix - Prefix for flattened keys
130
+ * @return {Object} Flattened object
131
+ */
132
+ export function flattenObject( obj, prefix = '' ) {
133
+ const flattened = {};
134
+
135
+ for ( const key in obj ) {
136
+ if ( Object.prototype.hasOwnProperty.call( obj, key ) ) {
137
+ const value = obj[key];
138
+ const newKey = prefix ? `${prefix}.${key}` : key;
139
+
140
+ if ( value !== null && typeof value === 'object' && !Array.isArray( value ) ) {
141
+ Object.assign( flattened, flattenObject( value, newKey ) );
142
+ } else if ( Array.isArray( value ) ) {
143
+ flattened[newKey] = JSON.stringify( value );
144
+ } else {
145
+ flattened[newKey] = value;
146
+ }
147
+ }
148
+ }
149
+
150
+ return flattened;
151
+ }
152
+
153
+ /**
154
+ * Flatten array of objects, handling nested data
155
+ * @param {Array} data - Array of objects to flatten
156
+ * @return {Array} Array of flattened objects
157
+ */
158
+ export function flattenDataForCSV( data ) {
159
+ return data.map( ( item ) => flattenObject( item ) );
160
+ }