251225:1703 On going update to 1.7.0: Refoctory drawing Module not finish
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-25 17:03:33 +07:00
parent 7db6a003db
commit cd73cc1549
60 changed files with 8201 additions and 832 deletions

View File

@@ -0,0 +1,282 @@
"use client";
import { useState } from "react";
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
import { ColumnDef } from "@tanstack/react-table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Loader2 } from "lucide-react";
import { useProjects } from "@/hooks/use-master-data";
import { drawingMasterDataService, ContractCategory, ContractSubCategory } from "@/lib/services/drawing-master-data.service";
import { Badge } from "@/components/ui/badge";
interface Category {
id: number;
catCode: string;
catName: string;
description?: string;
sortOrder: number;
}
export default function ContractCategoriesPage() {
const [selectedProjectId, setSelectedProjectId] = useState<number | undefined>(undefined);
const { data: projects = [], isLoading: isLoadingProjects } = useProjects();
const columns: ColumnDef<Category>[] = [
{
accessorKey: "catCode",
header: "Code",
cell: ({ row }) => (
<Badge variant="outline" className="font-mono">
{row.getValue("catCode")}
</Badge>
),
},
{
accessorKey: "catName",
header: "Category Name",
},
{
accessorKey: "description",
header: "Description",
cell: ({ row }) => (
<span className="text-muted-foreground text-sm">
{row.getValue("description") || "-"}
</span>
),
},
{
accessorKey: "sortOrder",
header: "Order",
cell: ({ row }) => (
<span className="font-mono">{row.getValue("sortOrder")}</span>
),
},
];
const projectFilter = (
<div className="flex items-center gap-4">
<span className="text-sm font-medium">Project:</span>
<Select
value={selectedProjectId?.toString() ?? ""}
onValueChange={(v) => setSelectedProjectId(v ? parseInt(v) : undefined)}
>
<SelectTrigger className="w-[300px]">
{isLoadingProjects ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<SelectValue placeholder="Select Project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map((project: { id: number; projectName: string; projectCode: string }) => (
<SelectItem key={project.id} value={String(project.id)}>
{project.projectCode} - {project.projectName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
if (!selectedProjectId) {
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-2xl font-bold">Contract Drawing Categories</h1>
<p className="text-muted-foreground mt-1">
Manage main categories () for contract drawings
</p>
</div>
{projectFilter}
<div className="text-center py-12 text-muted-foreground border rounded-lg border-dashed">
Please select a project to manage categories.
</div>
</div>
);
}
return (
<div className="p-6">
<GenericCrudTable
entityName="Category"
title="Contract Drawing Categories"
description="Manage main categories (หมวดหมู่หลัก) for contract drawings"
queryKey={["contract-drawing-categories", String(selectedProjectId)]}
fetchFn={() => drawingMasterDataService.getContractCategories(selectedProjectId)}
createFn={(data) => drawingMasterDataService.createContractCategory({ ...data, projectId: selectedProjectId })}
updateFn={(id, data) => drawingMasterDataService.updateContractCategory(id, data)}
deleteFn={(id) => drawingMasterDataService.deleteContractCategory(id)}
columns={columns}
fields={[
{ name: "catCode", label: "Category Code", type: "text", required: true },
{ name: "catName", label: "Category Name", type: "text", required: true },
{ name: "description", label: "Description", type: "textarea" },
{ name: "sortOrder", label: "Sort Order", type: "text", required: true },
]}
filters={projectFilter}
/>
{/*
Note: For mapping, we should ideally have a separate "Mappings" column or action button.
Since GenericCrudTable might not support custom action columns easily without modification,
we are currently just listing categories. To add mapping functionality, we might need
to either extend GenericCrudTable or create a dedicated page for mappings.
Given the constraints, I will add a "Mapped Sub-categories" management section
that opens when clicking a category ROW or adding a custom action if GenericCrudTable supports it.
For now, let's assume we need to extend GenericCrudTable or replace it to support this specific requirement.
However, to keep it simple and consistent:
Let's add a separate section below the table or a dialog triggered by a custom cell.
*/}
<div className="mt-8 border-t pt-8">
<CategoryMappingSection projectId={selectedProjectId} />
</div>
</div>
);
}
function CategoryMappingSection({ projectId }: { projectId: number }) {
// ... logic to manage mappings would go here ...
// But to properly implement this, we need a full mapping UI.
// Let's defer this implementation pattern to a separate component to keep this file clean
// and just mount it here.
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Category Mappings (Map Sub-categories to Categories)</h2>
<div className="bg-muted/30 p-4 rounded-lg border-dashed border">
<p className="text-sm text-muted-foreground">Select a category to view and manage its sub-categories.</p>
{/*
Real implementation would be complex here.
Better approach: Add a "Manage Sub-categories" button to the Categories table if possible.
Or simpler: A separate "Mapping" page.
*/}
<ManageMappings projectId={projectId} />
</div>
</div>
)
}
import { Plus, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner"; // Use sonner instead of use-toast
function ManageMappings({ projectId }: { projectId: number }) {
const queryClient = useQueryClient();
const [selectedCat, setSelectedCat] = useState<string>("");
const [selectedSubCat, setSelectedSubCat] = useState<string>("");
const { data: categories = [] } = useQuery({
queryKey: ["contract-categories", String(projectId)],
queryFn: () => drawingMasterDataService.getContractCategories(projectId),
});
const { data: subCategories = [] } = useQuery({
queryKey: ["contract-sub-categories", String(projectId)],
queryFn: () => drawingMasterDataService.getContractSubCategories(projectId),
});
const { data: mappings = [] } = useQuery({
queryKey: ["contract-mappings", String(projectId), selectedCat],
queryFn: () => drawingMasterDataService.getContractMappings(projectId, selectedCat ? parseInt(selectedCat) : undefined),
enabled: !!selectedCat,
});
const createMutation = useMutation({
mutationFn: drawingMasterDataService.createContractMapping,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["contract-mappings"] });
toast.success("Mapping created");
setSelectedSubCat("");
},
});
const deleteMutation = useMutation({
mutationFn: drawingMasterDataService.deleteContractMapping,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["contract-mappings"] });
toast.success("Mapping removed");
}
});
const handleAdd = () => {
if (!selectedCat || !selectedSubCat) return;
createMutation.mutate({
projectId,
categoryId: parseInt(selectedCat),
subCategoryId: parseInt(selectedSubCat),
});
};
return (
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium">Select Category</label>
<Select value={selectedCat} onValueChange={setSelectedCat}>
<SelectTrigger>
<SelectValue placeholder="Select Category..." />
</SelectTrigger>
<SelectContent>
{categories.map((c: ContractCategory) => (
<SelectItem key={c.id} value={String(c.id)}>{c.catCode} - {c.catName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedCat && (
<div className="space-y-4">
<div className="flex gap-2 items-end">
<div className="flex-1 space-y-2">
<label className="text-sm font-medium">Add Sub-Category</label>
<Select value={selectedSubCat} onValueChange={setSelectedSubCat}>
<SelectTrigger>
<SelectValue placeholder="Select Sub-Category to add..." />
</SelectTrigger>
<SelectContent>
{subCategories
.filter((s: ContractSubCategory) => !mappings.find((m: { subCategory: { id: number } }) => m.subCategory.id === s.id))
.map((s: ContractSubCategory) => (
<SelectItem key={s.id} value={String(s.id)}>{s.subCatCode} - {s.subCatName}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button onClick={handleAdd} disabled={!selectedSubCat || createMutation.isPending}>
<Plus className="h-4 w-4 mr-2" /> Add
</Button>
</div>
<div className="border rounded-md">
<div className="p-2 bg-muted/50 font-medium text-sm grid grid-cols-[1fr,auto] gap-2">
<span>Mapped Sub-Categories</span>
<span>Action</span>
</div>
{mappings.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">No sub-categories mapped yet.</div>
) : (
<div className="divide-y">
{mappings.map((m: { id: number; subCategory: ContractSubCategory }) => (
<div key={m.id} className="p-2 grid grid-cols-[1fr,auto] gap-2 items-center">
<span className="text-sm">{m.subCategory.subCatCode} - {m.subCategory.subCatName}</span>
<Button variant="ghost" size="sm" onClick={() => deleteMutation.mutate(m.id)} disabled={deleteMutation.isPending}>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,126 @@
"use client";
import { useState } from "react";
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
import { ColumnDef } from "@tanstack/react-table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Loader2 } from "lucide-react";
import { useProjects } from "@/hooks/use-master-data";
import { drawingMasterDataService } from "@/lib/services/drawing-master-data.service";
import { Badge } from "@/components/ui/badge";
interface SubCategory {
id: number;
subCatCode: string;
subCatName: string;
description?: string;
sortOrder: number;
}
export default function ContractSubCategoriesPage() {
const [selectedProjectId, setSelectedProjectId] = useState<number | undefined>(undefined);
const { data: projects = [], isLoading: isLoadingProjects } = useProjects();
const columns: ColumnDef<SubCategory>[] = [
{
accessorKey: "subCatCode",
header: "Code",
cell: ({ row }) => (
<Badge variant="outline" className="font-mono">
{row.getValue("subCatCode")}
</Badge>
),
},
{
accessorKey: "subCatName",
header: "Sub-category Name",
},
{
accessorKey: "description",
header: "Description",
cell: ({ row }) => (
<span className="text-muted-foreground text-sm">
{row.getValue("description") || "-"}
</span>
),
},
{
accessorKey: "sortOrder",
header: "Order",
cell: ({ row }) => (
<span className="font-mono">{row.getValue("sortOrder")}</span>
),
},
];
const projectFilter = (
<div className="flex items-center gap-4">
<span className="text-sm font-medium">Project:</span>
<Select
value={selectedProjectId?.toString() ?? ""}
onValueChange={(v) => setSelectedProjectId(v ? parseInt(v) : undefined)}
>
<SelectTrigger className="w-[300px]">
{isLoadingProjects ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<SelectValue placeholder="Select Project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map((project: { id: number; projectName: string; projectCode: string }) => (
<SelectItem key={project.id} value={String(project.id)}>
{project.projectCode} - {project.projectName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
if (!selectedProjectId) {
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-2xl font-bold">Contract Drawing Sub-categories</h1>
<p className="text-muted-foreground mt-1">
Manage sub-categories () for contract drawings
</p>
</div>
{projectFilter}
<div className="text-center py-12 text-muted-foreground border rounded-lg border-dashed">
Please select a project to manage sub-categories.
</div>
</div>
);
}
return (
<div className="p-6">
<GenericCrudTable
entityName="Sub-category"
title="Contract Drawing Sub-categories"
description="Manage sub-categories (หมวดหมู่ย่อย) for contract drawings"
queryKey={["contract-drawing-sub-categories", String(selectedProjectId)]}
fetchFn={() => drawingMasterDataService.getContractSubCategories(selectedProjectId)}
createFn={(data) => drawingMasterDataService.createContractSubCategory({ ...data, projectId: selectedProjectId })}
updateFn={(id, data) => drawingMasterDataService.updateContractSubCategory(id, data)}
deleteFn={(id) => drawingMasterDataService.deleteContractSubCategory(id)}
columns={columns}
fields={[
{ name: "subCatCode", label: "Sub-category Code", type: "text", required: true },
{ name: "subCatName", label: "Sub-category Name", type: "text", required: true },
{ name: "description", label: "Description", type: "textarea" },
{ name: "sortOrder", label: "Sort Order", type: "text", required: true },
]}
filters={projectFilter}
/>
</div>
);
}

View File

@@ -0,0 +1,126 @@
"use client";
import { useState } from "react";
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
import { ColumnDef } from "@tanstack/react-table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Loader2 } from "lucide-react";
import { useProjects } from "@/hooks/use-master-data";
import { drawingMasterDataService } from "@/lib/services/drawing-master-data.service";
import { Badge } from "@/components/ui/badge";
interface Volume {
id: number;
volumeCode: string;
volumeName: string;
description?: string;
sortOrder: number;
}
export default function ContractVolumesPage() {
const [selectedProjectId, setSelectedProjectId] = useState<number | undefined>(undefined);
const { data: projects = [], isLoading: isLoadingProjects } = useProjects();
const columns: ColumnDef<Volume>[] = [
{
accessorKey: "volumeCode",
header: "Code",
cell: ({ row }) => (
<Badge variant="outline" className="font-mono">
{row.getValue("volumeCode")}
</Badge>
),
},
{
accessorKey: "volumeName",
header: "Volume Name",
},
{
accessorKey: "description",
header: "Description",
cell: ({ row }) => (
<span className="text-muted-foreground text-sm">
{row.getValue("description") || "-"}
</span>
),
},
{
accessorKey: "sortOrder",
header: "Order",
cell: ({ row }) => (
<span className="font-mono">{row.getValue("sortOrder")}</span>
),
},
];
const projectFilter = (
<div className="flex items-center gap-4">
<span className="text-sm font-medium">Project:</span>
<Select
value={selectedProjectId?.toString() ?? ""}
onValueChange={(v) => setSelectedProjectId(v ? parseInt(v) : undefined)}
>
<SelectTrigger className="w-[300px]">
{isLoadingProjects ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<SelectValue placeholder="Select Project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map((project: { id: number; projectName: string; projectCode: string }) => (
<SelectItem key={project.id} value={String(project.id)}>
{project.projectCode} - {project.projectName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
if (!selectedProjectId) {
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-2xl font-bold">Contract Drawing Volumes</h1>
<p className="text-muted-foreground mt-1">
Manage drawing volumes () for contract drawings
</p>
</div>
{projectFilter}
<div className="text-center py-12 text-muted-foreground border rounded-lg border-dashed">
Please select a project to manage volumes.
</div>
</div>
);
}
return (
<div className="p-6">
<GenericCrudTable
entityName="Volume"
title="Contract Drawing Volumes"
description="Manage drawing volumes (เล่ม) for contract drawings"
queryKey={["contract-drawing-volumes", String(selectedProjectId)]}
fetchFn={() => drawingMasterDataService.getContractVolumes(selectedProjectId)}
createFn={(data) => drawingMasterDataService.createContractVolume({ ...data, projectId: selectedProjectId })}
updateFn={(id, data) => drawingMasterDataService.updateContractVolume(id, data)}
deleteFn={(id) => drawingMasterDataService.deleteContractVolume(id)}
columns={columns}
fields={[
{ name: "volumeCode", label: "Volume Code", type: "text", required: true },
{ name: "volumeName", label: "Volume Name", type: "text", required: true },
{ name: "description", label: "Description", type: "textarea" },
{ name: "sortOrder", label: "Sort Order", type: "text", required: true },
]}
filters={projectFilter}
/>
</div>
);
}

View File

@@ -0,0 +1,114 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
FileStack,
FolderTree,
Layers,
BookOpen,
FileBox
} from "lucide-react";
import Link from "next/link";
const contractDrawingMenu = [
{
title: "Volumes",
description: "Manage contract drawing volumes (เล่ม)",
href: "/admin/drawings/contract/volumes",
icon: BookOpen,
},
{
title: "Categories",
description: "Manage main categories (หมวดหมู่หลัก)",
href: "/admin/drawings/contract/categories",
icon: FolderTree,
},
{
title: "Sub-categories",
description: "Manage sub-categories (หมวดหมู่ย่อย)",
href: "/admin/drawings/contract/sub-categories",
icon: Layers,
},
];
const shopDrawingMenu = [
{
title: "Main Categories",
description: "Manage main categories (หมวดหมู่หลัก)",
href: "/admin/drawings/shop/main-categories",
icon: FolderTree,
},
{
title: "Sub-categories",
description: "Manage sub-categories (หมวดหมู่ย่อย)",
href: "/admin/drawings/shop/sub-categories",
icon: Layers,
},
];
export default function DrawingsAdminPage() {
return (
<div className="p-6 space-y-8">
<div>
<h1 className="text-2xl font-bold">Drawing Master Data</h1>
<p className="text-muted-foreground mt-1">
Manage categories and volumes for Contract and Shop Drawings
</p>
</div>
{/* Contract Drawings Section */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<FileStack className="h-5 w-5 text-blue-600" />
<h2 className="text-lg font-semibold">Contract Drawings</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{contractDrawingMenu.map((item) => (
<Link key={item.href} href={item.href}>
<Card className="hover:shadow-md transition-shadow cursor-pointer h-full border-blue-200 hover:border-blue-400">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
{item.title}
</CardTitle>
<item.icon className="h-4 w-4 text-blue-600" />
</CardHeader>
<CardContent>
<p className="text-xs text-muted-foreground">
{item.description}
</p>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
{/* Shop Drawings Section */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<FileBox className="h-5 w-5 text-green-600" />
<h2 className="text-lg font-semibold">Shop Drawings / As Built</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{shopDrawingMenu.map((item) => (
<Link key={item.href} href={item.href}>
<Card className="hover:shadow-md transition-shadow cursor-pointer h-full border-green-200 hover:border-green-400">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
{item.title}
</CardTitle>
<item.icon className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<p className="text-xs text-muted-foreground">
{item.description}
</p>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,146 @@
"use client";
import { useState } from "react";
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
import { ColumnDef } from "@tanstack/react-table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Loader2, CheckCircle, XCircle } from "lucide-react";
import { useProjects } from "@/hooks/use-master-data";
import { drawingMasterDataService } from "@/lib/services/drawing-master-data.service";
import { Badge } from "@/components/ui/badge";
interface MainCategory {
id: number;
mainCategoryCode: string;
mainCategoryName: string;
description?: string;
isActive: boolean;
sortOrder: number;
}
export default function ShopMainCategoriesPage() {
const [selectedProjectId, setSelectedProjectId] = useState<number | undefined>(undefined);
const { data: projects = [], isLoading: isLoadingProjects } = useProjects();
const columns: ColumnDef<MainCategory>[] = [
{
accessorKey: "mainCategoryCode",
header: "Code",
cell: ({ row }) => (
<Badge variant="outline" className="font-mono">
{row.getValue("mainCategoryCode")}
</Badge>
),
},
{
accessorKey: "mainCategoryName",
header: "Category Name",
},
{
accessorKey: "description",
header: "Description",
cell: ({ row }) => (
<span className="text-muted-foreground text-sm">
{row.getValue("description") || "-"}
</span>
),
},
{
accessorKey: "isActive",
header: "Active",
cell: ({ row }) => (
row.getValue("isActive") ? (
<CheckCircle className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-red-600" />
)
),
},
{
accessorKey: "sortOrder",
header: "Order",
cell: ({ row }) => (
<span className="font-mono">{row.getValue("sortOrder")}</span>
),
},
];
const projectFilter = (
<div className="flex items-center gap-4">
<span className="text-sm font-medium">Project:</span>
<Select
value={selectedProjectId?.toString() ?? ""}
onValueChange={(v) => setSelectedProjectId(v ? parseInt(v) : undefined)}
>
<SelectTrigger className="w-[300px]">
{isLoadingProjects ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<SelectValue placeholder="Select Project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map((project: { id: number; projectName: string; projectCode: string }) => (
<SelectItem key={project.id} value={String(project.id)}>
{project.projectCode} - {project.projectName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
if (!selectedProjectId) {
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-2xl font-bold">Shop Drawing Main Categories</h1>
<p className="text-muted-foreground mt-1">
Manage main categories () for shop drawings
</p>
</div>
{projectFilter}
<div className="text-center py-12 text-muted-foreground border rounded-lg border-dashed">
Please select a project to manage main categories.
</div>
</div>
);
}
return (
<div className="p-6">
<GenericCrudTable
entityName="Main Category"
title="Shop Drawing Main Categories"
description="Manage main categories (หมวดหมู่หลัก) for shop drawings"
queryKey={["shop-drawing-main-categories", String(selectedProjectId)]}
fetchFn={() => drawingMasterDataService.getShopMainCategories(selectedProjectId)}
createFn={(data) => drawingMasterDataService.createShopMainCategory({
...data,
projectId: selectedProjectId,
isActive: data.isActive === "true" || data.isActive === true
})}
updateFn={(id, data) => drawingMasterDataService.updateShopMainCategory(id, {
...data,
isActive: data.isActive === "true" || data.isActive === true
})}
deleteFn={(id) => drawingMasterDataService.deleteShopMainCategory(id)}
columns={columns}
fields={[
{ name: "mainCategoryCode", label: "Category Code", type: "text", required: true },
{ name: "mainCategoryName", label: "Category Name", type: "text", required: true },
{ name: "description", label: "Description", type: "textarea" },
{ name: "isActive", label: "Active", type: "checkbox" },
{ name: "sortOrder", label: "Sort Order", type: "text", required: true },
]}
filters={projectFilter}
/>
</div>
);
}

View File

@@ -0,0 +1,146 @@
"use client";
import { useState } from "react";
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
import { ColumnDef } from "@tanstack/react-table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Loader2, CheckCircle, XCircle } from "lucide-react";
import { useProjects } from "@/hooks/use-master-data";
import { drawingMasterDataService } from "@/lib/services/drawing-master-data.service";
import { Badge } from "@/components/ui/badge";
interface SubCategory {
id: number;
subCategoryCode: string;
subCategoryName: string;
description?: string;
isActive: boolean;
sortOrder: number;
}
export default function ShopSubCategoriesPage() {
const [selectedProjectId, setSelectedProjectId] = useState<number | undefined>(undefined);
const { data: projects = [], isLoading: isLoadingProjects } = useProjects();
const columns: ColumnDef<SubCategory>[] = [
{
accessorKey: "subCategoryCode",
header: "Code",
cell: ({ row }) => (
<Badge variant="outline" className="font-mono">
{row.getValue("subCategoryCode")}
</Badge>
),
},
{
accessorKey: "subCategoryName",
header: "Sub-category Name",
},
{
accessorKey: "description",
header: "Description",
cell: ({ row }) => (
<span className="text-muted-foreground text-sm">
{row.getValue("description") || "-"}
</span>
),
},
{
accessorKey: "isActive",
header: "Active",
cell: ({ row }) => (
row.getValue("isActive") ? (
<CheckCircle className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-red-600" />
)
),
},
{
accessorKey: "sortOrder",
header: "Order",
cell: ({ row }) => (
<span className="font-mono">{row.getValue("sortOrder")}</span>
),
},
];
const projectFilter = (
<div className="flex items-center gap-4">
<span className="text-sm font-medium">Project:</span>
<Select
value={selectedProjectId?.toString() ?? ""}
onValueChange={(v) => setSelectedProjectId(v ? parseInt(v) : undefined)}
>
<SelectTrigger className="w-[300px]">
{isLoadingProjects ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<SelectValue placeholder="Select Project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map((project: { id: number; projectName: string; projectCode: string }) => (
<SelectItem key={project.id} value={String(project.id)}>
{project.projectCode} - {project.projectName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
if (!selectedProjectId) {
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-2xl font-bold">Shop Drawing Sub-categories</h1>
<p className="text-muted-foreground mt-1">
Manage sub-categories () for shop drawings
</p>
</div>
{projectFilter}
<div className="text-center py-12 text-muted-foreground border rounded-lg border-dashed">
Please select a project to manage sub-categories.
</div>
</div>
);
}
return (
<div className="p-6">
<GenericCrudTable
entityName="Sub-category"
title="Shop Drawing Sub-categories"
description="Manage sub-categories (หมวดหมู่ย่อย) for shop drawings"
queryKey={["shop-drawing-sub-categories", String(selectedProjectId)]}
fetchFn={() => drawingMasterDataService.getShopSubCategories(selectedProjectId)}
createFn={(data) => drawingMasterDataService.createShopSubCategory({
...data,
projectId: selectedProjectId,
isActive: data.isActive === "true" || data.isActive === true
})}
updateFn={(id, data) => drawingMasterDataService.updateShopSubCategory(id, {
...data,
isActive: data.isActive === "true" || data.isActive === true
})}
deleteFn={(id) => drawingMasterDataService.deleteShopSubCategory(id)}
columns={columns}
fields={[
{ name: "subCategoryCode", label: "Sub-category Code", type: "text", required: true },
{ name: "subCategoryName", label: "Sub-category Name", type: "text", required: true },
{ name: "description", label: "Description", type: "textarea" },
{ name: "isActive", label: "Active", type: "checkbox" },
{ name: "sortOrder", label: "Sort Order", type: "text", required: true },
]}
filters={projectFilter}
/>
</div>
);
}

View File

@@ -11,6 +11,7 @@ import {
Shield,
Activity,
ArrowRight,
FileStack,
} from "lucide-react";
import Link from "next/link";
import { Skeleton } from "@/components/ui/skeleton";
@@ -78,6 +79,12 @@ export default function AdminPage() {
href: "/admin/numbering",
icon: Settings,
},
{
title: "Drawing Master Data",
description: "Manage drawing categories, volumes, and classifications",
href: "/admin/drawings",
icon: FileStack,
},
];
return (

View File

@@ -1,19 +1,31 @@
"use client";
import { useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { DrawingList } from "@/components/drawings/list";
import { Button } from "@/components/ui/button";
import { Upload } from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Upload, Loader2 } from "lucide-react";
import Link from "next/link";
import { useProjects } from "@/hooks/use-master-data";
export default function DrawingsPage() {
const [selectedProjectId, setSelectedProjectId] = useState<number | undefined>(undefined);
const { data: projects = [], isLoading: isLoadingProjects } = useProjects();
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Drawings</h1>
<p className="text-muted-foreground mt-1">
Manage contract and shop drawings
Manage contract, shop, and as-built drawings
</p>
</div>
<Link href="/drawings/upload">
@@ -24,25 +36,79 @@ export default function DrawingsPage() {
</Link>
</div>
<Tabs defaultValue="contract" className="w-full">
<TabsList className="grid w-full grid-cols-3 max-w-[600px]">
<TabsTrigger value="contract">Contract Drawings</TabsTrigger>
<TabsTrigger value="shop">Shop Drawings</TabsTrigger>
<TabsTrigger value="asbuilt">As Built Drawings</TabsTrigger>
</TabsList>
{/* Project Selector */}
<div className="flex items-center gap-4">
<span className="text-sm font-medium">Project:</span>
<Select
value={selectedProjectId?.toString() ?? ""}
onValueChange={(v) => setSelectedProjectId(v ? parseInt(v) : undefined)}
>
<SelectTrigger className="w-[300px]">
{isLoadingProjects ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<SelectValue placeholder="Select Project" />
)}
</SelectTrigger>
<SelectContent>
{projects.map((project: { id: number; projectName: string; projectCode: string }) => (
<SelectItem key={project.id} value={String(project.id)}>
{project.projectCode} - {project.projectName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<TabsContent value="contract" className="mt-6">
<DrawingList type="CONTRACT" />
</TabsContent>
<TabsContent value="shop" className="mt-6">
<DrawingList type="SHOP" />
</TabsContent>
<TabsContent value="asbuilt" className="mt-6">
<DrawingList type="AS_BUILT" />
</TabsContent>
</Tabs>
{!selectedProjectId ? (
<div className="text-center py-12 text-muted-foreground border rounded-lg border-dashed">
Please select a project to view drawings.
</div>
) : (
<DrawingTabs projectId={selectedProjectId} />
)}
</div>
);
}
function DrawingTabs({ projectId }: { projectId: number }) {
const [search, setSearch] = useState("");
// We can add more specific filters here (e.g. category) later
return (
<Tabs defaultValue="contract" className="w-full">
<div className="flex justify-between items-center mb-6">
<TabsList className="grid w-full grid-cols-3 max-w-[400px]">
<TabsTrigger value="contract">Contract</TabsTrigger>
<TabsTrigger value="shop">Shop</TabsTrigger>
<TabsTrigger value="asbuilt">As Built</TabsTrigger>
</TabsList>
<div className="flex gap-2">
<div className="relative">
<input
type="text"
placeholder="Search drawings..."
className="h-10 w-[250px] rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
</div>
<TabsContent value="contract" className="mt-0">
<DrawingList type="CONTRACT" projectId={projectId} filters={{ search }} />
</TabsContent>
<TabsContent value="shop" className="mt-0">
<DrawingList type="SHOP" projectId={projectId} filters={{ search }} />
</TabsContent>
<TabsContent value="asbuilt" className="mt-0">
<DrawingList type="AS_BUILT" projectId={projectId} filters={{ search }} />
</TabsContent>
</Tabs>
)
}

View File

@@ -2,15 +2,46 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Users, Building2, Settings, FileText, Activity, GitGraph, Shield, BookOpen } from "lucide-react";
import {
Users,
Building2,
Settings,
FileText,
Activity,
GitGraph,
Shield,
BookOpen,
FileStack,
ChevronDown,
ChevronRight,
} from "lucide-react";
const menuItems = [
interface MenuItem {
href?: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
children?: { href: string; label: string }[];
}
const menuItems: MenuItem[] = [
{ href: "/admin/users", label: "Users", icon: Users },
{ href: "/admin/organizations", label: "Organizations", icon: Building2 },
{ href: "/admin/projects", label: "Projects", icon: FileText },
{ href: "/admin/contracts", label: "Contracts", icon: FileText },
{ href: "/admin/reference", label: "Reference Data", icon: BookOpen },
{
label: "Drawing Master Data",
icon: FileStack,
children: [
{ href: "/admin/drawings/contract/volumes", label: "Contract: Volumes" },
{ href: "/admin/drawings/contract/categories", label: "Contract: Categories" },
{ href: "/admin/drawings/contract/sub-categories", label: "Contract: Sub-categories" },
{ href: "/admin/drawings/shop/main-categories", label: "Shop: Main Categories" },
{ href: "/admin/drawings/shop/sub-categories", label: "Shop: Sub-categories" },
]
},
{ href: "/admin/numbering", label: "Numbering", icon: FileText },
{ href: "/admin/workflows", label: "Workflows", icon: GitGraph },
{ href: "/admin/security/roles", label: "Security Roles", icon: Shield },
@@ -22,6 +53,20 @@ const menuItems = [
export function AdminSidebar() {
const pathname = usePathname();
const [expandedMenus, setExpandedMenus] = useState<string[]>(
// Auto-expand if current path matches a child
menuItems
.filter(item => item.children?.some(child => pathname.startsWith(child.href)))
.map(item => item.label)
);
const toggleMenu = (label: string) => {
setExpandedMenus(prev =>
prev.includes(label)
? prev.filter(l => l !== label)
: [...prev, label]
);
};
return (
<aside className="w-64 border-r bg-card p-4 hidden md:block">
@@ -33,12 +78,65 @@ export function AdminSidebar() {
<nav className="space-y-1">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname.startsWith(item.href);
// Has children - collapsible menu
if (item.children) {
const isExpanded = expandedMenus.includes(item.label);
const hasActiveChild = item.children.some(child => pathname.startsWith(child.href));
return (
<div key={item.label}>
<button
onClick={() => toggleMenu(item.label)}
className={cn(
"w-full flex items-center justify-between gap-3 px-3 py-2 rounded-lg transition-colors text-sm font-medium",
hasActiveChild
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<span className="flex items-center gap-3">
<Icon className="h-4 w-4" />
<span>{item.label}</span>
</span>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
{isExpanded && (
<div className="ml-4 mt-1 space-y-1 border-l pl-4">
{item.children.map((child) => {
const isActive = pathname === child.href;
return (
<Link
key={child.href}
href={child.href}
className={cn(
"block px-3 py-1.5 rounded-lg transition-colors text-sm",
isActive
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
{child.label}
</Link>
);
})}
</div>
)}
</div>
);
}
// Simple menu item
const isActive = pathname.startsWith(item.href!);
return (
<Link
key={item.href}
href={item.href}
href={item.href!}
className={cn(
"flex items-center gap-3 px-3 py-2 rounded-lg transition-colors text-sm font-medium",
isActive

View File

@@ -5,13 +5,21 @@ import { useDrawings } from "@/hooks/use-drawing";
import { Drawing } from "@/types/drawing";
import { Loader2 } from "lucide-react";
import { SearchContractDrawingDto } from "@/types/dto/drawing/contract-drawing.dto";
import { SearchShopDrawingDto } from "@/types/dto/drawing/shop-drawing.dto";
import { SearchAsBuiltDrawingDto } from "@/types/dto/drawing/asbuilt-drawing.dto";
type DrawingSearchParams = SearchContractDrawingDto | SearchShopDrawingDto | SearchAsBuiltDrawingDto;
interface DrawingListProps {
type: "CONTRACT" | "SHOP" | "AS_BUILT";
projectId?: number;
projectId: number;
filters?: Partial<DrawingSearchParams>;
}
export function DrawingList({ type, projectId }: DrawingListProps) {
const { data: drawings, isLoading, isError } = useDrawings(type, { projectId: projectId ?? 1 });
export function DrawingList({ type, projectId, filters }: DrawingListProps) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data: drawings, isLoading, isError } = useDrawings(type, { projectId, ...filters } as any);
// Note: The hook handles switching services based on type.
// The params { type } might be redundant if getAll doesn't use it, but safe to pass.

View File

@@ -55,9 +55,11 @@ const shopSchema = baseSchema.extend({
const asBuiltSchema = baseSchema.extend({
drawingType: z.literal("AS_BUILT"),
drawingNumber: z.string().min(1, "Drawing Number is required"),
mainCategoryId: z.string().min(1, "Main Category is required"),
subCategoryId: z.string().min(1, "Sub Category is required"),
// Revision Fields
revisionLabel: z.string().default("0"),
title: z.string().optional(),
title: z.string().min(1, "Title is required"),
legacyDrawingNumber: z.string().optional(),
description: z.string().optional(),
});
@@ -130,8 +132,10 @@ export function DrawingUploadForm({ projectId = 1 }: DrawingUploadFormProps) {
// Date default to now
} else if (data.drawingType === 'AS_BUILT') {
formData.append('drawingNumber', data.drawingNumber);
formData.append('mainCategoryId', data.mainCategoryId);
formData.append('subCategoryId', data.subCategoryId);
formData.append('revisionLabel', data.revisionLabel || '0');
if (data.title) formData.append('title', data.title);
formData.append('title', data.title);
if (data.legacyDrawingNumber) formData.append('legacyDrawingNumber', data.legacyDrawingNumber);
if (data.description) formData.append('description', data.description);
}
@@ -293,7 +297,7 @@ export function DrawingUploadForm({ projectId = 1 }: DrawingUploadFormProps) {
{/* AS BUILT FIELDS */}
{drawingType === 'AS_BUILT' && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>Drawing No *</Label>
<Input {...register("drawingNumber")} placeholder="e.g. AB-101" />
@@ -306,9 +310,51 @@ export function DrawingUploadForm({ projectId = 1 }: DrawingUploadFormProps) {
<Input {...register("legacyDrawingNumber")} placeholder="Legacy No." />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>Main Category *</Label>
<Select onValueChange={(v) => {
setValue("mainCategoryId", v);
setSelectedShopMainCat(v ? parseInt(v) : undefined);
}}>
<SelectTrigger>
<SelectValue placeholder="Select Main Category" />
</SelectTrigger>
<SelectContent>
{shopMainCats?.map((c: any) => (
<SelectItem key={c.id} value={String(c.id)}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
{(errors as any).mainCategoryId && (
<p className="text-sm text-destructive">{(errors as any).mainCategoryId.message}</p>
)}
</div>
<div>
<Label>Sub Category *</Label>
<Select onValueChange={(v) => setValue("subCategoryId", v)}>
<SelectTrigger>
<SelectValue placeholder="Select Sub Category" />
</SelectTrigger>
<SelectContent>
{shopSubCats?.map((c: any) => (
<SelectItem key={c.id} value={String(c.id)}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
{(errors as any).subCategoryId && (
<p className="text-sm text-destructive">{(errors as any).subCategoryId.message}</p>
)}
</div>
</div>
<div>
<Label>Title</Label>
<Input {...register("title")} placeholder="Title" />
<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>
)}
</div>
<div>
<Label>Description</Label>

View File

@@ -30,8 +30,8 @@ const VARIABLES = [
{ key: '{DISCIPLINE}', name: 'Discipline Code', example: 'STR' },
{ key: '{SUBTYPE}', name: 'Sub-Type Code', example: 'GEN' },
{ key: '{SUBTYPE_NUM}', name: 'Sub-Type Number', example: '01' },
{ key: '{YEAR}', name: 'Year (B.E.)', example: '2568' },
{ key: '{YEAR_SHORT}', name: 'Year Short (68)', example: '68' },
{ key: '{YEAR:BE}', name: 'Year (B.E.)', example: '2568' },
{ key: '{YEAR:CE}', name: 'Year (C.E.)', example: '2025' },
{ key: '{SEQ:4}', name: 'Sequence (4-digit)', example: '0001' },
];
@@ -69,8 +69,8 @@ export function TemplateEditor({
VARIABLES.forEach((v) => {
// Simple mock replacement for preview
let replacement = v.example;
if (v.key === '{YEAR}') replacement = (new Date().getFullYear() + 543).toString();
if (v.key === '{YEAR_SHORT}') replacement = (new Date().getFullYear() + 543).toString().slice(-2);
if (v.key === '{YEAR:BE}') replacement = (new Date().getFullYear() + 543).toString();
if (v.key === '{YEAR:CE}') replacement = new Date().getFullYear().toString();
// Dynamic context based on selection (optional visual enhancement)
if (v.key === '{TYPE}' && typeId) {

View File

@@ -68,9 +68,9 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
try {
const result = await numberingApi.previewNumber({
projectId: projectId,
originatorId: parseInt(testData.originatorId || "0"),
originatorOrganizationId: parseInt(testData.originatorId || "0"),
recipientOrganizationId: parseInt(testData.recipientId || "0"),
typeId: parseInt(testData.correspondenceTypeId || "0"),
correspondenceTypeId: parseInt(testData.correspondenceTypeId || "0"),
disciplineId: parseInt(testData.disciplineId || "0"),
});
setGeneratedNumber(result.previewNumber);

View File

@@ -1,13 +1,14 @@
import { useQuery, useMutation, 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'; // Added
import { asBuiltDrawingService } from '@/lib/services/asbuilt-drawing.service';
import { SearchContractDrawingDto, CreateContractDrawingDto } from '@/types/dto/drawing/contract-drawing.dto';
import { SearchShopDrawingDto, CreateShopDrawingDto } from '@/types/dto/drawing/shop-drawing.dto';
import { SearchAsBuiltDrawingDto, CreateAsBuiltDrawingDto } from '@/types/dto/drawing/asbuilt-drawing.dto'; // Added
import { SearchAsBuiltDrawingDto, CreateAsBuiltDrawingDto } from '@/types/dto/drawing/asbuilt-drawing.dto';
import { toast } from 'sonner';
import { ContractDrawing, ShopDrawing, AsBuiltDrawing } from "@/types/drawing";
type DrawingType = 'CONTRACT' | 'SHOP' | 'AS_BUILT'; // Added AS_BUILT
type DrawingType = 'CONTRACT' | 'SHOP' | 'AS_BUILT';
type DrawingSearchParams = SearchContractDrawingDto | SearchShopDrawingDto | SearchAsBuiltDrawingDto;
type CreateDrawingData = CreateContractDrawingDto | CreateShopDrawingDto | CreateAsBuiltDrawingDto;
@@ -25,13 +26,45 @@ export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
return useQuery({
queryKey: drawingKeys.list(type, params),
queryFn: async () => {
let response;
if (type === 'CONTRACT') {
return contractDrawingService.getAll(params as SearchContractDrawingDto);
response = await contractDrawingService.getAll(params as SearchContractDrawingDto);
// Map ContractDrawing to Drawing
if (response && response.data) {
response.data = response.data.map((d: ContractDrawing) => ({
...d,
drawingId: d.id,
drawingNumber: d.contractDrawingNo,
type: 'CONTRACT',
}));
}
} else if (type === 'SHOP') {
return shopDrawingService.getAll(params as SearchShopDrawingDto);
response = await shopDrawingService.getAll(params as SearchShopDrawingDto);
// Map ShopDrawing to Drawing
if (response && response.data) {
response.data = response.data.map((d: ShopDrawing) => ({
...d,
drawingId: d.id,
type: 'SHOP',
title: d.currentRevision?.title || "Untitled",
revision: d.currentRevision?.revisionNumber,
legacyDrawingNumber: d.currentRevision?.legacyDrawingNumber,
}));
}
} else {
return asBuiltDrawingService.getAll(params as SearchAsBuiltDrawingDto);
response = await asBuiltDrawingService.getAll(params as SearchAsBuiltDrawingDto);
// Map AsBuiltDrawing to Drawing
if (response && response.data) {
response.data = response.data.map((d: AsBuiltDrawing) => ({
...d,
drawingId: d.id,
type: 'AS_BUILT',
title: d.currentRevision?.title || "Untitled",
revision: d.currentRevision?.revisionNumber,
}));
}
}
return response;
},
placeholderData: (previousData) => previousData,
});
@@ -69,7 +102,8 @@ export function useCreateDrawing(type: DrawingType) {
}
},
onSuccess: () => {
toast.success(`${type === 'CONTRACT' ? 'Contract' : 'Shop'} Drawing uploaded successfully`);
const typeName = type === 'CONTRACT' ? 'Contract' : type === 'SHOP' ? 'Shop' : 'As Built';
toast.success(`${typeName} Drawing uploaded successfully`);
queryClient.invalidateQueries({ queryKey: drawingKeys.lists() });
},
onError: (error: Error & { response?: { data?: { message?: string } } }) => {
@@ -79,5 +113,3 @@ export function useCreateDrawing(type: DrawingType) {
},
});
}
// You can add useCreateShopDrawingRevision logic here if needed separate

View File

@@ -1,7 +1,6 @@
// File: lib/api/client.ts
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosError } from "axios";
import { v4 as uuidv4 } from "uuid";
import { getSession } from "next-auth/react";
// อ่านค่า Base URL จาก Environment Variable
const baseURL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api";
@@ -29,18 +28,20 @@ apiClient.interceptors.request.use(
}
// 2. Authentication Token Injection
// ดึง Session จาก NextAuth (ทำงานเฉพาะฝั่ง Client)
// ดึง Token จาก Zustand persist store (localStorage)
if (typeof window !== "undefined") {
try {
const session = await getSession();
// @ts-ignore: Session type extended in types/next-auth.d.ts
const token = session?.accessToken;
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
const parsed = JSON.parse(authStorage);
const token = parsed?.state?.token;
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
}
}
} catch (error) {
console.warn("Failed to retrieve session token:", error);
console.warn("Failed to retrieve auth token:", error);
}
}
@@ -73,4 +74,4 @@ apiClient.interceptors.response.use(
}
);
export default apiClient;
export default apiClient;

View File

@@ -274,18 +274,19 @@ export const numberingApi = {
*/
previewNumber: async (ctx: {
projectId: number;
originatorId: number;
typeId: number;
originatorOrganizationId: number;
correspondenceTypeId: number;
disciplineId?: number;
subTypeId?: number;
rfaTypeId?: number;
recipientOrganizationId?: number;
}): Promise<{ previewNumber: string; nextSequence: number }> => {
const res = await apiClient.post<{ previewNumber: string; nextSequence: number }>(
const res = await apiClient.post<{ data: { previewNumber: string; nextSequence: number } }>(
'/document-numbering/preview',
ctx
);
return res.data;
// Backend wraps response in { data: { ... }, message: "Success" }
return res.data.data || res.data;
},
/**

View File

@@ -0,0 +1,245 @@
// File: lib/services/drawing-master-data.service.ts
import apiClient from "@/lib/api/client";
// ===========================
// Contract Drawing Volumes
// ===========================
export interface ContractVolume {
id: number;
projectId: number;
volumeCode: string;
volumeName: string;
description?: string;
sortOrder: number;
}
export interface CreateContractVolumeDto {
projectId: number;
volumeCode: string;
volumeName: string;
description?: string;
sortOrder: number;
}
// ===========================
// Contract Drawing Categories
// ===========================
export interface ContractCategory {
id: number;
projectId: number;
catCode: string;
catName: string;
description?: string;
sortOrder: number;
}
export interface CreateContractCategoryDto {
projectId: number;
catCode: string;
catName: string;
description?: string;
sortOrder: number;
}
// ===========================
// Contract Drawing Sub-categories
// ===========================
export interface ContractSubCategory {
id: number;
projectId: number;
subCatCode: string;
subCatName: string;
description?: string;
sortOrder: number;
}
export interface CreateContractSubCategoryDto {
projectId: number;
subCatCode: string;
subCatName: string;
description?: string;
sortOrder: number;
}
// ===========================
// Shop Drawing Main Categories
// ===========================
export interface ShopMainCategory {
id: number;
projectId: number;
mainCategoryCode: string;
mainCategoryName: string;
description?: string;
isActive: boolean;
sortOrder: number;
}
export interface CreateShopMainCategoryDto {
projectId: number;
mainCategoryCode: string;
mainCategoryName: string;
description?: string;
isActive?: boolean;
sortOrder: number;
}
// ===========================
// Shop Drawing Sub-categories
// ===========================
export interface ShopSubCategory {
id: number;
projectId: number;
subCategoryCode: string;
subCategoryName: string;
description?: string;
isActive: boolean;
sortOrder: number;
}
export interface CreateShopSubCategoryDto {
projectId: number;
subCategoryCode: string;
subCategoryName: string;
description?: string;
isActive?: boolean;
sortOrder: number;
}
// ===========================
// Service
// ===========================
export const drawingMasterDataService = {
// --- Contract Volumes ---
async getContractVolumes(projectId: number): Promise<ContractVolume[]> {
const response = await apiClient.get(`/drawings/master-data/contract/volumes`, {
params: { projectId },
});
return response.data;
},
async createContractVolume(data: CreateContractVolumeDto): Promise<ContractVolume> {
const response = await apiClient.post(`/drawings/master-data/contract/volumes`, data);
return response.data;
},
async updateContractVolume(id: number, data: Partial<CreateContractVolumeDto>): Promise<ContractVolume> {
const response = await apiClient.patch(`/drawings/master-data/contract/volumes/${id}`, data);
return response.data;
},
async deleteContractVolume(id: number): Promise<void> {
await apiClient.delete(`/drawings/master-data/contract/volumes/${id}`);
},
// --- Contract Categories ---
async getContractCategories(projectId: number): Promise<ContractCategory[]> {
const response = await apiClient.get(`/drawings/master-data/contract/categories`, {
params: { projectId },
});
return response.data;
},
async createContractCategory(data: CreateContractCategoryDto): Promise<ContractCategory> {
const response = await apiClient.post(`/drawings/master-data/contract/categories`, data);
return response.data;
},
async updateContractCategory(id: number, data: Partial<CreateContractCategoryDto>): Promise<ContractCategory> {
const response = await apiClient.patch(`/drawings/master-data/contract/categories/${id}`, data);
return response.data;
},
async deleteContractCategory(id: number): Promise<void> {
await apiClient.delete(`/drawings/master-data/contract/categories/${id}`);
},
// --- Contract Sub-categories ---
async getContractSubCategories(projectId: number): Promise<ContractSubCategory[]> {
const response = await apiClient.get(`/drawings/master-data/contract/sub-categories`, {
params: { projectId },
});
return response.data;
},
async createContractSubCategory(data: CreateContractSubCategoryDto): Promise<ContractSubCategory> {
const response = await apiClient.post(`/drawings/master-data/contract/sub-categories`, data);
return response.data;
},
async updateContractSubCategory(id: number, data: Partial<CreateContractSubCategoryDto>): Promise<ContractSubCategory> {
const response = await apiClient.patch(`/drawings/master-data/contract/sub-categories/${id}`, data);
return response.data;
},
async deleteContractSubCategory(id: number): Promise<void> {
await apiClient.delete(`/drawings/master-data/contract/sub-categories/${id}`);
},
// --- Contract Category Mappings ---
async getContractMappings(
projectId: number,
categoryId?: number
): Promise<{ id: number; subCategory: ContractSubCategory; category: ContractCategory }[]> {
const response = await apiClient.get(`/drawings/master-data/contract/mappings`, {
params: { projectId, categoryId },
});
return response.data;
},
async createContractMapping(data: {
projectId: number;
categoryId: number;
subCategoryId: number;
}): Promise<{ id: number }> {
const response = await apiClient.post(`/drawings/master-data/contract/mappings`, data);
return response.data;
},
async deleteContractMapping(id: number): Promise<void> {
await apiClient.delete(`/drawings/master-data/contract/mappings/${id}`);
},
// --- Shop Main Categories ---
async getShopMainCategories(projectId: number): Promise<ShopMainCategory[]> {
const response = await apiClient.get(`/drawings/master-data/shop/main-categories`, {
params: { projectId },
});
return response.data;
},
async createShopMainCategory(data: CreateShopMainCategoryDto): Promise<ShopMainCategory> {
const response = await apiClient.post(`/drawings/master-data/shop/main-categories`, data);
return response.data;
},
async updateShopMainCategory(id: number, data: Partial<CreateShopMainCategoryDto>): Promise<ShopMainCategory> {
const response = await apiClient.patch(`/drawings/master-data/shop/main-categories/${id}`, data);
return response.data;
},
async deleteShopMainCategory(id: number): Promise<void> {
await apiClient.delete(`/drawings/master-data/shop/main-categories/${id}`);
},
// --- Shop Sub-categories ---
async getShopSubCategories(projectId: number, mainCategoryId?: number): Promise<ShopSubCategory[]> {
const response = await apiClient.get(`/drawings/master-data/shop/sub-categories`, {
params: { projectId, mainCategoryId },
});
return response.data;
},
async createShopSubCategory(data: CreateShopSubCategoryDto): Promise<ShopSubCategory> {
const response = await apiClient.post(`/drawings/master-data/shop/sub-categories`, data);
return response.data;
},
async updateShopSubCategory(id: number, data: Partial<CreateShopSubCategoryDto>): Promise<ShopSubCategory> {
const response = await apiClient.patch(`/drawings/master-data/shop/sub-categories/${id}`, data);
return response.data;
},
async deleteShopSubCategory(id: number): Promise<void> {
await apiClient.delete(`/drawings/master-data/shop/sub-categories/${id}`);
},
};

View File

@@ -8,7 +8,9 @@ export interface DrawingRevision {
revisionDescription?: string;
revisedByName: string;
fileUrl: string;
isCurrent: boolean;
isCurrent: boolean | null; // Updated: null = not current (MariaDB UNIQUE pattern)
createdBy?: number; // Added v1.7.0
updatedBy?: number; // Added v1.7.0
}
export interface ContractDrawing {
@@ -39,6 +41,8 @@ export interface AsBuiltDrawing {
id: number;
drawingNumber: string;
projectId: number;
mainCategoryId: number;
subCategoryId: number;
currentRevision?: DrawingRevision;
createdAt: string;
updatedAt: string;

View File

@@ -4,6 +4,8 @@
export interface CreateAsBuiltDrawingDto {
projectId: number;
drawingNumber: string;
mainCategoryId: number;
subCategoryId: number;
// First Revision Data
revisionLabel?: string;