1244 lines
33 KiB
TypeScript
1244 lines
33 KiB
TypeScript
import {
|
|
ROLE,
|
|
ROLE_NAME,
|
|
USER_STATUS,
|
|
} from '@/common/utils/constants/common.constant';
|
|
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_INVITATION_STATUS,
|
|
MINGLAR_STATUS_DISPLAY,
|
|
MINGLAR_STATUS_INTERNAL,
|
|
} from '@/common/utils/constants/minglar.constant';
|
|
import { Injectable } from '@nestjs/common';
|
|
import { User } from '@prisma/client';
|
|
import * as bcrypt from 'bcryptjs';
|
|
import { PrismaService } from '../../../common/database/prisma.service';
|
|
import ApiError from '../../../common/utils/helper/ApiError';
|
|
import { CreateMinglarDto, UpdateMinglarDto } from '../dto/minglar.dto';
|
|
import { sendAMEmailForHostAssign } from './AMEmail.service';
|
|
|
|
@Injectable()
|
|
export class MinglarService {
|
|
constructor(private prisma: PrismaService) { }
|
|
|
|
async createPassword(user_xid: number, password: string): Promise<boolean> {
|
|
// Find user by id
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id: user_xid, isActive: true, userStatus: USER_STATUS.INVITED },
|
|
select: { id: true, emailAddress: true, userPassword: true },
|
|
});
|
|
|
|
const invitationDetails = await this.prisma.inviteDetails.findMany({
|
|
where: {
|
|
userXid: user.id,
|
|
isActive: true,
|
|
isMinglarInvitation: true,
|
|
},
|
|
});
|
|
if (invitationDetails.length > 0) {
|
|
await this.prisma.inviteDetails.update({
|
|
where: { id: invitationDetails[0].id },
|
|
data: {
|
|
invitation_status: MINGLAR_INVITATION_STATUS.ACCEPTED,
|
|
accepted_on: new Date(),
|
|
is_accepted: 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,
|
|
userStatus: USER_STATUS.ACTIVE,
|
|
isEmailVerfied: true,
|
|
},
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
async createHost(data: CreateMinglarDto) {
|
|
return this.prisma.user.create({ data });
|
|
}
|
|
|
|
async getAllHosts() {
|
|
return this.prisma.user.findMany({ where: { roleXid: ROLE.HOST } });
|
|
}
|
|
|
|
async updateHost(id: number, data: UpdateMinglarDto) {
|
|
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 getUserDetails(id: number) {
|
|
return await this.prisma.user.findUnique({
|
|
where: { id: id },
|
|
});
|
|
}
|
|
|
|
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 loginForMinglar(emailAddress: string, userPassword: string) {
|
|
const existingUser = await this.prisma.user.findUnique({
|
|
where: {
|
|
emailAddress: emailAddress,
|
|
isActive: true,
|
|
userStatus: USER_STATUS.ACTIVE,
|
|
},
|
|
});
|
|
|
|
if (!existingUser) {
|
|
throw new ApiError(404, 'User not found');
|
|
}
|
|
|
|
if (
|
|
existingUser.roleXid !== ROLE.MINGLAR_ADMIN &&
|
|
existingUser.roleXid !== ROLE.CO_ADMIN &&
|
|
existingUser.roleXid !== ROLE.ACCOUNT_MANAGER
|
|
) {
|
|
throw new ApiError(403, 'Access denied.');
|
|
}
|
|
|
|
const matchPassword = await bcrypt.compare(
|
|
userPassword,
|
|
existingUser.userPassword,
|
|
);
|
|
if (!matchPassword) {
|
|
throw new ApiError(401, 'Invalid credentials');
|
|
}
|
|
|
|
return existingUser;
|
|
}
|
|
|
|
async checkUserExists(emailAddress: string) {
|
|
return await this.prisma.user.findUnique({
|
|
where: { emailAddress: emailAddress, isActive: true },
|
|
});
|
|
}
|
|
|
|
async createUserForInvite(emailAddress: string, roleXid: number) {
|
|
return await this.prisma.user.create({
|
|
data: {
|
|
emailAddress: emailAddress,
|
|
roleXid: roleXid,
|
|
userStatus: USER_STATUS.INVITED,
|
|
},
|
|
});
|
|
}
|
|
|
|
async getAllHostActivityForMinglar(search?: string, hostXid?: number) {
|
|
return await this.prisma.activities.findMany({
|
|
where: {
|
|
isActive: true,
|
|
hostXid: hostXid,
|
|
},
|
|
include: {
|
|
ActivitiesMedia: {
|
|
select: {
|
|
id: true,
|
|
mediaFileName: true,
|
|
mediaType: true,
|
|
displayOrder: true,
|
|
}
|
|
},
|
|
ActivityAmDetails: {
|
|
select: {
|
|
accountManager: {
|
|
select: {
|
|
id: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
profileImage: true,
|
|
emailAddress: true,
|
|
roleXid: true,
|
|
}
|
|
}
|
|
}
|
|
},
|
|
activityType: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
async createUserRevenue(
|
|
userXid: number,
|
|
isFixedSalary: boolean,
|
|
perValue: number,
|
|
) {
|
|
return await this.prisma.userRevenue.create({
|
|
data: {
|
|
userXid: userXid,
|
|
is_fixed_salary: isFixedSalary,
|
|
per_value: perValue || 0,
|
|
isActive: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
async createInviteDetails(
|
|
userXid: number,
|
|
invitedBy: number,
|
|
invitationStatus: string,
|
|
) {
|
|
return await this.prisma.inviteDetails.create({
|
|
data: {
|
|
userXid: userXid,
|
|
is_invited: true,
|
|
invited_by: invitedBy,
|
|
invited_on: new Date(),
|
|
is_accepted: false,
|
|
invitation_status: invitationStatus,
|
|
isActive: true,
|
|
isMinglarInvitation: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Invite teammate flow: checks existing user, creates user, revenue and invite details
|
|
* All operations are performed inside a single DB transaction to avoid races.
|
|
*/
|
|
async inviteTeammate(
|
|
emailAddress: string,
|
|
roleXid: number,
|
|
isFixedSalary: boolean,
|
|
perValue: number,
|
|
invitedBy: number,
|
|
) {
|
|
return await this.prisma.$transaction(async (tx) => {
|
|
// Check existing user
|
|
const existingUser = await tx.user.findFirst({
|
|
where: { emailAddress: emailAddress, isActive: true },
|
|
});
|
|
|
|
if (existingUser) {
|
|
throw new ApiError(400, 'User already exists.');
|
|
}
|
|
|
|
// Create user with INVITED status
|
|
const user = await tx.user.create({
|
|
data: {
|
|
emailAddress: emailAddress,
|
|
roleXid: roleXid,
|
|
userStatus: USER_STATUS.INVITED,
|
|
},
|
|
});
|
|
|
|
// Create revenue record
|
|
await tx.userRevenue.create({
|
|
data: {
|
|
userXid: user.id,
|
|
is_fixed_salary: isFixedSalary,
|
|
per_value: perValue || 0,
|
|
isActive: true,
|
|
},
|
|
});
|
|
|
|
// Create invite details
|
|
await tx.inviteDetails.create({
|
|
data: {
|
|
userXid: user.id,
|
|
is_invited: true,
|
|
invited_by: invitedBy,
|
|
invited_on: new Date(),
|
|
is_accepted: false,
|
|
invitation_status: MINGLAR_INVITATION_STATUS.INVITED,
|
|
isActive: true,
|
|
isMinglarInvitation: true,
|
|
},
|
|
});
|
|
|
|
return user;
|
|
});
|
|
}
|
|
|
|
async updateProfile(
|
|
userId: number,
|
|
userData: {
|
|
firstName?: string;
|
|
lastName?: string;
|
|
mobileNumber?: string;
|
|
dateOfBirth?: string;
|
|
profileImage?: string;
|
|
},
|
|
addressData: {
|
|
address1?: string;
|
|
address2?: string;
|
|
stateXid?: number;
|
|
countryXid?: number;
|
|
cityXid?: number;
|
|
pinCode?: string;
|
|
},
|
|
documents: Array<{ fileName: string; filePath: string }>,
|
|
) {
|
|
try {
|
|
return await this.prisma.$transaction(async (tx) => {
|
|
console.log('Starting transaction for user:', userId);
|
|
|
|
// 1. Update User table (optimized)
|
|
const userUpdateData: any = {};
|
|
const userFields = [
|
|
'firstName',
|
|
'lastName',
|
|
'mobileNumber',
|
|
'dateOfBirth',
|
|
'profileImage',
|
|
];
|
|
|
|
userFields.forEach((field) => {
|
|
if (userData[field as keyof typeof userData] !== undefined) {
|
|
if (field === 'dateOfBirth' && userData.dateOfBirth) {
|
|
userUpdateData[field] = new Date(userData.dateOfBirth);
|
|
} else {
|
|
userUpdateData[field] = userData[field as keyof typeof userData];
|
|
}
|
|
}
|
|
});
|
|
|
|
if (Object.keys(userUpdateData).length > 0) {
|
|
console.log('Updating user data:', userUpdateData);
|
|
await tx.user.update({
|
|
where: { id: userId },
|
|
data: userUpdateData,
|
|
});
|
|
}
|
|
|
|
// 2. Update or create UserAddressDetails
|
|
if (Object.keys(addressData).length > 0) {
|
|
console.log('Processing address data:', addressData);
|
|
|
|
const existingAddress = await tx.userAddressDetails.findFirst({
|
|
where: { userXid: userId, isActive: true },
|
|
select: { id: true }, // Only select needed field
|
|
});
|
|
|
|
const addressUpdateData: any = {};
|
|
const addressFields = [
|
|
'address1',
|
|
'address2',
|
|
'stateXid',
|
|
'countryXid',
|
|
'cityXid',
|
|
'pinCode',
|
|
];
|
|
|
|
addressFields.forEach((field) => {
|
|
if (addressData[field as keyof typeof addressData] !== undefined) {
|
|
addressUpdateData[field] =
|
|
addressData[field as keyof typeof addressData];
|
|
}
|
|
});
|
|
|
|
if (existingAddress) {
|
|
await tx.userAddressDetails.update({
|
|
where: { id: existingAddress.id },
|
|
data: addressUpdateData,
|
|
});
|
|
} else {
|
|
// Validate required fields
|
|
const requiredFields = [
|
|
'address1',
|
|
'stateXid',
|
|
'countryXid',
|
|
'cityXid',
|
|
'pinCode',
|
|
];
|
|
const missingFields = requiredFields.filter(
|
|
(field) => !addressData[field as keyof typeof addressData],
|
|
);
|
|
|
|
if (missingFields.length > 0) {
|
|
throw new ApiError(
|
|
400,
|
|
`Missing required address fields: ${missingFields.join(', ')}`,
|
|
);
|
|
}
|
|
|
|
await tx.userAddressDetails.create({
|
|
data: {
|
|
userXid: userId,
|
|
...addressUpdateData,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// 3. Handle documents more efficiently
|
|
if (documents && documents.length > 0) {
|
|
console.log('Processing documents:', documents.length);
|
|
|
|
// Use deleteMany and createMany for better performance
|
|
await tx.userDocuments.deleteMany({
|
|
where: { userXid: userId, isActive: true },
|
|
});
|
|
|
|
if (documents.length > 0) {
|
|
await tx.userDocuments.createMany({
|
|
data: documents.map((doc) => ({
|
|
userXid: userId,
|
|
fileName: doc.filePath,
|
|
isActive: true,
|
|
})),
|
|
});
|
|
}
|
|
}
|
|
|
|
// 4. Fetch updated user data efficiently
|
|
const updatedUser = await tx.user.findUnique({
|
|
where: { id: userId },
|
|
select: {
|
|
id: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
mobileNumber: true,
|
|
dateOfBirth: true,
|
|
profileImage: true,
|
|
userAddressDetails: {
|
|
where: { isActive: true },
|
|
take: 1,
|
|
select: {
|
|
id: true,
|
|
address1: true,
|
|
address2: true,
|
|
stateXid: true,
|
|
countryXid: true,
|
|
cityXid: true,
|
|
pinCode: true,
|
|
},
|
|
},
|
|
userDocuments: {
|
|
where: { isActive: true },
|
|
select: {
|
|
id: true,
|
|
fileName: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!updatedUser) {
|
|
throw new ApiError(404, 'User not found after update');
|
|
}
|
|
|
|
// 5. Calculate profile completion percentage
|
|
let percentage = 0;
|
|
|
|
// Profile Image: 15%
|
|
if (updatedUser.profileImage) percentage += 15;
|
|
|
|
// Name and Phone Number: 15%
|
|
if (
|
|
updatedUser.firstName &&
|
|
updatedUser.lastName &&
|
|
updatedUser.mobileNumber
|
|
) {
|
|
percentage += 15;
|
|
}
|
|
|
|
// Location Info: 25%
|
|
if (updatedUser.userAddressDetails.length > 0) {
|
|
const address = updatedUser.userAddressDetails[0];
|
|
if (
|
|
address.address1 &&
|
|
address.stateXid &&
|
|
address.countryXid &&
|
|
address.cityXid &&
|
|
address.pinCode
|
|
) {
|
|
percentage += 25;
|
|
}
|
|
}
|
|
|
|
// Documents: 45%
|
|
if (updatedUser.userDocuments.length >= 2) {
|
|
percentage += 45;
|
|
} else if (updatedUser.userDocuments.length === 1) {
|
|
percentage += 22.5;
|
|
}
|
|
|
|
const profilePercentage = Math.min(percentage, 100);
|
|
|
|
// Update profile completion status
|
|
if (profilePercentage > 80) {
|
|
await tx.user.update({
|
|
where: { id: userId },
|
|
data: { isProfileUpdated: true },
|
|
});
|
|
}
|
|
|
|
console.log('Transaction completed successfully');
|
|
|
|
return {
|
|
user: {
|
|
id: updatedUser.id,
|
|
firstName: updatedUser.firstName,
|
|
lastName: updatedUser.lastName,
|
|
mobileNumber: updatedUser.mobileNumber,
|
|
dateOfBirth: updatedUser.dateOfBirth,
|
|
profileImage: updatedUser.profileImage,
|
|
},
|
|
address: updatedUser.userAddressDetails[0] || null,
|
|
documents: updatedUser.userDocuments,
|
|
profileCompletionPercentage: profilePercentage,
|
|
};
|
|
});
|
|
} catch (error) {
|
|
console.error('Error in updateProfile transaction:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getAllInvitationDetails() {
|
|
return await this.prisma.inviteDetails.findMany({
|
|
where: {
|
|
isMinglarInvitation: true,
|
|
isActive: true,
|
|
},
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
emailAddress: true,
|
|
mobileNumber: true,
|
|
roleXid: true,
|
|
role: {
|
|
select: {
|
|
id: true,
|
|
roleName: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async getAllHostApplications(
|
|
userId: number,
|
|
userRoleXid: number,
|
|
search?: string,
|
|
userStatus?: string,
|
|
) {
|
|
const filters: any = {
|
|
isActive: true,
|
|
user: {
|
|
roleXid: {
|
|
notIn: [ROLE.CO_ADMIN, ROLE.ACCOUNT_MANAGER],
|
|
},
|
|
},
|
|
};
|
|
|
|
/** -----------------------------------
|
|
* SEARCH FILTER (ID, EMAIL, NAME)
|
|
* ----------------------------------- */
|
|
if (search?.trim()) {
|
|
const term = search.trim();
|
|
|
|
if (/^\d+$/.test(term)) {
|
|
// Search by Host ID
|
|
filters.id = Number(term);
|
|
} else {
|
|
// Search by email or name
|
|
filters.user = {
|
|
...filters.user,
|
|
OR: [
|
|
{ emailAddress: { contains: term, mode: 'insensitive' } },
|
|
{ firstName: { contains: term, mode: 'insensitive' } },
|
|
{ lastName: { contains: term, mode: 'insensitive' } },
|
|
],
|
|
};
|
|
}
|
|
}
|
|
|
|
/** -----------------------------------
|
|
* USER STATUS FILTER (NEW)
|
|
* ----------------------------------- */
|
|
if (
|
|
userStatus &&
|
|
userStatus.trim().toLowerCase() ===
|
|
MINGLAR_STATUS_DISPLAY.NEW.toLowerCase()
|
|
) {
|
|
filters.adminStatusInternal = MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW;
|
|
}
|
|
|
|
/** -----------------------------------
|
|
* ROLE-BASED FILTER:
|
|
* CO_ADMIN & ACCOUNT_MANAGER only see assigned hosts
|
|
* ----------------------------------- */
|
|
if (userRoleXid === ROLE.CO_ADMIN || userRoleXid === ROLE.ACCOUNT_MANAGER) {
|
|
filters.accountManagerXid = userId;
|
|
}
|
|
|
|
/** -----------------------------------
|
|
* MAIN QUERY
|
|
* ----------------------------------- */
|
|
const results = await this.prisma.hostHeader.findMany({
|
|
where: filters,
|
|
select: {
|
|
id: true,
|
|
hostStatusInternal: true,
|
|
hostStatusDisplay: true,
|
|
adminStatusDisplay: true,
|
|
adminStatusInternal: true,
|
|
createdAt: true,
|
|
companyName: true,
|
|
assignedOn: true,
|
|
|
|
cities: { select: { id: true, cityName: true } },
|
|
states: { select: { id: true, stateName: true } },
|
|
countries: { select: { id: true, countryName: true } },
|
|
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
emailAddress: true,
|
|
mobileNumber: true,
|
|
},
|
|
},
|
|
accountManager: {
|
|
select: {
|
|
id: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
emailAddress: true,
|
|
mobileNumber: true,
|
|
roleXid: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
|
|
/** -----------------------------------
|
|
* TRANSFORM RESPONSE
|
|
* ----------------------------------- */
|
|
return results.map((h) => ({
|
|
hostId: h.id,
|
|
host: h.user,
|
|
hostStatusDisplay: h.hostStatusDisplay,
|
|
hostStatusInternal: h.hostStatusInternal,
|
|
adminStatusDisplay: h.adminStatusDisplay,
|
|
adminStatusInternal: h.adminStatusInternal,
|
|
submittedOn: h.createdAt,
|
|
accountManager: h.accountManager || null,
|
|
companyName: h.companyName || null,
|
|
city: h.cities || null,
|
|
state: h.states || null,
|
|
country: h.countries || null,
|
|
assignedOn: h.assignedOn || null,
|
|
}));
|
|
}
|
|
|
|
async getAllOnboardingHostApplications() {
|
|
return await this.prisma.hostHeader.findMany({
|
|
where: {
|
|
isActive: true,
|
|
hostStatusInternal: { notIn: [HOST_STATUS_INTERNAL.DRAFT] },
|
|
},
|
|
select: {
|
|
id: true,
|
|
hostRefNumber: true,
|
|
companyName: true,
|
|
adminStatusDisplay: true,
|
|
assignedOn: true,
|
|
accountManagerXid: true,
|
|
createdAt: true,
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
},
|
|
},
|
|
accountManager: {
|
|
select: {
|
|
id: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
profileImage: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async getAllOnboardingHostApplications_New() {
|
|
return await this.prisma.hostHeader.findMany({
|
|
where: {
|
|
isActive: true,
|
|
adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW,
|
|
},
|
|
select: {
|
|
id: true,
|
|
hostRefNumber: true,
|
|
companyName: true,
|
|
adminStatusDisplay: true,
|
|
assignedOn: true,
|
|
accountManagerXid: true,
|
|
createdAt: true,
|
|
cities: {
|
|
select: {
|
|
id: true,
|
|
cityName: true,
|
|
},
|
|
},
|
|
countries: {
|
|
select: {
|
|
id: true,
|
|
countryName: true,
|
|
},
|
|
},
|
|
states: {
|
|
select: {
|
|
id: true,
|
|
stateName: true,
|
|
},
|
|
},
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
},
|
|
},
|
|
accountManager: {
|
|
select: {
|
|
id: true,
|
|
firstName: true,
|
|
lastName: true,
|
|
profileImage: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async getAllCoadminAndAM() {
|
|
// 1. Fetch all required users (Admin, Co-Admin, AM)
|
|
const users = await this.prisma.user.findMany({
|
|
where: {
|
|
roleXid: {
|
|
in: [
|
|
ROLE.MINGLAR_ADMIN, // Admin
|
|
ROLE.CO_ADMIN, // Co-Admin
|
|
ROLE.ACCOUNT_MANAGER, // AM
|
|
],
|
|
},
|
|
isActive: true,
|
|
userStatus: USER_STATUS.ACTIVE,
|
|
},
|
|
include: {
|
|
role: {
|
|
select: {
|
|
id: true,
|
|
roleName: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!users.length) return [];
|
|
|
|
const userIds = users.map((u) => u.id);
|
|
|
|
// 2. Count assigned hosts for ANY user (Admin / Co-Admin / AM)
|
|
const groupedHosts = await this.prisma.hostHeader.groupBy({
|
|
by: ['accountManagerXid'],
|
|
where: {
|
|
accountManagerXid: { in: userIds }, // assigned user
|
|
isActive: true,
|
|
},
|
|
_count: {
|
|
id: true,
|
|
},
|
|
});
|
|
|
|
// 3. Build quick lookup map: userId -> hostCount
|
|
const hostCountMap: Record<number, number> = {};
|
|
groupedHosts.forEach((g) => {
|
|
const uid = Number(g.accountManagerXid);
|
|
hostCountMap[uid] = g._count.id;
|
|
});
|
|
|
|
// 4. Attach host counts to each user
|
|
return users.map((user) => ({
|
|
...user,
|
|
assignedHostCount: hostCountMap[user.id] ?? 0,
|
|
}));
|
|
}
|
|
|
|
async getAllInvitedCoadminAndAM() {
|
|
return await this.prisma.user.findMany({
|
|
where: {
|
|
roleXid: {
|
|
in: [
|
|
ROLE.MINGLAR_ADMIN, // Admin
|
|
ROLE.CO_ADMIN, // Co-Admin
|
|
ROLE.ACCOUNT_MANAGER, // AM
|
|
],
|
|
},
|
|
isActive: true,
|
|
userStatus: {
|
|
not: USER_STATUS.DE_ACTIVATED, // Exclude DE_ACTIVATED status
|
|
},
|
|
},
|
|
include: {
|
|
role: {
|
|
select: {
|
|
id: true,
|
|
roleName: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async assignAMToHost(
|
|
userId: number,
|
|
hostXid: number,
|
|
accountManagerXid: number,
|
|
) {
|
|
const hostDetails = await this.prisma.hostHeader.findFirst({
|
|
where: { id: hostXid },
|
|
});
|
|
|
|
if (!hostDetails) {
|
|
throw new ApiError(404, 'Host not found');
|
|
}
|
|
|
|
if (hostDetails.accountManagerXid !== null) {
|
|
throw new ApiError(400, 'AM already assigned to this host');
|
|
}
|
|
|
|
if (
|
|
hostDetails.adminStatusInternal !==
|
|
MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED &&
|
|
hostDetails.adminStatusDisplay !== MINGLAR_STATUS_DISPLAY.AM_NOT_ASSIGNED
|
|
) {
|
|
throw new ApiError(400, 'Invalid host status');
|
|
}
|
|
|
|
await this.prisma.hostHeader.update({
|
|
where: { id: hostXid },
|
|
data: {
|
|
accountManagerXid: accountManagerXid,
|
|
assignedOn: new Date(),
|
|
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
|
|
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
|
|
adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW,
|
|
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.TO_REVIEW,
|
|
},
|
|
});
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Notify Account Manager by email after assignment.
|
|
* Encapsulates lookup + email send so handlers can call a single method.
|
|
*/
|
|
async notifyAMOfAssignment(accountManagerXid: number): Promise<boolean> {
|
|
if (!accountManagerXid) return false;
|
|
|
|
const amUser = await this.prisma.user.findUnique({
|
|
where: { id: accountManagerXid, isActive: true },
|
|
select: { emailAddress: true },
|
|
});
|
|
|
|
if (!amUser || !amUser.emailAddress) {
|
|
console.warn(
|
|
`AM notification skipped: user not found or missing email for id=${accountManagerXid}`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await sendAMEmailForHostAssign(amUser.emailAddress);
|
|
return true;
|
|
} catch (err) {
|
|
console.error('Error sending AM assignment email', err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async addHostSuggestion(
|
|
hostXid: number,
|
|
title: string,
|
|
comments: string,
|
|
reviewedByXid: number,
|
|
) {
|
|
// Check if host exists
|
|
const hostHeader = await this.prisma.hostHeader.findUnique({
|
|
where: { id: hostXid },
|
|
select: { id: true },
|
|
});
|
|
console.log(hostHeader);
|
|
|
|
if (!hostHeader) {
|
|
throw new ApiError(404, 'Host not found');
|
|
}
|
|
|
|
// Create suggestion in host_suggestion table
|
|
await this.prisma.hostSuggestion.create({
|
|
data: {
|
|
hostXid: hostXid,
|
|
title: title,
|
|
comments: comments,
|
|
isparent: false,
|
|
isreviewed: false,
|
|
reviewedByXid: reviewedByXid,
|
|
reviewOn: null,
|
|
isActive: true,
|
|
},
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
async addPqqSuggestion(
|
|
title: string,
|
|
comments: string,
|
|
activity_pqq_header_xid: number,
|
|
reviewedByXid: number,
|
|
) {
|
|
// Check if host exists
|
|
const ActivityHeader = await this.prisma.activityPQQheader.findUnique({
|
|
where: { id: activity_pqq_header_xid, isActive: true },
|
|
select: { id: true },
|
|
});
|
|
|
|
if (!ActivityHeader) {
|
|
throw new ApiError(404, 'Host not found');
|
|
}
|
|
|
|
await this.prisma.activityPQQSuggestions.create({
|
|
data: {
|
|
title: title,
|
|
comments: comments,
|
|
isReviewed: false,
|
|
reviewedOn: new Date(),
|
|
isActive: true,
|
|
activityPqqHeaderXid: activity_pqq_header_xid,
|
|
reviewedByXid: reviewedByXid,
|
|
},
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
async getHostSuggestions(userId: number) {
|
|
const hostDetail = await this.prisma.hostHeader.findFirst({
|
|
where: { userXid: userId, isActive: true },
|
|
});
|
|
|
|
const suggestions = await this.prisma.hostSuggestion.findMany({
|
|
where: { hostXid: hostDetail.id, isreviewed: false, isActive: true },
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
comments: true,
|
|
isparent: true,
|
|
isreviewed: true,
|
|
reviewOn: true,
|
|
},
|
|
orderBy: {
|
|
id: 'asc',
|
|
},
|
|
});
|
|
|
|
return suggestions;
|
|
}
|
|
|
|
async editAgreementDetails(
|
|
host_xid: number,
|
|
agreementStartDate: string,
|
|
duration: number,
|
|
isCommisionBase: boolean,
|
|
commisionPer: number,
|
|
amountPerBooking: number,
|
|
durationFrequency: string,
|
|
payoutDurationNum: number,
|
|
payoutDurationFrequency: string,
|
|
) {
|
|
return await this.prisma.hostHeader.update({
|
|
where: { id: host_xid },
|
|
data: {
|
|
durationNumber: Number(duration),
|
|
durationFrequency: durationFrequency,
|
|
agreementStartDate: new Date(agreementStartDate),
|
|
isCommisionBase: isCommisionBase,
|
|
commisionPer: commisionPer ? Number(commisionPer) : null, // Convert to number if exists
|
|
amountPerBooking: amountPerBooking ? Number(amountPerBooking) : null, // Convert to number if exists
|
|
payoutDurationNum: Number(payoutDurationNum), // Convert to number
|
|
payoutDurationFrequency: payoutDurationFrequency,
|
|
},
|
|
});
|
|
}
|
|
|
|
async acceptHostApplication(host_xid: number, user_xid: number) {
|
|
return await this.prisma.$transaction(async (tx) => {
|
|
await this.prisma.hostHeader.update({
|
|
where: {
|
|
id: host_xid,
|
|
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
|
|
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
|
|
adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_TO_REVIEW,
|
|
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.TO_REVIEW,
|
|
},
|
|
data: {
|
|
hostStatusInternal: HOST_STATUS_INTERNAL.APPROVED,
|
|
hostStatusDisplay: HOST_STATUS_DISPLAY.APPROVED,
|
|
adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_APPROVED,
|
|
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.APPROVED,
|
|
stepper: STEPPER.COMPANY_DETAILS_APPROVED,
|
|
},
|
|
});
|
|
|
|
await this.prisma.hostTrack.create({
|
|
data: {
|
|
hostXid: host_xid,
|
|
updatedByRole: ROLE_NAME.ACCOUNT_MANAGER,
|
|
updatedByXid: user_xid,
|
|
trackStatus: MINGLAR_STATUS_INTERNAL.AM_APPROVED,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
async acceptHostApplicationMinglarAdmin(host_xid: number, user_xid: number) {
|
|
return await this.prisma.$transaction(async (tx) => {
|
|
await tx.hostHeader.update({
|
|
where: {
|
|
id: host_xid,
|
|
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
|
|
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
|
|
adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_TO_REVIEW,
|
|
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.NEW,
|
|
},
|
|
data: {
|
|
isApproved: true,
|
|
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
|
|
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
|
|
adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED,
|
|
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.AM_NOT_ASSIGNED,
|
|
},
|
|
});
|
|
|
|
await this.prisma.hostTrack.create({
|
|
data: {
|
|
hostXid: host_xid,
|
|
updatedByRole: ROLE_NAME.MINGLAR_ADMIN,
|
|
updatedByXid: user_xid,
|
|
trackStatus: MINGLAR_STATUS_INTERNAL.AM_NOT_ASSIGNED,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
async rejectHostApplication(host_xid: number, user_xid: number) {
|
|
await this.prisma.$transaction(async (tx) => {
|
|
const hostDetails = await tx.hostHeader.findFirst({
|
|
where: { id: host_xid },
|
|
select: { id: true, userXid: true },
|
|
});
|
|
if (!hostDetails) {
|
|
throw new Error('Host not found');
|
|
}
|
|
await tx.hostHeader.update({
|
|
where: {
|
|
id: host_xid,
|
|
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
|
|
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
|
|
},
|
|
data: {
|
|
hostStatusInternal: HOST_STATUS_INTERNAL.REJECTED,
|
|
hostStatusDisplay: HOST_STATUS_DISPLAY.REJECTED,
|
|
adminStatusInternal: MINGLAR_STATUS_INTERNAL.ADMIN_REJECTED,
|
|
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.REJECTED,
|
|
},
|
|
});
|
|
|
|
await this.prisma.hostTrack.create({
|
|
data: {
|
|
hostXid: hostDetails.id,
|
|
updatedByRole: ROLE_NAME.MINGLAR_ADMIN,
|
|
updatedByXid: user_xid,
|
|
trackStatus: MINGLAR_STATUS_INTERNAL.ADMIN_REJECTED,
|
|
},
|
|
});
|
|
|
|
await tx.user.update({
|
|
where: { id: hostDetails.userXid },
|
|
data: {
|
|
userStatus: USER_STATUS.REJECTED,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
async rejectHostApplicationAM(host_xid: number, user_xid: number) {
|
|
return await this.prisma.$transaction(async (tx) => {
|
|
const hostDetails = await this.prisma.hostHeader.findFirst({
|
|
where: { id: host_xid },
|
|
select: { id: true, userXid: true },
|
|
});
|
|
if (!hostDetails) {
|
|
throw new Error('Host not found');
|
|
}
|
|
await this.prisma.hostHeader.update({
|
|
where: {
|
|
id: host_xid,
|
|
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_SUBMITTED,
|
|
hostStatusDisplay: HOST_STATUS_DISPLAY.UNDER_REVIEW,
|
|
},
|
|
data: {
|
|
hostStatusInternal: HOST_STATUS_INTERNAL.HOST_TO_UPDATE,
|
|
hostStatusDisplay: HOST_STATUS_DISPLAY.ENHANCING,
|
|
adminStatusInternal: MINGLAR_STATUS_INTERNAL.AM_REJECTED,
|
|
adminStatusDisplay: MINGLAR_STATUS_DISPLAY.ENHANCING,
|
|
},
|
|
});
|
|
|
|
await this.prisma.hostTrack.create({
|
|
data: {
|
|
hostXid: hostDetails.id,
|
|
updatedByRole: ROLE_NAME.ACCOUNT_MANAGER,
|
|
updatedByXid: user_xid,
|
|
trackStatus: MINGLAR_STATUS_INTERNAL.AM_REJECTED,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
async getAMdetailById(id: number) {
|
|
return this.prisma.user.findUnique({
|
|
where: { id: id, isActive: true, userStatus: USER_STATUS.ACTIVE },
|
|
include: {
|
|
userAddressDetails: {
|
|
select: {
|
|
id: true,
|
|
userXid: true,
|
|
address1: true,
|
|
address2: true,
|
|
locationAddress: true,
|
|
locationLat: true,
|
|
locationLong: true,
|
|
locationName: true,
|
|
}
|
|
},
|
|
userDocuments: {
|
|
select: {
|
|
id: true,
|
|
fileName: true,
|
|
}
|
|
},
|
|
userRevenues: {
|
|
select: {
|
|
id: true,
|
|
is_fixed_salary: true,
|
|
per_value: true
|
|
}
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async rejectPQQbyAM(activityId: number) {
|
|
return await this.prisma.activities.update({
|
|
where: {
|
|
id: activityId,
|
|
isActive: true
|
|
},
|
|
data: {
|
|
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.PQQ_TO_UPDATE,
|
|
activityDisplayStatus: ACTIVITY_DISPLAY_STATUS.ENHANCING,
|
|
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.PQQ_REJECTED,
|
|
amDisplayStatus: ACTIVITY_AM_DISPLAY_STATUS.ENHANCING
|
|
}
|
|
})
|
|
}
|
|
}
|