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}
/> />
@@ -131,80 +124,75 @@ export default function ContractCategoriesPage() {
Given the constraints, I will add a "Mapped Sub-categories" management section 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. 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. 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: 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],