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.
|
||||
- *Crucial:* Check `specs/05-decisions/` (ADRs) to ensure you do not violate previously agreed-upon technical decisions.
|
||||
|
||||
5. **💾 DATABASE & SCHEMA (`specs/07-databasee/`)**
|
||||
5. **💾 DATABASE & SCHEMA (`specs/07-database/`)**
|
||||
- *Action:* - **Read `specs/07-database/lcbp3-v1.7.0-schema.sql`** (or relevant `.sql` files) for exact table structures and constraints.
|
||||
- **Consult `specs/07-database/data-dictionary-v1.7.0.md`** for field meanings and business rules.
|
||||
- **Check `specs/07-database/lcbp3-v1.7.0-seed.sql`** to understand initial data states.
|
||||
- **Check `specs/07-database/lcbp3-v1.7.0-seed-basic.sql`** to understand initial data states.
|
||||
- **Check `specs/07-database/lcbp3-v1.7.0-seed-permissions.sql`** to understand initial permissions states.
|
||||
- *Constraint:* NEVER invent table names or columns. Use ONLY what is defined here.
|
||||
|
||||
@@ -43,6 +43,10 @@ Before generating code or planning a solution, you MUST conceptually load the co
|
||||
7. **🚀 OPERATIONS (`specs/04-operations/`)**
|
||||
- *Action:* Ensure deployability and configuration compliance.
|
||||
|
||||
8. **🏗️ INFRASTRUCTURE (`specs/08-infrastructure/`)**
|
||||
- *Action:* Review Docker Compose configurations, network diagrams, monitoring setup, and security zones.
|
||||
- *Constraint:* Ensure deployment paths, port mappings, and volume mounts are consistent with this documentation.
|
||||
|
||||
## Execution Rules
|
||||
|
||||
### 1. Citation Requirement
|
||||
@@ -60,4 +64,4 @@ When proposing a change or writing code, you must explicitly reference the sourc
|
||||
### 4. Data Migration
|
||||
- Do not migrate. The schema can be modified directly.
|
||||
|
||||
---
|
||||
---
|
||||
@@ -1,20 +1,15 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
description: Control which shell commands the agent may run automatically.
|
||||
allowAuto: ["pnpm test:watch", "pnpm test:debug", "pnpm test:e2e", "git status"]
|
||||
denyAuto: ["rm -rf", "Remove-Item", "git push --force", "curl | bash"]
|
||||
alwaysReview: true
|
||||
scopes: ["backend/src/**", "backend/test/**", "frontend/app/**"]
|
||||
|
||||
---
|
||||
|
||||
# Execution Rules
|
||||
|
||||
- Only auto-execute commands that are explicitly listed in `allowAuto`.
|
||||
- Commands in denyAuto must always be blocked, even if manually requested.
|
||||
- All shell operations that create, modify, or delete files in `backend/src/` or `backend/test/` or `frontend/app/`require human review.
|
||||
- Alert if environment variables related to DB connection or secrets would be displayed or logged.
|
||||
- All shell operations that create, modify, or delete files in `backend/src/` or `backend/test/` or `frontend/app/` require human review.
|
||||
- Alert if environment variables related to DB connection or secrets would be displayed or logged.
|
||||
@@ -18,8 +18,8 @@ This is **LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**.
|
||||
|
||||
## 💻 Tech Stack & Constraints
|
||||
|
||||
- **Backend:** NestJS (Modular Architecture), TypeORM, MariaDB 10.11, Redis 7.2 (BullMQ), Elasticsearch 8.11, JWT (JSON Web Tokens), CASL (4-Level RBAC).
|
||||
- **Frontend:** Next.js 14+ (App Router), Tailwind CSS, Shadcn/UI, React Context / Zustand, React Hook Form + Zod, Axios.
|
||||
- **Backend:** NestJS (Modular Architecture), TypeORM, MariaDB 11.8, Redis 7.2 (BullMQ), Elasticsearch 8.11, JWT (JSON Web Tokens), CASL (4-Level RBAC).
|
||||
- **Frontend:** Next.js 14+ (App Router), Tailwind CSS, Shadcn/UI, TanStack Query (Server State), Zustand (Client State), React Hook Form + Zod, Axios.
|
||||
- **Language:** TypeScript (Strict Mode). **NO `any` types allowed.**
|
||||
|
||||
## 🛡️ Security & Integrity Rules
|
||||
|
||||
@@ -6,21 +6,15 @@ import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Plus, Edit, Play } from 'lucide-react';
|
||||
import { numberingApi, NumberingTemplate } from '@/lib/api/numbering';
|
||||
import { NumberingTemplate } from '@/lib/api/numbering';
|
||||
import { useTemplates, useSaveTemplate } from '@/hooks/use-numbering';
|
||||
import { TemplateEditor } from '@/components/numbering/template-editor';
|
||||
import { SequenceViewer } from '@/components/numbering/sequence-viewer';
|
||||
import { TemplateTester } from '@/components/numbering/template-tester';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { useProjects, useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data';
|
||||
|
||||
|
||||
import { ManualOverrideForm } from '@/components/numbering/manual-override-form';
|
||||
import { MetricsDashboard } from '@/components/numbering/metrics-dashboard';
|
||||
import { AuditLogsTable } from '@/components/numbering/audit-logs-table';
|
||||
@@ -28,13 +22,10 @@ import { VoidReplaceForm } from '@/components/numbering/void-replace-form';
|
||||
import { CancelNumberForm } from '@/components/numbering/cancel-number-form';
|
||||
import { BulkImportForm } from '@/components/numbering/bulk-import-form';
|
||||
|
||||
|
||||
export default function NumberingPage() {
|
||||
const { data: projects = [] } = useProjects();
|
||||
const [selectedProjectId, setSelectedProjectId] = useState("1");
|
||||
const [activeTab, setActiveTab] = useState("templates");
|
||||
|
||||
const [templates, setTemplates] = useState<NumberingTemplate[]>([]);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState('1');
|
||||
const [activeTab, setActiveTab] = useState('templates');
|
||||
|
||||
// View states
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
@@ -42,7 +33,9 @@ export default function NumberingPage() {
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [testTemplate, setTestTemplate] = useState<NumberingTemplate | null>(null);
|
||||
|
||||
const selectedProjectName = projects.find((p: { id: number; projectName: string }) => p.id.toString() === selectedProjectId)?.projectName || 'Unknown Project';
|
||||
const selectedProjectName =
|
||||
projects.find((p: { id: number; projectName: string }) => p.id.toString() === selectedProjectId)?.projectName ||
|
||||
'Unknown Project';
|
||||
|
||||
// Master Data
|
||||
const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
|
||||
@@ -50,189 +43,168 @@ export default function NumberingPage() {
|
||||
const contractId = contracts[0]?.id;
|
||||
const { data: disciplines = [] } = useDisciplines(contractId);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
const response = await numberingApi.getTemplates();
|
||||
// Handle wrapped response { data: [...] } or direct array
|
||||
const data = Array.isArray(response) ? response : (response as { data?: NumberingTemplate[] })?.data ?? [];
|
||||
setTemplates(data);
|
||||
} catch {
|
||||
toast.error("Failed to load templates");
|
||||
setTemplates([]);
|
||||
}
|
||||
};
|
||||
const { data: templateResponse, isLoading: isLoadingTemplates } = useTemplates();
|
||||
const saveTemplateMutation = useSaveTemplate();
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, []);
|
||||
// Extract templates array from response (handles both direct array and { data: array } formats)
|
||||
const templates: NumberingTemplate[] = Array.isArray(templateResponse)
|
||||
? templateResponse
|
||||
: ((templateResponse as any)?.data ?? []);
|
||||
|
||||
const handleEdit = (template?: NumberingTemplate) => {
|
||||
setActiveTemplate(template);
|
||||
setIsEditing(true);
|
||||
setActiveTemplate(template);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleSave = async (data: Partial<NumberingTemplate>) => {
|
||||
try {
|
||||
await numberingApi.saveTemplate(data);
|
||||
toast.success(data.id ? "Template updated" : "Template created");
|
||||
setIsEditing(false);
|
||||
loadTemplates();
|
||||
} catch {
|
||||
toast.error("Failed to save template");
|
||||
}
|
||||
try {
|
||||
await saveTemplateMutation.mutateAsync(data);
|
||||
toast.success(data.id ? 'Template updated' : 'Template created');
|
||||
setIsEditing(false);
|
||||
} catch {
|
||||
toast.error('Failed to save template');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTest = (template: NumberingTemplate) => {
|
||||
setTestTemplate(template);
|
||||
setIsTesting(true);
|
||||
setTestTemplate(template);
|
||||
setIsTesting(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4">
|
||||
<TemplateEditor
|
||||
template={activeTemplate}
|
||||
projectId={Number(selectedProjectId)}
|
||||
projectName={selectedProjectName}
|
||||
correspondenceTypes={correspondenceTypes}
|
||||
disciplines={disciplines}
|
||||
onSave={handleSave}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4">
|
||||
<TemplateEditor
|
||||
template={activeTemplate}
|
||||
projectId={Number(selectedProjectId)}
|
||||
projectName={selectedProjectName}
|
||||
correspondenceTypes={correspondenceTypes}
|
||||
disciplines={disciplines}
|
||||
onSave={handleSave}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
Document Numbering
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Manage numbering templates, audit logs, and tools
|
||||
</p>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Document Numbering</h1>
|
||||
<p className="text-muted-foreground mt-1">Manage numbering templates, audit logs, and tools</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Select value={selectedProjectId} onValueChange={setSelectedProjectId}>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select Project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projects.map((project: { id: number; projectCode: string; projectName: string }) => (
|
||||
<SelectItem key={project.id} value={project.id.toString()}>
|
||||
{project.projectCode} - {project.projectName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={selectedProjectId} onValueChange={setSelectedProjectId}>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select Project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projects.map((project: { id: number; projectCode: string; projectName: string }) => (
|
||||
<SelectItem key={project.id} value={project.id.toString()}>
|
||||
{project.projectCode} - {project.projectName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="templates">Templates</TabsTrigger>
|
||||
<TabsTrigger value="metrics">Metrics & Audit</TabsTrigger>
|
||||
<TabsTrigger value="tools">Admin Tools</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsList>
|
||||
<TabsTrigger value="templates">Templates</TabsTrigger>
|
||||
<TabsTrigger value="metrics">Metrics & Audit</TabsTrigger>
|
||||
<TabsTrigger value="tools">Admin Tools</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="templates" className="space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={() => handleEdit(undefined)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Template
|
||||
</Button>
|
||||
</div>
|
||||
<TabsContent value="templates" className="space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={() => handleEdit(undefined)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Template
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<div className="grid gap-4">
|
||||
{templates
|
||||
.filter(t => !t.projectId || t.projectId === Number(selectedProjectId))
|
||||
.map((template) => (
|
||||
<Card key={template.id} className="p-6 hover:shadow-md transition-shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{template.correspondenceType?.typeName || 'Default Format'}
|
||||
</h3>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{template.project?.projectCode || selectedProjectName}
|
||||
</Badge>
|
||||
{template.description && <Badge variant="secondary">{template.description}</Badge>}
|
||||
</div>
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<div className="grid gap-4">
|
||||
{templates
|
||||
.filter((t) => !t.projectId || t.projectId === Number(selectedProjectId))
|
||||
.map((template) => (
|
||||
<Card key={template.id} className="p-6 hover:shadow-md transition-shadow">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{template.correspondenceType?.typeName || 'Default Format'}
|
||||
</h3>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{template.project?.projectCode || selectedProjectName}
|
||||
</Badge>
|
||||
{template.description && <Badge variant="secondary">{template.description}</Badge>}
|
||||
</div>
|
||||
|
||||
<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}
|
||||
</div>
|
||||
<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}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm mt-2">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Type Code: </span>
|
||||
<span className="font-medium font-mono text-green-600 dark:text-green-400">
|
||||
{template.correspondenceType?.typeCode || 'DEFAULT'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Reset: </span>
|
||||
<span>
|
||||
{template.resetSequenceYearly ? 'Annually' : 'Continuous'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm mt-2">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Type Code: </span>
|
||||
<span className="font-medium font-mono text-green-600 dark:text-green-400">
|
||||
{template.correspondenceType?.typeCode || 'DEFAULT'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => handleEdit(template)}>
|
||||
<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>
|
||||
<span className="text-muted-foreground">Reset: </span>
|
||||
<span>{template.resetSequenceYearly ? 'Annually' : 'Continuous'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SequenceViewer />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => handleEdit(template)}>
|
||||
<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>
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
<TabsContent value="metrics" className="space-y-4">
|
||||
<MetricsDashboard />
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium mb-4">Audit Logs</h3>
|
||||
<AuditLogsTable />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<div className="space-y-4">
|
||||
<SequenceViewer />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tools" className="space-y-4">
|
||||
<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>
|
||||
<TabsContent value="metrics" className="space-y-4">
|
||||
<MetricsDashboard />
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium mb-4">Audit Logs</h3>
|
||||
<AuditLogsTable />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tools" className="space-y-4">
|
||||
<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>
|
||||
|
||||
<TemplateTester
|
||||
open={isTesting}
|
||||
onOpenChange={setIsTesting}
|
||||
template={testTemplate}
|
||||
/>
|
||||
<TemplateTester open={isTesting} onOpenChange={setIsTesting} template={testTemplate} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,64 +1,43 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
|
||||
import { masterDataService } from "@/lib/services/master-data.service";
|
||||
import { contractService } from "@/lib/services/contract.service";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { GenericCrudTable } from '@/components/admin/reference/generic-crud-table';
|
||||
import { masterDataService } from '@/lib/services/master-data.service';
|
||||
import { useContracts } from '@/hooks/use-master-data';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useState } from 'react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
||||
export default function DisciplinesPage() {
|
||||
const [contracts, setContracts] = useState<any[]>([]);
|
||||
const [selectedContractId, setSelectedContractId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [selectedContractId, setSelectedContractId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch contracts for filter and form options
|
||||
contractService.getAll().then((data) => {
|
||||
setContracts(Array.isArray(data) ? data : []);
|
||||
}).catch(err => {
|
||||
console.error("Failed to load contracts:", err);
|
||||
setContracts([]);
|
||||
});
|
||||
}, []);
|
||||
const { data: contractsData = [] } = useContracts();
|
||||
// Ensure we consistently use an array
|
||||
const contracts = Array.isArray(contractsData) ? contractsData : [];
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
accessorKey: "disciplineCode",
|
||||
header: "Code",
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono font-bold">
|
||||
{row.getValue("disciplineCode")}
|
||||
</span>
|
||||
),
|
||||
accessorKey: 'disciplineCode',
|
||||
header: 'Code',
|
||||
cell: ({ row }) => <span className="font-mono font-bold">{row.getValue('disciplineCode')}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "codeNameTh",
|
||||
header: "Name (TH)",
|
||||
accessorKey: 'codeNameTh',
|
||||
header: 'Name (TH)',
|
||||
},
|
||||
{
|
||||
accessorKey: "codeNameEn",
|
||||
header: "Name (EN)",
|
||||
accessorKey: 'codeNameEn',
|
||||
header: 'Name (EN)',
|
||||
},
|
||||
{
|
||||
accessorKey: "isActive",
|
||||
header: "Status",
|
||||
accessorKey: 'isActive',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs ${
|
||||
row.getValue("isActive")
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
row.getValue('isActive') ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{row.getValue("isActive") ? "Active" : "Inactive"}
|
||||
{row.getValue('isActive') ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
@@ -75,23 +54,17 @@ export default function DisciplinesPage() {
|
||||
entityName="Discipline"
|
||||
title="Disciplines Management"
|
||||
description="Manage system disciplines (e.g., ARCH, STR, MEC)"
|
||||
queryKey={["disciplines", selectedContractId ?? "all"]}
|
||||
fetchFn={() =>
|
||||
masterDataService.getDisciplines(
|
||||
selectedContractId ? parseInt(selectedContractId) : undefined
|
||||
)
|
||||
}
|
||||
queryKey={['disciplines', selectedContractId ?? 'all']}
|
||||
fetchFn={() => masterDataService.getDisciplines(selectedContractId ? parseInt(selectedContractId) : undefined)}
|
||||
createFn={(data) => masterDataService.createDiscipline(data)}
|
||||
updateFn={(id, data) => Promise.reject("Not implemented yet")} // Update endpoint needs to be verified/added if missing
|
||||
updateFn={(id, data) => Promise.reject('Not implemented yet')} // Update endpoint needs to be verified/added if missing
|
||||
deleteFn={(id) => masterDataService.deleteDiscipline(id)}
|
||||
columns={columns}
|
||||
filters={
|
||||
<div className="w-[300px]">
|
||||
<Select
|
||||
value={selectedContractId || "all"}
|
||||
onValueChange={(val) =>
|
||||
setSelectedContractId(val === "all" ? null : val)
|
||||
}
|
||||
value={selectedContractId || 'all'}
|
||||
onValueChange={(val) => setSelectedContractId(val === 'all' ? null : val)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Filter by Contract" />
|
||||
@@ -109,26 +82,26 @@ export default function DisciplinesPage() {
|
||||
}
|
||||
fields={[
|
||||
{
|
||||
name: "contractId",
|
||||
label: "Contract",
|
||||
type: "select",
|
||||
name: 'contractId',
|
||||
label: 'Contract',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: contractOptions,
|
||||
},
|
||||
{
|
||||
name: "disciplineCode",
|
||||
label: "Code",
|
||||
type: "text",
|
||||
name: 'disciplineCode',
|
||||
label: 'Code',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "codeNameTh",
|
||||
label: "Name (TH)",
|
||||
type: "text",
|
||||
name: 'codeNameTh',
|
||||
label: 'Name (TH)',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{ name: "codeNameEn", label: "Name (EN)", type: "text" },
|
||||
{ name: "isActive", label: "Active", type: "checkbox" },
|
||||
{ name: 'codeNameEn', label: 'Name (EN)', type: 'text' },
|
||||
{ name: 'isActive', label: 'Active', type: 'checkbox' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,66 +1,47 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
|
||||
import { masterDataService } from "@/lib/services/master-data.service";
|
||||
import { contractService } from "@/lib/services/contract.service";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { GenericCrudTable } from '@/components/admin/reference/generic-crud-table';
|
||||
import { masterDataService } from '@/lib/services/master-data.service';
|
||||
import { useContracts } from '@/hooks/use-master-data';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useState } from 'react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
||||
export default function RfaTypesPage() {
|
||||
const [contracts, setContracts] = useState<any[]>([]);
|
||||
const [selectedContractId, setSelectedContractId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [selectedContractId, setSelectedContractId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch contracts for filter and form options
|
||||
contractService.getAll().then((data) => {
|
||||
setContracts(Array.isArray(data) ? data : []);
|
||||
}).catch(err => {
|
||||
console.error("Failed to load contracts:", err);
|
||||
setContracts([]);
|
||||
});
|
||||
}, []);
|
||||
const { data: contractsData = [] } = useContracts();
|
||||
// Ensure we consistently use an array
|
||||
const contracts = Array.isArray(contractsData) ? contractsData : [];
|
||||
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
accessorKey: "typeCode",
|
||||
header: "Code",
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono font-bold">{row.getValue("typeCode")}</span>
|
||||
),
|
||||
accessorKey: 'typeCode',
|
||||
header: 'Code',
|
||||
cell: ({ row }) => <span className="font-mono font-bold">{row.getValue('typeCode')}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "typeNameTh",
|
||||
header: "Name (TH)",
|
||||
accessorKey: 'typeNameTh',
|
||||
header: 'Name (TH)',
|
||||
},
|
||||
{
|
||||
accessorKey: "typeNameEn",
|
||||
header: "Name (EN)",
|
||||
accessorKey: 'typeNameEn',
|
||||
header: 'Name (EN)',
|
||||
},
|
||||
{
|
||||
accessorKey: "remark",
|
||||
header: "Remark",
|
||||
accessorKey: 'remark',
|
||||
header: 'Remark',
|
||||
},
|
||||
{
|
||||
accessorKey: "isActive",
|
||||
header: "Status",
|
||||
accessorKey: 'isActive',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs ${
|
||||
row.getValue("isActive")
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
row.getValue('isActive') ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{row.getValue("isActive") ? "Active" : "Inactive"}
|
||||
{row.getValue('isActive') ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
@@ -76,12 +57,8 @@ export default function RfaTypesPage() {
|
||||
<GenericCrudTable
|
||||
entityName="RFA Type"
|
||||
title="RFA Types Management"
|
||||
queryKey={["rfa-types", selectedContractId ?? "all"]}
|
||||
fetchFn={() =>
|
||||
masterDataService.getRfaTypes(
|
||||
selectedContractId ? parseInt(selectedContractId) : undefined
|
||||
)
|
||||
}
|
||||
queryKey={['rfa-types', selectedContractId ?? 'all']}
|
||||
fetchFn={() => masterDataService.getRfaTypes(selectedContractId ? parseInt(selectedContractId) : undefined)}
|
||||
createFn={(data) => masterDataService.createRfaType(data)}
|
||||
updateFn={(id, data) => masterDataService.updateRfaType(id, data)}
|
||||
deleteFn={(id) => masterDataService.deleteRfaType(id)}
|
||||
@@ -89,10 +66,8 @@ export default function RfaTypesPage() {
|
||||
filters={
|
||||
<div className="w-[300px]">
|
||||
<Select
|
||||
value={selectedContractId || "all"}
|
||||
onValueChange={(val) =>
|
||||
setSelectedContractId(val === "all" ? null : val)
|
||||
}
|
||||
value={selectedContractId || 'all'}
|
||||
onValueChange={(val) => setSelectedContractId(val === 'all' ? null : val)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Filter by Contract" />
|
||||
@@ -110,17 +85,17 @@ export default function RfaTypesPage() {
|
||||
}
|
||||
fields={[
|
||||
{
|
||||
name: "contractId",
|
||||
label: "Contract",
|
||||
type: "select",
|
||||
name: 'contractId',
|
||||
label: 'Contract',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: contractOptions,
|
||||
},
|
||||
{ name: "typeCode", label: "Code", type: "text", required: true },
|
||||
{ name: "typeNameTh", label: "Name (TH)", type: "text", required: true },
|
||||
{ name: "typeNameEn", label: "Name (EN)", type: "text" },
|
||||
{ name: "remark", label: "Remark", type: "textarea" },
|
||||
{ name: "isActive", label: "Active", type: "checkbox" },
|
||||
{ name: 'typeCode', label: 'Code', type: 'text', required: true },
|
||||
{ name: 'typeNameTh', label: 'Name (TH)', type: 'text', required: true },
|
||||
{ name: 'typeNameEn', label: 'Name (EN)', type: 'text' },
|
||||
{ name: 'remark', label: 'Remark', type: 'textarea' },
|
||||
{ name: 'isActive', label: 'Active', type: 'checkbox' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,8 +11,9 @@ import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { workflowApi } from '@/lib/api/workflows';
|
||||
import { Workflow, CreateWorkflowDto } from '@/types/workflow';
|
||||
import { useWorkflowDefinition, useCreateWorkflowDefinition, useUpdateWorkflowDefinition } from '@/hooks/use-workflows';
|
||||
import { Workflow } from '@/types/workflow';
|
||||
import { CreateWorkflowDefinitionDto } from '@/types/dto/workflow-engine/workflow-engine.dto';
|
||||
import { toast } from 'sonner';
|
||||
import { Save, ArrowLeft, Loader2 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
@@ -22,8 +23,6 @@ export default function WorkflowEditPage() {
|
||||
const router = useRouter();
|
||||
const id = params?.id === 'new' ? null : Number(params?.id);
|
||||
|
||||
const [loading, setLoading] = useState(!!id);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [workflowData, setWorkflowData] = useState<Partial<Workflow>>({
|
||||
workflowName: '',
|
||||
description: '',
|
||||
@@ -32,84 +31,77 @@ export default function WorkflowEditPage() {
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const { data: fetchedWorkflow, isLoading: loadingWorkflow } = useWorkflowDefinition(id as number);
|
||||
const createMutation = useCreateWorkflowDefinition();
|
||||
const updateMutation = useUpdateWorkflowDefinition();
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
const fetchWorkflow = async () => {
|
||||
try {
|
||||
const data = await workflowApi.getWorkflow(id);
|
||||
if (data) {
|
||||
setWorkflowData(data);
|
||||
} else {
|
||||
toast.error("Workflow not found");
|
||||
router.push('/admin/workflows');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Failed to load workflow");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchWorkflow();
|
||||
if (fetchedWorkflow) {
|
||||
setWorkflowData(fetchedWorkflow);
|
||||
}
|
||||
}, [id, router]);
|
||||
}, [fetchedWorkflow]);
|
||||
|
||||
const loading = (!!id && loadingWorkflow) || createMutation.isPending || updateMutation.isPending;
|
||||
const saving = createMutation.isPending || updateMutation.isPending;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!workflowData.workflowName) {
|
||||
toast.error("Workflow name is required");
|
||||
return;
|
||||
toast.error('Workflow name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const dto: CreateWorkflowDto = {
|
||||
workflowName: workflowData.workflowName || '',
|
||||
description: workflowData.description || '',
|
||||
workflowType: workflowData.workflowType || 'CORRESPONDENCE',
|
||||
dslDefinition: workflowData.dslDefinition || '',
|
||||
};
|
||||
const dto: CreateWorkflowDefinitionDto = {
|
||||
workflow_code: workflowData.workflowType || 'CORRESPONDENCE',
|
||||
dsl: {
|
||||
workflowName: workflowData.workflowName,
|
||||
description: workflowData.description,
|
||||
dslDefinition: workflowData.dslDefinition,
|
||||
},
|
||||
is_active: workflowData.isActive,
|
||||
};
|
||||
|
||||
if (id) {
|
||||
await workflowApi.updateWorkflow(id, dto);
|
||||
toast.success("Workflow updated successfully");
|
||||
} else {
|
||||
await workflowApi.createWorkflow(dto);
|
||||
toast.success("Workflow created successfully");
|
||||
router.push('/admin/workflows');
|
||||
}
|
||||
if (id) {
|
||||
await updateMutation.mutateAsync({ id, data: dto });
|
||||
toast.success('Workflow updated successfully');
|
||||
} else {
|
||||
await createMutation.mutateAsync(dto);
|
||||
toast.success('Workflow created successfully');
|
||||
router.push('/admin/workflows');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Failed to save workflow");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
toast.error('Failed to save workflow');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6 max-w-7xl mx-auto">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin/workflows">
|
||||
<Button variant="ghost" size="icon">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{id ? 'Edit Workflow' : 'New Workflow'}</h1>
|
||||
<p className="text-muted-foreground">{id ? `Version ${workflowData.version}` : 'Create a new workflow definition'}</p>
|
||||
</div>
|
||||
<Link href="/admin/workflows">
|
||||
<Button variant="ghost" size="icon">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{id ? 'Edit Workflow' : 'New Workflow'}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{id ? `Version ${workflowData.version}` : 'Create a new workflow definition'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Link href="/admin/workflows">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</Link>
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
{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="lg:col-span-1 space-y-6">
|
||||
<Card className="p-6">
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
<Label htmlFor="name">Workflow Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={workflowData.workflowName}
|
||||
onChange={(e) =>
|
||||
setWorkflowData({
|
||||
...workflowData,
|
||||
workflowName: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="e.g. Standard RFA Workflow"
|
||||
/>
|
||||
</div>
|
||||
<Card className="p-6">
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
<Label htmlFor="name">Workflow Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={workflowData.workflowName}
|
||||
onChange={(e) =>
|
||||
setWorkflowData({
|
||||
...workflowData,
|
||||
workflowName: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="e.g. Standard RFA Workflow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="desc">Description</Label>
|
||||
<Textarea
|
||||
id="desc"
|
||||
value={workflowData.description}
|
||||
onChange={(e) =>
|
||||
setWorkflowData({
|
||||
...workflowData,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Describe the purpose of this workflow"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="desc">Description</Label>
|
||||
<Textarea
|
||||
id="desc"
|
||||
value={workflowData.description}
|
||||
onChange={(e) =>
|
||||
setWorkflowData({
|
||||
...workflowData,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Describe the purpose of this workflow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="type">Workflow Type</Label>
|
||||
<Select
|
||||
value={workflowData.workflowType}
|
||||
onValueChange={(value: Workflow['workflowType']) =>
|
||||
setWorkflowData({ ...workflowData, workflowType: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="CORRESPONDENCE">Correspondence</SelectItem>
|
||||
<SelectItem value="RFA">RFA</SelectItem>
|
||||
<SelectItem value="DRAWING">Drawing</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div>
|
||||
<Label htmlFor="type">Workflow Type</Label>
|
||||
<Select
|
||||
value={workflowData.workflowType}
|
||||
onValueChange={(value: Workflow['workflowType']) =>
|
||||
setWorkflowData({ ...workflowData, workflowType: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="CORRESPONDENCE">Correspondence</SelectItem>
|
||||
<SelectItem value="RFA">RFA</SelectItem>
|
||||
<SelectItem value="DRAWING">Drawing</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
<Tabs defaultValue="dsl" className="w-full">
|
||||
<TabsList className="w-full justify-start">
|
||||
<TabsTrigger value="dsl">DSL Editor</TabsTrigger>
|
||||
<TabsTrigger value="visual">Visual Builder</TabsTrigger>
|
||||
</TabsList>
|
||||
<Tabs defaultValue="dsl" className="w-full">
|
||||
<TabsList className="w-full justify-start">
|
||||
<TabsTrigger value="dsl">DSL Editor</TabsTrigger>
|
||||
<TabsTrigger value="visual">Visual Builder</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="dsl" className="mt-4">
|
||||
<DSLEditor
|
||||
initialValue={workflowData.dslDefinition}
|
||||
onChange={(value) =>
|
||||
setWorkflowData({ ...workflowData, dslDefinition: value })
|
||||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="dsl" className="mt-4">
|
||||
<DSLEditor
|
||||
initialValue={workflowData.dslDefinition}
|
||||
onChange={(value) => setWorkflowData({ ...workflowData, dslDefinition: value })}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="visual" className="mt-4 h-[600px]">
|
||||
<VisualWorkflowBuilder
|
||||
dslString={workflowData.dslDefinition}
|
||||
onDslChange={(newDsl) => setWorkflowData({ ...workflowData, dslDefinition: newDsl })}
|
||||
onSave={() => toast.info("Visual state saving not implemented in this demo")}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="visual" className="mt-4 h-[600px]">
|
||||
<VisualWorkflowBuilder
|
||||
dslString={workflowData.dslDefinition}
|
||||
onDslChange={(newDsl) => setWorkflowData({ ...workflowData, dslDefinition: newDsl })}
|
||||
onSave={() => toast.info('Visual state saving not implemented in this demo')}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Plus, Edit, Copy, Trash, Loader2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Workflow } from "@/types/workflow";
|
||||
import { workflowApi } from "@/lib/api/workflows";
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Plus, Edit, Copy, Trash, Loader2 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Workflow } from '@/types/workflow';
|
||||
import { workflowApi } from '@/lib/api/workflows';
|
||||
|
||||
export default function WorkflowsPage() {
|
||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||
@@ -20,7 +20,7 @@ export default function WorkflowsPage() {
|
||||
const data = await workflowApi.getWorkflows();
|
||||
setWorkflows(data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch workflows", error);
|
||||
console.error('Failed to fetch workflows', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -34,9 +34,7 @@ export default function WorkflowsPage() {
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Workflow Configuration</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Manage workflow definitions and routing rules
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-1">Manage workflow definitions and routing rules</p>
|
||||
</div>
|
||||
<Link href="/admin/workflows/new">
|
||||
<Button>
|
||||
@@ -57,24 +55,20 @@ export default function WorkflowsPage() {
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{workflow.workflowName}
|
||||
</h3>
|
||||
<Badge variant={workflow.isActive ? "default" : "secondary"} className={workflow.isActive ? "bg-green-600 hover:bg-green-700" : ""}>
|
||||
{workflow.isActive ? "Active" : "Inactive"}
|
||||
<h3 className="text-lg font-semibold">{workflow.workflowName}</h3>
|
||||
<Badge
|
||||
variant={workflow.isActive ? 'default' : 'secondary'}
|
||||
className={workflow.isActive ? 'bg-green-600 hover:bg-green-700' : ''}
|
||||
>
|
||||
{workflow.isActive ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
<Badge variant="outline">v{workflow.version}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
{workflow.description}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mb-3">{workflow.description}</p>
|
||||
<div className="flex gap-6 text-sm text-muted-foreground">
|
||||
<span>Type: {workflow.workflowType}</span>
|
||||
<span>Steps: {workflow.stepCount}</span>
|
||||
<span>
|
||||
Updated:{" "}
|
||||
{new Date(workflow.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
<span>Updated: {new Date(workflow.updatedAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,11 +79,16 @@ export default function WorkflowsPage() {
|
||||
Edit
|
||||
</Button>
|
||||
</Link>
|
||||
<Button variant="outline" size="sm" onClick={() => alert("Clone functionality mocked")}>
|
||||
<Button variant="outline" size="sm" onClick={() => alert('Clone functionality mocked')}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Clone
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="text-destructive hover:text-destructive" onClick={() => alert("Delete functionality mocked")}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => alert('Delete functionality mocked')}
|
||||
>
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
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
|
||||
|
||||
**Project Name:** Laem Chabang Port Phase 3 - Document Management System (LCBP3-DMS)
|
||||
**Version:** 1.6.0
|
||||
**Version:** 1.8.0
|
||||
**Status:** Active Development (~95% Complete)
|
||||
**Last Updated:** 2025-12-13
|
||||
**Last Updated:** 2026-02-20
|
||||
|
||||
---
|
||||
|
||||
@@ -140,8 +140,9 @@ LCBP3-DMS is a comprehensive Document Management System (DMS) designed specifica
|
||||
- **Language:** TypeScript
|
||||
- **Styling:** Tailwind CSS
|
||||
- **UI Components:** Shadcn/UI
|
||||
- **State Management:** React Context / Zustand
|
||||
- **Forms:** React Hook Form + Zod
|
||||
- **Server State:** TanStack Query
|
||||
- **Client State:** Zustand
|
||||
- **Form State:** React Hook Form + Zod
|
||||
- **API Client:** Axios
|
||||
|
||||
### Infrastructure
|
||||
@@ -384,10 +385,10 @@ lcbp3/
|
||||
|
||||
## 📝 Document Control
|
||||
|
||||
- **Version:** 1.7.0
|
||||
- **Version:** 1.8.0
|
||||
- **Status:** Active Development
|
||||
- **Last Updated:** 2025-12-18
|
||||
- **Next Review:** 2026-01-01
|
||||
- **Last Updated:** 2026-02-20
|
||||
- **Next Review:** 2026-03-31
|
||||
- **Owner:** System Architect
|
||||
- **Classification:** Internal Use Only
|
||||
|
||||
@@ -397,6 +398,8 @@ lcbp3/
|
||||
|
||||
| Version | Date | Description |
|
||||
| ------- | ---------- | ------------------------------------------ |
|
||||
| 1.8.0 | 2026-02-20 | Contract Categories Page Crash Fix |
|
||||
| 1.7.0 | 2025-12-18 | Schema refactoring, documentation updated |
|
||||
| 1.6.0 | 2025-12-13 | Schema refactoring, documentation updated |
|
||||
| 1.5.1 | 2025-12-09 | TASK-FE-011/012 completed, docs updated |
|
||||
| 1.5.1 | 2025-12-02 | Reorganized documentation structure |
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
|
||||
| Attribute | Value |
|
||||
| ------------------ | -------------------------------- |
|
||||
| **Version** | 1.7.0 |
|
||||
| **Version** | 1.8.0 |
|
||||
| **Status** | Active |
|
||||
| **Last Updated** | 2025-12-18 |
|
||||
| **Last Updated** | 2026-02-20 |
|
||||
| **Owner** | Nattanin Peancharoen |
|
||||
| **Classification** | Internal Technical Documentation |
|
||||
|
||||
@@ -200,7 +200,9 @@ Layer 6: File Security (Virus Scanning, Access Control)
|
||||
| **Language** | TypeScript (ESM) | Type-safe JavaScript |
|
||||
| **Styling** | Tailwind CSS + PostCSS | Utility-first CSS |
|
||||
| **Components** | shadcn/ui | Accessible Component Library |
|
||||
| **State Management** | TanStack Query + React Hook Form | Server State + Form State |
|
||||
| **Server State** | TanStack Query | Server State Management |
|
||||
| **Client State** | Zustand | Client State Management |
|
||||
| **Form State** | React Hook Form + Zod | Form State Management |
|
||||
| **Validation** | Zod | Schema Validation |
|
||||
| **Testing** | Vitest + Playwright | Unit + E2E Testing |
|
||||
|
||||
@@ -340,7 +342,7 @@ graph TB
|
||||
MariaDB[(MariaDB 11.8<br/>Primary Database)]
|
||||
Redis[(Redis<br/>Cache + Queue)]
|
||||
Elastic[Elasticsearch<br/>Search Engine]
|
||||
Storage[File Storage<br/>/share/dms-data]
|
||||
Storage[File Storage<br/>/share/np-dms/data/]
|
||||
end
|
||||
|
||||
subgraph "Integration Layer"
|
||||
@@ -490,7 +492,7 @@ sequenceDiagram
|
||||
|
||||
<div align="center">
|
||||
|
||||
**LCBP3-DMS Architecture Specification v1.6.0**
|
||||
**LCBP3-DMS Architecture Specification v1.8.0**
|
||||
|
||||
[System Architecture](02-01-system-architecture.md) • [API Design](02-02-api-design.md) • [Data Model](02-03-data-model.md)
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
**Chosen Option:** **Zustand (Client State) + Native Fetch with Server Components (Server State)**
|
||||
**Chosen Option:** **Zustand (Client State) + TanStack Query (Server State) + React Hook Form + Zod (Form State)**
|
||||
|
||||
### Rationale
|
||||
|
||||
@@ -103,8 +103,12 @@
|
||||
|
||||
**For Server State (API data):**
|
||||
|
||||
- Use **Server Components** + **SWR** (เฉพาะที่จำเป็น)
|
||||
- Server Components ดึงข้อมูลฝั่ง Server ไม่ต้องจัดการ state
|
||||
- Use **TanStack Query** (React Query) สำหรับ data fetching, caching, synchronization
|
||||
- Server Components สำหรับ initial data loading
|
||||
|
||||
**For Form State:**
|
||||
|
||||
- Use **React Hook Form + Zod** สำหรับ type-safe form management
|
||||
|
||||
---
|
||||
|
||||
@@ -253,38 +257,35 @@ export default async function CorrespondencesPage() {
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Client-Side Fetching (with SWR for real-time data)
|
||||
### 6. Client-Side Fetching (with TanStack Query)
|
||||
|
||||
```bash
|
||||
npm install swr
|
||||
npm install @tanstack/react-query
|
||||
```
|
||||
|
||||
```typescript
|
||||
// File: components/correspondences/realtime-list.tsx
|
||||
// File: components/correspondences/correspondence-list.tsx
|
||||
'use client';
|
||||
|
||||
import useSWR from 'swr';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getCorrespondences } from '@/lib/api/correspondences';
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
||||
|
||||
export function RealtimeCorrespondenceList() {
|
||||
const { data, error, isLoading, mutate } = useSWR(
|
||||
'/api/correspondences',
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: 30000, // Auto refresh every 30s
|
||||
}
|
||||
);
|
||||
export function CorrespondenceList() {
|
||||
const { data, error, isLoading, refetch } = useQuery({
|
||||
queryKey: ['correspondences'],
|
||||
queryFn: getCorrespondences,
|
||||
refetchInterval: 30000, // Auto refresh every 30s
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error loading data</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{data.map((item) => (
|
||||
{data?.map((item) => (
|
||||
<div key={item.id}>{item.subject}</div>
|
||||
))}
|
||||
<button onClick={() => mutate()}>Refresh</button>
|
||||
<button onClick={() => refetch()}>Refresh</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -347,14 +348,16 @@ export const useUIStore = create<UIState>()(
|
||||
- SEO-important content
|
||||
- Data that doesn't need real-time updates
|
||||
|
||||
### When to Use SWR (Client-Side Server State)
|
||||
### When to Use TanStack Query (Client-Side Server State)
|
||||
|
||||
✅ Use SWR for:
|
||||
✅ Use TanStack Query for:
|
||||
|
||||
- Real-time data (notifications count)
|
||||
- Polling/Auto-refresh data
|
||||
- User-specific data that changes often
|
||||
- Optimistic UI updates
|
||||
- Complex cache invalidation
|
||||
- Paginated/infinite scroll data
|
||||
|
||||
---
|
||||
|
||||
@@ -392,9 +395,10 @@ export const useUIStore = create<UIState>()(
|
||||
## References
|
||||
|
||||
- [Zustand Documentation](https://github.com/pmndrs/zustand)
|
||||
- [SWR Documentation](https://swr.vercel.app/)
|
||||
- [TanStack Query Documentation](https://tanstack.com/query/latest)
|
||||
- [React Hook Form Documentation](https://react-hook-form.com/)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-12-01
|
||||
**Last Updated:** 2026-02-20
|
||||
**Next Review:** 2026-06-01
|
||||
|
||||
Reference in New Issue
Block a user