update till 22/05/2026

update till 22/05/2026
main
Manu Krishna 2026-05-22 14:46:34 +05:30
parent 6f4b5aa67c
commit d8a0920ed0
30 changed files with 14777 additions and 98 deletions

14329
API_REQUEST_LOG.txt Normal file

File diff suppressed because it is too large Load Diff

BIN
api_test_logs.txt Normal file

Binary file not shown.

12
clients_debug.txt Normal file
View File

@ -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

19
fix_invalid_data.js Normal file
View File

@ -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();

14
migrate_stages.js Normal file
View File

@ -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();

View File

@ -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
} }

BIN
prisma/schema_dump.prisma Normal file

Binary file not shown.

View File

@ -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());

18
scratch/check_users.ts Normal file
View File

@ -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();

17
scratch/reset_admin.ts Normal file
View File

@ -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();

19
scratch/reset_multiple.ts Normal file
View File

@ -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();

View File

@ -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

View File

@ -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);

View File

@ -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 subordinateIds = await this.usersService.getSubordinateIds(user.id);
const allowedUserIds = [user.id, ...subordinateIds]; const allowedUserIds = [user.id, ...subordinateIds];
return this.prisma.client.findMany({ return await this.prisma.client.findMany({
where: { where: {
assignedTo: { in: allowedUserIds } assignedTo: { in: allowedUserIds }
}, },
include: { user: true, files: true } 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;
}
} }
findOne(id: string) { findOne(id: string) {

View File

@ -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 }
@ -170,9 +170,9 @@ export class DashboardService {
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,

View File

@ -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;
} }

View File

@ -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')

View File

@ -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: {

View File

@ -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,
});
}
}

View File

@ -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();

View File

@ -73,4 +73,12 @@ export class CreateOpportunityDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
negotiationRemarks?: string; negotiationRemarks?: string;
@IsOptional()
@IsNumber()
closingProbability?: number;
@IsOptional()
@IsString()
expectedClosingTimeframe?: string;
} }

View File

@ -30,6 +30,8 @@ export class OpportunitiesService {
? 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(),
}, },
}); });
@ -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() === '');

View File

@ -67,7 +67,7 @@ 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
@ -84,7 +84,7 @@ export class PerformanceService implements OnModuleInit {
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
@ -160,10 +160,10 @@ export class PerformanceService implements OnModuleInit {
// 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 };

View File

@ -108,7 +108,7 @@ export class ReportsService {
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,7 +116,7 @@ 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
}; };
})); }));
@ -130,7 +130,7 @@ export class ReportsService {
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);
@ -163,9 +163,9 @@ export class ReportsService {
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) },

View File

@ -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;
})
);
}
}

View File

@ -21,4 +21,8 @@ export class CreateUserDto {
@IsString() @IsString()
@IsOptional() @IsOptional()
managerId?: string; managerId?: string;
@IsString()
@IsOptional()
permissions?: string;
} }

View File

@ -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();
} }

29
test_db_direct.js Normal file
View File

@ -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