From 863a7277565eb6e68703b7d05a8e92f69e421f39 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 8 Dec 2025 16:25:56 +0700 Subject: [PATCH] 251208:1625 Frontend: to be complete admin panel, Backend: tobe recheck all task --- backend/src/common/guards/rbac.guard.ts | 3 +- .../interceptors/transform.interceptor.ts | 28 +- backend/src/main.ts | 2 +- .../src/modules/auth/entities/role.entity.ts | 39 +- .../dto/search-correspondence.dto.ts | 11 + .../drawing/contract-drawing.controller.ts | 8 +- .../drawing/contract-drawing.service.ts | 24 +- .../dto/search-contract-drawing.dto.ts | 6 +- .../drawing/dto/search-shop-drawing.dto.ts | 2 +- .../modules/drawing/shop-drawing.service.ts | 16 +- backend/src/modules/rfa/dto/search-rfa.dto.ts | 2 +- backend/src/modules/rfa/rfa.controller.ts | 9 + backend/src/modules/rfa/rfa.service.ts | 46 + .../transmittal/dto/search-transmittal.dto.ts | 2 +- frontend/app/(admin)/admin/numbering/page.tsx | 264 ++-- frontend/app/(admin)/admin/page.tsx | 5 + .../admin/workflows/[id]/edit/page.tsx | 296 ++-- frontend/app/(admin)/layout.tsx | 16 +- frontend/app/(dashboard)/search/page.tsx | 8 +- frontend/components/admin/sidebar.tsx | 4 +- frontend/components/auth/auth-sync.tsx | 6 +- .../components/correspondences/detail.tsx | 113 +- frontend/components/correspondences/form.tsx | 45 +- frontend/components/drawings/list.tsx | 9 +- frontend/components/layout/sidebar.tsx | 2 +- .../components/numbering/sequence-viewer.tsx | 110 +- .../components/numbering/template-editor.tsx | 193 +-- .../components/numbering/template-tester.tsx | 71 +- frontend/components/rfas/detail.tsx | 97 +- frontend/components/rfas/form.tsx | 32 +- frontend/components/rfas/list.tsx | 4 +- frontend/components/ui/alert.tsx | 59 + frontend/components/ui/dialog.tsx | 122 ++ frontend/components/workflows/dsl-editor.tsx | 104 +- .../components/workflows/visual-builder.tsx | 309 +++- frontend/hooks/use-correspondence.ts | 19 + frontend/hooks/use-master-data.ts | 10 +- frontend/lib/api/numbering.ts | 184 ++- frontend/lib/auth.ts | 5 + frontend/lib/stores/auth-store.ts | 2 +- frontend/types/correspondence.ts | 11 +- pnpm-lock.yaml | 44 +- specs/03-implementation/backend-guidelines.md | 38 +- .../03-implementation/frontend-guidelines.md | 10 +- .../TASK-BE-001-database-migrations.md | 2 +- .../TASK-BE-004-document-numbering.md | 135 +- specs/06-tasks/TASK-BE-006-workflow-engine.md | 2 +- specs/06-tasks/TASK-BE-007-rfa-module.md | 2 +- specs/06-tasks/TASK-BE-008-drawing-module.md | 2 +- .../TASK-BE-009-circulation-transmittal.md | 2 +- .../TASK-BE-010-search-elasticsearch.md | 44 +- .../TASK-BE-011-notification-audit.md | 2 +- .../TASK-BE-012-master-data-management.md | 2 +- specs/06-tasks/TASK-BE-013-user-management.md | 2 +- .../TASK-FE-011-workflow-config-ui.md | 10 +- .../TASK-FE-012-numbering-config-ui.md | 64 +- specs/06-tasks/backend-progress-report.md | 30 +- specs/07-database/data-dictionary-v1.5.1.md | 35 +- specs/07-database/lcbp3-v1.5.1-schema.sql | 28 +- specs/07-database/lcbp3-v1.5.1-seed.sql | 1299 ++++++++++++----- specs/07-database/permissions-seed-data.sql | 1067 ++++++++++++++ .../07-database/permissions-verification.sql | 276 ++++ ...20251208-TASK-BE-004-document-numbering.md | 1281 ++++++++++++++++ ...0251208-TASK-FE-012-numbering-config-ui.md | 537 +++++++ 64 files changed, 5956 insertions(+), 1256 deletions(-) create mode 100644 frontend/app/(admin)/admin/page.tsx create mode 100644 frontend/components/ui/alert.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 specs/07-database/permissions-seed-data.sql create mode 100644 specs/07-database/permissions-verification.sql create mode 100644 specs/09-history/20251208-TASK-BE-004-document-numbering.md create mode 100644 specs/09-history/20251208-TASK-FE-012-numbering-config-ui.md diff --git a/backend/src/common/guards/rbac.guard.ts b/backend/src/common/guards/rbac.guard.ts index 98c4297..73a66d1 100644 --- a/backend/src/common/guards/rbac.guard.ts +++ b/backend/src/common/guards/rbac.guard.ts @@ -34,9 +34,8 @@ export class RbacGuard implements CanActivate { } // 3. (สำคัญ) ดึงสิทธิ์ทั้งหมดของ User คนนี้จาก Database - // เราต้องเขียนฟังก์ชัน getUserPermissions ใน UserService เพิ่ม (เดี๋ยวพาทำ) const userPermissions = await this.userService.getUserPermissions( - user.userId + user.user_id // ✅ FIX: ใช้ user_id ตาม Entity field name ); // 4. ตรวจสอบว่ามีสิทธิ์ที่ต้องการไหม? (User ต้องมีครบทุกสิทธิ์) diff --git a/backend/src/common/interceptors/transform.interceptor.ts b/backend/src/common/interceptors/transform.interceptor.ts index b225d22..c9a899d 100644 --- a/backend/src/common/interceptors/transform.interceptor.ts +++ b/backend/src/common/interceptors/transform.interceptor.ts @@ -11,6 +11,7 @@ export interface Response { statusCode: number; message: string; data: T; + meta?: any; } @Injectable() @@ -19,14 +20,29 @@ export class TransformInterceptor { intercept( context: ExecutionContext, - next: CallHandler, + next: CallHandler ): Observable> { return next.handle().pipe( - map((data) => ({ - statusCode: context.switchToHttp().getResponse().statusCode, - message: data?.message || 'Success', // ถ้า data มี message ให้ใช้ ถ้าไม่มีใช้ 'Success' - data: data?.result || data, // รองรับกรณีส่ง object ที่มี key result มา - })), + map((data: any) => { + const response = context.switchToHttp().getResponse(); + + // Handle Pagination Response (Standardize) + // ถ้า data มี structure { data: [], meta: {} } ให้ unzip ออกมา + if (data && data.data && data.meta) { + return { + statusCode: response.statusCode, + message: data.message || 'Success', + data: data.data, + meta: data.meta, + }; + } + + return { + statusCode: response.statusCode, + message: data?.message || 'Success', + data: data?.result || data, + }; + }) ); } } diff --git a/backend/src/main.ts b/backend/src/main.ts index 93c7bb8..ab42e41 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -86,7 +86,7 @@ async function bootstrap() { // 🚀 7. Start Server const port = configService.get('PORT') || 3001; - await app.listen(port); + await app.listen(port, '0.0.0.0'); logger.log(`Application is running on: ${await app.getUrl()}/api`); logger.log(`Swagger UI is available at: ${await app.getUrl()}/docs`); diff --git a/backend/src/modules/auth/entities/role.entity.ts b/backend/src/modules/auth/entities/role.entity.ts index af7e891..7ce113e 100644 --- a/backend/src/modules/auth/entities/role.entity.ts +++ b/backend/src/modules/auth/entities/role.entity.ts @@ -5,28 +5,37 @@ import { ManyToMany, JoinTable, } from 'typeorm'; +import { BaseEntity } from '../../../common/entities/base.entity'; @Entity('permissions') -export class Permission { - @PrimaryGeneratedColumn() +export class Permission extends BaseEntity { + @PrimaryGeneratedColumn({ name: 'permission_id' }) id!: number; - @Column({ name: 'permission_code', length: 50, unique: true }) - permissionCode!: string; + @Column({ name: 'permission_name', length: 100, unique: true }) + permissionName!: string; @Column({ name: 'description', type: 'text', nullable: true }) description!: string; - @Column({ name: 'resource', length: 50 }) - resource!: string; + @Column({ name: 'module', length: 50, nullable: true }) + module?: string; - @Column({ name: 'action', length: 50 }) - action!: string; + @Column({ + name: 'scope_level', + type: 'enum', + enum: ['GLOBAL', 'ORG', 'PROJECT'], + nullable: true, + }) + scopeLevel?: 'GLOBAL' | 'ORG' | 'PROJECT'; + + @Column({ name: 'is_active', default: true, type: 'tinyint' }) + isActive!: boolean; } @Entity('roles') -export class Role { - @PrimaryGeneratedColumn() +export class Role extends BaseEntity { + @PrimaryGeneratedColumn({ name: 'role_id' }) id!: number; @Column({ name: 'role_name', length: 50, unique: true }) @@ -35,6 +44,16 @@ export class Role { @Column({ name: 'description', type: 'text', nullable: true }) description!: string; + @Column({ + type: 'enum', + enum: ['Global', 'Organization', 'Project', 'Contract'], + default: 'Global', + }) + scope!: 'Global' | 'Organization' | 'Project' | 'Contract'; + + @Column({ name: 'is_system', default: false }) + isSystem!: boolean; + @ManyToMany(() => Permission) @JoinTable({ name: 'role_permissions', diff --git a/backend/src/modules/correspondence/dto/search-correspondence.dto.ts b/backend/src/modules/correspondence/dto/search-correspondence.dto.ts index 7b49ffb..bda04cf 100644 --- a/backend/src/modules/correspondence/dto/search-correspondence.dto.ts +++ b/backend/src/modules/correspondence/dto/search-correspondence.dto.ts @@ -21,4 +21,15 @@ export class SearchCorrespondenceDto { @Type(() => Number) @IsInt() statusId?: number; + + // Pagination + @IsOptional() + @Type(() => Number) + @IsInt() + page?: number; + + @IsOptional() + @Type(() => Number) + @IsInt() + limit?: number; } diff --git a/backend/src/modules/drawing/contract-drawing.controller.ts b/backend/src/modules/drawing/contract-drawing.controller.ts index 78aab1a..2e5a752 100644 --- a/backend/src/modules/drawing/contract-drawing.controller.ts +++ b/backend/src/modules/drawing/contract-drawing.controller.ts @@ -29,15 +29,17 @@ import { User } from '../user/entities/user.entity'; @Controller('drawings/contract') export class ContractDrawingController { constructor( - private readonly contractDrawingService: ContractDrawingService, + private readonly contractDrawingService: ContractDrawingService ) {} + // Force rebuild for DTO changes + @Post() @ApiOperation({ summary: 'Create new Contract Drawing' }) @RequirePermission('drawing.create') // สิทธิ์ ID 39: สร้าง/แก้ไขข้อมูลแบบ create( @Body() createDto: CreateContractDrawingDto, - @CurrentUser() user: User, + @CurrentUser() user: User ) { return this.contractDrawingService.create(createDto, user); } @@ -62,7 +64,7 @@ export class ContractDrawingController { update( @Param('id', ParseIntPipe) id: number, @Body() updateDto: UpdateContractDrawingDto, - @CurrentUser() user: User, + @CurrentUser() user: User ) { return this.contractDrawingService.update(id, updateDto, user); } diff --git a/backend/src/modules/drawing/contract-drawing.service.ts b/backend/src/modules/drawing/contract-drawing.service.ts index f096774..7d48ea3 100644 --- a/backend/src/modules/drawing/contract-drawing.service.ts +++ b/backend/src/modules/drawing/contract-drawing.service.ts @@ -31,7 +31,7 @@ export class ContractDrawingService { @InjectRepository(Attachment) private attachmentRepo: Repository, private fileStorageService: FileStorageService, - private dataSource: DataSource, + private dataSource: DataSource ) {} /** @@ -51,7 +51,7 @@ export class ContractDrawingService { if (exists) { throw new ConflictException( - `Contract Drawing No. "${createDto.contractDrawingNo}" already exists in this project.`, + `Contract Drawing No. "${createDto.contractDrawingNo}" already exists in this project.` ); } @@ -85,7 +85,7 @@ export class ContractDrawingService { if (createDto.attachmentIds?.length) { // ✅ FIX TS2345: แปลง number[] เป็น string[] ก่อนส่ง await this.fileStorageService.commit( - createDto.attachmentIds.map(String), + createDto.attachmentIds.map(String) ); } @@ -95,7 +95,7 @@ export class ContractDrawingService { await queryRunner.rollbackTransaction(); // ✅ FIX TS18046: Cast err เป็น Error this.logger.error( - `Failed to create contract drawing: ${(err as Error).message}`, + `Failed to create contract drawing: ${(err as Error).message}` ); throw err; } finally { @@ -114,7 +114,7 @@ export class ContractDrawingService { subCategoryId, search, page = 1, - pageSize = 20, + limit = 20, } = searchDto; const query = this.drawingRepo @@ -143,14 +143,14 @@ export class ContractDrawingService { qb.where('drawing.contractDrawingNo LIKE :search', { search: `%${search}%`, }).orWhere('drawing.title LIKE :search', { search: `%${search}%` }); - }), + }) ); } query.orderBy('drawing.contractDrawingNo', 'ASC'); - const skip = (page - 1) * pageSize; - query.skip(skip).take(pageSize); + const skip = (page - 1) * limit; + query.skip(skip).take(limit); const [items, total] = await query.getManyAndCount(); @@ -159,8 +159,8 @@ export class ContractDrawingService { meta: { total, page, - pageSize, - totalPages: Math.ceil(total / pageSize), + limit, + totalPages: Math.ceil(total / limit), }, }; } @@ -213,7 +213,7 @@ export class ContractDrawingService { // Commit new files // ✅ FIX TS2345: แปลง number[] เป็น string[] ก่อนส่ง await this.fileStorageService.commit( - updateDto.attachmentIds.map(String), + updateDto.attachmentIds.map(String) ); } @@ -225,7 +225,7 @@ export class ContractDrawingService { await queryRunner.rollbackTransaction(); // ✅ FIX TS18046: Cast err เป็น Error (Optional: Added logger here too for consistency) this.logger.error( - `Failed to update contract drawing: ${(err as Error).message}`, + `Failed to update contract drawing: ${(err as Error).message}` ); throw err; } finally { diff --git a/backend/src/modules/drawing/dto/search-contract-drawing.dto.ts b/backend/src/modules/drawing/dto/search-contract-drawing.dto.ts index 0d70dc7..b4e96bb 100644 --- a/backend/src/modules/drawing/dto/search-contract-drawing.dto.ts +++ b/backend/src/modules/drawing/dto/search-contract-drawing.dto.ts @@ -29,5 +29,9 @@ export class SearchContractDrawingDto { @IsOptional() @IsInt() @Type(() => Number) - pageSize: number = 20; // มีค่า Default ไม่ต้องใส่ ! หรือ ? + limit: number = 20; + + @IsOptional() + @IsString() + type?: string; } diff --git a/backend/src/modules/drawing/dto/search-shop-drawing.dto.ts b/backend/src/modules/drawing/dto/search-shop-drawing.dto.ts index 9ad1324..50de93e 100644 --- a/backend/src/modules/drawing/dto/search-shop-drawing.dto.ts +++ b/backend/src/modules/drawing/dto/search-shop-drawing.dto.ts @@ -28,5 +28,5 @@ export class SearchShopDrawingDto { @IsOptional() @IsInt() @Type(() => Number) - pageSize: number = 20; // มีค่า Default + limit: number = 20; // มีค่า Default } diff --git a/backend/src/modules/drawing/shop-drawing.service.ts b/backend/src/modules/drawing/shop-drawing.service.ts index b6a96fd..7e8cdec 100644 --- a/backend/src/modules/drawing/shop-drawing.service.ts +++ b/backend/src/modules/drawing/shop-drawing.service.ts @@ -208,10 +208,10 @@ export class ShopDrawingService { const { projectId, mainCategoryId, - subCategoryId, + // subCategoryId, // Unused search, page = 1, - pageSize = 20, + limit = 20, } = searchDto; const query = this.shopDrawingRepo @@ -225,10 +225,6 @@ export class ShopDrawingService { query.andWhere('sd.mainCategoryId = :mainCategoryId', { mainCategoryId }); } - if (subCategoryId) { - query.andWhere('sd.subCategoryId = :subCategoryId', { subCategoryId }); - } - if (search) { query.andWhere( new Brackets((qb) => { @@ -241,8 +237,8 @@ export class ShopDrawingService { query.orderBy('sd.updatedAt', 'DESC'); - const skip = (page - 1) * pageSize; - query.skip(skip).take(pageSize); + const skip = (page - 1) * limit; + query.skip(skip).take(limit); const [items, total] = await query.getManyAndCount(); @@ -262,8 +258,8 @@ export class ShopDrawingService { meta: { total, page, - pageSize, - totalPages: Math.ceil(total / pageSize), + limit, + totalPages: Math.ceil(total / limit), }, }; } diff --git a/backend/src/modules/rfa/dto/search-rfa.dto.ts b/backend/src/modules/rfa/dto/search-rfa.dto.ts index 6ef040c..8d9d782 100644 --- a/backend/src/modules/rfa/dto/search-rfa.dto.ts +++ b/backend/src/modules/rfa/dto/search-rfa.dto.ts @@ -29,5 +29,5 @@ export class SearchRfaDto { @IsOptional() @IsInt() @Type(() => Number) - pageSize: number = 20; + limit: number = 20; } diff --git a/backend/src/modules/rfa/rfa.controller.ts b/backend/src/modules/rfa/rfa.controller.ts index af7b2e5..09bf681 100644 --- a/backend/src/modules/rfa/rfa.controller.ts +++ b/backend/src/modules/rfa/rfa.controller.ts @@ -6,6 +6,7 @@ import { Param, ParseIntPipe, Post, + Query, UseGuards, } from '@nestjs/common'; import { @@ -79,6 +80,14 @@ export class RfaController { return this.rfaService.processAction(id, actionDto, user); } + @Get() + @ApiOperation({ summary: 'List all RFAs with pagination' }) + @ApiResponse({ status: 200, description: 'List of RFAs' }) + @RequirePermission('document.view') + findAll(@Query() query: any) { + return this.rfaService.findAll(query); + } + @Get(':id') @ApiOperation({ summary: 'Get RFA details with revisions and items' }) @ApiParam({ name: 'id', description: 'RFA ID' }) diff --git a/backend/src/modules/rfa/rfa.service.ts b/backend/src/modules/rfa/rfa.service.ts index 33b9080..ad9500c 100644 --- a/backend/src/modules/rfa/rfa.service.ts +++ b/backend/src/modules/rfa/rfa.service.ts @@ -230,6 +230,52 @@ export class RfaService { // ... (ส่วน findOne, submit, processAction คงเดิมจากไฟล์ที่แนบมา แค่ปรับปรุงเล็กน้อยตาม Context) ... + async findAll(query: any) { + const { page = 1, limit = 20, projectId, status, search } = query; + const skip = (page - 1) * limit; + + // Fix: Start query from Rfa entity instead of Correspondence, + // because Correspondence has no 'rfas' relation. + // [Force Rebuild] + const queryBuilder = this.rfaRepo + .createQueryBuilder('rfa') + .leftJoinAndSelect('rfa.revisions', 'rev') + .leftJoinAndSelect('rev.correspondence', 'corr') + .leftJoinAndSelect('rev.statusCode', 'status') + .where('rev.isCurrent = :isCurrent', { isCurrent: true }); + + if (projectId) { + queryBuilder.andWhere('corr.projectId = :projectId', { projectId }); + } + + if (status) { + queryBuilder.andWhere('status.statusCode = :status', { status }); + } + + if (search) { + queryBuilder.andWhere( + '(corr.correspondenceNumber LIKE :search OR rev.title LIKE :search)', + { search: `%${search}%` } + ); + } + + const [items, total] = await queryBuilder + .orderBy('corr.createdAt', 'DESC') + .skip(skip) + .take(limit) + .getManyAndCount(); + + return { + data: items, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }; + } + async findOne(id: number) { const rfa = await this.rfaRepo.findOne({ where: { id }, diff --git a/backend/src/modules/transmittal/dto/search-transmittal.dto.ts b/backend/src/modules/transmittal/dto/search-transmittal.dto.ts index 2cc5ca5..1973de8 100644 --- a/backend/src/modules/transmittal/dto/search-transmittal.dto.ts +++ b/backend/src/modules/transmittal/dto/search-transmittal.dto.ts @@ -30,5 +30,5 @@ export class SearchTransmittalDto { @IsOptional() @IsInt() @Type(() => Number) - pageSize: number = 20; + limit: number = 20; } diff --git a/frontend/app/(admin)/admin/numbering/page.tsx b/frontend/app/(admin)/admin/numbering/page.tsx index 3e51ad9..337c40d 100644 --- a/frontend/app/(admin)/admin/numbering/page.tsx +++ b/frontend/app/(admin)/admin/numbering/page.tsx @@ -1,135 +1,193 @@ -"use client"; +'use client'; -import { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Card } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Plus, Edit, Eye, Loader2 } from "lucide-react"; -import Link from "next/link"; -import { NumberingTemplate } from "@/types/numbering"; -import { numberingApi } from "@/lib/api/numbering"; -import { TemplateTester } from "@/components/numbering/template-tester"; +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Plus, Edit, Play } from 'lucide-react'; +import { numberingApi, NumberingTemplate } from '@/lib/api/numbering'; +import { TemplateEditor } from '@/components/numbering/template-editor'; +import { SequenceViewer } from '@/components/numbering/sequence-viewer'; +import { TemplateTester } from '@/components/numbering/template-tester'; +import { toast } from 'sonner'; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +const PROJECTS = [ + { id: '1', name: 'LCBP3' }, + { id: '2', name: 'LCBP3-Maintenance' }, +]; export default function NumberingPage() { + const [selectedProjectId, setSelectedProjectId] = useState("1"); const [templates, setTemplates] = useState([]); - const [loading, setLoading] = useState(true); - const [testerOpen, setTesterOpen] = useState(false); - const [selectedTemplate, setSelectedTemplate] = useState(null); + const [, setLoading] = useState(true); - useEffect(() => { - const fetchTemplates = async () => { - setLoading(true); - try { + // View states + const [isEditing, setIsEditing] = useState(false); + const [activeTemplate, setActiveTemplate] = useState(undefined); + const [isTesting, setIsTesting] = useState(false); + const [testTemplate, setTestTemplate] = useState(null); + + const selectedProjectName = PROJECTS.find(p => p.id === selectedProjectId)?.name || 'Unknown Project'; + + const loadTemplates = async () => { + setLoading(true); + try { const data = await numberingApi.getTemplates(); setTemplates(data); - } catch (error) { - console.error("Failed to fetch templates", error); - } finally { + } catch { + toast.error("Failed to load templates"); + } finally { setLoading(false); - } - }; + } + }; - fetchTemplates(); + useEffect(() => { + loadTemplates(); }, []); - const handleTest = (template: NumberingTemplate) => { - setSelectedTemplate(template); - setTesterOpen(true); + const handleEdit = (template?: NumberingTemplate) => { + setActiveTemplate(template); + setIsEditing(true); }; + const handleSave = async (data: Partial) => { + try { + await numberingApi.saveTemplate(data); + toast.success(data.template_id ? "Template updated" : "Template created"); + setIsEditing(false); + loadTemplates(); + } catch { + toast.error("Failed to save template"); + } + }; + + const handleTest = (template: NumberingTemplate) => { + setTestTemplate(template); + setIsTesting(true); + }; + + if (isEditing) { + return ( +
+ setIsEditing(false)} + /> +
+ ); + } + return (
-

- Document Numbering Configuration +

+ Document Numbering

- Manage document numbering templates and sequences + Manage numbering templates and sequences

- - - +
+ + +
- {loading ? ( -
- -
- ) : ( -
- {templates.map((template) => ( - -
-
-
-

- {template.document_type_name} -

- {template.discipline_code || "All"} - - {template.is_active ? "Active" : "Inactive"} - -
+
+
+

Templates - {selectedProjectName}

+
+ {templates + .filter(t => !t.project_id || t.project_id === Number(selectedProjectId)) // Show all if no project_id (legacy mock), or match + .map((template) => ( + +
+
+
+

+ {template.document_type_name} +

+ + {PROJECTS.find(p => p.id === template.project_id?.toString())?.name || selectedProjectName} + + {template.discipline_code && {template.discipline_code}} + + {template.is_active ? 'Active' : 'Inactive'} + +
-
- {template.template_format} -
+
+ {template.template_format} +
-
-
- Example: - - {template.example_number} - +
+
+ Example: + + {template.example_number} + +
+
+ Reset: + + {template.reset_annually ? 'Annually' : 'Never'} + +
+
-
- Current Sequence: - - {template.current_number} - -
-
- Annual Reset: - - {template.reset_annually ? "Yes" : "No"} - -
-
- Padding: - - {template.padding_length} digits - -
-
-
-
- - - - -
-
-
- ))} -
- )} +
+ + +
+
+ + ))} +
+
+ +
+ {/* Sequence Viewer Sidebar */} + +
+
); diff --git a/frontend/app/(admin)/admin/page.tsx b/frontend/app/(admin)/admin/page.tsx new file mode 100644 index 0000000..55cff26 --- /dev/null +++ b/frontend/app/(admin)/admin/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation'; + +export default function AdminPage() { + redirect('/admin/workflows'); +} diff --git a/frontend/app/(admin)/admin/workflows/[id]/edit/page.tsx b/frontend/app/(admin)/admin/workflows/[id]/edit/page.tsx index 8667d77..6281c91 100644 --- a/frontend/app/(admin)/admin/workflows/[id]/edit/page.tsx +++ b/frontend/app/(admin)/admin/workflows/[id]/edit/page.tsx @@ -1,164 +1,206 @@ -"use client"; +'use client'; -import { useState, useEffect } from "react"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { DSLEditor } from "@/components/workflows/dsl-editor"; -import { VisualWorkflowBuilder } from "@/components/workflows/visual-builder"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { Card } from "@/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { workflowApi } from "@/lib/api/workflows"; -import { WorkflowType } from "@/types/workflow"; -import { useRouter } from "next/navigation"; -import { Loader2 } from "lucide-react"; +import { useState, useEffect } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { DSLEditor } from '@/components/workflows/dsl-editor'; +import { VisualWorkflowBuilder } from '@/components/workflows/visual-builder'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Card } from '@/components/ui/card'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { workflowApi } from '@/lib/api/workflows'; +import { Workflow, CreateWorkflowDto } from '@/types/workflow'; +import { toast } from 'sonner'; +import { Save, ArrowLeft, Loader2 } from 'lucide-react'; +import Link from 'next/link'; -export default function WorkflowEditPage({ params }: { params: { id: string } }) { +export default function WorkflowEditPage() { + const params = useParams(); const router = useRouter(); - const [loading, setLoading] = useState(true); + const id = params?.id === 'new' ? null : Number(params?.id); + + const [loading, setLoading] = useState(!!id); const [saving, setSaving] = useState(false); - const [workflowData, setWorkflowData] = useState({ - workflow_name: "", - description: "", - workflow_type: "CORRESPONDENCE" as WorkflowType, - dsl_definition: "", + const [workflowData, setWorkflowData] = useState>({ + workflow_name: '', + description: '', + workflow_type: 'CORRESPONDENCE', + dsl_definition: 'name: New Workflow\nversion: 1.0\nsteps: []', + is_active: true, }); useEffect(() => { - const fetchWorkflow = async () => { - setLoading(true); - try { - const data = await workflowApi.getWorkflow(parseInt(params.id)); - if (data) { - setWorkflowData({ - workflow_name: data.workflow_name, - description: data.description, - workflow_type: data.workflow_type, - dsl_definition: data.dsl_definition, - }); - } - } catch (error) { - console.error("Failed to fetch workflow", error); - } finally { - setLoading(false); - } - }; - - fetchWorkflow(); - }, [params.id]); + if (id) { + const fetchWorkflow = async () => { + try { + const data = await workflowApi.getWorkflow(id); + if (data) { + setWorkflowData(data); + } else { + toast.error("Workflow not found"); + router.push('/admin/workflows'); + } + } catch (error) { + toast.error("Failed to load workflow"); + console.error(error); + } finally { + setLoading(false); + } + }; + fetchWorkflow(); + } + }, [id, router]); const handleSave = async () => { + if (!workflowData.workflow_name) { + toast.error("Workflow name is required"); + return; + } + setSaving(true); try { - await workflowApi.updateWorkflow(parseInt(params.id), workflowData); - router.push("/admin/workflows"); + const dto: CreateWorkflowDto = { + workflow_name: workflowData.workflow_name || '', + description: workflowData.description || '', + workflow_type: workflowData.workflow_type || 'CORRESPONDENCE', + dsl_definition: workflowData.dsl_definition || '', + }; + + if (id) { + await workflowApi.updateWorkflow(id, dto); + toast.success("Workflow updated successfully"); + } else { + await workflowApi.createWorkflow(dto); + toast.success("Workflow created successfully"); + router.push('/admin/workflows'); + } } catch (error) { - console.error("Failed to save workflow", error); - alert("Failed to save workflow"); + toast.error("Failed to save workflow"); + console.error(error); } finally { - setSaving(false); + setSaving(false); } }; if (loading) { - return ( -
- -
- ); + return ( +
+ +
+ ); } return ( -
+
-

Edit Workflow

+
+ + + +
+

{id ? 'Edit Workflow' : 'New Workflow'}

+

{id ? `Version ${workflowData.version}` : 'Create a new workflow definition'}

+
+
- + + +
- -
-
- - - setWorkflowData({ - ...workflowData, - workflow_name: e.target.value, - }) - } - /> -
+
+
+ +
+
+ + + setWorkflowData({ + ...workflowData, + workflow_name: e.target.value, + }) + } + placeholder="e.g. Standard RFA Workflow" + /> +
-
- -