This commit is contained in:
phaichayon
2026-06-22 10:22:45 +07:00
parent 51d67ef7c2
commit b154a8de33
37 changed files with 5952 additions and 332 deletions

View File

@@ -40,7 +40,12 @@ export async function GET(request: NextRequest) {
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
permissions: membership.permissions
permissions: membership.permissions,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
},
{
dateFrom: searchParams.get('dateFrom'),

View File

@@ -15,7 +15,12 @@ export async function GET(request: NextRequest) {
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
permissions: membership.permissions
permissions: membership.permissions,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
},
{
dateFrom: searchParams.get('dateFrom'),

View File

@@ -17,13 +17,25 @@ export async function POST(request: NextRequest, { params }: Params) {
permission: PERMISSIONS.crmEnquiryAssign
});
const payload = enquiryAssignmentRequestSchema.parse(await request.json());
const before = await getEnquiryDetail(id, organization.id, {
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
});
const updated = await assignEnquiryToUser(id, organization.id, session.user.id, payload);
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const before = await getEnquiryDetail(id, organization.id, accessContext);
const updated = await assignEnquiryToUser(
id,
organization.id,
session.user.id,
payload,
accessContext
);
await auditAction({
organizationId: organization.id,

View File

@@ -13,12 +13,18 @@ export async function GET(_request: NextRequest, { params }: Params) {
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmEnquiryRead
});
const items = await listEnquiryProjectParties(id, organization.id, {
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
});
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const items = await listEnquiryProjectParties(id, organization.id, accessContext);
return NextResponse.json({
success: true,

View File

@@ -26,13 +26,19 @@ export async function PATCH(request: NextRequest, { params }: Params) {
permission: PERMISSIONS.crmEnquiryFollowupUpdate
});
const payload = followupRequestSchema.parse(await request.json());
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const before = (
await listEnquiryFollowups(id, organization.id, {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
})
await listEnquiryFollowups(id, organization.id, accessContext)
).find(
(item) => item.id === followupId
);
@@ -47,12 +53,7 @@ export async function PATCH(request: NextRequest, { params }: Params) {
organization.id,
session.user.id,
payload,
{
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
}
accessContext
);
await auditUpdate({
@@ -95,13 +96,19 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmEnquiryFollowupDelete
});
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const before = (
await listEnquiryFollowups(id, organization.id, {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
})
await listEnquiryFollowups(id, organization.id, accessContext)
).find(
(item) => item.id === followupId
);
@@ -115,12 +122,7 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
followupId,
organization.id,
session.user.id,
{
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
}
accessContext
);
await auditDelete({

View File

@@ -24,12 +24,18 @@ export async function GET(_request: NextRequest, { params }: Params) {
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmEnquiryFollowupRead
});
const items = await listEnquiryFollowups(id, organization.id, {
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
});
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const items = await listEnquiryFollowups(id, organization.id, accessContext);
return NextResponse.json({
success: true,
@@ -57,17 +63,23 @@ export async function POST(request: NextRequest, { params }: Params) {
permission: PERMISSIONS.crmEnquiryFollowupCreate
});
const payload = followupRequestSchema.parse(await request.json());
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const created = await createEnquiryFollowup(
id,
organization.id,
session.user.id,
payload,
{
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
}
accessContext
);
await auditCreate({

View File

@@ -17,13 +17,25 @@ export async function POST(request: NextRequest, { params }: Params) {
permission: PERMISSIONS.crmEnquiryReassign
});
const payload = enquiryAssignmentRequestSchema.parse(await request.json());
const before = await getEnquiryDetail(id, organization.id, {
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
});
const updated = await reassignEnquiryToUser(id, organization.id, session.user.id, payload);
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const before = await getEnquiryDetail(id, organization.id, accessContext);
const updated = await reassignEnquiryToUser(
id,
organization.id,
session.user.id,
payload,
accessContext
);
await auditAction({
organizationId: organization.id,

View File

@@ -32,7 +32,12 @@ export async function GET(_request: NextRequest, { params }: Params) {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const [enquiry, activity] = await Promise.all([
getEnquiryDetail(id, organization.id, accessContext),
@@ -66,18 +71,19 @@ export async function PATCH(request: NextRequest, { params }: Params) {
permission: PERMISSIONS.crmEnquiryUpdate
});
const payload = enquiryRequestSchema.parse(await request.json());
const before = await getEnquiryDetail(id, organization.id, {
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
});
const updated = await updateEnquiry(id, organization.id, session.user.id, payload, {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
});
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const before = await getEnquiryDetail(id, organization.id, accessContext);
const updated = await updateEnquiry(id, organization.id, session.user.id, payload, accessContext);
await auditUpdate({
organizationId: organization.id,
@@ -120,18 +126,19 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmEnquiryDelete
});
const before = await getEnquiryDetail(id, organization.id, {
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
});
const updated = await softDeleteEnquiry(id, organization.id, session.user.id, {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
});
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const before = await getEnquiryDetail(id, organization.id, accessContext);
const updated = await softDeleteEnquiry(id, organization.id, session.user.id, accessContext);
await auditDelete({
organizationId: organization.id,

View File

@@ -29,13 +29,19 @@ export async function GET(request: NextRequest) {
const branch = searchParams.get('branch') ?? undefined;
const customer = searchParams.get('customer') ?? undefined;
const sort = searchParams.get('sort') ?? undefined;
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const result = await listEnquiries(
{
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole
},
accessContext,
{
page,
limit,
@@ -79,10 +85,21 @@ export async function POST(request: NextRequest) {
permission: PERMISSIONS.crmEnquiryCreate
});
const payload = enquiryRequestSchema.parse(await request.json());
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
branchScopeMode: membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
const created = await createEnquiry(
organization.id,
session.user.id,
membership.businessRole,
accessContext,
payload
);

View File

@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { cloneCrmRoleProfile } from '@/features/foundation/role-management/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
const cloneRoleSchema = z.object({
code: z.string().min(2),
name: z.string().min(2),
description: z.string().nullable().optional()
});
type RouteProps = {
params: Promise<{ code: string }>;
};
export async function POST(request: NextRequest, props: RouteProps) {
try {
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmRoleCreate
});
const payload = cloneRoleSchema.parse(await request.json());
const { code } = await props.params;
const role = await cloneCrmRoleProfile(organization.id, session.user.id, code, payload);
return NextResponse.json(
{
success: true,
message: 'CRM role cloned successfully',
role
},
{ status: 201 }
);
} 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 });
}
return NextResponse.json({ message: 'Unable to clone CRM role' }, { status: 500 });
}
}

View File

@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { updateCrmRoleProfile } from '@/features/foundation/role-management/server/service';
import {
APPROVAL_AUTHORITIES,
ALL_PERMISSIONS,
OWNERSHIP_SCOPES,
PERMISSIONS,
SCOPE_MODES,
type Permission
} from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
const updateRoleSchema = z.object({
name: z.string().min(2),
description: z.string().nullable().optional(),
permissions: z
.array(z.string())
.refine(
(values) => values.every((value) => ALL_PERMISSIONS.includes(value as Permission)),
'Invalid permission key'
)
.transform((values) => values as Permission[]),
branchScopeMode: z.enum(SCOPE_MODES),
productScopeMode: z.enum(SCOPE_MODES),
ownershipScope: z.enum(OWNERSHIP_SCOPES),
approvalAuthority: z.enum(APPROVAL_AUTHORITIES),
isActive: z.boolean()
});
type RouteProps = {
params: Promise<{ code: string }>;
};
export async function PATCH(request: NextRequest, props: RouteProps) {
try {
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmRoleUpdate
});
const payload = updateRoleSchema.parse(await request.json());
const { code } = await props.params;
const role = await updateCrmRoleProfile(organization.id, session.user.id, code, payload);
return NextResponse.json({
success: true,
message: 'CRM role updated successfully',
role
});
} 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 });
}
return NextResponse.json({ message: 'Unable to update CRM role' }, { status: 500 });
}
}

View File

@@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { listCrmRoleProfiles } from '@/features/foundation/role-management/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
export async function GET() {
try {
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmRoleRead
});
const result = await listCrmRoleProfiles(organization.id, session.user.id);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'CRM roles loaded successfully',
...result
});
} 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 request' }, { status: 400 });
}
return NextResponse.json({ message: 'Unable to load CRM roles' }, { status: 500 });
}
}