260220:1504 20260220 TASK-BEFE-001 Refactor by ADR-014
All checks were successful
Build and Deploy / deploy (push) Successful in 2m34s
All checks were successful
Build and Deploy / deploy (push) Successful in 2m34s
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,21 +43,13 @@ 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);
|
||||||
@@ -73,12 +58,11 @@ export default function NumberingPage() {
|
|||||||
|
|
||||||
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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,8 +71,6 @@ export default function NumberingPage() {
|
|||||||
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">
|
||||||
@@ -109,12 +91,8 @@ export default function NumberingPage() {
|
|||||||
<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}>
|
||||||
@@ -151,7 +129,7 @@ export default function NumberingPage() {
|
|||||||
<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">
|
||||||
@@ -179,9 +157,7 @@ export default function NumberingPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Reset: </span>
|
<span className="text-muted-foreground">Reset: </span>
|
||||||
<span>
|
<span>{template.resetSequenceYearly ? 'Annually' : 'Continuous'}</span>
|
||||||
{template.resetSequenceYearly ? 'Annually' : 'Continuous'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,11 +204,7 @@ export default function NumberingPage() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<TemplateTester
|
<TemplateTester open={isTesting} onOpenChange={setIsTesting} template={testTemplate} />
|
||||||
open={isTesting}
|
|
||||||
onOpenChange={setIsTesting}
|
|
||||||
template={testTemplate}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,56 +31,47 @@ 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) {
|
}, [fetchedWorkflow]);
|
||||||
toast.error("Failed to load workflow");
|
|
||||||
console.error(error);
|
const loading = (!!id && loadingWorkflow) || createMutation.isPending || updateMutation.isPending;
|
||||||
} finally {
|
const saving = createMutation.isPending || updateMutation.isPending;
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchWorkflow();
|
|
||||||
}
|
|
||||||
}, [id, router]);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,7 +94,9 @@ export default function WorkflowEditPage() {
|
|||||||
</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">
|
||||||
|
{id ? `Version ${workflowData.version}` : 'Create a new workflow definition'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -185,9 +177,7 @@ export default function WorkflowEditPage() {
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -195,7 +185,7 @@ export default function WorkflowEditPage() {
|
|||||||
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
83
frontend/hooks/use-numbering.ts
Normal file
83
frontend/hooks/use-numbering.ts
Normal 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 });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
113
frontend/hooks/use-reference-data.ts
Normal file
113
frontend/hooks/use-reference-data.ts
Normal 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'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
78
frontend/hooks/use-workflows.ts
Normal file
78
frontend/hooks/use-workflows.ts
Normal 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
26
frontend/tsc_errors.txt
Normal 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.
|
||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user