This commit is contained in:
@@ -1,36 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { use, useState } from "react";
|
||||
import { notFound, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, Download, FileText, Loader2 } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ArrowLeft, Download, FileText, Loader2, Pencil, Upload, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { RevisionHistory } from "@/components/drawings/revision-history";
|
||||
import { format } from "date-fns";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { contractDrawingService } from "@/lib/services/contract-drawing.service";
|
||||
import { shopDrawingService } from "@/lib/services/shop-drawing.service";
|
||||
import { asBuiltDrawingService } from "@/lib/services/asbuilt-drawing.service";
|
||||
import { useUpdateContractDrawing, useUploadRevision } from "@/hooks/use-drawing";
|
||||
|
||||
async function fetchDrawingByUuid(uuid: string) {
|
||||
// Try each drawing type until one succeeds
|
||||
type DrawingType = "CONTRACT" | "SHOP" | "AS_BUILT";
|
||||
|
||||
interface FetchedDrawing {
|
||||
_type: DrawingType;
|
||||
uuid: string;
|
||||
contractDrawingNo?: string;
|
||||
drawingNumber?: string;
|
||||
title?: string;
|
||||
volumePage?: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
currentRevision?: { title?: string; revisionNumber?: string; legacyDrawingNumber?: string };
|
||||
revisions?: { revisionId?: number; uuid: string; revisionNumber: string; title?: string; legacyDrawingNumber?: string; revisionDate: string; revisionDescription?: string; revisedByName: string; fileUrl: string; isCurrent: boolean | null; createdBy?: number; updatedBy?: number }[];
|
||||
}
|
||||
|
||||
async function fetchDrawingByUuid(uuid: string): Promise<FetchedDrawing | null> {
|
||||
try {
|
||||
const result = await contractDrawingService.getByUuid(uuid);
|
||||
if (result?.data) return { ...result.data, _type: "CONTRACT" };
|
||||
} catch { /* not found in contract drawings */ }
|
||||
if (result?.data) return { ...result.data, _type: "CONTRACT" as const };
|
||||
} catch { /* not found */ }
|
||||
|
||||
try {
|
||||
const result = await shopDrawingService.getByUuid(uuid);
|
||||
if (result?.data) return { ...result.data, _type: "SHOP" };
|
||||
} catch { /* not found in shop drawings */ }
|
||||
if (result?.data) return { ...result.data, _type: "SHOP" as const };
|
||||
} catch { /* not found */ }
|
||||
|
||||
try {
|
||||
const result = await asBuiltDrawingService.getByUuid(uuid);
|
||||
if (result?.data) return { ...result.data, _type: "AS_BUILT" };
|
||||
} catch { /* not found in asbuilt drawings */ }
|
||||
if (result?.data) return { ...result.data, _type: "AS_BUILT" as const };
|
||||
} catch { /* not found */ }
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -41,6 +59,10 @@ export default function DrawingDetailPage({
|
||||
params: Promise<{ uuid: string }>;
|
||||
}) {
|
||||
const { uuid } = use(params);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const isEditMode = searchParams.get("edit") === "true";
|
||||
const isUploadMode = searchParams.get("upload") === "true";
|
||||
|
||||
const { data: drawing, isLoading } = useQuery({
|
||||
queryKey: ["drawing-detail", uuid],
|
||||
@@ -99,67 +121,251 @@ export default function DrawingDetailPage({
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download Current
|
||||
</Button>
|
||||
{!isEditMode && !isUploadMode && (
|
||||
<>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={`/drawings/${uuid}?edit=true`}>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
Edit Detail
|
||||
</Link>
|
||||
</Button>
|
||||
{drawing._type !== "CONTRACT" && (
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={`/drawings/${uuid}?upload=true`}>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Upload Revision
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download Current
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(isEditMode || isUploadMode) && (
|
||||
<Button variant="ghost" asChild>
|
||||
<Link href={`/drawings/${uuid}`}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Cancel
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Main Info */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<CardTitle className="text-xl">Drawing Details</CardTitle>
|
||||
<Badge>{drawing._type}</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Drawing Number</p>
|
||||
<p className="font-medium mt-1">{drawingNumber}</p>
|
||||
{/* Edit Detail Form */}
|
||||
{isEditMode && (
|
||||
<EditDetailForm drawing={drawing} uuid={uuid} onDone={() => router.push(`/drawings/${uuid}`)} />
|
||||
)}
|
||||
|
||||
{/* Upload Revision Form */}
|
||||
{isUploadMode && drawing._type !== "CONTRACT" && (
|
||||
<UploadRevisionForm drawingType={drawing._type} uuid={uuid} onDone={() => router.push(`/drawings/${uuid}`)} />
|
||||
)}
|
||||
|
||||
{/* Detail View (shown when not in edit/upload mode) */}
|
||||
{!isEditMode && !isUploadMode && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<CardTitle className="text-xl">Drawing Details</CardTitle>
|
||||
<Badge>{drawing._type}</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Type</p>
|
||||
<p className="font-medium mt-1">{drawing._type}</p>
|
||||
</div>
|
||||
{drawing.volumePage && (
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Volume Page</p>
|
||||
<p className="font-medium mt-1">{drawing.volumePage}</p>
|
||||
<p className="text-sm font-medium text-muted-foreground">Drawing Number</p>
|
||||
<p className="font-medium mt-1">{drawingNumber}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Type</p>
|
||||
<p className="font-medium mt-1">{drawing._type}</p>
|
||||
</div>
|
||||
{drawing.volumePage && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Volume Page</p>
|
||||
<p className="font-medium mt-1">{drawing.volumePage}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Created</p>
|
||||
<p className="font-medium mt-1">
|
||||
{drawing.createdAt ? format(new Date(drawing.createdAt), "dd MMM yyyy") : "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Created</p>
|
||||
<p className="font-medium mt-1">
|
||||
{drawing.createdAt ? format(new Date(drawing.createdAt), "dd MMM yyyy") : "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-3">Preview</h3>
|
||||
<div className="aspect-video bg-muted rounded-lg flex items-center justify-center border-2 border-dashed">
|
||||
<div className="text-center">
|
||||
<FileText className="h-12 w-12 mx-auto text-muted-foreground mb-2" />
|
||||
<p className="text-muted-foreground">PDF Preview Placeholder</p>
|
||||
<h3 className="font-semibold mb-3">Preview</h3>
|
||||
<div className="aspect-video bg-muted rounded-lg flex items-center justify-center border-2 border-dashed">
|
||||
<div className="text-center">
|
||||
<FileText className="h-12 w-12 mx-auto text-muted-foreground mb-2" />
|
||||
<p className="text-muted-foreground">PDF Preview Placeholder</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Revisions */}
|
||||
<div className="space-y-6">
|
||||
<RevisionHistory revisions={revisions} />
|
||||
<div className="space-y-6">
|
||||
<RevisionHistory revisions={revisions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Edit Detail Form ─── */
|
||||
function EditDetailForm({
|
||||
drawing,
|
||||
uuid,
|
||||
onDone,
|
||||
}: {
|
||||
drawing: FetchedDrawing;
|
||||
uuid: string;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const updateMutation = useUpdateContractDrawing();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [formTitle, setFormTitle] = useState(drawing.title || drawing.currentRevision?.title || "");
|
||||
const [formDrawingNo, setFormDrawingNo] = useState(drawing.contractDrawingNo || drawing.drawingNumber || "");
|
||||
const [formVolumePage, setFormVolumePage] = useState(drawing.volumePage?.toString() || "");
|
||||
|
||||
const handleSave = () => {
|
||||
if (drawing._type === "CONTRACT") {
|
||||
updateMutation.mutate(
|
||||
{
|
||||
uuid,
|
||||
data: {
|
||||
title: formTitle,
|
||||
contractDrawingNo: formDrawingNo,
|
||||
volumePage: formVolumePage ? parseInt(formVolumePage, 10) : undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["drawing-detail", uuid] });
|
||||
onDone();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Edit Drawing Details</h3>
|
||||
<div className="space-y-4 max-w-xl">
|
||||
<div>
|
||||
<Label>Drawing Number</Label>
|
||||
<Input value={formDrawingNo} onChange={(e) => setFormDrawingNo(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Title</Label>
|
||||
<Input value={formTitle} onChange={(e) => setFormTitle(e.target.value)} />
|
||||
</div>
|
||||
{drawing._type === "CONTRACT" && (
|
||||
<div>
|
||||
<Label>Volume Page</Label>
|
||||
<Input type="number" value={formVolumePage} onChange={(e) => setFormVolumePage(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button onClick={handleSave} disabled={updateMutation.isPending}>
|
||||
{updateMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={`/drawings/${uuid}`}>Cancel</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Upload Revision Form ─── */
|
||||
function UploadRevisionForm({
|
||||
drawingType,
|
||||
uuid,
|
||||
onDone,
|
||||
}: {
|
||||
drawingType: DrawingType;
|
||||
uuid: string;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const uploadMutation = useUploadRevision(drawingType);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [revisionLabel, setRevisionLabel] = useState("");
|
||||
const [revTitle, setRevTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [legacyNo, setLegacyNo] = useState("");
|
||||
|
||||
const handleUpload = () => {
|
||||
if (!revisionLabel || !revTitle) return;
|
||||
|
||||
uploadMutation.mutate(
|
||||
{
|
||||
uuid,
|
||||
data: {
|
||||
revisionLabel,
|
||||
title: revTitle,
|
||||
description: description || undefined,
|
||||
legacyDrawingNumber: legacyNo || undefined,
|
||||
revisionDate: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["drawing-detail", uuid] });
|
||||
onDone();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Upload New Revision</h3>
|
||||
<div className="space-y-4 max-w-xl">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Revision Label *</Label>
|
||||
<Input placeholder="e.g. A, B, 1, 2" value={revisionLabel} onChange={(e) => setRevisionLabel(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Legacy Drawing No.</Label>
|
||||
<Input placeholder="Optional" value={legacyNo} onChange={(e) => setLegacyNo(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Revision Title *</Label>
|
||||
<Input placeholder="Title for this revision" value={revTitle} onChange={(e) => setRevTitle(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<Textarea placeholder="What changed in this revision?" value={description} onChange={(e) => setDescription(e.target.value)} rows={3} />
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button onClick={handleUpload} disabled={uploadMutation.isPending || !revisionLabel || !revTitle}>
|
||||
{uploadMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Upload Revision
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href={`/drawings/${uuid}`}>Cancel</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { AdminSidebar } from "@/components/admin/sidebar";
|
||||
|
||||
export default function TestAdminSidebarPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-background">
|
||||
<AdminSidebar />
|
||||
<div className="flex-1 p-6">
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Admin Sidebar Test</h1>
|
||||
<p className="text-muted-foreground">
|
||||
ทดสอบการทำงานของเมนู admin sidebar
|
||||
</p>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h2 className="text-xl font-semibold mb-2">รายการเมนู Admin:</h2>
|
||||
<ul className="list-disc list-inside space-y-1 text-sm">
|
||||
<li>Access Control - Users, Roles, Organizations</li>
|
||||
<li>Document Control - Projects, Contracts, Numbering, Reference Data, Workflows</li>
|
||||
<li>Drawing Master - Contract/Shop Categories</li>
|
||||
<li>Monitoring - Audit Logs, System Logs, Sessions</li>
|
||||
<li>Migration - Review Queue, Error Logs</li>
|
||||
<li>Settings</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-800">
|
||||
ℹ️ Admin sidebar มีเมนูแบบ collapsible สำหรับแต่ละหมวดหมู่
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { Sidebar } from "@/components/layout/sidebar";
|
||||
import { Header } from "@/components/layout/header";
|
||||
|
||||
export default function TestSidebarPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-background">
|
||||
<Sidebar />
|
||||
<div className="flex-1 flex flex-col min-h-screen overflow-hidden">
|
||||
<Header />
|
||||
<main className="flex-1 overflow-y-auto p-6 bg-muted/10">
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Sidebar Menu Test</h1>
|
||||
<p className="text-muted-foreground">
|
||||
ทดสอบการทำงานของเมนู sidebar ว่าสามารถใช้งานได้จริง
|
||||
</p>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h2 className="text-xl font-semibold mb-2">รายการเมนูที่ตรวจสอบ:</h2>
|
||||
<ul className="list-disc list-inside space-y-1 text-sm">
|
||||
<li>Dashboard - ลิงก์ไป /dashboard</li>
|
||||
<li>Correspondences - ลิงก์ไป /correspondences</li>
|
||||
<li>RFAs - ลิงก์ไป /rfas</li>
|
||||
<li>Drawings - ลิงก์ไป /drawings</li>
|
||||
<li>Circulations - ลิงก์ไป /circulation</li>
|
||||
<li>Transmittals - ลิงก์ไป /transmittals</li>
|
||||
<li>Search - ลิงก์ไป /search</li>
|
||||
<li>Admin Panel - ลิงก์ไป /admin (สำหรับ admin/DC เท่านั้น)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h2 className="text-xl font-semibold mb-2">การทดสอบ:</h2>
|
||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
||||
<li>คลิกที่เมนูแต่ละรายการใน sidebar</li>
|
||||
<li>ตรวจสอบว่าลิงก์ทำงานได้ถูกต้อง</li>
|
||||
<li>ตรวจสอบว่าเมนู active state แสดงผลถูกต้อง</li>
|
||||
<li>ทดสอบการสลับ collapse/expand sidebar</li>
|
||||
<li>ทดสอบ responsive menu บนมือถือ</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<p className="text-sm text-green-800">
|
||||
✅ Sidebar component โหลดสำเร็จแล้ว - สามารถทดสอบการทำงานได้
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user