260220:1504 20260220 TASK-BEFE-001 Refactor by ADR-014
All checks were successful
Build and Deploy / deploy (push) Successful in 2m34s

This commit is contained in:
admin
2026-02-20 15:04:02 +07:00
parent 7f27f9478b
commit 397c46bc4e
15 changed files with 709 additions and 492 deletions

View File

@@ -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

View File

@@ -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.
- 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.

View File

@@ -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

View File

@@ -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<NumberingTemplate[]>([]);
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<NumberingTemplate | null>(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,21 +43,13 @@ 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);
@@ -73,12 +58,11 @@ export default function NumberingPage() {
const handleSave = async (data: Partial<NumberingTemplate>) => {
try {
await numberingApi.saveTemplate(data);
toast.success(data.id ? "Template updated" : "Template created");
await saveTemplateMutation.mutateAsync(data);
toast.success(data.id ? 'Template updated' : 'Template created');
setIsEditing(false);
loadTemplates();
} catch {
toast.error("Failed to save template");
toast.error('Failed to save template');
}
};
@@ -87,8 +71,6 @@ export default function NumberingPage() {
setIsTesting(true);
};
if (isEditing) {
return (
<div className="p-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4">
@@ -109,12 +91,8 @@ export default function NumberingPage() {
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Document Numbering
</h1>
<p className="text-muted-foreground mt-1">
Manage numbering templates, audit logs, and tools
</p>
<h1 className="text-3xl font-bold tracking-tight">Document Numbering</h1>
<p className="text-muted-foreground mt-1">Manage numbering templates, audit logs, and tools</p>
</div>
<div className="flex gap-2">
<Select value={selectedProjectId} onValueChange={setSelectedProjectId}>
@@ -151,7 +129,7 @@ export default function NumberingPage() {
<div className="lg:col-span-2 space-y-4">
<div className="grid gap-4">
{templates
.filter(t => !t.projectId || t.projectId === Number(selectedProjectId))
.filter((t) => !t.projectId || t.projectId === Number(selectedProjectId))
.map((template) => (
<Card key={template.id} className="p-6 hover:shadow-md transition-shadow">
<div className="flex justify-between items-start">
@@ -179,9 +157,7 @@ export default function NumberingPage() {
</div>
<div>
<span className="text-muted-foreground">Reset: </span>
<span>
{template.resetSequenceYearly ? 'Annually' : 'Continuous'}
</span>
<span>{template.resetSequenceYearly ? 'Annually' : 'Continuous'}</span>
</div>
</div>
</div>
@@ -228,11 +204,7 @@ export default function NumberingPage() {
</TabsContent>
</Tabs>
<TemplateTester
open={isTesting}
onOpenChange={setIsTesting}
template={testTemplate}
/>
<TemplateTester open={isTesting} onOpenChange={setIsTesting} template={testTemplate} />
</div>
);
}

View File

@@ -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<any[]>([]);
const [selectedContractId, setSelectedContractId] = useState<string | null>(
null
);
const [selectedContractId, setSelectedContractId] = useState<string | null>(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<any>[] = [
{
accessorKey: "disciplineCode",
header: "Code",
cell: ({ row }) => (
<span className="font-mono font-bold">
{row.getValue("disciplineCode")}
</span>
),
accessorKey: 'disciplineCode',
header: 'Code',
cell: ({ row }) => <span className="font-mono font-bold">{row.getValue('disciplineCode')}</span>,
},
{
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 }) => (
<span
className={`px-2 py-1 rounded-full text-xs ${
row.getValue("isActive")
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
row.getValue('isActive') ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
{row.getValue("isActive") ? "Active" : "Inactive"}
{row.getValue('isActive') ? 'Active' : 'Inactive'}
</span>
),
},
@@ -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={
<div className="w-[300px]">
<Select
value={selectedContractId || "all"}
onValueChange={(val) =>
setSelectedContractId(val === "all" ? null : val)
}
value={selectedContractId || 'all'}
onValueChange={(val) => setSelectedContractId(val === 'all' ? null : val)}
>
<SelectTrigger>
<SelectValue placeholder="Filter by Contract" />
@@ -109,26 +82,26 @@ export default function DisciplinesPage() {
}
fields={[
{
name: "contractId",
label: "Contract",
type: "select",
name: 'contractId',
label: 'Contract',
type: 'select',
required: true,
options: contractOptions,
},
{
name: "disciplineCode",
label: "Code",
type: "text",
name: 'disciplineCode',
label: 'Code',
type: 'text',
required: true,
},
{
name: "codeNameTh",
label: "Name (TH)",
type: "text",
name: 'codeNameTh',
label: 'Name (TH)',
type: 'text',
required: true,
},
{ name: "codeNameEn", label: "Name (EN)", type: "text" },
{ name: "isActive", label: "Active", type: "checkbox" },
{ name: 'codeNameEn', label: 'Name (EN)', type: 'text' },
{ name: 'isActive', label: 'Active', type: 'checkbox' },
]}
/>
</div>

View File

@@ -1,66 +1,47 @@
"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 RfaTypesPage() {
const [contracts, setContracts] = useState<any[]>([]);
const [selectedContractId, setSelectedContractId] = useState<string | null>(
null
);
const [selectedContractId, setSelectedContractId] = useState<string | null>(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<any>[] = [
{
accessorKey: "typeCode",
header: "Code",
cell: ({ row }) => (
<span className="font-mono font-bold">{row.getValue("typeCode")}</span>
),
accessorKey: 'typeCode',
header: 'Code',
cell: ({ row }) => <span className="font-mono font-bold">{row.getValue('typeCode')}</span>,
},
{
accessorKey: "typeNameTh",
header: "Name (TH)",
accessorKey: 'typeNameTh',
header: 'Name (TH)',
},
{
accessorKey: "typeNameEn",
header: "Name (EN)",
accessorKey: 'typeNameEn',
header: 'Name (EN)',
},
{
accessorKey: "remark",
header: "Remark",
accessorKey: 'remark',
header: 'Remark',
},
{
accessorKey: "isActive",
header: "Status",
accessorKey: 'isActive',
header: 'Status',
cell: ({ row }) => (
<span
className={`px-2 py-1 rounded-full text-xs ${
row.getValue("isActive")
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
row.getValue('isActive') ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
{row.getValue("isActive") ? "Active" : "Inactive"}
{row.getValue('isActive') ? 'Active' : 'Inactive'}
</span>
),
},
@@ -76,12 +57,8 @@ export default function RfaTypesPage() {
<GenericCrudTable
entityName="RFA Type"
title="RFA Types Management"
queryKey={["rfa-types", selectedContractId ?? "all"]}
fetchFn={() =>
masterDataService.getRfaTypes(
selectedContractId ? parseInt(selectedContractId) : undefined
)
}
queryKey={['rfa-types', selectedContractId ?? 'all']}
fetchFn={() => masterDataService.getRfaTypes(selectedContractId ? parseInt(selectedContractId) : undefined)}
createFn={(data) => masterDataService.createRfaType(data)}
updateFn={(id, data) => masterDataService.updateRfaType(id, data)}
deleteFn={(id) => masterDataService.deleteRfaType(id)}
@@ -89,10 +66,8 @@ export default function RfaTypesPage() {
filters={
<div className="w-[300px]">
<Select
value={selectedContractId || "all"}
onValueChange={(val) =>
setSelectedContractId(val === "all" ? null : val)
}
value={selectedContractId || 'all'}
onValueChange={(val) => setSelectedContractId(val === 'all' ? null : val)}
>
<SelectTrigger>
<SelectValue placeholder="Filter by Contract" />
@@ -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' },
]}
/>
</div>

View File

@@ -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<Partial<Workflow>>({
workflowName: '',
description: '',
@@ -32,56 +31,47 @@ 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');
if (fetchedWorkflow) {
setWorkflowData(fetchedWorkflow);
}
} catch (error) {
toast.error("Failed to load workflow");
console.error(error);
} finally {
setLoading(false);
}
};
fetchWorkflow();
}
}, [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");
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");
await updateMutation.mutateAsync({ id, data: dto });
toast.success('Workflow updated successfully');
} else {
await workflowApi.createWorkflow(dto);
toast.success("Workflow created successfully");
await createMutation.mutateAsync(dto);
toast.success('Workflow created successfully');
router.push('/admin/workflows');
}
} catch (error) {
toast.error("Failed to save workflow");
toast.error('Failed to save workflow');
console.error(error);
} finally {
setSaving(false);
}
};
@@ -104,7 +94,9 @@ export default function WorkflowEditPage() {
</Link>
<div>
<h1 className="text-3xl font-bold">{id ? 'Edit Workflow' : 'New Workflow'}</h1>
<p className="text-muted-foreground">{id ? `Version ${workflowData.version}` : 'Create a new workflow definition'}</p>
<p className="text-muted-foreground">
{id ? `Version ${workflowData.version}` : 'Create a new workflow definition'}
</p>
</div>
</div>
<div className="flex gap-2">
@@ -185,9 +177,7 @@ export default function WorkflowEditPage() {
<TabsContent value="dsl" className="mt-4">
<DSLEditor
initialValue={workflowData.dslDefinition}
onChange={(value) =>
setWorkflowData({ ...workflowData, dslDefinition: value })
}
onChange={(value) => setWorkflowData({ ...workflowData, dslDefinition: value })}
/>
</TabsContent>
@@ -195,7 +185,7 @@ export default function WorkflowEditPage() {
<VisualWorkflowBuilder
dslString={workflowData.dslDefinition}
onDslChange={(newDsl) => setWorkflowData({ ...workflowData, dslDefinition: newDsl })}
onSave={() => toast.info("Visual state saving not implemented in this demo")}
onSave={() => toast.info('Visual state saving not implemented in this demo')}
/>
</TabsContent>
</Tabs>

View File

@@ -1,13 +1,13 @@
"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, Copy, Trash, Loader2 } from "lucide-react";
import Link from "next/link";
import { Workflow } from "@/types/workflow";
import { workflowApi } from "@/lib/api/workflows";
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, Copy, Trash, Loader2 } from 'lucide-react';
import Link from 'next/link';
import { Workflow } from '@/types/workflow';
import { workflowApi } from '@/lib/api/workflows';
export default function WorkflowsPage() {
const [workflows, setWorkflows] = useState<Workflow[]>([]);
@@ -20,7 +20,7 @@ export default function WorkflowsPage() {
const data = await workflowApi.getWorkflows();
setWorkflows(data);
} catch (error) {
console.error("Failed to fetch workflows", error);
console.error('Failed to fetch workflows', error);
} finally {
setLoading(false);
}
@@ -34,9 +34,7 @@ export default function WorkflowsPage() {
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Workflow Configuration</h1>
<p className="text-muted-foreground mt-1">
Manage workflow definitions and routing rules
</p>
<p className="text-muted-foreground mt-1">Manage workflow definitions and routing rules</p>
</div>
<Link href="/admin/workflows/new">
<Button>
@@ -57,24 +55,20 @@ export default function WorkflowsPage() {
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold">
{workflow.workflowName}
</h3>
<Badge variant={workflow.isActive ? "default" : "secondary"} className={workflow.isActive ? "bg-green-600 hover:bg-green-700" : ""}>
{workflow.isActive ? "Active" : "Inactive"}
<h3 className="text-lg font-semibold">{workflow.workflowName}</h3>
<Badge
variant={workflow.isActive ? 'default' : 'secondary'}
className={workflow.isActive ? 'bg-green-600 hover:bg-green-700' : ''}
>
{workflow.isActive ? 'Active' : 'Inactive'}
</Badge>
<Badge variant="outline">v{workflow.version}</Badge>
</div>
<p className="text-sm text-muted-foreground mb-3">
{workflow.description}
</p>
<p className="text-sm text-muted-foreground mb-3">{workflow.description}</p>
<div className="flex gap-6 text-sm text-muted-foreground">
<span>Type: {workflow.workflowType}</span>
<span>Steps: {workflow.stepCount}</span>
<span>
Updated:{" "}
{new Date(workflow.updatedAt).toLocaleDateString()}
</span>
<span>Updated: {new Date(workflow.updatedAt).toLocaleDateString()}</span>
</div>
</div>
@@ -85,11 +79,16 @@ export default function WorkflowsPage() {
Edit
</Button>
</Link>
<Button variant="outline" size="sm" onClick={() => alert("Clone functionality mocked")}>
<Button variant="outline" size="sm" onClick={() => alert('Clone functionality mocked')}>
<Copy className="mr-2 h-4 w-4" />
Clone
</Button>
<Button variant="outline" size="sm" className="text-destructive hover:text-destructive" onClick={() => alert("Delete functionality mocked")}>
<Button
variant="outline"
size="sm"
className="text-destructive hover:text-destructive"
onClick={() => alert('Delete functionality mocked')}
>
<Trash className="mr-2 h-4 w-4" />
Delete
</Button>

View File

@@ -0,0 +1,83 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { documentNumberingService } from '@/lib/services/document-numbering.service';
import { numberingApi, NumberingTemplate } from '@/lib/api/numbering';
import { ManualOverrideDto, VoidReplaceDto, CancelNumberDto, AuditQueryParams } from '@/types/dto/numbering.dto';
export const numberingKeys = {
all: ['numbering'] as const,
templates: () => [...numberingKeys.all, 'templates'] as const,
metrics: () => [...numberingKeys.all, 'metrics'] as const,
auditLogs: (params?: AuditQueryParams) => [...numberingKeys.all, 'auditLogs', params] as const,
};
export const useTemplates = () => {
return useQuery({
queryKey: numberingKeys.templates(),
queryFn: () => numberingApi.getTemplates(),
});
};
export const useSaveTemplate = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Partial<NumberingTemplate>) => numberingApi.saveTemplate(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: numberingKeys.templates() });
},
});
};
export const useNumberingMetrics = () => {
return useQuery({
queryKey: numberingKeys.metrics(),
queryFn: () => documentNumberingService.getMetrics(),
});
};
export const useNumberingAuditLogs = (params?: AuditQueryParams) => {
return useQuery({
queryKey: numberingKeys.auditLogs(params),
queryFn: () => documentNumberingService.getAuditLogs(params),
});
};
export const useManualOverrideNumbering = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: ManualOverrideDto) => documentNumberingService.manualOverride(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: numberingKeys.metrics() });
queryClient.invalidateQueries({ queryKey: numberingKeys.all }); // depending on keys
},
});
};
export const useVoidAndReplaceNumbering = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: VoidReplaceDto) => documentNumberingService.voidAndReplace(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: numberingKeys.all });
},
});
};
export const useCancelNumbering = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CancelNumberDto) => documentNumberingService.cancelNumber(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: numberingKeys.all });
},
});
};
export const useBulkImportNumbering = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: FormData | any[]) => documentNumberingService.bulkImport(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: numberingKeys.all });
},
});
};

View File

@@ -0,0 +1,113 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { masterDataService } from '@/lib/services/master-data.service';
export const referenceDataKeys = {
all: ['reference-data'] as const,
rfaTypes: (contractId?: number) => [...referenceDataKeys.all, 'rfaTypes', contractId] as const,
disciplines: (contractId?: number) => [...referenceDataKeys.all, 'disciplines', contractId] as const,
correspondenceTypes: () => [...referenceDataKeys.all, 'correspondenceTypes'] as const,
};
// --- RFA Types ---
export const useRfaTypes = (contractId?: number) => {
return useQuery({
queryKey: referenceDataKeys.rfaTypes(contractId),
queryFn: () => masterDataService.getRfaTypes(contractId),
});
};
export const useCreateRfaType = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => masterDataService.createRfaType(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'rfaTypes'] });
},
});
};
export const useUpdateRfaType = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => masterDataService.updateRfaType(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'rfaTypes'] });
},
});
};
export const useDeleteRfaType = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => masterDataService.deleteRfaType(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'rfaTypes'] });
},
});
};
// --- Disciplines ---
export const useDisciplines = (contractId?: number) => {
return useQuery({
queryKey: referenceDataKeys.disciplines(contractId),
queryFn: () => masterDataService.getDisciplines(contractId),
});
};
export const useCreateDiscipline = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => masterDataService.createDiscipline(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'disciplines'] });
},
});
};
export const useDeleteDiscipline = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => masterDataService.deleteDiscipline(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'disciplines'] });
},
});
};
// --- Correspondence Types ---
export const useCorrespondenceTypes = () => {
return useQuery({
queryKey: referenceDataKeys.correspondenceTypes(),
queryFn: () => masterDataService.getCorrespondenceTypes(),
});
};
export const useCreateCorrespondenceType = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => masterDataService.createCorrespondenceType(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'correspondenceTypes'] });
},
});
};
export const useUpdateCorrespondenceType = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => masterDataService.updateCorrespondenceType(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'correspondenceTypes'] });
},
});
};
export const useDeleteCorrespondenceType = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => masterDataService.deleteCorrespondenceType(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'correspondenceTypes'] });
},
});
};

View File

@@ -0,0 +1,78 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { workflowEngineService } from '@/lib/services/workflow-engine.service';
import {
CreateWorkflowDefinitionDto,
UpdateWorkflowDefinitionDto,
EvaluateWorkflowDto,
GetAvailableActionsDto,
} from '@/types/dto/workflow-engine/workflow-engine.dto';
export const workflowKeys = {
all: ['workflows'] as const,
definitions: () => [...workflowKeys.all, 'definitions'] as const,
definition: (id: string | number) => [...workflowKeys.definitions(), id] as const,
};
export const useWorkflowDefinitions = () => {
return useQuery({
queryKey: workflowKeys.definitions(),
queryFn: () => workflowEngineService.getDefinitions(),
});
};
export const useWorkflowDefinition = (id: string | number) => {
return useQuery({
queryKey: workflowKeys.definition(id),
queryFn: () => workflowEngineService.getDefinitionById(id),
enabled: !!id,
});
};
export const useCreateWorkflowDefinition = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateWorkflowDefinitionDto) => workflowEngineService.createDefinition(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workflowKeys.definitions() });
},
});
};
export const useUpdateWorkflowDefinition = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string | number; data: UpdateWorkflowDefinitionDto }) =>
workflowEngineService.updateDefinition(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: workflowKeys.definitions() });
queryClient.invalidateQueries({ queryKey: workflowKeys.definition(variables.id) });
},
});
};
export const useDeleteWorkflowDefinition = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string | number) => workflowEngineService.deleteDefinition(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workflowKeys.definitions() });
},
});
};
/**
* Since this is a POST request, we use mutation. If you prefer to use useQuery,
* you would need to adjust the service to use GET (if possible) or pass the body via queryKey.
* For now, using useMutation for actions evaluation.
*/
export const useEvaluateWorkflow = () => {
return useMutation({
mutationFn: (data: EvaluateWorkflowDto) => workflowEngineService.evaluate(data),
});
};
export const useGetAvailableActions = () => {
return useMutation({
mutationFn: (data: GetAvailableActionsDto) => workflowEngineService.getAvailableActions(data),
});
};

26
frontend/tsc_errors.txt Normal file
View File

@@ -0,0 +1,26 @@
.next/dev/types/validator.ts(53,39): error TS2307: Cannot find module '../../../app/(admin)/admin/audit-logs/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(62,39): error TS2307: Cannot find module '../../../app/(admin)/admin/contracts/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(71,39): error TS2307: Cannot find module '../../../app/(admin)/admin/drawings/contract/categories/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(80,39): error TS2307: Cannot find module '../../../app/(admin)/admin/drawings/contract/sub-categories/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(89,39): error TS2307: Cannot find module '../../../app/(admin)/admin/drawings/contract/volumes/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(98,39): error TS2307: Cannot find module '../../../app/(admin)/admin/drawings/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(107,39): error TS2307: Cannot find module '../../../app/(admin)/admin/drawings/shop/main-categories/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(116,39): error TS2307: Cannot find module '../../../app/(admin)/admin/drawings/shop/sub-categories/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(125,39): error TS2307: Cannot find module '../../../app/(admin)/admin/numbering/[id]/edit/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(134,39): error TS2307: Cannot find module '../../../app/(admin)/admin/numbering/new/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(143,39): error TS2307: Cannot find module '../../../app/(admin)/admin/numbering/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(152,39): error TS2307: Cannot find module '../../../app/(admin)/admin/organizations/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(170,39): error TS2307: Cannot find module '../../../app/(admin)/admin/projects/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(179,39): error TS2307: Cannot find module '../../../app/(admin)/admin/reference/correspondence-types/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(188,39): error TS2307: Cannot find module '../../../app/(admin)/admin/reference/disciplines/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(197,39): error TS2307: Cannot find module '../../../app/(admin)/admin/reference/drawing-categories/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(206,39): error TS2307: Cannot find module '../../../app/(admin)/admin/reference/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(215,39): error TS2307: Cannot find module '../../../app/(admin)/admin/reference/rfa-types/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(224,39): error TS2307: Cannot find module '../../../app/(admin)/admin/reference/tags/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(233,39): error TS2307: Cannot find module '../../../app/(admin)/admin/security/roles/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(242,39): error TS2307: Cannot find module '../../../app/(admin)/admin/security/sessions/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(260,39): error TS2307: Cannot find module '../../../app/(admin)/admin/system-logs/numbering/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(269,39): error TS2307: Cannot find module '../../../app/(admin)/admin/users/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(278,39): error TS2307: Cannot find module '../../../app/(admin)/admin/workflows/[id]/edit/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(287,39): error TS2307: Cannot find module '../../../app/(admin)/admin/workflows/new/page.js' or its corresponding type declarations.
.next/dev/types/validator.ts(296,39): error TS2307: Cannot find module '../../../app/(admin)/admin/workflows/page.js' or its corresponding type declarations.

View File

@@ -1,9 +1,9 @@
# LCBP3-DMS - Project Overview
**Project Name:** Laem Chabang Port Phase 3 - Document Management System (LCBP3-DMS)
**Version:** 1.6.0
**Version:** 1.8.0
**Status:** Active Development (~95% Complete)
**Last Updated:** 2025-12-13
**Last Updated:** 2026-02-20
---
@@ -140,8 +140,9 @@ LCBP3-DMS is a comprehensive Document Management System (DMS) designed specifica
- **Language:** TypeScript
- **Styling:** Tailwind CSS
- **UI Components:** Shadcn/UI
- **State Management:** React Context / Zustand
- **Forms:** React Hook Form + Zod
- **Server State:** TanStack Query
- **Client State:** Zustand
- **Form State:** React Hook Form + Zod
- **API Client:** Axios
### Infrastructure
@@ -384,10 +385,10 @@ lcbp3/
## 📝 Document Control
- **Version:** 1.7.0
- **Version:** 1.8.0
- **Status:** Active Development
- **Last Updated:** 2025-12-18
- **Next Review:** 2026-01-01
- **Last Updated:** 2026-02-20
- **Next Review:** 2026-03-31
- **Owner:** System Architect
- **Classification:** Internal Use Only
@@ -397,6 +398,8 @@ lcbp3/
| Version | Date | Description |
| ------- | ---------- | ------------------------------------------ |
| 1.8.0 | 2026-02-20 | Contract Categories Page Crash Fix |
| 1.7.0 | 2025-12-18 | Schema refactoring, documentation updated |
| 1.6.0 | 2025-12-13 | Schema refactoring, documentation updated |
| 1.5.1 | 2025-12-09 | TASK-FE-011/012 completed, docs updated |
| 1.5.1 | 2025-12-02 | Reorganized documentation structure |

View File

@@ -10,9 +10,9 @@
| Attribute | Value |
| ------------------ | -------------------------------- |
| **Version** | 1.7.0 |
| **Version** | 1.8.0 |
| **Status** | Active |
| **Last Updated** | 2025-12-18 |
| **Last Updated** | 2026-02-20 |
| **Owner** | Nattanin Peancharoen |
| **Classification** | Internal Technical Documentation |
@@ -200,7 +200,9 @@ Layer 6: File Security (Virus Scanning, Access Control)
| **Language** | TypeScript (ESM) | Type-safe JavaScript |
| **Styling** | Tailwind CSS + PostCSS | Utility-first CSS |
| **Components** | shadcn/ui | Accessible Component Library |
| **State Management** | TanStack Query + React Hook Form | Server State + Form State |
| **Server State** | TanStack Query | Server State Management |
| **Client State** | Zustand | Client State Management |
| **Form State** | React Hook Form + Zod | Form State Management |
| **Validation** | Zod | Schema Validation |
| **Testing** | Vitest + Playwright | Unit + E2E Testing |
@@ -340,7 +342,7 @@ graph TB
MariaDB[(MariaDB 11.8<br/>Primary Database)]
Redis[(Redis<br/>Cache + Queue)]
Elastic[Elasticsearch<br/>Search Engine]
Storage[File Storage<br/>/share/dms-data]
Storage[File Storage<br/>/share/np-dms/data/]
end
subgraph "Integration Layer"
@@ -490,7 +492,7 @@ sequenceDiagram
<div align="center">
**LCBP3-DMS Architecture Specification v1.6.0**
**LCBP3-DMS Architecture Specification v1.8.0**
[System Architecture](02-01-system-architecture.md) • [API Design](02-02-api-design.md) • [Data Model](02-03-data-model.md)

View File

@@ -93,7 +93,7 @@
## Decision Outcome
**Chosen Option:** **Zustand (Client State) + Native Fetch with Server Components (Server State)**
**Chosen Option:** **Zustand (Client State) + TanStack Query (Server State) + React Hook Form + Zod (Form State)**
### Rationale
@@ -103,8 +103,12 @@
**For Server State (API data):**
- Use **Server Components** + **SWR** (เฉพาะที่จำเป็น)
- Server Components ดึงข้อมูลฝั่ง Server ไม่ต้องจัดการ state
- Use **TanStack Query** (React Query) สำหรับ data fetching, caching, synchronization
- Server Components สำหรับ initial data loading
**For Form State:**
- Use **React Hook Form + Zod** สำหรับ type-safe form management
---
@@ -253,38 +257,35 @@ export default async function CorrespondencesPage() {
}
```
### 6. Client-Side Fetching (with SWR for real-time data)
### 6. Client-Side Fetching (with TanStack Query)
```bash
npm install swr
npm install @tanstack/react-query
```
```typescript
// File: components/correspondences/realtime-list.tsx
// File: components/correspondences/correspondence-list.tsx
'use client';
import useSWR from 'swr';
import { useQuery } from '@tanstack/react-query';
import { getCorrespondences } from '@/lib/api/correspondences';
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export function RealtimeCorrespondenceList() {
const { data, error, isLoading, mutate } = useSWR(
'/api/correspondences',
fetcher,
{
refreshInterval: 30000, // Auto refresh every 30s
}
);
export function CorrespondenceList() {
const { data, error, isLoading, refetch } = useQuery({
queryKey: ['correspondences'],
queryFn: getCorrespondences,
refetchInterval: 30000, // Auto refresh every 30s
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading data</div>;
return (
<div>
{data.map((item) => (
{data?.map((item) => (
<div key={item.id}>{item.subject}</div>
))}
<button onClick={() => mutate()}>Refresh</button>
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}
@@ -347,14 +348,16 @@ export const useUIStore = create<UIState>()(
- SEO-important content
- Data that doesn't need real-time updates
### When to Use SWR (Client-Side Server State)
### When to Use TanStack Query (Client-Side Server State)
✅ Use SWR for:
✅ Use TanStack Query for:
- Real-time data (notifications count)
- Polling/Auto-refresh data
- User-specific data that changes often
- Optimistic UI updates
- Complex cache invalidation
- Paginated/infinite scroll data
---
@@ -392,9 +395,10 @@ export const useUIStore = create<UIState>()(
## References
- [Zustand Documentation](https://github.com/pmndrs/zustand)
- [SWR Documentation](https://swr.vercel.app/)
- [TanStack Query Documentation](https://tanstack.com/query/latest)
- [React Hook Form Documentation](https://react-hook-form.com/)
---
**Last Updated:** 2025-12-01
**Last Updated:** 2026-02-20
**Next Review:** 2026-06-01