task-d5.5.1
This commit is contained in:
@@ -107,7 +107,7 @@ export async function GET(request: NextRequest) {
|
||||
...data.salesRanking.map((row) => [
|
||||
row.salesPersonName,
|
||||
String(row.leadCount),
|
||||
String(row.enquiryCount),
|
||||
String(row.opportunityCount),
|
||||
String(row.quotationCount),
|
||||
String(row.approvedQuotations),
|
||||
String(row.wonRevenue),
|
||||
@@ -148,3 +148,4 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ message: 'Unable to export CRM dashboard data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
success: true,
|
||||
message: 'Lead assigned successfully',
|
||||
leadId: result.leadId,
|
||||
enquiryId: result.enquiryId,
|
||||
enquiryCode: result.enquiryCode,
|
||||
reusedExistingEnquiry: result.reusedExistingEnquiry
|
||||
opportunityId: result.opportunityId,
|
||||
opportunityCode: result.opportunityCode,
|
||||
reusedExistingOpportunity: result.reusedExistingOpportunity
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
@@ -61,3 +61,4 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: 'Unable to assign lead' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const enquiryAssignmentRequestSchema = z.object({
|
||||
export const opportunityAssignmentRequestSchema = z.object({
|
||||
assignedToUserId: z.string().min(1, 'Sales user is required'),
|
||||
remark: z.string().trim().max(1000).optional()
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditAction } from '@/features/foundation/audit-log/service';
|
||||
import { assignEnquiryToUser, getEnquiryDetail } from '@/features/crm/enquiries/server/service';
|
||||
import { assignOpportunityToUser, getOpportunityDetail } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
import { enquiryAssignmentRequestSchema } from '../_schemas';
|
||||
import { opportunityAssignmentRequestSchema } from '../_schemas';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -14,9 +14,9 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryAssign
|
||||
permission: PERMISSIONS.crmOpportunityAssign
|
||||
});
|
||||
const payload = enquiryAssignmentRequestSchema.parse(await request.json());
|
||||
const payload = opportunityAssignmentRequestSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
@@ -29,8 +29,8 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const before = await getEnquiryDetail(id, organization.id, accessContext);
|
||||
const updated = await assignEnquiryToUser(
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await assignOpportunityToUser(
|
||||
id,
|
||||
organization.id,
|
||||
session.user.id,
|
||||
@@ -42,9 +42,9 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
organizationId: organization.id,
|
||||
branchId: updated.branchId,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_enquiry',
|
||||
entityType: 'crm_opportunity',
|
||||
entityId: id,
|
||||
action: 'lead_to_enquiry',
|
||||
action: 'lead_to_opportunity',
|
||||
beforeData: {
|
||||
oldAssignedToUserId: before.assignedToUserId,
|
||||
oldPipelineStage: before.pipelineStage
|
||||
@@ -58,8 +58,8 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Lead converted to enquiry successfully',
|
||||
enquiry: updated
|
||||
message: 'Lead converted to opportunity successfully',
|
||||
opportunity: updated
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
@@ -77,6 +77,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to assign enquiry' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to assign opportunity' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { listEnquiryProjectParties } from '@/features/crm/enquiries/server/service';
|
||||
import { listOpportunityProjectParties } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryRead
|
||||
permission: PERMISSIONS.crmOpportunityRead
|
||||
});
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
@@ -24,12 +24,12 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const items = await listEnquiryProjectParties(id, organization.id, accessContext);
|
||||
const items = await listOpportunityProjectParties(id, organization.id, accessContext);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Enquiry project parties loaded successfully',
|
||||
message: 'Opportunity project parties loaded successfully',
|
||||
items
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -42,8 +42,9 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: 'Unable to load enquiry project parties' },
|
||||
{ message: 'Unable to load opportunity project parties' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
listEnquiryFollowups,
|
||||
softDeleteEnquiryFollowup,
|
||||
updateEnquiryFollowup
|
||||
} from '@/features/crm/enquiries/server/service';
|
||||
import { enquiryFollowupSchema } from '@/features/crm/enquiries/schemas/enquiry.schema';
|
||||
listOpportunityFollowups,
|
||||
softDeleteOpportunityFollowup,
|
||||
updateOpportunityFollowup
|
||||
} from '@/features/crm/opportunities/server/service';
|
||||
import { opportunityFollowupSchema } from '@/features/crm/opportunities/schemas/opportunity.schema';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -14,7 +14,7 @@ type Params = {
|
||||
params: Promise<{ id: string; followupId: string }>;
|
||||
};
|
||||
|
||||
const followupRequestSchema = enquiryFollowupSchema.extend({
|
||||
const followupRequestSchema = opportunityFollowupSchema.extend({
|
||||
contactId: z.string().nullable().optional(),
|
||||
nextFollowupDate: z.string().nullable().optional()
|
||||
});
|
||||
@@ -23,7 +23,7 @@ export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id, followupId } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryFollowupUpdate
|
||||
permission: PERMISSIONS.crmOpportunityFollowupUpdate
|
||||
});
|
||||
const payload = followupRequestSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
@@ -38,7 +38,7 @@ export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const before = (
|
||||
await listEnquiryFollowups(id, organization.id, accessContext)
|
||||
await listOpportunityFollowups(id, organization.id, accessContext)
|
||||
).find(
|
||||
(item) => item.id === followupId
|
||||
);
|
||||
@@ -47,7 +47,7 @@ export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: 'Follow-up not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updated = await updateEnquiryFollowup(
|
||||
const updated = await updateOpportunityFollowup(
|
||||
id,
|
||||
followupId,
|
||||
organization.id,
|
||||
@@ -59,7 +59,7 @@ export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
await auditUpdate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_enquiry_followup',
|
||||
entityType: 'crm_opportunity_followup',
|
||||
entityId: followupId,
|
||||
beforeData: before,
|
||||
afterData: updated
|
||||
@@ -67,7 +67,7 @@ export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Enquiry follow-up updated successfully',
|
||||
message: 'Opportunity follow-up updated successfully',
|
||||
followup: updated
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -86,7 +86,7 @@ export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to update enquiry follow-up' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to update opportunity follow-up' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id, followupId } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryFollowupDelete
|
||||
permission: PERMISSIONS.crmOpportunityFollowupDelete
|
||||
});
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
@@ -108,7 +108,7 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const before = (
|
||||
await listEnquiryFollowups(id, organization.id, accessContext)
|
||||
await listOpportunityFollowups(id, organization.id, accessContext)
|
||||
).find(
|
||||
(item) => item.id === followupId
|
||||
);
|
||||
@@ -117,7 +117,7 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: 'Follow-up not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updated = await softDeleteEnquiryFollowup(
|
||||
const updated = await softDeleteOpportunityFollowup(
|
||||
id,
|
||||
followupId,
|
||||
organization.id,
|
||||
@@ -128,7 +128,7 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
await auditDelete({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_enquiry_followup',
|
||||
entityType: 'crm_opportunity_followup',
|
||||
entityId: followupId,
|
||||
beforeData: before,
|
||||
afterData: updated
|
||||
@@ -136,7 +136,7 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Enquiry follow-up deleted successfully'
|
||||
message: 'Opportunity follow-up deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
@@ -147,6 +147,7 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to delete enquiry follow-up' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to delete opportunity follow-up' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditCreate } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
createEnquiryFollowup,
|
||||
listEnquiryFollowups
|
||||
} from '@/features/crm/enquiries/server/service';
|
||||
import { enquiryFollowupSchema } from '@/features/crm/enquiries/schemas/enquiry.schema';
|
||||
createOpportunityFollowup,
|
||||
listOpportunityFollowups
|
||||
} from '@/features/crm/opportunities/server/service';
|
||||
import { opportunityFollowupSchema } from '@/features/crm/opportunities/schemas/opportunity.schema';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -13,7 +13,7 @@ type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
const followupRequestSchema = enquiryFollowupSchema.extend({
|
||||
const followupRequestSchema = opportunityFollowupSchema.extend({
|
||||
contactId: z.string().nullable().optional(),
|
||||
nextFollowupDate: z.string().nullable().optional()
|
||||
});
|
||||
@@ -22,7 +22,7 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryFollowupRead
|
||||
permission: PERMISSIONS.crmOpportunityFollowupRead
|
||||
});
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
@@ -35,12 +35,12 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const items = await listEnquiryFollowups(id, organization.id, accessContext);
|
||||
const items = await listOpportunityFollowups(id, organization.id, accessContext);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Enquiry follow-ups loaded successfully',
|
||||
message: 'Opportunity follow-ups loaded successfully',
|
||||
items
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -52,7 +52,7 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load enquiry follow-ups' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to load opportunity follow-ups' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryFollowupCreate
|
||||
permission: PERMISSIONS.crmOpportunityFollowupCreate
|
||||
});
|
||||
const payload = followupRequestSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
@@ -74,7 +74,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const created = await createEnquiryFollowup(
|
||||
const created = await createOpportunityFollowup(
|
||||
id,
|
||||
organization.id,
|
||||
session.user.id,
|
||||
@@ -85,7 +85,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
await auditCreate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_enquiry_followup',
|
||||
entityType: 'crm_opportunity_followup',
|
||||
entityId: created.id,
|
||||
afterData: created
|
||||
});
|
||||
@@ -93,7 +93,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Enquiry follow-up created successfully',
|
||||
message: 'Opportunity follow-up created successfully',
|
||||
followup: created
|
||||
},
|
||||
{ status: 201 }
|
||||
@@ -114,6 +114,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to create enquiry follow-up' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to create opportunity follow-up' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
enquiryMarkLostSchema
|
||||
} from '@/features/crm/enquiries/schemas/enquiry.schema';
|
||||
import { getEnquiryDetail, markEnquiryAsLost } from '@/features/crm/enquiries/server/service';
|
||||
opportunityMarkLostSchema
|
||||
} from '@/features/crm/opportunities/schemas/opportunity.schema';
|
||||
import { getOpportunityDetail, markOpportunityAsLost } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -15,9 +15,9 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryMarkLost
|
||||
permission: PERMISSIONS.crmOpportunityMarkLost
|
||||
});
|
||||
const payload = enquiryMarkLostSchema.parse(await request.json());
|
||||
const payload = opportunityMarkLostSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
@@ -30,15 +30,15 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const before = await getEnquiryDetail(id, organization.id, accessContext);
|
||||
const updated = await markEnquiryAsLost(id, organization.id, session.user.id, payload, accessContext);
|
||||
const after = await getEnquiryDetail(id, organization.id, accessContext);
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await markOpportunityAsLost(id, organization.id, session.user.id, payload, accessContext);
|
||||
const after = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Enquiry marked as lost successfully',
|
||||
message: 'Opportunity marked as lost successfully',
|
||||
before,
|
||||
enquiry: updated,
|
||||
opportunity: updated,
|
||||
after
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -57,6 +57,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to mark enquiry as lost' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to mark opportunity as lost' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
enquiryMarkWonSchema
|
||||
} from '@/features/crm/enquiries/schemas/enquiry.schema';
|
||||
import { getEnquiryDetail, markEnquiryAsWon } from '@/features/crm/enquiries/server/service';
|
||||
opportunityMarkWonSchema
|
||||
} from '@/features/crm/opportunities/schemas/opportunity.schema';
|
||||
import { getOpportunityDetail, markOpportunityAsWon } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -15,9 +15,9 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryMarkWon
|
||||
permission: PERMISSIONS.crmOpportunityMarkWon
|
||||
});
|
||||
const payload = enquiryMarkWonSchema.parse(await request.json());
|
||||
const payload = opportunityMarkWonSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
@@ -30,15 +30,15 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const before = await getEnquiryDetail(id, organization.id, accessContext);
|
||||
const updated = await markEnquiryAsWon(id, organization.id, session.user.id, payload, accessContext);
|
||||
const after = await getEnquiryDetail(id, organization.id, accessContext);
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await markOpportunityAsWon(id, organization.id, session.user.id, payload, accessContext);
|
||||
const after = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Enquiry marked as won successfully',
|
||||
message: 'Opportunity marked as won successfully',
|
||||
before,
|
||||
enquiry: updated,
|
||||
opportunity: updated,
|
||||
after
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -57,6 +57,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to mark enquiry as won' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to mark opportunity as won' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getEnquiryAttachment } from '@/features/crm/enquiries/server/service';
|
||||
import { getOpportunityAttachment } from '@/features/crm/opportunities/server/service';
|
||||
import { getStorageProvider } from '@/features/foundation/storage/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
@@ -40,7 +40,7 @@ export async function GET(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id, attachmentId } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryRead
|
||||
permission: PERMISSIONS.crmOpportunityRead
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -50,7 +50,7 @@ export async function GET(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const attachment = await getEnquiryAttachment(
|
||||
const attachment = await getOpportunityAttachment(
|
||||
id,
|
||||
attachmentId,
|
||||
organization.id,
|
||||
@@ -81,3 +81,4 @@ export async function GET(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: 'Unable to load PO attachment' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
listEnquiryAttachments,
|
||||
listOpportunityAttachments,
|
||||
uploadPurchaseOrderAttachment
|
||||
} from '@/features/crm/enquiries/server/service';
|
||||
} from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -49,14 +49,14 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryRead
|
||||
permission: PERMISSIONS.crmOpportunityRead
|
||||
});
|
||||
|
||||
if (!canViewCommercialData(membership)) {
|
||||
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const items = await listEnquiryAttachments(
|
||||
const items = await listOpportunityAttachments(
|
||||
id,
|
||||
organization.id,
|
||||
buildAccessContext({
|
||||
@@ -69,7 +69,7 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Enquiry purchase order attachments loaded successfully',
|
||||
message: 'Opportunity purchase order attachments loaded successfully',
|
||||
items
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -89,7 +89,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryMarkWon
|
||||
permission: PERMISSIONS.crmOpportunityMarkWon
|
||||
});
|
||||
|
||||
if (!canViewCommercialData(membership)) {
|
||||
@@ -104,7 +104,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
}
|
||||
|
||||
const created = await uploadPurchaseOrderAttachment({
|
||||
enquiryId: id,
|
||||
opportunityId: id,
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
description:
|
||||
@@ -139,3 +139,4 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: 'Unable to upload PO attachment' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditAction } from '@/features/foundation/audit-log/service';
|
||||
import { getEnquiryDetail, reassignEnquiryToUser } from '@/features/crm/enquiries/server/service';
|
||||
import { getOpportunityDetail, reassignOpportunityToUser } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
import { enquiryAssignmentRequestSchema } from '../_schemas';
|
||||
import { opportunityAssignmentRequestSchema } from '../_schemas';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -14,9 +14,9 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryReassign
|
||||
permission: PERMISSIONS.crmOpportunityReassign
|
||||
});
|
||||
const payload = enquiryAssignmentRequestSchema.parse(await request.json());
|
||||
const payload = opportunityAssignmentRequestSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
@@ -29,8 +29,8 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const before = await getEnquiryDetail(id, organization.id, accessContext);
|
||||
const updated = await reassignEnquiryToUser(
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await reassignOpportunityToUser(
|
||||
id,
|
||||
organization.id,
|
||||
session.user.id,
|
||||
@@ -42,7 +42,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
organizationId: organization.id,
|
||||
branchId: updated.branchId,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_enquiry',
|
||||
entityType: 'crm_opportunity',
|
||||
entityId: id,
|
||||
action: 'reassign',
|
||||
beforeData: {
|
||||
@@ -58,8 +58,8 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Enquiry reassigned successfully',
|
||||
enquiry: updated
|
||||
message: 'Opportunity reassigned successfully',
|
||||
opportunity: updated
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
@@ -77,6 +77,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to reassign enquiry' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to reassign opportunity' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { enquiryReopenSchema } from '@/features/crm/enquiries/schemas/enquiry.schema';
|
||||
import { getEnquiryDetail, reopenLostEnquiry } from '@/features/crm/enquiries/server/service';
|
||||
import { opportunityReopenSchema } from '@/features/crm/opportunities/schemas/opportunity.schema';
|
||||
import { getOpportunityDetail, reopenLostOpportunity } from '@/features/crm/opportunities/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -13,9 +13,9 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryReopen
|
||||
permission: PERMISSIONS.crmOpportunityReopen
|
||||
});
|
||||
const payload = enquiryReopenSchema.parse(await request.json());
|
||||
const payload = opportunityReopenSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
@@ -28,15 +28,15 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const before = await getEnquiryDetail(id, organization.id, accessContext);
|
||||
const updated = await reopenLostEnquiry(id, organization.id, session.user.id, payload, accessContext);
|
||||
const after = await getEnquiryDetail(id, organization.id, accessContext);
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await reopenLostOpportunity(id, organization.id, session.user.id, payload, accessContext);
|
||||
const after = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Enquiry reopened successfully',
|
||||
message: 'Opportunity reopened successfully',
|
||||
before,
|
||||
enquiry: updated,
|
||||
opportunity: updated,
|
||||
after
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -55,6 +55,7 @@ export async function POST(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to reopen enquiry' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to reopen opportunity' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
getEnquiryActivity,
|
||||
getEnquiryDetail,
|
||||
softDeleteEnquiry,
|
||||
updateEnquiry
|
||||
} from '@/features/crm/enquiries/server/service';
|
||||
import { enquirySchema } from '@/features/crm/enquiries/schemas/enquiry.schema';
|
||||
getOpportunityActivity,
|
||||
getOpportunityDetail,
|
||||
softDeleteOpportunity,
|
||||
updateOpportunity
|
||||
} from '@/features/crm/opportunities/server/service';
|
||||
import { opportunitySchema } from '@/features/crm/opportunities/schemas/opportunity.schema';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -15,7 +15,7 @@ type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
const enquiryRequestSchema = enquirySchema.extend({
|
||||
const opportunityRequestSchema = opportunitySchema.extend({
|
||||
contactId: z.string().nullable().optional(),
|
||||
branchId: z.string().nullable().optional(),
|
||||
leadChannel: z.string().nullable().optional(),
|
||||
@@ -26,7 +26,7 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryRead
|
||||
permission: PERMISSIONS.crmOpportunityRead
|
||||
});
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
@@ -40,16 +40,16 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const [enquiry, activity] = await Promise.all([
|
||||
getEnquiryDetail(id, organization.id, accessContext),
|
||||
getEnquiryActivity(id, organization.id, accessContext)
|
||||
const [opportunity, activity] = await Promise.all([
|
||||
getOpportunityDetail(id, organization.id, accessContext),
|
||||
getOpportunityActivity(id, organization.id, accessContext)
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Enquiry loaded successfully',
|
||||
enquiry,
|
||||
message: 'Opportunity loaded successfully',
|
||||
opportunity,
|
||||
activity
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -61,7 +61,7 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load enquiry' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to load opportunity' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,9 +69,9 @@ export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryUpdate
|
||||
permission: PERMISSIONS.crmOpportunityUpdate
|
||||
});
|
||||
const payload = enquiryRequestSchema.parse(await request.json());
|
||||
const payload = opportunityRequestSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
@@ -84,14 +84,14 @@ export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const before = await getEnquiryDetail(id, organization.id, accessContext);
|
||||
const updated = await updateEnquiry(id, organization.id, session.user.id, payload, accessContext);
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await updateOpportunity(id, organization.id, session.user.id, payload, accessContext);
|
||||
|
||||
await auditUpdate({
|
||||
organizationId: organization.id,
|
||||
branchId: updated.branchId,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_enquiry',
|
||||
entityType: 'crm_opportunity',
|
||||
entityId: id,
|
||||
beforeData: before,
|
||||
afterData: updated
|
||||
@@ -99,8 +99,8 @@ export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Enquiry updated successfully',
|
||||
enquiry: updated
|
||||
message: 'Opportunity updated successfully',
|
||||
opportunity: updated
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
@@ -118,7 +118,7 @@ export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to update enquiry' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to update opportunity' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryDelete
|
||||
permission: PERMISSIONS.crmOpportunityDelete
|
||||
});
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
@@ -140,14 +140,14 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const before = await getEnquiryDetail(id, organization.id, accessContext);
|
||||
const updated = await softDeleteEnquiry(id, organization.id, session.user.id, accessContext);
|
||||
const before = await getOpportunityDetail(id, organization.id, accessContext);
|
||||
const updated = await softDeleteOpportunity(id, organization.id, session.user.id, accessContext);
|
||||
|
||||
await auditDelete({
|
||||
organizationId: organization.id,
|
||||
branchId: updated.branchId,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_enquiry',
|
||||
entityType: 'crm_opportunity',
|
||||
entityId: id,
|
||||
beforeData: before,
|
||||
afterData: updated
|
||||
@@ -155,7 +155,7 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Enquiry deleted successfully'
|
||||
message: 'Opportunity deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
@@ -166,6 +166,7 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to delete enquiry' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to delete opportunity' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditAction, auditCreate } from '@/features/foundation/audit-log/service';
|
||||
import { createEnquiry, listEnquiries } from '@/features/crm/enquiries/server/service';
|
||||
import { createOpportunity, listOpportunities } from '@/features/crm/opportunities/server/service';
|
||||
import { getCustomerDetail } from '@/features/crm/customers/server/service';
|
||||
import { enquirySchema } from '@/features/crm/enquiries/schemas/enquiry.schema';
|
||||
import { opportunitySchema } from '@/features/crm/opportunities/schemas/opportunity.schema';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
const enquiryRequestSchema = enquirySchema.extend({
|
||||
const opportunityRequestSchema = opportunitySchema.extend({
|
||||
contactId: z.string().nullable().optional(),
|
||||
assignedToUserId: z.string().nullable().optional(),
|
||||
branchId: z.string().nullable().optional(),
|
||||
@@ -18,7 +18,7 @@ const enquiryRequestSchema = enquirySchema.extend({
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryRead
|
||||
permission: PERMISSIONS.crmOpportunityRead
|
||||
});
|
||||
const { searchParams } = request.nextUrl;
|
||||
const page = Number(searchParams.get('page') ?? 1);
|
||||
@@ -42,13 +42,13 @@ export async function GET(request: NextRequest) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const result = await listEnquiries(
|
||||
const result = await listOpportunities(
|
||||
accessContext,
|
||||
{
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
pipelineStage: pipelineStage as 'lead' | 'enquiry' | 'closed_won' | 'closed_lost' | undefined,
|
||||
pipelineStage: pipelineStage as 'lead' | 'opportunity' | 'closed_won' | 'closed_lost' | undefined,
|
||||
status,
|
||||
productType,
|
||||
priority,
|
||||
@@ -62,7 +62,7 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Enquiries loaded successfully',
|
||||
message: 'Opportunities loaded successfully',
|
||||
totalItems: result.totalItems,
|
||||
offset,
|
||||
limit,
|
||||
@@ -77,16 +77,16 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load enquiries' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to load opportunities' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmEnquiryCreate
|
||||
permission: PERMISSIONS.crmOpportunityCreate
|
||||
});
|
||||
const payload = enquiryRequestSchema.parse(await request.json());
|
||||
const payload = opportunityRequestSchema.parse(await request.json());
|
||||
const accessContext = {
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
@@ -98,7 +98,7 @@ export async function POST(request: NextRequest) {
|
||||
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
|
||||
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
|
||||
};
|
||||
const created = await createEnquiry(
|
||||
const created = await createOpportunity(
|
||||
organization.id,
|
||||
session.user.id,
|
||||
accessContext,
|
||||
@@ -110,7 +110,7 @@ export async function POST(request: NextRequest) {
|
||||
organizationId: organization.id,
|
||||
branchId: created.branchId,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_enquiry',
|
||||
entityType: 'crm_opportunity',
|
||||
entityId: created.id,
|
||||
afterData: created
|
||||
});
|
||||
@@ -120,7 +120,7 @@ export async function POST(request: NextRequest) {
|
||||
organizationId: organization.id,
|
||||
branchId: created.branchId,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_enquiry',
|
||||
entityType: 'crm_opportunity',
|
||||
entityId: created.id,
|
||||
action:
|
||||
payload.assignedToUserId && payload.assignedToUserId !== customer.ownerUserId
|
||||
@@ -137,8 +137,8 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Enquiry created successfully',
|
||||
enquiry: created
|
||||
message: 'Opportunity created successfully',
|
||||
opportunity: created
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
@@ -158,6 +158,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to create enquiry' }, { status: 500 });
|
||||
return NextResponse.json({ message: 'Unable to create opportunity' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function GET(request: NextRequest) {
|
||||
const quotationType = searchParams.get('quotationType') ?? undefined;
|
||||
const branch = searchParams.get('branch') ?? undefined;
|
||||
const customer = searchParams.get('customer') ?? undefined;
|
||||
const enquiry = searchParams.get('enquiry') ?? undefined;
|
||||
const opportunity = searchParams.get('opportunity') ?? undefined;
|
||||
const hot = searchParams.get('hot') ?? undefined;
|
||||
const sort = searchParams.get('sort') ?? undefined;
|
||||
const accessContext = buildCrmSecurityContext({
|
||||
@@ -36,7 +36,7 @@ export async function GET(request: NextRequest) {
|
||||
quotationType,
|
||||
branch,
|
||||
customer,
|
||||
enquiry,
|
||||
opportunity,
|
||||
hot,
|
||||
sort
|
||||
}, accessContext);
|
||||
@@ -113,3 +113,4 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ message: 'Unable to create quotation' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@/features/crm/reports/server/exports/service';
|
||||
import { parseSharedReportFilters } from '@/features/crm/reports/server/filters/shared';
|
||||
import {
|
||||
getCrmEnquiryAgingReportResponse,
|
||||
getCrmOpportunityAgingReportResponse,
|
||||
getCrmLeadAgingReportResponse,
|
||||
getCrmPipelineReportResponse
|
||||
} from '@/features/crm/reports/server/service';
|
||||
@@ -18,7 +18,7 @@ function buildExportRows(input: {
|
||||
reportCode: string;
|
||||
pipeline?: Awaited<ReturnType<typeof getCrmPipelineReportResponse>>;
|
||||
leadAging?: Awaited<ReturnType<typeof getCrmLeadAgingReportResponse>>;
|
||||
enquiryAging?: Awaited<ReturnType<typeof getCrmEnquiryAgingReportResponse>>;
|
||||
opportunityAging?: Awaited<ReturnType<typeof getCrmOpportunityAgingReportResponse>>;
|
||||
}) {
|
||||
switch (input.reportCode) {
|
||||
case 'lead_pipeline':
|
||||
@@ -37,15 +37,15 @@ function buildExportRows(input: {
|
||||
String(row.convertedLeads)
|
||||
])
|
||||
];
|
||||
case 'enquiry_pipeline':
|
||||
case 'opportunity_pipeline':
|
||||
return [
|
||||
['Salesman', 'Branch', 'Product Type', 'Customer Owner', 'Open Enquiries', 'Converted To Quotation', 'Won', 'Lost'],
|
||||
...(input.pipeline?.enquiryPipeline ?? []).map((row) => [
|
||||
['Salesman', 'Branch', 'Product Type', 'Customer Owner', 'Open Opportunities', 'Converted To Quotation', 'Won', 'Lost'],
|
||||
...(input.pipeline?.opportunityPipeline ?? []).map((row) => [
|
||||
row.salesmanName,
|
||||
row.branchLabel,
|
||||
row.productTypeLabel,
|
||||
row.customerOwnerName,
|
||||
String(row.openEnquiries),
|
||||
String(row.openOpportunities),
|
||||
String(row.convertedToQuotation),
|
||||
String(row.won),
|
||||
String(row.lost)
|
||||
@@ -53,26 +53,26 @@ function buildExportRows(input: {
|
||||
];
|
||||
case 'lead_conversion':
|
||||
return [
|
||||
['Lead Source', 'Branch', 'Product Type', 'Marketing User', 'Total Leads', 'Converted Enquiries', 'Conversion Rate %'],
|
||||
['Lead Source', 'Branch', 'Product Type', 'Marketing User', 'Total Leads', 'Converted Opportunities', 'Conversion Rate %'],
|
||||
...(input.pipeline?.leadConversion ?? []).map((row) => [
|
||||
row.leadSourceLabel,
|
||||
row.branchLabel,
|
||||
row.productTypeLabel,
|
||||
row.marketingUserName,
|
||||
String(row.totalLeads),
|
||||
String(row.convertedEnquiries),
|
||||
String(row.convertedOpportunities),
|
||||
String(row.conversionRate)
|
||||
])
|
||||
];
|
||||
case 'enquiry_conversion':
|
||||
case 'opportunity_conversion':
|
||||
return [
|
||||
['Salesman', 'Branch', 'Product Type', 'Total Enquiries', 'Enquiries With Quotation', 'Conversion Rate %'],
|
||||
...(input.pipeline?.enquiryConversion ?? []).map((row) => [
|
||||
['Salesman', 'Branch', 'Product Type', 'Total Opportunities', 'Opportunities With Quotation', 'Conversion Rate %'],
|
||||
...(input.pipeline?.opportunityConversion ?? []).map((row) => [
|
||||
row.salesmanName,
|
||||
row.branchLabel,
|
||||
row.productTypeLabel,
|
||||
String(row.totalEnquiries),
|
||||
String(row.enquiriesWithQuotation),
|
||||
String(row.totalOpportunities),
|
||||
String(row.opportunitiesWithQuotation),
|
||||
String(row.conversionRate)
|
||||
])
|
||||
];
|
||||
@@ -97,11 +97,11 @@ function buildExportRows(input: {
|
||||
String(row.agingDays)
|
||||
])
|
||||
];
|
||||
case 'enquiry_aging':
|
||||
case 'opportunity_aging':
|
||||
return [
|
||||
['Enquiry No', 'Customer', 'Salesman', 'Last Follow-Up', 'Aging Days'],
|
||||
...(input.enquiryAging?.rows ?? []).map((row) => [
|
||||
row.enquiryCode,
|
||||
['Opportunity No', 'Customer', 'Salesman', 'Last Follow-Up', 'Aging Days'],
|
||||
...(input.opportunityAging?.rows ?? []).map((row) => [
|
||||
row.opportunityCode,
|
||||
row.customerName,
|
||||
row.salesmanName ?? '',
|
||||
row.lastFollowupDate ?? '',
|
||||
@@ -117,23 +117,23 @@ function getRecordCount(input: {
|
||||
reportCode: string;
|
||||
pipeline?: Awaited<ReturnType<typeof getCrmPipelineReportResponse>>;
|
||||
leadAging?: Awaited<ReturnType<typeof getCrmLeadAgingReportResponse>>;
|
||||
enquiryAging?: Awaited<ReturnType<typeof getCrmEnquiryAgingReportResponse>>;
|
||||
opportunityAging?: Awaited<ReturnType<typeof getCrmOpportunityAgingReportResponse>>;
|
||||
}) {
|
||||
switch (input.reportCode) {
|
||||
case 'lead_pipeline':
|
||||
return input.pipeline?.leadPipeline.length ?? 0;
|
||||
case 'enquiry_pipeline':
|
||||
return input.pipeline?.enquiryPipeline.length ?? 0;
|
||||
case 'opportunity_pipeline':
|
||||
return input.pipeline?.opportunityPipeline.length ?? 0;
|
||||
case 'lead_conversion':
|
||||
return input.pipeline?.leadConversion.length ?? 0;
|
||||
case 'enquiry_conversion':
|
||||
return input.pipeline?.enquiryConversion.length ?? 0;
|
||||
case 'opportunity_conversion':
|
||||
return input.pipeline?.opportunityConversion.length ?? 0;
|
||||
case 'pipeline_funnel':
|
||||
return input.pipeline?.funnel.length ?? 0;
|
||||
case 'lead_aging':
|
||||
return input.leadAging?.rows.length ?? 0;
|
||||
case 'enquiry_aging':
|
||||
return input.enquiryAging?.rows.length ?? 0;
|
||||
case 'opportunity_aging':
|
||||
return input.opportunityAging?.rows.length ?? 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -155,9 +155,9 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const pipelineCodes = new Set([
|
||||
'lead_pipeline',
|
||||
'enquiry_pipeline',
|
||||
'opportunity_pipeline',
|
||||
'lead_conversion',
|
||||
'enquiry_conversion',
|
||||
'opportunity_conversion',
|
||||
'pipeline_funnel'
|
||||
]);
|
||||
const pipeline = pipelineCodes.has(reportCode)
|
||||
@@ -165,11 +165,11 @@ export async function GET(request: NextRequest) {
|
||||
: undefined;
|
||||
const leadAging =
|
||||
reportCode === 'lead_aging' ? await getCrmLeadAgingReportResponse(context, filters) : undefined;
|
||||
const enquiryAging =
|
||||
reportCode === 'enquiry_aging'
|
||||
? await getCrmEnquiryAgingReportResponse(context, filters)
|
||||
const opportunityAging =
|
||||
reportCode === 'opportunity_aging'
|
||||
? await getCrmOpportunityAgingReportResponse(context, filters)
|
||||
: undefined;
|
||||
const rows = buildExportRows({ reportCode, pipeline, leadAging, enquiryAging });
|
||||
const rows = buildExportRows({ reportCode, pipeline, leadAging, opportunityAging });
|
||||
|
||||
if (!rows) {
|
||||
return NextResponse.json({ message: 'Unsupported report code' }, { status: 400 });
|
||||
@@ -186,7 +186,7 @@ export async function GET(request: NextRequest) {
|
||||
reportCode,
|
||||
format,
|
||||
filters: { ...filters },
|
||||
recordCount: getRecordCount({ reportCode, pipeline, leadAging, enquiryAging })
|
||||
recordCount: getRecordCount({ reportCode, pipeline, leadAging, opportunityAging })
|
||||
});
|
||||
|
||||
return new NextResponse(body, {
|
||||
@@ -203,3 +203,4 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ message: 'Unable to export CRM report' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { resolveCrmAccess } from '@/features/crm/reports/server/context';
|
||||
import { parseSharedReportFilters } from '@/features/crm/reports/server/filters/shared';
|
||||
import { getCrmEnquiryAgingReportResponse } from '@/features/crm/reports/server/service';
|
||||
import { getCrmOpportunityAgingReportResponse } from '@/features/crm/reports/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function GET(request: NextRequest) {
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
await getCrmEnquiryAgingReportResponse(
|
||||
await getCrmOpportunityAgingReportResponse(
|
||||
resolveCrmAccess({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
@@ -27,8 +27,9 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: 'Unable to load CRM enquiry aging report' },
|
||||
{ message: 'Unable to load CRM opportunity aging report' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user