feat(ai): implement unified prompt management UX/UI (ADR-037)
- Add context config endpoints (GET/PUT /api/ai/prompts/:type/:version/context-config) - Add execution profile endpoints (CRUD /api/ai/execution-profiles) - Add sandbox RAG Prep endpoint (POST /api/ai/admin/sandbox/rag-prep) - Create Prompt Management UI with multi-type support - Add ContextConfigEditor, PromptEditor, RuntimeParametersPanel components - Add SandboxTabs for 3-step workflow (OCR, Extract, RAG Prep) - Add database deltas for ai_execution_profiles and additional prompt types - Update quickstart.md with production backend URLs - Add comprehensive test coverage for new features
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
Post,
|
||||
Body,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
Request,
|
||||
Param,
|
||||
Query,
|
||||
@@ -36,6 +37,8 @@ import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { Audit } from '../../common/decorators/audit.decorator';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
import { ValidationException } from '../../common/exceptions';
|
||||
import { IdempotencyInterceptor } from '../../common/interceptors/idempotency.interceptor';
|
||||
import type { RequestWithUser } from '../../common/interfaces/request-with-user.interface';
|
||||
|
||||
@ApiTags('Correspondences')
|
||||
@@ -52,6 +55,8 @@ export class CorrespondenceController {
|
||||
@ApiOperation({ summary: 'Process workflow action (Approve/Reject/Review)' })
|
||||
@ApiResponse({ status: 201, description: 'Action processed successfully.' })
|
||||
@RequirePermission('workflow.action_review')
|
||||
@Audit('correspondence.workflow_action', 'correspondence')
|
||||
@UseInterceptors(IdempotencyInterceptor)
|
||||
processAction(
|
||||
@Body() actionDto: WorkflowActionDto,
|
||||
@Request() req: RequestWithUser
|
||||
@@ -62,7 +67,9 @@ export class CorrespondenceController {
|
||||
|
||||
// Use Unified Workflow Engine via CorrespondenceWorkflowService
|
||||
if (!actionDto.instanceId) {
|
||||
throw new Error('instanceId is required for workflow action');
|
||||
throw new ValidationException(
|
||||
'instanceId is required for workflow action'
|
||||
);
|
||||
}
|
||||
|
||||
return this.workflowService.processAction(
|
||||
@@ -85,6 +92,7 @@ export class CorrespondenceController {
|
||||
})
|
||||
@RequirePermission('correspondence.create')
|
||||
@Audit('correspondence.create', 'correspondence')
|
||||
@UseInterceptors(IdempotencyInterceptor)
|
||||
create(
|
||||
@Body() createDto: CreateCorrespondenceDto,
|
||||
@Request() req: RequestWithUser
|
||||
@@ -125,6 +133,7 @@ export class CorrespondenceController {
|
||||
})
|
||||
@RequirePermission('correspondence.create')
|
||||
@Audit('correspondence.submit', 'correspondence')
|
||||
@UseInterceptors(IdempotencyInterceptor)
|
||||
async submit(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() submitDto: SubmitCorrespondenceDto,
|
||||
@@ -158,8 +167,9 @@ export class CorrespondenceController {
|
||||
status: 200,
|
||||
description: 'Correspondence updated successfully.',
|
||||
})
|
||||
@RequirePermission('correspondence.create') // Assuming create permission is enough for draft update, or add 'correspondence.edit'
|
||||
@RequirePermission('correspondence.edit')
|
||||
@Audit('correspondence.update', 'correspondence')
|
||||
@UseInterceptors(IdempotencyInterceptor)
|
||||
async update(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() updateDto: UpdateCorrespondenceDto,
|
||||
@@ -241,6 +251,7 @@ export class CorrespondenceController {
|
||||
@ApiOperation({ summary: 'Bulk cancel correspondences (Org Admin+)' })
|
||||
@RequirePermission('correspondence.cancel')
|
||||
@Audit('correspondence.bulk_cancel', 'correspondence')
|
||||
@UseInterceptors(IdempotencyInterceptor)
|
||||
async bulkCancel(
|
||||
@Body() dto: BulkCancelDto,
|
||||
@Request() req: RequestWithUser
|
||||
@@ -274,6 +285,7 @@ export class CorrespondenceController {
|
||||
})
|
||||
@RequirePermission('correspondence.cancel')
|
||||
@Audit('correspondence.cancel', 'correspondence')
|
||||
@UseInterceptors(IdempotencyInterceptor)
|
||||
async cancel(
|
||||
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||
@Body() cancelDto: CancelCorrespondenceDto,
|
||||
|
||||
@@ -890,6 +890,11 @@ export class CorrespondenceService {
|
||||
const updated = await this.findOne(id);
|
||||
|
||||
// Re-index updated document in Elasticsearch (fire-and-forget)
|
||||
// ใช้ status จริงจาก current revision แทนการ hardcode 'DRAFT'
|
||||
const currentRevisionStatus =
|
||||
updated.revisions?.find((r) => r.isCurrent)?.status?.statusCode ??
|
||||
updated.revisions?.[0]?.status?.statusCode ??
|
||||
'DRAFT';
|
||||
void this.searchService.indexDocument({
|
||||
id: updated.id,
|
||||
publicId: updated.publicId,
|
||||
@@ -897,7 +902,7 @@ export class CorrespondenceService {
|
||||
docNumber: updated.correspondenceNumber,
|
||||
title: updateDto.subject ?? updated.revisions?.[0]?.subject,
|
||||
description: updateDto.description ?? updated.revisions?.[0]?.description,
|
||||
status: 'DRAFT',
|
||||
status: currentRevisionStatus,
|
||||
projectId: updated.projectId,
|
||||
createdAt: updated.createdAt,
|
||||
});
|
||||
@@ -1141,7 +1146,10 @@ export class CorrespondenceService {
|
||||
try {
|
||||
await this.cancel(publicId, reason, user);
|
||||
succeeded.push(publicId);
|
||||
} catch {
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`Bulk cancel failed for ${publicId}: ${(err as Error).message}`
|
||||
);
|
||||
failed.push(publicId);
|
||||
}
|
||||
}
|
||||
@@ -1150,7 +1158,12 @@ export class CorrespondenceService {
|
||||
}
|
||||
|
||||
async exportCsv(searchDto: SearchCorrespondenceDto): Promise<string> {
|
||||
const { data } = await this.findAll(searchDto);
|
||||
// ดึงทุกแถวที่ตรงเงื่อนไข — ไม่ใช้ pagination สำหรับ export
|
||||
const { data } = await this.findAll({
|
||||
...searchDto,
|
||||
page: 1,
|
||||
limit: 10000,
|
||||
});
|
||||
|
||||
const header = [
|
||||
'Document No.',
|
||||
@@ -1182,9 +1195,12 @@ export class CorrespondenceService {
|
||||
}
|
||||
|
||||
private escapeCsv(value: string): string {
|
||||
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
// กัน CSV formula injection (OWASP)
|
||||
let v = value;
|
||||
if (/^[=+\-@\t\r]/.test(v)) v = `'${v}`;
|
||||
if (v.includes(',') || v.includes('"') || v.includes('\n')) {
|
||||
return `"${v.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,32 @@ import {
|
||||
IsObject,
|
||||
IsDateString,
|
||||
IsArray,
|
||||
IsIn,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* DTO ของผู้รับเอกสาร — ใช้กับ @ValidateNested เพื่อตรวจสอบแต่ละ element ใน recipients array
|
||||
*/
|
||||
export class RecipientDto {
|
||||
@ApiProperty({
|
||||
description: 'Organization ID or UUID ของผู้รับ',
|
||||
example: '019505a1-7c3e-7000-8000-abc123def456',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
organizationId!: number | string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ประเภทผู้รับ: TO หรือ CC',
|
||||
enum: ['TO', 'CC'],
|
||||
example: 'TO',
|
||||
})
|
||||
@IsIn(['TO', 'CC'])
|
||||
type!: 'TO' | 'CC';
|
||||
}
|
||||
|
||||
export class CreateCorrespondenceDto {
|
||||
@ApiProperty({ description: 'Project ID or UUID', example: 1 })
|
||||
@IsNotEmpty()
|
||||
@@ -125,9 +148,15 @@ export class CreateCorrespondenceDto {
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Recipients',
|
||||
example: [{ organizationId: 1, type: 'TO' }],
|
||||
example: [
|
||||
{ organizationId: '019505a1-7c3e-7000-8000-abc123def456', type: 'TO' },
|
||||
],
|
||||
type: () => RecipientDto,
|
||||
isArray: true,
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
recipients?: { organizationId: number | string; type: 'TO' | 'CC' }[];
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => RecipientDto)
|
||||
recipients?: RecipientDto[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user