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