260320:1131 Refactor Overrall #01
Build and Deploy / deploy (push) Has been cancelled

This commit is contained in:
admin
2026-03-20 11:31:27 +07:00
parent f1b81a7d0d
commit 1d3479770b
147 changed files with 1745 additions and 1567 deletions
@@ -308,7 +308,7 @@ export default function ContractsPage() {
<SelectValue placeholder="Select Project" />
</SelectTrigger>
<SelectContent>
{(projects as any[])?.map((p) => (
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[])?.map((p) => (
<SelectItem key={p.uuid || p.id} value={String(p.id || p.uuid)}>
{p.projectCode} - {p.projectName}
</SelectItem>
@@ -70,7 +70,7 @@ export default function ContractCategoriesPage() {
)}
</SelectTrigger>
<SelectContent>
{(projects as any[]).map((project) => (
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[]).map((project) => (
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
{project.projectCode} - {project.projectName}
</SelectItem>
@@ -103,9 +103,7 @@ export default function ContractCategoriesPage() {
description="Manage main categories (หมวดหมู่หลัก) for contract drawings"
queryKey={['contract-drawing-categories', String(selectedProjectId)]}
fetchFn={async () => {
console.log(`Fetching Contract Categories for project ${selectedProjectId}`);
const data = await drawingMasterDataService.getContractCategories(selectedProjectId);
console.log('Contract Categories Data:', data);
return data;
}}
createFn={(data: Record<string, unknown>) => drawingMasterDataService.createContractCategory({ ...(data as unknown as CreateContractCategoryDto), projectId: selectedProjectId })}
@@ -62,7 +62,7 @@ export default function ContractSubCategoriesPage() {
)}
</SelectTrigger>
<SelectContent>
{(projects as any[]).map((project) => (
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[]).map((project) => (
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
{project.projectCode} - {project.projectName}
</SelectItem>
@@ -95,9 +95,7 @@ export default function ContractSubCategoriesPage() {
description="Manage sub-categories (หมวดหมู่ย่อย) for contract drawings"
queryKey={['contract-drawing-sub-categories', String(selectedProjectId)]}
fetchFn={async () => {
console.log(`Fetching Contract Sub-Categories for project ${selectedProjectId}`);
const data = await drawingMasterDataService.getContractSubCategories(selectedProjectId);
console.log('Contract Sub-Categories Data:', data);
return data;
}}
createFn={(data: Record<string, unknown>) =>
@@ -74,7 +74,7 @@ export default function ContractVolumesPage() {
)}
</SelectTrigger>
<SelectContent>
{(projects as any[]).map((project) => (
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[]).map((project) => (
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
{project.projectCode} - {project.projectName}
</SelectItem>
@@ -73,7 +73,7 @@ export default function ShopMainCategoriesPage() {
)}
</SelectTrigger>
<SelectContent>
{(projects as any[]).map((project) => (
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[]).map((project) => (
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
{project.projectCode} - {project.projectName}
</SelectItem>
@@ -106,9 +106,7 @@ export default function ShopMainCategoriesPage() {
description="Manage main categories (หมวดหมู่หลัก) for shop drawings"
queryKey={['shop-drawing-main-categories', String(selectedProjectId)]}
fetchFn={async () => {
console.log(`Fetching Shop Main Categories for project ${selectedProjectId}`);
const data = await drawingMasterDataService.getShopMainCategories(selectedProjectId);
console.log('Shop Main Categories Data:', data);
return data;
}}
createFn={(data: Record<string, unknown>) =>
@@ -22,8 +22,6 @@ export default function ShopSubCategoriesPage() {
const [selectedProjectId, setSelectedProjectId] = useState<string | undefined>(undefined);
const { data: projects = [], isLoading: isLoadingProjects } = useProjects();
console.log('Projects Data:', projects);
const columns: ColumnDef<SubCategory>[] = [
{
accessorKey: 'subCategoryCode',
@@ -75,7 +73,7 @@ export default function ShopSubCategoriesPage() {
)}
</SelectTrigger>
<SelectContent>
{(projects as any[]).map((project) => (
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[]).map((project) => (
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}>
{project.projectCode} - {project.projectName}
</SelectItem>
@@ -108,9 +106,7 @@ export default function ShopSubCategoriesPage() {
description="Manage sub-categories (หมวดหมู่ย่อย) for shop drawings"
queryKey={['shop-drawing-sub-categories', String(selectedProjectId)]}
fetchFn={async () => {
console.log(`Fetching Shop Sub-Categories for project ${selectedProjectId}`);
const data = await drawingMasterDataService.getShopSubCategories(selectedProjectId);
console.log('Shop Sub-Categories Data:', data);
return data;
}}
createFn={(data: Record<string, unknown>) =>
@@ -42,7 +42,6 @@ export default function EditTemplatePage() {
}
} catch (error) {
toast.error('Failed to load template');
console.error('[EditTemplatePage] fetchTemplate:', error);
} finally {
setLoading(false);
}
@@ -57,7 +56,6 @@ export default function EditTemplatePage() {
router.push('/admin/doc-control/numbering');
} catch (error) {
toast.error('Failed to update template');
console.error('[EditTemplatePage] handleSave:', error);
}
};
@@ -27,7 +27,6 @@ export default function NewTemplatePage() {
router.push("/admin/numbering");
} catch (error) {
toast.error('Failed to create template');
console.error('[NewTemplatePage]', error);
}
};
@@ -15,6 +15,13 @@ import { toast } from 'sonner';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useProjects, useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data';
interface ProjectItem {
id: number | string;
uuid?: string;
projectName: string;
projectCode: string;
}
import { ManualOverrideForm } from '@/components/numbering/manual-override-form';
import { MetricsDashboard } from '@/components/numbering/metrics-dashboard';
import { AuditLogsTable } from '@/components/numbering/audit-logs-table';
@@ -30,8 +37,8 @@ export default function NumberingPage() {
useEffect(() => {
if (projects.length > 0 && !selectedProjectId) {
const first = projects[0] as any;
setSelectedProjectId(String(first.id ?? first.uuid));
const first = projects[0] as ProjectItem;
setSelectedProjectId(String(first.uuid ?? first.id));
}
}, [projects, selectedProjectId]);
@@ -41,14 +48,14 @@ export default function NumberingPage() {
const [isTesting, setIsTesting] = useState(false);
const [testTemplate, setTestTemplate] = useState<NumberingTemplate | null>(null);
const selectedProject = projects.find((p: any) => String(p.id ?? p.uuid) === selectedProjectId) as any;
const selectedProject = (projects as ProjectItem[]).find((p) => String(p.uuid ?? p.id) === selectedProjectId);
const selectedProjectName = selectedProject?.projectName || 'Unknown Project';
// Master Data
const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
const { data: contracts = [] } = useContracts(selectedProjectId as any); // Passing UUID/ID string
const firstContract = contracts[0] as any;
const contractId = firstContract?.id || firstContract?.uuid;
const { data: contracts = [] } = useContracts(selectedProjectId);
const firstContract = contracts[0] as { id?: number; uuid?: string } | undefined;
const contractId = firstContract?.uuid ?? firstContract?.id;
const { data: disciplines = [] } = useDisciplines(contractId);
const { data: templateResponse, isLoading: isLoadingTemplates } = useTemplates();
@@ -57,7 +64,7 @@ export default function NumberingPage() {
// Extract templates array from response
const templates: NumberingTemplate[] = Array.isArray(templateResponse)
? templateResponse
: ((templateResponse as any)?.data ?? []);
: ((templateResponse as { data?: NumberingTemplate[] } | undefined)?.data ?? []);
const handleEdit = (template?: NumberingTemplate) => {
setActiveTemplate(template);
@@ -84,7 +91,7 @@ export default function NumberingPage() {
<div className="p-6 max-w-4xl mx-auto animate-in fade-in slide-in-from-bottom-4">
<TemplateEditor
template={activeTemplate}
projectId={selectedProjectId as any}
projectId={selectedProjectId}
projectName={selectedProjectName}
correspondenceTypes={correspondenceTypes}
disciplines={disciplines}
@@ -108,8 +115,8 @@ export default function NumberingPage() {
<SelectValue placeholder="Select Project" />
</SelectTrigger>
<SelectContent>
{(projects as any[]).map((project) => (
<SelectItem key={project.id ?? project.uuid} value={String(project.id ?? project.uuid)}>
{(projects as ProjectItem[]).map((project) => (
<SelectItem key={String(project.uuid ?? project.id)} value={String(project.uuid ?? project.id)}>
{project.projectCode} - {project.projectName}
</SelectItem>
))}
@@ -137,7 +144,7 @@ export default function NumberingPage() {
<div className="lg:col-span-2 space-y-4">
<div className="grid gap-4">
{templates
.filter((t: any) => !t.projectId || String(t.project?.id ?? t.project?.uuid) === selectedProjectId || t.project?.uuid === selectedProjectId)
.filter((t) => !t.projectId || String(t.project?.id ?? t.project?.uuid) === selectedProjectId || t.project?.uuid === selectedProjectId)
.map((template) => (
<Card key={template.id} className="p-6 hover:shadow-md transition-shadow">
<div className="flex justify-between items-start">
@@ -202,11 +209,11 @@ export default function NumberingPage() {
<TabsContent value="tools" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<ManualOverrideForm projectId={selectedProjectId as any} />
<VoidReplaceForm projectId={selectedProjectId as any} />
<ManualOverrideForm projectId={selectedProjectId} />
<VoidReplaceForm projectId={selectedProjectId} />
<CancelNumberForm />
<div className="md:col-span-2">
<BulkImportForm projectId={selectedProjectId as any} />
<BulkImportForm projectId={selectedProjectId} />
</div>
</div>
</TabsContent>
@@ -58,12 +58,15 @@ export default function DisciplinesPage() {
fetchFn={async () => {
const items = await masterDataService.getDisciplines(selectedContractId ? selectedContractId : undefined);
// ADR-019: Map contractId INT → contract UUID for edit mode select matching
return (items as any[]).map((item: any) => ({
...item,
contractId: item.contract?.id || item.contract?.uuid || String(item.contractId),
}));
return (items as Record<string, unknown>[]).map((item) => {
const rec = item as { contract?: { id?: number; uuid?: string }; contractId?: number };
return {
...item,
contractId: rec.contract?.id || rec.contract?.uuid || String(rec.contractId),
};
});
}}
createFn={(data: Record<string, unknown>) => masterDataService.createDiscipline(data as any)}
createFn={(data) => masterDataService.createDiscipline(data as unknown as Parameters<typeof masterDataService.createDiscipline>[0])}
updateFn={(id, data) => Promise.reject('Not implemented yet')}
deleteFn={(id) => masterDataService.deleteDiscipline(id)}
columns={columns}
@@ -61,12 +61,15 @@ export default function RfaTypesPage() {
fetchFn={async () => {
const items = await masterDataService.getRfaTypes(selectedContractId ? selectedContractId : undefined);
// ADR-019: Map contractId INT → contract UUID for edit mode select matching
return (items as any[]).map((item: any) => ({
...item,
contractId: item.contract?.id || item.contract?.uuid || String(item.contractId),
}));
return (items as Record<string, unknown>[]).map((item) => {
const rec = item as { contract?: { id?: number; uuid?: string }; contractId?: number };
return {
...item,
contractId: rec.contract?.id || rec.contract?.uuid || String(rec.contractId),
};
});
}}
createFn={(data: Record<string, unknown>) => masterDataService.createRfaType(data as any)}
createFn={(data) => masterDataService.createRfaType(data as unknown as Parameters<typeof masterDataService.createRfaType>[0])}
updateFn={(id, data) => masterDataService.updateRfaType(id, data)}
deleteFn={(id) => masterDataService.deleteRfaType(id)}
columns={columns}
@@ -73,10 +73,13 @@ export default function TagsPage() {
fetchFn={async () => {
const items = await masterDataService.getTags();
// ADR-019: Map project_id INT → project UUID for edit mode select matching
return (items as any[]).map((item: any) => ({
...item,
project_id: item.project?.id || item.project?.uuid || (item.project_id ? String(item.project_id) : null),
}));
return (items as Record<string, unknown>[]).map((item) => {
const rec = item as { project?: { id?: number; uuid?: string }; project_id?: number };
return {
...item,
project_id: rec.project?.id || rec.project?.uuid || (rec.project_id ? String(rec.project_id) : null),
};
});
}}
createFn={(data: Record<string, unknown>) => masterDataService.createTag(formatPayload(data) as unknown as CreateTagDto)}
updateFn={(id, data) => masterDataService.updateTag(id, formatPayload(data))}
@@ -71,7 +71,6 @@ export default function WorkflowEditPage() {
}
} catch (error) {
toast.error('Failed to save workflow');
console.error(error);
}
};
@@ -33,7 +33,6 @@ export default function NewWorkflowPage() {
router.push('/admin/doc-control/workflows');
} catch (error) {
toast.error('Failed to create workflow');
console.error('[NewWorkflowPage]', error);
} finally {
setSaving(false);
}
@@ -28,7 +28,6 @@ export default function SessionManagementPage() {
},
onError: (error) => {
toast.error('Failed to revoke session');
console.error(error);
},
});
+4 -4
View File
@@ -86,8 +86,8 @@ export default function AdminPage() {
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{stats.map((stat, index) => (
<Card key={index} className="hover:shadow-md transition-shadow">
{stats.map((stat) => (
<Card key={stat.title} className="hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
<stat.icon className={`h-4 w-4 ${stat.color}`} />
@@ -111,8 +111,8 @@ export default function AdminPage() {
<div>
<h2 className="text-xl font-semibold mb-4">Quick Access</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{quickLinks.map((link, index) => (
<Link key={index} href={link.href}>
{quickLinks.map((link) => (
<Link key={link.href} href={link.href}>
<Card className="h-full hover:bg-muted/50 transition-colors cursor-pointer border-l-4 border-l-transparent hover:border-l-primary">
<CardHeader>
<CardTitle className="flex items-center text-lg">
-2
View File
@@ -63,7 +63,6 @@ export default function LoginPage() {
if (result?.error) {
// กรณี Login ไม่สำเร็จ
console.error("Login failed:", result.error);
toast.error("เข้าสู่ระบบไม่สำเร็จ", {
description: "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง กรุณาลองใหม่",
});
@@ -77,7 +76,6 @@ export default function LoginPage() {
router.push("/dashboard");
router.refresh(); // Refresh เพื่อให้ Server Component รับรู้ Session ใหม่
} catch (error) {
console.error("Login error:", error);
toast.error("เกิดข้อผิดพลาด", {
description: "ระบบขัดข้อง กรุณาลองใหม่อีกครั้ง หรือติดต่อผู้ดูแลระบบ",
});
@@ -32,7 +32,7 @@ export default function MigrationErrorsPage() {
const res = await migrationService.getErrors({ limit: 100 });
setItems(res.items);
} catch (error) {
console.error("Failed to fetch errors", error);
// Failed to fetch errors - loading state handles display
} finally {
setLoading(false);
}
@@ -13,6 +13,7 @@ import {
} from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { format } from "date-fns";
@@ -41,7 +42,7 @@ export default function MigrationReviewQueuePage() {
setItems(res.items);
setSelectedIds([]); // reset selection on fetch
} catch (error) {
console.error("Failed to fetch queue", error);
// Failed to fetch queue - loading state handles display
} finally {
setLoading(false);
}
@@ -56,7 +57,7 @@ export default function MigrationReviewQueuePage() {
};
const handleToggleSelect = (id: number) => {
setSelectedIds((prev) =>
setSelectedIds((prev) =>
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
);
};
@@ -65,7 +66,7 @@ export default function MigrationReviewQueuePage() {
if (selectedIds.length === 0) return;
try {
setSubmitting(true);
const batchItems = items
.filter((i) => selectedIds.includes(i.id))
.map((item) => ({
@@ -94,11 +95,10 @@ export default function MigrationReviewQueuePage() {
{ items: batchItems, batchId },
batchId
);
fetchData();
} catch (error) {
console.error("Batch commit failed", error);
alert("Batch commit failed. See console for details.");
toast.error("Batch commit failed.");
} finally {
setSubmitting(false);
}
@@ -115,12 +115,12 @@ export default function MigrationReviewQueuePage() {
</div>
<div className="flex items-center gap-4">
{selectedIds.length > 0 && (
<Button
variant="default"
onClick={handleBatchApprove}
<Button
variant="default"
onClick={handleBatchApprove}
disabled={submitting}
>
<CheckSquareIcon className="mr-2 h-4 w-4" />
<CheckSquareIcon className="mr-2 h-4 w-4" />
{submitting ? "Processing..." : `Batch Approve (${selectedIds.length})`}
</Button>
)}
@@ -158,7 +158,7 @@ export default function MigrationReviewQueuePage() {
<TableHeader>
<TableRow>
<TableHead className="w-[50px]">
<Checkbox
<Checkbox
checked={items.length > 0 && selectedIds.length === items.length}
onCheckedChange={handleToggleSelectAll}
aria-label="Select all"
@@ -176,7 +176,7 @@ export default function MigrationReviewQueuePage() {
{items.map((item) => (
<TableRow key={item.id}>
<TableCell>
<Checkbox
<Checkbox
checked={selectedIds.includes(item.id)}
onCheckedChange={() => handleToggleSelect(item.id)}
aria-label={`Select item ${item.id}`}
@@ -185,14 +185,14 @@ export default function MigrationReviewQueuePage() {
<TableCell className="font-medium">{item.documentNumber}</TableCell>
<TableCell>{item.aiSuggestedCategory || "Unknown"}</TableCell>
<TableCell>
<Badge
<Badge
variant={
!item.aiConfidence
? "destructive"
: item.aiConfidence > 0.8
? "default"
: item.aiConfidence > 0.5
? "secondary"
!item.aiConfidence
? "destructive"
: item.aiConfidence > 0.8
? "default"
: item.aiConfidence > 0.5
? "secondary"
: "destructive"
}
>
@@ -87,7 +87,6 @@ export default function MigrationReviewPage() {
});
}
} catch (error) {
console.error("Failed to load queue item", error);
toast.error("Failed to load queue item");
} finally {
setLoading(false);
@@ -100,7 +99,7 @@ export default function MigrationReviewPage() {
try {
setSubmitting(true);
const issues = item.aiIssues || {};
const payload = {
document_number: values.document_number,
subject: values.subject,
@@ -123,12 +122,12 @@ export default function MigrationReviewPage() {
// Mock idempotency key based on timestamp to ensure uniqueness per approval retry
const idempotencyKey = `review-${item.id}-${Date.now()}`;
await migrationService.approveQueueItem(item.id, payload, idempotencyKey);
toast.success("Document approved and imported successfully");
router.push("/admin/migration");
} catch (error: any) {
console.error("Failed to approve item", error);
toast.error(error?.response?.data?.message || "Failed to approve and import");
} catch (error: unknown) {
const err = error as { response?: { data?: { message?: string } } };
toast.error(err?.response?.data?.message || "Failed to approve and import");
} finally {
setSubmitting(false);
}
@@ -142,8 +141,7 @@ export default function MigrationReviewPage() {
await migrationService.rejectQueueItem(item.id);
toast.success("Document rejected");
router.push("/admin/migration");
} catch (error: any) {
console.error("Failed to reject item", error);
} catch (error: unknown) {
toast.error("Failed to reject document");
} finally {
setSubmitting(false);
@@ -158,7 +156,7 @@ export default function MigrationReviewPage() {
return <div className="py-10 text-center text-red-500">Document not found</div>;
}
const pdfUrl = item.aiIssues?.source_file_path
const pdfUrl = item.aiIssues?.source_file_path
? migrationService.getStagingFileUrl(item.aiIssues.source_file_path)
: null;
@@ -240,7 +238,7 @@ export default function MigrationReviewPage() {
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
@@ -343,9 +341,9 @@ export default function MigrationReviewPage() {
<XCircleIcon className="w-4 h-4 mr-2" />
Reject
</Button>
<Button
type="submit"
className="flex-1 bg-green-600 hover:bg-green-700 text-white"
<Button
type="submit"
className="flex-1 bg-green-600 hover:bg-green-700 text-white"
disabled={submitting || item.status !== 'PENDING'}
>
<CheckCircleIcon className="w-4 h-4 mr-2" />
@@ -43,6 +43,12 @@ import Link from "next/link";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
// Force dynamic rendering to prevent build-time prerendering issues
export const dynamic = 'force-dynamic';
// Ensure this page is never statically generated
export const fetchCache = 'force-no-store';
// Form validation schema
const formSchema = z.object({
correspondenceId: z.string().min(1, "Please select a document"),
@@ -1,5 +1,11 @@
import { CorrespondenceForm } from "@/components/correspondences/form";
// Force dynamic rendering to prevent build-time prerendering issues
export const dynamic = 'force-dynamic';
// Ensure this page is never statically generated
export const fetchCache = 'force-no-store';
export default function NewCorrespondencePage() {
return (
<div className="max-w-4xl mx-auto py-6">
@@ -1,5 +1,11 @@
import { DrawingUploadForm } from "@/components/drawings/upload-form";
// Force dynamic rendering to prevent build-time prerendering issues
export const dynamic = 'force-dynamic';
// Ensure this page is never statically generated
export const fetchCache = 'force-no-store';
export default function DrawingUploadPage() {
return (
<div className="max-w-4xl mx-auto py-6">
+1 -1
View File
@@ -69,7 +69,7 @@ export default function ProfilePage() {
reset();
} catch (error) {
toast.error('ไม่สามารถเปลี่ยนรหัสผ่านได้: รหัสผ่านปัจจุบันไม่ถูกต้อง');
console.error('[ProfilePage] onPasswordSubmit:', error);
// Password change failed - toast shown
} finally {
setIsLoading(false);
}
@@ -29,6 +29,12 @@ import {
} from "@/components/ui/card";
import { toast } from "sonner";
// Force dynamic rendering to prevent build-time prerendering issues
export const dynamic = 'force-dynamic';
// Ensure this page is never statically generated
export const fetchCache = 'force-no-store';
// 1. กำหนด Schema สำหรับตรวจสอบข้อมูล (Validation)
// อ้างอิงจาก Data Dictionary ตาราง projects
const projectSchema = z.object({
@@ -74,7 +80,6 @@ export default function CreateProjectPage() {
try {
// เรียก API สร้างโครงการ (Mockup URL)
// ใน Phase หลัง Backend จะเตรียม Endpoint POST /projects ไว้ให้
console.log("Submitting project data:", data);
// จำลองการส่งข้อมูล (Artificial Delay)
await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -86,7 +91,7 @@ export default function CreateProjectPage() {
router.refresh();
} catch (error) {
toast.error('เกิดข้อผิดพลาดในการสร้างโครงการ');
console.error('[CreateProjectPage]', error);
// Project creation failed - toast shown
} finally {
setIsLoading(false);
}
@@ -1,5 +1,11 @@
import { RFAForm } from "@/components/rfas/form";
// Force dynamic rendering to prevent build-time prerendering issues
export const dynamic = 'force-dynamic';
// Ensure this page is never statically generated
export const fetchCache = 'force-no-store';
export default function NewRFAPage() {
return (
<div className="max-w-4xl mx-auto py-6">
@@ -8,6 +8,9 @@ import { TransmittalForm } from "@/components/transmittal/transmittal-form";
// Force dynamic rendering to prevent build-time prerendering issues
export const dynamic = 'force-dynamic';
// Ensure this page is never statically generated
export const fetchCache = 'force-no-store';
export default function CreateTransmittalPage() {
return (
<section className="space-y-6 max-w-4xl">
-85
View File
@@ -1,85 +0,0 @@
/* File: app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Base Color: Slate (Professional/Enterprise look) */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
/* Primary: Brand Blue for Actions */
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
/* Secondary: Muted/Gray for secondary actions */
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
/* Muted: For disabled or subtle text */
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
/* Accent: For hover states or highlights */
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
/* Destructive: For delete/danger actions */
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
/* Borders & Inputs */
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
/* Dark Mode (Prepared for future use) */
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@@ -57,15 +57,19 @@ interface Field {
options?: { label: string; value: string | number }[];
}
interface ApiError extends Error {
response?: { data?: { message?: string } };
}
interface GenericCrudTableProps<T> {
title: string;
description?: string;
entityName: string;
queryKey: any[];
queryKey: string[];
fetchFn: () => Promise<T[] | { data: T[] }>;
createFn: (data: any) => Promise<any>;
updateFn: (id: number, data: any) => Promise<any>;
deleteFn: (id: number) => Promise<any>;
createFn: (data: Record<string, unknown>) => Promise<unknown>;
updateFn: (id: number, data: Record<string, unknown>) => Promise<unknown>;
deleteFn: (id: number) => Promise<unknown>;
columns: ColumnDef<T>[];
fields: Field[];
filters?: React.ReactNode;
@@ -95,7 +99,7 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
});
// ADR-019: Support both direct array or wrapped data object
const data: T[] = Array.isArray(rawData) ? rawData : (rawData as any)?.data || [];
const data: T[] = Array.isArray(rawData) ? rawData : (rawData as { data?: T[] } | undefined)?.data || [];
const createMutation = useMutation({
mutationFn: createFn,
@@ -105,13 +109,13 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
setIsDialogOpen(false);
reset();
},
onError: (error: any) => {
onError: (error: ApiError) => {
toast.error(error.response?.data?.message || `Failed to create ${entityName}`);
},
});
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => updateFn(id, data),
mutationFn: ({ id, data }: { id: number; data: Record<string, unknown> }) => updateFn(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey });
toast.success(`${entityName} updated successfully`);
@@ -119,7 +123,7 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
setEditingId(null);
reset();
},
onError: (error: any) => {
onError: (error: ApiError) => {
toast.error(error.response?.data?.message || `Failed to update ${entityName}`);
},
});
@@ -131,7 +135,7 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
toast.success(`${entityName} deleted successfully`);
setItemToDelete(null);
},
onError: (error: any) => {
onError: (error: ApiError) => {
toast.error(error.response?.data?.message || `Failed to delete ${entityName}`);
},
});
@@ -184,19 +188,20 @@ export function GenericCrudTable<T extends { id?: number; uuid?: string }>({
setIsDialogOpen(true);
};
const handleEdit = (item: any) => {
setEditingId(item.id);
reset(item);
const handleEdit = (item: T) => {
setEditingId(item.id as number);
reset(item as Record<string, unknown>);
// Ensure select values are strings for Shadcn Select
fields.forEach(f => {
if (f.type === 'select' && item[f.name]) {
setValue(f.name, String(item[f.name]));
const record = item as Record<string, unknown>;
if (f.type === 'select' && record[f.name]) {
setValue(f.name, String(record[f.name]));
}
});
setIsDialogOpen(true);
};
const onSubmit = (formData: any) => {
const onSubmit = (formData: Record<string, unknown>) => {
if (editingItem) {
updateMutation.mutate({ id: editingItem, data: formData });
} else {
@@ -35,13 +35,15 @@ interface RbacMatrixProps {
}
const securityService = {
getRoles: async () => {
const response = await apiClient.get<any>("/users/roles");
return response.data?.data || response.data;
getRoles: async (): Promise<Role[]> => {
const response = await apiClient.get("/users/roles");
const data = response.data?.data || response.data;
return Array.isArray(data) ? data : [];
},
getPermissions: async () => {
const response = await apiClient.get<any>("/users/permissions");
return response.data?.data || response.data;
getPermissions: async (): Promise<Permission[]> => {
const response = await apiClient.get("/users/permissions");
const data = response.data?.data || response.data;
return Array.isArray(data) ? data : [];
},
updateRolePermissions: async (roleId: number, permissionIds: number[]) => {
// This endpoint might not exist as a bulk update, usually it's per role
@@ -137,9 +139,9 @@ export function RbacMatrix() {
<div>{perm.permissionName}</div>
<div className="text-xs text-muted-foreground">{perm.description}</div>
</TableCell>
{roles.map((role: any) => {
{roles.map((role) => {
// Assume role.permissions is populated
const currentRolePerms = role.permissions?.map((p: any) => p.permissionId) || [];
const currentRolePerms = role.permissions?.map((p) => p.permissionId) || [];
const activePerms = pendingChanges[role.roleId] || currentRolePerms;
const isChecked = activePerms.includes(perm.permissionId);
+14 -4
View File
@@ -108,7 +108,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
isActive: user.isActive,
lineId: user.lineId || "",
primaryOrganizationId: user.primaryOrganizationId?.toString(),
roleIds: user.roles?.map((r: any) => r.roleId) || [],
roleIds: user.roles?.map((r: { roleId: number }) => r.roleId) || [],
password: "",
confirmPassword: ""
});
@@ -158,7 +158,17 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
// Create req: Password mandatory
if (!payload.password) return; // Should allow Zod to catch or show error
createUser.mutate(payload as any, {
createUser.mutate({
username: payload.username,
email: payload.email,
firstName: payload.firstName,
lastName: payload.lastName,
password: payload.password,
isActive: payload.isActive ?? true,
lineId: payload.lineId,
primaryOrganizationId: payload.primaryOrganizationId,
roleIds: payload.roleIds ?? [],
}, {
onSuccess: () => onOpenChange(false),
});
}
@@ -230,7 +240,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
<SelectValue placeholder="Select Organization" />
</SelectTrigger>
<SelectContent>
{organizations?.map((org: any) => (
{organizations?.map((org: { uuid: string; organizationCode: string; organizationName: string }) => (
<SelectItem
key={org.uuid}
value={org.uuid}
@@ -300,7 +310,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
<Label className="mb-3 block">Roles</Label>
<div className="space-y-2 border p-3 rounded-md max-h-[200px] overflow-y-auto">
{Array.isArray(roles) && roles.length === 0 && <p className="text-sm text-muted-foreground">Loading roles...</p>}
{Array.isArray(roles) && roles.map((role: any) => (
{Array.isArray(roles) && roles.map((role: { roleId: number; roleName: string; description?: string }) => (
<div key={role.roleId} className="flex items-start space-x-2">
<Checkbox
id={`role-${role.roleId}`}
+19 -10
View File
@@ -15,20 +15,29 @@ export function AuthSync() {
// Map NextAuth session to AuthStore user
// Assuming session.user has the fields we need based on types/next-auth.d.ts
// cast to any or specific type if needed, as NextAuth types might need assertion
const user = session.user as any;
// Map NextAuth session user to AuthStore user type
const user = session.user as {
id?: string;
user_id?: string;
username?: string;
email?: string;
firstName?: string;
lastName?: string;
role?: string;
permissions?: string[];
};
setAuth(
{
id: user.id || user.user_id,
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
permissions: user.permissions // If backend/auth.ts provides this
id: user.id || user.user_id || '',
username: user.username || '',
email: user.email || '',
firstName: user.firstName || '',
lastName: user.lastName || '',
role: user.role || 'User',
permissions: user.permissions
},
session.accessToken || '' // If we store token in session
(session as { accessToken?: string }).accessToken || ''
);
} else if (status === 'unauthenticated') {
logout();
-102
View File
@@ -1,102 +0,0 @@
"use client";
import { useCallback, useState } from "react";
import { useDropzone } from "react-dropzone";
import { Upload, X, File } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface FileUploadProps {
onFilesSelected: (files: File[]) => void;
maxFiles?: number;
accept?: string;
maxSize?: number; // bytes
}
export function FileUpload({
onFilesSelected,
maxFiles = 5,
accept = ".pdf,.doc,.docx",
maxSize = 10485760, // 10MB
}: FileUploadProps) {
const [files, setFiles] = useState<File[]>([]);
const onDrop = useCallback(
(acceptedFiles: File[]) => {
setFiles((prev) => {
const newFiles = [...prev, ...acceptedFiles].slice(0, maxFiles);
onFilesSelected(newFiles);
return newFiles;
});
},
[maxFiles, onFilesSelected]
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
maxFiles,
accept: accept.split(",").reduce((acc, ext) => ({ ...acc, [ext]: [] }), {}),
maxSize,
});
const removeFile = (index: number) => {
setFiles((prev) => {
const newFiles = prev.filter((_, i) => i !== index);
onFilesSelected(newFiles);
return newFiles;
});
};
return (
<div className="space-y-4">
<div
{...getRootProps()}
className={cn(
"border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors",
isDragActive
? "border-primary bg-primary/5"
: "border-gray-300 hover:border-gray-400"
)}
>
<input {...getInputProps()} />
<Upload className="mx-auto h-12 w-12 text-gray-400" />
<p className="mt-2 text-sm text-gray-600">
{isDragActive
? "Drop files here"
: "Drag & drop files or click to browse"}
</p>
<p className="mt-1 text-xs text-gray-500">
Maximum {maxFiles} files, {(maxSize / 1024 / 1024).toFixed(0)}MB each
</p>
</div>
{files.length > 0 && (
<div className="space-y-2">
{files.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div className="flex items-center gap-3">
<File className="h-5 w-5 text-gray-500" />
<div>
<p className="text-sm font-medium">{file.name}</p>
<p className="text-xs text-gray-500">
{(file.size / 1024).toFixed(1)} KB
</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeFile(index)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</div>
);
}
@@ -18,10 +18,9 @@ export function CorrespondencesContent() {
const { data, isLoading, isError } = useCorrespondences({
page,
status,
search,
revisionStatus,
} as any);
});
if (isLoading) {
return (
@@ -24,8 +24,6 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
if (!data) return <div>No data found</div>;
console.log("Correspondence Detail Data:", data);
// Derive Current Revision Data
const currentRevision = data.revisions?.find(r => r.isCurrent) || data.revisions?.[0];
const subject = currentRevision?.subject || "-";
+6 -6
View File
@@ -14,7 +14,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { FileUpload } from "@/components/common/file-upload";
import { FileUploadZone } from "@/components/custom/file-upload-zone";
import { useRouter } from "next/navigation";
import { Loader2 } from "lucide-react";
import { useCreateCorrespondence, useUpdateCorrespondence } from "@/hooks/use-correspondence";
@@ -80,7 +80,7 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(correspondenceSchema),
defaultValues: defaultValues as any,
defaultValues: defaultValues as FormData,
});
// Watch for controlled inputs
@@ -407,10 +407,10 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
{!initialData && (
<div className="space-y-2">
<Label>Attachments</Label>
<FileUpload
onFilesSelected={(files) => setValue("attachments", files)}
maxFiles={10}
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.png"
<FileUploadZone
onFilesChanged={(files) => setValue("attachments", files)}
multiple
accept={[".pdf", ".doc", ".docx", ".xls", ".xlsx", ".jpg", ".png"]}
/>
</div>
)}
@@ -74,7 +74,7 @@ export function FileUploadZone({
const processedFiles: FileWithMeta[] = newFiles.map((file) => {
const error = validateFile(file);
// สร้าง Object ใหม่เพื่อไม่ให้กระทบ File object เดิม
const fileWithMeta = new File([file], file.name, { type: file.type } as any) as FileWithMeta;
const fileWithMeta = new File([file], file.name, { type: file.type }) as FileWithMeta;
fileWithMeta.validationError = error;
return fileWithMeta;
});
@@ -1,125 +0,0 @@
// File: components/custom/responsive-data-table.tsx
import React from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { cn } from "@/lib/utils";
/**
* Interface สำหรับ Column Definition
*/
export interface ColumnDef<T> {
key: string;
header: string;
/** ฟังก์ชันสำหรับ render cell content (optional) */
cell?: (item: T) => React.ReactNode;
/** คลาส CSS เพิ่มเติมสำหรับ cell */
className?: string;
}
/**
* Props สำหรับ ResponsiveDataTable
*/
interface ResponsiveDataTableProps<T> {
/** ข้อมูลที่จะแสดงในตาราง */
data: T[];
/** นิยามของคอลัมน์ */
columns: ColumnDef<T>[];
/** Key ที่เป็น Unique ID ของข้อมูล (เช่น 'id', 'user_id') */
keyExtractor: (item: T) => string | number;
/** ฟังก์ชันสำหรับ Render Card View บน Mobile (ถ้าไม่ใส่จะ Render แบบ Default Key-Value) */
renderMobileCard?: (item: T) => React.ReactNode;
/** ข้อความเมื่อไม่มีข้อมูล */
emptyMessage?: string;
/** คลาส CSS เพิ่มเติมสำหรับ Container */
className?: string;
}
/**
* ResponsiveDataTable Component
* * แสดงผลเป็น Table ปกติในหน้าจอขนาด md ขึ้นไป
* และแสดงผลเป็น Card List ในหน้าจอขนาดเล็กกว่า md
*/
export function ResponsiveDataTable<T>({
data,
columns,
keyExtractor,
renderMobileCard,
emptyMessage = "ไม่พบข้อมูล",
className,
}: ResponsiveDataTableProps<T>) {
if (!data || data.length === 0) {
return (
<div className="p-8 text-center text-muted-foreground border rounded-md bg-background">
{emptyMessage}
</div>
);
}
return (
<div className={cn("w-full space-y-4", className)}>
{/* --- Desktop View (Table) --- */}
<div className="hidden md:block rounded-md border bg-background">
<Table>
<TableHeader>
<TableRow>
{columns.map((col) => (
<TableHead key={col.key} className={col.className}>
{col.header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{data.map((item) => (
<TableRow key={keyExtractor(item)}>
{columns.map((col) => (
<TableCell key={`${keyExtractor(item)}-${col.key}`} className={col.className}>
{col.cell ? col.cell(item) : (item as any)[col.key]}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* --- Mobile View (Cards) --- */}
<div className="md:hidden space-y-4">
{data.map((item) => (
<div key={keyExtractor(item)}>
{renderMobileCard ? (
// Custom Mobile Render
renderMobileCard(item)
) : (
// Default Mobile Render (Key-Value Pairs)
<Card>
<CardHeader className="pb-2 font-semibold border-b mb-2">
# {keyExtractor(item)}
</CardHeader>
<CardContent className="space-y-2 text-sm">
{columns.map((col) => (
<div key={col.key} className="flex justify-between items-start border-b pb-1 last:border-0">
<span className="font-medium text-muted-foreground w-1/3">{col.header}:</span>
<span className="text-right w-2/3 break-words">
{col.cell ? col.cell(item) : (item as any)[col.key]}
</span>
</div>
))}
</CardContent>
</Card>
)}
</div>
))}
</div>
</div>
);
}
+1 -1
View File
@@ -35,7 +35,7 @@ export function DrawingList({ type, projectUuid, filters }: DrawingListProps) {
...filters,
page: pagination.pageIndex + 1, // API is 1-based
limit: pagination.pageSize,
} as any);
} as DrawingSearchParams);
const drawings = response?.data || [];
const meta = response?.meta || { total: 0, page: 1, limit: 20, totalPages: 0 };
+35 -32
View File
@@ -1,6 +1,6 @@
"use client";
import { useForm } from "react-hook-form";
import { useForm, FieldError } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
@@ -95,12 +95,15 @@ export function DrawingUploadForm() {
watch,
formState: { errors },
} = useForm<DrawingFormData>({
resolver: zodResolver(formSchema) as any,
resolver: zodResolver(formSchema),
defaultValues: {
drawingType: "CONTRACT",
}
} as DrawingFormData
});
// Type-safe error access for discriminated union fields
const formErrors = errors as Record<string, FieldError | undefined>;
const drawingType = watch("drawingType");
const watchedProjectId = watch("projectId");
const createMutation = useCreateDrawing(drawingType);
@@ -148,7 +151,7 @@ export function DrawingUploadForm() {
if (data.description) formData.append('description', data.description);
}
createMutation.mutate(formData as any, {
createMutation.mutate(formData, {
onSuccess: () => {
router.push("/drawings");
}
@@ -191,7 +194,7 @@ export function DrawingUploadForm() {
<Label>Drawing Type *</Label>
<Select
onValueChange={(v) => {
setValue("drawingType", v as any);
setValue("drawingType", v as DrawingFormData["drawingType"]);
// Reset errors or fields if needed
}}
defaultValue="CONTRACT"
@@ -214,15 +217,15 @@ export function DrawingUploadForm() {
<div>
<Label>Contract Drawing No *</Label>
<Input {...register("contractDrawingNo")} placeholder="e.g. CD-001" />
{(errors as any).contractDrawingNo && (
<p className="text-sm text-destructive">{(errors as any).contractDrawingNo.message}</p>
{formErrors.contractDrawingNo && (
<p className="text-sm text-destructive">{formErrors.contractDrawingNo.message}</p>
)}
</div>
<div>
<Label>Title *</Label>
<Input {...register("title")} placeholder="Drawing Title" />
{(errors as any).title && (
<p className="text-sm text-destructive">{(errors as any).title.message}</p>
{formErrors.title && (
<p className="text-sm text-destructive">{formErrors.title.message}</p>
)}
</div>
</div>
@@ -235,13 +238,13 @@ export function DrawingUploadForm() {
<SelectValue placeholder="Select Category" />
</SelectTrigger>
<SelectContent>
{contractCategories?.map((c: any) => (
{contractCategories?.map((c: { id: number; catName?: string; catCode?: string; name?: string }) => (
<SelectItem key={c.id} value={String(c.id)}>{c.catName || c.catCode || c.name}</SelectItem>
))}
</SelectContent>
</Select>
{(errors as any).mapCatId && (
<p className="text-sm text-destructive">{(errors as any).mapCatId.message}</p>
{formErrors.mapCatId && (
<p className="text-sm text-destructive">{formErrors.mapCatId.message}</p>
)}
</div>
<div className="grid grid-cols-2 gap-2">
@@ -265,8 +268,8 @@ export function DrawingUploadForm() {
<div>
<Label>Shop Drawing No *</Label>
<Input {...register("drawingNumber")} placeholder="e.g. SD-101" />
{(errors as any).drawingNumber && (
<p className="text-sm text-destructive">{(errors as any).drawingNumber.message}</p>
{formErrors.drawingNumber && (
<p className="text-sm text-destructive">{formErrors.drawingNumber.message}</p>
)}
</div>
<div>
@@ -286,13 +289,13 @@ export function DrawingUploadForm() {
<SelectValue placeholder="Select Main Category" />
</SelectTrigger>
<SelectContent>
{shopMainCats?.map((c: any) => (
{shopMainCats?.map((c: { id: number; mainCategoryName?: string; mainCategoryCode?: string; name?: string }) => (
<SelectItem key={c.id} value={String(c.id)}>{c.mainCategoryName || c.mainCategoryCode || c.name}</SelectItem>
))}
</SelectContent>
</Select>
{(errors as any).mainCategoryId && (
<p className="text-sm text-destructive">{(errors as any).mainCategoryId.message}</p>
{formErrors.mainCategoryId && (
<p className="text-sm text-destructive">{formErrors.mainCategoryId.message}</p>
)}
</div>
<div>
@@ -302,13 +305,13 @@ export function DrawingUploadForm() {
<SelectValue placeholder="Select Sub Category" />
</SelectTrigger>
<SelectContent>
{shopSubCats?.map((c: any) => (
{shopSubCats?.map((c: { id: number; subCategoryName?: string; subCategoryCode?: string; name?: string }) => (
<SelectItem key={c.id} value={String(c.id)}>{c.subCategoryName || c.subCategoryCode || c.name}</SelectItem>
))}
</SelectContent>
</Select>
{(errors as any).subCategoryId && (
<p className="text-sm text-destructive">{(errors as any).subCategoryId.message}</p>
{formErrors.subCategoryId && (
<p className="text-sm text-destructive">{formErrors.subCategoryId.message}</p>
)}
</div>
</div>
@@ -316,8 +319,8 @@ export function DrawingUploadForm() {
<div>
<Label>Revision Title *</Label>
<Input {...register("title")} placeholder="Current Revision Title" />
{(errors as any).title && (
<p className="text-sm text-destructive">{(errors as any).title.message}</p>
{formErrors.title && (
<p className="text-sm text-destructive">{formErrors.title.message}</p>
)}
</div>
@@ -335,8 +338,8 @@ export function DrawingUploadForm() {
<div>
<Label>Drawing No *</Label>
<Input {...register("drawingNumber")} placeholder="e.g. AB-101" />
{(errors as any).drawingNumber && (
<p className="text-sm text-destructive">{(errors as any).drawingNumber.message}</p>
{formErrors.drawingNumber && (
<p className="text-sm text-destructive">{formErrors.drawingNumber.message}</p>
)}
</div>
<div>
@@ -356,13 +359,13 @@ export function DrawingUploadForm() {
<SelectValue placeholder="Select Main Category" />
</SelectTrigger>
<SelectContent>
{shopMainCats?.map((c: any) => (
{shopMainCats?.map((c: { id: number; mainCategoryName?: string; mainCategoryCode?: string; name?: string }) => (
<SelectItem key={c.id} value={String(c.id)}>{c.mainCategoryName || c.mainCategoryCode || c.name}</SelectItem>
))}
</SelectContent>
</Select>
{(errors as any).mainCategoryId && (
<p className="text-sm text-destructive">{(errors as any).mainCategoryId.message}</p>
{formErrors.mainCategoryId && (
<p className="text-sm text-destructive">{formErrors.mainCategoryId.message}</p>
)}
</div>
<div>
@@ -372,13 +375,13 @@ export function DrawingUploadForm() {
<SelectValue placeholder="Select Sub Category" />
</SelectTrigger>
<SelectContent>
{shopSubCats?.map((c: any) => (
{shopSubCats?.map((c: { id: number; subCategoryName?: string; subCategoryCode?: string; name?: string }) => (
<SelectItem key={c.id} value={String(c.id)}>{c.subCategoryName || c.subCategoryCode || c.name}</SelectItem>
))}
</SelectContent>
</Select>
{(errors as any).subCategoryId && (
<p className="text-sm text-destructive">{(errors as any).subCategoryId.message}</p>
{formErrors.subCategoryId && (
<p className="text-sm text-destructive">{formErrors.subCategoryId.message}</p>
)}
</div>
</div>
@@ -386,8 +389,8 @@ export function DrawingUploadForm() {
<div>
<Label>Title *</Label>
<Input {...register("title")} placeholder="Drawing Title" />
{(errors as any).title && (
<p className="text-sm text-destructive">{(errors as any).title.message}</p>
{formErrors.title && (
<p className="text-sm text-destructive">{formErrors.title.message}</p>
)}
</div>
<div>
-124
View File
@@ -1,124 +0,0 @@
// File: components/forms/file-upload.tsx
"use client";
import { useRef, useState } from "react";
import { UploadCloud, X, File, FileText, Image as ImageIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface FileUploadProps {
onFilesChange: (files: File[]) => void;
maxFiles?: number;
maxSize?: number; // MB
accept?: string; // e.g. ".pdf,.jpg,.png"
}
export function FileUpload({ onFilesChange, maxFiles = 5, maxSize = 50, accept }: FileUploadProps) {
const [dragActive, setDragActive] = useState(false);
const [files, setFiles] = useState<File[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFiles(Array.from(e.dataTransfer.files));
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
if (e.target.files && e.target.files[0]) {
handleFiles(Array.from(e.target.files));
}
};
const handleFiles = (newFiles: File[]) => {
// Validate size & type here if needed
const validFiles = newFiles.slice(0, maxFiles - files.length);
const updatedFiles = [...files, ...validFiles];
setFiles(updatedFiles);
onFilesChange(updatedFiles);
};
const removeFile = (idx: number) => {
const updatedFiles = files.filter((_, i) => i !== idx);
setFiles(updatedFiles);
onFilesChange(updatedFiles);
};
const getFileIcon = (type: string) => {
if (type.includes("image")) return <ImageIcon className="h-5 w-5 text-blue-500" />;
if (type.includes("pdf")) return <FileText className="h-5 w-5 text-red-500" />;
return <File className="h-5 w-5 text-gray-500" />;
};
return (
<div className="space-y-4">
<div
className={cn(
"relative flex flex-col items-center justify-center w-full h-32 rounded-lg border-2 border-dashed transition-colors",
dragActive ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:bg-muted/5"
)}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
<input
ref={inputRef}
className="hidden"
type="file"
multiple
accept={accept}
onChange={handleChange}
/>
<div className="flex flex-col items-center justify-center pt-5 pb-6 text-center cursor-pointer" onClick={() => inputRef.current?.click()}>
<UploadCloud className="w-8 h-8 mb-2 text-muted-foreground" />
<p className="mb-1 text-sm text-gray-500 dark:text-gray-400">
<span className="font-semibold">Click to upload</span> or drag and drop
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
PDF, DWG, DOCX (Max {maxSize}MB)
</p>
</div>
</div>
{/* File List */}
{files.length > 0 && (
<div className="space-y-2">
{files.map((file, idx) => (
<div key={idx} className="flex items-center justify-between p-2 bg-muted/40 rounded-md border text-sm">
<div className="flex items-center gap-2 overflow-hidden">
{getFileIcon(file.type)}
<span className="truncate max-w-[200px]">{file.name}</span>
<span className="text-xs text-muted-foreground">({(file.size / 1024 / 1024).toFixed(2)} MB)</span>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-destructive"
onClick={() => removeFile(idx)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</div>
);
}
+6 -6
View File
@@ -112,14 +112,14 @@ export function Sidebar({ className }: SidebarProps) {
<div className="flex-1 overflow-y-auto py-4">
<nav className="grid gap-1 px-2">
{mainNavItems.map((item, index) => {
{mainNavItems.map((item) => {
if (item.adminOnly && !isAdmin) return null;
const isActive = pathname.startsWith(item.href);
const LinkComponent = (
<Link
key={index}
key={item.href}
href={item.href}
className={cn(
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground',
@@ -135,7 +135,7 @@ export function Sidebar({ className }: SidebarProps) {
if (item.permission) {
return (
<Can key={index} permission={item.permission}>
<Can key={item.href} permission={item.permission}>
{LinkComponent}
</Can>
);
@@ -186,14 +186,14 @@ export function MobileSidebar() {
</div>
<div className="flex-1 overflow-y-auto py-4 h-[calc(100vh-4rem)]">
<nav className="grid gap-1 px-2">
{mainNavItems.map((item, index) => {
{mainNavItems.map((item) => {
if (item.adminOnly && !isAdmin) return null;
const isActive = pathname.startsWith(item.href);
const LinkComponent = (
<Link
key={index}
key={item.href}
href={item.href}
onClick={() => setOpen(false)}
className={cn(
@@ -208,7 +208,7 @@ export function MobileSidebar() {
if (item.permission) {
return (
<Can key={index} permission={item.permission}>
<Can key={item.href} permission={item.permission}>
{LinkComponent}
</Can>
);
@@ -24,7 +24,7 @@ export function AuditLogsTable() {
setLogs(data.audit);
}
} catch (error) {
console.error("Failed to fetch audit logs", error);
// Failed to fetch audit logs - empty state shown
} finally {
setLoading(false);
}
@@ -7,7 +7,7 @@ import { Label } from "@/components/ui/label";
import { toast } from "sonner";
import { documentNumberingService } from "@/lib/services/document-numbering.service";
export function BulkImportForm({ projectId = 1 }: { projectId?: number }) {
export function BulkImportForm({ projectId = 1 }: { projectId?: number | string }) {
const [file, setFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
@@ -30,7 +30,6 @@ export function BulkImportForm({ projectId = 1 }: { projectId?: number }) {
setFile(null);
} catch (error) {
toast.error("Failed to import numbers.");
console.error(error);
} finally {
setLoading(false);
}
@@ -29,7 +29,7 @@ export function CancelNumberForm() {
const [loading, setLoading] = useState(false);
const form = useForm<CancelNumberFormData>({
resolver: zodResolver(formSchema) as any,
resolver: zodResolver(formSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- zod 4 + @hookform/resolvers compat
defaultValues: {
documentNumber: "",
reason: "",
@@ -45,7 +45,6 @@ export function CancelNumberForm() {
form.reset();
} catch (error) {
toast.error("Failed to cancel number. It may not exist or is already cancelled.");
console.error(error);
} finally {
setLoading(false);
}
@@ -29,13 +29,13 @@ const formSchema = z.object({
resetScope: z.string().optional()
});
export function ManualOverrideForm({ projectId = 1 }: { projectId?: number }) {
export function ManualOverrideForm({ projectId = 1 }: { projectId?: number | string }) {
const [loading, setLoading] = useState(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema) as any,
resolver: zodResolver(formSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- zod 4 + @hookform/resolvers compat
defaultValues: {
projectId: projectId,
projectId: Number(projectId),
originatorOrganizationId: 0,
recipientOrganizationId: 0,
correspondenceTypeId: 0,
@@ -57,7 +57,6 @@ export function ManualOverrideForm({ projectId = 1 }: { projectId?: number }) {
form.reset();
} catch (error) {
toast.error("Failed to apply override.");
console.error(error);
} finally {
setLoading(false);
}
@@ -16,7 +16,7 @@ export function MetricsDashboard() {
const data = await documentNumberingService.getMetrics();
setMetrics(data);
} catch (error) {
console.error("Failed to fetch metrics", error);
// Failed to fetch metrics - handled by loading state
} finally {
setLoading(false);
}
@@ -21,7 +21,7 @@ export function SequenceViewer() {
const data = Array.isArray(response) ? response : (response as { data?: NumberSequence[] })?.data ?? [];
setSequences(data);
} catch {
console.error('Failed to fetch sequences');
// Failed to fetch sequences - show empty state
setSequences([]);
} finally {
setLoading(false);
@@ -71,7 +71,7 @@ export function TemplateEditor({
// Dynamic context based on selection (optional visual enhancement)
if (v.key === '{TYPE}' && typeId) {
const t = (correspondenceTypes as any[]).find((ct: any) => ct.id?.toString() === typeId);
const t = (correspondenceTypes as { id: number; typeCode: string; typeName: string }[]).find((ct) => ct.id?.toString() === typeId);
if (t) replacement = t.typeCode;
}
@@ -53,7 +53,8 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
const [loading, setLoading] = useState(false);
// Master Data Hooks
const projectId = (template as any)?.project?.id ?? (template as any)?.project?.uuid ?? template?.projectId ?? 1;
const templateWithProject = template as (NumberingTemplate & { project?: { id?: number; uuid?: string } }) | null;
const projectId = templateWithProject?.project?.id ?? templateWithProject?.project?.uuid ?? template?.projectId ?? 1;
const { data: organizations } = useOrganizations({ isActive: true });
const { data: correspondenceTypes } = useCorrespondenceTypes();
const { data: contracts } = useContracts(projectId);
@@ -74,14 +75,9 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
disciplineId: parseInt(testData.disciplineId || "0"),
});
setGeneratedNumber(result.previewNumber);
} catch (error: any) {
console.error("Failed to generate test number", error);
setGeneratedNumber("");
// Assuming toast is available globally or we can use console for now,
// but better to show visible error.
// Alert is primitive but effective for 'tester' component debugging if toast not imported.
// Actually, let's just set the error string in display if we can, or add a simple red text.
setGeneratedNumber(`Error: ${error.response?.data?.message || error.message || "Unknown error"}`);
} catch (error: unknown) {
const err = error as { response?: { data?: { message?: string } }; message?: string };
setGeneratedNumber(`Error: ${err.response?.data?.message || err.message || "Unknown error"}`);
} finally {
setLoading(false);
}
@@ -29,16 +29,16 @@ const formSchema = z.object({
type VoidReplaceFormData = z.infer<typeof formSchema>;
export function VoidReplaceForm({ projectId = 1 }: { projectId?: number }) {
export function VoidReplaceForm({ projectId = 1 }: { projectId?: number | string }) {
const [loading, setLoading] = useState(false);
const form = useForm<VoidReplaceFormData>({
resolver: zodResolver(formSchema) as any,
resolver: zodResolver(formSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- zod 4 + @hookform/resolvers compat
defaultValues: {
documentNumber: "",
reason: "",
replace: false,
projectId: projectId
projectId: Number(projectId)
},
});
@@ -53,7 +53,6 @@ export function VoidReplaceForm({ projectId = 1 }: { projectId?: number }) {
form.reset();
} catch (error) {
toast.error("Failed to void number. Check if it exists.");
console.error(error);
} finally {
setLoading(false);
}
+23 -3
View File
@@ -1,6 +1,5 @@
"use client";
import { RFA } from "@/types/rfa";
import { StatusBadge } from "@/components/common/status-badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -13,8 +12,29 @@ import { Textarea } from "@/components/ui/textarea";
import { useRouter } from "next/navigation";
import { useProcessRFA } from "@/hooks/use-rfa";
interface RFADetailItem {
id: number;
itemNo: string;
description: string;
quantity: number;
unit: string;
status?: string;
}
interface RFADetailData {
uuid: string;
rfaNumber: string;
subject: string;
description?: string;
status: string;
createdAt: string;
contractName?: string;
disciplineName?: string;
items: RFADetailItem[];
}
interface RFADetailProps {
data: any;
data: RFADetailData;
}
export function RFADetail({ data }: RFADetailProps) {
@@ -152,7 +172,7 @@ export function RFADetail({ data }: RFADetailProps) {
</tr>
</thead>
<tbody className="divide-y">
{data.items.map((item: any) => (
{data.items.map((item) => (
<tr key={item.id}>
<td className="px-4 py-3 font-medium">{item.itemNo}</td>
<td className="px-4 py-3">{item.description}</td>
+3 -3
View File
@@ -20,7 +20,7 @@ import { useRouter } from "next/navigation";
import { useCreateRFA } from "@/hooks/use-rfa";
import { useDisciplines, useContracts } from "@/hooks/use-master-data";
import { useProjects } from "@/hooks/use-projects";
import { CreateRFADto } from "@/types/rfa";
import { CreateRfaDto } from "@/types/dto/rfa/rfa.dto";
import { useState, useEffect } from "react";
import { correspondenceService } from "@/lib/services/correspondence.service";
@@ -126,11 +126,11 @@ export function RFAForm() {
});
const onSubmit = (data: RFAFormData) => {
const payload: CreateRFADto = {
const payload: CreateRfaDto = {
...data,
// ADR-019: projectId is already a UUID string from the form
};
createMutation.mutate(payload as any, {
createMutation.mutate(payload, {
onSuccess: () => {
router.push("/rfas");
},
@@ -77,7 +77,7 @@ export function TransmittalForm() {
const [docOpen, setDocOpen] = useState(false);
const form = useForm<FormData>({
resolver: zodResolver(formSchema) as any,
resolver: zodResolver(formSchema) as any, // eslint-disable-line @typescript-eslint/no-explicit-any -- zod 4 + @hookform/resolvers compat
defaultValues: {
projectId: "",
recipientOrganizationId: "",
+1 -1
View File
@@ -48,7 +48,7 @@ export function DSLEditor({ initialValue = '', onChange, readOnly = false }: DSL
const result = await workflowApi.validateDSL(dsl);
setValidationResult(result);
} catch (error) {
console.error("Validation error:", error);
// Validation failed - error state shown in UI
setValidationResult({ valid: false, errors: ['Validation failed due to server error'] });
} finally {
setIsValidating(false);
@@ -120,7 +120,7 @@ function parseDSL(dsl: string): { nodes: Node[], edges: Edge[] } {
});
} catch (e) {
console.error("Failed to parse DSL as JSON", e);
// Failed to parse DSL as JSON - nodes/edges remain empty
}
return { nodes, edges };
@@ -226,7 +226,7 @@ function VisualWorkflowBuilderContent({ initialNodes: propNodes, initialEdges: p
};
const dsl = JSON.stringify(dslObj, null, 2);
console.log("Generated DSL:", dsl);
// DSL generated from visual builder
onDslChange?.(dsl);
alert("DSL Updated from Visual Builder!");
};
-69
View File
@@ -1,69 +0,0 @@
// File: config/menu.ts
import {
LayoutDashboard,
FileText,
CheckSquare,
PenTool,
FileOutput,
RefreshCw,
Settings,
Users,
Briefcase,
Search
} from "lucide-react";
export const sidebarMenuItems = [
{
title: "Dashboard",
href: "/dashboard",
icon: LayoutDashboard,
},
{
title: "Projects (โครงการ)", // เพิ่มเมนูนี้
href: "/projects",
icon: Briefcase,
},
{
title: "Correspondences",
href: "/correspondences",
icon: FileText,
},
{
title: "RFAs (ขออนุมัติ)",
href: "/rfas",
icon: CheckSquare,
},
{
title: "Drawings (แบบแปลน)",
href: "/drawings",
icon: PenTool,
},
{
title: "Transmittals (นำส่ง)",
href: "/transmittals",
icon: FileOutput,
},
{
title: "Circulations (ใบเวียน)",
href: "/circulations",
icon: RefreshCw,
},
{
title: "ค้นหาเอกสาร",
href: "/search",
icon: Search,
},
];
export const adminMenuItems = [
{
title: "จัดการผู้ใช้งาน",
href: "/admin/users",
icon: Users,
},
{
title: "ตั้งค่าระบบ",
href: "/admin/settings",
icon: Settings,
},
];
+17 -6
View File
@@ -1,6 +1,8 @@
import js from "@eslint/js";
import globals from "globals";
import typescriptParser from "@typescript-eslint/parser";
import typescriptPlugin from "@typescript-eslint/eslint-plugin";
import reactHooksPlugin from "eslint-plugin-react-hooks";
const eslintConfig = [
js.configs.recommended,
@@ -16,8 +18,7 @@ const eslintConfig = [
},
},
rules: {
// Allow console statements in development
"no-console": "off",
"no-console": "warn",
"no-unused-vars": "warn",
},
},
@@ -38,11 +39,21 @@ const eslintConfig = [
...globals.es2021,
},
},
plugins: {
"@typescript-eslint": typescriptPlugin,
"react-hooks": reactHooksPlugin,
},
rules: {
// Allow console statements in development
"no-console": "off",
"no-unused-vars": "off", // TypeScript handles this better
"no-undef": "off", // TypeScript handles this better
"no-console": "warn",
"no-unused-vars": "off",
"no-undef": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["warn", {
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
}],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
},
// Ignore config files and build outputs
+1 -1
View File
@@ -10,7 +10,7 @@ import { ContractDrawing, ShopDrawing, AsBuiltDrawing } from '@/types/drawing';
type DrawingType = 'CONTRACT' | 'SHOP' | 'AS_BUILT';
type DrawingSearchParams = SearchContractDrawingDto | SearchShopDrawingDto | SearchAsBuiltDrawingDto;
type CreateDrawingData = CreateContractDrawingDto | CreateShopDrawingDto | CreateAsBuiltDrawingDto;
type CreateDrawingData = CreateContractDrawingDto | CreateShopDrawingDto | CreateAsBuiltDrawingDto | FormData;
export const drawingKeys = {
all: ['drawings'] as const,
+2 -2
View File
@@ -41,7 +41,7 @@ apiClient.interceptors.request.use(
}
}
} catch (error) {
console.warn("Failed to retrieve auth token:", error);
// Auth token retrieval failed - request will proceed without token
}
}
@@ -66,7 +66,7 @@ apiClient.interceptors.response.use(
// กรณี Token หมดอายุ หรือ ไม่มีสิทธิ์
if (status === 401) {
console.error("Unauthorized: Please login again.");
// Unauthorized: redirect handled by auth interceptor
// สามารถเพิ่ม Logic Redirect ไปหน้า Login ได้ถ้าต้องการ
}
}
-1
View File
@@ -300,7 +300,6 @@ export const numberingApi = {
): Promise<{ number: string }> => {
// Fallback mock for legacy UI - requires proper context for real use
const mockNumber = `TEST-${new Date().getFullYear()}-${String(Math.floor(Math.random() * 9999)).padStart(4, '0')}`;
console.log('Using mock generateTestNumber. Context:', context);
return { number: mockNumber };
},
};
+1 -5
View File
@@ -47,7 +47,7 @@ async function refreshAccessToken(token: JWT) {
refreshToken: data.refresh_token ?? token.refreshToken,
};
} catch (error) {
console.log("RefreshAccessTokenError", error);
// RefreshAccessTokenError - token will be invalidated
return {
...token,
@@ -73,7 +73,6 @@ export const {
try {
const { username, password } = await loginSchema.parseAsync(credentials);
console.log(`Attempting login to: ${baseUrl}/auth/login`);
const res = await fetch(`${baseUrl}/auth/login`, {
method: "POST",
@@ -83,7 +82,6 @@ export const {
if (!res.ok) {
const errorMsg = await res.text();
console.error("Login failed:", errorMsg);
return null;
}
@@ -91,7 +89,6 @@ export const {
const backendData = responseJson.data || responseJson;
if (!backendData || !backendData.access_token) {
console.error("No access token received");
return null;
}
@@ -107,7 +104,6 @@ export const {
} as User;
} catch (error) {
console.error("Auth error:", error);
return null;
}
},
@@ -14,10 +14,8 @@ export const dashboardService = {
if (Array.isArray(response.data)) {
return response.data;
}
console.warn('Dashboard activity: expected array, got:', typeof response.data);
return [];
} catch (error) {
console.error('Failed to fetch recent activity:', error);
return [];
}
},
@@ -32,10 +30,8 @@ export const dashboardService = {
if (Array.isArray(response.data)) {
return response.data;
}
console.warn('Dashboard pending: unexpected format:', typeof response.data);
return [];
} catch (error) {
console.error('Failed to fetch pending tasks:', error);
return [];
}
},
+4 -1
View File
@@ -52,7 +52,10 @@ const nextConfig = {
return config;
},
// 5.1. Security Headers + MIME Types
// 6. Static Generation Prevention for Dynamic Routes
// Individual pages will use dynamic exports
// 7. Security Headers + MIME Types
async headers() {
return [
{
+4
View File
@@ -1,4 +1,5 @@
// File: src/types/dto/rfa/rfa.dto.ts
import type { RFAItem } from '@/types/rfa';
// --- Create ---
export interface CreateRfaDto {
@@ -37,6 +38,9 @@ export interface CreateRfaDto {
/** รายการ ID ของ Shop Drawings ที่แนบมา (ถ้ามี) */
shopDrawingRevisionIds?: number[];
/** รายการ Items ของ RFA */
items?: RFAItem[];
}
// --- Update (Partial) ---
+1
View File
@@ -67,4 +67,5 @@ export interface CreateRFADto {
documentDate?: string;
details?: Record<string, unknown>;
shopDrawingRevisionIds?: number[];
items?: RFAItem[];
}