1934 lines
57 KiB
TypeScript
1934 lines
57 KiB
TypeScript
// src/modules/host/services/host.service.ts
|
||
import { Injectable } from '@nestjs/common';
|
||
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 { PrismaClient } 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 {
|
||
ACTIVITY_TRACK_STATUS,
|
||
ACTIVITY_TRACK_TYPE,
|
||
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<typeof hostCompanyDetailsSchema>;
|
||
|
||
// Document input after S3 upload (with S3 URL as filePath)
|
||
interface HostDocumentInput {
|
||
documentTypeXid: number;
|
||
documentName: string;
|
||
filePath: string; // S3 URL
|
||
}
|
||
export async function generateActivityRefNumber(tx: any) {
|
||
const lastrecord = await tx.activities.findFirst({
|
||
orderBy: {
|
||
id: 'desc',
|
||
},
|
||
select: {
|
||
id: true,
|
||
},
|
||
});
|
||
|
||
const nextId = lastrecord ? lastrecord.id + 1 : 1;
|
||
|
||
return `ACT-${String(nextId).padStart(6, '0')}`;;
|
||
}
|
||
|
||
function round2(value: number) {
|
||
return Math.round(value);
|
||
}
|
||
|
||
const bucket = config.aws.bucketName;
|
||
|
||
@Injectable()
|
||
export class HostService {
|
||
constructor(private prisma: PrismaClient) { }
|
||
|
||
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,
|
||
},
|
||
},
|
||
accountManager: {
|
||
select: {
|
||
id: true,
|
||
firstName: true,
|
||
lastName: true,
|
||
emailAddress: true,
|
||
mobileNumber: true,
|
||
profileImage: true,
|
||
userRefNumber: true,
|
||
}
|
||
},
|
||
user: {
|
||
select: {
|
||
id: true,
|
||
emailAddress: true,
|
||
firstName: true,
|
||
lastName: true,
|
||
mobileNumber: true,
|
||
profileImage: true,
|
||
userStatus: true,
|
||
userRefNumber: true,
|
||
}
|
||
},
|
||
companyTypes: {
|
||
select: {
|
||
id: true,
|
||
companyTypeName: true,
|
||
},
|
||
},
|
||
HostSuggestion: {
|
||
where: {
|
||
isActive: true,
|
||
isreviewed: false,
|
||
},
|
||
select: {
|
||
id: true,
|
||
hostXid: true,
|
||
title: true,
|
||
comments: true,
|
||
isparent: 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;
|
||
}
|
||
|
||
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?.profileImage) {
|
||
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.accountManager?.profileImage) {
|
||
const key = host.accountManager.profileImage.startsWith('http')
|
||
? host.accountManager.profileImage.split('.com/')[1]
|
||
: host.accountManager.profileImage;
|
||
|
||
host.accountManager.profileImage = 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<User> {
|
||
return this.prisma.user.findUnique({ where: { emailAddress: email } });
|
||
}
|
||
|
||
async verifyHostOtp(email: string, otp: string): Promise<boolean> {
|
||
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 },
|
||
select: {
|
||
id: true,
|
||
roleXid: true,
|
||
userPassword: true,
|
||
userStatus: true
|
||
}
|
||
});
|
||
console.log(existingUser, "ajsbfkjd")
|
||
|
||
if (!existingUser) {
|
||
throw new ApiError(404, 'User not found');
|
||
}
|
||
if (existingUser.userStatus == USER_STATUS.REJECTED) {
|
||
throw new ApiError(403, "You are not allowed to login. Please contact minglar admin.")
|
||
}
|
||
|
||
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<boolean> {
|
||
// 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 getBankBranchById(bankBranchXid: number) {
|
||
return await this.prisma.bankBranches.findUnique({
|
||
where: { id: bankBranchXid },
|
||
select: {
|
||
id: true,
|
||
ifscCode: true,
|
||
bankXid: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
async addPaymentDetails(data: AddPaymentDetailsDTO) {
|
||
return await this.prisma.$transaction(async (tx) => {
|
||
const existingAccount = await tx.hostBankDetails.findFirst({
|
||
where: {
|
||
accountNumber: data.accountNumber,
|
||
isActive: true,
|
||
},
|
||
});
|
||
|
||
if (existingAccount) {
|
||
throw new ApiError(
|
||
400,
|
||
'Host account with this account number already exists.'
|
||
);
|
||
}
|
||
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,
|
||
currencyXid: data.currencyXid
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
async getAllHostActivity(search?: string, user_xid?: number, paginationOptions?: { page: number; limit: number; skip: number }) {
|
||
const hostDetails = await this.prisma.hostHeader.findFirst({
|
||
where: { userXid: user_xid, isActive: true }
|
||
})
|
||
|
||
const whereClause: any = {
|
||
isActive: true,
|
||
hostXid: hostDetails.id,
|
||
};
|
||
if (!hostDetails) {
|
||
return {
|
||
data: [],
|
||
total: 0,
|
||
page: paginationOptions?.page || 1,
|
||
limit: paginationOptions?.limit || 10
|
||
};
|
||
}
|
||
|
||
// 🔍 SEARCH (fixed)
|
||
if (search?.trim()) {
|
||
const term = search.trim();
|
||
|
||
whereClause.OR = [
|
||
{ activityRefNumber: { contains: term, mode: 'insensitive' } },
|
||
{ activityTitle: { contains: term, mode: 'insensitive' } },
|
||
{
|
||
activityType: {
|
||
activityTypeName: { contains: term, mode: 'insensitive' }
|
||
}
|
||
},
|
||
];
|
||
}
|
||
|
||
const [hostAllActivities, totalCount] = await Promise.all([
|
||
this.prisma.activities.findMany({
|
||
where: whereClause,
|
||
select: {
|
||
id: true,
|
||
activityRefNumber: true,
|
||
activityTitle: true,
|
||
totalScore: true,
|
||
activityInternalStatus: true,
|
||
activityDisplayStatus: true,
|
||
amInternalStatus: true,
|
||
amDisplayStatus: true,
|
||
createdAt: true,
|
||
checkInAddress: true,
|
||
frequency: {
|
||
select: {
|
||
id: true,
|
||
frequencyName: true
|
||
}
|
||
},
|
||
ActivityAmDetails: {
|
||
select: {
|
||
accountManager: {
|
||
select: {
|
||
id: true,
|
||
firstName: true,
|
||
lastName: true,
|
||
profileImage: true,
|
||
emailAddress: true,
|
||
roleXid: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
activityType: {
|
||
select: {
|
||
id: true,
|
||
activityTypeName: true,
|
||
interests: {
|
||
select: {
|
||
id: true,
|
||
interestName: true
|
||
}
|
||
}
|
||
}
|
||
},
|
||
},
|
||
skip: paginationOptions?.skip || 0,
|
||
take: paginationOptions?.limit || 10,
|
||
orderBy: { id: 'desc' },
|
||
}),
|
||
this.prisma.activities.count({ where: whereClause }),
|
||
]);
|
||
|
||
for (const activity of hostAllActivities) {
|
||
/** 2️⃣ Process AM Profile Image */
|
||
const am = activity.ActivityAmDetails?.[0]?.accountManager;
|
||
|
||
if (am?.profileImage) {
|
||
const key = am.profileImage.startsWith("http")
|
||
? am.profileImage.split(".com/")[1]
|
||
: am.profileImage;
|
||
|
||
const presignedUrl = await getPresignedUrl(bucket, key);
|
||
|
||
activity.ActivityAmDetails[0].accountManager = {
|
||
...am,
|
||
profileImage: presignedUrl,
|
||
};
|
||
}
|
||
}
|
||
|
||
const { paginationService } = require('@/common/utils/pagination/pagination.service');
|
||
return paginationService.createPaginatedResponse(
|
||
hostAllActivities,
|
||
totalCount,
|
||
paginationOptions || { page: 1, limit: 10, skip: 0 }
|
||
);
|
||
}
|
||
|
||
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,
|
||
isApproved: true,
|
||
agreementAccepted: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
async getPQQQuestionDetail(question_xid: number, activity_xid: number) {
|
||
const detailsOfQuestion = await this.prisma.activityPQQheader.findFirst({
|
||
where: {
|
||
activityXid: activity_xid,
|
||
pqqQuestionXid: question_xid,
|
||
isActive: true,
|
||
},
|
||
select: {
|
||
pqqQuestionXid: true,
|
||
pqqAnswerXid: true,
|
||
ActivityPQQSupportings: {
|
||
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) {
|
||
return await this.prisma.activityPQQheader.findFirst({
|
||
where: {
|
||
activityXid: activity_xid,
|
||
isActive: true,
|
||
pqqAnswerXid: {
|
||
not: null
|
||
}
|
||
},
|
||
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 },
|
||
});
|
||
|
||
let existingParentCompany;
|
||
|
||
if (existingHostCompany) {
|
||
existingParentCompany = await tx.hostParent.findFirst({
|
||
where: { hostXid: existingHostCompany.id },
|
||
select: {
|
||
id: true,
|
||
logoPath: true
|
||
}
|
||
})
|
||
}
|
||
|
||
let hostStatusInternal;
|
||
let hostStatusDisplay;
|
||
let minglarStatusInternal;
|
||
let minglarStatusDisplay;
|
||
|
||
if (existingHostCompany) {
|
||
hostStatusInternal = existingHostCompany.hostStatusInternal;
|
||
hostStatusDisplay = existingHostCompany.hostStatusDisplay;
|
||
minglarStatusInternal = existingHostCompany.adminStatusInternal;
|
||
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({
|
||
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 || existingHostCompany.logoPath,
|
||
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,
|
||
referencedBy: companyData.referencedBy || null,
|
||
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 || existingParentCompany.logoPath,
|
||
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 || existingHostCompany.logoPath,
|
||
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,
|
||
referencedBy: companyData.referencedBy || null,
|
||
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,
|
||
documentTypeXid: doc.documentTypeXid,
|
||
documentName: doc.documentName,
|
||
filePath: doc.filePath,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// parent logic untouched
|
||
if (companyData.isSubsidairy) {
|
||
const parentRecords = existingHostCompany.hostParent;
|
||
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 || existingParentCompany.logoPath,
|
||
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: {
|
||
hostParentXid: createdParent.id,
|
||
documentTypeXid: doc.documentTypeXid,
|
||
documentName: doc.documentName,
|
||
filePath: doc.filePath,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
} 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 || existingParentCompany.logoPath,
|
||
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 } });
|
||
}
|
||
}
|
||
|
||
// ⭐ 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: {
|
||
isreviewed: true,
|
||
reviewedByXid: user_xid,
|
||
reviewOn: new Date(),
|
||
},
|
||
});
|
||
}
|
||
|
||
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,
|
||
userRefNumber: 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 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, isActive: true },
|
||
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 ? round2((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<string, number> = {};
|
||
|
||
for (const c of topTwo) {
|
||
categoryWise[c.categoryName] =
|
||
c.maxPoints > 0 ? round2((c.userPoints / c.maxPoints) * 100) : 0;
|
||
}
|
||
|
||
await this.prisma.activities.update({
|
||
where: {
|
||
id: activityXid
|
||
},
|
||
data: {
|
||
totalScore: round2(overallPercentage),
|
||
sustainabilityScore: round2(categoryWise.Sustainability),
|
||
safetyScore: round2(categoryWise.Safety),
|
||
}
|
||
})
|
||
|
||
// 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,
|
||
) {
|
||
return await this.prisma.activityPQQheader.findFirst({
|
||
where: {
|
||
activityXid,
|
||
pqqQuestionXid,
|
||
},
|
||
});
|
||
}
|
||
|
||
async updateHeader(headerId: number, pqqAnswerXid: number, comments?: string | null) {
|
||
return await this.prisma.activityPQQheader.update({
|
||
where: {
|
||
id: headerId,
|
||
},
|
||
data: {
|
||
comments: comments || null, // Handle null comments
|
||
pqqAnswerXid: pqqAnswerXid,
|
||
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 submitpqqforreview(activity_xid: number, user_xid: number) {
|
||
return await this.prisma.$transaction(async (tx) => {
|
||
const activity = await this.prisma.activities.findFirst({
|
||
where: { id: activity_xid, isActive: true },
|
||
select: {
|
||
id: true,
|
||
activityTitle: true,
|
||
activityRefNumber: true,
|
||
activityDisplayStatus: true,
|
||
activityInternalStatus: true,
|
||
amInternalStatus: true,
|
||
amDisplayStatus: true
|
||
}
|
||
})
|
||
|
||
if (!activity) {
|
||
throw new ApiError(404, "Activity not found")
|
||
}
|
||
|
||
if (activity.activityInternalStatus == ACTIVITY_INTERNAL_STATUS.PQ_TO_UPDATE) {
|
||
return await this.prisma.$transaction(async (tx) => {
|
||
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.REVISED
|
||
}
|
||
})
|
||
|
||
await tx.activityTrack.create({
|
||
data: {
|
||
activityXid: activity_xid,
|
||
trackType: ACTIVITY_TRACK_TYPE.PQ,
|
||
trackStatus: ACTIVITY_TRACK_STATUS.PQ_SUBMITTED,
|
||
updatedByXid: user_xid,
|
||
updatedByRole: ROLE_NAME.HOST,
|
||
updatedOn: new Date()
|
||
}
|
||
})
|
||
})
|
||
} else {
|
||
return await this.prisma.$transaction(async (tx) => {
|
||
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
|
||
}
|
||
})
|
||
|
||
await tx.activityTrack.create({
|
||
data: {
|
||
activityXid: activity_xid,
|
||
trackType: ACTIVITY_TRACK_TYPE.PQ,
|
||
trackStatus: ACTIVITY_TRACK_STATUS.PQ_SUBMITTED,
|
||
updatedByXid: user_xid,
|
||
updatedByRole: ROLE_NAME.HOST,
|
||
updatedOn: new Date()
|
||
}
|
||
})
|
||
})
|
||
}
|
||
})
|
||
|
||
}
|
||
|
||
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(activity_xid: number) {
|
||
return await this.prisma.activityPQQheader.findMany({
|
||
where: { isActive: true, activityXid: activity_xid },
|
||
select: {
|
||
id: true,
|
||
activityXid: true,
|
||
pqqQuestionXid: true,
|
||
pqqAnswerXid: true,
|
||
comments: true,
|
||
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,
|
||
) {
|
||
|
||
return await this.prisma.$transaction(async (tx) => {
|
||
|
||
// Fetch host
|
||
const host = await tx.hostHeader.findFirst({
|
||
where: { userXid: userId, isActive: true },
|
||
});
|
||
if (!host) throw new ApiError(404, 'Host not found for the user');
|
||
|
||
// Validate activityType
|
||
const activityType = await tx.activityTypes.findUnique({
|
||
where: { id: activityTypeXid },
|
||
});
|
||
if (!activityType) throw new ApiError(404, 'Activity type not found');
|
||
|
||
// Validate frequency
|
||
if (frequenciesXid) {
|
||
const freq = await tx.frequencies.findUnique({
|
||
where: { id: frequenciesXid },
|
||
});
|
||
if (!freq) throw new ApiError(404, 'Frequency not found');
|
||
}
|
||
|
||
// Generate reference number
|
||
const referenceNumber = await generateActivityRefNumber(tx);
|
||
|
||
// Create activity
|
||
const created = await tx.activities.create({
|
||
data: {
|
||
hostXid: host.id,
|
||
activityTypeXid,
|
||
frequenciesXid: frequenciesXid || null,
|
||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.DRAFT_PQ,
|
||
activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.DRAFT_PQ,
|
||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.DRAFT_PQ,
|
||
amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.DRAFT_PQ,
|
||
activityRefNumber: referenceNumber,
|
||
},
|
||
});
|
||
|
||
return created;
|
||
});
|
||
}
|
||
|
||
async createActivityAndAllQuestionsEntry(
|
||
userId: number,
|
||
activityTypeXid: number,
|
||
frequenciesXid: number
|
||
) {
|
||
return await this.prisma.$transaction(async (tx) => {
|
||
|
||
const host = await tx.hostHeader.findFirst({
|
||
where: { userXid: userId, isActive: true },
|
||
});
|
||
if (!host) throw new ApiError(404, 'Host not found for the user');
|
||
|
||
const activityType = await tx.activityTypes.findUnique({
|
||
where: { id: activityTypeXid },
|
||
});
|
||
if (!activityType) throw new ApiError(404, 'Activity type not found');
|
||
|
||
if (frequenciesXid) {
|
||
const freq = await tx.frequencies.findUnique({
|
||
where: { id: frequenciesXid },
|
||
});
|
||
if (!freq) throw new ApiError(404, 'Frequency not found');
|
||
}
|
||
|
||
const referenceNumber = await generateActivityRefNumber(tx);
|
||
|
||
const created = await tx.activities.create({
|
||
data: {
|
||
hostXid: host.id,
|
||
activityTypeXid,
|
||
frequenciesXid: frequenciesXid || null,
|
||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.DRAFT_PQ,
|
||
activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.DRAFT_PQ,
|
||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.DRAFT_PQ,
|
||
amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.DRAFT_PQ,
|
||
activityRefNumber: referenceNumber,
|
||
},
|
||
});
|
||
|
||
const questions = await tx.pQQCategories.findMany({
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
categoryName: true,
|
||
displayOrder: true,
|
||
pqqsubCategories: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
subCategoryName: true,
|
||
displayOrder: true,
|
||
questions: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
questionName: true,
|
||
maxPoints: true,
|
||
displayOrder: true,
|
||
},
|
||
orderBy: { displayOrder: 'asc' },
|
||
},
|
||
},
|
||
orderBy: { displayOrder: 'asc' },
|
||
},
|
||
},
|
||
orderBy: { displayOrder: 'asc' },
|
||
});
|
||
|
||
// FLATTEN questions
|
||
const allQuestions: number[] = [];
|
||
for (const cat of questions) {
|
||
for (const sub of cat.pqqsubCategories) {
|
||
for (const q of sub.questions) {
|
||
allQuestions.push(q.id);
|
||
}
|
||
}
|
||
}
|
||
|
||
await tx.activityPQQheader.createMany({
|
||
data: allQuestions.map((id) => ({
|
||
activityXid: created.id,
|
||
pqqQuestionXid: id,
|
||
pqqAnswerXid: null,
|
||
})),
|
||
});
|
||
|
||
const pqqHeaderData = await tx.activityPQQheader.findMany({
|
||
where: {
|
||
activityXid: created.id,
|
||
isActive: true,
|
||
},
|
||
select: {
|
||
comments: true,
|
||
pqqAnswerXid: true,
|
||
pqqQuestions: {
|
||
select: {
|
||
id: true,
|
||
questionName: true,
|
||
maxPoints: true,
|
||
displayOrder: true,
|
||
pqqSubCategories: {
|
||
select: {
|
||
id: true,
|
||
subCategoryName: true,
|
||
displayOrder: true,
|
||
category: {
|
||
select: {
|
||
id: true,
|
||
categoryName: true,
|
||
displayOrder: true
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
// 🔥 ALL ANSWER OPTIONS FOR THIS QUESTION
|
||
PQQAnswers: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
answerName: true,
|
||
answerPoints: true,
|
||
displayOrder: true
|
||
},
|
||
orderBy: { displayOrder: "asc" }
|
||
}
|
||
}
|
||
},
|
||
ActivityPQQSuggestions: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
title: true,
|
||
comments: true,
|
||
activityPqqHeaderXid: true
|
||
}
|
||
},
|
||
ActivityPQQSupportings: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
mediaType: true,
|
||
mediaFileName: true
|
||
}
|
||
},
|
||
},
|
||
orderBy: { id: "asc" }
|
||
});
|
||
|
||
// ---------------- GROUPING ------------------
|
||
const grouped: any = {};
|
||
|
||
for (const item of pqqHeaderData) {
|
||
const q = item.pqqQuestions;
|
||
const sub = q.pqqSubCategories;
|
||
const cat = sub.category;
|
||
|
||
// 1️⃣ Category level
|
||
if (!grouped[cat.id]) {
|
||
grouped[cat.id] = {
|
||
id: cat.id,
|
||
categoryName: cat.categoryName,
|
||
displayOrder: cat.displayOrder,
|
||
pqqsubCategories: []
|
||
};
|
||
}
|
||
|
||
const category = grouped[cat.id];
|
||
|
||
let subCat = category.pqqsubCategories.find((s: any) => s.id === sub.id);
|
||
if (!subCat) {
|
||
subCat = {
|
||
id: sub.id,
|
||
subCategoryName: sub.subCategoryName,
|
||
displayOrder: sub.displayOrder,
|
||
questions: []
|
||
};
|
||
category.pqqsubCategories.push(subCat);
|
||
}
|
||
|
||
subCat.questions.push({
|
||
id: q.id,
|
||
questionName: q.questionName,
|
||
maxPoints: q.maxPoints,
|
||
comments: item.comments || null,
|
||
displayOrder: q.displayOrder,
|
||
allAnswerOptions: q.PQQAnswers || [],
|
||
suggestions: item.ActivityPQQSuggestions || [],
|
||
supportings: item.ActivityPQQSupportings || [],
|
||
});
|
||
}
|
||
|
||
const sortedCategories: any = Object.values(grouped)
|
||
.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
|
||
|
||
for (const cat of sortedCategories) {
|
||
cat.pqqsubCategories.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
|
||
|
||
for (const sub of cat.pqqsubCategories) {
|
||
sub.questions.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
|
||
}
|
||
}
|
||
|
||
return {
|
||
activity_xid: created.id,
|
||
sortedCategories
|
||
};
|
||
});
|
||
}
|
||
|
||
|
||
async getAllPQUpdatedResponse(activityXid: number) {
|
||
const pqqHeaderData = await this.prisma.activityPQQheader.findMany({
|
||
where: {
|
||
activityXid: activityXid,
|
||
isActive: true,
|
||
},
|
||
select: {
|
||
id: true,
|
||
comments: true,
|
||
pqqAnswerXid: true,
|
||
pqqQuestions: {
|
||
select: {
|
||
id: true,
|
||
questionName: true,
|
||
maxPoints: true,
|
||
displayOrder: true,
|
||
pqqSubCategories: {
|
||
select: {
|
||
id: true,
|
||
subCategoryName: true,
|
||
displayOrder: true,
|
||
category: {
|
||
select: {
|
||
id: true,
|
||
categoryName: true,
|
||
displayOrder: true
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
// 🔥 ALL ANSWER OPTIONS FOR THIS QUESTION
|
||
PQQAnswers: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
answerName: true,
|
||
answerPoints: true,
|
||
displayOrder: true
|
||
},
|
||
orderBy: { displayOrder: "asc" }
|
||
}
|
||
}
|
||
},
|
||
ActivityPQQSuggestions: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
title: true,
|
||
comments: true,
|
||
activityPqqHeaderXid: true
|
||
}
|
||
},
|
||
ActivityPQQSupportings: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
mediaType: true,
|
||
mediaFileName: true
|
||
}
|
||
},
|
||
},
|
||
orderBy: { id: "asc" }
|
||
});
|
||
|
||
// ---------- GROUPING START ----------
|
||
const grouped: any = {};
|
||
|
||
for (const item of pqqHeaderData) {
|
||
const q = item.pqqQuestions;
|
||
const sub = q.pqqSubCategories;
|
||
const cat = sub.category;
|
||
|
||
// 1️⃣ Category level
|
||
// 1️⃣ Category level
|
||
if (!grouped[cat.id]) {
|
||
grouped[cat.id] = {
|
||
id: cat.id,
|
||
categoryName: cat.categoryName,
|
||
displayOrder: cat.displayOrder,
|
||
activityPqqHeaderId: item.id, // ✅ Added to match AM response
|
||
pqqsubCategories: []
|
||
};
|
||
} else if (!grouped[cat.id].activityPqqHeaderId) {
|
||
grouped[cat.id].activityPqqHeaderId = item.id; // Ensures it's set if missing
|
||
}
|
||
|
||
const category = grouped[cat.id];
|
||
|
||
// 2️⃣ Subcategory level
|
||
let subCat = category.pqqsubCategories.find((s: any) => s.id === sub.id);
|
||
if (!subCat) {
|
||
subCat = {
|
||
id: sub.id,
|
||
subCategoryName: sub.subCategoryName,
|
||
displayOrder: sub.displayOrder,
|
||
questions: []
|
||
};
|
||
category.pqqsubCategories.push(subCat);
|
||
}
|
||
|
||
// 3️⃣ Questions level
|
||
subCat.questions.push({
|
||
id: q.id,
|
||
questionName: q.questionName,
|
||
maxPoints: q.maxPoints,
|
||
pqqAnswerXid: item.pqqAnswerXid,
|
||
comments: item.comments || null,
|
||
displayOrder: q.displayOrder,
|
||
allAnswerOptions: q.PQQAnswers || [], // 🔥 All answers
|
||
suggestions: item.ActivityPQQSuggestions,
|
||
supportings: item.ActivityPQQSupportings,
|
||
});
|
||
}
|
||
|
||
// ---------- SORTING ----------
|
||
const sortedCategories: any = Object.values(grouped)
|
||
.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
|
||
|
||
for (const cat of sortedCategories) {
|
||
cat.pqqsubCategories.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
|
||
|
||
for (const sub of cat.pqqsubCategories) {
|
||
sub.questions.sort((a: any, b: any) => a.displayOrder - b.displayOrder);
|
||
}
|
||
}
|
||
|
||
// ---------- PRESIGNED URL GENERATION ----------
|
||
for (const cat of sortedCategories) {
|
||
for (const sub of cat.pqqsubCategories) {
|
||
for (const q of sub.questions) {
|
||
if (q.supportings?.length) {
|
||
for (const doc of q.supportings) {
|
||
if (doc.mediaFileName) {
|
||
const filePath = doc.mediaFileName;
|
||
const key = filePath.startsWith("http")
|
||
? filePath.split(".com/")[1]
|
||
: filePath;
|
||
|
||
doc.presignedUrl = await getPresignedUrl(bucket, key);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------- RETURN GROUPED STRUCTURE ----------
|
||
return sortedCategories;
|
||
}
|
||
}
|