260219:1649 20260219 TASK-BEFE-001 Fixed Blank Admin Pages
All checks were successful
Build and Deploy / deploy (push) Successful in 2m41s

This commit is contained in:
admin
2026-02-19 16:49:07 +07:00
parent fbd663e870
commit d455598dc2
3 changed files with 169 additions and 175 deletions

View File

@@ -1,19 +1,20 @@
"use client"; 'use client';
import { useState } from "react"; import { useState } from 'react';
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table"; import { GenericCrudTable } from '@/components/admin/reference/generic-crud-table';
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from '@tanstack/react-table';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Loader2, Plus, Trash2 } from 'lucide-react';
import { useProjects } from '@/hooks/use-master-data';
import { import {
Select, drawingMasterDataService,
SelectContent, ContractCategory,
SelectItem, ContractSubCategory,
SelectTrigger, } from '@/lib/services/drawing-master-data.service';
SelectValue, import { Badge } from '@/components/ui/badge';
} from "@/components/ui/select"; import { Button } from '@/components/ui/button';
import { Loader2 } from "lucide-react"; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useProjects } from "@/hooks/use-master-data"; import { toast } from 'sonner';
import { drawingMasterDataService, ContractCategory, ContractSubCategory } from "@/lib/services/drawing-master-data.service";
import { Badge } from "@/components/ui/badge";
interface Category { interface Category {
id: number; id: number;
@@ -29,33 +30,27 @@ export default function ContractCategoriesPage() {
const columns: ColumnDef<Category>[] = [ const columns: ColumnDef<Category>[] = [
{ {
accessorKey: "catCode", accessorKey: 'catCode',
header: "Code", header: 'Code',
cell: ({ row }) => ( cell: ({ row }) => (
<Badge variant="outline" className="font-mono"> <Badge variant="outline" className="font-mono">
{row.getValue("catCode")} {row.getValue('catCode')}
</Badge> </Badge>
), ),
}, },
{ {
accessorKey: "catName", accessorKey: 'catName',
header: "Category Name", header: 'Category Name',
}, },
{ {
accessorKey: "description", accessorKey: 'description',
header: "Description", header: 'Description',
cell: ({ row }) => ( cell: ({ row }) => <span className="text-muted-foreground text-sm">{row.getValue('description') || '-'}</span>,
<span className="text-muted-foreground text-sm">
{row.getValue("description") || "-"}
</span>
),
}, },
{ {
accessorKey: "sortOrder", accessorKey: 'sortOrder',
header: "Order", header: 'Order',
cell: ({ row }) => ( cell: ({ row }) => <span className="font-mono">{row.getValue('sortOrder')}</span>,
<span className="font-mono">{row.getValue("sortOrder")}</span>
),
}, },
]; ];
@@ -63,7 +58,7 @@ export default function ContractCategoriesPage() {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-sm font-medium">Project:</span> <span className="text-sm font-medium">Project:</span>
<Select <Select
value={selectedProjectId?.toString() ?? ""} value={selectedProjectId?.toString() ?? ''}
onValueChange={(v) => setSelectedProjectId(v ? parseInt(v) : undefined)} onValueChange={(v) => setSelectedProjectId(v ? parseInt(v) : undefined)}
> >
<SelectTrigger className="w-[300px]"> <SelectTrigger className="w-[300px]">
@@ -89,9 +84,7 @@ export default function ContractCategoriesPage() {
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<div> <div>
<h1 className="text-2xl font-bold">Contract Drawing Categories</h1> <h1 className="text-2xl font-bold">Contract Drawing Categories</h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1">Manage main categories () for contract drawings</p>
Manage main categories () for contract drawings
</p>
</div> </div>
{projectFilter} {projectFilter}
<div className="text-center py-12 text-muted-foreground border rounded-lg border-dashed"> <div className="text-center py-12 text-muted-foreground border rounded-lg border-dashed">
@@ -107,17 +100,17 @@ export default function ContractCategoriesPage() {
entityName="Category" entityName="Category"
title="Contract Drawing Categories" title="Contract Drawing Categories"
description="Manage main categories (หมวดหมู่หลัก) for contract drawings" description="Manage main categories (หมวดหมู่หลัก) for contract drawings"
queryKey={["contract-drawing-categories", String(selectedProjectId)]} queryKey={['contract-drawing-categories', String(selectedProjectId)]}
fetchFn={() => drawingMasterDataService.getContractCategories(selectedProjectId)} fetchFn={() => drawingMasterDataService.getContractCategories(selectedProjectId)}
createFn={(data) => drawingMasterDataService.createContractCategory({ ...data, projectId: selectedProjectId })} createFn={(data) => drawingMasterDataService.createContractCategory({ ...data, projectId: selectedProjectId })}
updateFn={(id, data) => drawingMasterDataService.updateContractCategory(id, data)} updateFn={(id, data) => drawingMasterDataService.updateContractCategory(id, data)}
deleteFn={(id) => drawingMasterDataService.deleteContractCategory(id)} deleteFn={(id) => drawingMasterDataService.deleteContractCategory(id)}
columns={columns} columns={columns}
fields={[ fields={[
{ name: "catCode", label: "Category Code", type: "text", required: true }, { name: 'catCode', label: 'Category Code', type: 'text', required: true },
{ name: "catName", label: "Category Name", type: "text", required: true }, { name: 'catName', label: 'Category Name', type: 'text', required: true },
{ name: "description", label: "Description", type: "textarea" }, { name: 'description', label: 'Description', type: 'textarea' },
{ name: "sortOrder", label: "Sort Order", type: "text", required: true }, { name: 'sortOrder', label: 'Sort Order', type: 'text', required: true },
]} ]}
filters={projectFilter} filters={projectFilter}
/> />
@@ -135,76 +128,71 @@ export default function ContractCategoriesPage() {
However, to keep it simple and consistent: However, to keep it simple and consistent:
Let's add a separate section below the table or a dialog triggered by a custom cell. 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"> <div className="mt-8 border-t pt-8">
<CategoryMappingSection projectId={selectedProjectId} /> <CategoryMappingSection projectId={selectedProjectId} />
</div> </div>
</div> </div>
); );
} }
function CategoryMappingSection({ projectId }: { projectId: number }) { function CategoryMappingSection({ projectId }: { projectId: number }) {
// ... logic to manage mappings would go here ...
// ... logic to manage mappings would go here ... // But to properly implement this, we need a full mapping UI.
// 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
// Let's defer this implementation pattern to a separate component to keep this file clean // and just mount it here.
// and just mount it here. return (
return ( <div className="space-y-4">
<div className="space-y-4"> <h2 className="text-xl font-semibold">Category Mappings (Map Sub-categories to Categories)</h2>
<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">
<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>
<p className="text-sm text-muted-foreground">Select a category to view and manage its sub-categories.</p> {/*
{/*
Real implementation would be complex here. Real implementation would be complex here.
Better approach: Add a "Manage Sub-categories" button to the Categories table if possible. Better approach: Add a "Manage Sub-categories" button to the Categories table if possible.
Or simpler: A separate "Mapping" page. Or simpler: A separate "Mapping" page.
*/} */}
<ManageMappings projectId={projectId} /> <ManageMappings projectId={projectId} />
</div> </div>
</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 }) { function ManageMappings({ projectId }: { projectId: number }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [selectedCat, setSelectedCat] = useState<string>(""); const [selectedCat, setSelectedCat] = useState<string>('');
const [selectedSubCat, setSelectedSubCat] = useState<string>(""); const [selectedSubCat, setSelectedSubCat] = useState<string>('');
const { data: categories = [] } = useQuery({ const { data: categories = [] } = useQuery({
queryKey: ["contract-categories", String(projectId)], queryKey: ['contract-categories', String(projectId)],
queryFn: () => drawingMasterDataService.getContractCategories(projectId), queryFn: () => drawingMasterDataService.getContractCategories(projectId),
}); });
const { data: subCategories = [] } = useQuery({ const { data: subCategories = [] } = useQuery({
queryKey: ["contract-sub-categories", String(projectId)], queryKey: ['contract-sub-categories', String(projectId)],
queryFn: () => drawingMasterDataService.getContractSubCategories(projectId), queryFn: () => drawingMasterDataService.getContractSubCategories(projectId),
}); });
const { data: mappings = [] } = useQuery({ const { data: mappings = [] } = useQuery({
queryKey: ["contract-mappings", String(projectId), selectedCat], queryKey: ['contract-mappings', String(projectId), selectedCat],
queryFn: () => drawingMasterDataService.getContractMappings(projectId, selectedCat ? parseInt(selectedCat) : undefined), queryFn: () =>
drawingMasterDataService.getContractMappings(projectId, selectedCat ? parseInt(selectedCat) : undefined),
enabled: !!selectedCat, enabled: !!selectedCat,
}); });
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: drawingMasterDataService.createContractMapping, mutationFn: drawingMasterDataService.createContractMapping,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["contract-mappings"] }); queryClient.invalidateQueries({ queryKey: ['contract-mappings'] });
toast.success("Mapping created"); toast.success('Mapping created');
setSelectedSubCat(""); setSelectedSubCat('');
}, },
}); });
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: drawingMasterDataService.deleteContractMapping, mutationFn: drawingMasterDataService.deleteContractMapping,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["contract-mappings"] }); queryClient.invalidateQueries({ queryKey: ['contract-mappings'] });
toast.success("Mapping removed"); toast.success('Mapping removed');
} },
}); });
const handleAdd = () => { const handleAdd = () => {
@@ -218,65 +206,79 @@ function ManageMappings({ projectId }: { projectId: number }) {
return ( return (
<div className="grid gap-6 md:grid-cols-2"> <div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Select Category</label> <label className="text-sm font-medium">Select Category</label>
<Select value={selectedCat} onValueChange={setSelectedCat}> <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> <SelectTrigger>
<SelectValue placeholder="Select Category..." /> <SelectValue placeholder="Select Sub-Category to add..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{categories.map((c: ContractCategory) => ( {subCategories
<SelectItem key={c.id} value={String(c.id)}>{c.catCode} - {c.catName}</SelectItem> .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> </SelectContent>
</Select> </Select>
</div> </div>
<Button onClick={handleAdd} disabled={!selectedSubCat || createMutation.isPending}>
<Plus className="h-4 w-4 mr-2" /> Add
</Button>
</div>
{selectedCat && ( <div className="border rounded-md">
<div className="space-y-4"> <div className="p-2 bg-muted/50 font-medium text-sm grid grid-cols-[1fr,auto] gap-2">
<div className="flex gap-2 items-end"> <span>Mapped Sub-Categories</span>
<div className="flex-1 space-y-2"> <span>Action</span>
<label className="text-sm font-medium">Add Sub-Category</label> </div>
<Select value={selectedSubCat} onValueChange={setSelectedSubCat}> {mappings.length === 0 ? (
<SelectTrigger> <div className="p-4 text-center text-sm text-muted-foreground">No sub-categories mapped yet.</div>
<SelectValue placeholder="Select Sub-Category to add..." /> ) : (
</SelectTrigger> <div className="divide-y">
<SelectContent> {mappings.map((m: { id: number; subCategory: ContractSubCategory }) => (
{subCategories <div key={m.id} className="p-2 grid grid-cols-[1fr,auto] gap-2 items-center">
.filter((s: ContractSubCategory) => !mappings.find((m: { subCategory: { id: number } }) => m.subCategory.id === s.id)) <span className="text-sm">
.map((s: ContractSubCategory) => ( {m.subCategory.subCatCode} - {m.subCategory.subCatName}
<SelectItem key={s.id} value={String(s.id)}>{s.subCatCode} - {s.subCatName}</SelectItem> </span>
))} <Button
</SelectContent> variant="ghost"
</Select> size="sm"
</div> onClick={() => deleteMutation.mutate(m.id)}
<Button onClick={handleAdd} disabled={!selectedSubCat || createMutation.isPending}> disabled={deleteMutation.isPending}
<Plus className="h-4 w-4 mr-2" /> Add >
<Trash2 className="h-4 w-4 text-red-500" />
</Button> </Button>
</div> </div>
))}
<div className="border rounded-md"> </div>
<div className="p-2 bg-muted/50 font-medium text-sm grid grid-cols-[1fr,auto] gap-2"> )}
<span>Mapped Sub-Categories</span> </div>
<span>Action</span> </div>
</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> </div>
) );
} }

View File

@@ -1,36 +1,36 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BookOpen, Tag, Settings, Layers } from "lucide-react"; import { BookOpen, Tag, Settings, Layers } from 'lucide-react';
import Link from "next/link"; import Link from 'next/link';
const refMenu = [ const refMenu = [
{ {
title: "Disciplines", title: 'Disciplines',
description: "Manage system-wide disciplines (e.g., ARCH, STR)", description: 'Manage system-wide disciplines (e.g., ARCH, STR)',
href: "/admin/reference/disciplines", href: '/admin/doc-control/reference/disciplines',
icon: Layers, icon: Layers,
}, },
{ {
title: "RFA Types", title: 'RFA Types',
description: "Manage RFA types and approve codes", description: 'Manage RFA types and approve codes',
href: "/admin/reference/rfa-types", href: '/admin/doc-control/reference/rfa-types',
icon: BookOpen, icon: BookOpen,
}, },
{ {
title: "Correspondence Types", title: 'Correspondence Types',
description: "Manage generic correspondence types", description: 'Manage generic correspondence types',
href: "/admin/reference/correspondence-types", href: '/admin/doc-control/reference/correspondence-types',
icon: Settings, icon: Settings,
}, },
{ {
title: "Tags", title: 'Tags',
description: "Manage system tags for documents", description: 'Manage system tags for documents',
href: "/admin/reference/tags", href: '/admin/doc-control/reference/tags',
icon: Tag, icon: Tag,
}, },
{ {
title: "Drawing Categories", title: 'Drawing Categories',
description: "Manage drawing sub-types and classifications", description: 'Manage drawing sub-types and classifications',
href: "/admin/reference/drawing-categories", href: '/admin/doc-control/reference/drawing-categories',
icon: Layers, icon: Layers,
}, },
]; ];
@@ -44,15 +44,11 @@ export default function ReferenceDataPage() {
<Link key={item.href} href={item.href}> <Link key={item.href} href={item.href}>
<Card className="hover:shadow-md transition-shadow cursor-pointer h-full"> <Card className="hover:shadow-md transition-shadow cursor-pointer h-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> <CardTitle className="text-sm font-medium">{item.title}</CardTitle>
{item.title}
</CardTitle>
<item.icon className="h-4 w-4 text-muted-foreground" /> <item.icon className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{item.description}</p>
{item.description}
</p>
</CardContent> </CardContent>
</Card> </Card>
</Link> </Link>

View File

@@ -7,6 +7,9 @@ import {
SearchOrganizationDto, SearchOrganizationDto,
} from '@/types/dto/organization/organization.dto'; } from '@/types/dto/organization/organization.dto';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { organizationService } from '@/lib/services/organization.service';
import { projectService } from '@/lib/services/project.service';
import { contractService } from '@/lib/services/contract.service';
export const masterDataKeys = { export const masterDataKeys = {
all: ['masterData'] as const, all: ['masterData'] as const,
@@ -15,8 +18,6 @@ export const masterDataKeys = {
disciplines: (contractId?: number) => [...masterDataKeys.all, 'disciplines', contractId] as const, disciplines: (contractId?: number) => [...masterDataKeys.all, 'disciplines', contractId] as const,
}; };
import { organizationService } from '@/lib/services/organization.service';
export function useOrganizations(params?: SearchOrganizationDto) { export function useOrganizations(params?: SearchOrganizationDto) {
return useQuery({ return useQuery({
queryKey: [...masterDataKeys.organizations(), params], queryKey: [...masterDataKeys.organizations(), params],
@@ -29,30 +30,31 @@ export function useCreateOrganization() {
return useMutation({ return useMutation({
mutationFn: (data: CreateOrganizationDto) => masterDataService.createOrganization(data), mutationFn: (data: CreateOrganizationDto) => masterDataService.createOrganization(data),
onSuccess: () => { onSuccess: () => {
toast.success("Organization created successfully"); toast.success('Organization created successfully');
queryClient.invalidateQueries({ queryKey: masterDataKeys.organizations() }); queryClient.invalidateQueries({ queryKey: masterDataKeys.organizations() });
}, },
onError: (error: AxiosError<{ message?: string }>) => { onError: (error: AxiosError<{ message?: string }>) => {
toast.error("Failed to create organization", { toast.error('Failed to create organization', {
description: error.response?.data?.message || "Unknown error" description: error.response?.data?.message || 'Unknown error',
}); });
} },
}); });
} }
export function useUpdateOrganization() { export function useUpdateOrganization() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ id, data }: { id: number; data: UpdateOrganizationDto }) => masterDataService.updateOrganization(id, data), mutationFn: ({ id, data }: { id: number; data: UpdateOrganizationDto }) =>
masterDataService.updateOrganization(id, data),
onSuccess: () => { onSuccess: () => {
toast.success("Organization updated successfully"); toast.success('Organization updated successfully');
queryClient.invalidateQueries({ queryKey: masterDataKeys.organizations() }); queryClient.invalidateQueries({ queryKey: masterDataKeys.organizations() });
}, },
onError: (error: AxiosError<{ message?: string }>) => { onError: (error: AxiosError<{ message?: string }>) => {
toast.error("Failed to update organization", { toast.error('Failed to update organization', {
description: error.response?.data?.message || "Unknown error" description: error.response?.data?.message || 'Unknown error',
}); });
} },
}); });
} }
@@ -61,14 +63,14 @@ export function useDeleteOrganization() {
return useMutation({ return useMutation({
mutationFn: (id: number) => masterDataService.deleteOrganization(id), mutationFn: (id: number) => masterDataService.deleteOrganization(id),
onSuccess: () => { onSuccess: () => {
toast.success("Organization deleted successfully"); toast.success('Organization deleted successfully');
queryClient.invalidateQueries({ queryKey: masterDataKeys.organizations() }); queryClient.invalidateQueries({ queryKey: masterDataKeys.organizations() });
}, },
onError: (error: AxiosError<{ message?: string }>) => { onError: (error: AxiosError<{ message?: string }>) => {
toast.error("Failed to delete organization", { toast.error('Failed to delete organization', {
description: error.response?.data?.message || "Unknown error" description: error.response?.data?.message || 'Unknown error',
}); });
} },
}); });
} }
@@ -79,9 +81,6 @@ export function useDisciplines(contractId?: number) {
}); });
} }
// Add useProjects hook
import { projectService } from '@/lib/services/project.service';
export function useProjects(isActive: boolean = true) { export function useProjects(isActive: boolean = true) {
return useQuery({ return useQuery({
queryKey: ['projects', { isActive }], queryKey: ['projects', { isActive }],
@@ -89,9 +88,6 @@ export function useProjects(isActive: boolean = true) {
}); });
} }
// Add useContracts hook
import { contractService } from '@/lib/services/contract.service';
export function useContracts(projectId: number = 1) { export function useContracts(projectId: number = 1) {
return useQuery({ return useQuery({
queryKey: ['contracts', projectId], queryKey: ['contracts', projectId],