task-fic-display

This commit is contained in:
phaichayon
2026-06-27 22:14:16 +07:00
parent 1c019d12f3
commit 071293b60f
30 changed files with 2329 additions and 107 deletions

View File

@@ -0,0 +1,156 @@
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { auditDelete, auditUpdate } from '@/features/foundation/audit-log/service';
import {
getMasterOptionById,
softDeleteMasterOption,
updateMasterOption
} from '@/features/foundation/master-options/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
const masterOptionUpdateSchema = z.object({
category: z.string().min(1).optional(),
code: z.string().min(1).optional(),
label: z.string().min(1).optional(),
value: z.string().nullable().optional(),
parent_id: z.string().nullable().optional(),
sort_order: z.number().int().optional(),
is_active: z.boolean().optional(),
metadata: z.record(z.string(), z.unknown()).nullable().optional()
});
function mapMasterOptionResponse(item: Awaited<ReturnType<typeof getMasterOptionById>>) {
return {
id: item.id,
organization_id: item.organizationId,
category: item.category,
code: item.code,
label: item.label,
value: item.value,
parent_id: item.parentId,
parent_label: item.parentLabel,
sort_order: item.sortOrder,
is_active: item.isActive,
metadata: item.metadata,
deleted_at: item.deletedAt,
created_at: item.createdAt,
updated_at: item.updatedAt
};
}
type RouteContext = {
params: Promise<{ id: string }>;
};
export async function GET(_request: NextRequest, context: RouteContext) {
try {
const { organization } = await requireOrganizationAccess({
permission: PERMISSIONS.crmMasterOptionRead
});
const { id } = await context.params;
const item = await getMasterOptionById(id, organization.id, { includeDeleted: true });
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Master option loaded successfully',
item: mapMasterOptionResponse(item)
});
} 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 master option' }, { status: 500 });
}
}
export async function PATCH(request: NextRequest, context: RouteContext) {
try {
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmMasterOptionUpdate
});
const { id } = await context.params;
const before = await getMasterOptionById(id, organization.id, { includeDeleted: true });
const payload = masterOptionUpdateSchema.parse(await request.json());
const updated = await updateMasterOption(id, organization.id, {
category: payload.category,
code: payload.code,
label: payload.label,
value: payload.value,
parentId: payload.parent_id,
sortOrder: payload.sort_order,
isActive: payload.is_active,
metadata: payload.metadata
});
await auditUpdate({
organizationId: organization.id,
userId: session.user.id,
entityType: 'ms_option',
entityId: updated.id,
beforeData: before,
afterData: updated
});
return NextResponse.json({
success: true,
message: 'Master option 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.flatten().formErrors.join(', ') }, { status: 400 });
}
if (error instanceof Error) {
return NextResponse.json({ message: error.message }, { status: 400 });
}
return NextResponse.json({ message: 'Unable to update master option' }, { status: 500 });
}
}
export async function DELETE(_request: NextRequest, context: RouteContext) {
try {
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmMasterOptionDelete
});
const { id } = await context.params;
const before = await getMasterOptionById(id, organization.id, { includeDeleted: true });
const deleted = await softDeleteMasterOption(id, organization.id);
await auditDelete({
organizationId: organization.id,
userId: session.user.id,
entityType: 'ms_option',
entityId: deleted.id,
beforeData: before,
afterData: deleted
});
return NextResponse.json({
success: true,
message: 'Master option 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 master option' }, { status: 500 });
}
}

View File

@@ -0,0 +1,34 @@
import { NextResponse } from 'next/server';
import { listMasterOptionCategories } from '@/features/foundation/master-options/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
export async function GET() {
try {
const { organization } = await requireOrganizationAccess({
permission: PERMISSIONS.crmMasterOptionRead
});
const items = await listMasterOptionCategories(organization.id);
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Master option categories loaded successfully',
items: items.map((item) => ({
category: item.category,
total_items: item.totalItems,
active_items: item.activeItems
}))
});
} 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 master option categories' }, { status: 500 });
}
}

View File

@@ -1,12 +1,47 @@
import { NextRequest, NextResponse } from 'next/server';
import { listMasterOptions } from '@/features/foundation/master-options/service';
import { z } from 'zod';
import {
createMasterOption,
listMasterOptions
} from '@/features/foundation/master-options/service';
import { auditCreate } from '@/features/foundation/audit-log/service';
import { PERMISSIONS } from '@/lib/auth/rbac';
import { AuthError, requireOrganizationAccess } from '@/lib/auth/session';
const masterOptionSchema = z.object({
category: z.string().min(1),
code: z.string().min(1),
label: z.string().min(1),
value: z.string().nullable().optional(),
parent_id: z.string().nullable().optional(),
sort_order: z.number().int().optional(),
is_active: z.boolean().optional(),
metadata: z.record(z.string(), z.unknown()).nullable().optional()
});
function mapMasterOptionResponse(item: Awaited<ReturnType<typeof createMasterOption>>) {
return {
id: item.id,
organization_id: item.organizationId,
category: item.category,
code: item.code,
label: item.label,
value: item.value,
parent_id: item.parentId,
parent_label: item.parentLabel,
sort_order: item.sortOrder,
is_active: item.isActive,
metadata: item.metadata,
deleted_at: item.deletedAt,
created_at: item.createdAt,
updated_at: item.updatedAt
};
}
export async function GET(request: NextRequest) {
try {
const { organization } = await requireOrganizationAccess({
permission: PERMISSIONS.organizationManage
permission: PERMISSIONS.crmMasterOptionRead
});
const { searchParams } = request.nextUrl;
const page = Number(searchParams.get('page') ?? 1);
@@ -14,6 +49,7 @@ export async function GET(request: NextRequest) {
const search = searchParams.get('search') ?? undefined;
const category = searchParams.get('category') ?? undefined;
const sort = searchParams.get('sort') ?? undefined;
const result = await listMasterOptions({
organizationId: organization.id,
page,
@@ -22,29 +58,15 @@ export async function GET(request: NextRequest) {
category,
sort
});
const offset = (page - 1) * limit;
return NextResponse.json({
success: true,
time: new Date().toISOString(),
message: 'Master options loaded successfully',
total_items: result.totalItems,
offset,
offset: (page - 1) * limit,
limit,
items: result.items.map((item) => ({
id: item.id,
organization_id: item.organizationId,
category: item.category,
code: item.code,
label: item.label,
value: item.value,
parent_id: item.parentId,
parent_label: item.parentLabel,
sort_order: item.sortOrder,
is_active: item.isActive,
deleted_at: item.deletedAt,
updated_at: item.updatedAt
}))
items: result.items.map(mapMasterOptionResponse)
});
} catch (error) {
if (error instanceof AuthError) {
@@ -58,3 +80,49 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ message: 'Unable to load master options' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const { organization, session } = await requireOrganizationAccess({
permission: PERMISSIONS.crmMasterOptionCreate
});
const payload = masterOptionSchema.parse(await request.json());
const created = await createMasterOption(organization.id, {
category: payload.category,
code: payload.code,
label: payload.label,
value: payload.value,
parentId: payload.parent_id,
sortOrder: payload.sort_order,
isActive: payload.is_active,
metadata: payload.metadata
});
await auditCreate({
organizationId: organization.id,
userId: session.user.id,
entityType: 'ms_option',
entityId: created.id,
afterData: created
});
return NextResponse.json({
success: true,
message: 'Master option created 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.flatten().formErrors.join(', ') }, { status: 400 });
}
if (error instanceof Error) {
return NextResponse.json({ message: error.message }, { status: 400 });
}
return NextResponse.json({ message: 'Unable to create master option' }, { status: 500 });
}
}

View File

@@ -0,0 +1 @@
export { DELETE, GET, PATCH } from '@/app/api/foundation/master-options/[id]/route';

View File

@@ -0,0 +1 @@
export { GET } from '@/app/api/foundation/master-options/categories/route';

View File

@@ -0,0 +1 @@
export { GET, POST } from '@/app/api/foundation/master-options/route';