This commit is contained in:
phaichayon
2026-06-17 16:04:19 +07:00
parent 5be6c54272
commit 04a886ea26
57 changed files with 6477 additions and 105 deletions

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

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

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

View File

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

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

View File

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

View File

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

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

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

View File

@@ -0,0 +1 @@
export { GET, PATCH, DELETE } from '@/app/api/crm/document-templates/[id]/route';

View File

@@ -0,0 +1 @@
export { GET, POST } from '@/app/api/crm/document-templates/[id]/versions/route';

View File

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

View File

@@ -0,0 +1 @@
export { GET, POST } from '@/app/api/crm/document-templates/route';

View File

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

View File

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