This commit is contained in:
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 || "-";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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!");
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ได้ถ้าต้องการ
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -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) ---
|
||||
|
||||
@@ -67,4 +67,5 @@ export interface CreateRFADto {
|
||||
documentDate?: string;
|
||||
details?: Record<string, unknown>;
|
||||
shopDrawingRevisionIds?: number[];
|
||||
items?: RFAItem[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user