task-c.1
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auditAction } from '@/features/foundation/audit-log/service';
|
||||
import { unshareCustomerContact } from '@/features/crm/customers/server/service';
|
||||
import { buildCrmSecurityContext } from '@/features/crm/security/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string; contactId: string; shareId: string }>;
|
||||
};
|
||||
|
||||
export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id, contactId, shareId } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmContactShareDelete
|
||||
});
|
||||
const accessContext = buildCrmSecurityContext({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membership
|
||||
});
|
||||
const removed = await unshareCustomerContact(
|
||||
id,
|
||||
contactId,
|
||||
shareId,
|
||||
organization.id,
|
||||
accessContext
|
||||
);
|
||||
|
||||
await auditAction({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_contact_share',
|
||||
entityId: shareId,
|
||||
action: 'unshare',
|
||||
beforeData: {
|
||||
customerId: id,
|
||||
contactId,
|
||||
sharedUser: removed.sharedToUserId
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Contact share removed successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
console.error(
|
||||
'DELETE /api/crm/customers/[id]/contacts/[contactId]/shares/[shareId] failed',
|
||||
error
|
||||
);
|
||||
return NextResponse.json({ message: 'Unable to remove contact share' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditAction } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
listCustomerContactShares,
|
||||
shareCustomerContact
|
||||
} from '@/features/crm/customers/server/service';
|
||||
import { buildCrmSecurityContext } from '@/features/crm/security/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string; contactId: string }>;
|
||||
};
|
||||
|
||||
const shareSchema = z.object({
|
||||
sharedToUserId: z.string().min(1, 'Shared user is required'),
|
||||
remark: z.string().optional().nullable()
|
||||
});
|
||||
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id, contactId } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmContactShareRead
|
||||
});
|
||||
const accessContext = buildCrmSecurityContext({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membership
|
||||
});
|
||||
const items = await listCustomerContactShares(
|
||||
id,
|
||||
contactId,
|
||||
organization.id,
|
||||
accessContext
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Contact shares loaded successfully',
|
||||
items
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return NextResponse.json({ message: error.message }, { status: error.status });
|
||||
}
|
||||
|
||||
console.error('GET /api/crm/customers/[id]/contacts/[contactId]/shares failed', error);
|
||||
return NextResponse.json({ message: 'Unable to load contact shares' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id, contactId } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmContactShareCreate
|
||||
});
|
||||
const payload = shareSchema.parse(await request.json());
|
||||
const accessContext = buildCrmSecurityContext({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membership
|
||||
});
|
||||
const created = await shareCustomerContact(
|
||||
id,
|
||||
contactId,
|
||||
organization.id,
|
||||
session.user.id,
|
||||
payload,
|
||||
accessContext
|
||||
);
|
||||
|
||||
await auditAction({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_contact_share',
|
||||
entityId: created.id,
|
||||
action: 'share',
|
||||
afterData: {
|
||||
customerId: id,
|
||||
contactId,
|
||||
sharedUser: payload.sharedToUserId,
|
||||
remark: payload.remark ?? null
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Contact shared successfully'
|
||||
},
|
||||
{ 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 }
|
||||
);
|
||||
}
|
||||
|
||||
console.error('POST /api/crm/customers/[id]/contacts/[contactId]/shares failed', error);
|
||||
return NextResponse.json({ message: 'Unable to share contact' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
146
src/app/api/crm/customers/[id]/owner/route.ts
Normal file
146
src/app/api/crm/customers/[id]/owner/route.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditAction } from '@/features/foundation/audit-log/service';
|
||||
import {
|
||||
assignCustomerOwner,
|
||||
clearCustomerOwner,
|
||||
getCustomerDetail
|
||||
} from '@/features/crm/customers/server/service';
|
||||
import { buildCrmSecurityContext } from '@/features/crm/security/server/service';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
type Params = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
const ownerSchema = z.object({
|
||||
ownerUserId: z.string().min(1, 'Owner user is required'),
|
||||
remark: z.string().optional().nullable()
|
||||
});
|
||||
|
||||
const clearOwnerSchema = z.object({
|
||||
remark: z.string().optional().nullable()
|
||||
});
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmCustomerOwnerManage
|
||||
});
|
||||
const payload = ownerSchema.parse(await request.json());
|
||||
const accessContext = buildCrmSecurityContext({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membership
|
||||
});
|
||||
const before = await getCustomerDetail(id, organization.id, accessContext);
|
||||
const updated = await assignCustomerOwner(
|
||||
id,
|
||||
organization.id,
|
||||
session.user.id,
|
||||
payload,
|
||||
accessContext
|
||||
);
|
||||
|
||||
await auditAction({
|
||||
organizationId: organization.id,
|
||||
branchId: updated.branchId,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_customer_owner',
|
||||
entityId: id,
|
||||
action: before.ownerUserId ? 'change_owner' : 'assign_owner',
|
||||
beforeData: {
|
||||
customerId: id,
|
||||
oldOwnerUserId: before.ownerUserId
|
||||
},
|
||||
afterData: {
|
||||
customerId: id,
|
||||
newOwnerUserId: payload.ownerUserId,
|
||||
remark: payload.remark ?? null
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Customer owner 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 }
|
||||
);
|
||||
}
|
||||
|
||||
console.error('PATCH /api/crm/customers/[id]/owner failed', error);
|
||||
return NextResponse.json({ message: 'Unable to update customer owner' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const { organization, session, membership } = await requireOrganizationAccess({
|
||||
permission: PERMISSIONS.crmCustomerOwnerManage
|
||||
});
|
||||
const payload = clearOwnerSchema.parse(
|
||||
request.method === 'DELETE' ? (await request.json().catch(() => ({}))) : {}
|
||||
);
|
||||
const accessContext = buildCrmSecurityContext({
|
||||
organizationId: organization.id,
|
||||
userId: session.user.id,
|
||||
membership
|
||||
});
|
||||
const before = await getCustomerDetail(id, organization.id, accessContext);
|
||||
const updated = await clearCustomerOwner(
|
||||
id,
|
||||
organization.id,
|
||||
session.user.id,
|
||||
payload,
|
||||
accessContext
|
||||
);
|
||||
|
||||
await auditAction({
|
||||
organizationId: organization.id,
|
||||
branchId: updated.branchId,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_customer_owner',
|
||||
entityId: id,
|
||||
action: 'clear_owner',
|
||||
beforeData: {
|
||||
customerId: id,
|
||||
oldOwnerUserId: before.ownerUserId
|
||||
},
|
||||
afterData: {
|
||||
customerId: id,
|
||||
newOwnerUserId: null,
|
||||
remark: payload.remark ?? null
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Customer owner cleared 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 }
|
||||
);
|
||||
}
|
||||
|
||||
console.error('DELETE /api/crm/customers/[id]/owner failed', error);
|
||||
return NextResponse.json({ message: 'Unable to clear customer owner' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import {
|
||||
getCustomerActivity,
|
||||
getCustomerDetail,
|
||||
getCustomerOwnerHistory,
|
||||
softDeleteCustomer,
|
||||
updateCustomer
|
||||
} from '@/features/crm/customers/server/service';
|
||||
@@ -37,9 +38,10 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
userId: session.user.id,
|
||||
membership
|
||||
});
|
||||
const [customer, activity] = await Promise.all([
|
||||
const [customer, activity, ownerHistory] = await Promise.all([
|
||||
getCustomerDetail(id, organization.id, accessContext),
|
||||
getCustomerActivity(id, organization.id)
|
||||
getCustomerActivity(id, organization.id),
|
||||
getCustomerOwnerHistory(id, organization.id, accessContext)
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -47,7 +49,8 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
time: new Date().toISOString(),
|
||||
message: 'Customer loaded successfully',
|
||||
customer,
|
||||
activity
|
||||
activity,
|
||||
ownerHistory
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
|
||||
@@ -29,6 +29,8 @@ export async function GET(request: NextRequest) {
|
||||
const customerStatus = searchParams.get('customerStatus') ?? undefined;
|
||||
const customerType = searchParams.get('customerType') ?? undefined;
|
||||
const branch = searchParams.get('branch') ?? undefined;
|
||||
const ownerUserId = searchParams.get('ownerUserId') ?? undefined;
|
||||
const myCustomers = searchParams.get('myCustomers') ?? undefined;
|
||||
const sort = searchParams.get('sort') ?? undefined;
|
||||
const accessContext = buildCrmSecurityContext({
|
||||
organizationId: organization.id,
|
||||
@@ -42,6 +44,8 @@ export async function GET(request: NextRequest) {
|
||||
customerStatus,
|
||||
customerType,
|
||||
branch,
|
||||
ownerUserId,
|
||||
myCustomers,
|
||||
sort
|
||||
}, accessContext);
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { auditCreate } from '@/features/foundation/audit-log/service';
|
||||
import { auditAction, auditCreate } from '@/features/foundation/audit-log/service';
|
||||
import { createEnquiry, listEnquiries } from '@/features/crm/enquiries/server/service';
|
||||
import { getCustomerDetail } from '@/features/crm/customers/server/service';
|
||||
import { enquirySchema } from '@/features/crm/enquiries/schemas/enquiry.schema';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
|
||||
|
||||
const enquiryRequestSchema = enquirySchema.extend({
|
||||
contactId: z.string().nullable().optional(),
|
||||
assignedToUserId: z.string().nullable().optional(),
|
||||
branchId: z.string().nullable().optional(),
|
||||
leadChannel: z.string().nullable().optional(),
|
||||
expectedCloseDate: z.string().nullable().optional()
|
||||
@@ -102,6 +104,7 @@ export async function POST(request: NextRequest) {
|
||||
accessContext,
|
||||
payload
|
||||
);
|
||||
const customer = await getCustomerDetail(payload.customerId, organization.id);
|
||||
|
||||
await auditCreate({
|
||||
organizationId: organization.id,
|
||||
@@ -112,6 +115,25 @@ export async function POST(request: NextRequest) {
|
||||
afterData: created
|
||||
});
|
||||
|
||||
if (customer.ownerUserId) {
|
||||
await auditAction({
|
||||
organizationId: organization.id,
|
||||
branchId: created.branchId,
|
||||
userId: session.user.id,
|
||||
entityType: 'crm_enquiry',
|
||||
entityId: created.id,
|
||||
action:
|
||||
payload.assignedToUserId && payload.assignedToUserId !== customer.ownerUserId
|
||||
? 'lead_owner_suggestion_overridden'
|
||||
: 'lead_owner_suggestion_used',
|
||||
afterData: {
|
||||
customerId: payload.customerId,
|
||||
suggestedOwnerUserId: customer.ownerUserId,
|
||||
assignedToUserId: payload.assignedToUserId ?? customer.ownerUserId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user