parent
6f4b5aa67c
commit
d8a0920ed0
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
|
@ -0,0 +1,12 @@
|
||||||
|
[2026-05-15T05:11:42.509Z] findAll called for user: test-admin, role: ADMIN
|
||||||
|
[2026-05-15T05:11:42.515Z] Fetching as ADMIN
|
||||||
|
[2026-05-15T05:13:33.060Z] findAll called for user: test-admin, role: ADMIN
|
||||||
|
[2026-05-15T05:13:33.066Z] Fetching as ADMIN (take 1)
|
||||||
|
[2026-05-15T05:14:22.476Z] findAll called for user: test-admin, role: ADMIN
|
||||||
|
[2026-05-15T05:14:22.481Z] Fetching as ADMIN (no include)
|
||||||
|
[2026-05-15T05:15:31.552Z] findAll called for user: test-admin, role: ADMIN
|
||||||
|
[2026-05-15T05:15:31.555Z] Testing: Fetching USERS
|
||||||
|
[2026-05-15T05:23:16.418Z] findAll called for user: test-admin, role: ADMIN
|
||||||
|
[2026-05-15T05:23:16.423Z] Testing: Fetching USERS
|
||||||
|
[2026-05-15T05:23:56.486Z] findAll called for user: test-admin, role: ADMIN
|
||||||
|
[2026-05-15T05:23:56.488Z] Testing: Fetching USERS
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Fixing invalid statuses...');
|
||||||
|
try {
|
||||||
|
const count = await prisma.$executeRaw`UPDATE client SET status = 'LEAD' WHERE status = '' OR status IS NULL`;
|
||||||
|
console.log('Fixed client records:', count);
|
||||||
|
|
||||||
|
const countOpp = await prisma.$executeRaw`UPDATE opportunity SET stage = 'LEAD' WHERE stage = '' OR stage IS NULL`;
|
||||||
|
console.log('Fixed opportunity records:', countOpp);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error fixing statuses:', e);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Migrating Opportunity stages...');
|
||||||
|
await prisma.$executeRaw`UPDATE opportunity SET stage = 'WON' WHERE stage = 'SALES'`; // In case we already pushed and it's mixed
|
||||||
|
// Actually we want to go from WON to SALES
|
||||||
|
// But since WON is being removed, we should do it after schema change if we use executeRaw
|
||||||
|
// But wait, if I use executeRaw, I can do it before schema change if SALES is not in enum yet? No.
|
||||||
|
|
||||||
|
// Let's just try to push and see. If it fails, I'll do a more manual approach.
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
@ -204,6 +204,8 @@ model Opportunity {
|
||||||
closingOwnerId String?
|
closingOwnerId String?
|
||||||
demoOwnerId String?
|
demoOwnerId String?
|
||||||
isDemoDone Boolean @default(false)
|
isDemoDone Boolean @default(false)
|
||||||
|
closingProbability Int? @default(0)
|
||||||
|
expectedClosingTimeframe String?
|
||||||
activities Followup[]
|
activities Followup[]
|
||||||
user User @relation("opportunity_assignedToTouser", fields: [assignedTo], references: [id], map: "Opportunity_assignedTo_fkey")
|
user User @relation("opportunity_assignedToTouser", fields: [assignedTo], references: [id], map: "Opportunity_assignedTo_fkey")
|
||||||
client Client @relation(fields: [clientId], references: [id], map: "Opportunity_clientId_fkey")
|
client Client @relation(fields: [clientId], references: [id], map: "Opportunity_clientId_fkey")
|
||||||
|
|
@ -381,6 +383,7 @@ model User {
|
||||||
targets Target[]
|
targets Target[]
|
||||||
manager User? @relation("userTouser", fields: [managerId], references: [id], map: "User_managerId_fkey")
|
manager User? @relation("userTouser", fields: [managerId], references: [id], map: "User_managerId_fkey")
|
||||||
subordinates User[] @relation("userTouser")
|
subordinates User[] @relation("userTouser")
|
||||||
|
permissions String? @db.LongText
|
||||||
|
|
||||||
@@index([managerId], map: "User_managerId_fkey")
|
@@index([managerId], map: "User_managerId_fkey")
|
||||||
@@map("user")
|
@@map("user")
|
||||||
|
|
@ -428,7 +431,7 @@ enum opportunity_stage {
|
||||||
LEAD
|
LEAD
|
||||||
QUALIFIED
|
QUALIFIED
|
||||||
POTENTIAL
|
POTENTIAL
|
||||||
WON
|
SALES
|
||||||
LOST
|
LOST
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -467,7 +470,7 @@ enum expense_status {
|
||||||
|
|
||||||
enum client_status {
|
enum client_status {
|
||||||
LEAD
|
LEAD
|
||||||
QUALITY
|
QUALIFIED
|
||||||
POTENTIAL
|
POTENTIAL
|
||||||
SALES
|
SALES
|
||||||
CLOSED
|
CLOSED
|
||||||
|
|
@ -475,15 +478,23 @@ enum client_status {
|
||||||
|
|
||||||
enum followup_stage {
|
enum followup_stage {
|
||||||
LEAD
|
LEAD
|
||||||
QUALITY
|
QUALIFIED
|
||||||
POTENTIAL
|
POTENTIAL
|
||||||
SALES
|
SALES
|
||||||
CLOSED
|
CLOSED
|
||||||
}
|
}
|
||||||
|
|
||||||
enum activity_type {
|
enum activity_type {
|
||||||
|
CALL
|
||||||
|
MESSAGE
|
||||||
|
DEMO_SCHEDULED
|
||||||
|
DEMO_COMPLETED
|
||||||
|
QUOTE_REQUEST
|
||||||
|
QUOTE_SEND
|
||||||
|
VISIT_SCHEDULED
|
||||||
|
VISIT_COMPLETED
|
||||||
|
NEGOTIATION
|
||||||
FOLLOWUP
|
FOLLOWUP
|
||||||
DEMO
|
DEMO
|
||||||
QUOTE
|
QUOTE
|
||||||
NEGOTIATION
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
function getDefaultPermissions(role: string): string[] {
|
||||||
|
const common = ['dashboard', 'opportunities', 'clients', 'activities', 'products'];
|
||||||
|
switch (role) {
|
||||||
|
case 'ADMIN':
|
||||||
|
case 'GENERAL_MANAGER':
|
||||||
|
return [...common, 'tracking', 'targets', 'incentives', 'reports', 'funnel-analysis', 'users', 'settings', 'expenses', 'call-logs'];
|
||||||
|
case 'MANAGER':
|
||||||
|
return [...common, 'reports', 'incentives', 'targets', 'call-logs'];
|
||||||
|
default:
|
||||||
|
return common;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const users = await prisma.user.findMany();
|
||||||
|
for (const user of users) {
|
||||||
|
if (!user.permissions) {
|
||||||
|
const permissions = getDefaultPermissions(user.role);
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { permissions: JSON.stringify(permissions) }
|
||||||
|
});
|
||||||
|
console.log(`Updated permissions for ${user.email} (${user.role})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => prisma.$disconnect());
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
role: true,
|
||||||
|
status: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify(users, null, 2));
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const password = await bcrypt.hash('admin123', 10);
|
||||||
|
|
||||||
|
const user = await prisma.user.update({
|
||||||
|
where: { email: 'admin@igcrm.com' },
|
||||||
|
data: { password }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Password updated for:', user.email);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const password = await bcrypt.hash('admin123', 10);
|
||||||
|
|
||||||
|
await prisma.user.updateMany({
|
||||||
|
where: {
|
||||||
|
email: { in: ['admin@igcrm.com', 'akhil@gmail.com', 'ramesh@gmail.com'] }
|
||||||
|
},
|
||||||
|
data: { password }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Passwords updated to admin123 for admin, akhil, and ramesh');
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
@ -4,3 +4,4 @@
|
||||||
[2026-05-07T10:48:13.353Z] Manual send triggered for ID: f51c4391-6df5-4dc4-b3ec-ad628db954ed
|
[2026-05-07T10:48:13.353Z] Manual send triggered for ID: f51c4391-6df5-4dc4-b3ec-ad628db954ed
|
||||||
[2026-05-07T10:48:50.250Z] Manual send triggered for ID: f51c4391-6df5-4dc4-b3ec-ad628db954ed
|
[2026-05-07T10:48:50.250Z] Manual send triggered for ID: f51c4391-6df5-4dc4-b3ec-ad628db954ed
|
||||||
[2026-05-09T08:48:57.668Z] Manual send triggered for ID: debedbbf-8473-4b9a-a936-f98a3c030c65
|
[2026-05-09T08:48:57.668Z] Manual send triggered for ID: debedbbf-8473-4b9a-a936-f98a3c030c65
|
||||||
|
[2026-05-15T12:07:28.108Z] Manual send triggered for ID: f51c4391-6df5-4dc4-b3ec-ad628db954ed
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ export class ClientsController {
|
||||||
return this.clientsService.findAll(req.user);
|
return this.clientsService.findAll(req.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('test-500')
|
||||||
|
throwError() {
|
||||||
|
throw new Error('This is a test 500 error to verify the global exception filter');
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
return this.clientsService.findOne(id);
|
return this.clientsService.findOne(id);
|
||||||
|
|
|
||||||
|
|
@ -52,21 +52,42 @@ export class ClientsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(user: any) {
|
async findAll(user: any) {
|
||||||
if (user.role === user_role.ADMIN) {
|
try {
|
||||||
return this.prisma.client.findMany({
|
console.log(`[ClientsService] findAll called for user: ${user.id}, role: ${user.role}`);
|
||||||
include: { user: true, files: true }
|
|
||||||
|
// Use string literal for role check to be safe against enum mismatch
|
||||||
|
if (user.role === 'ADMIN') {
|
||||||
|
return await this.prisma.client.findMany({
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: { id: true, name: true, email: true, role: true }
|
||||||
|
},
|
||||||
|
files: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const subordinateIds = await this.usersService.getSubordinateIds(user.id);
|
||||||
|
const allowedUserIds = [user.id, ...subordinateIds];
|
||||||
|
|
||||||
|
return await this.prisma.client.findMany({
|
||||||
|
where: {
|
||||||
|
assignedTo: { in: allowedUserIds }
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: { id: true, name: true, email: true, role: true }
|
||||||
|
},
|
||||||
|
files: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const fs = require('fs');
|
||||||
|
const errorMsg = `[${new Date().toISOString()}] Error in ClientsService.findAll: ${error.message}\nStack: ${error.stack}\n`;
|
||||||
|
fs.appendFileSync('API_ERROR_FOUND.txt', errorMsg);
|
||||||
|
console.error('Error in ClientsService.findAll:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subordinateIds = await this.usersService.getSubordinateIds(user.id);
|
|
||||||
const allowedUserIds = [user.id, ...subordinateIds];
|
|
||||||
|
|
||||||
return this.prisma.client.findMany({
|
|
||||||
where: {
|
|
||||||
assignedTo: { in: allowedUserIds }
|
|
||||||
},
|
|
||||||
include: { user: true, files: true }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(id: string) {
|
findOne(id: string) {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export class DashboardService {
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
// Total Enquiries
|
// Total Enquiries
|
||||||
this.prisma.enquiry.count({ where: userFilter }),
|
this.prisma.enquiry.count({ where: userFilter }),
|
||||||
|
|
||||||
// Enquiries Today
|
// Enquiries Today
|
||||||
this.prisma.enquiry.count({
|
this.prisma.enquiry.count({
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -57,7 +57,7 @@ export class DashboardService {
|
||||||
this.prisma.opportunity.aggregate({
|
this.prisma.opportunity.aggregate({
|
||||||
where: {
|
where: {
|
||||||
...leadSharingFilter,
|
...leadSharingFilter,
|
||||||
stage: { not: 'WON' }
|
stage: { not: 'SALES' }
|
||||||
},
|
},
|
||||||
_sum: { value: true },
|
_sum: { value: true },
|
||||||
_count: true
|
_count: true
|
||||||
|
|
@ -67,7 +67,7 @@ export class DashboardService {
|
||||||
this.prisma.opportunity.aggregate({
|
this.prisma.opportunity.aggregate({
|
||||||
where: {
|
where: {
|
||||||
...leadSharingFilter,
|
...leadSharingFilter,
|
||||||
stage: 'WON',
|
stage: 'SALES',
|
||||||
updatedAt: { gte: monthStart }
|
updatedAt: { gte: monthStart }
|
||||||
},
|
},
|
||||||
_sum: { value: true }
|
_sum: { value: true }
|
||||||
|
|
@ -147,7 +147,7 @@ export class DashboardService {
|
||||||
const wonOpportunities = await this.prisma.opportunity.findMany({
|
const wonOpportunities = await this.prisma.opportunity.findMany({
|
||||||
where: {
|
where: {
|
||||||
...leadSharingFilter,
|
...leadSharingFilter,
|
||||||
stage: 'WON',
|
stage: 'SALES',
|
||||||
updatedAt: { gte: monthStart }
|
updatedAt: { gte: monthStart }
|
||||||
},
|
},
|
||||||
select: { value: true, assignedTo: true, creatorId: true }
|
select: { value: true, assignedTo: true, creatorId: true }
|
||||||
|
|
@ -163,16 +163,16 @@ export class DashboardService {
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Calculate Conversion Rate
|
// Calculate Conversion Rate
|
||||||
const conversionRate = enquiriesCount > 0
|
const conversionRate = enquiriesCount > 0
|
||||||
? Math.round((clientsCount / enquiriesCount) * 100)
|
? Math.round((clientsCount / enquiriesCount) * 100)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kpis: {
|
kpis: {
|
||||||
enquiriesToday,
|
enquiriesToday,
|
||||||
pipelineValue: opportunityStats._sum.value || 0,
|
pipelineValue: opportunityStats._sum?.value || 0,
|
||||||
pipelineCount: opportunityStats._count || 0,
|
pipelineCount: opportunityStats._count || 0,
|
||||||
monthlyRevenue: monthlyRevenue._sum.value || 0,
|
monthlyRevenue: monthlyRevenue._sum?.value || 0,
|
||||||
contributionRevenue,
|
contributionRevenue,
|
||||||
conversionRate,
|
conversionRate,
|
||||||
pendingExpenses: (user.role === user_role.ADMIN || [user_role.GENERAL_MANAGER, user_role.MANAGER, user_role.OFFICER].includes(user.role)) ? pendingExpenses : 0,
|
pendingExpenses: (user.role === user_role.ADMIN || [user_role.GENERAL_MANAGER, user_role.MANAGER, user_role.OFFICER].includes(user.role)) ? pendingExpenses : 0,
|
||||||
|
|
@ -196,7 +196,7 @@ export class DashboardService {
|
||||||
minimum: target.minTarget,
|
minimum: target.minTarget,
|
||||||
weekly: target.weeklyTarget,
|
weekly: target.weeklyTarget,
|
||||||
dailyLead: target.dailyLeadTarget,
|
dailyLead: target.dailyLeadTarget,
|
||||||
achieved: monthlyRevenue._sum.value || 0
|
achieved: monthlyRevenue._sum?.value || 0
|
||||||
} : null,
|
} : null,
|
||||||
recentActivity: {
|
recentActivity: {
|
||||||
enquiries: recentEnquiries,
|
enquiries: recentEnquiries,
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export class CreateFollowupDto {
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
type?: 'FOLLOWUP' | 'DEMO' | 'QUOTE' | 'NEGOTIATION';
|
type?: 'CALL' | 'MESSAGE' | 'DEMO_SCHEDULED' | 'DEMO_COMPLETED' | 'QUOTE_REQUEST' | 'QUOTE_SEND' | 'VISIT_SCHEDULED' | 'VISIT_COMPLETED' | 'NEGOTIATION' | 'FOLLOWUP' | 'DEMO' | 'QUOTE';
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|
@ -58,4 +58,40 @@ export class CreateFollowupDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
competitorMention?: string;
|
competitorMention?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
customerFeedback?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
requirementDetails?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
suggestions?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
budget?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
expectedClosingTimeline?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
competitorInfo?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
staffRemarks?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
customerCommitments?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
caCsDetails?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,10 @@ export class FollowupsController {
|
||||||
@Query('dateFrom') dateFrom?: string,
|
@Query('dateFrom') dateFrom?: string,
|
||||||
@Query('dateTo') dateTo?: string,
|
@Query('dateTo') dateTo?: string,
|
||||||
@Query('status') status?: string,
|
@Query('status') status?: string,
|
||||||
|
@Query('opportunityId') opportunityId?: string,
|
||||||
|
@Query('enquiryId') enquiryId?: string,
|
||||||
) {
|
) {
|
||||||
return this.followupsService.findAll({ userId, clientId, dateFrom, dateTo, status });
|
return this.followupsService.findAll({ userId, clientId, dateFrom, dateTo, status, opportunityId, enquiryId });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,13 @@ export class FollowupsService {
|
||||||
return followup;
|
return followup;
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll(filters?: { userId?: string; clientId?: string; dateFrom?: string; dateTo?: string; status?: string }) {
|
findAll(filters?: { userId?: string; clientId?: string; dateFrom?: string; dateTo?: string; status?: string; opportunityId?: string; enquiryId?: string }) {
|
||||||
const where: any = {};
|
const where: any = {};
|
||||||
if (filters?.userId) where.userId = filters.userId;
|
if (filters?.userId) where.userId = filters.userId;
|
||||||
if (filters?.clientId) where.clientId = filters.clientId;
|
if (filters?.clientId) where.clientId = filters.clientId;
|
||||||
if (filters?.status) where.status = filters.status;
|
if (filters?.status) where.status = filters.status;
|
||||||
|
if (filters?.opportunityId) where.opportunityId = filters.opportunityId;
|
||||||
|
if (filters?.enquiryId) where.enquiryId = filters.enquiryId;
|
||||||
if (filters?.dateFrom || filters?.dateTo) {
|
if (filters?.dateFrom || filters?.dateTo) {
|
||||||
where.date = {};
|
where.date = {};
|
||||||
if (filters.dateFrom) where.date.gte = new Date(filters.dateFrom);
|
if (filters.dateFrom) where.date.gte = new Date(filters.dateFrom);
|
||||||
|
|
@ -63,7 +65,7 @@ export class FollowupsService {
|
||||||
}
|
}
|
||||||
return this.prisma.followup.findMany({
|
return this.prisma.followup.findMany({
|
||||||
where,
|
where,
|
||||||
include: { client: true, user: true, enquiry: true },
|
include: { client: true, user: true, enquiry: { include: { products: true } }, opportunity: true },
|
||||||
orderBy: { date: 'asc' }
|
orderBy: { date: 'asc' }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +118,7 @@ export class FollowupsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-sync DEMO feedback to Opportunity
|
// Auto-sync DEMO feedback to Opportunity
|
||||||
if (updateFollowupDto.status === 'DONE' && followup.type === 'DEMO' && followup.opportunityId) {
|
if (updateFollowupDto.status === 'DONE' && (followup.type === 'DEMO' || followup.type === 'DEMO_COMPLETED') && followup.opportunityId) {
|
||||||
await this.prisma.opportunity.update({
|
await this.prisma.opportunity.update({
|
||||||
where: { id: followup.opportunityId },
|
where: { id: followup.opportunityId },
|
||||||
data: {
|
data: {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class GlobalExceptionFilter implements ExceptionFilter {
|
||||||
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse();
|
||||||
|
const request = ctx.getRequest();
|
||||||
|
|
||||||
|
const status =
|
||||||
|
exception instanceof HttpException
|
||||||
|
? exception.getStatus()
|
||||||
|
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
|
||||||
|
const errorMsg = exception instanceof Error ? exception.message : 'Unknown error';
|
||||||
|
const stack = exception instanceof Error ? exception.stack : '';
|
||||||
|
|
||||||
|
if (status === HttpStatus.INTERNAL_SERVER_ERROR) {
|
||||||
|
const logContent = `[${new Date().toISOString()}] 500 ERROR on ${request.method} ${request.url}\nError: ${errorMsg}\nStack: ${stack}\n\n`;
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(path.join(process.cwd(), 'GLOBAL_500_ERRORS.txt'), logContent);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to write to GLOBAL_500_ERRORS.txt', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.status(status).json({
|
||||||
|
statusCode: status,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
path: request.url,
|
||||||
|
message: status === HttpStatus.INTERNAL_SERVER_ERROR ? 'Internal server error' : errorMsg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main.ts
13
src/main.ts
|
|
@ -1,11 +1,20 @@
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { GlobalExceptionFilter } from './global-exception.filter';
|
||||||
|
import { RequestLoggerInterceptor } from './request-logger.interceptor';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.enableCors(); // Enable CORS for all origins
|
// app.enableCors(); // Enable CORS for all origins
|
||||||
|
app.enableCors({
|
||||||
|
origin: ['https://crm.ignosimoney.in', 'http://localhost:3001', 'http://localhost:3005'],
|
||||||
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
app.useGlobalPipes(new ValidationPipe({ transform: true }));
|
app.useGlobalPipes(new ValidationPipe({ transform: true }));
|
||||||
await app.listen(process.env.PORT ?? 3000, '0.0.0.0');
|
app.useGlobalFilters(new GlobalExceptionFilter());
|
||||||
|
app.useGlobalInterceptors(new RequestLoggerInterceptor());
|
||||||
|
await app.listen(process.env.PORT ?? 3004, '0.0.0.0');
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|
|
||||||
|
|
@ -73,4 +73,12 @@ export class CreateOpportunityDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
negotiationRemarks?: string;
|
negotiationRemarks?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
closingProbability?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
expectedClosingTimeframe?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,12 @@ export class OpportunitiesService {
|
||||||
assignedTo: createOpportunityDto.assignedTo,
|
assignedTo: createOpportunityDto.assignedTo,
|
||||||
stage: createOpportunityDto.stage || 'LEAD',
|
stage: createOpportunityDto.stage || 'LEAD',
|
||||||
priority: createOpportunityDto.priority,
|
priority: createOpportunityDto.priority,
|
||||||
expectedCloseDate: (createOpportunityDto.expectedCloseDate && createOpportunityDto.expectedCloseDate.trim() !== '')
|
expectedCloseDate: (createOpportunityDto.expectedCloseDate && createOpportunityDto.expectedCloseDate.trim() !== '')
|
||||||
? new Date(createOpportunityDto.expectedCloseDate)
|
? new Date(createOpportunityDto.expectedCloseDate)
|
||||||
: null,
|
: null,
|
||||||
creatorId: createOpportunityDto.creatorId,
|
creatorId: createOpportunityDto.creatorId,
|
||||||
|
closingProbability: createOpportunityDto.closingProbability ? Number(createOpportunityDto.closingProbability) : 0,
|
||||||
|
expectedClosingTimeframe: createOpportunityDto.expectedClosingTimeframe || null,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -85,7 +87,7 @@ export class OpportunitiesService {
|
||||||
if (!current) throw new NotFoundException('Opportunity not found');
|
if (!current) throw new NotFoundException('Opportunity not found');
|
||||||
|
|
||||||
const newStage = updateOpportunityDto.stage || current.stage;
|
const newStage = updateOpportunityDto.stage || current.stage;
|
||||||
|
|
||||||
// Validation Logic for Demo
|
// Validation Logic for Demo
|
||||||
if (updateOpportunityDto.isDemoDone === true && !current.isDemoDone) {
|
if (updateOpportunityDto.isDemoDone === true && !current.isDemoDone) {
|
||||||
const missing: string[] = [];
|
const missing: string[] = [];
|
||||||
|
|
@ -101,7 +103,7 @@ export class OpportunitiesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newStage === 'WON') {
|
if (newStage === 'SALES') {
|
||||||
const missing: string[] = [];
|
const missing: string[] = [];
|
||||||
const check = (val: any) => val === null || val === undefined || (typeof val === 'string' && val.trim() === '');
|
const check = (val: any) => val === null || val === undefined || (typeof val === 'string' && val.trim() === '');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export class PerformanceService implements OnModuleInit {
|
||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
private notifications: NotificationsService
|
private notifications: NotificationsService
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -47,16 +47,16 @@ export class PerformanceService implements OnModuleInit {
|
||||||
this.prisma.target.findFirst({
|
this.prisma.target.findFirst({
|
||||||
where: { userId, month: new Date().getMonth() + 1, year: new Date().getFullYear() },
|
where: { userId, month: new Date().getMonth() + 1, year: new Date().getFullYear() },
|
||||||
}),
|
}),
|
||||||
this.prisma.opportunity.findMany({
|
this.prisma.opportunity.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{ assignedTo: userId },
|
{ assignedTo: userId },
|
||||||
{ creatorId: userId },
|
{ creatorId: userId },
|
||||||
{ demoOwnerId: userId },
|
{ demoOwnerId: userId },
|
||||||
{ closingOwnerId: userId }
|
{ closingOwnerId: userId }
|
||||||
],
|
],
|
||||||
updatedAt: { gte: start, lte: end }
|
updatedAt: { gte: start, lte: end }
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
this.prisma.enquiry.findMany({ where: { userId, createdAt: { gte: start, lte: end } } }),
|
this.prisma.enquiry.findMany({ where: { userId, createdAt: { gte: start, lte: end } } }),
|
||||||
this.prisma.followup.findMany({ where: { userId, date: { gte: start, lte: end } } }),
|
this.prisma.followup.findMany({ where: { userId, date: { gte: start, lte: end } } }),
|
||||||
|
|
@ -67,24 +67,24 @@ export class PerformanceService implements OnModuleInit {
|
||||||
|
|
||||||
// 2. Revenue Achievement (Weight: 40) - LEAD SHARING LOGIC (50% Creator / 50% Closer)
|
// 2. Revenue Achievement (Weight: 40) - LEAD SHARING LOGIC (50% Creator / 50% Closer)
|
||||||
const totalRevenue = opportunities.reduce((sum, o) => {
|
const totalRevenue = opportunities.reduce((sum, o) => {
|
||||||
if (o.stage !== 'WON') return sum;
|
if (o.stage !== 'SALES') return sum;
|
||||||
|
|
||||||
let contribution = 0;
|
let contribution = 0;
|
||||||
// 50% goes to the Lead Creator
|
// 50% goes to the Lead Creator
|
||||||
if (o.creatorId === userId) contribution += o.value * 0.5;
|
if (o.creatorId === userId) contribution += o.value * 0.5;
|
||||||
|
|
||||||
// 50% goes to the Closer (closingOwnerId or assignedTo if not specified)
|
// 50% goes to the Closer (closingOwnerId or assignedTo if not specified)
|
||||||
if (o.closingOwnerId === userId || (!o.closingOwnerId && o.assignedTo === userId)) {
|
if (o.closingOwnerId === userId || (!o.closingOwnerId && o.assignedTo === userId)) {
|
||||||
contribution += o.value * 0.5;
|
contribution += o.value * 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sum + contribution;
|
return sum + contribution;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const revenueScore = Math.min(40, (totalRevenue / target.monthlyTarget) * 40);
|
const revenueScore = Math.min(40, (totalRevenue / target.monthlyTarget) * 40);
|
||||||
|
|
||||||
// 3. Conversion Efficiency (Weight: 20)
|
// 3. Conversion Efficiency (Weight: 20)
|
||||||
const wonCount = opportunities.filter(o => o.stage === 'WON' && (o.closingOwnerId === userId || o.assignedTo === userId)).length;
|
const wonCount = opportunities.filter(o => o.stage === 'SALES' && (o.closingOwnerId === userId || o.assignedTo === userId)).length;
|
||||||
const conversionEfficiency = enquiries.length > 0 ? (wonCount / enquiries.length) : 0;
|
const conversionEfficiency = enquiries.length > 0 ? (wonCount / enquiries.length) : 0;
|
||||||
const conversionScore = Math.min(20, conversionEfficiency * 100 * 0.2); // Scaling 100% to 20 points
|
const conversionScore = Math.min(20, conversionEfficiency * 100 * 0.2); // Scaling 100% to 20 points
|
||||||
|
|
||||||
|
|
@ -154,16 +154,16 @@ export class PerformanceService implements OnModuleInit {
|
||||||
const end = endOfMonth(new Date());
|
const end = endOfMonth(new Date());
|
||||||
|
|
||||||
const [activities, opportunities] = await Promise.all([
|
const [activities, opportunities] = await Promise.all([
|
||||||
this.prisma.strategicActivity.findMany({ where: { userId, date: { gte: start, lte: end } } }),
|
this.prisma.strategicActivity.findMany({ where: { userId, date: { gte: start, lte: end } } }),
|
||||||
this.prisma.opportunity.findMany({ where: { assignedTo: userId, updatedAt: { gte: start, lte: end } } }),
|
this.prisma.opportunity.findMany({ where: { assignedTo: userId, updatedAt: { gte: start, lte: end } } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Actual Counts
|
// Actual Counts
|
||||||
const actualCalls = activities.length;
|
const actualCalls = activities.length;
|
||||||
const actualQuality = opportunities.filter(o => ['QUALIFIED', 'POTENTIAL', 'WON'].includes(o.stage) || o.isDemoDone).length;
|
const actualQuality = opportunities.filter(o => ['QUALIFIED', 'POTENTIAL', 'SALES'].includes(o.stage) || o.isDemoDone).length;
|
||||||
const actualPotential = opportunities.filter(o => ['POTENTIAL', 'WON'].includes(o.stage) || o.isDemoDone).length;
|
const actualPotential = opportunities.filter(o => ['POTENTIAL', 'SALES'].includes(o.stage) || o.isDemoDone).length;
|
||||||
const actualDemo = opportunities.filter(o => o.isDemoDone || o.stage === 'WON').length;
|
const actualDemo = opportunities.filter(o => o.isDemoDone || o.stage === 'SALES').length;
|
||||||
const actualWon = opportunities.filter(o => o.stage === 'WON').length;
|
const actualWon = opportunities.filter(o => o.stage === 'SALES').length;
|
||||||
|
|
||||||
// Default ratios (configurable)
|
// Default ratios (configurable)
|
||||||
const DEFAULT_RATIOS = { callsToQuality: 5, qualityToPotential: 3.33, potentialToDemo: 1.5, demoToWon: 2 };
|
const DEFAULT_RATIOS = { callsToQuality: 5, qualityToPotential: 3.33, potentialToDemo: 1.5, demoToWon: 2 };
|
||||||
|
|
@ -172,7 +172,7 @@ export class PerformanceService implements OnModuleInit {
|
||||||
try {
|
try {
|
||||||
const cfg: any = await this.prisma.$queryRaw`SELECT value FROM systemconfig WHERE key = 'funnel_ratios' LIMIT 1`;
|
const cfg: any = await this.prisma.$queryRaw`SELECT value FROM systemconfig WHERE key = 'funnel_ratios' LIMIT 1`;
|
||||||
if (cfg && cfg[0]) ratios = JSON.parse(cfg[0].value);
|
if (cfg && cfg[0]) ratios = JSON.parse(cfg[0].value);
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
// Ideal Funnel (derived from actual calls and configured ratios)
|
// Ideal Funnel (derived from actual calls and configured ratios)
|
||||||
const idealQuality = actualCalls / ratios.callsToQuality;
|
const idealQuality = actualCalls / ratios.callsToQuality;
|
||||||
|
|
@ -181,10 +181,10 @@ export class PerformanceService implements OnModuleInit {
|
||||||
const idealWon = idealDemo / ratios.demoToWon;
|
const idealWon = idealDemo / ratios.demoToWon;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actual: { calls: actualCalls, quality: actualQuality, potential: actualPotential, demo: actualDemo, won: actualWon },
|
actual: { calls: actualCalls, quality: actualQuality, potential: actualPotential, demo: actualDemo, won: actualWon },
|
||||||
ideal: { calls: actualCalls, quality: Math.round(idealQuality), potential: Math.round(idealPotential), demo: Math.round(idealDemo), won: Math.round(idealWon) },
|
ideal: { calls: actualCalls, quality: Math.round(idealQuality), potential: Math.round(idealPotential), demo: Math.round(idealDemo), won: Math.round(idealWon) },
|
||||||
deviation: { quality: actualQuality - idealQuality, potential: actualPotential - idealPotential, demo: actualDemo - idealDemo, won: actualWon - idealWon },
|
deviation: { quality: actualQuality - idealQuality, potential: actualPotential - idealPotential, demo: actualDemo - idealDemo, won: actualWon - idealWon },
|
||||||
ratios
|
ratios
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,7 +206,7 @@ export class PerformanceService implements OnModuleInit {
|
||||||
const id = require('crypto').randomUUID();
|
const id = require('crypto').randomUUID();
|
||||||
await this.prisma.$executeRaw`INSERT INTO systemconfig (id, key, value, updatedAt) VALUES (${id}, 'funnel_ratios', ${value}, NOW())`;
|
await this.prisma.$executeRaw`INSERT INTO systemconfig (id, key, value, updatedAt) VALUES (${id}, 'funnel_ratios', ${value}, NOW())`;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
return ratios;
|
return ratios;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,22 +244,22 @@ export class PerformanceService implements OnModuleInit {
|
||||||
const latest = scores[0];
|
const latest = scores[0];
|
||||||
const suggestions: string[] = [];
|
const suggestions: string[] = [];
|
||||||
if (latest.score < 80) {
|
if (latest.score < 80) {
|
||||||
if (latest.revenueScore < 20) suggestions.push('Focus on high-value closures to meet revenue targets.');
|
if (latest.revenueScore < 20) suggestions.push('Focus on high-value closures to meet revenue targets.');
|
||||||
if (latest.conversionScore < 10) suggestions.push('Improve conversion rate by qualifying leads better before the Demo stage.');
|
if (latest.conversionScore < 10) suggestions.push('Improve conversion rate by qualifying leads better before the Demo stage.');
|
||||||
if (latest.activityScore < 7) suggestions.push('Increase daily call and meeting activity to fill your pipeline.');
|
if (latest.activityScore < 7) suggestions.push('Increase daily call and meeting activity to fill your pipeline.');
|
||||||
if (latest.disciplineScore < 7) suggestions.push('Ensure all follow-ups are marked as complete on time.');
|
if (latest.disciplineScore < 7) suggestions.push('Ensure all follow-ups are marked as complete on time.');
|
||||||
if (latest.dataQualityScore < 5) suggestions.push('Fill in all mandatory fields during the Demo stage to improve data quality.');
|
if (latest.dataQualityScore < 5) suggestions.push('Fill in all mandatory fields during the Demo stage to improve data quality.');
|
||||||
} else {
|
} else {
|
||||||
suggestions.push('Keep up the great work! You are exceeding your targets.');
|
suggestions.push('Keep up the great work! You are exceeding your targets.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status !== 'NORMAL') {
|
if (status !== 'NORMAL') {
|
||||||
await this.notifications.create(
|
await this.notifications.create(
|
||||||
userId,
|
userId,
|
||||||
`PERFORMANCE ${status}`,
|
`PERFORMANCE ${status}`,
|
||||||
`Your performance has been below minimum for ${status === 'ACTION' ? '3' : '2'} months. Please check your improvement plan.`,
|
`Your performance has been below minimum for ${status === 'ACTION' ? '3' : '2'} months. Please check your improvement plan.`,
|
||||||
'PERFORMANCE_ALERT'
|
'PERFORMANCE_ALERT'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,10 @@ export class ReportsService {
|
||||||
|
|
||||||
async getSummaryData(filters: any, user: any) {
|
async getSummaryData(filters: any, user: any) {
|
||||||
const { startDate, endDate, targetUserId } = filters;
|
const { startDate, endDate, targetUserId } = filters;
|
||||||
|
|
||||||
// 1. Determine Scope
|
// 1. Determine Scope
|
||||||
let allowedUserIds: string[] = [user.id];
|
let allowedUserIds: string[] = [user.id];
|
||||||
|
|
||||||
if (user.role === user_role.ADMIN) {
|
if (user.role === user_role.ADMIN) {
|
||||||
// Admin can see anyone or target a specific user
|
// Admin can see anyone or target a specific user
|
||||||
if (targetUserId) {
|
if (targetUserId) {
|
||||||
|
|
@ -27,7 +27,7 @@ export class ReportsService {
|
||||||
} else if ([user_role.GENERAL_MANAGER, user_role.MANAGER, user_role.OFFICER].includes(user.role)) {
|
} else if ([user_role.GENERAL_MANAGER, user_role.MANAGER, user_role.OFFICER].includes(user.role)) {
|
||||||
const subordinates = await this.usersService.getSubordinateIds(user.id);
|
const subordinates = await this.usersService.getSubordinateIds(user.id);
|
||||||
const managedIds = [user.id, ...subordinates];
|
const managedIds = [user.id, ...subordinates];
|
||||||
|
|
||||||
if (targetUserId) {
|
if (targetUserId) {
|
||||||
// Check if target is a subordinate
|
// Check if target is a subordinate
|
||||||
if (managedIds.includes(targetUserId)) {
|
if (managedIds.includes(targetUserId)) {
|
||||||
|
|
@ -49,13 +49,13 @@ export class ReportsService {
|
||||||
};
|
};
|
||||||
|
|
||||||
const userFilter = allowedUserIds.length > 0 ? { userId: { in: allowedUserIds } } : {};
|
const userFilter = allowedUserIds.length > 0 ? { userId: { in: allowedUserIds } } : {};
|
||||||
const leadSharingFilter = allowedUserIds.length > 0
|
const leadSharingFilter = allowedUserIds.length > 0
|
||||||
? { OR: [{ assignedTo: { in: allowedUserIds } }, { creatorId: { in: allowedUserIds } }] }
|
? { OR: [{ assignedTo: { in: allowedUserIds } }, { creatorId: { in: allowedUserIds } }] }
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
// 2. Fetch Aggregations
|
// 2. Fetch Aggregations
|
||||||
const clientFilter = allowedUserIds.length > 0
|
const clientFilter = allowedUserIds.length > 0
|
||||||
? { assignedTo: { in: allowedUserIds } }
|
? { assignedTo: { in: allowedUserIds } }
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
const [
|
const [
|
||||||
|
|
@ -69,7 +69,7 @@ export class ReportsService {
|
||||||
where: { ...userFilter, createdAt: dateRange },
|
where: { ...userFilter, createdAt: dateRange },
|
||||||
select: { id: true, status: true, createdAt: true }
|
select: { id: true, status: true, createdAt: true }
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Opportunity breakdown
|
// Opportunity breakdown
|
||||||
this.prisma.opportunity.findMany({
|
this.prisma.opportunity.findMany({
|
||||||
where: { ...leadSharingFilter, updatedAt: dateRange },
|
where: { ...leadSharingFilter, updatedAt: dateRange },
|
||||||
|
|
@ -95,7 +95,7 @@ export class ReportsService {
|
||||||
// 3. Process Trends (Last 6 months)
|
// 3. Process Trends (Last 6 months)
|
||||||
const sixMonthsAgo = startOfMonth(new Date());
|
const sixMonthsAgo = startOfMonth(new Date());
|
||||||
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 5);
|
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 5);
|
||||||
|
|
||||||
const months = eachMonthOfInterval({
|
const months = eachMonthOfInterval({
|
||||||
start: sixMonthsAgo,
|
start: sixMonthsAgo,
|
||||||
end: new Date(),
|
end: new Date(),
|
||||||
|
|
@ -104,11 +104,11 @@ export class ReportsService {
|
||||||
const salesTrend = await Promise.all(months.map(async (date) => {
|
const salesTrend = await Promise.all(months.map(async (date) => {
|
||||||
const monthStart = startOfMonth(date);
|
const monthStart = startOfMonth(date);
|
||||||
const monthEnd = endOfMonth(date);
|
const monthEnd = endOfMonth(date);
|
||||||
|
|
||||||
const monthSales = await this.prisma.opportunity.aggregate({
|
const monthSales = await this.prisma.opportunity.aggregate({
|
||||||
where: {
|
where: {
|
||||||
...leadSharingFilter,
|
...leadSharingFilter,
|
||||||
stage: 'WON',
|
stage: 'SALES',
|
||||||
updatedAt: { gte: monthStart, lte: monthEnd }
|
updatedAt: { gte: monthStart, lte: monthEnd }
|
||||||
},
|
},
|
||||||
_sum: { value: true }
|
_sum: { value: true }
|
||||||
|
|
@ -116,21 +116,21 @@ export class ReportsService {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
month: format(date, 'MMM yy'),
|
month: format(date, 'MMM yy'),
|
||||||
revenue: monthSales._sum.value || 0
|
revenue: monthSales._sum?.value || 0
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 4. Team Performance (Only for Admin/Manager)
|
// 4. Team Performance (Only for Admin/Manager)
|
||||||
let teamPerformance: any[] = [];
|
let teamPerformance: any[] = [];
|
||||||
if (user.role !== user_role.TELESALES_EXECUTIVE) {
|
if (user.role !== user_role.TELESALES_EXECUTIVE) {
|
||||||
const usersToTrack = allowedUserIds.length > 0
|
const usersToTrack = allowedUserIds.length > 0
|
||||||
? await this.prisma.user.findMany({ where: { id: { in: allowedUserIds } } })
|
? await this.prisma.user.findMany({ where: { id: { in: allowedUserIds } } })
|
||||||
: await this.prisma.user.findMany({ take: 10 });
|
: await this.prisma.user.findMany({ take: 10 });
|
||||||
|
|
||||||
teamPerformance = await Promise.all(usersToTrack.map(async (u) => {
|
teamPerformance = await Promise.all(usersToTrack.map(async (u) => {
|
||||||
const [userSales, userEnq] = await Promise.all([
|
const [userSales, userEnq] = await Promise.all([
|
||||||
this.prisma.opportunity.aggregate({
|
this.prisma.opportunity.aggregate({
|
||||||
where: { assignedTo: u.id, stage: 'WON', updatedAt: dateRange },
|
where: { assignedTo: u.id, stage: 'SALES', updatedAt: dateRange },
|
||||||
_sum: { value: true }
|
_sum: { value: true }
|
||||||
}),
|
}),
|
||||||
this.prisma.enquiry.count({
|
this.prisma.enquiry.count({
|
||||||
|
|
@ -140,7 +140,7 @@ export class ReportsService {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: u.name,
|
name: u.name,
|
||||||
revenue: userSales._sum.value || 0,
|
revenue: userSales._sum?.value || 0,
|
||||||
enquiries: userEnq
|
enquiries: userEnq
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
@ -148,7 +148,7 @@ export class ReportsService {
|
||||||
|
|
||||||
// 3. Process Summary with Revenue Split Logic
|
// 3. Process Summary with Revenue Split Logic
|
||||||
const contributionRevenue = opportunities
|
const contributionRevenue = opportunities
|
||||||
.filter(o => o.stage === 'WON')
|
.filter(o => o.stage === 'SALES')
|
||||||
.reduce((total, o) => {
|
.reduce((total, o) => {
|
||||||
let share = 0;
|
let share = 0;
|
||||||
const isCloser = allowedUserIds.includes(o.assignedTo);
|
const isCloser = allowedUserIds.includes(o.assignedTo);
|
||||||
|
|
@ -156,16 +156,16 @@ export class ReportsService {
|
||||||
|
|
||||||
if (isCloser && isCreator) share = o.value; // Owns both
|
if (isCloser && isCreator) share = o.value; // Owns both
|
||||||
else if (isCloser || isCreator) share = o.value * 0.5; // Shared
|
else if (isCloser || isCreator) share = o.value * 0.5; // Shared
|
||||||
|
|
||||||
return total + share;
|
return total + share;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
summary: {
|
summary: {
|
||||||
totalEnquiries: enquiries.length,
|
totalEnquiries: enquiries.length,
|
||||||
totalRevenue: opportunities.filter(o => o.stage === 'WON').reduce((s, o) => s + o.value, 0),
|
totalRevenue: opportunities.filter(o => o.stage === 'SALES').reduce((s, o) => s + o.value, 0),
|
||||||
contributionRevenue,
|
contributionRevenue,
|
||||||
openPipeline: opportunities.filter(o => o.stage !== 'WON').reduce((s, o) => s + o.value, 0),
|
openPipeline: opportunities.filter(o => o.stage !== 'SALES').reduce((s, o) => s + o.value, 0),
|
||||||
totalExpenses: expenses.reduce((s, e) => s + e.amount, 0),
|
totalExpenses: expenses.reduce((s, e) => s + e.amount, 0),
|
||||||
conversionCount: conversions
|
conversionCount: conversions
|
||||||
},
|
},
|
||||||
|
|
@ -178,7 +178,7 @@ export class ReportsService {
|
||||||
{ name: 'Qualified', count: opportunities.filter(o => o.stage === 'QUALIFIED').length },
|
{ name: 'Qualified', count: opportunities.filter(o => o.stage === 'QUALIFIED').length },
|
||||||
{ name: 'Potential', count: opportunities.filter(o => o.stage === 'POTENTIAL').length },
|
{ name: 'Potential', count: opportunities.filter(o => o.stage === 'POTENTIAL').length },
|
||||||
{ name: 'Demo Done', count: opportunities.filter(o => o.isDemoDone).length },
|
{ name: 'Demo Done', count: opportunities.filter(o => o.isDemoDone).length },
|
||||||
{ name: 'Won', count: opportunities.filter(o => o.stage === 'WON').length },
|
{ name: 'SALES', count: opportunities.filter(o => o.stage === 'SALES').length },
|
||||||
],
|
],
|
||||||
expenses: [
|
expenses: [
|
||||||
{ name: 'Approved', amount: expenses.filter(e => e.status === 'APPROVED').reduce((s, e) => s + e.amount, 0) },
|
{ name: 'Approved', amount: expenses.filter(e => e.status === 'APPROVED').reduce((s, e) => s + e.amount, 0) },
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpException } from '@nestjs/common';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { tap, catchError } from 'rxjs/operators';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RequestLoggerInterceptor implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const req = context.switchToHttp().getRequest();
|
||||||
|
const method = req.method;
|
||||||
|
const url = req.url;
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
const logEntry = `[${timestamp}] Incoming Request: ${method} ${url}\nHeaders: ${JSON.stringify(req.headers)}\nQuery: ${JSON.stringify(req.query)}\nBody: ${JSON.stringify(req.body)}\nUser: ${JSON.stringify(req.user)}\n\n`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(path.join(process.cwd(), 'API_REQUEST_LOG.txt'), logEntry);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return next.handle().pipe(
|
||||||
|
tap(() => {
|
||||||
|
const res = context.switchToHttp().getResponse();
|
||||||
|
const successLog = `[${new Date().toISOString()}] Response: ${method} ${url} -> ${res.statusCode}\n\n`;
|
||||||
|
try { fs.appendFileSync(path.join(process.cwd(), 'API_REQUEST_LOG.txt'), successLog); } catch (e) {}
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
const status = error instanceof HttpException ? error.getStatus() : 500;
|
||||||
|
const errorLog = `[${new Date().toISOString()}] Error Response: ${method} ${url} -> ${status}\nMessage: ${error.message}\nStack: ${error.stack}\n\n`;
|
||||||
|
try { fs.appendFileSync(path.join(process.cwd(), 'API_REQUEST_LOG.txt'), errorLog); } catch (e) {}
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,9 +14,9 @@ export class StrategicActivitiesService {
|
||||||
async create(userId: string, data: any) {
|
async create(userId: string, data: any) {
|
||||||
let metadataObj = data.metadata || {};
|
let metadataObj = data.metadata || {};
|
||||||
if (typeof metadataObj === 'string') {
|
if (typeof metadataObj === 'string') {
|
||||||
try { metadataObj = JSON.parse(metadataObj); } catch(e) {}
|
try { metadataObj = JSON.parse(metadataObj); } catch (e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.updateClientStatus && metadataObj.clientId) {
|
if (data.updateClientStatus && metadataObj.clientId) {
|
||||||
// Update the client status
|
// Update the client status
|
||||||
const updatedClient = await this.prisma.client.update({
|
const updatedClient = await this.prisma.client.update({
|
||||||
|
|
@ -38,7 +38,7 @@ export class StrategicActivitiesService {
|
||||||
if (targetStage) {
|
if (targetStage) {
|
||||||
// Check if an opportunity already exists for this client to avoid duplicates
|
// Check if an opportunity already exists for this client to avoid duplicates
|
||||||
const existingOpp = await this.prisma.opportunity.findFirst({
|
const existingOpp = await this.prisma.opportunity.findFirst({
|
||||||
where: {
|
where: {
|
||||||
clientId: metadataObj.clientId,
|
clientId: metadataObj.clientId,
|
||||||
stage: { notIn: ['WON', 'LOST'] as any }
|
stage: { notIn: ['WON', 'LOST'] as any }
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +48,7 @@ export class StrategicActivitiesService {
|
||||||
// Update existing opportunity stage
|
// Update existing opportunity stage
|
||||||
await this.prisma.opportunity.update({
|
await this.prisma.opportunity.update({
|
||||||
where: { id: existingOpp.id },
|
where: { id: existingOpp.id },
|
||||||
data: {
|
data: {
|
||||||
stage: targetStage as any,
|
stage: targetStage as any,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +71,7 @@ export class StrategicActivitiesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringifiedMetadata = JSON.stringify(metadataObj);
|
const stringifiedMetadata = JSON.stringify(metadataObj);
|
||||||
|
|
||||||
return this.prisma.strategicActivity.create({
|
return this.prisma.strategicActivity.create({
|
||||||
data: {
|
data: {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,8 @@ export class CreateUserDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
managerId?: string;
|
managerId?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
permissions?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,33 @@ export class UsersService {
|
||||||
const salt = await bcrypt.genSalt();
|
const salt = await bcrypt.genSalt();
|
||||||
const hashedPassword = await bcrypt.hash(createUserDto.password, salt);
|
const hashedPassword = await bcrypt.hash(createUserDto.password, salt);
|
||||||
|
|
||||||
|
const defaultPermissions = this.getDefaultPermissions(createUserDto.role || 'TELESALES_EXECUTIVE');
|
||||||
|
|
||||||
return this.prisma.user.create({
|
return this.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
...createUserDto,
|
...createUserDto,
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
|
permissions: JSON.stringify(defaultPermissions),
|
||||||
status: (creatorRole === 'ADMIN' || creatorRole === 'GENERAL_MANAGER') ? 'APPROVED' : 'PENDING',
|
status: (creatorRole === 'ADMIN' || creatorRole === 'GENERAL_MANAGER') ? 'APPROVED' : 'PENDING',
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDefaultPermissions(role: string): string[] {
|
||||||
|
const common = ['dashboard', 'opportunities', 'clients', 'activities', 'products'];
|
||||||
|
switch (role) {
|
||||||
|
case 'ADMIN':
|
||||||
|
case 'GENERAL_MANAGER':
|
||||||
|
return [...common, 'tracking', 'targets', 'incentives', 'reports', 'funnel-analysis', 'users', 'settings', 'expenses', 'call-logs'];
|
||||||
|
case 'MANAGER':
|
||||||
|
return [...common, 'reports', 'incentives', 'targets', 'call-logs'];
|
||||||
|
default:
|
||||||
|
return common;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async findAll() {
|
async findAll() {
|
||||||
return this.prisma.user.findMany();
|
return this.prisma.user.findMany();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
async function test() {
|
||||||
|
const url = process.env.DATABASE_URL.replace('mysql://', '');
|
||||||
|
const [auth, hostPortDb] = url.split('@');
|
||||||
|
const [user, password] = auth.split(':');
|
||||||
|
const [hostPort, database] = hostPortDb.split('/');
|
||||||
|
const [host, port] = hostPort.split(':');
|
||||||
|
|
||||||
|
console.log(`Connecting to ${host}:${port || 3306} as ${user}...`);
|
||||||
|
try {
|
||||||
|
const connection = await mysql.createConnection({
|
||||||
|
host: host,
|
||||||
|
port: port || 3306,
|
||||||
|
user: user,
|
||||||
|
password: password || '',
|
||||||
|
database: database.split('?')[0]
|
||||||
|
});
|
||||||
|
console.log('Connected!');
|
||||||
|
const [rows] = await connection.execute('SELECT COUNT(*) as count FROM client');
|
||||||
|
console.log('Client count:', rows[0].count);
|
||||||
|
await connection.end();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Connection failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 520 KiB |
Loading…
Reference in New Issue