Merge branch 'sprint1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1
This commit is contained in:
@@ -161,7 +161,7 @@ getPQQ_LastUpdatedQuestion:
|
|||||||
path: /host/Activity_Hub/OnBoarding/get-latest-pqq-question-details
|
path: /host/Activity_Hub/OnBoarding/get-latest-pqq-question-details
|
||||||
method: get
|
method: get
|
||||||
|
|
||||||
getAllActivityType:
|
prePopulateNewActivity:
|
||||||
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllActivityType.handler
|
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllActivityType.handler
|
||||||
memorySize: 384
|
memorySize: 384
|
||||||
package:
|
package:
|
||||||
@@ -174,7 +174,7 @@ getAllActivityType:
|
|||||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||||
events:
|
events:
|
||||||
- httpApi:
|
- httpApi:
|
||||||
path: /host/Activity_Hub/OnBoarding/get-activity-type
|
path: /host/Activity_Hub/OnBoarding/prepopulate-new-activity
|
||||||
method: get
|
method: get
|
||||||
|
|
||||||
showSuggestion:
|
showSuggestion:
|
||||||
@@ -330,6 +330,23 @@ updatePQQ_LastAnswer:
|
|||||||
path: /host/Activity_Hub/OnBoarding/submit-final-pqq-answer
|
path: /host/Activity_Hub/OnBoarding/submit-final-pqq-answer
|
||||||
method: post
|
method: post
|
||||||
|
|
||||||
|
|
||||||
|
submitPQQForReview:
|
||||||
|
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQForReview.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/host/handlers/Activity_Hub/OnBoarding/submitPQQForReview.*'
|
||||||
|
- 'src/modules/host/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: /host/Activity_Hub/OnBoarding/submit-pqq-for-review
|
||||||
|
method: patch
|
||||||
|
|
||||||
getAllPQQwithSubmittedAns:
|
getAllPQQwithSubmittedAns:
|
||||||
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns.handler
|
handler: src/modules/host/handlers/Activity_Hub/OnBoarding/getAllPQQwithSubmittedAns.handler
|
||||||
memorySize: 512
|
memorySize: 512
|
||||||
|
|||||||
@@ -23,19 +23,15 @@ export const STEPPER = {
|
|||||||
REJECTED: 6
|
REJECTED: 6
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LAST_QUESTION_ID = {
|
|
||||||
Q_ID: 55
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ACTIVITY_INTERNAL_STATUS = {
|
export const ACTIVITY_INTERNAL_STATUS = {
|
||||||
DRAFT_PQ: 'Draft - PQ',
|
DRAFT_PQ: 'Draft - PQ',
|
||||||
APPROVED: 'Approved',
|
APPROVED: 'Approved',
|
||||||
REJECTED: 'Rejected',
|
REJECTED: 'Rejected',
|
||||||
DRAFT: 'Draft',
|
DRAFT: 'Draft',
|
||||||
UNDER_REVIEW: 'Under-Review',
|
UNDER_REVIEW: 'Under-Review',
|
||||||
PQQ_FAILED: 'PQQ Failed',
|
PQ_FAILED: 'PQ Failed',
|
||||||
PQQ_TO_UPDATE: 'PQ To Update',
|
PQ_TO_UPDATE: 'PQ To Update',
|
||||||
PQQ_SUBMITTED: 'PQ Submitted'
|
PQ_SUBMITTED: 'PQ Submitted'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ACTIVITY_DISPLAY_STATUS = {
|
export const ACTIVITY_DISPLAY_STATUS = {
|
||||||
@@ -44,7 +40,7 @@ export const ACTIVITY_DISPLAY_STATUS = {
|
|||||||
REJECTED: 'Rejected',
|
REJECTED: 'Rejected',
|
||||||
DRAFT: 'Draft',
|
DRAFT: 'Draft',
|
||||||
UNDER_REVIEW: 'Under-Review',
|
UNDER_REVIEW: 'Under-Review',
|
||||||
PQQ_FAILED: 'PQQ Failed',
|
PQ_FAILED: 'PQ Failed',
|
||||||
ENHANCING: 'Enchancing',
|
ENHANCING: 'Enchancing',
|
||||||
PQ_IN_REVIEW: 'PQ In Review'
|
PQ_IN_REVIEW: 'PQ In Review'
|
||||||
}
|
}
|
||||||
@@ -55,9 +51,9 @@ export const ACTIVITY_AM_INTERNAL_STATUS = {
|
|||||||
REJECTED: 'Rejected',
|
REJECTED: 'Rejected',
|
||||||
DRAFT: 'Draft',
|
DRAFT: 'Draft',
|
||||||
UNDER_REVIEW: 'Under-Review',
|
UNDER_REVIEW: 'Under-Review',
|
||||||
PQQ_FAILED: 'PQQ Failed',
|
PQ_FAILED: 'PQ Failed',
|
||||||
PQQ_REJECTED: 'PQ Rejected',
|
PQ_REJECTED: 'PQ Rejected',
|
||||||
PQQ_TO_REVIEW: 'PQ To Review'
|
PQ_TO_REVIEW: 'PQ To Review'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ACTIVITY_AM_DISPLAY_STATUS = {
|
export const ACTIVITY_AM_DISPLAY_STATUS = {
|
||||||
@@ -66,7 +62,7 @@ export const ACTIVITY_AM_DISPLAY_STATUS = {
|
|||||||
REJECTED: 'Rejected',
|
REJECTED: 'Rejected',
|
||||||
DRAFT: 'Draft',
|
DRAFT: 'Draft',
|
||||||
UNDER_REVIEW: 'Under-Review',
|
UNDER_REVIEW: 'Under-Review',
|
||||||
PQQ_FAILED: 'PQQ Failed',
|
PQ_FAILED: 'PQ Failed',
|
||||||
ENHANCING: 'Enchancing',
|
ENHANCING: 'Enchancing',
|
||||||
NEW: 'New'
|
NEW: 'New'
|
||||||
}
|
}
|
||||||
@@ -8,48 +8,36 @@ import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHo
|
|||||||
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
|
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
|
||||||
import ApiError from '../../../../../common/utils/helper/ApiError';
|
import ApiError from '../../../../../common/utils/helper/ApiError';
|
||||||
import { HostService } from '../../../services/host.service';
|
import { HostService } from '../../../services/host.service';
|
||||||
import { LAST_QUESTION_ID } from '@/common/utils/constants/host.constant';
|
|
||||||
|
|
||||||
const prisma = new PrismaService();
|
const prisma = new PrismaService();
|
||||||
const pqqService = new HostService(prisma);
|
const pqqService = new HostService(prisma);
|
||||||
|
|
||||||
const s3 = new AWS.S3({ region: config.aws.region });
|
const s3 = new AWS.S3({ region: config.aws.region });
|
||||||
|
|
||||||
// Function to extract S3 key from URL
|
// Extract S3 key from URL
|
||||||
function getS3KeyFromUrl(url: string): string {
|
function getS3KeyFromUrl(url: string): string {
|
||||||
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
|
const bucketBaseUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/`;
|
||||||
return url.replace(bucketBaseUrl, '');
|
return url.replace(bucketBaseUrl, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to delete file from S3
|
// Delete file from S3
|
||||||
async function deleteFromS3(s3Key: string): Promise<void> {
|
async function deleteFromS3(s3Key: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await s3.deleteObject({
|
await s3.deleteObject({
|
||||||
Bucket: config.aws.bucketName,
|
Bucket: config.aws.bucketName,
|
||||||
Key: s3Key,
|
Key: s3Key,
|
||||||
}).promise();
|
}).promise();
|
||||||
console.log(`✅ File deleted from S3: ${s3Key}`);
|
console.log(`✅ Deleted from S3: ${s3Key}`);
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error(`❌ Error deleting file from S3: ${s3Key}`, error);
|
console.error(`❌ Failed to delete from S3: ${s3Key}`, err);
|
||||||
// Don't throw error here, continue with upload
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise<string> {
|
// Upload new file
|
||||||
let s3Key: string;
|
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string): Promise<string> {
|
||||||
|
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
|
||||||
|
const s3Key = `${prefix}/${uniqueKey}`;
|
||||||
|
|
||||||
// If existing URL provided, use the same S3 key to replace the file
|
|
||||||
if (existingUrl) {
|
|
||||||
s3Key = getS3KeyFromUrl(existingUrl);
|
|
||||||
// Delete existing file first
|
|
||||||
await deleteFromS3(s3Key);
|
|
||||||
} else {
|
|
||||||
// Generate new unique key for new file
|
|
||||||
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
|
|
||||||
s3Key = `${prefix}/${uniqueKey}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload new file (replaces existing if same key)
|
|
||||||
await s3.upload({
|
await s3.upload({
|
||||||
Bucket: config.aws.bucketName,
|
Bucket: config.aws.bucketName,
|
||||||
Key: s3Key,
|
Key: s3Key,
|
||||||
@@ -58,253 +46,161 @@ async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string
|
|||||||
ACL: 'private'
|
ACL: 'private'
|
||||||
}).promise();
|
}).promise();
|
||||||
|
|
||||||
console.log(`✅ File uploaded to S3: ${s3Key}`);
|
console.log(`✅ Uploaded to S3: ${s3Key}`);
|
||||||
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
|
return `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${s3Key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
|
export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
|
||||||
try {
|
try {
|
||||||
// 1) Auth
|
// AUTH
|
||||||
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
|
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
|
||||||
if (!token) throw new ApiError(401, 'Missing token.');
|
if (!token) throw new ApiError(401, 'Missing token');
|
||||||
|
|
||||||
const user = await verifyHostToken(token);
|
const user = await verifyHostToken(token);
|
||||||
|
|
||||||
// 2) Content-Type check
|
// Content-Type
|
||||||
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
|
const contentType = event.headers["content-type"] || event.headers["Content-Type"];
|
||||||
if (!contentType?.startsWith("multipart/form-data"))
|
if (!contentType?.startsWith("multipart/form-data"))
|
||||||
throw new ApiError(400, "Content-Type must be multipart/form-data");
|
throw new ApiError(400, "Content-Type must be multipart/form-data");
|
||||||
|
|
||||||
if (!event.isBase64Encoded)
|
if (!event.isBase64Encoded) throw new ApiError(400, "Body must be base64 encoded");
|
||||||
throw new ApiError(400, "Body must be base64 encoded");
|
|
||||||
|
|
||||||
const bodyBuffer = Buffer.from(event.body!, "base64");
|
const bodyBuffer = Buffer.from(event.body!, "base64");
|
||||||
|
|
||||||
const fields: any = {};
|
const fields: any = {};
|
||||||
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
|
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
|
||||||
|
|
||||||
// 3) Parse multipart data
|
// Parse multipart
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const bb = Busboy({ headers: { 'content-type': contentType } });
|
const bb = Busboy({ headers: { 'content-type': contentType } });
|
||||||
|
|
||||||
bb.on('file', (fieldname, file, info) => {
|
bb.on('file', (fieldname, file, info) => {
|
||||||
const { filename, mimeType } = info;
|
const { filename, mimeType } = info;
|
||||||
|
if (!filename) return file.resume();
|
||||||
// Skip if no filename (empty file field)
|
|
||||||
if (!filename) {
|
|
||||||
file.resume();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
let totalSize = 0;
|
let size = 0;
|
||||||
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
||||||
|
file.on('data', chunk => {
|
||||||
|
size += chunk.length;
|
||||||
|
if (size > 5 * 1024 * 1024)
|
||||||
|
return reject(new ApiError(400, `File ${filename} exceeds 5MB limit`));
|
||||||
|
|
||||||
file.on('data', (chunk) => {
|
|
||||||
totalSize += chunk.length;
|
|
||||||
if (totalSize > MAX_SIZE) {
|
|
||||||
file.resume();
|
|
||||||
return reject(new ApiError(400, `File ${filename} exceeds 5MB limit.`));
|
|
||||||
}
|
|
||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
});
|
});
|
||||||
|
|
||||||
file.on('end', () => {
|
file.on('end', () => {
|
||||||
// Only add file if we have data
|
|
||||||
if (chunks.length > 0) {
|
if (chunks.length > 0) {
|
||||||
files.push({
|
files.push({
|
||||||
buffer: Buffer.concat(chunks),
|
buffer: Buffer.concat(chunks),
|
||||||
mimeType,
|
mimeType,
|
||||||
fileName: filename,
|
fileName: filename,
|
||||||
fieldName: fieldname,
|
fieldName: fieldname
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
file.on('error', (err) => {
|
|
||||||
reject(new ApiError(400, `File upload error: ${err.message}`));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bb.on('field', (fieldname, val) => {
|
bb.on('field', (fieldname, val) => {
|
||||||
// Handle empty or null values
|
try { fields[fieldname] = JSON.parse(val); }
|
||||||
if (val === '' || val === 'null' || val === 'undefined') {
|
catch { fields[fieldname] = val; }
|
||||||
fields[fieldname] = null;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
fields[fieldname] = JSON.parse(val);
|
|
||||||
} catch {
|
|
||||||
fields[fieldname] = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
bb.on('close', () => {
|
|
||||||
console.log("✅ Busboy parsing completed");
|
|
||||||
console.log("📌 Fields:", fields);
|
|
||||||
console.log("📁 Files:", files.length);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
bb.on('error', (err) => {
|
|
||||||
console.error("❌ Busboy error:", err);
|
|
||||||
reject(new ApiError(400, `Multipart parsing error: ${err.message}`));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bb.on('close', resolve);
|
||||||
|
bb.on('error', err => reject(new ApiError(400, err.message)));
|
||||||
bb.end(bodyBuffer);
|
bb.end(bodyBuffer);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4) Extract required fields - only activityXid, pqqQuestionXid, pqqAnswerXid are required
|
// Required fields
|
||||||
const activityXid = Number(fields.activityXid);
|
const activityXid = Number(fields.activityXid);
|
||||||
const pqqQuestionXid = Number(fields.pqqQuestionXid);
|
const pqqQuestionXid = Number(fields.pqqQuestionXid);
|
||||||
const pqqAnswerXid = Number(fields.pqqAnswerXid);
|
const pqqAnswerXid = Number(fields.pqqAnswerXid);
|
||||||
|
|
||||||
// Comments and files are optional
|
|
||||||
const comments = fields.comments || null;
|
const comments = fields.comments || null;
|
||||||
|
|
||||||
// Validate required fields
|
if (!activityXid || !pqqQuestionXid || !pqqAnswerXid)
|
||||||
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Valid activityXid is required");
|
throw new ApiError(400, "Missing required fields");
|
||||||
if (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Valid pqqQuestionXid is required");
|
|
||||||
if (pqqQuestionXid !== LAST_QUESTION_ID.Q_ID) throw new ApiError(400, "Wrong question id.")
|
|
||||||
if (!pqqAnswerXid || isNaN(pqqAnswerXid)) throw new ApiError(400, "Valid pqqAnswerXid is required");
|
|
||||||
|
|
||||||
// console.log(`📝 Processing - Activity: ${activityXid}, Question: ${pqqQuestionXid}, Answer: ${pqqAnswerXid}`);
|
|
||||||
// console.log(`💬 Comments: ${comments ? 'Provided' : 'Not provided'}`);
|
|
||||||
// console.log(`📎 Files: ${files.length}`);
|
|
||||||
|
|
||||||
// 5) UPSERT: Check if header already exists for this combination
|
|
||||||
const existingHeader = await pqqService.findHeaderByCompositeKey(
|
|
||||||
activityXid,
|
|
||||||
pqqQuestionXid,
|
|
||||||
pqqAnswerXid
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// UPSERT header
|
||||||
|
const existingHeader = await pqqService.findHeaderByCompositeKey(activityXid, pqqQuestionXid);
|
||||||
let header;
|
let header;
|
||||||
|
|
||||||
if (existingHeader) {
|
if (existingHeader) {
|
||||||
console.log("🔄 Updating existing PQQ header");
|
header = await pqqService.updateHeader(existingHeader.id, pqqAnswerXid, comments);
|
||||||
// Update existing header (comments can be null)
|
|
||||||
header = await pqqService.updateHeader(
|
|
||||||
existingHeader.id,
|
|
||||||
comments
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.log("🆕 Creating new PQQ header");
|
header = await pqqService.createHeader(activityXid, pqqQuestionXid, pqqAnswerXid, comments);
|
||||||
// Create new header (comments can be null)
|
|
||||||
header = await pqqService.createHeader(
|
|
||||||
activityXid,
|
|
||||||
pqqQuestionXid,
|
|
||||||
pqqAnswerXid,
|
|
||||||
comments
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Calculate score after answer submission
|
|
||||||
|
// SCORE
|
||||||
const score = await pqqService.calculatePqqScoreForUser(activityXid);
|
const score = await pqqService.calculatePqqScoreForUser(activityXid);
|
||||||
|
|
||||||
|
// Existing supporting files
|
||||||
// 6) Get existing supporting files for this header
|
|
||||||
const existingSupportingFiles = await pqqService.getSupportingFilesByHeaderId(header.id);
|
const existingSupportingFiles = await pqqService.getSupportingFilesByHeaderId(header.id);
|
||||||
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
|
|
||||||
|
|
||||||
// 7) Handle file UPSERT - only if files are provided
|
// Read deletedFiles from frontend
|
||||||
const uploadedFiles: any[] = [];
|
const deletedFiles = Array.isArray(fields.deletedFiles) ? fields.deletedFiles : [];
|
||||||
|
|
||||||
|
const deleteResults = [];
|
||||||
|
const addResults = [];
|
||||||
|
|
||||||
|
// DELETE explicitly requested files (Case 3)
|
||||||
|
if (deletedFiles.length > 0) {
|
||||||
|
for (const del of deletedFiles) {
|
||||||
|
const id = Number(del.id);
|
||||||
|
const record = existingSupportingFiles.find(f => f.id === id);
|
||||||
|
if (!record) continue;
|
||||||
|
|
||||||
|
// Delete from S3
|
||||||
|
if (record.mediaFileName) {
|
||||||
|
const key = getS3KeyFromUrl(record.mediaFileName);
|
||||||
|
await deleteFromS3(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete from DB
|
||||||
|
await pqqService.deleteSupportingFile(record.id);
|
||||||
|
deleteResults.push({ id: record.id, deleted: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADD new uploaded files (Case 1 + Case 3 new files)
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
console.log("📤 Processing file uploads...");
|
for (const file of files) {
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
const file = files[i];
|
|
||||||
const existingFile = existingSupportingFiles[i] || null;
|
|
||||||
|
|
||||||
const url = await uploadToS3(
|
const url = await uploadToS3(
|
||||||
file.buffer,
|
file.buffer,
|
||||||
file.mimeType,
|
file.mimeType,
|
||||||
file.fileName,
|
file.fileName,
|
||||||
`ActivityOnboarding/supportings/${activityXid}`,
|
`ActivityOnboarding/supportings/${activityXid}`
|
||||||
existingFile ? existingFile.mediaFileName : undefined
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let supporting;
|
const newRec = await pqqService.addSupportingFile(header.id, file.mimeType, url);
|
||||||
if (existingFile) {
|
addResults.push(newRec);
|
||||||
// Update existing supporting file record
|
|
||||||
supporting = await pqqService.updateSupportingFile(
|
|
||||||
existingFile.id,
|
|
||||||
file.mimeType,
|
|
||||||
url
|
|
||||||
);
|
|
||||||
console.log(`🔄 Updated supporting file: ${existingFile.id}`);
|
|
||||||
} else {
|
|
||||||
// Create new supporting file record
|
|
||||||
supporting = await pqqService.addSupportingFile(
|
|
||||||
header.id,
|
|
||||||
file.mimeType,
|
|
||||||
url
|
|
||||||
);
|
|
||||||
console.log(`🆕 Created new supporting file: ${supporting.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadedFiles.push(supporting);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8) Delete any remaining existing files that weren't replaced
|
|
||||||
if (existingSupportingFiles.length > files.length) {
|
|
||||||
const filesToDelete = existingSupportingFiles.slice(files.length);
|
|
||||||
console.log(`🗑️ Deleting ${filesToDelete.length} unused supporting files`);
|
|
||||||
|
|
||||||
for (const fileToDelete of filesToDelete) {
|
|
||||||
await pqqService.deleteSupportingFile(fileToDelete.id);
|
|
||||||
// Also delete from S3
|
|
||||||
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
|
|
||||||
await deleteFromS3(s3Key);
|
|
||||||
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("📭 No files provided in request");
|
|
||||||
|
|
||||||
// If no files provided but existing files exist, delete them (cleanup)
|
|
||||||
if (existingSupportingFiles.length > 0) {
|
|
||||||
console.log(`🗑️ No new files provided, deleting ${existingSupportingFiles.length} existing files`);
|
|
||||||
for (const fileToDelete of existingSupportingFiles) {
|
|
||||||
await pqqService.deleteSupportingFile(fileToDelete.id);
|
|
||||||
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
|
|
||||||
await deleteFromS3(s3Key);
|
|
||||||
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9) Prepare response
|
// CASE 2 — NO deletion & NO new files => DO NOTHING to existing files
|
||||||
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
headers: {
|
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Access-Control-Allow-Origin": "*"
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
message: responseMessage,
|
message: existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully",
|
||||||
data: {
|
data: {
|
||||||
headerId: header.id,
|
headerId: header.id,
|
||||||
activityXid,
|
activityXid,
|
||||||
pqqQuestionXid,
|
pqqQuestionXid,
|
||||||
pqqAnswerXid,
|
pqqAnswerXid,
|
||||||
comments: comments,
|
comments,
|
||||||
score,
|
score,
|
||||||
files: {
|
files: {
|
||||||
uploaded: uploadedFiles,
|
added: addResults,
|
||||||
total: uploadedFiles.length
|
deleted: deleteResults
|
||||||
},
|
}
|
||||||
operation: existingHeader ? 'updated' : 'created',
|
|
||||||
fileOperation: files.length > 0 ?
|
|
||||||
(existingSupportingFiles.length > 0 ? 'replaced' : 'added') :
|
|
||||||
(existingSupportingFiles.length > 0 ? 'removed' : 'unchanged')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (err: any) {
|
||||||
console.error("❌ Error in submitPqqAnswer:", error);
|
console.error("❌ Error:", err);
|
||||||
throw error;
|
throw err;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,15 +29,18 @@ export const handler = safeHandler(async (
|
|||||||
if (!activity_xid || isNaN(activity_xid)) {
|
if (!activity_xid || isNaN(activity_xid)) {
|
||||||
throw new ApiError(400, "Activity id is required and must be a number.");
|
throw new ApiError(400, "Activity id is required and must be a number.");
|
||||||
}
|
}
|
||||||
|
let result = null;
|
||||||
|
|
||||||
// Fetch user with their HostHeader stepper info
|
// Fetch user with their HostHeader stepper info
|
||||||
const pqqQuestionDetails = await hostService.getLatestQuestionDetailsPQQ(activity_xid);
|
const pqqQuestionDetails = await hostService.getLatestQuestionDetailsPQQ(activity_xid);
|
||||||
|
|
||||||
const result = {
|
if (pqqQuestionDetails) {
|
||||||
pqqQuestionXid: pqqQuestionDetails.pqqQuestionXid,
|
result = {
|
||||||
pqqAnswerXid: pqqQuestionDetails.pqqAnswerXid,
|
pqqQuestionXid: pqqQuestionDetails.pqqQuestionXid,
|
||||||
pqqSubCategoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategoryXid,
|
pqqAnswerXid: pqqQuestionDetails.pqqAnswerXid || null,
|
||||||
categoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategories.categoryXid
|
pqqSubCategoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategoryXid || null,
|
||||||
|
categoryXid: pqqQuestionDetails.pqqQuestions.pqqSubCategories.categoryXid || null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { verifyHostToken } from '@/common/middlewares/jwt/authForHost';
|
||||||
|
import { APIGatewayProxyEvent, APIGatewayProxyResult } 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 prisma = new PrismaService();
|
||||||
|
const pqqService = new HostService(prisma);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const activity_xid = event.queryStringParameters?.activity_xid
|
||||||
|
? Number(event.queryStringParameters.activity_xid)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await pqqService.submitpqqforreview(Number(activity_xid))
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Access-Control-Allow-Origin": "*"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Your PQQ has been submitted for review.",
|
||||||
|
data: null
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("❌ Error in submitPqqAnswer:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -30,34 +30,24 @@ async function deleteFromS3(s3Key: string): Promise<void> {
|
|||||||
console.log(`✅ File deleted from S3: ${s3Key}`);
|
console.log(`✅ File deleted from S3: ${s3Key}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error deleting file from S3: ${s3Key}`, error);
|
console.error(`❌ Error deleting file from S3: ${s3Key}`, error);
|
||||||
// Don't throw error here, continue with upload
|
// 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> {
|
async function uploadToS3(buffer: Buffer, mimeType: string, originalName: string, prefix: string, existingUrl?: string): Promise<string> {
|
||||||
let s3Key: string;
|
// We intentionally do NOT reuse old key. If existingUrl is provided we delete old file and create a new random key.
|
||||||
|
|
||||||
// If existing URL provided, use the same S3 key to replace the file
|
|
||||||
// if (existingUrl) {
|
|
||||||
// s3Key = getS3KeyFromUrl(existingUrl);
|
|
||||||
// // Delete existing file first
|
|
||||||
// await deleteFromS3(s3Key);
|
|
||||||
// } else {
|
|
||||||
// // Generate new unique key for new file
|
|
||||||
// const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
|
|
||||||
// s3Key = `${prefix}/${uniqueKey}`;
|
|
||||||
// }
|
|
||||||
if (existingUrl) {
|
if (existingUrl) {
|
||||||
// Delete old file, but DO NOT reuse its name
|
try {
|
||||||
const oldKey = getS3KeyFromUrl(existingUrl);
|
const oldKey = getS3KeyFromUrl(existingUrl);
|
||||||
await deleteFromS3(oldKey);
|
await deleteFromS3(oldKey);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Warning deleting existingUrl before upload', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new key always
|
|
||||||
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
|
const uniqueKey = `${crypto.randomUUID()}_${originalName}`;
|
||||||
s3Key = `${prefix}/${uniqueKey}`;
|
const s3Key = `${prefix}/${uniqueKey}`;
|
||||||
|
|
||||||
// Upload new file (replaces existing if same key)
|
|
||||||
await s3.upload({
|
await s3.upload({
|
||||||
Bucket: config.aws.bucketName,
|
Bucket: config.aws.bucketName,
|
||||||
Key: s3Key,
|
Key: s3Key,
|
||||||
@@ -82,7 +72,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
|
|||||||
if (!contentType?.includes("multipart/form-data"))
|
if (!contentType?.includes("multipart/form-data"))
|
||||||
throw new ApiError(400, "Content-Type must be multipart/form-data");
|
throw new ApiError(400, "Content-Type must be multipart/form-data");
|
||||||
|
|
||||||
// 3) Body decoding (FIXED – same as addCompanyDetails)
|
// 3) Body decoding
|
||||||
const bodyBuffer = event.isBase64Encoded
|
const bodyBuffer = event.isBase64Encoded
|
||||||
? Buffer.from(event.body!, "base64")
|
? Buffer.from(event.body!, "base64")
|
||||||
: Buffer.from(event.body!, "binary");
|
: Buffer.from(event.body!, "binary");
|
||||||
@@ -90,7 +80,7 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
|
|||||||
const fields: any = {};
|
const fields: any = {};
|
||||||
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
|
const files: Array<{ buffer: Buffer; mimeType: string; fileName: string; fieldName: string }> = [];
|
||||||
|
|
||||||
// 4) Parse multipart data (FIXED – using bb.write + bb.end exactly like working lambda)
|
// 4) Parse multipart data
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const bb = Busboy({ headers: { 'content-type': contentType } });
|
const bb = Busboy({ headers: { 'content-type': contentType } });
|
||||||
|
|
||||||
@@ -152,41 +142,32 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
|
|||||||
bb.end();
|
bb.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4) Extract required fields - only activityXid, pqqQuestionXid, pqqAnswerXid are required
|
// 5) Extract required fields
|
||||||
const activityXid = Number(fields.activityXid);
|
const activityXid = Number(fields.activityXid);
|
||||||
const pqqQuestionXid = Number(fields.pqqQuestionXid);
|
const pqqQuestionXid = Number(fields.pqqQuestionXid);
|
||||||
const pqqAnswerXid = Number(fields.pqqAnswerXid);
|
const pqqAnswerXid = Number(fields.pqqAnswerXid);
|
||||||
|
|
||||||
// Comments and files are optional
|
|
||||||
const comments = fields.comments || null;
|
const comments = fields.comments || null;
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!activityXid || isNaN(activityXid)) throw new ApiError(400, "Valid activityXid is required");
|
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 (!pqqQuestionXid || isNaN(pqqQuestionXid)) throw new ApiError(400, "Valid pqqQuestionXid is required");
|
||||||
if (!pqqAnswerXid || isNaN(pqqAnswerXid)) throw new ApiError(400, "Valid pqqAnswerXid is required");
|
if (!pqqAnswerXid || isNaN(pqqAnswerXid)) throw new ApiError(400, "Valid pqqAnswerXid is required");
|
||||||
|
|
||||||
// console.log(`📝 Processing - Activity: ${activityXid}, Question: ${pqqQuestionXid}, Answer: ${pqqAnswerXid}`);
|
// 6) UPSERT header
|
||||||
// console.log(`💬 Comments: ${comments ? 'Provided' : 'Not provided'}`);
|
|
||||||
// console.log(`📎 Files: ${files.length}`);
|
|
||||||
|
|
||||||
// 5) UPSERT: Check if header already exists for this combination
|
|
||||||
const existingHeader = await pqqService.findHeaderByCompositeKey(
|
const existingHeader = await pqqService.findHeaderByCompositeKey(
|
||||||
activityXid,
|
activityXid,
|
||||||
pqqQuestionXid,
|
pqqQuestionXid,
|
||||||
pqqAnswerXid
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let header;
|
let header;
|
||||||
if (existingHeader) {
|
if (existingHeader) {
|
||||||
console.log("🔄 Updating existing PQQ header");
|
console.log("🔄 Updating existing PQQ header");
|
||||||
// Update existing header (comments can be null)
|
|
||||||
header = await pqqService.updateHeader(
|
header = await pqqService.updateHeader(
|
||||||
existingHeader.id,
|
existingHeader.id,
|
||||||
|
pqqAnswerXid,
|
||||||
comments
|
comments
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log("🆕 Creating new PQQ header");
|
console.log("🆕 Creating new PQQ header");
|
||||||
// Create new header (comments can be null)
|
|
||||||
header = await pqqService.createHeader(
|
header = await pqqService.createHeader(
|
||||||
activityXid,
|
activityXid,
|
||||||
pqqQuestionXid,
|
pqqQuestionXid,
|
||||||
@@ -195,79 +176,93 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) Get existing supporting files for this header
|
// 7) Get existing supporting files
|
||||||
const existingSupportingFiles = await pqqService.getSupportingFilesByHeaderId(header.id);
|
const existingSupportingFiles = await pqqService.getSupportingFilesByHeaderId(header.id);
|
||||||
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
|
console.log(`📁 Found ${existingSupportingFiles.length} existing supporting files`);
|
||||||
|
|
||||||
// 7) Handle file UPSERT - only if files are provided
|
// 8) Parse incoming control fields
|
||||||
const uploadedFiles: any[] = [];
|
// 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 pqqService.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) {
|
if (files.length > 0) {
|
||||||
console.log("📤 Processing file uploads...");
|
console.log(`📤 Processing ${files.length} uploaded new file(s)`);
|
||||||
|
for (const file of files) {
|
||||||
for (let i = 0; i < files.length; i++) {
|
try {
|
||||||
const file = files[i];
|
const url = await uploadToS3(
|
||||||
const existingFile = existingSupportingFiles[i] || null;
|
file.buffer,
|
||||||
|
|
||||||
const url = await uploadToS3(
|
|
||||||
file.buffer,
|
|
||||||
file.mimeType,
|
|
||||||
file.fileName,
|
|
||||||
`ActivityOnboarding/supportings/${activityXid}`,
|
|
||||||
existingFile ? existingFile.mediaFileName : undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
let supporting;
|
|
||||||
if (existingFile) {
|
|
||||||
// Update existing supporting file record
|
|
||||||
supporting = await pqqService.updateSupportingFile(
|
|
||||||
existingFile.id,
|
|
||||||
file.mimeType,
|
file.mimeType,
|
||||||
url
|
file.fileName,
|
||||||
|
`ActivityOnboarding/supportings/${activityXid}`
|
||||||
);
|
);
|
||||||
console.log(`🔄 Updated supporting file: ${existingFile.id}`);
|
|
||||||
} else {
|
// create DB record
|
||||||
// Create new supporting file record
|
const supporting = await pqqService.addSupportingFile(
|
||||||
supporting = await pqqService.addSupportingFile(
|
|
||||||
header.id,
|
header.id,
|
||||||
file.mimeType,
|
file.mimeType,
|
||||||
url
|
url
|
||||||
);
|
);
|
||||||
console.log(`🆕 Created new supporting file: ${supporting.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadedFiles.push(supporting);
|
addedResults.push(supporting);
|
||||||
}
|
console.log(`🆕 Created new supporting file record: ${supporting.id}`);
|
||||||
|
} catch (err: any) {
|
||||||
// 8) Delete any remaining existing files that weren't replaced
|
console.error('❌ Error uploading/creating supporting file', err);
|
||||||
if (existingSupportingFiles.length > files.length) {
|
// push failure result but continue processing other files
|
||||||
const filesToDelete = existingSupportingFiles.slice(files.length);
|
addedResults.push({ success: false, reason: err.message || 'upload/create failed' });
|
||||||
console.log(`🗑️ Deleting ${filesToDelete.length} unused supporting files`);
|
|
||||||
|
|
||||||
for (const fileToDelete of filesToDelete) {
|
|
||||||
await pqqService.deleteSupportingFile(fileToDelete.id);
|
|
||||||
// Also delete from S3
|
|
||||||
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
|
|
||||||
await deleteFromS3(s3Key);
|
|
||||||
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("📭 No files provided in request");
|
console.log('📭 No new files uploaded in request');
|
||||||
|
|
||||||
// If no files provided but existing files exist, delete them (cleanup)
|
|
||||||
if (existingSupportingFiles.length > 0) {
|
|
||||||
console.log(`🗑️ No new files provided, deleting ${existingSupportingFiles.length} existing files`);
|
|
||||||
for (const fileToDelete of existingSupportingFiles) {
|
|
||||||
await pqqService.deleteSupportingFile(fileToDelete.id);
|
|
||||||
const s3Key = getS3KeyFromUrl(fileToDelete.mediaFileName);
|
|
||||||
await deleteFromS3(s3Key);
|
|
||||||
console.log(`🗑️ Deleted supporting file: ${fileToDelete.id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9) Prepare response
|
// 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".
|
||||||
|
|
||||||
|
// 11) Compose response
|
||||||
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
|
const responseMessage = existingHeader ? "PQQ answer updated successfully" : "PQQ answer submitted successfully";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -284,15 +279,15 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
|
|||||||
activityXid,
|
activityXid,
|
||||||
pqqQuestionXid,
|
pqqQuestionXid,
|
||||||
pqqAnswerXid,
|
pqqAnswerXid,
|
||||||
comments: comments,
|
comments,
|
||||||
files: {
|
files: {
|
||||||
uploaded: uploadedFiles,
|
added: addedResults,
|
||||||
total: uploadedFiles.length
|
deleted: deletedResults,
|
||||||
|
existingKeptCount: (existingSupportingFiles.length - deletedResults.filter(d => d.success).length)
|
||||||
},
|
},
|
||||||
operation: existingHeader ? 'updated' : 'created',
|
operation: existingHeader ? 'updated' : 'created',
|
||||||
fileOperation: files.length > 0 ?
|
// summary label for UI convenience:
|
||||||
(existingSupportingFiles.length > 0 ? 'replaced' : 'added') :
|
fileOperation: (deletedResults.length > 0 || addedResults.length > 0) ? 'modified' : 'unchanged'
|
||||||
(existingSupportingFiles.length > 0 ? 'removed' : 'unchanged')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -301,4 +296,4 @@ export const handler = safeHandler(async (event: APIGatewayProxyEvent): Promise<
|
|||||||
console.error("❌ Error in submitPqqAnswer:", error);
|
console.error("❌ Error in submitPqqAnswer:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ export async function generateActivityRefNumber(tx: any) {
|
|||||||
return `ACT-${String(nextId).padStart(6, '0')}`;;
|
return `ACT-${String(nextId).padStart(6, '0')}`;;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function round2(value: number) {
|
||||||
|
return Math.round(value * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucket = config.aws.bucketName;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HostService {
|
export class HostService {
|
||||||
constructor(private prisma: PrismaService) { }
|
constructor(private prisma: PrismaService) { }
|
||||||
@@ -140,8 +146,6 @@ export class HostService {
|
|||||||
return { stepper: STEPPER.NOT_SUBMITTED } as any;
|
return { stepper: STEPPER.NOT_SUBMITTED } as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bucket = config.aws.bucketName;
|
|
||||||
|
|
||||||
if (host.HostDocuments?.length) {
|
if (host.HostDocuments?.length) {
|
||||||
|
|
||||||
for (const doc of host.HostDocuments) {
|
for (const doc of host.HostDocuments) {
|
||||||
@@ -394,7 +398,7 @@ export class HostService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPQQQuestionDetail(question_xid: number, activity_xid: number) {
|
async getPQQQuestionDetail(question_xid: number, activity_xid: number) {
|
||||||
return await this.prisma.activityPQQheader.findFirst({
|
const detailsOfQuestion = await this.prisma.activityPQQheader.findFirst({
|
||||||
where: {
|
where: {
|
||||||
activityXid: activity_xid,
|
activityXid: activity_xid,
|
||||||
pqqQuestionXid: question_xid,
|
pqqQuestionXid: question_xid,
|
||||||
@@ -403,10 +407,42 @@ export class HostService {
|
|||||||
select: {
|
select: {
|
||||||
pqqQuestionXid: true,
|
pqqQuestionXid: true,
|
||||||
pqqAnswerXid: true,
|
pqqAnswerXid: true,
|
||||||
ActivityPQQSupportings: true,
|
ActivityPQQSupportings: {
|
||||||
ActivityPQQSuggestions: true,
|
select: {
|
||||||
|
id: true,
|
||||||
|
activityPqqHeaderXid: true,
|
||||||
|
mediaFileName: true,
|
||||||
|
mediaType: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ActivityPQQSuggestions: {
|
||||||
|
where: { isActive: true, isReviewed: false },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
comments: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (detailsOfQuestion.ActivityPQQSupportings?.length) {
|
||||||
|
|
||||||
|
for (const doc of detailsOfQuestion.ActivityPQQSupportings) {
|
||||||
|
if (doc.mediaFileName) {
|
||||||
|
const filePath = doc.mediaFileName;
|
||||||
|
|
||||||
|
// If full URL is saved, extract only key
|
||||||
|
const key = filePath.startsWith('http')
|
||||||
|
? filePath.split('.com/')[1]
|
||||||
|
: filePath;
|
||||||
|
|
||||||
|
(doc as any).presignedUrl = await getPresignedUrl(bucket, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return detailsOfQuestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLatestQuestionDetailsPQQ(activity_xid: number) {
|
async getLatestQuestionDetailsPQQ(activity_xid: number) {
|
||||||
@@ -431,92 +467,201 @@ export class HostService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addOrUpdateCompanyDetails(
|
async addOrUpdateCompanyDetails(
|
||||||
user_xid: number,
|
user_xid: number,
|
||||||
companyData: HostCompanyDetailsInput,
|
companyData: HostCompanyDetailsInput,
|
||||||
documents: HostDocumentInput[],
|
documents: HostDocumentInput[],
|
||||||
parentCompanyData?: any | null,
|
parentCompanyData?: any | null,
|
||||||
parentDocuments?: HostDocumentInput[],
|
parentDocuments?: HostDocumentInput[],
|
||||||
isDraft: boolean = false,
|
isDraft: boolean = false,
|
||||||
) {
|
) {
|
||||||
return await this.prisma.$transaction(async (tx) => {
|
return await this.prisma.$transaction(async (tx) => {
|
||||||
// Check if host already has a company
|
// Check if host already has a company
|
||||||
const existingHostCompany = await tx.hostHeader.findFirst({
|
const existingHostCompany = await tx.hostHeader.findFirst({
|
||||||
where: { userXid: user_xid },
|
where: { userXid: user_xid },
|
||||||
include: { hostParent: true },
|
include: { hostParent: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
let hostStatusInternal;
|
let hostStatusInternal;
|
||||||
let hostStatusDisplay;
|
let hostStatusDisplay;
|
||||||
let minglarStatusInternal;
|
let minglarStatusInternal;
|
||||||
let minglarStatusDisplay;
|
let minglarStatusDisplay;
|
||||||
|
|
||||||
if (existingHostCompany) {
|
if (existingHostCompany) {
|
||||||
hostStatusInternal = existingHostCompany.hostStatusInternal;
|
hostStatusInternal = existingHostCompany.hostStatusInternal;
|
||||||
hostStatusDisplay = existingHostCompany.hostStatusDisplay;
|
hostStatusDisplay = existingHostCompany.hostStatusDisplay;
|
||||||
minglarStatusInternal = existingHostCompany.adminStatusInternal;
|
minglarStatusInternal = existingHostCompany.adminStatusInternal;
|
||||||
minglarStatusDisplay = existingHostCompany.adminStatusDisplay;
|
minglarStatusDisplay = existingHostCompany.adminStatusDisplay;
|
||||||
}
|
|
||||||
|
|
||||||
// CASE 1: Host was asked to update AND is submitting final
|
|
||||||
if (
|
|
||||||
existingHostCompany &&
|
|
||||||
existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE &&
|
|
||||||
!isDraft
|
|
||||||
) {
|
|
||||||
hostStatusInternal = HOST_STATUS_INTERNAL.HOST_SUBMITTED;
|
|
||||||
hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW;
|
|
||||||
|
|
||||||
minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW;
|
|
||||||
minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.TO_REVIEW;
|
|
||||||
}
|
|
||||||
// CASE 2: Host was asked to update BUT saving draft
|
|
||||||
else if (
|
|
||||||
existingHostCompany &&
|
|
||||||
existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE &&
|
|
||||||
isDraft
|
|
||||||
) {
|
|
||||||
// keep original
|
|
||||||
hostStatusInternal = existingHostCompany.hostStatusInternal;
|
|
||||||
hostStatusDisplay = existingHostCompany.hostStatusDisplay;
|
|
||||||
minglarStatusInternal = existingHostCompany.adminStatusInternal;
|
|
||||||
minglarStatusDisplay = existingHostCompany.adminStatusDisplay;
|
|
||||||
}
|
|
||||||
// CASE 3: Normal create or update
|
|
||||||
else {
|
|
||||||
hostStatusInternal = isDraft
|
|
||||||
? HOST_STATUS_INTERNAL.DRAFT
|
|
||||||
: HOST_STATUS_INTERNAL.HOST_SUBMITTED;
|
|
||||||
|
|
||||||
hostStatusDisplay = isDraft
|
|
||||||
? HOST_STATUS_DISPLAY.DRAFT
|
|
||||||
: HOST_STATUS_DISPLAY.UNDER_REVIEW;
|
|
||||||
|
|
||||||
minglarStatusInternal = isDraft
|
|
||||||
? MINGLAR_STATUS_INTERNAL.DRAFT
|
|
||||||
: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW;
|
|
||||||
|
|
||||||
minglarStatusDisplay = isDraft
|
|
||||||
? MINGLAR_STATUS_DISPLAY.DRAFT
|
|
||||||
: MINGLAR_STATUS_DISPLAY.NEW;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stepper = isDraft ? STEPPER.NOT_SUBMITTED : STEPPER.UNDER_REVIEW;
|
|
||||||
|
|
||||||
// -------------------------------------------------------
|
|
||||||
// CREATE FLOW
|
|
||||||
// -------------------------------------------------------
|
|
||||||
if (!existingHostCompany) {
|
|
||||||
if (!isDraft) {
|
|
||||||
const existingByPan = await tx.hostHeader.findFirst({
|
|
||||||
where: { panNumber: companyData.panNumber },
|
|
||||||
});
|
|
||||||
if (existingByPan)
|
|
||||||
throw new ApiError(400, 'Company already exists with this pan/bin number');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdHost = await tx.hostHeader.create({
|
// CASE 1: Host was asked to update AND is submitting final
|
||||||
|
if (
|
||||||
|
existingHostCompany &&
|
||||||
|
existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE &&
|
||||||
|
!isDraft
|
||||||
|
) {
|
||||||
|
hostStatusInternal = HOST_STATUS_INTERNAL.HOST_SUBMITTED;
|
||||||
|
hostStatusDisplay = HOST_STATUS_DISPLAY.UNDER_REVIEW;
|
||||||
|
|
||||||
|
minglarStatusInternal = MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW;
|
||||||
|
minglarStatusDisplay = MINGLAR_STATUS_DISPLAY.TO_REVIEW;
|
||||||
|
}
|
||||||
|
// CASE 2: Host was asked to update BUT saving draft
|
||||||
|
else if (
|
||||||
|
existingHostCompany &&
|
||||||
|
existingHostCompany.hostStatusInternal === HOST_STATUS_INTERNAL.HOST_TO_UPDATE &&
|
||||||
|
isDraft
|
||||||
|
) {
|
||||||
|
// keep original
|
||||||
|
hostStatusInternal = existingHostCompany.hostStatusInternal;
|
||||||
|
hostStatusDisplay = existingHostCompany.hostStatusDisplay;
|
||||||
|
minglarStatusInternal = existingHostCompany.adminStatusInternal;
|
||||||
|
minglarStatusDisplay = existingHostCompany.adminStatusDisplay;
|
||||||
|
}
|
||||||
|
// CASE 3: Normal create or update
|
||||||
|
else {
|
||||||
|
hostStatusInternal = isDraft
|
||||||
|
? HOST_STATUS_INTERNAL.DRAFT
|
||||||
|
: HOST_STATUS_INTERNAL.HOST_SUBMITTED;
|
||||||
|
|
||||||
|
hostStatusDisplay = isDraft
|
||||||
|
? HOST_STATUS_DISPLAY.DRAFT
|
||||||
|
: HOST_STATUS_DISPLAY.UNDER_REVIEW;
|
||||||
|
|
||||||
|
minglarStatusInternal = isDraft
|
||||||
|
? MINGLAR_STATUS_INTERNAL.DRAFT
|
||||||
|
: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW;
|
||||||
|
|
||||||
|
minglarStatusDisplay = isDraft
|
||||||
|
? MINGLAR_STATUS_DISPLAY.DRAFT
|
||||||
|
: MINGLAR_STATUS_DISPLAY.NEW;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepper = isDraft ? STEPPER.NOT_SUBMITTED : STEPPER.UNDER_REVIEW;
|
||||||
|
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// CREATE FLOW
|
||||||
|
// -------------------------------------------------------
|
||||||
|
if (!existingHostCompany) {
|
||||||
|
if (!isDraft) {
|
||||||
|
const existingByPan = await tx.hostHeader.findFirst({
|
||||||
|
where: { panNumber: companyData.panNumber },
|
||||||
|
});
|
||||||
|
if (existingByPan)
|
||||||
|
throw new ApiError(400, 'Company already exists with this pan/bin number');
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdHost = await tx.hostHeader.create({
|
||||||
|
data: {
|
||||||
|
user: { connect: { id: user_xid } },
|
||||||
|
companyName: companyData.companyName,
|
||||||
|
address1: companyData.address1,
|
||||||
|
address2: companyData.address2,
|
||||||
|
cities: companyData.cityXid ? { connect: { id: companyData.cityXid } } : undefined,
|
||||||
|
states: companyData.stateXid ? { connect: { id: companyData.stateXid } } : undefined,
|
||||||
|
countries: companyData.countryXid ? { connect: { id: companyData.countryXid } } : undefined,
|
||||||
|
pinCode: companyData.pinCode,
|
||||||
|
logoPath: companyData.logoPath || null,
|
||||||
|
isSubsidairy: companyData.isSubsidairy,
|
||||||
|
registrationNumber: companyData.registrationNumber,
|
||||||
|
panNumber: companyData.panNumber,
|
||||||
|
gstNumber: companyData.gstNumber || null,
|
||||||
|
formationDate: companyData.formationDate
|
||||||
|
? new Date(companyData.formationDate as any)
|
||||||
|
: null,
|
||||||
|
companyTypes: companyData.companyTypeXid
|
||||||
|
? { connect: { id: companyData.companyTypeXid } }
|
||||||
|
: undefined,
|
||||||
|
websiteUrl: companyData.websiteUrl || null,
|
||||||
|
instagramUrl: companyData.instagramUrl || null,
|
||||||
|
facebookUrl: companyData.facebookUrl || null,
|
||||||
|
linkedinUrl: companyData.linkedinUrl || null,
|
||||||
|
twitterUrl: companyData.twitterUrl || null,
|
||||||
|
stepper,
|
||||||
|
hostStatusInternal,
|
||||||
|
hostStatusDisplay,
|
||||||
|
adminStatusInternal: minglarStatusInternal,
|
||||||
|
adminStatusDisplay: minglarStatusDisplay,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// host documents
|
||||||
|
if (documents?.length) {
|
||||||
|
const docsData = documents.map((doc) => ({
|
||||||
|
hostXid: createdHost.id,
|
||||||
|
documentTypeXid: doc.documentTypeXid,
|
||||||
|
documentName: doc.documentName,
|
||||||
|
filePath: doc.filePath,
|
||||||
|
}));
|
||||||
|
await tx.hostDocuments.createMany({ data: docsData });
|
||||||
|
}
|
||||||
|
|
||||||
|
// parent create
|
||||||
|
if (companyData.isSubsidairy && parentCompanyData) {
|
||||||
|
const createdParent = await tx.hostParent.create({
|
||||||
|
data: {
|
||||||
|
host: { connect: { id: createdHost.id } },
|
||||||
|
companyName: parentCompanyData.companyName,
|
||||||
|
address1: parentCompanyData.address1 || null,
|
||||||
|
address2: parentCompanyData.address2 || null,
|
||||||
|
cities: parentCompanyData.cityXid
|
||||||
|
? { connect: { id: parentCompanyData.cityXid } }
|
||||||
|
: undefined,
|
||||||
|
states: parentCompanyData.stateXid
|
||||||
|
? { connect: { id: parentCompanyData.stateXid } }
|
||||||
|
: undefined,
|
||||||
|
countries: parentCompanyData.countryXid
|
||||||
|
? { connect: { id: parentCompanyData.countryXid } }
|
||||||
|
: undefined,
|
||||||
|
pinCode: parentCompanyData.pinCode || null,
|
||||||
|
logoPath: parentCompanyData.logoPath || null,
|
||||||
|
registrationNumber: parentCompanyData.registrationNumber || null,
|
||||||
|
panNumber: parentCompanyData.panNumber || null,
|
||||||
|
gstNumber: parentCompanyData.gstNumber || null,
|
||||||
|
formationDate: parentCompanyData.formationDate
|
||||||
|
? new Date(parentCompanyData.formationDate as any)
|
||||||
|
: null,
|
||||||
|
companyTypes: parentCompanyData.companyTypeXid
|
||||||
|
? { connect: { id: parentCompanyData.companyTypeXid } }
|
||||||
|
: undefined,
|
||||||
|
websiteUrl: parentCompanyData.websiteUrl || null,
|
||||||
|
instagramUrl: parentCompanyData.instagramUrl || null,
|
||||||
|
facebookUrl: parentCompanyData.facebookUrl || null,
|
||||||
|
linkedinUrl: parentCompanyData.linkedinUrl || null,
|
||||||
|
twitterUrl: parentCompanyData.twitterUrl || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// parent docs
|
||||||
|
if (parentDocuments?.length) {
|
||||||
|
const parentDocsData = parentDocuments.map((doc) => ({
|
||||||
|
hostParentXid: createdParent.id,
|
||||||
|
documentTypeXid: doc.documentTypeXid,
|
||||||
|
documentName: doc.documentName,
|
||||||
|
filePath: doc.filePath,
|
||||||
|
}));
|
||||||
|
await tx.hostParenetDocuments.createMany({ data: parentDocsData });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⭐ FIX — TRACK USING createdHost (no null risk)
|
||||||
|
await tx.hostTrack.create({
|
||||||
|
data: {
|
||||||
|
hostXid: createdHost.id,
|
||||||
|
updatedByRole: ROLE_NAME.HOST,
|
||||||
|
updatedByXid: user_xid,
|
||||||
|
trackStatus: createdHost.hostStatusInternal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// UPDATE FLOW
|
||||||
|
// -------------------------------------------------------
|
||||||
|
const updatedHost = await tx.hostHeader.update({
|
||||||
|
where: { id: existingHostCompany.id },
|
||||||
data: {
|
data: {
|
||||||
user: { connect: { id: user_xid } },
|
|
||||||
companyName: companyData.companyName,
|
companyName: companyData.companyName,
|
||||||
address1: companyData.address1,
|
address1: companyData.address1,
|
||||||
address2: companyData.address2,
|
address2: companyData.address2,
|
||||||
@@ -548,191 +693,28 @@ export class HostService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// host documents
|
// documents UPSERT
|
||||||
if (documents?.length) {
|
if (documents?.length) {
|
||||||
const docsData = documents.map((doc) => ({
|
for (const doc of documents) {
|
||||||
hostXid: createdHost.id,
|
const existingDoc = await tx.hostDocuments.findFirst({
|
||||||
documentTypeXid: doc.documentTypeXid,
|
where: {
|
||||||
documentName: doc.documentName,
|
|
||||||
filePath: doc.filePath,
|
|
||||||
}));
|
|
||||||
await tx.hostDocuments.createMany({ data: docsData });
|
|
||||||
}
|
|
||||||
|
|
||||||
// parent create
|
|
||||||
if (companyData.isSubsidairy && parentCompanyData) {
|
|
||||||
const createdParent = await tx.hostParent.create({
|
|
||||||
data: {
|
|
||||||
host: { connect: { id: createdHost.id } },
|
|
||||||
companyName: parentCompanyData.companyName,
|
|
||||||
address1: parentCompanyData.address1 || null,
|
|
||||||
address2: parentCompanyData.address2 || null,
|
|
||||||
cities: parentCompanyData.cityXid
|
|
||||||
? { connect: { id: parentCompanyData.cityXid } }
|
|
||||||
: undefined,
|
|
||||||
states: parentCompanyData.stateXid
|
|
||||||
? { connect: { id: parentCompanyData.stateXid } }
|
|
||||||
: undefined,
|
|
||||||
countries: parentCompanyData.countryXid
|
|
||||||
? { connect: { id: parentCompanyData.countryXid } }
|
|
||||||
: undefined,
|
|
||||||
pinCode: parentCompanyData.pinCode || null,
|
|
||||||
logoPath: parentCompanyData.logoPath || null,
|
|
||||||
registrationNumber: parentCompanyData.registrationNumber || null,
|
|
||||||
panNumber: parentCompanyData.panNumber || null,
|
|
||||||
gstNumber: parentCompanyData.gstNumber || null,
|
|
||||||
formationDate: parentCompanyData.formationDate
|
|
||||||
? new Date(parentCompanyData.formationDate as any)
|
|
||||||
: null,
|
|
||||||
companyTypes: parentCompanyData.companyTypeXid
|
|
||||||
? { connect: { id: parentCompanyData.companyTypeXid } }
|
|
||||||
: undefined,
|
|
||||||
websiteUrl: parentCompanyData.websiteUrl || null,
|
|
||||||
instagramUrl: parentCompanyData.instagramUrl || null,
|
|
||||||
facebookUrl: parentCompanyData.facebookUrl || null,
|
|
||||||
linkedinUrl: parentCompanyData.linkedinUrl || null,
|
|
||||||
twitterUrl: parentCompanyData.twitterUrl || null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// parent docs
|
|
||||||
if (parentDocuments?.length) {
|
|
||||||
const parentDocsData = parentDocuments.map((doc) => ({
|
|
||||||
hostParentXid: createdParent.id,
|
|
||||||
documentTypeXid: doc.documentTypeXid,
|
|
||||||
documentName: doc.documentName,
|
|
||||||
filePath: doc.filePath,
|
|
||||||
}));
|
|
||||||
await tx.hostParenetDocuments.createMany({ data: parentDocsData });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⭐ FIX — TRACK USING createdHost (no null risk)
|
|
||||||
await tx.hostTrack.create({
|
|
||||||
data: {
|
|
||||||
hostXid: createdHost.id,
|
|
||||||
updatedByRole: ROLE_NAME.HOST,
|
|
||||||
updatedByXid: user_xid,
|
|
||||||
trackStatus: createdHost.hostStatusInternal,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return createdHost;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------
|
|
||||||
// UPDATE FLOW
|
|
||||||
// -------------------------------------------------------
|
|
||||||
const updatedHost = await tx.hostHeader.update({
|
|
||||||
where: { id: existingHostCompany.id },
|
|
||||||
data: {
|
|
||||||
companyName: companyData.companyName,
|
|
||||||
address1: companyData.address1,
|
|
||||||
address2: companyData.address2,
|
|
||||||
cities: companyData.cityXid ? { connect: { id: companyData.cityXid } } : undefined,
|
|
||||||
states: companyData.stateXid ? { connect: { id: companyData.stateXid } } : undefined,
|
|
||||||
countries: companyData.countryXid ? { connect: { id: companyData.countryXid } } : undefined,
|
|
||||||
pinCode: companyData.pinCode,
|
|
||||||
logoPath: companyData.logoPath || null,
|
|
||||||
isSubsidairy: companyData.isSubsidairy,
|
|
||||||
registrationNumber: companyData.registrationNumber,
|
|
||||||
panNumber: companyData.panNumber,
|
|
||||||
gstNumber: companyData.gstNumber || null,
|
|
||||||
formationDate: companyData.formationDate
|
|
||||||
? new Date(companyData.formationDate as any)
|
|
||||||
: null,
|
|
||||||
companyTypes: companyData.companyTypeXid
|
|
||||||
? { connect: { id: companyData.companyTypeXid } }
|
|
||||||
: undefined,
|
|
||||||
websiteUrl: companyData.websiteUrl || null,
|
|
||||||
instagramUrl: companyData.instagramUrl || null,
|
|
||||||
facebookUrl: companyData.facebookUrl || null,
|
|
||||||
linkedinUrl: companyData.linkedinUrl || null,
|
|
||||||
twitterUrl: companyData.twitterUrl || null,
|
|
||||||
stepper,
|
|
||||||
hostStatusInternal,
|
|
||||||
hostStatusDisplay,
|
|
||||||
adminStatusInternal: minglarStatusInternal,
|
|
||||||
adminStatusDisplay: minglarStatusDisplay,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// documents UPSERT
|
|
||||||
if (documents?.length) {
|
|
||||||
for (const doc of documents) {
|
|
||||||
const existingDoc = await tx.hostDocuments.findFirst({
|
|
||||||
where: {
|
|
||||||
hostXid: updatedHost.id,
|
|
||||||
documentTypeXid: doc.documentTypeXid,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingDoc) {
|
|
||||||
await tx.hostDocuments.update({
|
|
||||||
where: { id: existingDoc.id },
|
|
||||||
data: {
|
|
||||||
filePath: doc.filePath,
|
|
||||||
documentName: doc.documentName || existingDoc.documentName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await tx.hostDocuments.create({
|
|
||||||
data: {
|
|
||||||
hostXid: updatedHost.id,
|
hostXid: updatedHost.id,
|
||||||
documentTypeXid: doc.documentTypeXid,
|
documentTypeXid: doc.documentTypeXid,
|
||||||
documentName: doc.documentName,
|
|
||||||
filePath: doc.filePath,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parent logic untouched
|
if (existingDoc) {
|
||||||
if (companyData.isSubsidairy) {
|
await tx.hostDocuments.update({
|
||||||
const parentRecords = existingHostCompany.hostParent;
|
where: { id: existingDoc.id },
|
||||||
const parentRecord = Array.isArray(parentRecords) ? parentRecords[0] : parentRecords;
|
|
||||||
|
|
||||||
if (!parentRecord) {
|
|
||||||
const createdParent = await tx.hostParent.create({
|
|
||||||
data: {
|
|
||||||
host: { connect: { id: updatedHost.id } },
|
|
||||||
companyName: parentCompanyData.companyName,
|
|
||||||
address1: parentCompanyData.address1 || null,
|
|
||||||
address2: parentCompanyData.address2 || null,
|
|
||||||
cities: parentCompanyData.cityXid
|
|
||||||
? { connect: { id: parentCompanyData.cityXid } }
|
|
||||||
: undefined,
|
|
||||||
states: parentCompanyData.stateXid
|
|
||||||
? { connect: { id: parentCompanyData.stateXid } }
|
|
||||||
: undefined,
|
|
||||||
countries: parentCompanyData.countryXid
|
|
||||||
? { connect: { id: parentCompanyData.countryXid } }
|
|
||||||
: undefined,
|
|
||||||
pinCode: parentCompanyData.pinCode || null,
|
|
||||||
logoPath: parentCompanyData.logoPath || null,
|
|
||||||
registrationNumber: parentCompanyData.registrationNumber || null,
|
|
||||||
panNumber: parentCompanyData.panNumber || null,
|
|
||||||
gstNumber: parentCompanyData.gstNumber || null,
|
|
||||||
formationDate: parentCompanyData.formationDate
|
|
||||||
? new Date(parentCompanyData.formationDate as any)
|
|
||||||
: null,
|
|
||||||
companyTypes: parentCompanyData.companyTypeXid
|
|
||||||
? { connect: { id: parentCompanyData.companyTypeXid } }
|
|
||||||
: undefined,
|
|
||||||
websiteUrl: parentCompanyData.websiteUrl || null,
|
|
||||||
instagramUrl: parentCompanyData.instagramUrl || null,
|
|
||||||
facebookUrl: parentCompanyData.facebookUrl || null,
|
|
||||||
linkedinUrl: parentCompanyData.linkedinUrl || null,
|
|
||||||
twitterUrl: parentCompanyData.twitterUrl || null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parentDocuments?.length) {
|
|
||||||
for (const doc of parentDocuments) {
|
|
||||||
await tx.hostParenetDocuments.create({
|
|
||||||
data: {
|
data: {
|
||||||
hostParentXid: createdParent.id,
|
filePath: doc.filePath,
|
||||||
|
documentName: doc.documentName || existingDoc.documentName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await tx.hostDocuments.create({
|
||||||
|
data: {
|
||||||
|
hostXid: updatedHost.id,
|
||||||
documentTypeXid: doc.documentTypeXid,
|
documentTypeXid: doc.documentTypeXid,
|
||||||
documentName: doc.documentName,
|
documentName: doc.documentName,
|
||||||
filePath: doc.filePath,
|
filePath: doc.filePath,
|
||||||
@@ -740,62 +722,53 @@ export class HostService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
await tx.hostParent.update({
|
|
||||||
where: { id: parentRecord.id },
|
|
||||||
data: {
|
|
||||||
companyName: parentCompanyData.companyName,
|
|
||||||
address1: parentCompanyData.address1 || null,
|
|
||||||
address2: parentCompanyData.address2 || null,
|
|
||||||
cities: parentCompanyData.cityXid
|
|
||||||
? { connect: { id: parentCompanyData.cityXid } }
|
|
||||||
: undefined,
|
|
||||||
states: parentCompanyData.stateXid
|
|
||||||
? { connect: { id: parentCompanyData.stateXid } }
|
|
||||||
: undefined,
|
|
||||||
countries: parentCompanyData.countryXid
|
|
||||||
? { connect: { id: parentCompanyData.countryXid } }
|
|
||||||
: undefined,
|
|
||||||
pinCode: parentCompanyData.pinCode || null,
|
|
||||||
logoPath: parentCompanyData.logoPath || null,
|
|
||||||
registrationNumber: parentCompanyData.registrationNumber || null,
|
|
||||||
panNumber: parentCompanyData.panNumber || null,
|
|
||||||
gstNumber: parentCompanyData.gstNumber || null,
|
|
||||||
formationDate: parentCompanyData.formationDate
|
|
||||||
? new Date(parentCompanyData.formationDate as any)
|
|
||||||
: null,
|
|
||||||
companyTypes: parentCompanyData.companyTypeXid
|
|
||||||
? { connect: { id: parentCompanyData.companyTypeXid } }
|
|
||||||
: undefined,
|
|
||||||
websiteUrl: parentCompanyData.websiteUrl || null,
|
|
||||||
instagramUrl: parentCompanyData.instagramUrl || null,
|
|
||||||
facebookUrl: parentCompanyData.facebookUrl || null,
|
|
||||||
linkedinUrl: parentCompanyData.linkedinUrl || null,
|
|
||||||
twitterUrl: parentCompanyData.twitterUrl || null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parentDocuments?.length) {
|
// parent logic untouched
|
||||||
for (const doc of parentDocuments) {
|
if (companyData.isSubsidairy) {
|
||||||
const existingParentDoc = await tx.hostParenetDocuments.findFirst({
|
const parentRecords = existingHostCompany.hostParent;
|
||||||
where: {
|
const parentRecord = Array.isArray(parentRecords) ? parentRecords[0] : parentRecords;
|
||||||
hostParentXid: parentRecord.id,
|
|
||||||
documentTypeXid: doc.documentTypeXid,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingParentDoc) {
|
if (!parentRecord) {
|
||||||
await tx.hostParenetDocuments.update({
|
const createdParent = await tx.hostParent.create({
|
||||||
where: { id: existingParentDoc.id },
|
data: {
|
||||||
data: {
|
host: { connect: { id: updatedHost.id } },
|
||||||
filePath: doc.filePath,
|
companyName: parentCompanyData.companyName,
|
||||||
documentName: doc.documentName || existingParentDoc.documentName,
|
address1: parentCompanyData.address1 || null,
|
||||||
},
|
address2: parentCompanyData.address2 || null,
|
||||||
});
|
cities: parentCompanyData.cityXid
|
||||||
} else {
|
? { connect: { id: parentCompanyData.cityXid } }
|
||||||
|
: undefined,
|
||||||
|
states: parentCompanyData.stateXid
|
||||||
|
? { connect: { id: parentCompanyData.stateXid } }
|
||||||
|
: undefined,
|
||||||
|
countries: parentCompanyData.countryXid
|
||||||
|
? { connect: { id: parentCompanyData.countryXid } }
|
||||||
|
: undefined,
|
||||||
|
pinCode: parentCompanyData.pinCode || null,
|
||||||
|
logoPath: parentCompanyData.logoPath || null,
|
||||||
|
registrationNumber: parentCompanyData.registrationNumber || null,
|
||||||
|
panNumber: parentCompanyData.panNumber || null,
|
||||||
|
gstNumber: parentCompanyData.gstNumber || null,
|
||||||
|
formationDate: parentCompanyData.formationDate
|
||||||
|
? new Date(parentCompanyData.formationDate as any)
|
||||||
|
: null,
|
||||||
|
companyTypes: parentCompanyData.companyTypeXid
|
||||||
|
? { connect: { id: parentCompanyData.companyTypeXid } }
|
||||||
|
: undefined,
|
||||||
|
websiteUrl: parentCompanyData.websiteUrl || null,
|
||||||
|
instagramUrl: parentCompanyData.instagramUrl || null,
|
||||||
|
facebookUrl: parentCompanyData.facebookUrl || null,
|
||||||
|
linkedinUrl: parentCompanyData.linkedinUrl || null,
|
||||||
|
twitterUrl: parentCompanyData.twitterUrl || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parentDocuments?.length) {
|
||||||
|
for (const doc of parentDocuments) {
|
||||||
await tx.hostParenetDocuments.create({
|
await tx.hostParenetDocuments.create({
|
||||||
data: {
|
data: {
|
||||||
hostParentXid: parentRecord.id,
|
hostParentXid: createdParent.id,
|
||||||
documentTypeXid: doc.documentTypeXid,
|
documentTypeXid: doc.documentTypeXid,
|
||||||
documentName: doc.documentName,
|
documentName: doc.documentName,
|
||||||
filePath: doc.filePath,
|
filePath: doc.filePath,
|
||||||
@@ -803,53 +776,116 @@ export class HostService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
await tx.hostParent.update({
|
||||||
|
where: { id: parentRecord.id },
|
||||||
|
data: {
|
||||||
|
companyName: parentCompanyData.companyName,
|
||||||
|
address1: parentCompanyData.address1 || null,
|
||||||
|
address2: parentCompanyData.address2 || null,
|
||||||
|
cities: parentCompanyData.cityXid
|
||||||
|
? { connect: { id: parentCompanyData.cityXid } }
|
||||||
|
: undefined,
|
||||||
|
states: parentCompanyData.stateXid
|
||||||
|
? { connect: { id: parentCompanyData.stateXid } }
|
||||||
|
: undefined,
|
||||||
|
countries: parentCompanyData.countryXid
|
||||||
|
? { connect: { id: parentCompanyData.countryXid } }
|
||||||
|
: undefined,
|
||||||
|
pinCode: parentCompanyData.pinCode || null,
|
||||||
|
logoPath: parentCompanyData.logoPath || null,
|
||||||
|
registrationNumber: parentCompanyData.registrationNumber || null,
|
||||||
|
panNumber: parentCompanyData.panNumber || null,
|
||||||
|
gstNumber: parentCompanyData.gstNumber || null,
|
||||||
|
formationDate: parentCompanyData.formationDate
|
||||||
|
? new Date(parentCompanyData.formationDate as any)
|
||||||
|
: null,
|
||||||
|
companyTypes: parentCompanyData.companyTypeXid
|
||||||
|
? { connect: { id: parentCompanyData.companyTypeXid } }
|
||||||
|
: undefined,
|
||||||
|
websiteUrl: parentCompanyData.websiteUrl || null,
|
||||||
|
instagramUrl: parentCompanyData.instagramUrl || null,
|
||||||
|
facebookUrl: parentCompanyData.facebookUrl || null,
|
||||||
|
linkedinUrl: parentCompanyData.linkedinUrl || null,
|
||||||
|
twitterUrl: parentCompanyData.twitterUrl || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parentDocuments?.length) {
|
||||||
|
for (const doc of parentDocuments) {
|
||||||
|
const existingParentDoc = await tx.hostParenetDocuments.findFirst({
|
||||||
|
where: {
|
||||||
|
hostParentXid: parentRecord.id,
|
||||||
|
documentTypeXid: doc.documentTypeXid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingParentDoc) {
|
||||||
|
await tx.hostParenetDocuments.update({
|
||||||
|
where: { id: existingParentDoc.id },
|
||||||
|
data: {
|
||||||
|
filePath: doc.filePath,
|
||||||
|
documentName: doc.documentName || existingParentDoc.documentName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await tx.hostParenetDocuments.create({
|
||||||
|
data: {
|
||||||
|
hostParentXid: parentRecord.id,
|
||||||
|
documentTypeXid: doc.documentTypeXid,
|
||||||
|
documentName: doc.documentName,
|
||||||
|
filePath: doc.filePath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const previousParent = existingHostCompany.hostParent;
|
||||||
|
let prevParentId = null;
|
||||||
|
|
||||||
|
if (Array.isArray(previousParent) && previousParent.length) {
|
||||||
|
prevParentId = previousParent[0].id;
|
||||||
|
} else if (previousParent && typeof previousParent === 'object' && 'id' in previousParent) {
|
||||||
|
prevParentId = previousParent.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevParentId) {
|
||||||
|
await tx.hostParenetDocuments.deleteMany({
|
||||||
|
where: { hostParentXid: prevParentId },
|
||||||
|
});
|
||||||
|
await tx.hostParent.delete({ where: { id: prevParentId } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const previousParent = existingHostCompany.hostParent;
|
|
||||||
let prevParentId = null;
|
|
||||||
|
|
||||||
if (Array.isArray(previousParent) && previousParent.length) {
|
// ⭐ FIX — USE updatedHost instead of re-querying hostHeader
|
||||||
prevParentId = previousParent[0].id;
|
await tx.hostTrack.create({
|
||||||
} else if (previousParent && typeof previousParent === 'object' && 'id' in previousParent) {
|
|
||||||
prevParentId = previousParent.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevParentId) {
|
|
||||||
await tx.hostParenetDocuments.deleteMany({
|
|
||||||
where: { hostParentXid: prevParentId },
|
|
||||||
});
|
|
||||||
await tx.hostParent.delete({ where: { id: prevParentId } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⭐ FIX — USE updatedHost instead of re-querying hostHeader
|
|
||||||
await tx.hostTrack.create({
|
|
||||||
data: {
|
|
||||||
hostXid: updatedHost.id,
|
|
||||||
updatedByRole: ROLE_NAME.HOST,
|
|
||||||
updatedByXid: user_xid,
|
|
||||||
trackStatus: updatedHost.hostStatusInternal,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// suggestion update unchanged
|
|
||||||
if (!isDraft) {
|
|
||||||
await tx.hostSuggestion.updateMany({
|
|
||||||
where: { hostXid: updatedHost.id, isActive: true, isreviewed: false },
|
|
||||||
data: {
|
data: {
|
||||||
isreviewed: true,
|
hostXid: updatedHost.id,
|
||||||
reviewedByXid: user_xid,
|
updatedByRole: ROLE_NAME.HOST,
|
||||||
reviewOn: new Date(),
|
updatedByXid: user_xid,
|
||||||
|
trackStatus: updatedHost.hostStatusInternal,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return updatedHost;
|
// suggestion update unchanged
|
||||||
});
|
if (!isDraft) {
|
||||||
}
|
await tx.hostSuggestion.updateMany({
|
||||||
|
where: { hostXid: updatedHost.id, isActive: true, isreviewed: false },
|
||||||
|
data: {
|
||||||
|
isreviewed: true,
|
||||||
|
reviewedByXid: user_xid,
|
||||||
|
reviewOn: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedHost;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async getSuggestionDetails(user_xid: number) {
|
async getSuggestionDetails(user_xid: number) {
|
||||||
const hostDetails = await this.prisma.hostHeader.findFirst({
|
const hostDetails = await this.prisma.hostHeader.findFirst({
|
||||||
@@ -954,7 +990,7 @@ export class HostService {
|
|||||||
return await this.prisma.$transaction(async (tx) => {
|
return await this.prisma.$transaction(async (tx) => {
|
||||||
// 1. Get all headers for this activity (user's answers)
|
// 1. Get all headers for this activity (user's answers)
|
||||||
const answers = await this.prisma.activityPQQheader.findMany({
|
const answers = await this.prisma.activityPQQheader.findMany({
|
||||||
where: { activityXid },
|
where: { activityXid, isActive: true },
|
||||||
include: {
|
include: {
|
||||||
pqqQuestions: {
|
pqqQuestions: {
|
||||||
include: {
|
include: {
|
||||||
@@ -1020,7 +1056,7 @@ export class HostService {
|
|||||||
|
|
||||||
// Overall percent
|
// Overall percent
|
||||||
const overallPercentage =
|
const overallPercentage =
|
||||||
totalMaxPoints > 0 ? (totalUserPoints / totalMaxPoints) * 100 : 0;
|
totalMaxPoints > 0 ? round2((totalUserPoints / totalMaxPoints) * 100) : 0;
|
||||||
|
|
||||||
// ---------- 🔥 ONLY FIRST 2 CATEGORIES ----------
|
// ---------- 🔥 ONLY FIRST 2 CATEGORIES ----------
|
||||||
const categoryArray = Object.values(categories);
|
const categoryArray = Object.values(categories);
|
||||||
@@ -1035,7 +1071,7 @@ export class HostService {
|
|||||||
|
|
||||||
for (const c of topTwo) {
|
for (const c of topTwo) {
|
||||||
categoryWise[c.categoryName] =
|
categoryWise[c.categoryName] =
|
||||||
c.maxPoints > 0 ? (c.userPoints / c.maxPoints) * 100 : 0;
|
c.maxPoints > 0 ? round2((c.userPoints / c.maxPoints) * 100) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.activities.update({
|
await this.prisma.activities.update({
|
||||||
@@ -1043,13 +1079,9 @@ export class HostService {
|
|||||||
id: activityXid
|
id: activityXid
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
totalScore: overallPercentage,
|
totalScore: round2(overallPercentage),
|
||||||
sustainabilityScore: categoryWise.Sustainability,
|
sustainabilityScore: round2(categoryWise.Sustainability),
|
||||||
safetyScore: categoryWise.Safety,
|
safetyScore: round2(categoryWise.Safety),
|
||||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.PQQ_SUBMITTED,
|
|
||||||
activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.PQ_IN_REVIEW,
|
|
||||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.PQQ_TO_REVIEW,
|
|
||||||
amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.NEW
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1080,24 +1112,23 @@ export class HostService {
|
|||||||
async findHeaderByCompositeKey(
|
async findHeaderByCompositeKey(
|
||||||
activityXid: number,
|
activityXid: number,
|
||||||
pqqQuestionXid: number,
|
pqqQuestionXid: number,
|
||||||
pqqAnswerXid: number,
|
|
||||||
) {
|
) {
|
||||||
return await this.prisma.activityPQQheader.findFirst({
|
return await this.prisma.activityPQQheader.findFirst({
|
||||||
where: {
|
where: {
|
||||||
activityXid,
|
activityXid,
|
||||||
pqqQuestionXid,
|
pqqQuestionXid,
|
||||||
pqqAnswerXid,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateHeader(headerId: number, comments?: string | null) {
|
async updateHeader(headerId: number, pqqAnswerXid: number, comments?: string | null) {
|
||||||
return await this.prisma.activityPQQheader.update({
|
return await this.prisma.activityPQQheader.update({
|
||||||
where: {
|
where: {
|
||||||
id: headerId,
|
id: headerId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
comments: comments || null, // Handle null comments
|
comments: comments || null, // Handle null comments
|
||||||
|
pqqAnswerXid: pqqAnswerXid,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1124,6 +1155,32 @@ export class HostService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async submitpqqforreview(activity_xid: number) {
|
||||||
|
const activity = await this.prisma.activities.findFirst({
|
||||||
|
where: { id: activity_xid, isActive: true },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
activityTitle: true,
|
||||||
|
activityRefNumber: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!activity) {
|
||||||
|
throw new ApiError(404, "Activity not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.activities.update({
|
||||||
|
where: { id: activity_xid },
|
||||||
|
data: {
|
||||||
|
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.PQ_SUBMITTED,
|
||||||
|
activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.PQ_IN_REVIEW,
|
||||||
|
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.PQ_TO_REVIEW,
|
||||||
|
amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.NEW
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
async updateSupportingFile(
|
async updateSupportingFile(
|
||||||
supportingFileId: number,
|
supportingFileId: number,
|
||||||
mimeType: string,
|
mimeType: string,
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ export class PrePopulateService {
|
|||||||
async getAllPQQQuesAndAns() {
|
async getAllPQQQuesAndAns() {
|
||||||
return await this.prisma.pQQCategories.findMany({
|
return await this.prisma.pQQCategories.findMany({
|
||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
include: {
|
select: {
|
||||||
|
id: true,
|
||||||
|
categoryName: true,
|
||||||
|
displayOrder: true,
|
||||||
pqqsubCategories: {
|
pqqsubCategories: {
|
||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
Reference in New Issue
Block a user