task-j1
This commit is contained in:
131
src/app/api/crm/dashboard/export/route.ts
Normal file
131
src/app/api/crm/dashboard/export/route.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auditAction } from '@/features/foundation/audit-log/service';
|
||||
import { getCrmDashboardData } from '@/features/crm/dashboard/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
function escapeCsv(value: string | number | null | undefined) {
|
||||
const text = value === null || value === undefined ? '' : String(value);
|
||||
return `"${text.replaceAll('"', '""')}"`;
|
||||
}
|
||||
|
||||
function buildCsv(rows: string[][]) {
|
||||
return rows.map((row) => row.map((cell) => escapeCsv(cell)).join(',')).join('\n');
|
||||
}
|
||||
|
||||
function buildExcelTable(rows: string[][]) {
|
||||
const body = rows
|
||||
.map(
|
||||
(row, rowIndex) =>
|
||||
`<tr>${row
|
||||
.map((cell) => (rowIndex === 0 ? `<th>${cell}</th>` : `<td>${cell}</td>`))
|
||||
.join('')}</tr>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
return `<!DOCTYPE html><html><head><meta charset="utf-8" /></head><body><table>${body}</table></body></html>`;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { session, membership, organization } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDashboardExport
|
||||
});
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const section = searchParams.get('section') ?? 'sales-ranking';
|
||||
const format = searchParams.get('format') ?? 'csv';
|
||||
const data = await getCrmDashboardData(
|
||||
{
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membershipRole: membership.role,
|
||||
businessRole: membership.businessRole
|
||||
},
|
||||
{
|
||||
dateFrom: searchParams.get('dateFrom'),
|
||||
dateTo: searchParams.get('dateTo'),
|
||||
branch: searchParams.get('branch'),
|
||||
salesman: searchParams.get('salesman'),
|
||||
projectPartyRole: searchParams.get('projectPartyRole'),
|
||||
productType: searchParams.get('productType')
|
||||
}
|
||||
);
|
||||
|
||||
let rows: string[][] = [];
|
||||
|
||||
if (section === 'revenue-analytics') {
|
||||
rows = [
|
||||
['Dimension', 'Customer Code', 'Customer Name', 'Revenue', 'Quotation Count', 'Won Revenue'],
|
||||
...[
|
||||
['End Customer', ...data.revenueAnalytics.topEndCustomers],
|
||||
['Billing Customer', ...data.revenueAnalytics.topBillingCustomers],
|
||||
['Contractor', ...data.revenueAnalytics.topContractors],
|
||||
['Consultant', ...data.revenueAnalytics.topConsultants]
|
||||
].flatMap(([dimension, ...entries]) =>
|
||||
(entries as typeof data.revenueAnalytics.topEndCustomers).map((row) => [
|
||||
String(dimension),
|
||||
row.customerCode,
|
||||
row.customerName,
|
||||
String(row.revenue),
|
||||
String(row.quotationCount),
|
||||
String(row.wonRevenue)
|
||||
])
|
||||
)
|
||||
];
|
||||
} else if (section === 'pipeline-funnel') {
|
||||
rows = [
|
||||
['Stage', 'Count', 'Value', 'Conversion Rate'],
|
||||
...data.funnel.map((row) => [
|
||||
row.label,
|
||||
String(row.count),
|
||||
String(row.value),
|
||||
row.conversionRate === null ? '' : String(row.conversionRate)
|
||||
])
|
||||
];
|
||||
} else {
|
||||
rows = [
|
||||
['Sales Person', 'Lead Count', 'Opportunity Count', 'Quotation Count', 'Approved Quotations', 'Won Revenue', 'Conversion Rate'],
|
||||
...data.salesRanking.map((row) => [
|
||||
row.salesPersonName,
|
||||
String(row.leadCount),
|
||||
String(row.opportunityCount),
|
||||
String(row.quotationCount),
|
||||
String(row.approvedQuotations),
|
||||
String(row.wonRevenue),
|
||||
String(row.conversionRate)
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
await auditAction({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_dashboard_export',
|
||||
entityId: section,
|
||||
action: format === 'xls' ? 'export_excel' : 'export_csv',
|
||||
afterData: {
|
||||
section,
|
||||
format,
|
||||
filters: data.filters
|
||||
}
|
||||
});
|
||||
|
||||
const body = format === 'xls' ? buildExcelTable(rows) : buildCsv(rows);
|
||||
const contentType =
|
||||
format === 'xls' ? 'application/vnd.ms-excel; charset=utf-8' : 'text/csv; charset=utf-8';
|
||||
const extension = format === 'xls' ? 'xls' : 'csv';
|
||||
|
||||
return new NextResponse(body, {
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Content-Disposition': `attachment; filename="crm-dashboard-${section}.${extension}"`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to export CRM dashboard data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
37
src/app/api/crm/dashboard/route.ts
Normal file
37
src/app/api/crm/dashboard/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getCrmDashboardData } from '@/features/crm/dashboard/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { session, membership, organization } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDashboardRead
|
||||
});
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const data = await getCrmDashboardData(
|
||||
{
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membershipRole: membership.role,
|
||||
businessRole: membership.businessRole
|
||||
},
|
||||
{
|
||||
dateFrom: searchParams.get('dateFrom'),
|
||||
dateTo: searchParams.get('dateTo'),
|
||||
branch: searchParams.get('branch'),
|
||||
salesman: searchParams.get('salesman'),
|
||||
projectPartyRole: searchParams.get('projectPartyRole'),
|
||||
productType: searchParams.get('productType')
|
||||
}
|
||||
);
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Unable to load CRM dashboard' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
118
src/app/api/crm/settings/approval-workflows/[id]/route.ts
Normal file
118
src/app/api/crm/settings/approval-workflows/[id]/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
getApprovalWorkflow,
|
||||
softDeleteApprovalWorkflow,
|
||||
updateApprovalWorkflow
|
||||
} from '@/features/foundation/approval/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
const workflowSchema = z.object({
|
||||
code: z.string().min(1).optional(),
|
||||
name: z.string().min(1).optional(),
|
||||
entityType: z.string().min(1).optional(),
|
||||
isActive: z.boolean().optional()
|
||||
});
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmApprovalWorkflowRead
|
||||
});
|
||||
const workflow = await getApprovalWorkflow(id, organization.id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Approval workflow loaded successfully',
|
||||
workflow
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to load approval workflow' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmApprovalWorkflowUpdate
|
||||
});
|
||||
const payload = workflowSchema.parse(await request.json());
|
||||
const before = await getApprovalWorkflow(id, organization.id);
|
||||
const updated = await updateApprovalWorkflow(id, organization.id, payload);
|
||||
|
||||
await auditUpdate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_approval_workflow',
|
||||
entityId: id,
|
||||
beforeData: before,
|
||||
afterData: updated
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Approval workflow updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ message: error.issues[0]?.message ?? 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to update approval workflow' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmApprovalWorkflowDelete
|
||||
});
|
||||
const result = await softDeleteApprovalWorkflow(id, organization.id);
|
||||
|
||||
await auditDelete({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_approval_workflow',
|
||||
entityId: id,
|
||||
beforeData: result.before,
|
||||
afterData: result.after
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Approval workflow deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to delete approval workflow' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditUpdate } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
getApprovalWorkflow,
|
||||
replaceApprovalWorkflowSteps
|
||||
} from '@/features/foundation/approval/server/service';
|
||||
import { BUSINESS_ROLES, PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
const stepSchema = z.object({
|
||||
stepNumber: z.number().int().min(1),
|
||||
roleCode: z.enum(BUSINESS_ROLES),
|
||||
roleName: z.string().min(1),
|
||||
isRequired: z.boolean().optional()
|
||||
});
|
||||
|
||||
const payloadSchema = z.object({
|
||||
steps: z.array(stepSchema)
|
||||
});
|
||||
|
||||
export async function PUT(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmApprovalWorkflowUpdate
|
||||
});
|
||||
const payload = payloadSchema.parse(await request.json());
|
||||
const before = await getApprovalWorkflow(id, organization.id);
|
||||
const steps = await replaceApprovalWorkflowSteps(id, organization.id, payload.steps);
|
||||
|
||||
await auditUpdate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_approval_step',
|
||||
entityId: id,
|
||||
beforeData: before.steps,
|
||||
afterData: steps
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Approval workflow steps updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ message: error.issues[0]?.message ?? 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ message: 'Unable to update approval workflow steps' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
78
src/app/api/crm/settings/approval-workflows/route.ts
Normal file
78
src/app/api/crm/settings/approval-workflows/route.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditCreate } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
createApprovalWorkflow,
|
||||
listApprovalWorkflows
|
||||
} from '@/features/foundation/approval/server/service';
|
||||
import { BUSINESS_ROLES, PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
const workflowSchema = z.object({
|
||||
code: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
entityType: z.string().min(1),
|
||||
isActive: z.boolean().optional()
|
||||
});
|
||||
|
||||
export async function GET(_request: NextRequest) {
|
||||
try {
|
||||
const { organization } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmApprovalWorkflowRead
|
||||
});
|
||||
const items = await listApprovalWorkflows(organization.id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Approval workflows loaded successfully',
|
||||
items
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to load approval workflows' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmApprovalWorkflowCreate
|
||||
});
|
||||
const payload = workflowSchema.parse(await request.json());
|
||||
const created = await createApprovalWorkflow(organization.id, payload);
|
||||
|
||||
await auditCreate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_approval_workflow',
|
||||
entityId: created.id,
|
||||
afterData: created
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Approval workflow created successfully',
|
||||
allowedRoles: BUSINESS_ROLES
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ message: error.issues[0]?.message ?? 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to create approval workflow' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { previewDocumentSequenceById } from '@/features/foundation/document-sequence/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDocumentSequenceRead
|
||||
});
|
||||
const preview = await previewDocumentSequenceById(id, organization.id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Document sequence preview loaded successfully',
|
||||
preview
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ message: 'Unable to preview document sequence' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditUpdate } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
getDocumentSequence,
|
||||
resetDocumentSequence
|
||||
} from '@/features/foundation/document-sequence/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
const resetSchema = z.object({
|
||||
currentNumber: z.number().int().min(0)
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDocumentSequenceReset
|
||||
});
|
||||
const payload = resetSchema.parse(await request.json());
|
||||
const before = await getDocumentSequence(id, organization.id);
|
||||
const updated = await resetDocumentSequence(id, organization.id, payload);
|
||||
|
||||
await auditUpdate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_document_sequence',
|
||||
entityId: id,
|
||||
beforeData: before,
|
||||
afterData: updated
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Document sequence reset successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ message: error.issues[0]?.message ?? 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to reset document sequence' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
122
src/app/api/crm/settings/document-sequences/[id]/route.ts
Normal file
122
src/app/api/crm/settings/document-sequences/[id]/route.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
deleteDocumentSequence,
|
||||
getDocumentSequence,
|
||||
updateDocumentSequence
|
||||
} from '@/features/foundation/document-sequence/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
const sequenceSchema = z.object({
|
||||
documentType: z.string().min(1).optional(),
|
||||
prefix: z.string().min(1).optional(),
|
||||
period: z.string().min(1).optional(),
|
||||
branchId: z.string().optional().nullable(),
|
||||
currentNumber: z.number().int().min(0).optional(),
|
||||
paddingLength: z.number().int().min(1).optional(),
|
||||
isActive: z.boolean().optional()
|
||||
});
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDocumentSequenceRead
|
||||
});
|
||||
const sequence = await getDocumentSequence(id, organization.id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Document sequence loaded successfully',
|
||||
sequence
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to load document sequence' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDocumentSequenceUpdate
|
||||
});
|
||||
const payload = sequenceSchema.parse(await request.json());
|
||||
const before = await getDocumentSequence(id, organization.id);
|
||||
const updated = await updateDocumentSequence(id, organization.id, payload);
|
||||
|
||||
await auditUpdate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_document_sequence',
|
||||
entityId: id,
|
||||
beforeData: before,
|
||||
afterData: updated
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Document sequence updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ message: error.issues[0]?.message ?? 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to update document sequence' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDocumentSequenceUpdate
|
||||
});
|
||||
const before = await getDocumentSequence(id, organization.id);
|
||||
const deleted = await deleteDocumentSequence(id, organization.id);
|
||||
|
||||
await auditDelete({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_document_sequence',
|
||||
entityId: id,
|
||||
beforeData: before,
|
||||
afterData: deleted
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Document sequence deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to delete document sequence' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
80
src/app/api/crm/settings/document-sequences/route.ts
Normal file
80
src/app/api/crm/settings/document-sequences/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditCreate } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
createDocumentSequence,
|
||||
listDocumentSequences
|
||||
} from '@/features/foundation/document-sequence/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
const sequenceSchema = z.object({
|
||||
documentType: z.string().min(1),
|
||||
prefix: z.string().min(1),
|
||||
period: z.string().min(1),
|
||||
branchId: z.string().optional().nullable(),
|
||||
currentNumber: z.number().int().min(0).optional(),
|
||||
paddingLength: z.number().int().min(1),
|
||||
isActive: z.boolean().optional()
|
||||
});
|
||||
|
||||
export async function GET(_request: NextRequest) {
|
||||
try {
|
||||
const { organization } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDocumentSequenceRead
|
||||
});
|
||||
const items = await listDocumentSequences(organization.id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
time: new Date().toISOString(),
|
||||
message: 'Document sequences loaded successfully',
|
||||
items
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to load document sequences' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDocumentSequenceCreate
|
||||
});
|
||||
const payload = sequenceSchema.parse(await request.json());
|
||||
const created = await createDocumentSequence(organization.id, payload);
|
||||
|
||||
await auditCreate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_document_sequence',
|
||||
entityId: created.id,
|
||||
afterData: created
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Document sequence created successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ message: error.issues[0]?.message ?? 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ message: 'Unable to create document sequence' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { GET, PATCH, DELETE } from '@/app/api/crm/document-templates/[id]/route';
|
||||
@@ -0,0 +1 @@
|
||||
export { GET, POST } from '@/app/api/crm/document-templates/[id]/versions/route';
|
||||
@@ -0,0 +1,108 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
deleteDocumentTemplateMapping,
|
||||
updateDocumentTemplateMapping
|
||||
} from '@/features/foundation/document-template/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
const columnSchema = z.object({
|
||||
columnName: z.string().min(1),
|
||||
sourceField: z.string().min(1),
|
||||
columnLetter: z.string().optional().nullable(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
formatMask: z.string().optional().nullable()
|
||||
});
|
||||
|
||||
const mappingSchema = z.object({
|
||||
placeholderKey: z.string().min(1).optional(),
|
||||
sourcePath: z.string().min(1).optional(),
|
||||
dataType: z.enum(['scalar', 'multiline', 'table', 'image']).optional(),
|
||||
sheetName: z.string().optional().nullable(),
|
||||
defaultValue: z.string().optional().nullable(),
|
||||
formatMask: z.string().optional().nullable(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
columns: z.array(columnSchema).optional()
|
||||
});
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDocumentTemplateUpdate
|
||||
});
|
||||
const payload = mappingSchema.parse(await request.json());
|
||||
const updated = await updateDocumentTemplateMapping(id, organization.id, payload);
|
||||
|
||||
await auditUpdate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_document_template_mapping',
|
||||
entityId: id,
|
||||
afterData: updated
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Document template mapping updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ message: error.issues[0]?.message ?? 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ message: 'Unable to update document template mapping' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDocumentTemplateDelete
|
||||
});
|
||||
const result = await deleteDocumentTemplateMapping(id, organization.id);
|
||||
|
||||
await auditDelete({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_document_template_mapping',
|
||||
entityId: id,
|
||||
beforeData: result.before,
|
||||
afterData: result.after
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Document template mapping deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ message: 'Unable to delete document template mapping' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
1
src/app/api/crm/settings/document-templates/route.ts
Normal file
1
src/app/api/crm/settings/document-templates/route.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { GET, POST } from '@/app/api/crm/document-templates/route';
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditCreate } from '@/features/foundation/audit-log/service';
|
||||
import { createDocumentTemplateMapping } from '@/features/foundation/document-template/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
const columnSchema = z.object({
|
||||
columnName: z.string().min(1),
|
||||
sourceField: z.string().min(1),
|
||||
columnLetter: z.string().optional().nullable(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
formatMask: z.string().optional().nullable()
|
||||
});
|
||||
|
||||
const mappingSchema = z.object({
|
||||
placeholderKey: z.string().min(1),
|
||||
sourcePath: z.string().min(1),
|
||||
dataType: z.enum(['scalar', 'multiline', 'table', 'image']),
|
||||
sheetName: z.string().optional().nullable(),
|
||||
defaultValue: z.string().optional().nullable(),
|
||||
formatMask: z.string().optional().nullable(),
|
||||
sortOrder: z.number().int().optional(),
|
||||
columns: z.array(columnSchema).optional()
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDocumentTemplateUpdate
|
||||
});
|
||||
const payload = mappingSchema.parse(await request.json());
|
||||
const created = await createDocumentTemplateMapping(id, organization.id, payload);
|
||||
|
||||
await auditCreate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_document_template_mapping',
|
||||
entityId: created.id,
|
||||
afterData: created
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Document template mapping created successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ message: error.issues[0]?.message ?? 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ message: 'Unable to create document template mapping' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditUpdate } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
setDocumentTemplateVersionActive,
|
||||
updateDocumentTemplateVersion
|
||||
} from '@/features/foundation/document-template/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
const versionSchema = z.object({
|
||||
version: z.string().min(1).optional(),
|
||||
filePath: z.string().optional().nullable(),
|
||||
schemaJson: z.unknown().optional(),
|
||||
previewImageUrl: z.string().optional().nullable(),
|
||||
isActive: z.boolean().optional()
|
||||
});
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmDocumentTemplateUpdate
|
||||
});
|
||||
const payload = versionSchema.parse(await request.json());
|
||||
|
||||
if (Object.keys(payload).length === 1 && typeof payload.isActive === 'boolean') {
|
||||
const result = await setDocumentTemplateVersionActive(id, organization.id, payload.isActive);
|
||||
|
||||
await auditUpdate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_document_template_version',
|
||||
entityId: id,
|
||||
beforeData: result.before,
|
||||
afterData: result.after
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Document template version status updated successfully'
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await updateDocumentTemplateVersion(id, organization.id, payload);
|
||||
|
||||
await auditUpdate({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_document_template_version',
|
||||
entityId: id,
|
||||
afterData: updated
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Document template version updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ message: error.issues[0]?.message ?? 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json({ message: error.message }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ message: 'Unable to update document template version' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user