task-d.5.2.1

This commit is contained in:
phaichayon
2026-06-24 13:39:25 +07:00
parent b42a4c0777
commit 350239b307
9 changed files with 1943 additions and 1 deletions

View File

@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { leadFollowupSchema } from '@/features/crm/leads/schemas/lead.schema';
import {
buildLeadAccessContext,
createLeadFollowup,
listLeadFollowups
} from '@/features/crm/leads/server/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, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmLeadRead
});
const accessContext = buildLeadAccessContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const items = await listLeadFollowups(id, organization.id, accessContext);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Lead follow-ups 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 lead follow-ups' }, { status: 500 });
}
}
export async function POST(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmLeadUpdate
});
const payload = leadFollowupSchema.parse(await request.json());
const accessContext = buildLeadAccessContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const created = await createLeadFollowup(
id,
organization.id,
session.user.id,
payload,
accessContext
);
return NextResponse.json(
{
success: true,
message: 'Lead follow-up created successfully',
followup: created
},
{ 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 }
);
}
if (error instanceof Error) {
return NextResponse.json({ message: error.message }, { status: 400 });
}
return NextResponse.json({ message: 'Unable to create lead follow-up' }, { status: 500 });
}
}

View File

@@ -0,0 +1,167 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditAction } from '@/features/foundation/audit-log/service';
import { updateLeadSchema } from '@/features/crm/leads/schemas/lead.schema';
import {
buildLeadAccessContext,
deleteLead,
getLeadById,
getLeadReferenceData,
listLeadFollowups,
updateLead
} from '@/features/crm/leads/server/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, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmLeadRead
});
const accessContext = buildLeadAccessContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const [lead, followups, referenceData] = await Promise.all([
getLeadById(id, organization.id, accessContext),
listLeadFollowups(id, organization.id, accessContext),
getLeadReferenceData(organization.id)
]);
await auditAction({
organizationId: organization.id,
branchId: lead.branchId,
userId: session.user.id,
entityType: 'crm_lead',
entityId: id,
action: 'view_lead',
afterData: {
code: lead.code
}
});
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Lead loaded successfully',
lead,
followups,
referenceData
});
} 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 lead' }, { status: 500 });
}
}
export async function PATCH(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmLeadUpdate
});
const payload = updateLeadSchema.parse(await request.json());
const accessContext = buildLeadAccessContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const before = await getLeadById(id, organization.id, accessContext);
const updated = await updateLead(
id,
organization.id,
session.user.id,
payload,
accessContext
);
await auditAction({
organizationId: organization.id,
branchId: updated.branchId,
userId: session.user.id,
entityType: 'crm_lead',
entityId: id,
action: 'update_lead',
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Lead updated successfully',
lead: updated
});
} 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 lead' }, { status: 500 });
}
}
export async function DELETE(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmLeadDelete
});
const accessContext = buildLeadAccessContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const before = await getLeadById(id, organization.id, accessContext);
const deleted = await deleteLead(id, organization.id, accessContext);
await auditAction({
organizationId: organization.id,
branchId: deleted.branchId,
userId: session.user.id,
entityType: 'crm_lead',
entityId: id,
action: 'delete_lead',
beforeData: before,
afterData: deleted
});
return NextResponse.json({
success: true,
message: 'Lead 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 lead' }, { status: 500 });
}
}

View File

@@ -0,0 +1,125 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditAction } from '@/features/foundation/audit-log/service';
import { leadSchema } from '@/features/crm/leads/schemas/lead.schema';
import {
buildLeadAccessContext,
createLead,
getLeadReferenceData,
listLeads
} from '@/features/crm/leads/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
export async function GET(request: NextRequest) {
try {
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmLeadRead
});
const { searchParams } = request.nextUrl;
const page = Number(searchParams.get('page') ?? 1);
const limit = Number(searchParams.get('limit') ?? 10);
const accessContext = buildLeadAccessContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const [result, referenceData] = await Promise.all([
listLeads(
organization.id,
{
page,
limit,
search: searchParams.get('search') ?? undefined,
branchId: searchParams.get('branchId') ?? undefined,
customerId: searchParams.get('customerId') ?? undefined,
status: searchParams.get('status') ?? undefined,
awarenessId: searchParams.get('awarenessId') ?? undefined,
followupStatus: searchParams.get('followupStatus') ?? undefined,
outcome: searchParams.get('outcome') ?? undefined,
ownerMarketingUserId: searchParams.get('ownerMarketingUserId') ?? undefined,
sort: searchParams.get('sort') ?? undefined
},
accessContext
),
getLeadReferenceData(organization.id)
]);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Leads loaded successfully',
totalItems: result.totalItems,
offset: (page - 1) * limit,
limit,
items: result.items,
referenceData
});
} 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 leads' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmLeadCreate
});
const payload = leadSchema.parse(await request.json());
const accessContext = buildLeadAccessContext({
organizationId: organization.id,
userId: session.user.id,
membership
});
const created = await createLead(
organization.id,
session.user.id,
payload,
accessContext
);
await auditAction({
organizationId: organization.id,
branchId: created.branchId,
userId: session.user.id,
entityType: 'crm_lead',
entityId: created.id,
action: 'create_lead',
afterData: created
});
return NextResponse.json(
{
success: true,
message: 'Lead created successfully',
lead: created
},
{ 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 }
);
}
if (error instanceof Error) {
return NextResponse.json({ message: error.message }, { status: 400 });
}
return NextResponse.json({ message: 'Unable to create lead' }, { status: 500 });
}
}