This commit is contained in:
phaichayon
2026-06-15 16:34:41 +07:00
parent 1d2406483e
commit eae0dee1f2
40 changed files with 9982 additions and 132 deletions

View File

@@ -0,0 +1,204 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditCreate, auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
import {
createQuotationAttachment,
getQuotationDetail,
listQuotationAttachments,
softDeleteQuotationAttachment,
updateQuotationAttachment
} from '@/features/crm/quotations/server/service';
import { quotationAttachmentSchema } from '@/features/crm/quotations/schemas/quotation.schema';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
type Params = {
params: Promise<{ id: string }>;
};
const updateSchema = quotationAttachmentSchema.extend({
id: z.string().min(1, 'Attachment id is required')
});
const deleteSchema = z.object({
id: z.string().min(1, 'Attachment id is required')
});
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationRead
});
const items = await listQuotationAttachments(id, organization.id);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Quotation attachments 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 quotation attachments' }, { status: 500 });
}
}
export async function POST(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationAttachmentManage
});
const payload = quotationAttachmentSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const created = await createQuotationAttachment(id, organization.id, session.user.id, payload);
await auditCreate({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_attachment',
entityId: created.id,
afterData: created
});
return NextResponse.json(
{
success: true,
message: 'Quotation attachment created successfully',
attachment: 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 quotation attachment' }, { status: 500 });
}
}
export async function PATCH(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationAttachmentManage
});
const payload = updateSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const before = (await listQuotationAttachments(id, organization.id)).find(
(item) => item.id === payload.id
);
if (!before) {
return NextResponse.json({ message: 'Quotation attachment not found' }, { status: 404 });
}
const updated = await updateQuotationAttachment(id, payload.id, organization.id, payload);
await auditUpdate({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_attachment',
entityId: payload.id,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation attachment updated successfully',
attachment: 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 quotation attachment' }, { status: 500 });
}
}
export async function DELETE(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationAttachmentManage
});
const payload = deleteSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const before = (await listQuotationAttachments(id, organization.id)).find(
(item) => item.id === payload.id
);
if (!before) {
return NextResponse.json({ message: 'Quotation attachment not found' }, { status: 404 });
}
const updated = await softDeleteQuotationAttachment(id, payload.id, organization.id);
await auditDelete({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_attachment',
entityId: payload.id,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation attachment deleted 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 delete quotation attachment' }, { status: 500 });
}
}

View File

@@ -0,0 +1,204 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditCreate, auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
import {
createQuotationCustomer,
getQuotationDetail,
listQuotationCustomers,
softDeleteQuotationCustomer,
updateQuotationCustomer
} from '@/features/crm/quotations/server/service';
import { quotationCustomerSchema } from '@/features/crm/quotations/schemas/quotation.schema';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
type Params = {
params: Promise<{ id: string }>;
};
const updateSchema = quotationCustomerSchema.extend({
id: z.string().min(1, 'Relation id is required')
});
const deleteSchema = z.object({
id: z.string().min(1, 'Relation id is required')
});
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationRead
});
const items = await listQuotationCustomers(id, organization.id);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Quotation customers 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 quotation customers' }, { status: 500 });
}
}
export async function POST(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationCustomerManage
});
const payload = quotationCustomerSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const created = await createQuotationCustomer(id, organization.id, payload);
await auditCreate({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_customer',
entityId: created.id,
afterData: created
});
return NextResponse.json(
{
success: true,
message: 'Quotation customer created successfully',
relation: 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 quotation customer' }, { status: 500 });
}
}
export async function PATCH(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationCustomerManage
});
const payload = updateSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const before = (await listQuotationCustomers(id, organization.id)).find(
(item) => item.id === payload.id
);
if (!before) {
return NextResponse.json({ message: 'Quotation customer relation not found' }, { status: 404 });
}
const updated = await updateQuotationCustomer(id, payload.id, organization.id, payload);
await auditUpdate({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_customer',
entityId: payload.id,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation customer updated successfully',
relation: 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 quotation customer' }, { status: 500 });
}
}
export async function DELETE(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationCustomerManage
});
const payload = deleteSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const before = (await listQuotationCustomers(id, organization.id)).find(
(item) => item.id === payload.id
);
if (!before) {
return NextResponse.json({ message: 'Quotation customer relation not found' }, { status: 404 });
}
const updated = await softDeleteQuotationCustomer(id, payload.id, organization.id);
await auditDelete({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_customer',
entityId: payload.id,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation customer deleted 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 delete quotation customer' }, { status: 500 });
}
}

View File

@@ -0,0 +1,215 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditCreate, auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
import {
createQuotationFollowup,
getQuotationDetail,
listQuotationFollowups,
softDeleteQuotationFollowup,
updateQuotationFollowup
} from '@/features/crm/quotations/server/service';
import { quotationFollowupSchema } from '@/features/crm/quotations/schemas/quotation.schema';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
type Params = {
params: Promise<{ id: string }>;
};
const updateSchema = quotationFollowupSchema.extend({
id: z.string().min(1, 'Follow-up id is required')
});
const deleteSchema = z.object({
id: z.string().min(1, 'Follow-up id is required')
});
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationRead
});
const items = await listQuotationFollowups(id, organization.id);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Quotation 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 quotation follow-ups' }, { status: 500 });
}
}
export async function POST(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationFollowupManage
});
const payload = quotationFollowupSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const created = await createQuotationFollowup(id, organization.id, session.user.id, payload);
await auditCreate({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_followup',
entityId: created.id,
afterData: created
});
return NextResponse.json(
{
success: true,
message: 'Quotation 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 quotation follow-up' }, { status: 500 });
}
}
export async function PATCH(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationFollowupManage
});
const payload = updateSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const before = (await listQuotationFollowups(id, organization.id)).find(
(item) => item.id === payload.id
);
if (!before) {
return NextResponse.json({ message: 'Quotation follow-up not found' }, { status: 404 });
}
const updated = await updateQuotationFollowup(
id,
payload.id,
organization.id,
session.user.id,
payload
);
await auditUpdate({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_followup',
entityId: payload.id,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation follow-up updated successfully',
followup: 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 quotation follow-up' }, { status: 500 });
}
}
export async function DELETE(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationFollowupManage
});
const payload = deleteSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const before = (await listQuotationFollowups(id, organization.id)).find(
(item) => item.id === payload.id
);
if (!before) {
return NextResponse.json({ message: 'Quotation follow-up not found' }, { status: 404 });
}
const updated = await softDeleteQuotationFollowup(
id,
payload.id,
organization.id,
session.user.id
);
await auditDelete({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_followup',
entityId: payload.id,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation follow-up deleted 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 delete quotation follow-up' }, { status: 500 });
}
}

View File

@@ -0,0 +1,109 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
import {
getQuotationDetail,
listQuotationItems,
softDeleteQuotationItem,
updateQuotationItem
} from '@/features/crm/quotations/server/service';
import { quotationItemSchema } from '@/features/crm/quotations/schemas/quotation.schema';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
type Params = {
params: Promise<{ id: string; itemId: string }>;
};
export async function PATCH(request: NextRequest, { params }: Params) {
try {
const { id, itemId } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationItemManage
});
const payload = quotationItemSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const before = (await listQuotationItems(id, organization.id)).find((item) => item.id === itemId);
if (!before) {
return NextResponse.json({ message: 'Quotation item not found' }, { status: 404 });
}
const updated = await updateQuotationItem(id, itemId, organization.id, session.user.id, payload);
await auditUpdate({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_item',
entityId: itemId,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation item updated successfully',
item: 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 quotation item' }, { status: 500 });
}
}
export async function DELETE(_request: NextRequest, { params }: Params) {
try {
const { id, itemId } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationItemManage
});
const quotation = await getQuotationDetail(id, organization.id);
const before = (await listQuotationItems(id, organization.id)).find((item) => item.id === itemId);
if (!before) {
return NextResponse.json({ message: 'Quotation item not found' }, { status: 404 });
}
const updated = await softDeleteQuotationItem(id, itemId, organization.id, session.user.id);
await auditDelete({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_item',
entityId: itemId,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation item 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 quotation item' }, { status: 500 });
}
}

View File

@@ -0,0 +1,89 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditCreate } from '@/features/foundation/audit-log/service';
import {
createQuotationItem,
getQuotationDetail,
listQuotationItems
} from '@/features/crm/quotations/server/service';
import { quotationItemSchema } from '@/features/crm/quotations/schemas/quotation.schema';
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.crmQuotationRead
});
const items = await listQuotationItems(id, organization.id);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Quotation items 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 quotation items' }, { status: 500 });
}
}
export async function POST(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationItemManage
});
const payload = quotationItemSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const created = await createQuotationItem(id, organization.id, session.user.id, payload);
await auditCreate({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_item',
entityId: created.id,
afterData: created
});
return NextResponse.json(
{
success: true,
message: 'Quotation item created successfully',
item: 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 quotation item' }, { status: 500 });
}
}

View File

@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditCreate } from '@/features/foundation/audit-log/service';
import {
createQuotationRevision,
getQuotationDetail,
listQuotationRevisions
} from '@/features/crm/quotations/server/service';
import { quotationRevisionSchema } from '@/features/crm/quotations/schemas/quotation.schema';
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.crmQuotationRead
});
const items = await listQuotationRevisions(id, organization.id);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Quotation revisions 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 quotation revisions' }, { status: 500 });
}
}
export async function POST(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationRevisionCreate
});
const payload = quotationRevisionSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const created = await createQuotationRevision(
id,
organization.id,
session.user.id,
payload.revisionRemark
);
await auditCreate({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation',
entityId: created.id,
afterData: created
});
return NextResponse.json(
{
success: true,
message: 'Quotation revision created successfully',
quotation: 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 quotation revision' }, { status: 500 });
}
}

View File

@@ -0,0 +1,128 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
import {
getQuotationActivity,
getQuotationDetail,
softDeleteQuotation,
updateQuotation
} from '@/features/crm/quotations/server/service';
import { quotationSchema } from '@/features/crm/quotations/schemas/quotation.schema';
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.crmQuotationRead
});
const [quotation, activity] = await Promise.all([
getQuotationDetail(id, organization.id),
getQuotationActivity(id, organization.id)
]);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Quotation loaded successfully',
quotation,
activity
});
} 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 quotation' }, { status: 500 });
}
}
export async function PATCH(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationUpdate
});
const payload = quotationSchema.parse(await request.json());
const before = await getQuotationDetail(id, organization.id);
const updated = await updateQuotation(id, organization.id, session.user.id, payload);
await auditUpdate({
organizationId: organization.id,
branchId: updated.branchId,
userId: session.user.id,
entityType: 'crm_quotation',
entityId: id,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation updated successfully',
quotation: 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 quotation' }, { status: 500 });
}
}
export async function DELETE(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationDelete
});
const before = await getQuotationDetail(id, organization.id);
const updated = await softDeleteQuotation(id, organization.id, session.user.id);
await auditDelete({
organizationId: organization.id,
branchId: updated.branchId,
userId: session.user.id,
entityType: 'crm_quotation',
entityId: id,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation 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 quotation' }, { status: 500 });
}
}

View File

@@ -0,0 +1,202 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditCreate, auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
import {
createQuotationTopic,
getQuotationDetail,
listQuotationTopics,
softDeleteQuotationTopic,
updateQuotationTopic
} from '@/features/crm/quotations/server/service';
import { quotationTopicSchema } from '@/features/crm/quotations/schemas/quotation.schema';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
type Params = {
params: Promise<{ id: string }>;
};
const deleteSchema = z.object({
id: z.string().min(1, 'Topic id is required')
});
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationRead
});
const items = await listQuotationTopics(id, organization.id);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Quotation topics 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 quotation topics' }, { status: 500 });
}
}
export async function POST(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationTopicManage
});
const payload = quotationTopicSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const created = await createQuotationTopic(id, organization.id, payload);
await auditCreate({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_topic',
entityId: created.id,
afterData: created
});
return NextResponse.json(
{
success: true,
message: 'Quotation topic created successfully',
topic: 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 quotation topic' }, { status: 500 });
}
}
export async function PATCH(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationTopicManage
});
const payload = quotationTopicSchema.extend({
id: z.string().min(1, 'Topic id is required')
}).parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const before = (await listQuotationTopics(id, organization.id)).find(
(item) => item.id === payload.id
);
if (!before) {
return NextResponse.json({ message: 'Quotation topic not found' }, { status: 404 });
}
const updated = await updateQuotationTopic(id, payload.id, organization.id, payload);
await auditUpdate({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_topic',
entityId: payload.id,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation topic updated successfully',
topic: 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 quotation topic' }, { status: 500 });
}
}
export async function DELETE(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationTopicManage
});
const payload = deleteSchema.parse(await request.json());
const quotation = await getQuotationDetail(id, organization.id);
const before = (await listQuotationTopics(id, organization.id)).find(
(item) => item.id === payload.id
);
if (!before) {
return NextResponse.json({ message: 'Quotation topic not found' }, { status: 404 });
}
const updated = await softDeleteQuotationTopic(id, payload.id, organization.id);
await auditDelete({
organizationId: organization.id,
branchId: quotation.branchId,
userId: session.user.id,
entityType: 'crm_quotation_topic',
entityId: payload.id,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Quotation topic deleted 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 delete quotation topic' }, { status: 500 });
}
}

View File

@@ -0,0 +1,104 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditCreate } from '@/features/foundation/audit-log/service';
import { createQuotation, listQuotations } from '@/features/crm/quotations/server/service';
import { quotationSchema } from '@/features/crm/quotations/schemas/quotation.schema';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
export async function GET(request: NextRequest) {
try {
const { organization } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationRead
});
const { searchParams } = request.nextUrl;
const page = Number(searchParams.get('page') ?? 1);
const limit = Number(searchParams.get('limit') ?? 10);
const search = searchParams.get('search') ?? undefined;
const status = searchParams.get('status') ?? undefined;
const quotationType = searchParams.get('quotationType') ?? undefined;
const branch = searchParams.get('branch') ?? undefined;
const customer = searchParams.get('customer') ?? undefined;
const enquiry = searchParams.get('enquiry') ?? undefined;
const hot = searchParams.get('hot') ?? undefined;
const sort = searchParams.get('sort') ?? undefined;
const result = await listQuotations(organization.id, {
page,
limit,
search,
status,
quotationType,
branch,
customer,
enquiry,
hot,
sort
});
const offset = (page - 1) * limit;
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Quotations loaded successfully',
totalItems: result.totalItems,
offset,
limit,
items: result.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 quotations' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmQuotationCreate
});
const payload = quotationSchema.parse(await request.json());
const created = await createQuotation(organization.id, session.user.id, payload);
await auditCreate({
organizationId: organization.id,
branchId: created.branchId,
userId: session.user.id,
entityType: 'crm_quotation',
entityId: created.id,
afterData: created
});
return NextResponse.json(
{
success: true,
message: 'Quotation created successfully',
quotation: 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 quotation' }, { status: 500 });
}
}