Add PQQ functionality: Introduce new endpoints for creating activities and submitting answers, along with updates to the Minglar service for retrieving PQQ details. Update serverless configuration to include new function files.

This commit is contained in:
2025-12-03 19:21:21 +05:30
parent ca5936d0db
commit 4a7e5fbb1e
10 changed files with 975 additions and 15 deletions

View File

@@ -1141,8 +1141,8 @@ model ActivityPQQheader {
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
pqqQuestionXid Int @map("pqq_question_xid")
pqqQuestions PQQQuestions @relation(fields: [pqqQuestionXid], references: [id], onDelete: Restrict)
pqqAnswerXid Int @map("pqq_answer_xid")
pqqAnswers PQQAnswers @relation(fields: [pqqAnswerXid], references: [id], onDelete: Restrict)
pqqAnswerXid Int? @map("pqq_answer_xid")
pqqAnswers PQQAnswers? @relation(fields: [pqqAnswerXid], references: [id], onDelete: Restrict)
comments String? @map("comments") @db.VarChar(200)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")

View File

@@ -88,6 +88,7 @@ functions:
- ${file(./serverless/functions/host.yml)}
- ${file(./serverless/functions/minglaradmin.yml)}
- ${file(./serverless/functions/prepopulate.yml)}
- ${file(./serverless/functions/pqq.yml)}
plugins:
- serverless-offline

View File

@@ -406,3 +406,19 @@ addPQQSuggestion:
- httpApi:
path: /minglaradmin/hosthub/hosts/add-Pqq-suggestion
method: post
getAllPQPDetailsForAM:
handler: src/modules/minglaradmin/handlers/hosthub/pqp/getAllPQPDetailsForAM.handler
memorySize: 384
package:
patterns:
- 'src/modules/minglaradmin/handlers/hosthub/pqp/getAllPQPDetailsForAM**'
- 'src/modules/minglaradmin/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /minglaradmin/hosthub/pqp/pqp-details-for-am/{activityXid}
method: get

View File

@@ -0,0 +1,29 @@
createActivityAndAllQuestionsEntry:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/createActivityAndAllQuestionsEntry.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/createActivityAndAllQuestionsEntry**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/create-activity
method: post
submitPQAnswer:
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQAnswer.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQAnswer**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host/Activity_Hub/OnBoarding/submit-pq-answer
method: patch

View File

@@ -0,0 +1,62 @@
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
const prismaService = new PrismaService();
const hostService = new HostService(prismaService);
export const handler = safeHandler(
async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
// Verify authentication token
const token =
event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
401,
'This is a protected route. Please provide a valid token.',
);
}
// Verify token and get user info
const userInfo = await verifyHostToken(token);
let body: any = {};
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (err) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityTypeXid, frequenciesXid } = body;
if (!activityTypeXid || !frequenciesXid) {
throw new ApiError(400, 'activityType and frequency ID is required');
}
// Get all host applications from service based on user role
const createdData = await hostService.createActivityAndAllQuestionsEntry(userInfo.id, activityTypeXid, frequenciesXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Activity created successfully',
data: createdData
}),
};
},
);

View File

@@ -10,7 +10,7 @@ import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
const prisma = new PrismaService();
const pqqService = new HostService(prisma);
const hostService = new HostService(prisma);
const s3 = new AWS.S3({ region: config.aws.region });
@@ -121,20 +121,20 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
throw new ApiError(400, "Missing required fields");
// UPSERT header
const existingHeader = await pqqService.findHeaderByCompositeKey(activityXid, pqqQuestionXid);
const existingHeader = await hostService.findHeaderByCompositeKey(activityXid, pqqQuestionXid);
let header;
if (existingHeader) {
header = await pqqService.updateHeader(existingHeader.id, pqqAnswerXid, comments);
header = await hostService.updateHeader(existingHeader.id, pqqAnswerXid, comments);
} else {
header = await pqqService.createHeader(activityXid, pqqQuestionXid, pqqAnswerXid, comments);
header = await hostService.createHeader(activityXid, pqqQuestionXid, pqqAnswerXid, comments);
}
// SCORE
const score = await pqqService.calculatePqqScoreForUser(activityXid);
const score = await hostService.calculatePqqScoreForUser(activityXid);
// Existing supporting files
const existingSupportingFiles = await pqqService.getSupportingFilesByHeaderId(header.id);
const existingSupportingFiles = await hostService.getSupportingFilesByHeaderId(header.id);
// Read deletedFiles from frontend
const deletedFiles = Array.isArray(fields.deletedFiles) ? fields.deletedFiles : [];
@@ -156,7 +156,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
}
// Delete from DB
await pqqService.deleteSupportingFile(record.id);
await hostService.deleteSupportingFile(record.id);
deleteResults.push({ id: record.id, deleted: true });
}
}
@@ -171,11 +171,13 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
`ActivityOnboarding/supportings/${activityXid}`
);
const newRec = await pqqService.addSupportingFile(header.id, file.mimeType, url);
const newRec = await hostService.addSupportingFile(header.id, file.mimeType, url);
addResults.push(newRec);
}
}
const getAllUpdatedQuestionResponse = await hostService.getAllPQUpdatedResponse(activityXid)
// CASE 2 — NO deletion & NO new files => DO NOTHING to existing files
return {
@@ -191,10 +193,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
pqqAnswerXid,
comments,
score,
files: {
added: addResults,
deleted: deleteResults
}
getAllUpdatedQuestionResponse
}
})
};

View File

@@ -0,0 +1,301 @@
import config from '../../../../../config/config';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import AWS from 'aws-sdk';
import Busboy from 'busboy';
import crypto from 'crypto';
import { PrismaService } from '../../../../../common/database/prisma.service';
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { HostService } from '../../../services/host.service';
const prisma = new PrismaService();
const hostService = new HostService(prisma);
const s3 = new AWS.S3({ region: config.aws.region });
// Function to extract S3 key from URL
function getS3KeyFromUrl(url: string): string {
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
return url.replace(bucketBaseUrl, '');
}
// Function to delete file from S3
async function deleteFromS3(s3Key: string): Promise<void> {
try {
await s3.deleteObject({
Bucket: config.aws.bucketName,
Key: s3Key,
}).promise();
console.log(`✅ File deleted from S3: ${s3Key}`);
} catch (error) {
console.error(`❌ Error deleting file from S3: ${s3Key}`, error);
// continue — we don't want S3 deletion failure to crash the whole request
}
}
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise<string> {
// We intentionally do NOT reuse old key. If existingUrl is provided we delete old file and create a new random key.
if (existingUrl) {
try {
const oldKey = getS3KeyFromUrl(existingUrl);
await deleteFromS3(oldKey);
} catch (err) {
console.warn('Warning deleting existingUrl before upload', err);
}
}
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
const s3Key = `${prefix}/${uniqueKey}`;
await s3.upload({
Bucket: config.aws.bucketName,
Key: s3Key,
Body: buffer,
ContentType: mimeType,
ACL: 'private'
}).promise();
console.log(`✅ File uploaded to S3: ${s3Key}`);
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
}
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
// 1) Auth
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) throw new ApiError(401, 'Missing token.');
const user = await verifyHostToken(token);
// 2) Content-Type check
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
if (!contentType?.includes("multipart/form-data"))
throw new ApiError(400, "Content-Type must be multipart/form-data");
// 3) Body decoding
const bodyBuffer = event.isBase64Encoded
? Buffer.from(event.body!, "base64")
: Buffer.from(event.body!, "binary");
const fields: any = {};
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
// 4) Parse multipart data
await new Promise<void>((resolve, reject) => {
const bb = Busboy({ headers: { 'content-type': contentType } });
bb.on('file', (fieldname, file, info) => {
const { filename, mimeType } = info;
if (!filename) {
file.resume();
return;
}
const chunks: Buffer[] = [];
let size = 0;
const MAX_SIZE = 5 * 1024 * 1024;
file.on("data", (chunk) => {
size += chunk.length;
if (size > MAX_SIZE) {
file.destroy(new Error(`File ${filename} exceeds 5MB limit.`));
return;
}
chunks.push(chunk);
});
file.on("end", () => {
if (chunks.length > 0) {
files.push({
buffer: Buffer.concat(chunks),
mimeType,
fileName: filename,
fieldName: fieldname,
});
}
});
file.on("error", (err) =>
reject(new ApiError(400, `File upload error: ${err.message}`))
);
});
bb.on("field", (fieldname, val) => {
console.log(`FIELD RAW: ${fieldname} =`, val);
if (val === '' || val === 'null' || val === 'undefined') fields[fieldname] = null;
else {
try {
const cleaned = val.trim();
// If it starts and ends with quotes, remove them
const withoutQuotes =
(cleaned.startsWith('"') && cleaned.endsWith('"'))
? cleaned.slice(1, -1)
: cleaned;
fields[fieldname] = JSON.parse(withoutQuotes);
} catch {
fields[fieldname] = val;
}
}
});
bb.on("close", () => resolve());
bb.on("error", (err) =>
reject(new ApiError(400, `Multipart parsing error: ${err.message}`))
);
// IMPORTANT FIX for HTTP API
bb.write(bodyBuffer);
bb.end();
});
// 5) Extract required fields
const activityXid = Number(fields.activityXid);
const pqqQuestionXid = Number(fields.pqqQuestionXid);
const pqqAnswerXid = Number(fields.pqqAnswerXid);
const comments = fields.comments || null;
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Valid activityXid is required");
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Valid pqqQuestionXid is required");
if (!pqqAnswerXid || isNaN(pqqAnswerXid)) throw new ApiError(400, "Valid pqqAnswerXid is required");
// 6) UPSERT header
const existingHeader = await hostService.findHeaderByCompositeKey(
activityXid,
pqqQuestionXid,
);
let header;
if (existingHeader) {
console.log("🔄 Updating existing PQQ header");
header = await hostService.updateHeader(
existingHeader.id,
pqqAnswerXid,
comments
);
} else {
console.log("🆕 Creating new PQQ header");
header = await hostService.createHeader(
activityXid,
pqqQuestionXid,
pqqAnswerXid,
comments
);
}
// 7) Get existing supporting files
const existingSupportingFiles = await hostService.getSupportingFilesByHeaderId(header.id);
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
// 8) Parse incoming control fields
// fields.deletedFiles should be array like [{ id: number, url: string }, ...] or null
const deletedFiles: Array<{ id: number; url?: string }> = Array.isArray(fields.deletedFiles) ? fields.deletedFiles : [];
// fields.existingFiles can be an array of urls; we accept it but do not require it
const existingFilesFromFront: string[] = Array.isArray(fields.existingFiles) ? fields.existingFiles : [];
// Prepare response trackers
const deletedResults: Array<{ id: number; success: boolean; reason?: string }> = [];
const addedResults: Array<any> = [];
// 9) Handle explicit deletions (ONLY delete ids provided in deletedFiles)
if (deletedFiles.length > 0) {
console.log(`🗑️ Processing ${deletedFiles.length} explicit deletions`);
// Build a map of existing supporting files by id for quick lookup
const existingById = new Map<number, any>();
for (const f of existingSupportingFiles) {
existingById.set(f.id, f);
}
for (const del of deletedFiles) {
const id = Number(del.id);
if (!id || !existingById.has(id)) {
deletedResults.push({ id, success: false, reason: 'Not found or invalid id' });
continue;
}
const record = existingById.get(id);
try {
// delete from s3
if (record.mediaFileName) {
const s3Key = getS3KeyFromUrl(record.mediaFileName);
await deleteFromS3(s3Key);
}
// delete DB record
await hostService.deleteSupportingFile(record.id);
deletedResults.push({ id: record.id, success: true });
console.log(`🗑️ Deleted supporting file record ${record.id}`);
} catch (err: any) {
console.error(`❌ Failed to delete supporting file id ${id}`, err);
deletedResults.push({ id, success: false, reason: err.message || 'delete failed' });
}
}
} else {
console.log(' No explicit deletions requested (deletedFiles empty)');
}
// 10) Handle new uploaded files (these are ALWAYS added as new rows)
if (files.length > 0) {
console.log(`📤 Processing ${files.length} uploaded new file(s)`);
for (const file of files) {
try {
const url = await uploadToS3(
file.buffer,
file.mimeType,
file.fileName,
`ActivityOnboarding/supportings/${activityXid}`
);
// create DB record
const supporting = await hostService.addSupportingFile(
header.id,
file.mimeType,
url
);
addedResults.push(supporting);
console.log(`🆕 Created new supporting file record: ${supporting.id}`);
} catch (err: any) {
console.error('❌ Error uploading/creating supporting file', err);
// push failure result but continue processing other files
addedResults.push({ success: false, reason: err.message || 'upload/create failed' });
}
}
} else {
console.log('📭 No new files uploaded in request');
}
// NOTE: We DO NOT delete or modify existing supporting files that were not listed in deletedFiles.
// This satisfies your Case 2: "if no files are provided, do not touch existing supporting files".
const allPQPQuestionAnswerResponse = await hostService.getAllPQUpdatedResponse(activityXid)
// 11) Compose response
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify({
success: true,
message: responseMessage,
data: {
responseOfUpdatedData: allPQPQuestionAnswerResponse,
operation: existingHeader ? 'updated' : 'created',
// summary label for UI convenience:
fileOperation: (deletedResults.length > 0 || addedResults.length > 0) ? 'modified' : 'unchanged'
}
})
};
} catch (error: any) {
console.error("❌ Error in submitPqqAnswer:", error);
throw error;
}
});

View File

@@ -567,7 +567,13 @@ export class HostService {
async getLatestQuestionDetailsPQQ(activity_xid: number) {
return await this.prisma.activityPQQheader.findFirst({
where: { activityXid: activity_xid, isActive: true },
where: {
activityXid: activity_xid,
isActive: true,
pqqAnswerXid: {
not: null
}
},
select: {
pqqQuestionXid: true,
pqqAnswerXid: true,
@@ -1528,4 +1534,361 @@ export class HostService {
});
}
async createActivityAndAllQuestionsEntry(
userId: number,
activityTypeXid: number,
frequenciesXid: number
) {
return await this.prisma.$transaction(async (tx) => {
// -------------- TYPES FIXED HERE -------------------
type GroupedCategory = {
id: number;
categoryName: string;
displayOrder: number;
pqqsubCategories: {
id: number;
subCategoryName: string;
displayOrder: number;
questions: {
id: number;
questionName: string;
maxPoints: number;
displayOrder: number;
PQQAnswers: any[];
suggestions: any[];
supportings: any[];
}[];
}[];
};
// ---------------------------------------------------
const host = await tx.hostHeader.findFirst({
where: { userXid: userId, isActive: true },
});
if (!host) throw new ApiError(404, 'Host not found for the user');
const activityType = await tx.activityTypes.findUnique({
where: { id: activityTypeXid },
});
if (!activityType) throw new ApiError(404, 'Activity type not found');
if (frequenciesXid) {
const freq = await tx.frequencies.findUnique({
where: { id: frequenciesXid },
});
if (!freq) throw new ApiError(404, 'Frequency not found');
}
const referenceNumber = await generateActivityRefNumber(tx);
const created = await tx.activities.create({
data: {
hostXid: host.id,
activityTypeXid,
frequenciesXid: frequenciesXid || null,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.DRAFT_PQ,
activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.DRAFT_PQ,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.DRAFT_PQ,
amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.DRAFT_PQ,
activityRefNumber: referenceNumber,
},
});
const questions = await tx.pQQCategories.findMany({
where: { isActive: true },
select: {
id: true,
categoryName: true,
displayOrder: true,
pqqsubCategories: {
where: { isActive: true },
select: {
id: true,
subCategoryName: true,
displayOrder: true,
questions: {
where: { isActive: true },
select: {
id: true,
questionName: true,
maxPoints: true,
displayOrder: true,
},
orderBy: { displayOrder: 'asc' },
},
},
orderBy: { displayOrder: 'asc' },
},
},
orderBy: { displayOrder: 'asc' },
});
// FLATTEN questions
const allQuestions: number[] = [];
for (const cat of questions) {
for (const sub of cat.pqqsubCategories) {
for (const q of sub.questions) {
allQuestions.push(q.id);
}
}
}
await tx.activityPQQheader.createMany({
data: allQuestions.map((id) => ({
activityXid: created.id,
pqqQuestionXid: id,
pqqAnswerXid: null,
})),
});
const pqqHeaderData = await tx.activityPQQheader.findMany({
where: { activityXid: created.id, isActive: true },
select: {
pqqQuestions: {
select: {
id: true,
questionName: true,
maxPoints: true,
displayOrder: true,
pqqSubCategories: {
select: {
id: true,
subCategoryName: true,
displayOrder: true,
category: {
select: {
id: true,
categoryName: true,
displayOrder: true,
},
},
},
},
},
},
pqqAnswers: {
select: {
id: true,
answerName: true,
answerPoints: true,
displayOrder: true,
},
},
ActivityPQQSuggestions: {
where: { isActive: true },
select: {
id: true,
title: true,
comments: true,
},
},
ActivityPQQSupportings: {
where: { isActive: true },
select: {
id: true,
mediaType: true,
mediaFileName: true,
},
},
},
orderBy: { id: "asc" },
});
// ---------------- GROUPING ------------------
const grouped: Record<number, GroupedCategory> = {};
for (const item of pqqHeaderData) {
const q = item.pqqQuestions;
const sub = q.pqqSubCategories;
const cat = sub.category;
if (!grouped[cat.id]) {
grouped[cat.id] = {
id: cat.id,
categoryName: cat.categoryName,
displayOrder: cat.displayOrder,
pqqsubCategories: [],
};
}
const category = grouped[cat.id];
let subCat = category.pqqsubCategories.find((x) => x.id === sub.id);
if (!subCat) {
subCat = {
id: sub.id,
subCategoryName: sub.subCategoryName,
displayOrder: sub.displayOrder,
questions: [],
};
category.pqqsubCategories.push(subCat);
}
subCat.questions.push({
id: q.id,
questionName: q.questionName,
maxPoints: q.maxPoints,
displayOrder: q.displayOrder,
PQQAnswers: item.pqqAnswers ? [item.pqqAnswers] : [],
suggestions: item.ActivityPQQSuggestions || [],
supportings: item.ActivityPQQSupportings || [],
});
}
const sortedCategories = Object.values(grouped) as GroupedCategory[];
// SORT
sortedCategories.sort((a, b) => a.displayOrder - b.displayOrder);
for (const cat of sortedCategories) {
cat.pqqsubCategories.sort((a, b) => a.displayOrder - b.displayOrder);
for (const sub of cat.pqqsubCategories) {
sub.questions.sort((a, b) => a.displayOrder - b.displayOrder);
}
}
return sortedCategories;
});
}
async getAllPQUpdatedResponse(activityXid: number) {
const pqqHeaderData = await this.prisma.activityPQQheader.findMany({
where: {
activityXid: activityXid,
isActive: true,
},
select: {
pqqQuestions: {
select: {
id: true,
questionName: true,
maxPoints: true,
displayOrder: true,
pqqSubCategories: {
select: {
id: true,
subCategoryName: true,
displayOrder: true,
category: {
select: {
id: true,
categoryName: true,
displayOrder: true
}
}
}
}
}
},
pqqAnswers: {
select: {
id: true,
answerName: true,
answerPoints: true,
displayOrder: true
}
},
ActivityPQQSuggestions: {
where: { isActive: true },
select: {
id: true,
title: true,
comments: true,
activityPqqHeaderXid: true
}
},
ActivityPQQSupportings: {
where: { isActive: true },
select: {
id: true,
mediaType: true,
mediaFileName: true
}
},
},
orderBy: { id: "asc" }
});
// ---------- GROUPING START ----------
const grouped: any = {};
for (const item of pqqHeaderData) {
const q = item.pqqQuestions;
const sub = q.pqqSubCategories;
const cat = sub.category;
// 1⃣ Category level
if (!grouped[cat.id]) {
grouped[cat.id] = {
id: cat.id,
categoryName: cat.categoryName,
displayOrder: cat.displayOrder,
pqqsubCategories: []
};
}
const category = grouped[cat.id];
// 2⃣ Subcategory level
let subCat = category.pqqsubCategories.find((s: any) => s.id === sub.id);
if (!subCat) {
subCat = {
id: sub.id,
subCategoryName: sub.subCategoryName,
displayOrder: sub.displayOrder,
questions: []
};
category.pqqsubCategories.push(subCat);
}
// 3⃣ Questions level
subCat.questions.push({
id: q.id,
questionName: q.questionName,
maxPoints: q.maxPoints,
displayOrder: q.displayOrder,
PQQAnswers: item.pqqAnswers ? [item.pqqAnswers] : [],
suggestions: item.ActivityPQQSuggestions,
supportings: item.ActivityPQQSupportings
});
}
// ---------- SORTING ----------
const sortedCategories: any = Object.values(grouped)
.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
for (const cat of sortedCategories) {
cat.pqqsubCategories.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
for (const sub of cat.pqqsubCategories) {
sub.questions.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
}
}
// ---------- PRESIGNED URL GENERATION ----------
for (const cat of sortedCategories) {
for (const sub of cat.pqqsubCategories) {
for (const q of sub.questions) {
if (q.supportings?.length) {
for (const doc of q.supportings) {
if (doc.mediaFileName) {
const filePath = doc.mediaFileName;
const key = filePath.startsWith("http")
? filePath.split(".com/")[1]
: filePath;
doc.presignedUrl = await getPresignedUrl(bucket, key);
}
}
}
}
}
}
// ---------- RETURN GROUPED STRUCTURE ----------
return sortedCategories;
}
}

View File

@@ -0,0 +1,50 @@
import { PrismaService } from '../../../../../common/database/prisma.service';
import { verifyMinglarAdminToken } from '../../../../../common/middlewares/jwt/authForMinglarAdmin';
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../../common/utils/helper/ApiError';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { MinglarService } from '../../../services/minglar.service';
const prismaService = new PrismaService();
const minglarService = new MinglarService(prismaService);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Get host ID from path parameters
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
await verifyMinglarAdminToken(token);
const activityXid = event.pathParameters?.activityXid;
if (!activityXid) {
throw new ApiError(
400,
'Host ID is required in path parameters.',
);
}
const pqpDetails = await minglarService.getAllPQPDetailsForAM(Number(activityXid));
if (!pqpDetails) {
throw new ApiError(404, 'Record not found');
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'PQ details retrieved successfully',
data: pqpDetails,
}),
};
});

View File

@@ -1558,6 +1558,7 @@ export class MinglarService {
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
},
data: {
stepper: STEPPER.NOT_SUBMITTED,
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_TO_UPDATE,
hostStatusDisplay: HOST_STATUS_DISPLAY.ENHANCING,
adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_REJECTED,
@@ -1875,4 +1876,142 @@ export class MinglarService {
return host;
}
async getAllPQPDetailsForAM(activityXid: number) {
const pqqHeaderData = await this.prisma.activityPQQheader.findMany({
where: {
activityXid: activityXid,
isActive: true,
},
select: {
pqqQuestions: {
select: {
id: true,
questionName: true,
maxPoints: true,
displayOrder: true,
pqqSubCategories: {
select: {
id: true,
subCategoryName: true,
displayOrder: true,
category: {
select: {
id: true,
categoryName: true,
displayOrder: true
}
}
}
}
}
},
pqqAnswers: {
select: {
id: true,
answerName: true,
answerPoints: true,
displayOrder: true
}
},
ActivityPQQSuggestions: {
where: { isActive: true },
select: {
id: true,
title: true,
comments: true,
activityPqqHeaderXid: true
}
},
ActivityPQQSupportings: {
where: { isActive: true },
select: {
id: true,
mediaType: true,
mediaFileName: true
}
},
},
orderBy: { id: "asc" }
});
// ---------- GROUPING START ----------
const grouped: any = {};
for (const item of pqqHeaderData) {
const q = item.pqqQuestions;
const sub = q.pqqSubCategories;
const cat = sub.category;
// 1⃣ Category level
if (!grouped[cat.id]) {
grouped[cat.id] = {
id: cat.id,
categoryName: cat.categoryName,
displayOrder: cat.displayOrder,
pqqsubCategories: []
};
}
const category = grouped[cat.id];
// 2⃣ Subcategory level
let subCat = category.pqqsubCategories.find((s: any) => s.id === sub.id);
if (!subCat) {
subCat = {
id: sub.id,
subCategoryName: sub.subCategoryName,
displayOrder: sub.displayOrder,
questions: []
};
category.pqqsubCategories.push(subCat);
}
// 3⃣ Questions level
subCat.questions.push({
id: q.id,
questionName: q.questionName,
maxPoints: q.maxPoints,
displayOrder: q.displayOrder,
PQQAnswers: item.pqqAnswers ? [item.pqqAnswers] : [],
suggestions: item.ActivityPQQSuggestions,
supportings: item.ActivityPQQSupportings
});
}
// ---------- SORTING ----------
const sortedCategories: any = Object.values(grouped)
.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
for (const cat of sortedCategories) {
cat.pqqsubCategories.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
for (const sub of cat.pqqsubCategories) {
sub.questions.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
}
}
// ---------- PRESIGNED URL GENERATION ----------
for (const cat of sortedCategories) {
for (const sub of cat.pqqsubCategories) {
for (const q of sub.questions) {
if (q.supportings?.length) {
for (const doc of q.supportings) {
if (doc.mediaFileName) {
const filePath = doc.mediaFileName;
const key = filePath.startsWith("http")
? filePath.split(".com/")[1]
: filePath;
doc.presignedUrl = await getPresignedUrl(bucket, key);
}
}
}
}
}
}
// ---------- RETURN GROUPED STRUCTURE ----------
return sortedCategories;
}
}