This commit is contained in:
phaichayon
2026-06-22 16:57:02 +07:00
parent 3f28fde39f
commit ffa5de8311
33 changed files with 6706 additions and 101 deletions

View File

@@ -22,6 +22,7 @@ export async function POST(request: NextRequest, { params }: Params) {
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
permissions: membership.permissions ?? [],
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',

View File

@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import {
enquiryMarkLostSchema
} from '@/features/crm/enquiries/schemas/enquiry.schema';
import { getEnquiryDetail, markEnquiryAsLost } from '@/features/crm/enquiries/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
type Params = {
params: Promise<{ id: string }>;
};
export async function POST(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmEnquiryMarkLost
});
const payload = enquiryMarkLostSchema.parse(await request.json());
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
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'
};
const before = await getEnquiryDetail(id, organization.id, accessContext);
const updated = await markEnquiryAsLost(id, organization.id, session.user.id, payload, accessContext);
const after = await getEnquiryDetail(id, organization.id, accessContext);
return NextResponse.json({
success: true,
message: 'Enquiry marked as lost successfully',
before,
enquiry: updated,
after
});
} 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 mark enquiry as lost' }, { status: 500 });
}
}

View File

@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import {
enquiryMarkWonSchema
} from '@/features/crm/enquiries/schemas/enquiry.schema';
import { getEnquiryDetail, markEnquiryAsWon } from '@/features/crm/enquiries/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
type Params = {
params: Promise<{ id: string }>;
};
export async function POST(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmEnquiryMarkWon
});
const payload = enquiryMarkWonSchema.parse(await request.json());
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
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'
};
const before = await getEnquiryDetail(id, organization.id, accessContext);
const updated = await markEnquiryAsWon(id, organization.id, session.user.id, payload, accessContext);
const after = await getEnquiryDetail(id, organization.id, accessContext);
return NextResponse.json({
success: true,
message: 'Enquiry marked as won successfully',
before,
enquiry: updated,
after
});
} 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 mark enquiry as won' }, { status: 500 });
}
}

View File

@@ -0,0 +1,83 @@
import { NextRequest, NextResponse } from 'next/server';
import { getEnquiryAttachment } from '@/features/crm/enquiries/server/service';
import { getStorageProvider } from '@/features/foundation/storage/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
type Params = {
params: Promise<{ id: string; attachmentId: string }>;
};
function buildAccessContext(input: {
organizationId: string;
userId: string;
membership: {
role: string;
businessRole: string;
permissions?: string[] | null;
branchScopeIds?: string[] | null;
productTypeScopeIds?: string[] | null;
ownershipScope?: string | null;
branchScopeMode?: string | null;
productScopeMode?: string | null;
};
}) {
return {
organizationId: input.organizationId,
userId: input.userId,
membershipRole: input.membership.role,
businessRole: input.membership.businessRole,
permissions: input.membership.permissions ?? [],
branchScopeIds: input.membership.branchScopeIds ?? [],
productTypeScopeIds: input.membership.productTypeScopeIds ?? [],
ownershipScope: input.membership.ownershipScope ?? 'organization',
branchScopeMode: input.membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: input.membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
}
export async function GET(request: NextRequest, { params }: Params) {
try {
const { id, attachmentId } = await params;
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmEnquiryRead
});
if (
membership.role !== 'admin' &&
!(membership.permissions ?? []).includes(PERMISSIONS.crmQuotationPricingRead)
) {
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
const attachment = await getEnquiryAttachment(
id,
attachmentId,
organization.id,
buildAccessContext({
organizationId: organization.id,
userId: session.user.id,
membership
})
);
const object = await getStorageProvider().getObject({ key: attachment.storageKey });
const download = request.nextUrl.searchParams.get('download') === '1';
return new NextResponse(object.body, {
headers: {
'Content-Type': attachment.fileType ?? object.contentType,
'Content-Disposition': `${download ? 'attachment' : 'inline'}; filename="${attachment.originalFileName}"`
}
});
} 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 PO attachment' }, { status: 500 });
}
}

View File

@@ -0,0 +1,141 @@
import { NextRequest, NextResponse } from 'next/server';
import {
listEnquiryAttachments,
uploadPurchaseOrderAttachment
} from '@/features/crm/enquiries/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
type Params = {
params: Promise<{ id: string }>;
};
function buildAccessContext(input: {
organizationId: string;
userId: string;
membership: {
role: string;
businessRole: string;
permissions?: string[] | null;
branchScopeIds?: string[] | null;
productTypeScopeIds?: string[] | null;
ownershipScope?: string | null;
branchScopeMode?: string | null;
productScopeMode?: string | null;
};
}) {
return {
organizationId: input.organizationId,
userId: input.userId,
membershipRole: input.membership.role,
businessRole: input.membership.businessRole,
permissions: input.membership.permissions ?? [],
branchScopeIds: input.membership.branchScopeIds ?? [],
productTypeScopeIds: input.membership.productTypeScopeIds ?? [],
ownershipScope: input.membership.ownershipScope ?? 'organization',
branchScopeMode: input.membership.branchScopeMode === 'assigned' ? 'assigned' : 'all',
productScopeMode: input.membership.productScopeMode === 'assigned' ? 'assigned' : 'all'
};
}
function canViewCommercialData(membership: { role: string; permissions?: string[] | null }) {
return (
membership.role === 'admin' ||
(membership.permissions ?? []).includes(PERMISSIONS.crmQuotationPricingRead)
);
}
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmEnquiryRead
});
if (!canViewCommercialData(membership)) {
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
const items = await listEnquiryAttachments(
id,
organization.id,
buildAccessContext({
organizationId: organization.id,
userId: session.user.id,
membership
})
);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Enquiry purchase order 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 PO attachments' }, { status: 500 });
}
}
export async function POST(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmEnquiryMarkWon
});
if (!canViewCommercialData(membership)) {
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
const formData = await request.formData();
const file = formData.get('file');
if (!(file instanceof File)) {
return NextResponse.json({ message: 'PO attachment file is required' }, { status: 400 });
}
const created = await uploadPurchaseOrderAttachment({
enquiryId: id,
organizationId: organization.id,
userId: session.user.id,
description:
typeof formData.get('description') === 'string' ? (formData.get('description') as string) : null,
file: {
name: file.name,
type: file.type,
size: file.size,
buffer: Buffer.from(await file.arrayBuffer())
},
accessContext: buildAccessContext({
organizationId: organization.id,
userId: session.user.id,
membership
})
});
return NextResponse.json({
success: true,
message: 'PO attachment uploaded successfully',
attachment: created
});
} 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 upload PO attachment' }, { status: 500 });
}
}

View File

@@ -22,6 +22,7 @@ export async function POST(request: NextRequest, { params }: Params) {
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
permissions: membership.permissions ?? [],
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',

View File

@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { enquiryReopenSchema } from '@/features/crm/enquiries/schemas/enquiry.schema';
import { getEnquiryDetail, reopenLostEnquiry } from '@/features/crm/enquiries/server/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
type Params = {
params: Promise<{ id: string }>;
};
export async function POST(request: NextRequest, { params }: Params) {
try {
const { id } = await params;
const { organization, session, membership } = await requireOrganizationAccess({
permission: PERMISSIONS.crmEnquiryReopen
});
const payload = enquiryReopenSchema.parse(await request.json());
const accessContext = {
organizationId: organization.id,
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
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'
};
const before = await getEnquiryDetail(id, organization.id, accessContext);
const updated = await reopenLostEnquiry(id, organization.id, session.user.id, payload, accessContext);
const after = await getEnquiryDetail(id, organization.id, accessContext);
return NextResponse.json({
success: true,
message: 'Enquiry reopened successfully',
before,
enquiry: updated,
after
});
} 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 reopen enquiry' }, { status: 500 });
}
}

View File

@@ -33,6 +33,7 @@ export async function GET(_request: NextRequest, { params }: Params) {
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
permissions: membership.permissions ?? [],
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
@@ -76,6 +77,7 @@ export async function PATCH(request: NextRequest, { params }: Params) {
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
permissions: membership.permissions ?? [],
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',
@@ -131,6 +133,7 @@ export async function DELETE(_request: NextRequest, { params }: Params) {
userId: session.user.id,
membershipRole: membership.role,
businessRole: membership.businessRole,
permissions: membership.permissions ?? [],
branchScopeIds: membership.branchScopeIds ?? [],
productTypeScopeIds: membership.productTypeScopeIds ?? [],
ownershipScope: membership.ownershipScope ?? 'organization',