From 397c46bc4e4d951e88a6a35c87475116187972b9 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 20 Feb 2026 15:04:02 +0700 Subject: [PATCH] 260220:1504 20260220 TASK-BEFE-001 Refactor by ADR-014 --- .agent/rules/00-project-specs.md | 10 +- .agent/rules/01-code-execution.md | 9 +- .gemini/GEMINI.md | 4 +- .../admin/doc-control/numbering/page.tsx | 296 ++++++++---------- .../reference/disciplines/page.tsx | 103 +++--- .../doc-control/reference/rfa-types/page.tsx | 97 +++--- .../doc-control/workflows/[id]/edit/page.tsx | 250 +++++++-------- .../admin/doc-control/workflows/page.tsx | 53 ++-- frontend/hooks/use-numbering.ts | 83 +++++ frontend/hooks/use-reference-data.ts | 113 +++++++ frontend/hooks/use-workflows.ts | 78 +++++ frontend/tsc_errors.txt | 26 ++ specs/00-overview/README.md | 17 +- specs/02-architecture/README.md | 12 +- .../05-decisions/ADR-014-state-management.md | 50 +-- 15 files changed, 709 insertions(+), 492 deletions(-) create mode 100644 frontend/hooks/use-numbering.ts create mode 100644 frontend/hooks/use-reference-data.ts create mode 100644 frontend/hooks/use-workflows.ts create mode 100644 frontend/tsc_errors.txt diff --git a/.agent/rules/00-project-specs.md b/.agent/rules/00-project-specs.md index 7f156c2..ca4d325 100644 --- a/.agent/rules/00-project-specs.md +++ b/.agent/rules/00-project-specs.md @@ -30,10 +30,10 @@ Before generating code or planning a solution, you MUST conceptually load the co - *Action:* Adhere to the defined system design. - *Crucial:* Check `specs/05-decisions/` (ADRs) to ensure you do not violate previously agreed-upon technical decisions. -5. **💾 DATABASE & SCHEMA (`specs/07-databasee/`)** +5. **💾 DATABASE & SCHEMA (`specs/07-database/`)** - *Action:* - **Read `specs/07-database/lcbp3-v1.7.0-schema.sql`** (or relevant `.sql` files) for exact table structures and constraints. - **Consult `specs/07-database/data-dictionary-v1.7.0.md`** for field meanings and business rules. - - **Check `specs/07-database/lcbp3-v1.7.0-seed.sql`** to understand initial data states. + - **Check `specs/07-database/lcbp3-v1.7.0-seed-basic.sql`** to understand initial data states. - **Check `specs/07-database/lcbp3-v1.7.0-seed-permissions.sql`** to understand initial permissions states. - *Constraint:* NEVER invent table names or columns. Use ONLY what is defined here. @@ -43,6 +43,10 @@ Before generating code or planning a solution, you MUST conceptually load the co 7. **🚀 OPERATIONS (`specs/04-operations/`)** - *Action:* Ensure deployability and configuration compliance. +8. **🏗️ INFRASTRUCTURE (`specs/08-infrastructure/`)** + - *Action:* Review Docker Compose configurations, network diagrams, monitoring setup, and security zones. + - *Constraint:* Ensure deployment paths, port mappings, and volume mounts are consistent with this documentation. + ## Execution Rules ### 1. Citation Requirement @@ -60,4 +64,4 @@ When proposing a change or writing code, you must explicitly reference the sourc ### 4. Data Migration - Do not migrate. The schema can be modified directly. ---- +--- \ No newline at end of file diff --git a/.agent/rules/01-code-execution.md b/.agent/rules/01-code-execution.md index b24d4c2..0897fdb 100644 --- a/.agent/rules/01-code-execution.md +++ b/.agent/rules/01-code-execution.md @@ -1,20 +1,15 @@ --- trigger: always_on ---- - ---- - description: Control which shell commands the agent may run automatically. allowAuto: ["pnpm test:watch", "pnpm test:debug", "pnpm test:e2e", "git status"] denyAuto: ["rm -rf", "Remove-Item", "git push --force", "curl | bash"] alwaysReview: true scopes: ["backend/src/**", "backend/test/**", "frontend/app/**"] - --- # Execution Rules - Only auto-execute commands that are explicitly listed in `allowAuto`. - Commands in denyAuto must always be blocked, even if manually requested. -- All shell operations that create, modify, or delete files in `backend/src/` or `backend/test/` or `frontend/app/`require human review. -- Alert if environment variables related to DB connection or secrets would be displayed or logged. +- All shell operations that create, modify, or delete files in `backend/src/` or `backend/test/` or `frontend/app/` require human review. +- Alert if environment variables related to DB connection or secrets would be displayed or logged. \ No newline at end of file diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index 2cf1892..88978c7 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -18,8 +18,8 @@ This is **LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**. ## 💻 Tech Stack & Constraints -- **Backend:** NestJS (Modular Architecture), TypeORM, MariaDB 10.11, Redis 7.2 (BullMQ), Elasticsearch 8.11, JWT (JSON Web Tokens), CASL (4-Level RBAC). -- **Frontend:** Next.js 14+ (App Router), Tailwind CSS, Shadcn/UI, React Context / Zustand, React Hook Form + Zod, Axios. +- **Backend:** NestJS (Modular Architecture), TypeORM, MariaDB 11.8, Redis 7.2 (BullMQ), Elasticsearch 8.11, JWT (JSON Web Tokens), CASL (4-Level RBAC). +- **Frontend:** Next.js 14+ (App Router), Tailwind CSS, Shadcn/UI, TanStack Query (Server State), Zustand (Client State), React Hook Form + Zod, Axios. - **Language:** TypeScript (Strict Mode). **NO `any` types allowed.** ## 🛡️ Security & Integrity Rules diff --git a/frontend/app/(admin)/admin/doc-control/numbering/page.tsx b/frontend/app/(admin)/admin/doc-control/numbering/page.tsx index 05a75af..8f7d8be 100644 --- a/frontend/app/(admin)/admin/doc-control/numbering/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/numbering/page.tsx @@ -6,21 +6,15 @@ import { Card } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Plus, Edit, Play } from 'lucide-react'; -import { numberingApi, NumberingTemplate } from '@/lib/api/numbering'; +import { NumberingTemplate } from '@/lib/api/numbering'; +import { useTemplates, useSaveTemplate } from '@/hooks/use-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"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { useProjects, useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data'; - import { ManualOverrideForm } from '@/components/numbering/manual-override-form'; import { MetricsDashboard } from '@/components/numbering/metrics-dashboard'; import { AuditLogsTable } from '@/components/numbering/audit-logs-table'; @@ -28,13 +22,10 @@ import { VoidReplaceForm } from '@/components/numbering/void-replace-form'; import { CancelNumberForm } from '@/components/numbering/cancel-number-form'; import { BulkImportForm } from '@/components/numbering/bulk-import-form'; - export default function NumberingPage() { const { data: projects = [] } = useProjects(); - const [selectedProjectId, setSelectedProjectId] = useState("1"); - const [activeTab, setActiveTab] = useState("templates"); - - const [templates, setTemplates] = useState([]); + const [selectedProjectId, setSelectedProjectId] = useState('1'); + const [activeTab, setActiveTab] = useState('templates'); // View states const [isEditing, setIsEditing] = useState(false); @@ -42,7 +33,9 @@ export default function NumberingPage() { const [isTesting, setIsTesting] = useState(false); const [testTemplate, setTestTemplate] = useState(null); - const selectedProjectName = projects.find((p: { id: number; projectName: string }) => p.id.toString() === selectedProjectId)?.projectName || 'Unknown Project'; + const selectedProjectName = + projects.find((p: { id: number; projectName: string }) => p.id.toString() === selectedProjectId)?.projectName || + 'Unknown Project'; // Master Data const { data: correspondenceTypes = [] } = useCorrespondenceTypes(); @@ -50,189 +43,168 @@ export default function NumberingPage() { const contractId = contracts[0]?.id; const { data: disciplines = [] } = useDisciplines(contractId); - const loadTemplates = async () => { - try { - const response = await numberingApi.getTemplates(); - // Handle wrapped response { data: [...] } or direct array - const data = Array.isArray(response) ? response : (response as { data?: NumberingTemplate[] })?.data ?? []; - setTemplates(data); - } catch { - toast.error("Failed to load templates"); - setTemplates([]); - } - }; + const { data: templateResponse, isLoading: isLoadingTemplates } = useTemplates(); + const saveTemplateMutation = useSaveTemplate(); - useEffect(() => { - loadTemplates(); - }, []); + // Extract templates array from response (handles both direct array and { data: array } formats) + const templates: NumberingTemplate[] = Array.isArray(templateResponse) + ? templateResponse + : ((templateResponse as any)?.data ?? []); const handleEdit = (template?: NumberingTemplate) => { - setActiveTemplate(template); - setIsEditing(true); + setActiveTemplate(template); + setIsEditing(true); }; const handleSave = async (data: Partial) => { - try { - await numberingApi.saveTemplate(data); - toast.success(data.id ? "Template updated" : "Template created"); - setIsEditing(false); - loadTemplates(); - } catch { - toast.error("Failed to save template"); - } + try { + await saveTemplateMutation.mutateAsync(data); + toast.success(data.id ? 'Template updated' : 'Template created'); + setIsEditing(false); + } catch { + toast.error('Failed to save template'); + } }; const handleTest = (template: NumberingTemplate) => { - setTestTemplate(template); - setIsTesting(true); + setTestTemplate(template); + setIsTesting(true); }; - - if (isEditing) { - return ( -
- setIsEditing(false)} - /> -
- ); + return ( +
+ setIsEditing(false)} + /> +
+ ); } return (
-

- Document Numbering -

-

- Manage numbering templates, audit logs, and tools -

+

Document Numbering

+

Manage numbering templates, audit logs, and tools

- +
- - Templates - Metrics & Audit - Admin Tools - + + Templates + Metrics & Audit + Admin Tools + - -
- -
+ +
+ +
-
-
-
- {templates - .filter(t => !t.projectId || t.projectId === Number(selectedProjectId)) - .map((template) => ( - -
-
-
-

- {template.correspondenceType?.typeName || 'Default Format'} -

- - {template.project?.projectCode || selectedProjectName} - - {template.description && {template.description}} -
+
+
+
+ {templates + .filter((t) => !t.projectId || t.projectId === Number(selectedProjectId)) + .map((template) => ( + +
+
+
+

+ {template.correspondenceType?.typeName || 'Default Format'} +

+ + {template.project?.projectCode || selectedProjectName} + + {template.description && {template.description}} +
-
- {template.formatTemplate} -
+
+ {template.formatTemplate} +
-
-
- Type Code: - - {template.correspondenceType?.typeCode || 'DEFAULT'} - -
-
- Reset: - - {template.resetSequenceYearly ? 'Annually' : 'Continuous'} - -
-
+
+
+ Type Code: + + {template.correspondenceType?.typeCode || 'DEFAULT'} +
- -
- - +
+ Reset: + {template.resetSequenceYearly ? 'Annually' : 'Continuous'}
-
- - ))} -
-
+
+
-
- -
+
+ + +
+
+ + ))}
- +
- - -
-

Audit Logs

- -
-
+
+ +
+
+ - -
- - - -
- -
-
-
+ + +
+

Audit Logs

+ +
+
+ + +
+ + + +
+ +
+
+
- +
); } diff --git a/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx b/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx index ee8fa68..c212d5a 100644 --- a/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx @@ -1,64 +1,43 @@ -"use client"; +'use client'; -import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table"; -import { masterDataService } from "@/lib/services/master-data.service"; -import { contractService } from "@/lib/services/contract.service"; -import { ColumnDef } from "@tanstack/react-table"; -import { useState, useEffect } from "react"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; +import { GenericCrudTable } from '@/components/admin/reference/generic-crud-table'; +import { masterDataService } from '@/lib/services/master-data.service'; +import { useContracts } from '@/hooks/use-master-data'; +import { ColumnDef } from '@tanstack/react-table'; +import { useState } from 'react'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; export default function DisciplinesPage() { - const [contracts, setContracts] = useState([]); - const [selectedContractId, setSelectedContractId] = useState( - null - ); + const [selectedContractId, setSelectedContractId] = useState(null); - useEffect(() => { - // Fetch contracts for filter and form options - contractService.getAll().then((data) => { - setContracts(Array.isArray(data) ? data : []); - }).catch(err => { - console.error("Failed to load contracts:", err); - setContracts([]); - }); - }, []); + const { data: contractsData = [] } = useContracts(); + // Ensure we consistently use an array + const contracts = Array.isArray(contractsData) ? contractsData : []; const columns: ColumnDef[] = [ { - accessorKey: "disciplineCode", - header: "Code", - cell: ({ row }) => ( - - {row.getValue("disciplineCode")} - - ), + accessorKey: 'disciplineCode', + header: 'Code', + cell: ({ row }) => {row.getValue('disciplineCode')}, }, { - accessorKey: "codeNameTh", - header: "Name (TH)", + accessorKey: 'codeNameTh', + header: 'Name (TH)', }, { - accessorKey: "codeNameEn", - header: "Name (EN)", + accessorKey: 'codeNameEn', + header: 'Name (EN)', }, { - accessorKey: "isActive", - header: "Status", + accessorKey: 'isActive', + header: 'Status', cell: ({ row }) => ( - {row.getValue("isActive") ? "Active" : "Inactive"} + {row.getValue('isActive') ? 'Active' : 'Inactive'} ), }, @@ -75,23 +54,17 @@ export default function DisciplinesPage() { entityName="Discipline" title="Disciplines Management" description="Manage system disciplines (e.g., ARCH, STR, MEC)" - queryKey={["disciplines", selectedContractId ?? "all"]} - fetchFn={() => - masterDataService.getDisciplines( - selectedContractId ? parseInt(selectedContractId) : undefined - ) - } + queryKey={['disciplines', selectedContractId ?? 'all']} + fetchFn={() => masterDataService.getDisciplines(selectedContractId ? parseInt(selectedContractId) : undefined)} createFn={(data) => masterDataService.createDiscipline(data)} - updateFn={(id, data) => Promise.reject("Not implemented yet")} // Update endpoint needs to be verified/added if missing + updateFn={(id, data) => Promise.reject('Not implemented yet')} // Update endpoint needs to be verified/added if missing deleteFn={(id) => masterDataService.deleteDiscipline(id)} columns={columns} filters={
- setSelectedContractId(val === "all" ? null : val) - } + value={selectedContractId || 'all'} + onValueChange={(val) => setSelectedContractId(val === 'all' ? null : val)} > @@ -110,17 +85,17 @@ export default function RfaTypesPage() { } fields={[ { - name: "contractId", - label: "Contract", - type: "select", + name: 'contractId', + label: 'Contract', + type: 'select', required: true, options: contractOptions, }, - { name: "typeCode", label: "Code", type: "text", required: true }, - { name: "typeNameTh", label: "Name (TH)", type: "text", required: true }, - { name: "typeNameEn", label: "Name (EN)", type: "text" }, - { name: "remark", label: "Remark", type: "textarea" }, - { name: "isActive", label: "Active", type: "checkbox" }, + { name: 'typeCode', label: 'Code', type: 'text', required: true }, + { name: 'typeNameTh', label: 'Name (TH)', type: 'text', required: true }, + { name: 'typeNameEn', label: 'Name (EN)', type: 'text' }, + { name: 'remark', label: 'Remark', type: 'textarea' }, + { name: 'isActive', label: 'Active', type: 'checkbox' }, ]} />
diff --git a/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx b/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx index dbbae88..664383a 100644 --- a/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/workflows/[id]/edit/page.tsx @@ -11,8 +11,9 @@ 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 { useWorkflowDefinition, useCreateWorkflowDefinition, useUpdateWorkflowDefinition } from '@/hooks/use-workflows'; +import { Workflow } from '@/types/workflow'; +import { CreateWorkflowDefinitionDto } from '@/types/dto/workflow-engine/workflow-engine.dto'; import { toast } from 'sonner'; import { Save, ArrowLeft, Loader2 } from 'lucide-react'; import Link from 'next/link'; @@ -22,8 +23,6 @@ export default function WorkflowEditPage() { const router = useRouter(); const id = params?.id === 'new' ? null : Number(params?.id); - const [loading, setLoading] = useState(!!id); - const [saving, setSaving] = useState(false); const [workflowData, setWorkflowData] = useState>({ workflowName: '', description: '', @@ -32,84 +31,77 @@ export default function WorkflowEditPage() { isActive: true, }); + const { data: fetchedWorkflow, isLoading: loadingWorkflow } = useWorkflowDefinition(id as number); + const createMutation = useCreateWorkflowDefinition(); + const updateMutation = useUpdateWorkflowDefinition(); + useEffect(() => { - 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(); + if (fetchedWorkflow) { + setWorkflowData(fetchedWorkflow); } - }, [id, router]); + }, [fetchedWorkflow]); + + const loading = (!!id && loadingWorkflow) || createMutation.isPending || updateMutation.isPending; + const saving = createMutation.isPending || updateMutation.isPending; const handleSave = async () => { if (!workflowData.workflowName) { - toast.error("Workflow name is required"); - return; + toast.error('Workflow name is required'); + return; } - setSaving(true); try { - const dto: CreateWorkflowDto = { - workflowName: workflowData.workflowName || '', - description: workflowData.description || '', - workflowType: workflowData.workflowType || 'CORRESPONDENCE', - dslDefinition: workflowData.dslDefinition || '', - }; + const dto: CreateWorkflowDefinitionDto = { + workflow_code: workflowData.workflowType || 'CORRESPONDENCE', + dsl: { + workflowName: workflowData.workflowName, + description: workflowData.description, + dslDefinition: workflowData.dslDefinition, + }, + is_active: workflowData.isActive, + }; - 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'); - } + if (id) { + await updateMutation.mutateAsync({ id, data: dto }); + toast.success('Workflow updated successfully'); + } else { + await createMutation.mutateAsync(dto); + toast.success('Workflow created successfully'); + router.push('/admin/workflows'); + } } catch (error) { - toast.error("Failed to save workflow"); - console.error(error); - } finally { - setSaving(false); + toast.error('Failed to save workflow'); + console.error(error); } }; if (loading) { - return ( -
- -
- ); + return ( +
+ +
+ ); } return (
- - - -
-

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

-

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

-
+ + + +
+

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

+

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

+
- +