251225:1703 On going update to 1.7.0: Refoctory drawing Module not finish
This commit is contained in:
282
frontend/app/(admin)/admin/drawings/contract/categories/page.tsx
Normal file
282
frontend/app/(admin)/admin/drawings/contract/categories/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
126
frontend/app/(admin)/admin/drawings/contract/volumes/page.tsx
Normal file
126
frontend/app/(admin)/admin/drawings/contract/volumes/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
114
frontend/app/(admin)/admin/drawings/page.tsx
Normal file
114
frontend/app/(admin)/admin/drawings/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
146
frontend/app/(admin)/admin/drawings/shop/sub-categories/page.tsx
Normal file
146
frontend/app/(admin)/admin/drawings/shop/sub-categories/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
245
frontend/lib/services/drawing-master-data.service.ts
Normal file
245
frontend/lib/services/drawing-master-data.service.ts
Normal 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}`);
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
export interface CreateAsBuiltDrawingDto {
|
||||
projectId: number;
|
||||
drawingNumber: string;
|
||||
mainCategoryId: number;
|
||||
subCategoryId: number;
|
||||
|
||||
// First Revision Data
|
||||
revisionLabel?: string;
|
||||
|
||||
Reference in New Issue
Block a user