task-k.2
This commit is contained in:
34
src/app/api/crm/reports/enquiry-aging/route.ts
Normal file
34
src/app/api/crm/reports/enquiry-aging/route.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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 { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmReportRead
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
await getCrmEnquiryAgingReportResponse(
|
||||
resolveCrmAccess({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membership
|
||||
}),
|
||||
parseSharedReportFilters(request.nextUrl.searchParams)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: 'Unable to load CRM enquiry aging report' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
205
src/app/api/crm/reports/export/route.ts
Normal file
205
src/app/api/crm/reports/export/route.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { resolveCrmAccess } from '@/features/crm/reports/server/context';
|
||||
import {
|
||||
auditReportExport,
|
||||
buildReportCsv,
|
||||
buildReportExcel
|
||||
} from '@/features/crm/reports/server/exports/service';
|
||||
import { parseSharedReportFilters } from '@/features/crm/reports/server/filters/shared';
|
||||
import {
|
||||
getCrmEnquiryAgingReportResponse,
|
||||
getCrmLeadAgingReportResponse,
|
||||
getCrmPipelineReportResponse
|
||||
} from '@/features/crm/reports/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
function buildExportRows(input: {
|
||||
reportCode: string;
|
||||
pipeline?: Awaited<ReturnType<typeof getCrmPipelineReportResponse>>;
|
||||
leadAging?: Awaited<ReturnType<typeof getCrmLeadAgingReportResponse>>;
|
||||
enquiryAging?: Awaited<ReturnType<typeof getCrmEnquiryAgingReportResponse>>;
|
||||
}) {
|
||||
switch (input.reportCode) {
|
||||
case 'lead_pipeline':
|
||||
return [
|
||||
['Lead Source', 'Customer Owner', 'Created By', 'Branch', 'Product Type', 'Total Leads', 'New Leads', 'Assigned Leads', 'Unassigned Leads', 'Converted Leads'],
|
||||
...(input.pipeline?.leadPipeline ?? []).map((row) => [
|
||||
row.leadSourceLabel,
|
||||
row.customerOwnerName,
|
||||
row.createdByName,
|
||||
row.branchLabel,
|
||||
row.productTypeLabel,
|
||||
String(row.totalLeads),
|
||||
String(row.newLeads),
|
||||
String(row.assignedLeads),
|
||||
String(row.unassignedLeads),
|
||||
String(row.convertedLeads)
|
||||
])
|
||||
];
|
||||
case 'enquiry_pipeline':
|
||||
return [
|
||||
['Salesman', 'Branch', 'Product Type', 'Customer Owner', 'Open Enquiries', 'Converted To Quotation', 'Won', 'Lost'],
|
||||
...(input.pipeline?.enquiryPipeline ?? []).map((row) => [
|
||||
row.salesmanName,
|
||||
row.branchLabel,
|
||||
row.productTypeLabel,
|
||||
row.customerOwnerName,
|
||||
String(row.openEnquiries),
|
||||
String(row.convertedToQuotation),
|
||||
String(row.won),
|
||||
String(row.lost)
|
||||
])
|
||||
];
|
||||
case 'lead_conversion':
|
||||
return [
|
||||
['Lead Source', 'Branch', 'Product Type', 'Marketing User', 'Total Leads', 'Converted Enquiries', 'Conversion Rate %'],
|
||||
...(input.pipeline?.leadConversion ?? []).map((row) => [
|
||||
row.leadSourceLabel,
|
||||
row.branchLabel,
|
||||
row.productTypeLabel,
|
||||
row.marketingUserName,
|
||||
String(row.totalLeads),
|
||||
String(row.convertedEnquiries),
|
||||
String(row.conversionRate)
|
||||
])
|
||||
];
|
||||
case 'enquiry_conversion':
|
||||
return [
|
||||
['Salesman', 'Branch', 'Product Type', 'Total Enquiries', 'Enquiries With Quotation', 'Conversion Rate %'],
|
||||
...(input.pipeline?.enquiryConversion ?? []).map((row) => [
|
||||
row.salesmanName,
|
||||
row.branchLabel,
|
||||
row.productTypeLabel,
|
||||
String(row.totalEnquiries),
|
||||
String(row.enquiriesWithQuotation),
|
||||
String(row.conversionRate)
|
||||
])
|
||||
];
|
||||
case 'pipeline_funnel':
|
||||
return [
|
||||
['Stage', 'Count', 'Value', 'Conversion Rate %'],
|
||||
...(input.pipeline?.funnel ?? []).map((row) => [
|
||||
row.stageLabel,
|
||||
String(row.count),
|
||||
row.value === null ? '' : String(row.value),
|
||||
row.conversionRate === null ? '' : String(row.conversionRate)
|
||||
])
|
||||
];
|
||||
case 'lead_aging':
|
||||
return [
|
||||
['Lead Number', 'Customer', 'Assigned User', 'Created Date', 'Aging Days'],
|
||||
...(input.leadAging?.rows ?? []).map((row) => [
|
||||
row.leadCode,
|
||||
row.customerName,
|
||||
row.assignedUserName ?? '',
|
||||
row.createdDate,
|
||||
String(row.agingDays)
|
||||
])
|
||||
];
|
||||
case 'enquiry_aging':
|
||||
return [
|
||||
['Enquiry No', 'Customer', 'Salesman', 'Last Follow-Up', 'Aging Days'],
|
||||
...(input.enquiryAging?.rows ?? []).map((row) => [
|
||||
row.enquiryCode,
|
||||
row.customerName,
|
||||
row.salesmanName ?? '',
|
||||
row.lastFollowupDate ?? '',
|
||||
String(row.agingDays)
|
||||
])
|
||||
];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getRecordCount(input: {
|
||||
reportCode: string;
|
||||
pipeline?: Awaited<ReturnType<typeof getCrmPipelineReportResponse>>;
|
||||
leadAging?: Awaited<ReturnType<typeof getCrmLeadAgingReportResponse>>;
|
||||
enquiryAging?: Awaited<ReturnType<typeof getCrmEnquiryAgingReportResponse>>;
|
||||
}) {
|
||||
switch (input.reportCode) {
|
||||
case 'lead_pipeline':
|
||||
return input.pipeline?.leadPipeline.length ?? 0;
|
||||
case 'enquiry_pipeline':
|
||||
return input.pipeline?.enquiryPipeline.length ?? 0;
|
||||
case 'lead_conversion':
|
||||
return input.pipeline?.leadConversion.length ?? 0;
|
||||
case 'enquiry_conversion':
|
||||
return input.pipeline?.enquiryConversion.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;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmReportExport
|
||||
});
|
||||
const reportCode = request.nextUrl.searchParams.get('reportCode') ?? '';
|
||||
const format = request.nextUrl.searchParams.get('format') === 'xls' ? 'xls' : 'csv';
|
||||
const filters = parseSharedReportFilters(request.nextUrl.searchParams);
|
||||
const context = resolveCrmAccess({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membership
|
||||
});
|
||||
|
||||
const pipelineCodes = new Set([
|
||||
'lead_pipeline',
|
||||
'enquiry_pipeline',
|
||||
'lead_conversion',
|
||||
'enquiry_conversion',
|
||||
'pipeline_funnel'
|
||||
]);
|
||||
const pipeline = pipelineCodes.has(reportCode)
|
||||
? await getCrmPipelineReportResponse(context, filters)
|
||||
: undefined;
|
||||
const leadAging =
|
||||
reportCode === 'lead_aging' ? await getCrmLeadAgingReportResponse(context, filters) : undefined;
|
||||
const enquiryAging =
|
||||
reportCode === 'enquiry_aging'
|
||||
? await getCrmEnquiryAgingReportResponse(context, filters)
|
||||
: undefined;
|
||||
const rows = buildExportRows({ reportCode, pipeline, leadAging, enquiryAging });
|
||||
|
||||
if (!rows) {
|
||||
return NextResponse.json({ message: 'Unsupported report code' }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = format === 'xls' ? buildReportExcel(rows) : buildReportCsv(rows);
|
||||
const extension = format === 'xls' ? 'xls' : 'csv';
|
||||
const contentType =
|
||||
format === 'xls' ? 'application/vnd.ms-excel; charset=utf-8' : 'text/csv; charset=utf-8';
|
||||
|
||||
await auditReportExport({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
reportCode,
|
||||
format,
|
||||
filters: { ...filters },
|
||||
recordCount: getRecordCount({ reportCode, pipeline, leadAging, enquiryAging })
|
||||
});
|
||||
|
||||
return new NextResponse(body, {
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Content-Disposition': `attachment; filename="crm-report-${reportCode}.${extension}"`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to export CRM report' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
31
src/app/api/crm/reports/lead-aging/route.ts
Normal file
31
src/app/api/crm/reports/lead-aging/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { resolveCrmAccess } from '@/features/crm/reports/server/context';
|
||||
import { parseSharedReportFilters } from '@/features/crm/reports/server/filters/shared';
|
||||
import { getCrmLeadAgingReportResponse } from '@/features/crm/reports/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmReportRead
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
await getCrmLeadAgingReportResponse(
|
||||
resolveCrmAccess({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membership
|
||||
}),
|
||||
parseSharedReportFilters(request.nextUrl.searchParams)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load CRM lead aging report' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
31
src/app/api/crm/reports/pipeline/route.ts
Normal file
31
src/app/api/crm/reports/pipeline/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { resolveCrmAccess } from '@/features/crm/reports/server/context';
|
||||
import { parseSharedReportFilters } from '@/features/crm/reports/server/filters/shared';
|
||||
import { getCrmPipelineReportResponse } from '@/features/crm/reports/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmReportRead
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
await getCrmPipelineReportResponse(
|
||||
resolveCrmAccess({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membership
|
||||
}),
|
||||
parseSharedReportFilters(request.nextUrl.searchParams)
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load CRM pipeline report' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user