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. - *Action:* Adhere to the defined system design.
- *Crucial:* Check `specs/05-decisions/` (ADRs) to ensure you do not violate previously agreed-upon technical decisions. - *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. - *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. - **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. - **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. - *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/`)** 7. **🚀 OPERATIONS (`specs/04-operations/`)**
- *Action:* Ensure deployability and configuration compliance. - *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 ## Execution Rules
### 1. Citation Requirement ### 1. Citation Requirement

View File

@@ -1,20 +1,15 @@
--- ---
trigger: always_on trigger: always_on
---
---
description: Control which shell commands the agent may run automatically. description: Control which shell commands the agent may run automatically.
allowAuto: ["pnpm test:watch", "pnpm test:debug", "pnpm test:e2e", "git status"] allowAuto: ["pnpm test:watch", "pnpm test:debug", "pnpm test:e2e", "git status"]
denyAuto: ["rm -rf", "Remove-Item", "git push --force", "curl | bash"] denyAuto: ["rm -rf", "Remove-Item", "git push --force", "curl | bash"]
alwaysReview: true alwaysReview: true
scopes: ["backend/src/**", "backend/test/**", "frontend/app/**"] scopes: ["backend/src/**", "backend/test/**", "frontend/app/**"]
--- ---
# Execution Rules # Execution Rules
- Only auto-execute commands that are explicitly listed in `allowAuto`. - Only auto-execute commands that are explicitly listed in `allowAuto`.
- Commands in denyAuto must always be blocked, even if manually requested. - 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. - 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 ## 💻 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). - **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, React Context / Zustand, React Hook Form + Zod, Axios. - **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.** - **Language:** TypeScript (Strict Mode). **NO `any` types allowed.**
## 🛡️ Security & Integrity Rules ## 🛡️ Security & Integrity Rules

View File

@@ -6,21 +6,15 @@ import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Plus, Edit, Play } from 'lucide-react'; 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 { TemplateEditor } from '@/components/numbering/template-editor';
import { SequenceViewer } from '@/components/numbering/sequence-viewer'; import { SequenceViewer } from '@/components/numbering/sequence-viewer';
import { TemplateTester } from '@/components/numbering/template-tester'; import { TemplateTester } from '@/components/numbering/template-tester';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useProjects, useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data'; import { useProjects, useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data';
import { ManualOverrideForm } from '@/components/numbering/manual-override-form'; import { ManualOverrideForm } from '@/components/numbering/manual-override-form';
import { MetricsDashboard } from '@/components/numbering/metrics-dashboard'; import { MetricsDashboard } from '@/components/numbering/metrics-dashboard';
import { AuditLogsTable } from '@/components/numbering/audit-logs-table'; 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 { CancelNumberForm } from '@/components/numbering/cancel-number-form';
import { BulkImportForm } from '@/components/numbering/bulk-import-form'; import { BulkImportForm } from '@/components/numbering/bulk-import-form';
export default function NumberingPage() { export default function NumberingPage() {
const { data: projects = [] } = useProjects(); const { data: projects = [] } = useProjects();
const [selectedProjectId, setSelectedProjectId] = useState("1"); const [selectedProjectId, setSelectedProjectId] = useState('1');
const [activeTab, setActiveTab] = useState("templates"); const [activeTab, setActiveTab] = useState('templates');
const [templates, setTemplates] = useState<NumberingTemplate[]>([]);
// View states // View states
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
@@ -42,7 +33,9 @@ export default function NumberingPage() {
const [isTesting, setIsTesting] = useState(false); const [isTesting, setIsTesting] = useState(false);
const [testTemplate, setTestTemplate] = useState<NumberingTemplate | null>(null); 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 // Master Data
const { data: correspondenceTypes = [] } = useCorrespondenceTypes(); const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
@@ -50,189 +43,168 @@ export default function NumberingPage() {
const contractId = contracts[0]?.id; const contractId = contracts[0]?.id;
const { data: disciplines = [] } = useDisciplines(contractId); const { data: disciplines = [] } = useDisciplines(contractId);
const loadTemplates = async () => { const { data: templateResponse, isLoading: isLoadingTemplates } = useTemplates();
try { const saveTemplateMutation = useSaveTemplate();
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([]);
}
};
useEffect(() => { // Extract templates array from response (handles both direct array and { data: array } formats)
loadTemplates(); const templates: NumberingTemplate[] = Array.isArray(templateResponse)
}, []); ? templateResponse
: ((templateResponse as any)?.data ?? []);
const handleEdit = (template?: NumberingTemplate) => { const handleEdit = (template?: NumberingTemplate) => {
setActiveTemplate(template); setActiveTemplate(template);
setIsEditing(true); setIsEditing(true);
}; };
const handleSave = async (data: Partial<NumberingTemplate>) => { const handleSave = async (data: Partial<NumberingTemplate>) => {
try { try {
await numberingApi.saveTemplate(data); await saveTemplateMutation.mutateAsync(data);
toast.success(data.id ? "Template updated" : "Template created"); toast.success(data.id ? 'Template updated' : 'Template created');
setIsEditing(false); setIsEditing(false);
loadTemplates(); } catch {
} catch { toast.error('Failed to save template');
toast.error("Failed to save template"); }
}
}; };
const handleTest = (template: NumberingTemplate) => { const handleTest = (template: NumberingTemplate) => {
setTestTemplate(template); setTestTemplate(template);
setIsTesting(true); setIsTesting(true);
}; };
if (isEditing) { if (isEditing) {
return ( return (
<div className="p-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4"> <div className="p-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4">
<TemplateEditor <TemplateEditor
template={activeTemplate} template={activeTemplate}
projectId={Number(selectedProjectId)} projectId={Number(selectedProjectId)}
projectName={selectedProjectName} projectName={selectedProjectName}
correspondenceTypes={correspondenceTypes} correspondenceTypes={correspondenceTypes}
disciplines={disciplines} disciplines={disciplines}
onSave={handleSave} onSave={handleSave}
onCancel={() => setIsEditing(false)} onCancel={() => setIsEditing(false)}
/> />
</div> </div>
); );
} }
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight"> <h1 className="text-3xl font-bold tracking-tight">Document Numbering</h1>
Document Numbering <p className="text-muted-foreground mt-1">Manage numbering templates, audit logs, and tools</p>
</h1>
<p className="text-muted-foreground mt-1">
Manage numbering templates, audit logs, and tools
</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Select value={selectedProjectId} onValueChange={setSelectedProjectId}> <Select value={selectedProjectId} onValueChange={setSelectedProjectId}>
<SelectTrigger className="w-[200px]"> <SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select Project" /> <SelectValue placeholder="Select Project" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{projects.map((project: { id: number; projectCode: string; projectName: string }) => ( {projects.map((project: { id: number; projectCode: string; projectName: string }) => (
<SelectItem key={project.id} value={project.id.toString()}> <SelectItem key={project.id} value={project.id.toString()}>
{project.projectCode} - {project.projectName} {project.projectCode} - {project.projectName}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div> </div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4"> <Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList> <TabsList>
<TabsTrigger value="templates">Templates</TabsTrigger> <TabsTrigger value="templates">Templates</TabsTrigger>
<TabsTrigger value="metrics">Metrics & Audit</TabsTrigger> <TabsTrigger value="metrics">Metrics & Audit</TabsTrigger>
<TabsTrigger value="tools">Admin Tools</TabsTrigger> <TabsTrigger value="tools">Admin Tools</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="templates" className="space-y-4"> <TabsContent value="templates" className="space-y-4">
<div className="flex justify-end"> <div className="flex justify-end">
<Button onClick={() => handleEdit(undefined)}> <Button onClick={() => handleEdit(undefined)}>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
New Template New Template
</Button> </Button>
</div> </div>
<div className="grid lg:grid-cols-3 gap-6"> <div className="grid lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-4"> <div className="lg:col-span-2 space-y-4">
<div className="grid gap-4"> <div className="grid gap-4">
{templates {templates
.filter(t => !t.projectId || t.projectId === Number(selectedProjectId)) .filter((t) => !t.projectId || t.projectId === Number(selectedProjectId))
.map((template) => ( .map((template) => (
<Card key={template.id} className="p-6 hover:shadow-md transition-shadow"> <Card key={template.id} className="p-6 hover:shadow-md transition-shadow">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold"> <h3 className="text-lg font-semibold">
{template.correspondenceType?.typeName || 'Default Format'} {template.correspondenceType?.typeName || 'Default Format'}
</h3> </h3>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{template.project?.projectCode || selectedProjectName} {template.project?.projectCode || selectedProjectName}
</Badge> </Badge>
{template.description && <Badge variant="secondary">{template.description}</Badge>} {template.description && <Badge variant="secondary">{template.description}</Badge>}
</div> </div>
<div className="bg-slate-100 dark:bg-slate-900 rounded px-3 py-2 mb-3 font-mono text-sm inline-block border"> <div className="bg-slate-100 dark:bg-slate-900 rounded px-3 py-2 mb-3 font-mono text-sm inline-block border">
{template.formatTemplate} {template.formatTemplate}
</div> </div>
<div className="grid grid-cols-2 gap-4 text-sm mt-2"> <div className="grid grid-cols-2 gap-4 text-sm mt-2">
<div> <div>
<span className="text-muted-foreground">Type Code: </span> <span className="text-muted-foreground">Type Code: </span>
<span className="font-medium font-mono text-green-600 dark:text-green-400"> <span className="font-medium font-mono text-green-600 dark:text-green-400">
{template.correspondenceType?.typeCode || 'DEFAULT'} {template.correspondenceType?.typeCode || 'DEFAULT'}
</span> </span>
</div>
<div>
<span className="text-muted-foreground">Reset: </span>
<span>
{template.resetSequenceYearly ? 'Annually' : 'Continuous'}
</span>
</div>
</div>
</div> </div>
<div>
<div className="flex flex-col gap-2"> <span className="text-muted-foreground">Reset: </span>
<Button variant="outline" size="sm" onClick={() => handleEdit(template)}> <span>{template.resetSequenceYearly ? 'Annually' : 'Continuous'}</span>
<Edit className="mr-2 h-4 w-4" />
Edit
</Button>
<Button variant="outline" size="sm" onClick={() => handleTest(template)}>
<Play className="mr-2 h-4 w-4" />
Test
</Button>
</div> </div>
</div> </div>
</Card> </div>
))}
</div>
</div>
<div className="space-y-4"> <div className="flex flex-col gap-2">
<SequenceViewer /> <Button variant="outline" size="sm" onClick={() => handleEdit(template)}>
</div> <Edit className="mr-2 h-4 w-4" />
Edit
</Button>
<Button variant="outline" size="sm" onClick={() => handleTest(template)}>
<Play className="mr-2 h-4 w-4" />
Test
</Button>
</div>
</div>
</Card>
))}
</div> </div>
</TabsContent> </div>
<TabsContent value="metrics" className="space-y-4"> <div className="space-y-4">
<MetricsDashboard /> <SequenceViewer />
<div className="mt-6"> </div>
<h3 className="text-lg font-medium mb-4">Audit Logs</h3> </div>
<AuditLogsTable /> </TabsContent>
</div>
</TabsContent>
<TabsContent value="tools" className="space-y-4"> <TabsContent value="metrics" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2"> <MetricsDashboard />
<ManualOverrideForm projectId={Number(selectedProjectId)} /> <div className="mt-6">
<VoidReplaceForm projectId={Number(selectedProjectId)} /> <h3 className="text-lg font-medium mb-4">Audit Logs</h3>
<CancelNumberForm /> <AuditLogsTable />
<div className="md:col-span-2"> </div>
<BulkImportForm projectId={Number(selectedProjectId)} /> </TabsContent>
</div>
</div> <TabsContent value="tools" className="space-y-4">
</TabsContent> <div className="grid gap-4 md:grid-cols-2">
<ManualOverrideForm projectId={Number(selectedProjectId)} />
<VoidReplaceForm projectId={Number(selectedProjectId)} />
<CancelNumberForm />
<div className="md:col-span-2">
<BulkImportForm projectId={Number(selectedProjectId)} />
</div>
</div>
</TabsContent>
</Tabs> </Tabs>
<TemplateTester <TemplateTester open={isTesting} onOpenChange={setIsTesting} template={testTemplate} />
open={isTesting}
onOpenChange={setIsTesting}
template={testTemplate}
/>
</div> </div>
); );
} }

View File

@@ -1,64 +1,43 @@
"use client"; 'use client';
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table"; import { GenericCrudTable } from '@/components/admin/reference/generic-crud-table';
import { masterDataService } from "@/lib/services/master-data.service"; import { masterDataService } from '@/lib/services/master-data.service';
import { contractService } from "@/lib/services/contract.service"; import { useContracts } from '@/hooks/use-master-data';
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from '@tanstack/react-table';
import { useState, useEffect } from "react"; import { useState } from 'react';
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function DisciplinesPage() { export default function DisciplinesPage() {
const [contracts, setContracts] = useState<any[]>([]); const [selectedContractId, setSelectedContractId] = useState<string | null>(null);
const [selectedContractId, setSelectedContractId] = useState<string | null>(
null
);
useEffect(() => { const { data: contractsData = [] } = useContracts();
// Fetch contracts for filter and form options // Ensure we consistently use an array
contractService.getAll().then((data) => { const contracts = Array.isArray(contractsData) ? contractsData : [];
setContracts(Array.isArray(data) ? data : []);
}).catch(err => {
console.error("Failed to load contracts:", err);
setContracts([]);
});
}, []);
const columns: ColumnDef<any>[] = [ const columns: ColumnDef<any>[] = [
{ {
accessorKey: "disciplineCode", accessorKey: 'disciplineCode',
header: "Code", header: 'Code',
cell: ({ row }) => ( cell: ({ row }) => <span className="font-mono font-bold">{row.getValue('disciplineCode')}</span>,
<span className="font-mono font-bold">
{row.getValue("disciplineCode")}
</span>
),
}, },
{ {
accessorKey: "codeNameTh", accessorKey: 'codeNameTh',
header: "Name (TH)", header: 'Name (TH)',
}, },
{ {
accessorKey: "codeNameEn", accessorKey: 'codeNameEn',
header: "Name (EN)", header: 'Name (EN)',
}, },
{ {
accessorKey: "isActive", accessorKey: 'isActive',
header: "Status", header: 'Status',
cell: ({ row }) => ( cell: ({ row }) => (
<span <span
className={`px-2 py-1 rounded-full text-xs ${ className={`px-2 py-1 rounded-full text-xs ${
row.getValue("isActive") row.getValue('isActive') ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`} }`}
> >
{row.getValue("isActive") ? "Active" : "Inactive"} {row.getValue('isActive') ? 'Active' : 'Inactive'}
</span> </span>
), ),
}, },
@@ -75,23 +54,17 @@ export default function DisciplinesPage() {
entityName="Discipline" entityName="Discipline"
title="Disciplines Management" title="Disciplines Management"
description="Manage system disciplines (e.g., ARCH, STR, MEC)" description="Manage system disciplines (e.g., ARCH, STR, MEC)"
queryKey={["disciplines", selectedContractId ?? "all"]} queryKey={['disciplines', selectedContractId ?? 'all']}
fetchFn={() => fetchFn={() => masterDataService.getDisciplines(selectedContractId ? parseInt(selectedContractId) : undefined)}
masterDataService.getDisciplines(
selectedContractId ? parseInt(selectedContractId) : undefined
)
}
createFn={(data) => masterDataService.createDiscipline(data)} 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)} deleteFn={(id) => masterDataService.deleteDiscipline(id)}
columns={columns} columns={columns}
filters={ filters={
<div className="w-[300px]"> <div className="w-[300px]">
<Select <Select
value={selectedContractId || "all"} value={selectedContractId || 'all'}
onValueChange={(val) => onValueChange={(val) => setSelectedContractId(val === 'all' ? null : val)}
setSelectedContractId(val === "all" ? null : val)
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Filter by Contract" /> <SelectValue placeholder="Filter by Contract" />
@@ -109,26 +82,26 @@ export default function DisciplinesPage() {
} }
fields={[ fields={[
{ {
name: "contractId", name: 'contractId',
label: "Contract", label: 'Contract',
type: "select", type: 'select',
required: true, required: true,
options: contractOptions, options: contractOptions,
}, },
{ {
name: "disciplineCode", name: 'disciplineCode',
label: "Code", label: 'Code',
type: "text", type: 'text',
required: true, required: true,
}, },
{ {
name: "codeNameTh", name: 'codeNameTh',
label: "Name (TH)", label: 'Name (TH)',
type: "text", type: 'text',
required: true, required: true,
}, },
{ name: "codeNameEn", label: "Name (EN)", type: "text" }, { name: 'codeNameEn', label: 'Name (EN)', type: 'text' },
{ name: "isActive", label: "Active", type: "checkbox" }, { name: 'isActive', label: 'Active', type: 'checkbox' },
]} ]}
/> />
</div> </div>

View File

@@ -1,66 +1,47 @@
"use client"; 'use client';
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table"; import { GenericCrudTable } from '@/components/admin/reference/generic-crud-table';
import { masterDataService } from "@/lib/services/master-data.service"; import { masterDataService } from '@/lib/services/master-data.service';
import { contractService } from "@/lib/services/contract.service"; import { useContracts } from '@/hooks/use-master-data';
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from '@tanstack/react-table';
import { useState, useEffect } from "react"; import { useState } from 'react';
import { import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function RfaTypesPage() { export default function RfaTypesPage() {
const [contracts, setContracts] = useState<any[]>([]); const [selectedContractId, setSelectedContractId] = useState<string | null>(null);
const [selectedContractId, setSelectedContractId] = useState<string | null>(
null
);
useEffect(() => { const { data: contractsData = [] } = useContracts();
// Fetch contracts for filter and form options // Ensure we consistently use an array
contractService.getAll().then((data) => { const contracts = Array.isArray(contractsData) ? contractsData : [];
setContracts(Array.isArray(data) ? data : []);
}).catch(err => {
console.error("Failed to load contracts:", err);
setContracts([]);
});
}, []);
const columns: ColumnDef<any>[] = [ const columns: ColumnDef<any>[] = [
{ {
accessorKey: "typeCode", accessorKey: 'typeCode',
header: "Code", header: 'Code',
cell: ({ row }) => ( cell: ({ row }) => <span className="font-mono font-bold">{row.getValue('typeCode')}</span>,
<span className="font-mono font-bold">{row.getValue("typeCode")}</span>
),
}, },
{ {
accessorKey: "typeNameTh", accessorKey: 'typeNameTh',
header: "Name (TH)", header: 'Name (TH)',
}, },
{ {
accessorKey: "typeNameEn", accessorKey: 'typeNameEn',
header: "Name (EN)", header: 'Name (EN)',
}, },
{ {
accessorKey: "remark", accessorKey: 'remark',
header: "Remark", header: 'Remark',
}, },
{ {
accessorKey: "isActive", accessorKey: 'isActive',
header: "Status", header: 'Status',
cell: ({ row }) => ( cell: ({ row }) => (
<span <span
className={`px-2 py-1 rounded-full text-xs ${ className={`px-2 py-1 rounded-full text-xs ${
row.getValue("isActive") row.getValue('isActive') ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`} }`}
> >
{row.getValue("isActive") ? "Active" : "Inactive"} {row.getValue('isActive') ? 'Active' : 'Inactive'}
</span> </span>
), ),
}, },
@@ -76,12 +57,8 @@ export default function RfaTypesPage() {
<GenericCrudTable <GenericCrudTable
entityName="RFA Type" entityName="RFA Type"
title="RFA Types Management" title="RFA Types Management"
queryKey={["rfa-types", selectedContractId ?? "all"]} queryKey={['rfa-types', selectedContractId ?? 'all']}
fetchFn={() => fetchFn={() => masterDataService.getRfaTypes(selectedContractId ? parseInt(selectedContractId) : undefined)}
masterDataService.getRfaTypes(
selectedContractId ? parseInt(selectedContractId) : undefined
)
}
createFn={(data) => masterDataService.createRfaType(data)} createFn={(data) => masterDataService.createRfaType(data)}
updateFn={(id, data) => masterDataService.updateRfaType(id, data)} updateFn={(id, data) => masterDataService.updateRfaType(id, data)}
deleteFn={(id) => masterDataService.deleteRfaType(id)} deleteFn={(id) => masterDataService.deleteRfaType(id)}
@@ -89,10 +66,8 @@ export default function RfaTypesPage() {
filters={ filters={
<div className="w-[300px]"> <div className="w-[300px]">
<Select <Select
value={selectedContractId || "all"} value={selectedContractId || 'all'}
onValueChange={(val) => onValueChange={(val) => setSelectedContractId(val === 'all' ? null : val)}
setSelectedContractId(val === "all" ? null : val)
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Filter by Contract" /> <SelectValue placeholder="Filter by Contract" />
@@ -110,17 +85,17 @@ export default function RfaTypesPage() {
} }
fields={[ fields={[
{ {
name: "contractId", name: 'contractId',
label: "Contract", label: 'Contract',
type: "select", type: 'select',
required: true, required: true,
options: contractOptions, options: contractOptions,
}, },
{ name: "typeCode", label: "Code", type: "text", required: true }, { name: 'typeCode', label: 'Code', type: 'text', required: true },
{ name: "typeNameTh", label: "Name (TH)", type: "text", required: true }, { name: 'typeNameTh', label: 'Name (TH)', type: 'text', required: true },
{ name: "typeNameEn", label: "Name (EN)", type: "text" }, { name: 'typeNameEn', label: 'Name (EN)', type: 'text' },
{ name: "remark", label: "Remark", type: "textarea" }, { name: 'remark', label: 'Remark', type: 'textarea' },
{ name: "isActive", label: "Active", type: "checkbox" }, { name: 'isActive', label: 'Active', type: 'checkbox' },
]} ]}
/> />
</div> </div>

View File

@@ -11,8 +11,9 @@ import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { workflowApi } from '@/lib/api/workflows'; import { useWorkflowDefinition, useCreateWorkflowDefinition, useUpdateWorkflowDefinition } from '@/hooks/use-workflows';
import { Workflow, CreateWorkflowDto } from '@/types/workflow'; import { Workflow } from '@/types/workflow';
import { CreateWorkflowDefinitionDto } from '@/types/dto/workflow-engine/workflow-engine.dto';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Save, ArrowLeft, Loader2 } from 'lucide-react'; import { Save, ArrowLeft, Loader2 } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
@@ -22,8 +23,6 @@ export default function WorkflowEditPage() {
const router = useRouter(); const router = useRouter();
const id = params?.id === 'new' ? null : Number(params?.id); 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>>({ const [workflowData, setWorkflowData] = useState<Partial<Workflow>>({
workflowName: '', workflowName: '',
description: '', description: '',
@@ -32,84 +31,77 @@ export default function WorkflowEditPage() {
isActive: true, isActive: true,
}); });
const { data: fetchedWorkflow, isLoading: loadingWorkflow } = useWorkflowDefinition(id as number);
const createMutation = useCreateWorkflowDefinition();
const updateMutation = useUpdateWorkflowDefinition();
useEffect(() => { useEffect(() => {
if (id) { if (fetchedWorkflow) {
const fetchWorkflow = async () => { setWorkflowData(fetchedWorkflow);
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]); }, [fetchedWorkflow]);
const loading = (!!id && loadingWorkflow) || createMutation.isPending || updateMutation.isPending;
const saving = createMutation.isPending || updateMutation.isPending;
const handleSave = async () => { const handleSave = async () => {
if (!workflowData.workflowName) { if (!workflowData.workflowName) {
toast.error("Workflow name is required"); toast.error('Workflow name is required');
return; return;
} }
setSaving(true);
try { try {
const dto: CreateWorkflowDto = { const dto: CreateWorkflowDefinitionDto = {
workflowName: workflowData.workflowName || '', workflow_code: workflowData.workflowType || 'CORRESPONDENCE',
description: workflowData.description || '', dsl: {
workflowType: workflowData.workflowType || 'CORRESPONDENCE', workflowName: workflowData.workflowName,
dslDefinition: workflowData.dslDefinition || '', description: workflowData.description,
}; dslDefinition: workflowData.dslDefinition,
},
is_active: workflowData.isActive,
};
if (id) { if (id) {
await workflowApi.updateWorkflow(id, dto); await updateMutation.mutateAsync({ id, data: dto });
toast.success("Workflow updated successfully"); toast.success('Workflow updated successfully');
} else { } else {
await workflowApi.createWorkflow(dto); await createMutation.mutateAsync(dto);
toast.success("Workflow created successfully"); toast.success('Workflow created successfully');
router.push('/admin/workflows'); router.push('/admin/workflows');
} }
} catch (error) { } catch (error) {
toast.error("Failed to save workflow"); toast.error('Failed to save workflow');
console.error(error); console.error(error);
} finally {
setSaving(false);
} }
}; };
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-screen"> <div className="flex items-center justify-center h-screen">
<Loader2 className="h-8 w-8 animate-spin" /> <Loader2 className="h-8 w-8 animate-spin" />
</div> </div>
); );
} }
return ( return (
<div className="p-6 space-y-6 max-w-7xl mx-auto"> <div className="p-6 space-y-6 max-w-7xl mx-auto">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Link href="/admin/workflows"> <Link href="/admin/workflows">
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
</Button> </Button>
</Link> </Link>
<div> <div>
<h1 className="text-3xl font-bold">{id ? 'Edit Workflow' : 'New Workflow'}</h1> <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">
</div> {id ? `Version ${workflowData.version}` : 'Create a new workflow definition'}
</p>
</div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Link href="/admin/workflows"> <Link href="/admin/workflows">
<Button variant="outline">Cancel</Button> <Button variant="outline">Cancel</Button>
</Link> </Link>
<Button onClick={handleSave} disabled={saving}> <Button onClick={handleSave} disabled={saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
@@ -121,84 +113,82 @@ export default function WorkflowEditPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-1 space-y-6"> <div className="lg:col-span-1 space-y-6">
<Card className="p-6"> <Card className="p-6">
<div className="grid gap-4"> <div className="grid gap-4">
<div> <div>
<Label htmlFor="name">Workflow Name *</Label> <Label htmlFor="name">Workflow Name *</Label>
<Input <Input
id="name" id="name"
value={workflowData.workflowName} value={workflowData.workflowName}
onChange={(e) => onChange={(e) =>
setWorkflowData({ setWorkflowData({
...workflowData, ...workflowData,
workflowName: e.target.value, workflowName: e.target.value,
}) })
} }
placeholder="e.g. Standard RFA Workflow" placeholder="e.g. Standard RFA Workflow"
/> />
</div> </div>
<div> <div>
<Label htmlFor="desc">Description</Label> <Label htmlFor="desc">Description</Label>
<Textarea <Textarea
id="desc" id="desc"
value={workflowData.description} value={workflowData.description}
onChange={(e) => onChange={(e) =>
setWorkflowData({ setWorkflowData({
...workflowData, ...workflowData,
description: e.target.value, description: e.target.value,
}) })
} }
placeholder="Describe the purpose of this workflow" placeholder="Describe the purpose of this workflow"
/> />
</div> </div>
<div> <div>
<Label htmlFor="type">Workflow Type</Label> <Label htmlFor="type">Workflow Type</Label>
<Select <Select
value={workflowData.workflowType} value={workflowData.workflowType}
onValueChange={(value: Workflow['workflowType']) => onValueChange={(value: Workflow['workflowType']) =>
setWorkflowData({ ...workflowData, workflowType: value }) setWorkflowData({ ...workflowData, workflowType: value })
} }
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select type" /> <SelectValue placeholder="Select type" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="CORRESPONDENCE">Correspondence</SelectItem> <SelectItem value="CORRESPONDENCE">Correspondence</SelectItem>
<SelectItem value="RFA">RFA</SelectItem> <SelectItem value="RFA">RFA</SelectItem>
<SelectItem value="DRAWING">Drawing</SelectItem> <SelectItem value="DRAWING">Drawing</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div> </div>
</Card> </Card>
</div> </div>
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<Tabs defaultValue="dsl" className="w-full"> <Tabs defaultValue="dsl" className="w-full">
<TabsList className="w-full justify-start"> <TabsList className="w-full justify-start">
<TabsTrigger value="dsl">DSL Editor</TabsTrigger> <TabsTrigger value="dsl">DSL Editor</TabsTrigger>
<TabsTrigger value="visual">Visual Builder</TabsTrigger> <TabsTrigger value="visual">Visual Builder</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="dsl" className="mt-4"> <TabsContent value="dsl" className="mt-4">
<DSLEditor <DSLEditor
initialValue={workflowData.dslDefinition} initialValue={workflowData.dslDefinition}
onChange={(value) => onChange={(value) => setWorkflowData({ ...workflowData, dslDefinition: value })}
setWorkflowData({ ...workflowData, dslDefinition: value }) />
} </TabsContent>
/>
</TabsContent>
<TabsContent value="visual" className="mt-4 h-[600px]"> <TabsContent value="visual" className="mt-4 h-[600px]">
<VisualWorkflowBuilder <VisualWorkflowBuilder
dslString={workflowData.dslDefinition} dslString={workflowData.dslDefinition}
onDslChange={(newDsl) => setWorkflowData({ ...workflowData, dslDefinition: newDsl })} 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> </TabsContent>
</Tabs> </Tabs>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,13 +1,13 @@
"use client"; 'use client';
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react';
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { Card } from "@/components/ui/card"; import { Card } from '@/components/ui/card';
import { Badge } from "@/components/ui/badge"; import { Badge } from '@/components/ui/badge';
import { Plus, Edit, Copy, Trash, Loader2 } from "lucide-react"; import { Plus, Edit, Copy, Trash, Loader2 } from 'lucide-react';
import Link from "next/link"; import Link from 'next/link';
import { Workflow } from "@/types/workflow"; import { Workflow } from '@/types/workflow';
import { workflowApi } from "@/lib/api/workflows"; import { workflowApi } from '@/lib/api/workflows';
export default function WorkflowsPage() { export default function WorkflowsPage() {
const [workflows, setWorkflows] = useState<Workflow[]>([]); const [workflows, setWorkflows] = useState<Workflow[]>([]);
@@ -20,7 +20,7 @@ export default function WorkflowsPage() {
const data = await workflowApi.getWorkflows(); const data = await workflowApi.getWorkflows();
setWorkflows(data); setWorkflows(data);
} catch (error) { } catch (error) {
console.error("Failed to fetch workflows", error); console.error('Failed to fetch workflows', error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -34,9 +34,7 @@ export default function WorkflowsPage() {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h1 className="text-3xl font-bold">Workflow Configuration</h1> <h1 className="text-3xl font-bold">Workflow Configuration</h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1">Manage workflow definitions and routing rules</p>
Manage workflow definitions and routing rules
</p>
</div> </div>
<Link href="/admin/workflows/new"> <Link href="/admin/workflows/new">
<Button> <Button>
@@ -57,24 +55,20 @@ export default function WorkflowsPage() {
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold"> <h3 className="text-lg font-semibold">{workflow.workflowName}</h3>
{workflow.workflowName} <Badge
</h3> variant={workflow.isActive ? 'default' : 'secondary'}
<Badge variant={workflow.isActive ? "default" : "secondary"} className={workflow.isActive ? "bg-green-600 hover:bg-green-700" : ""}> className={workflow.isActive ? 'bg-green-600 hover:bg-green-700' : ''}
{workflow.isActive ? "Active" : "Inactive"} >
{workflow.isActive ? 'Active' : 'Inactive'}
</Badge> </Badge>
<Badge variant="outline">v{workflow.version}</Badge> <Badge variant="outline">v{workflow.version}</Badge>
</div> </div>
<p className="text-sm text-muted-foreground mb-3"> <p className="text-sm text-muted-foreground mb-3">{workflow.description}</p>
{workflow.description}
</p>
<div className="flex gap-6 text-sm text-muted-foreground"> <div className="flex gap-6 text-sm text-muted-foreground">
<span>Type: {workflow.workflowType}</span> <span>Type: {workflow.workflowType}</span>
<span>Steps: {workflow.stepCount}</span> <span>Steps: {workflow.stepCount}</span>
<span> <span>Updated: {new Date(workflow.updatedAt).toLocaleDateString()}</span>
Updated:{" "}
{new Date(workflow.updatedAt).toLocaleDateString()}
</span>
</div> </div>
</div> </div>
@@ -85,11 +79,16 @@ export default function WorkflowsPage() {
Edit Edit
</Button> </Button>
</Link> </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" /> <Copy className="mr-2 h-4 w-4" />
Clone Clone
</Button> </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" /> <Trash className="mr-2 h-4 w-4" />
Delete Delete
</Button> </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 # LCBP3-DMS - Project Overview
**Project Name:** Laem Chabang Port Phase 3 - Document Management System (LCBP3-DMS) **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) **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 - **Language:** TypeScript
- **Styling:** Tailwind CSS - **Styling:** Tailwind CSS
- **UI Components:** Shadcn/UI - **UI Components:** Shadcn/UI
- **State Management:** React Context / Zustand - **Server State:** TanStack Query
- **Forms:** React Hook Form + Zod - **Client State:** Zustand
- **Form State:** React Hook Form + Zod
- **API Client:** Axios - **API Client:** Axios
### Infrastructure ### Infrastructure
@@ -384,10 +385,10 @@ lcbp3/
## 📝 Document Control ## 📝 Document Control
- **Version:** 1.7.0 - **Version:** 1.8.0
- **Status:** Active Development - **Status:** Active Development
- **Last Updated:** 2025-12-18 - **Last Updated:** 2026-02-20
- **Next Review:** 2026-01-01 - **Next Review:** 2026-03-31
- **Owner:** System Architect - **Owner:** System Architect
- **Classification:** Internal Use Only - **Classification:** Internal Use Only
@@ -397,6 +398,8 @@ lcbp3/
| Version | Date | Description | | 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.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-09 | TASK-FE-011/012 completed, docs updated |
| 1.5.1 | 2025-12-02 | Reorganized documentation structure | | 1.5.1 | 2025-12-02 | Reorganized documentation structure |

View File

@@ -10,9 +10,9 @@
| Attribute | Value | | Attribute | Value |
| ------------------ | -------------------------------- | | ------------------ | -------------------------------- |
| **Version** | 1.7.0 | | **Version** | 1.8.0 |
| **Status** | Active | | **Status** | Active |
| **Last Updated** | 2025-12-18 | | **Last Updated** | 2026-02-20 |
| **Owner** | Nattanin Peancharoen | | **Owner** | Nattanin Peancharoen |
| **Classification** | Internal Technical Documentation | | **Classification** | Internal Technical Documentation |
@@ -200,7 +200,9 @@ Layer 6: File Security (Virus Scanning, Access Control)
| **Language** | TypeScript (ESM) | Type-safe JavaScript | | **Language** | TypeScript (ESM) | Type-safe JavaScript |
| **Styling** | Tailwind CSS + PostCSS | Utility-first CSS | | **Styling** | Tailwind CSS + PostCSS | Utility-first CSS |
| **Components** | shadcn/ui | Accessible Component Library | | **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 | | **Validation** | Zod | Schema Validation |
| **Testing** | Vitest + Playwright | Unit + E2E Testing | | **Testing** | Vitest + Playwright | Unit + E2E Testing |
@@ -340,7 +342,7 @@ graph TB
MariaDB[(MariaDB 11.8<br/>Primary Database)] MariaDB[(MariaDB 11.8<br/>Primary Database)]
Redis[(Redis<br/>Cache + Queue)] Redis[(Redis<br/>Cache + Queue)]
Elastic[Elasticsearch<br/>Search Engine] Elastic[Elasticsearch<br/>Search Engine]
Storage[File Storage<br/>/share/dms-data] Storage[File Storage<br/>/share/np-dms/data/]
end end
subgraph "Integration Layer" subgraph "Integration Layer"
@@ -490,7 +492,7 @@ sequenceDiagram
<div align="center"> <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) [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 ## 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 ### Rationale
@@ -103,8 +103,12 @@
**For Server State (API data):** **For Server State (API data):**
- Use **Server Components** + **SWR** (เฉพาะที่จำเป็น) - Use **TanStack Query** (React Query) สำหรับ data fetching, caching, synchronization
- Server Components ดึงข้อมูลฝั่ง Server ไม่ต้องจัดการ state - 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 ```bash
npm install swr npm install @tanstack/react-query
``` ```
```typescript ```typescript
// File: components/correspondences/realtime-list.tsx // File: components/correspondences/correspondence-list.tsx
'use client'; '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 CorrespondenceList() {
const { data, error, isLoading, refetch } = useQuery({
export function RealtimeCorrespondenceList() { queryKey: ['correspondences'],
const { data, error, isLoading, mutate } = useSWR( queryFn: getCorrespondences,
'/api/correspondences', refetchInterval: 30000, // Auto refresh every 30s
fetcher, });
{
refreshInterval: 30000, // Auto refresh every 30s
}
);
if (isLoading) return <div>Loading...</div>; if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading data</div>; if (error) return <div>Error loading data</div>;
return ( return (
<div> <div>
{data.map((item) => ( {data?.map((item) => (
<div key={item.id}>{item.subject}</div> <div key={item.id}>{item.subject}</div>
))} ))}
<button onClick={() => mutate()}>Refresh</button> <button onClick={() => refetch()}>Refresh</button>
</div> </div>
); );
} }
@@ -347,14 +348,16 @@ export const useUIStore = create<UIState>()(
- SEO-important content - SEO-important content
- Data that doesn't need real-time updates - 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) - Real-time data (notifications count)
- Polling/Auto-refresh data - Polling/Auto-refresh data
- User-specific data that changes often - User-specific data that changes often
- Optimistic UI updates - Optimistic UI updates
- Complex cache invalidation
- Paginated/infinite scroll data
--- ---
@@ -392,9 +395,10 @@ export const useUIStore = create<UIState>()(
## References ## References
- [Zustand Documentation](https://github.com/pmndrs/zustand) - [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 **Next Review:** 2026-06-01