// src/modules/host/services/host.service.ts import { Injectable } from '@nestjs/common'; import { PrismaService } from '../../../common/database/prisma.service'; import { AddPaymentDetailsDTO, CreateHostDto, UpdateHostDto, } from '../dto/host.dto'; import * as bcrypt from 'bcryptjs'; import ApiError from '../../../common/utils/helper/ApiError'; import { User } from '@prisma/client'; import { z } from 'zod'; import { hostCompanyDetailsSchema } from '@/common/utils/validation/host/hostCompanyDetails.validation'; import { ACTIVITY_AM_DISPLAY_STATUS, ACTIVITY_AM_INTERNAL_STATUS, ACTIVITY_DISPLAY_STATUS, ACTIVITY_INTERNAL_STATUS, HOST_STATUS_DISPLAY, HOST_STATUS_INTERNAL, STEPPER, } from '@/common/utils/constants/host.constant'; import { MINGLAR_STATUS_DISPLAY, MINGLAR_STATUS_INTERNAL, } from '@/common/utils/constants/minglar.constant'; import { ROLE, ROLE_NAME, USER_STATUS, } from '@/common/utils/constants/common.constant'; import { getPresignedUrl } from '@/common/middlewares/aws/getPreSignedUrl'; import config from '@/config/config'; type HostCompanyDetailsInput = z.infer; // Document input after S3 upload (with S3 URL as filePath) interface HostDocumentInput { documentTypeXid: number; documentName: string; filePath: string; // S3 URL } @Injectable() export class HostService { constructor(private prisma: PrismaService) { } async createHost(data: CreateHostDto) { return this.prisma.user.create({ data }); } async getAllHosts() { return this.prisma.user.findMany({ where: { roleXid: 3 } }); } async getHostIdByUserXid(user_xid: number) { const host = await this.prisma.hostHeader.findFirst({ where: { userXid: user_xid }, select: { id: true, companyName: true, countryXid: true, stepper: true }, }); return host; } async getHostById(id: number) { const host = await this.prisma.hostHeader.findFirst({ where: { userXid: id }, include: { hostParent: { include: { HostParenetDocuments: { select: { id: true, filePath: true, documentName: true, documentTypeXid: true, documentType: true } } } }, HostBankDetails: true, HostDocuments: { include: { documentType: true, }, }, user: { select: { id: true, emailAddress: true, firstName: true, lastName: true, mobileNumber: true, profileImage: true, userStatus: true } }, HostSuggestion: true, HostTrack: true, countries: true, currencies: true, states: true, cities: true, }, }); if (!host) { // If host record doesn't exist yet, return stepper 1 (NOT_SUBMITTED) // so callers (like the stepper endpoint) can show initial step. return { stepper: STEPPER.NOT_SUBMITTED } as any; } const bucket = config.aws.bucketName; if (host.HostDocuments?.length) { for (const doc of host.HostDocuments) { if (doc.filePath) { const filePath = doc.filePath; // 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); } } } if (host.user) { const key = host.user.profileImage.startsWith("http") ? host.user.profileImage.split(".com/")[1] : host.user.profileImage; host.user.profileImage = await getPresignedUrl(bucket, key); } if (host.logoPath) { const key = host.logoPath.startsWith('http') ? host.logoPath.split('.com/')[1] : host.logoPath; host.logoPath = await getPresignedUrl(bucket, key); } if (host.hostParent?.length) { const parent = host.hostParent[0]; // since you allow only 1 parent // Parent company logo if (parent.logoPath) { const key = parent.logoPath.startsWith("http") ? parent.logoPath.split(".com/")[1] : parent.logoPath; parent.logoPath = await getPresignedUrl(bucket, key); } // Parent documents if (parent.HostParenetDocuments?.length) { for (const doc of parent.HostParenetDocuments) { if (doc.filePath) { const key = doc.filePath.startsWith("http") ? doc.filePath.split(".com/")[1] : doc.filePath; (doc as any).presignedUrl = await getPresignedUrl(bucket, key); } } } } return host; } async updateHost(id: number, data: UpdateHostDto) { return this.prisma.user.update({ where: { id }, data, }); } async deleteHost(id: number) { return this.prisma.user.delete({ where: { id } }); } async getHostByEmail(email: string): Promise { return this.prisma.user.findUnique({ where: { emailAddress: email } }); } async verifyHostOtp(email: string, otp: string): Promise { const user = await this.prisma.user.findUnique({ where: { emailAddress: email }, select: { id: true, emailAddress: true, UserOtp: { where: { isActive: true, isVerified: false }, orderBy: { createdAt: 'desc' }, take: 1, }, }, }); if (!user) { throw new ApiError(404, 'User not found.'); } const userOtp = user.UserOtp[0]; if (!userOtp) { throw new ApiError(400, 'No OTP found.'); } if (new Date() > userOtp.expiresOn) { throw new ApiError(400, 'OTP has expired.'); } const isMatch = await bcrypt.compare(otp, userOtp.otpCode); if (!isMatch) { throw new ApiError(400, 'Invalid OTP.'); } await this.prisma.userOtp.update({ where: { id: userOtp.id }, data: { isVerified: true, verifiedOn: new Date(), isActive: false, }, }); return true; } async loginForHost(emailAddress: string, userPassword: string) { const existingUser = await this.prisma.user.findUnique({ where: { emailAddress: emailAddress }, }); if (!existingUser) { throw new ApiError(404, 'User not found'); } if (existingUser.roleXid !== 4) { throw new ApiError(403, 'Access denied. Not a host user.'); } const matchPassword = await bcrypt.compare( userPassword, existingUser.userPassword, ); if (!matchPassword) { throw new ApiError(401, 'Invalid credentials'); } return existingUser; } async createMinglarUser(email: string) { const newUser = await this.prisma.user.create({ data: { emailAddress: email, roleXid: ROLE.HOST }, }); return newUser; } async createPassword(user_xid: number, password: string): Promise { // Find user by id const user = await this.prisma.user.findUnique({ where: { id: user_xid }, select: { id: true, emailAddress: true, userPassword: true }, }); if (!user) { throw new ApiError(404, 'User not found'); } // Check if password already exists if (user.userPassword) { throw new ApiError( 400, 'Password already exists. Use update password instead.', ); } // Hash the password const saltRounds = parseInt(process.env.SALT_ROUNDS || '10', 10); const hashedPassword = await bcrypt.hash(password, saltRounds); // Update user with hashed password await this.prisma.user.update({ where: { id: user.id }, data: { userPassword: hashedPassword, isEmailVerfied: true, userStatus: USER_STATUS.ACTIVE, }, }); return true; } async addPaymentDetails(data: AddPaymentDetailsDTO) { return await this.prisma.$transaction(async (tx) => { const addedPaymentDetails = await tx.hostBankDetails.create({ data, }); if (!addedPaymentDetails) { throw new ApiError(400, 'Failed to add payment details'); } await tx.hostHeader.update({ where: { id: data.hostXid }, data: { stepper: STEPPER.BANK_DETAILS_UPDATED, }, }); }); } async getAllHostActivity(search?: string, hostXid?: number) { return await this.prisma.activities.findMany({ where: { isActive: true, hostXid: hostXid, }, include: { ActivitiesMedia: true, ActivityAmDetails: true, activityType: true, }, }); } async acceptMinglarAgreement(user_xid: number) { const hostDetails = await this.prisma.hostHeader.findFirst({ where: { userXid: user_xid }, select: { id: true, userXid: true, }, }); await this.prisma.hostHeader.update({ where: { id: hostDetails.id }, data: { stepper: STEPPER.AGREEMENT_ACCEPTED, agreementAccepted: true, }, }); } async getPQQQuestionDetail(question_xid: number, activity_xid: number) { return await this.prisma.activityPQQheader.findFirst({ where: { activityXid: activity_xid, pqqQuestionXid: question_xid, isActive: true, }, select: { pqqQuestionXid: true, pqqAnswerXid: true, ActivityPQQSupportings: true, ActivityPQQSuggestions: true, }, }); } async getLatestQuestionDetailsPQQ(activity_xid: number) { return await this.prisma.activityPQQheader.findFirst({ where: { activityXid: activity_xid, isActive: true }, select: { pqqQuestionXid: true, pqqAnswerXid: true, pqqQuestions: { select: { pqqSubCategoryXid: true, pqqSubCategories: { select: { categoryXid: true, }, }, }, }, }, orderBy: { id: 'desc' }, }); } async addOrUpdateCompanyDetails( user_xid: number, companyData: HostCompanyDetailsInput, documents: HostDocumentInput[], parentCompanyData?: any | null, parentDocuments?: HostDocumentInput[], isDraft: boolean = false, ) { return await this.prisma.$transaction(async (tx) => { // Check if host already has a company const existingHostCompany = await tx.hostHeader.findFirst({ where: { userXid: user_xid }, include: { hostParent: true }, }); // Determine status based on isDraft flag const hostStatusInternal = isDraft ? HOST_STATUS_INTERNAL.DRAFT : HOST_STATUS_INTERNAL.HOST_SUBMITTED; const hostStatusDisplay = isDraft ? HOST_STATUS_DISPLAY.DRAFT : HOST_STATUS_DISPLAY.UNDER_REVIEW; const minglarStatusInternal = isDraft ? MINGLAR_STATUS_INTERNAL.DRAFT : MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW; const minglarStatusDisplay = isDraft ? MINGLAR_STATUS_DISPLAY.DRAFT : MINGLAR_STATUS_DISPLAY.NEW; const stepper = isDraft ? STEPPER.NOT_SUBMITTED : STEPPER.UNDER_REVIEW; // CREATE if (!existingHostCompany) { // Optionally check unique registration number (only for final submission) 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 refNumber = await this.generateHostRefNumber(tx); const createdHost = await tx.hostHeader.create({ data: { user: { connect: { id: user_xid } }, companyName: companyData.companyName, hostRefNumber: refNumber, 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, companyType: companyData.companyType, websiteUrl: companyData.websiteUrl || null, instagramUrl: companyData.instagramUrl || null, facebookUrl: companyData.facebookUrl || null, linkedinUrl: companyData.linkedinUrl || null, twitterUrl: companyData.twitterUrl || null, stepper: stepper, hostStatusInternal: hostStatusInternal, hostStatusDisplay: hostStatusDisplay, adminStatusInternal: minglarStatusInternal, adminStatusDisplay: minglarStatusDisplay, }, }); // Create host documents (initial insert) 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 company and its docs (if present) if (companyData.isSubsidairy && parentCompanyData) { const createdParent = await tx.hostParent.create({ data: { hostXid: createdHost.id, companyName: parentCompanyData.companyName, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, cityXid: parentCompanyData.cityXid || null, stateXid: parentCompanyData.stateXid || null, countryXid: parentCompanyData.countryXid || null, pinCode: parentCompanyData.pinCode || null, logoPath: parentCompanyData.logoPath || null, isSubsidairy: false, registrationNumber: parentCompanyData.registrationNumber || null, panNumber: parentCompanyData.panNumber || null, gstNumber: parentCompanyData.gstNumber || null, formationDate: parentCompanyData.formationDate ? new Date(parentCompanyData.formationDate as any) : null, companyType: parentCompanyData.companyType || null, websiteUrl: parentCompanyData.websiteUrl || null, instagramUrl: parentCompanyData.instagramUrl || null, facebookUrl: parentCompanyData.facebookUrl || null, linkedinUrl: parentCompanyData.linkedinUrl || null, twitterUrl: parentCompanyData.twitterUrl || null, }, }); 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 }); } } return createdHost; } // UPDATE existing 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, companyType: companyData.companyType, websiteUrl: companyData.websiteUrl || null, instagramUrl: companyData.instagramUrl || null, facebookUrl: companyData.facebookUrl || null, linkedinUrl: companyData.linkedinUrl || null, twitterUrl: companyData.twitterUrl || null, stepper: stepper, hostStatusInternal: hostStatusInternal, hostStatusDisplay: hostStatusDisplay, adminStatusInternal: minglarStatusInternal, adminStatusDisplay: minglarStatusDisplay, }, }); // REPLACE/UPSERT host documents by documentTypeXid (Option A) if (documents?.length) { for (const doc of documents) { const existingDoc = await tx.hostDocuments.findFirst({ where: { hostXid: updatedHost.id, documentTypeXid: doc.documentTypeXid, }, }); if (existingDoc) { // update only filePath (and name if required) 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, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, }, }); } } } // Parent company create/update and replace parent docs by documentTypeXid if (companyData.isSubsidairy) { let parentRecord = (existingHostCompany as any).hostParent; if (Array.isArray(parentRecord)) parentRecord = parentRecord[0]; if (!parentRecord) { // create const createdParent = await tx.hostParent.create({ data: { hostXid: updatedHost.id, companyName: parentCompanyData.companyName, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, cityXid: parentCompanyData.cityXid || null, stateXid: parentCompanyData.stateXid || null, countryXid: parentCompanyData.countryXid || null, pinCode: parentCompanyData.pinCode || null, logoPath: parentCompanyData.logoPath || null, isSubsidairy: false, registrationNumber: parentCompanyData.registrationNumber || null, panNumber: parentCompanyData.panNumber || null, gstNumber: parentCompanyData.gstNumber || null, formationDate: parentCompanyData.formationDate ? new Date(parentCompanyData.formationDate as any) : null, companyType: parentCompanyData.companyType || null, 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: { hostParentXid: createdParent.id, documentTypeXid: doc.documentTypeXid, documentName: doc.documentName, filePath: doc.filePath, }, }); } } } else { // update existing parent await tx.hostParent.update({ where: { id: parentRecord.id }, data: { companyName: parentCompanyData.companyName, address1: parentCompanyData.address1 || null, address2: parentCompanyData.address2 || null, cityXid: parentCompanyData.cityXid || null, stateXid: parentCompanyData.stateXid || null, countryXid: parentCompanyData.countryXid || null, 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, companyType: parentCompanyData.companyType || null, websiteUrl: parentCompanyData.websiteUrl || null, instagramUrl: parentCompanyData.instagramUrl || null, facebookUrl: parentCompanyData.facebookUrl || null, linkedinUrl: parentCompanyData.linkedinUrl || null, twitterUrl: parentCompanyData.twitterUrl || null, }, }); // replace / upsert parent docs by documentTypeXid (no deletes) 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 { // If previously had a parent and now isSubsidairy=false -> optionally delete parent and its docs const previousParent = (existingHostCompany as any).hostParent; let prevParentId = null; if (Array.isArray(previousParent) && previousParent.length) prevParentId = previousParent[0].id; else if (previousParent && previousParent.id) prevParentId = previousParent.id; if (prevParentId) { await tx.hostParenetDocuments.deleteMany({ where: { hostParentXid: prevParentId }, }); await tx.hostParent.delete({ where: { id: prevParentId } }); } } // Create a host track entry const hostDetails = await tx.hostHeader.findFirst({ where: { userXid: user_xid }, }); await tx.hostTrack.create({ data: { hostXid: hostDetails!.id, updatedByRole: ROLE_NAME.HOST, updatedByXid: user_xid, trackStatus: hostDetails!.hostStatusInternal, }, }); return updatedHost; }); } async getSuggestionDetails(user_xid: number) { const hostDetails = await this.prisma.hostHeader.findFirst({ where: { userXid: user_xid, isActive: true }, include: { user: { select: { id: true, emailAddress: true, firstName: true, }, }, accountManager: { select: { id: true, emailAddress: true, firstName: true, }, }, }, }); if (!hostDetails) { return { hostSuggestionDetails: [], hostDetails: null }; } const hostSuggestionDetails = await this.prisma.hostSuggestion.findMany({ where: { hostXid: hostDetails.id, isActive: true, isreviewed: false }, }); if (hostSuggestionDetails) { await this.prisma.hostSuggestion.updateMany({ where: { hostXid: hostDetails.id, isActive: true, isreviewed: false }, data: { isreviewed: true, reviewedByXid: hostDetails.id, reviewOn: new Date(), }, }); } return { hostSuggestionDetails, hostDetails }; } async generateHostRefNumber(tx: any) { const lastHost = await tx.hostHeader.findFirst({ orderBy: { id: 'desc', }, select: { id: true, }, }); const nextId = lastHost ? lastHost.id + 1 : 1; return `HOSTREFNO-${String(nextId).padStart(6, '0')}`; } // async createOrUpdateHeader( // activityXid: number, // pqqQuestionXid: number, // pqqAnswerXid: number, // comments: string | null // ) { // // find existing header // const existing = await this.prisma.activityPQQheader.findFirst({ // where: { activityXid, pqqQuestionXid, deletedAt: null } // }); // if (!existing) { // return await this.prisma.activityPQQheader.create({ // data: { // activityXid, // pqqQuestionXid, // pqqAnswerXid, // comments // } // }); // } // // mark old supportings deleted // await this.prisma.activityPQQSupportings.updateMany({ // where: { activityPqqHeaderXid: existing.id }, // data: { // isActive: false, // deletedAt: new Date() // } // }); // // update header // return await this.prisma.activityPQQheader.update({ // where: { id: existing.id }, // data: { // pqqAnswerXid, // comments // } // }); // } // async addSupportingFile( // headerId: number, // mimeType: string, // fileUrl: string // ) { // return await this.prisma.activityPQQSupportings.create({ // data: { // activityPqqHeaderXid: headerId, // mediaType: mimeType, // mediaFileName: fileUrl // } // }); // } async calculatePqqScoreForUser(activityXid: number) { return await this.prisma.$transaction(async (tx) => { // 1. Get all headers for this activity (user's answers) const answers = await this.prisma.activityPQQheader.findMany({ where: { activityXid }, include: { pqqQuestions: { include: { pqqSubCategories: { include: { category: true, }, }, }, }, pqqAnswers: true, }, }); if (!answers.length) { return { overallPercentage: 0, categoryWise: {}, }; } // Prepare accumulators let totalUserPoints = 0; let totalMaxPoints = 0; // For category-wise scoring const categories: Record< number, { categoryId: number; categoryName: string; userPoints: number; maxPoints: number; } > = {}; for (const item of answers) { const question = item.pqqQuestions; const answer = item.pqqAnswers; const maxPoints = question.maxPoints; const userPoints = answer.answerPoints; totalUserPoints += userPoints; totalMaxPoints += maxPoints; // Category const category = question.pqqSubCategories.category; const categoryId = category.id; if (!categories[categoryId]) { categories[categoryId] = { categoryId, categoryName: category.categoryName, userPoints: 0, maxPoints: 0, }; } categories[categoryId].userPoints += userPoints; categories[categoryId].maxPoints += maxPoints; } // Overall percent const overallPercentage = totalMaxPoints > 0 ? (totalUserPoints / totalMaxPoints) * 100 : 0; // ---------- 🔥 ONLY FIRST 2 CATEGORIES ---------- const categoryArray = Object.values(categories); // Sort by categoryId (or change to displayOrder if needed) categoryArray.sort((a, b) => a.categoryId - b.categoryId); // Take only first 2 categories const topTwo = categoryArray.slice(0, 2); const categoryWise: Record = {}; for (const c of topTwo) { categoryWise[c.categoryName] = c.maxPoints > 0 ? (c.userPoints / c.maxPoints) * 100 : 0; } await this.prisma.activities.update({ where: { id: activityXid }, data: { totalScore: overallPercentage, sustainabilityScore: categoryWise.Sustainability, safetyScore: 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 } }) // Return final score object return { overallPercentage, categoryWise, }; }); } async createHeader( activityXid: number, pqqQuestionXid: number, pqqAnswerXid: number, comments?: string | null, ) { return await this.prisma.activityPQQheader.create({ data: { activityXid, pqqQuestionXid, pqqAnswerXid, comments: comments || null, // Handle null comments }, }); } async findHeaderByCompositeKey( activityXid: number, pqqQuestionXid: number, pqqAnswerXid: number, ) { return await this.prisma.activityPQQheader.findFirst({ where: { activityXid, pqqQuestionXid, pqqAnswerXid, }, }); } async updateHeader(headerId: number, comments?: string | null) { return await this.prisma.activityPQQheader.update({ where: { id: headerId, }, data: { comments: comments || null, // Handle null comments updatedAt: new Date(), }, }); } async addSupportingFile(headerId: number, mimeType: string, fileUrl: string) { return await this.prisma.activityPQQSupportings.create({ data: { activityPqqHeaderXid: headerId, mediaType: mimeType, mediaFileName: fileUrl, }, }); } async getSupportingFilesByHeaderId(headerId: number) { return await this.prisma.activityPQQSupportings.findMany({ where: { activityPqqHeaderXid: headerId, }, orderBy: { id: 'asc', // Maintain consistent order }, }); } async updateSupportingFile( supportingFileId: number, mimeType: string, fileUrl: string, ) { return await this.prisma.activityPQQSupportings.update({ where: { id: supportingFileId, }, data: { mediaType: mimeType, mediaFileName: fileUrl, updatedAt: new Date(), }, }); } async deleteSupportingFile(supportingFileId: number) { return await this.prisma.activityPQQSupportings.delete({ where: { id: supportingFileId, }, }); } async markPQQSuggestionReviewed(user_xid: number, activityPqqHeaderXid: number, activityPQQSuggestionId: number) { return await this.prisma.activityPQQSuggestions.update({ where: { id: activityPQQSuggestionId, activityPqqHeaderXid: activityPqqHeaderXid, isActive: true, isReviewed: false }, data: { isReviewed: true, reviewedByXid: user_xid, reviewedOn: new Date() } }) } async getAllPQQQuesAndSubmittedAns() { return await this.prisma.activityPQQheader.findMany({ where: { isActive: true }, include: { pqqQuestions: { select: { questionName: true, maxPoints: true, displayOrder: true, pqqSubCategories: { select: { id: true, subCategoryName: true, displayOrder: true, category: { select: { id: true, categoryName: true, displayOrder: true, } } } } } }, ActivityPQQSuggestions: { select: { id: true, title: true, comments: true, isReviewed: true, reviewedBy: true, reviewedOn: true, } }, pqqAnswers: { select: { id: true, displayOrder: true, answerName: true, answerPoints: true } }, ActivityPQQSupportings: { select: { id: true, mediaFileName: true, mediaType: true, } }, }, }); } async getAllActivityTypesWithInterest(search?: string) { const where: any = { isActive: true, deletedAt: null, }; if (search && search.trim() !== '') { const q = search.trim(); where.OR = [ { activityTypeName: { contains: q, mode: 'insensitive' } }, { interests: { interestName: { contains: q, mode: 'insensitive' } } }, ]; } return await this.prisma.activityTypes.findMany({ where, select: { id: true, activityTypeName: true, interestXid: true, interests: { select: { id: true, interestName: true, displayOrder: true, }, }, }, orderBy: { activityTypeName: 'asc' }, }); } async createActivity( userId: number, activityTypeXid: number, frequenciesXid?: number, ) { // Find host header for this user const host = await this.prisma.hostHeader.findFirst({ where: { userXid: userId, isActive: true }, }); if (!host) { throw new ApiError(404, 'Host not found for the user'); } // Validate activityType exists const activityType = await this.prisma.activityTypes.findUnique({ where: { id: activityTypeXid }, }); if (!activityType) { throw new ApiError(404, 'Activity type not found'); } // Optionally validate frequency if (frequenciesXid) { const freq = await this.prisma.frequencies.findUnique({ where: { id: frequenciesXid }, }); if (!freq) throw new ApiError(404, 'Frequency not found'); } const created = await this.prisma.activities.create({ data: { hostXid: host.id, activityTypeXid: 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, }, }); return created; } }