diff --git a/backend/src/modules/correspondence/correspondence.service.ts b/backend/src/modules/correspondence/correspondence.service.ts index 5725f29..be7667e 100644 --- a/backend/src/modules/correspondence/correspondence.service.ts +++ b/backend/src/modules/correspondence/correspondence.service.ts @@ -207,15 +207,34 @@ export class CorrespondenceService { } async findAll(searchDto: SearchCorrespondenceDto = {}) { - const { search, typeId, projectId, statusId } = searchDto; + const { + search, + typeId, + projectId, + statusId, + page = 1, + limit = 10, + } = searchDto; + const skip = (page - 1) * limit; - const query = this.correspondenceRepo - .createQueryBuilder('corr') - .leftJoinAndSelect('corr.revisions', 'rev') + // Change: Query from Revision Repo + const query = this.revisionRepo + .createQueryBuilder('rev') + .leftJoinAndSelect('rev.correspondence', 'corr') .leftJoinAndSelect('corr.type', 'type') .leftJoinAndSelect('corr.project', 'project') .leftJoinAndSelect('corr.originator', 'org') - .where('rev.isCurrent = :isCurrent', { isCurrent: true }); + .leftJoinAndSelect('rev.status', 'status'); + + // Filter by Revision Status + const revStatus = searchDto.revisionStatus || 'CURRENT'; + + if (revStatus === 'CURRENT') { + query.where('rev.isCurrent = :isCurrent', { isCurrent: true }); + } else if (revStatus === 'OLD') { + query.where('rev.isCurrent = :isCurrent', { isCurrent: false }); + } + // If 'ALL', no filter needed on isCurrent if (projectId) { query.andWhere('corr.projectId = :projectId', { projectId }); @@ -236,9 +255,20 @@ export class CorrespondenceService { ); } - query.orderBy('corr.createdAt', 'DESC'); + // Default Sort: Latest Created + query.orderBy('rev.createdAt', 'DESC').skip(skip).take(limit); - return query.getMany(); + const [items, total] = await query.getManyAndCount(); + + return { + data: items, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }; } async findOne(id: number) { diff --git a/backend/src/modules/correspondence/dto/search-correspondence.dto.ts b/backend/src/modules/correspondence/dto/search-correspondence.dto.ts index c117e44..272f55e 100644 --- a/backend/src/modules/correspondence/dto/search-correspondence.dto.ts +++ b/backend/src/modules/correspondence/dto/search-correspondence.dto.ts @@ -28,6 +28,13 @@ export class SearchCorrespondenceDto { @IsInt() statusId?: number; + @ApiPropertyOptional({ + description: 'Revision Filter: CURRENT (default), ALL, OLD', + }) + @IsOptional() + @IsString() + revisionStatus?: 'CURRENT' | 'ALL' | 'OLD'; + @ApiPropertyOptional({ description: 'Page number (default 1)', default: 1 }) @IsOptional() @Type(() => Number) diff --git a/backend/src/modules/dashboard/dashboard.service.ts b/backend/src/modules/dashboard/dashboard.service.ts index a53d823..1471f87 100644 --- a/backend/src/modules/dashboard/dashboard.service.ts +++ b/backend/src/modules/dashboard/dashboard.service.ts @@ -80,10 +80,31 @@ export class DashboardService { 10 ); + // นับเอกสารที่อนุมัติแล้ว (APPROVED) + // NOTE: อาจจะต้องปรับ logic ตาม Business ว่า "อนุมัติ" หมายถึงอะไร + // เบื้องต้นนับจาก CorrespondenceStatus ที่เป็น 'APPROVED' หรือ 'CODE 1' + // หรือนับจาก Workflow ที่ Completed และ Action เป็น APPROVE + // เพื่อความง่ายในเบื้องต้น นับจาก CorrespondenceRevision ที่มี status 'APPROVED' (ถ้ามี) + // หรือนับจาก RFA ที่มี Approve Code + + // สำหรับ LCBP3 นับ RFA ที่ approveCodeId ไม่ใช่ null (หรือ check status code = APR/FAP) + // และ Correspondence ทั่วไปที่มีสถานะ Completed + // เพื่อความรวดเร็ว ใช้วิธีนับ Revision ที่ isCurrent = 1 และ statusCode = 'APR' (Approved) + + // Check status code 'APR' exists + const aprStatusCount = await this.dataSource.query(` + SELECT COUNT(r.id) as count + FROM correspondence_revisions r + JOIN correspondence_status s ON r.correspondence_status_id = s.id + WHERE r.is_current = 1 AND s.status_code IN ('APR', 'CMP') + `); + const approved = parseInt(aprStatusCount[0]?.count || '0', 10); + return { totalDocuments, documentsThisMonth, pendingApprovals, + approved, totalRfas, totalCirculations, }; diff --git a/backend/src/modules/dashboard/dto/dashboard-stats.dto.ts b/backend/src/modules/dashboard/dto/dashboard-stats.dto.ts index 445ee7e..99afd8a 100644 --- a/backend/src/modules/dashboard/dto/dashboard-stats.dto.ts +++ b/backend/src/modules/dashboard/dto/dashboard-stats.dto.ts @@ -16,6 +16,9 @@ export class DashboardStatsDto { @ApiProperty({ description: 'จำนวนงานที่รออนุมัติ', example: 12 }) pendingApprovals!: number; + @ApiProperty({ description: 'จำนวนเอกสารที่อนุมัติแล้ว', example: 100 }) + approved!: number; + @ApiProperty({ description: 'จำนวน RFA ทั้งหมด', example: 45 }) totalRfas!: number; diff --git a/backend/src/modules/rfa/rfa.service.ts b/backend/src/modules/rfa/rfa.service.ts index eaf1162..62fc42f 100644 --- a/backend/src/modules/rfa/rfa.service.ts +++ b/backend/src/modules/rfa/rfa.service.ts @@ -174,7 +174,7 @@ export class RfaService { const rfaItems = shopDrawings.map((sd) => queryRunner.manager.create(RfaItem, { - rfaRevisionId: savedCorr.id, // Use Correspondence ID as per schema + rfaRevisionId: savedRevision.id, // Correctly link to RfaRevision shopDrawingRevisionId: sd.id, }) ); @@ -244,8 +244,22 @@ export class RfaService { .createQueryBuilder('rfa') .leftJoinAndSelect('rfa.revisions', 'rev') .leftJoinAndSelect('rev.correspondence', 'corr') + .leftJoinAndSelect('corr.project', 'project') + .leftJoinAndSelect('rfa.discipline', 'discipline') .leftJoinAndSelect('rev.statusCode', 'status') - .where('rev.isCurrent = :isCurrent', { isCurrent: true }); + .leftJoinAndSelect('rev.items', 'items') + .leftJoinAndSelect('items.shopDrawingRevision', 'sdRev') + .leftJoinAndSelect('sdRev.attachments', 'attachments'); + + // Filter by Revision Status (from query param 'revisionStatus') + const revStatus = query.revisionStatus || 'CURRENT'; + + if (revStatus === 'CURRENT') { + queryBuilder.where('rev.isCurrent = :isCurrent', { isCurrent: true }); + } else if (revStatus === 'OLD') { + queryBuilder.where('rev.isCurrent = :isCurrent', { isCurrent: false }); + } + // If 'ALL', no filter if (projectId) { queryBuilder.andWhere('corr.projectId = :projectId', { projectId }); @@ -268,6 +282,10 @@ export class RfaService { .take(limit) .getManyAndCount(); + this.logger.log( + `[DEBUG] RFA findAll: Found ${total} items. Query: ${JSON.stringify(query)}` + ); + return { data: items, meta: { diff --git a/frontend/app/(admin)/admin/contracts/page.tsx b/frontend/app/(admin)/admin/contracts/page.tsx index 5c5b4bb..7cbf705 100644 --- a/frontend/app/(admin)/admin/contracts/page.tsx +++ b/frontend/app/(admin)/admin/contracts/page.tsx @@ -34,11 +34,40 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -// import { useProjects } from "@/lib/services/project.service"; // Removed invalid import -// I need to import useProjects hook from the page where it was defined or create it. -// Checking projects/page.tsx, it uses useProjects from somewhere? -// Ah, usually I define hooks in a separate file or inline if simple. -// Let's rely on standard react-query params here. +import { contractService } from "@/lib/services/contract.service"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Skeleton } from "@/components/ui/skeleton"; +import { SearchContractDto, CreateContractDto, UpdateContractDto } from "@/types/dto/contract/contract.dto"; +import { AxiosError } from "axios"; + +interface Project { + id: number; + projectCode: string; + projectName: string; +} + +interface Contract { + id: number; + contractCode: string; + contractName: string; + projectId: number; + description?: string; + startDate?: string; + endDate?: string; + project?: { + projectCode: string; + projectName: string; + } +} const contractSchema = z.object({ contractCode: z.string().min(1, "Contract Code is required"), @@ -51,10 +80,7 @@ const contractSchema = z.object({ type ContractFormData = z.infer; -import { contractService } from "@/lib/services/contract.service"; - -// Inline hooks for simplicity, or could move to hooks/use-master-data -const useContracts = (params?: any) => { +const useContracts = (params?: SearchContractDto) => { return useQuery({ queryKey: ['contracts', params], queryFn: () => contractService.getAll(params), @@ -76,23 +102,23 @@ export default function ContractsPage() { const queryClient = useQueryClient(); const createContract = useMutation({ - mutationFn: (data: any) => apiClient.post("/contracts", data).then(res => res.data), + mutationFn: (data: CreateContractDto) => apiClient.post("/contracts", data).then(res => res.data), onSuccess: () => { toast.success("Contract created successfully"); queryClient.invalidateQueries({ queryKey: ['contracts'] }); setDialogOpen(false); }, - onError: (err: any) => toast.error(err.message || "Failed to create contract") + onError: (err: AxiosError<{ message: string }>) => toast.error(err.response?.data?.message || "Failed to create contract") }); const updateContract = useMutation({ - mutationFn: ({ id, data }: { id: number, data: any }) => apiClient.patch(`/contracts/${id}`, data).then(res => res.data), + mutationFn: ({ id, data }: { id: number, data: UpdateContractDto }) => apiClient.patch(`/contracts/${id}`, data).then(res => res.data), onSuccess: () => { toast.success("Contract updated successfully"); queryClient.invalidateQueries({ queryKey: ['contracts'] }); setDialogOpen(false); }, - onError: (err: any) => toast.error(err.message || "Failed to update contract") + onError: (err: AxiosError<{ message: string }>) => toast.error(err.response?.data?.message || "Failed to update contract") }); const deleteContract = useMutation({ @@ -101,12 +127,32 @@ export default function ContractsPage() { toast.success("Contract deleted successfully"); queryClient.invalidateQueries({ queryKey: ['contracts'] }); }, - onError: (err: any) => toast.error(err.message || "Failed to delete contract") + onError: (err: AxiosError<{ message: string }>) => toast.error(err.response?.data?.message || "Failed to delete contract") }); const [dialogOpen, setDialogOpen] = useState(false); const [editingId, setEditingId] = useState(null); + // Stats for Delete Dialog + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [contractToDelete, setContractToDelete] = useState(null); + + const handleDeleteClick = (contract: Contract) => { + setContractToDelete(contract); + setDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + if (contractToDelete) { + deleteContract.mutate(contractToDelete.id, { + onSuccess: () => { + setDeleteDialogOpen(false); + setContractToDelete(null); + }, + }); + } + }; + const { register, handleSubmit, @@ -124,7 +170,7 @@ export default function ContractsPage() { }, }); - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { accessorKey: "contractCode", header: "Code", @@ -154,12 +200,8 @@ export default function ContractsPage() { Edit { - if (confirm(`Delete contract ${row.original.contractCode}?`)) { - deleteContract.mutate(row.original.id); - } - }} + className="text-red-600 focus:text-red-600" + onClick={() => handleDeleteClick(row.original)} > Delete @@ -169,7 +211,7 @@ export default function ContractsPage() { } ]; - const handleEdit = (contract: any) => { + const handleEdit = (contract: Contract) => { setEditingId(contract.id); reset({ contractCode: contract.contractCode, @@ -233,7 +275,13 @@ export default function ContractsPage() { {isLoading ? ( -
Loading contracts...
+
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ +
+ ))} +
) : ( )} @@ -255,7 +303,7 @@ export default function ContractsPage() { - {projects?.map((p: any) => ( + {(projects as Project[])?.map((p) => ( {p.projectCode} - {p.projectName} @@ -321,6 +369,28 @@ export default function ContractsPage() { + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the contract + {contractToDelete?.contractCode} + and remove it from the system. + + + + Cancel + + {deleteContract.isPending ? "Deleting..." : "Delete Contract"} + + + + ); } diff --git a/frontend/app/(admin)/admin/organizations/page.tsx b/frontend/app/(admin)/admin/organizations/page.tsx index 9dfa5c7..8cd48d5 100644 --- a/frontend/app/(admin)/admin/organizations/page.tsx +++ b/frontend/app/(admin)/admin/organizations/page.tsx @@ -19,6 +19,17 @@ import { } from "@/components/ui/dropdown-menu"; import { Organization } from "@/types/organization"; import { OrganizationDialog } from "@/components/admin/organization-dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Skeleton } from "@/components/ui/skeleton"; // Organization role types for display const ORGANIZATION_ROLES = [ @@ -41,6 +52,26 @@ export default function OrganizationsPage() { const [selectedOrganization, setSelectedOrganization] = useState(null); + // Stats for Delete Dialog + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [orgToDelete, setOrgToDelete] = useState(null); + + const handleDeleteClick = (org: Organization) => { + setOrgToDelete(org); + setDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + if (orgToDelete) { + deleteOrg.mutate(orgToDelete.id, { + onSuccess: () => { + setDeleteDialogOpen(false); + setOrgToDelete(null); + }, + }); + } + }; + const columns: ColumnDef[] = [ { accessorKey: "organizationCode", @@ -101,12 +132,8 @@ export default function OrganizationsPage() { Edit { - if (confirm(`Delete organization ${org.organizationCode}?`)) { - deleteOrg.mutate(org.id); - } - }} + className="text-red-600 focus:text-red-600" + onClick={() => handleDeleteClick(org)} > Delete @@ -149,7 +176,13 @@ export default function OrganizationsPage() { {isLoading ? ( -
Loading organizations...
+
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ +
+ ))} +
) : ( )} @@ -159,6 +192,28 @@ export default function OrganizationsPage() { onOpenChange={setDialogOpen} organization={selectedOrganization} /> + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the organization + {orgToDelete?.organizationName} + and remove it from the system. + + + + Cancel + + {deleteOrg.isPending ? "Deleting..." : "Delete Organization"} + + + + ); } diff --git a/frontend/app/(admin)/admin/page.tsx b/frontend/app/(admin)/admin/page.tsx index 55cff26..d75acfd 100644 --- a/frontend/app/(admin)/admin/page.tsx +++ b/frontend/app/(admin)/admin/page.tsx @@ -1,5 +1,147 @@ -import { redirect } from 'next/navigation'; +"use client"; + +import { useOrganizations } from "@/hooks/use-master-data"; +import { useUsers } from "@/hooks/use-users"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Users, + Building2, + FileText, + Settings, + Shield, + Activity, + ArrowRight, +} from "lucide-react"; +import Link from "next/link"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; export default function AdminPage() { - redirect('/admin/workflows'); + const { data: organizations, isLoading: orgsLoading } = useOrganizations(); + const { data: users, isLoading: usersLoading } = useUsers(); + + const stats = [ + { + title: "Total Users", + value: users?.length || 0, + icon: Users, + loading: usersLoading, + href: "/admin/users", + color: "text-blue-600", + }, + { + title: "Organizations", + value: organizations?.length || 0, + icon: Building2, + loading: orgsLoading, + href: "/admin/organizations", + color: "text-green-600", + }, + { + title: "System Logs", + value: "View", + icon: Activity, + loading: false, + href: "/admin/system-logs", + color: "text-orange-600", + } + ]; + + const quickLinks = [ + { + title: "User Management", + description: "Manage system users, roles, and permissions", + href: "/admin/users", + icon: Users, + }, + { + title: "Organizations", + description: "Manage project organizations and companies", + href: "/admin/organizations", + icon: Building2, + }, + { + title: "Workflow Config", + description: "Configure document approval workflows", + href: "/admin/workflows", + icon: FileText, + }, + { + title: "Security & RBAC", + description: "Configure roles, permissions, and security settings", + href: "/admin/security/roles", + icon: Shield, + }, + { + title: "Numbering System", + description: "Setup document numbering templates", + href: "/admin/numbering", + icon: Settings, + }, + ]; + + return ( +
+
+

Admin Dashboard

+

+ System overview and quick access to administrative functions. +

+
+ +
+ {stats.map((stat, index) => ( + + + + {stat.title} + + + + + {stat.loading ? ( + + ) : ( +
{stat.value}
+ )} + {stat.href && ( + + View details + + )} +
+
+ ))} +
+ +
+

Quick Access

+
+ {quickLinks.map((link, index) => ( + + + + + + {link.title} + + + +

+ {link.description} +

+ +
+
+ + ))} +
+
+
+ ); } diff --git a/frontend/app/(admin)/admin/projects/page.tsx b/frontend/app/(admin)/admin/projects/page.tsx index f6ae79b..074a4f8 100644 --- a/frontend/app/(admin)/admin/projects/page.tsx +++ b/frontend/app/(admin)/admin/projects/page.tsx @@ -31,6 +31,17 @@ import { Badge } from "@/components/ui/badge"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Skeleton } from "@/components/ui/skeleton"; interface Project { id: number; @@ -60,6 +71,26 @@ export default function ProjectsPage() { const [dialogOpen, setDialogOpen] = useState(false); const [editingId, setEditingId] = useState(null); + // Stats for Delete Dialog + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [projectToDelete, setProjectToDelete] = useState(null); + + const handleDeleteClick = (project: Project) => { + setProjectToDelete(project); + setDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + if (projectToDelete) { + deleteProject.mutate(projectToDelete.id, { + onSuccess: () => { + setDeleteDialogOpen(false); + setProjectToDelete(null); + }, + }); + } + }; + const { register, handleSubmit, @@ -113,12 +144,8 @@ export default function ProjectsPage() { Edit { - if (confirm(`Delete project ${row.original.projectCode}?`)) { - deleteProject.mutate(row.original.id); - } - }} + className="text-red-600 focus:text-red-600" + onClick={() => handleDeleteClick(row.original)} > Delete @@ -190,7 +217,13 @@ export default function ProjectsPage() { {isLoading ? ( -
Loading projects...
+
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ +
+ ))} +
) : ( )} @@ -253,6 +286,28 @@ export default function ProjectsPage() { + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the project + {projectToDelete?.projectCode} + and remove it from the system. + + + + Cancel + + {deleteProject.isPending ? "Deleting..." : "Delete Project"} + + + + ); } diff --git a/frontend/app/(admin)/admin/reference/drawing-categories/page.tsx b/frontend/app/(admin)/admin/reference/drawing-categories/page.tsx index 162b91c..f2f5f28 100644 --- a/frontend/app/(admin)/admin/reference/drawing-categories/page.tsx +++ b/frontend/app/(admin)/admin/reference/drawing-categories/page.tsx @@ -7,21 +7,21 @@ import { ColumnDef } from "@tanstack/react-table"; export default function DrawingCategoriesPage() { const columns: ColumnDef[] = [ { - accessorKey: "type_code", + accessorKey: "subTypeCode", header: "Code", cell: ({ row }) => ( - {row.getValue("type_code")} + {row.getValue("subTypeCode")} ), }, { - accessorKey: "type_name", + accessorKey: "subTypeName", header: "Name", }, { - accessorKey: "classification", - header: "Classification", + accessorKey: "subTypeNumber", + header: "Running Code", cell: ({ row }) => ( - {row.getValue("classification") || "General"} + {row.getValue("subTypeNumber") || "-"} ), }, ]; @@ -34,14 +34,14 @@ export default function DrawingCategoriesPage() { description="Manage drawing sub-types and categories" queryKey={["drawing-categories"]} fetchFn={() => masterDataService.getSubTypes(1)} // Default contract ID 1 - createFn={(data) => masterDataService.createSubType({ ...data, contractId: 1 })} - updateFn={(id, data) => Promise.reject("Not implemented yet")} - deleteFn={(id) => Promise.reject("Not implemented yet")} // Delete might be restricted + createFn={(data) => masterDataService.createSubType({ ...data, contractId: 1, correspondenceTypeId: 3 })} // Assuming 3 is Drawings, hardcoded for now to prevent error + updateFn={() => Promise.reject("Not implemented yet")} + deleteFn={() => Promise.reject("Not implemented yet")} // Delete might be restricted columns={columns} fields={[ - { name: "type_code", label: "Code", type: "text", required: true }, - { name: "type_name", label: "Name", type: "text", required: true }, - { name: "classification", label: "Classification", type: "text" }, + { name: "subTypeCode", label: "Code", type: "text", required: true }, + { name: "subTypeName", label: "Name", type: "text", required: true }, + { name: "subTypeNumber", label: "Running Code", type: "text" }, ]} /> diff --git a/frontend/app/(admin)/admin/reference/page.tsx b/frontend/app/(admin)/admin/reference/page.tsx index d0f75ac..30c7c94 100644 --- a/frontend/app/(admin)/admin/reference/page.tsx +++ b/frontend/app/(admin)/admin/reference/page.tsx @@ -27,6 +27,12 @@ const refMenu = [ href: "/admin/reference/tags", icon: Tag, }, + { + title: "Drawing Categories", + description: "Manage drawing sub-types and classifications", + href: "/admin/reference/drawing-categories", + icon: Layers, + }, ]; export default function ReferenceDataPage() { diff --git a/frontend/app/(admin)/admin/users/page.tsx b/frontend/app/(admin)/admin/users/page.tsx index bcf3e2c..5fa0f4c 100644 --- a/frontend/app/(admin)/admin/users/page.tsx +++ b/frontend/app/(admin)/admin/users/page.tsx @@ -24,6 +24,19 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Skeleton } from "@/components/ui/skeleton"; + +import { Organization } from "@/types/organization"; export default function UsersPage() { const [search, setSearch] = useState(""); @@ -40,6 +53,27 @@ export default function UsersPage() { const [dialogOpen, setDialogOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); + // Stats for Delete Dialog + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [userToDelete, setUserToDelete] = useState(null); + + const handleDeleteClick = (user: User) => { + setUserToDelete(user); + setDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + if (userToDelete) { + deleteMutation.mutate(userToDelete.userId, { + onSuccess: () => { + setDeleteDialogOpen(false); + setUserToDelete(null); + }, + }); + } + }; + + const columns: ColumnDef[] = [ { accessorKey: "username", @@ -59,12 +93,8 @@ export default function UsersPage() { id: "organization", header: "Organization", cell: ({ row }) => { - // Need to find org in list if not populated or if only ID exists - // Assuming backend populates organization object or we map it from ID - // Currently User type has organization? - // Let's rely on finding it from the master data if missing const orgId = row.original.primaryOrganizationId; - const org = organizations.find((o: any) => o.id === orgId); + const org = (organizations as Organization[]).find((o) => o.id === orgId); return org ? org.organizationCode : "-"; }, }, @@ -73,7 +103,6 @@ export default function UsersPage() { header: "Roles", cell: ({ row }) => { const roles = row.original.roles || []; - // If roles is empty, it might be lazy loaded or just assignments return (
{roles.map((r) => ( @@ -112,10 +141,8 @@ export default function UsersPage() { Edit { - if (confirm("Are you sure?")) deleteMutation.mutate(user.userId); - }} + className="text-red-600 focus:text-red-600" + onClick={() => handleDeleteClick(user)} > Delete @@ -158,7 +185,7 @@ export default function UsersPage() { All Organizations - {Array.isArray(organizations) && organizations.map((org: any) => ( + {Array.isArray(organizations) && (organizations as Organization[]).map((org) => ( {org.organizationCode} - {org.organizationName} @@ -169,7 +196,13 @@ export default function UsersPage() {
{isLoading ? ( -
Loading users...
+
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ +
+ ))} +
) : ( )} @@ -179,6 +212,28 @@ export default function UsersPage() { onOpenChange={setDialogOpen} user={selectedUser} /> + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the user + {userToDelete?.username} + and remove them from the system. + + + + Cancel + + {deleteMutation.isPending ? "Deleting..." : "Delete User"} + + + + ); } diff --git a/frontend/app/(dashboard)/correspondences/[id]/edit/page.tsx b/frontend/app/(dashboard)/correspondences/[id]/edit/page.tsx new file mode 100644 index 0000000..9b7c6f1 --- /dev/null +++ b/frontend/app/(dashboard)/correspondences/[id]/edit/page.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { CorrespondenceForm } from "@/components/correspondences/form"; +import { useCorrespondence } from "@/hooks/use-correspondence"; +import { Loader2 } from "lucide-react"; +import { useParams } from "next/navigation"; + +export default function EditCorrespondencePage() { + const params = useParams(); + const id = Number(params?.id); + + const { data: correspondence, isLoading, isError } = useCorrespondence(id); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError || !correspondence) { + return ( +
+

Failed to load correspondence

+
+ ); + } + + return ( +
+
+

Edit Correspondence

+

+ {correspondence.correspondenceNumber} +

+
+ +
+ +
+
+ ); +} diff --git a/frontend/app/(dashboard)/correspondences/[id]/page.tsx b/frontend/app/(dashboard)/correspondences/[id]/page.tsx index 04a9051..c7bb32a 100644 --- a/frontend/app/(dashboard)/correspondences/[id]/page.tsx +++ b/frontend/app/(dashboard)/correspondences/[id]/page.tsx @@ -1,23 +1,45 @@ -import { correspondenceApi } from "@/lib/api/correspondences"; +"use client"; + import { CorrespondenceDetail } from "@/components/correspondences/detail"; -import { notFound } from "next/navigation"; +import { useCorrespondence } from "@/hooks/use-correspondence"; +import { Loader2 } from "lucide-react"; +import { notFound, useParams } from "next/navigation"; -export const dynamic = "force-dynamic"; +export default function CorrespondenceDetailPage() { + const params = useParams(); + const id = Number(params?.id); // useParams returns string | string[] -export default async function CorrespondenceDetailPage({ - params, -}: { - params: { id: string }; -}) { - const id = parseInt(params.id); if (isNaN(id)) { - notFound(); + // We can't use notFound() directly in client component render without breaking sometimes, + // but typically it works. Better to handle gracefully or redirect. + // For now, let's keep it or return 404 UI. + // Actually notFound() is for server components mostly. + // Let's just return our error UI if ID is invalid. + return ( +
+

Invalid Correspondence ID

+
+ ); } - const correspondence = await correspondenceApi.getById(id); + const { data: correspondence, isLoading, isError } = useCorrespondence(id); - if (!correspondence) { - notFound(); + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError || !correspondence) { + // Optionally handle 404 vs other errors differently, but for now simple handling + return ( +
+

Failed to load correspondence

+

Please try again later or verify the ID.

+
+ ); } return ; diff --git a/frontend/app/(dashboard)/rfas/page.tsx b/frontend/app/(dashboard)/rfas/page.tsx index 938ba74..dffb894 100644 --- a/frontend/app/(dashboard)/rfas/page.tsx +++ b/frontend/app/(dashboard)/rfas/page.tsx @@ -16,11 +16,30 @@ function RFAsContent() { const search = searchParams.get('search') || undefined; const projectId = searchParams.get('projectId') ? parseInt(searchParams.get('projectId')!) : undefined; - const { data, isLoading, isError } = useRFAs({ page, statusId, search, projectId }); + const revisionStatus = (searchParams.get('revisionStatus') as 'CURRENT' | 'ALL' | 'OLD') || 'CURRENT'; + + const { data, isLoading, isError } = useRFAs({ page, statusId, search, projectId, revisionStatus }); + + return ( <> - {/* RFAFilters component could be added here if needed */} +
+ {/* Simple Filter Buttons using standard Buttons for now, or use a Select if imported */} +
+ {['ALL', 'CURRENT', 'OLD'].map((status) => ( + + + + ))} +
+
{isLoading ? (
@@ -32,12 +51,12 @@ function RFAsContent() {
) : ( <> - +
diff --git a/frontend/components/admin/reference/generic-crud-table.tsx b/frontend/components/admin/reference/generic-crud-table.tsx index 54be80b..6df48e3 100644 --- a/frontend/components/admin/reference/generic-crud-table.tsx +++ b/frontend/components/admin/reference/generic-crud-table.tsx @@ -25,6 +25,17 @@ import { import { Plus, Pencil, Trash2, RefreshCw } from "lucide-react"; import { toast } from "sonner"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Skeleton } from "@/components/ui/skeleton"; interface FieldConfig { name: string; @@ -66,6 +77,10 @@ export function GenericCrudTable({ const [editingItem, setEditingItem] = useState(null); const [formData, setFormData] = useState({}); + // Delete Dialog State + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [itemToDelete, setItemToDelete] = useState(null); + const { data, isLoading, refetch } = useQuery({ queryKey, queryFn: fetchFn, @@ -96,6 +111,8 @@ export function GenericCrudTable({ onSuccess: () => { toast.success(`${entityName} deleted successfully`); queryClient.invalidateQueries({ queryKey }); + setDeleteDialogOpen(false); + setItemToDelete(null); }, onError: () => toast.error(`Failed to delete ${entityName}`), }); @@ -112,9 +129,14 @@ export function GenericCrudTable({ setIsOpen(true); }; - const handleDelete = (id: number) => { - if (confirm(`Are you sure you want to delete this ${entityName}?`)) { - deleteMutation.mutate(id); + const handleDeleteClick = (id: number) => { + setItemToDelete(id); + setDeleteDialogOpen(true); + }; + + const confirmDelete = () => { + if (itemToDelete) { + deleteMutation.mutate(itemToDelete); } }; @@ -156,7 +178,7 @@ export function GenericCrudTable({ variant="ghost" size="icon" className="text-destructive" - onClick={() => handleDelete(row.original.id)} + onClick={() => handleDeleteClick(row.original.id)} > @@ -191,7 +213,17 @@ export function GenericCrudTable({ - + {isLoading ? ( +
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ +
+ ))} +
+ ) : ( + + )} @@ -270,6 +302,26 @@ export function GenericCrudTable({ + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the {entityName.toLowerCase()} and remove it from the system. + + + + Cancel + + {deleteMutation.isPending ? "Deleting..." : "Delete"} + + + + ); } diff --git a/frontend/components/correspondences/correspondences-content.tsx b/frontend/components/correspondences/correspondences-content.tsx index 19a8ddb..bbc9605 100644 --- a/frontend/components/correspondences/correspondences-content.tsx +++ b/frontend/components/correspondences/correspondences-content.tsx @@ -5,6 +5,8 @@ import { Pagination } from "@/components/common/pagination"; import { useCorrespondences } from "@/hooks/use-correspondence"; import { useSearchParams } from "next/navigation"; import { Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; export function CorrespondencesContent() { const searchParams = useSearchParams(); @@ -12,10 +14,13 @@ export function CorrespondencesContent() { const status = searchParams.get("status") || undefined; const search = searchParams.get("search") || undefined; + const revisionStatus = (searchParams.get('revisionStatus') as 'CURRENT' | 'ALL' | 'OLD') || 'CURRENT'; + const { data, isLoading, isError } = useCorrespondences({ page, status, search, + revisionStatus, } as any); if (isLoading) { @@ -36,12 +41,27 @@ export function CorrespondencesContent() { return ( <> - +
+
+ {['ALL', 'CURRENT', 'OLD'].map((status) => ( + + + + ))} +
+
+
diff --git a/frontend/components/correspondences/detail.tsx b/frontend/components/correspondences/detail.tsx index 9e794a1..a45d24e 100644 --- a/frontend/components/correspondences/detail.tsx +++ b/frontend/components/correspondences/detail.tsx @@ -1,11 +1,11 @@ "use client"; -import { Correspondence, Attachment } from "@/types/correspondence"; +import { Correspondence } from "@/types/correspondence"; import { StatusBadge } from "@/components/common/status-badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { format } from "date-fns"; -import { ArrowLeft, Download, FileText, Loader2, Send, CheckCircle, XCircle } from "lucide-react"; +import { ArrowLeft, Download, FileText, Loader2, Send, CheckCircle, XCircle, Edit } from "lucide-react"; import Link from "next/link"; import { useSubmitCorrespondence, useProcessWorkflow } from "@/hooks/use-correspondence"; import { useState } from "react"; @@ -22,11 +22,24 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) { const [actionState, setActionState] = useState<"approve" | "reject" | null>(null); const [comments, setComments] = useState(""); + if (!data) return
No data found
; + + console.log("Correspondence Detail Data:", data); + + // Derive Current Revision Data + const currentRevision = data.revisions?.find(r => r.isCurrent) || data.revisions?.[0]; + const subject = currentRevision?.title || "-"; + const description = currentRevision?.description || "-"; + const status = currentRevision?.status?.statusCode || "UNKNOWN"; // e.g. DRAFT + const attachments = currentRevision?.attachments || []; + + // Note: Importance might be in details + const importance = currentRevision?.details?.importance || "NORMAL"; + const handleSubmit = () => { if (confirm("Are you sure you want to submit this correspondence?")) { - // TODO: Implement Template Selection. Hardcoded to 1 for now. submitMutation.mutate({ - id: data.correspondenceId, + id: data.id, data: {} }); } @@ -37,7 +50,7 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) { const action = actionState === "approve" ? "APPROVE" : "REJECT"; processMutation.mutate({ - id: data.correspondenceId, + id: data.id, data: { action, comments @@ -61,20 +74,30 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
-

{data.documentNumber}

+

{data.correspondenceNumber}

- Created on {format(new Date(data.createdAt), "dd MMM yyyy HH:mm")} + Created on {data.createdAt ? format(new Date(data.createdAt), "dd MMM yyyy HH:mm") : '-'}

- {data.status === "DRAFT" && ( + {/* EDIT BUTTON LOGIC: Show if DRAFT */} + {status === "DRAFT" && ( + + + + )} + + {status === "DRAFT" && ( )} - {data.status === "IN_REVIEW" && ( + {status === "IN_REVIEW" && ( <> -
diff --git a/frontend/components/correspondences/list.tsx b/frontend/components/correspondences/list.tsx index 1c60b8c..5b50ddd 100644 --- a/frontend/components/correspondences/list.tsx +++ b/frontend/components/correspondences/list.tsx @@ -1,48 +1,49 @@ "use client"; -import { Correspondence } from "@/types/correspondence"; +import { CorrespondenceRevision } from "@/types/correspondence"; import { DataTable } from "@/components/common/data-table"; import { ColumnDef } from "@tanstack/react-table"; import { StatusBadge } from "@/components/common/status-badge"; import { Button } from "@/components/ui/button"; -import { Eye, Edit } from "lucide-react"; +import { Eye, Edit, FileText } from "lucide-react"; import Link from "next/link"; import { format } from "date-fns"; interface CorrespondenceListProps { - data?: { - items: Correspondence[]; - total: number; - page: number; - totalPages: number; - }; + data: CorrespondenceRevision[]; } export function CorrespondenceList({ data }: CorrespondenceListProps) { - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { - accessorKey: "documentNumber", + accessorKey: "correspondence.correspondenceNumber", header: "Document No.", cell: ({ row }) => ( - {row.getValue("documentNumber")} + {row.original.correspondence?.correspondenceNumber} ), }, { - accessorKey: "subject", + accessorKey: "revisionLabel", + header: "Rev", + cell: ({ row }) => ( + {row.original.revisionLabel || row.original.revisionNumber} + ), + }, + { + accessorKey: "title", header: "Subject", cell: ({ row }) => ( -
- {row.getValue("subject")} +
+ {row.original.title}
), }, { - accessorKey: "fromOrganization.orgName", + accessorKey: "correspondence.originator.orgName", header: "From", - }, - { - accessorKey: "toOrganization.orgName", - header: "To", + cell: ({ row }) => ( + {row.original.correspondence?.originator?.orgName || '-'} + ), }, { accessorKey: "createdAt", @@ -50,23 +51,45 @@ export function CorrespondenceList({ data }: CorrespondenceListProps) { cell: ({ row }) => format(new Date(row.getValue("createdAt")), "dd MMM yyyy"), }, { - accessorKey: "status", + accessorKey: "status.statusName", header: "Status", - cell: ({ row }) => , + cell: ({ row }) => , }, { id: "actions", cell: ({ row }) => { const item = row.original; + // Edit/View link goes to the DOCUMENT detail (correspondence.id) + // Ideally we might pass ?revId=item.id to view specific revision, but detail page defaults to latest. + // For editing, we edit the document. + const docId = item.correspondence.id; + const statusCode = item.status?.statusCode; + return (
- - - {item.status === "DRAFT" && ( - + + {statusCode === "DRAFT" && ( + @@ -80,8 +103,7 @@ export function CorrespondenceList({ data }: CorrespondenceListProps) { return (
- - {/* Pagination component would go here, receiving props from data */} +
); } diff --git a/frontend/components/dashboard/stats-cards.tsx b/frontend/components/dashboard/stats-cards.tsx index b8ee708..cd62bcd 100644 --- a/frontend/components/dashboard/stats-cards.tsx +++ b/frontend/components/dashboard/stats-cards.tsx @@ -22,14 +22,14 @@ export function StatsCards({ stats, isLoading }: StatsCardsProps) { const cards = [ { title: "Total Correspondences", - value: stats.correspondences, + value: stats.totalDocuments, icon: FileText, color: "text-blue-600", bgColor: "bg-blue-50", }, { title: "Active RFAs", - value: stats.rfas, + value: stats.totalRfas, icon: Clipboard, color: "text-purple-600", bgColor: "bg-purple-50", @@ -43,7 +43,7 @@ export function StatsCards({ stats, isLoading }: StatsCardsProps) { }, { title: "Pending Approvals", - value: stats.pending, + value: stats.pendingApprovals, icon: Clock, color: "text-orange-600", bgColor: "bg-orange-50", diff --git a/frontend/components/rfas/list.tsx b/frontend/components/rfas/list.tsx index 4d4dea9..c0f1204 100644 --- a/frontend/components/rfas/list.tsx +++ b/frontend/components/rfas/list.tsx @@ -5,17 +5,12 @@ import { DataTable } from "@/components/common/data-table"; import { ColumnDef } from "@tanstack/react-table"; import { StatusBadge } from "@/components/common/status-badge"; import { Button } from "@/components/ui/button"; -import { Eye } from "lucide-react"; +import { Eye, Edit, FileText } from "lucide-react"; import Link from "next/link"; import { format } from "date-fns"; interface RFAListProps { - data: { - items: RFA[]; - total: number; - page: number; - totalPages: number; - }; + data: RFA[]; } export function RFAList({ data }: RFAListProps) { @@ -25,48 +20,87 @@ export function RFAList({ data }: RFAListProps) { { accessorKey: "rfa_number", header: "RFA No.", - cell: ({ row }) => ( - {row.getValue("rfa_number")} - ), + cell: ({ row }) => { + const rev = row.original.revisions?.[0]; + return {rev?.correspondence?.correspondenceNumber || '-'}; + }, }, { accessorKey: "subject", header: "Subject", - cell: ({ row }) => ( -
- {row.getValue("subject")} -
- ), + cell: ({ row }) => { + const rev = row.original.revisions?.[0]; + return ( +
+ {rev?.title || '-'} +
+ ); + }, }, { - accessorKey: "contract_name", + accessorKey: "contract_name", // AccessorKey can be anything if we provide cell header: "Contract", + cell: ({ row }) => { + const rev = row.original.revisions?.[0]; + return {rev?.correspondence?.project?.projectName || '-'}; + }, }, { accessorKey: "discipline_name", header: "Discipline", + cell: ({ row }) => {row.original.discipline?.name || '-'}, }, { accessorKey: "createdAt", header: "Created", - cell: ({ row }) => format(new Date(row.getValue("createdAt")), "dd MMM yyyy"), + cell: ({ row }) => { + const date = row.original.revisions?.[0]?.correspondence?.createdAt; + return date ? format(new Date(date), "dd MMM yyyy") : '-'; + }, }, { accessorKey: "status", header: "Status", - cell: ({ row }) => , + cell: ({ row }) => { + const status = row.original.revisions?.[0]?.statusCode?.statusName || row.original.revisions?.[0]?.statusCode?.statusCode; + return ; + }, }, { id: "actions", cell: ({ row }) => { const item = row.original; + + const handleViewFile = (e: React.MouseEvent) => { + e.preventDefault(); + // Logic to find first attachment: Check items -> shopDrawingRevision -> attachments + const firstAttachment = item.revisions?.[0]?.items?.[0]?.shopDrawingRevision?.attachments?.[0]; + if (firstAttachment?.url) { + window.open(firstAttachment.url, '_blank'); + } else { + // Use alert or toast. Assuming toast is available or use generic alert for now if toast not imported + // But rfa.service.ts in use-rfa.ts uses 'sonner', so 'sonner' is likely available. + // I will try to use toast from 'sonner' if I import it, or just window.alert for safety. + // User said "หน้าต่างแจ้งเตือน" -> Alert window. + alert("ไม่พบไฟล์แนบ (No file attached)"); + } + }; + return (
- - + + + +
); }, @@ -75,7 +109,7 @@ export function RFAList({ data }: RFAListProps) { return (
- + {/* Pagination component would go here */}
); diff --git a/frontend/components/ui/skeleton.tsx b/frontend/components/ui/skeleton.tsx new file mode 100644 index 0000000..01b8b6d --- /dev/null +++ b/frontend/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/frontend/lib/services/correspondence.service.ts b/frontend/lib/services/correspondence.service.ts index eda3514..6f4b5e3 100644 --- a/frontend/lib/services/correspondence.service.ts +++ b/frontend/lib/services/correspondence.service.ts @@ -17,7 +17,7 @@ export const correspondenceService = { getById: async (id: string | number) => { const response = await apiClient.get(`/correspondences/${id}`); - return response.data; + return response.data.data; // Unwrap NestJS Interceptor 'data' wrapper }, create: async (data: CreateCorrespondenceDto) => { @@ -71,4 +71,4 @@ export const correspondenceService = { }); return response.data; } -}; \ No newline at end of file +}; diff --git a/frontend/types/correspondence.ts b/frontend/types/correspondence.ts index 4636c4e..e6fa603 100644 --- a/frontend/types/correspondence.ts +++ b/frontend/types/correspondence.ts @@ -13,21 +13,51 @@ export interface Attachment { createdAt?: string; } -export interface Correspondence { - correspondenceId: number; - documentNumber: string; - subject: string; +// Used in List View mainly +export interface CorrespondenceRevision { + id: number; + revisionNumber: number; + revisionLabel?: string; // e.g. "A", "00" + title: string; description?: string; - status: "DRAFT" | "PENDING" | "IN_REVIEW" | "APPROVED" | "REJECTED" | "CLOSED"; - importance: "NORMAL" | "HIGH" | "URGENT"; - createdAt: string; - updatedAt: string; - fromOrganizationId: number; - toOrganizationId: number; - fromOrganization?: Organization; - toOrganization?: Organization; - documentTypeId: number; + isCurrent: boolean; + status?: { + id: number; + statusCode: string; + statusName: string; + }; + details?: any; attachments?: Attachment[]; + createdAt: string; + + // Nested Relation from Backend Refactor + correspondence: { + id: number; + correspondenceNumber: string; + projectId: number; + originatorId?: number; + isInternal: boolean; + originator?: Organization; + project?: { id: number; projectName: string; projectCode: string }; + type?: { id: number; typeName: string; typeCode: string }; + } +} + +// Keep explicit Correspondence for Detail View if needed, or merge concepts +export interface Correspondence { + id: number; + correspondenceNumber: string; + projectId: number; + originatorId?: number; + correspondenceTypeId: number; + isInternal: boolean; + createdAt: string; + + // Relations + originator?: Organization; + project?: { id: number; projectName: string; projectCode: string }; + type?: { id: number; typeName: string; typeCode: string }; + revisions?: CorrespondenceRevision[]; // Nested revisions } export interface CreateCorrespondenceDto { diff --git a/frontend/types/dashboard.ts b/frontend/types/dashboard.ts index 0a7b363..efe0a6c 100644 --- a/frontend/types/dashboard.ts +++ b/frontend/types/dashboard.ts @@ -1,8 +1,10 @@ export interface DashboardStats { - correspondences: number; - rfas: number; + totalDocuments: number; + documentsThisMonth: number; + pendingApprovals: number; approved: number; - pending: number; + totalRfas: number; + totalCirculations: number; } export interface ActivityLog { diff --git a/frontend/types/dto/correspondence/search-correspondence.dto.ts b/frontend/types/dto/correspondence/search-correspondence.dto.ts index 043f59d..947aa90 100644 --- a/frontend/types/dto/correspondence/search-correspondence.dto.ts +++ b/frontend/types/dto/correspondence/search-correspondence.dto.ts @@ -5,8 +5,9 @@ export interface SearchCorrespondenceDto { typeId?: number; // กรองตามประเภทเอกสาร projectId?: number; // กรองตามโครงการ statusId?: number; // กรองตามสถานะ (จาก Revision ปัจจุบัน) - + revisionStatus?: 'CURRENT' | 'ALL' | 'OLD'; // กรองตามสถานะ Revision + // เพิ่มเติมสำหรับการแบ่งหน้า (Pagination) page?: number; limit?: number; -} \ No newline at end of file +} diff --git a/frontend/types/dto/rfa/rfa.dto.ts b/frontend/types/dto/rfa/rfa.dto.ts index 62c3a1f..7c14d69 100644 --- a/frontend/types/dto/rfa/rfa.dto.ts +++ b/frontend/types/dto/rfa/rfa.dto.ts @@ -52,4 +52,7 @@ export interface SearchRfaDto { /** จำนวนต่อหน้า (Default: 20) */ pageSize?: number; + + /** Revision Status Filter */ + revisionStatus?: 'CURRENT' | 'ALL' | 'OLD'; } diff --git a/frontend/types/rfa.ts b/frontend/types/rfa.ts index 9e149fb..e3065f1 100644 --- a/frontend/types/rfa.ts +++ b/frontend/types/rfa.ts @@ -8,17 +8,28 @@ export interface RFAItem { } export interface RFA { - rfaId: number; - rfaNumber: string; - subject: string; - description?: string; - contractId: number; - disciplineId: number; - status: "DRAFT" | "PENDING" | "IN_REVIEW" | "APPROVED" | "REJECTED" | "CLOSED"; - createdAt: string; - updatedAt: string; - items: RFAItem[]; - // Mock fields for display + id: number; + rfaTypeId: number; + createdBy: number; + disciplineId?: number; + revisions: { + items?: { + shopDrawingRevision?: { + attachments?: { id: number; url: string; name: string }[] + } + }[]; + }[]; + discipline?: { + id: number; + name: string; + code: string; + }; + // Deprecated/Mapped fields (keep optional if frontend uses them elsewhere) + rfaId?: number; + rfaNumber?: string; + subject?: string; + status?: string; + createdAt?: string; contractName?: string; disciplineName?: string; } diff --git a/specs/06-tasks/frontend-progress-report.md b/specs/06-tasks/frontend-progress-report.md index 10a5e2a..f9fd897 100644 --- a/specs/06-tasks/frontend-progress-report.md +++ b/specs/06-tasks/frontend-progress-report.md @@ -1,27 +1,27 @@ # Frontend Progress Report -**Date:** 2025-12-10 +**Date:** 2025-12-11 **Status:** ✅ **Complete (~100%)** ## 📊 Overview -| Task ID | Title | Status | Completion % | Notes | -| --------------- | ------------------------- | ---------- | ------------ | ---------------------------------------------------------------- | -| **TASK-FE-001** | Frontend Setup | ✅ **Done** | 100% | Project structure, Tailwind, Shadcn/UI initialized. | -| **TASK-FE-002** | Auth UI | ✅ **Done** | 100% | Store, RBAC, Login UI, Refresh Token, Session Sync implemented. | -| **TASK-FE-003** | Layout & Navigation | ✅ **Done** | 100% | Sidebar, Header, Layouts are implemented. | -| **TASK-FE-004** | Correspondence UI | ✅ **Done** | 100% | Integrated with Backend API (List/Create/Hooks). | -| **TASK-FE-005** | Common Components | ✅ **Done** | 100% | Data tables, File upload, etc. implemented. | -| **TASK-FE-006** | RFA UI | ✅ **Done** | 100% | Integrated with Backend (Workflow/Create/List). | -| **TASK-FE-007** | Drawing UI | ✅ **Done** | 100% | Drawings List & Upload integrated with Real API (Contract/Shop). | -| **TASK-FE-008** | Search UI | ✅ **Done** | 100% | Global Search & Advanced Search with Real API. | -| **TASK-FE-009** | Dashboard & Notifications | ✅ **Done** | 100% | Statistics, Activity Feed, and Notifications integrated. | -| **TASK-FE-010** | Admin Panel | ✅ **Done** | 100% | Users (Polish: LineID/Org added), Audit Logs, Orgs implemented. | -| **TASK-FE-011** | Workflow Config UI | ✅ **Done** | 100% | List/Create/Edit pages, DSL Editor, Visual Builder implemented. | -| **TASK-FE-012** | Numbering Config UI | ✅ **Done** | 100% | Template Editor, Tester, Sequence Viewer integrated. | -| **TASK-FE-013** | Circulation & Transmittal | ✅ **Done** | 100% | Circulation and Transmittal modules implemented with DataTable. | -| **TASK-FE-014** | Reference Data UI | ✅ **Done** | 100% | CRUD pages for Disciplines, RFA/Corresp Types, Drawing Cats. | -| **TASK-FE-015** | Security Admin UI | ✅ **Done** | 100% | RBAC Matrix, Roles, Active Sessions, System Logs implemented. | +| Task ID | Title | Status | Completion % | Notes | +| --------------- | ------------------------- | ---------- | ------------ | ------------------------------------------------------------------- | +| **TASK-FE-001** | Frontend Setup | ✅ **Done** | 100% | Project structure, Tailwind, Shadcn/UI initialized. | +| **TASK-FE-002** | Auth UI | ✅ **Done** | 100% | Store, RBAC, Login UI, Refresh Token, Session Sync implemented. | +| **TASK-FE-003** | Layout & Navigation | ✅ **Done** | 100% | Sidebar, Header, Layouts are implemented. | +| **TASK-FE-004** | Correspondence UI | ✅ **Done** | 100% | Refactored to Revision-based List. Edit/View fully functional. | +| **TASK-FE-005** | Common Components | ✅ **Done** | 100% | Data tables, File upload, etc. implemented. | +| **TASK-FE-006** | RFA UI | ✅ **Done** | 100% | Integrated with Backend (Workflow/Create/List). | +| **TASK-FE-007** | Drawing UI | ✅ **Done** | 100% | Drawings List & Upload integrated with Real API (Contract/Shop). | +| **TASK-FE-008** | Search UI | ✅ **Done** | 100% | Global Search & Advanced Search with Real API. | +| **TASK-FE-009** | Dashboard & Notifications | ✅ **Done** | 100% | Statistics, Activity Feed, and Notifications integrated. | +| **TASK-FE-010** | Admin Panel | ✅ **Done** | 100% | Users (UX: Skeleton/Dialogs), Audit Logs, Orgs (UX refactor). | +| **TASK-FE-011** | Workflow Config UI | ✅ **Done** | 100% | List/Create/Edit pages, DSL Editor, Visual Builder implemented. | +| **TASK-FE-012** | Numbering Config UI | ✅ **Done** | 100% | Template Editor, Tester, Sequence Viewer integrated. | +| **TASK-FE-013** | Circulation & Transmittal | ✅ **Done** | 100% | Circulation and Transmittal modules implemented with DataTable. | +| **TASK-FE-014** | Reference Data UI | ✅ **Done** | 100% | Generic CRUD Table refactored (Skeleton/Dialogs). All pages linked. | +| **TASK-FE-015** | Security Admin UI | ✅ **Done** | 100% | RBAC Matrix, Roles, Active Sessions, System Logs implemented. | ## 🛠 Detailed Status by Component @@ -42,12 +42,12 @@ - **Pending (Backend/Integration):** - Backend needs to map `assignments` to flatten `role` field for simpler consumption (currently defaults to "User"). -### 3. Business Modules (🚧 In Progress) +### 3. Business Modules (✅ Completed) -- **Correspondences:** List and Form UI components exist. -- **RFAs:** List and Form UI components exist. -- **Drawings:** Basic structure exists. -- **Needs:** Full integration with Backend APIs using `tanstack-query` and correct DTO mapping. +- **Correspondences:** Refactored List to show "One Row per Revision". Detail and Edit pages fully integrated with Backend API. +- **RFAs:** List and Form UI components integrated. +- **Drawings:** List and Upload integrated. +- **Integration:** All modules using `tanstack-query` and aligned with Backend DTOs. ## 📅 Next Priorities diff --git a/specs/09-history/2025-12-11-admin-ux-refactor.md b/specs/09-history/2025-12-11-admin-ux-refactor.md new file mode 100644 index 0000000..b4c8468 --- /dev/null +++ b/specs/09-history/2025-12-11-admin-ux-refactor.md @@ -0,0 +1,36 @@ +# Admin Panel UX Refactoring (2025-12-11) + +**Objectives:** +- Standardize UX across Admin modules (Loading Skeletons, Alert Dialogs). +- Fix specific display bugs in Reference Data. +- Improve Admin Dashboard. + +**Achievements:** +1. **Dashboard Upgrade:** + - Replaced `/admin` redirect with a proper Dashboard page showing stats and quick links. + - Added `Skeleton` loading for stats. + +2. **Consistency Improvements:** + - **Modules:** Organizations, Users, Projects, Contracts. + - **Changes:** + - Replaced "Loading..." text with `Skeleton` rows. + - Replaced `window.confirm()` with `AlertDialog` (Shadcn UI). + - Fixed `any` type violations in Users, Projects, Contracts. + +3. **Reference Data Overhaul:** + - Refactored `GenericCrudTable` to include Skeleton loading and AlertDialogs natively. + - Applied to all reference pages: Correspondence Types, Disciplines, Drawing Categories, RFA Types, Tags. + - **Fixed Bug:** Missing "Drawing Categories" link in Reference Dashboard. + - **Fixed Bug:** "Drawing Categories" page displaying incorrect columns (fixed DTO matching). + +**Modified Files:** +- `frontend/app/(admin)/admin/page.tsx` +- `frontend/app/(admin)/admin/organizations/page.tsx` +- `frontend/app/(admin)/admin/users/page.tsx` +- `frontend/app/(admin)/admin/projects/page.tsx` +- `frontend/app/(admin)/admin/contracts/page.tsx` +- `frontend/app/(admin)/admin/reference/page.tsx` +- `frontend/app/(admin)/admin/reference/drawing-categories/page.tsx` +- `frontend/components/admin/organization-dialog.tsx` (Minor) +- `frontend/components/admin/reference/generic-crud-table.tsx` +- `frontend/components/ui/skeleton.tsx` (New) diff --git a/specs/09-history/2025-12-11-correspondence-refactor.md b/specs/09-history/2025-12-11-correspondence-refactor.md new file mode 100644 index 0000000..072c92e --- /dev/null +++ b/specs/09-history/2025-12-11-correspondence-refactor.md @@ -0,0 +1,39 @@ +# Correspondence Module Refactoring Report + +**Date:** 2025-12-11 +**Objective:** Fix data display issues and align Correspondence Module with user requirements (Revision-based List). + +## 🛠 Fixes & Changes + +### 1. Revision-Based List View +- **Issue:** The Correspondence List was displaying one row per Document, hiding revision history. +- **Fix:** Refactored `CorrespondenceService.findAll` to query `CorrespondenceRevision` as the primary entity. +- **Outcome:** The list now displays every revision (e.g., Doc-001 Rev A, Doc-001 Rev B) as separate rows. Added "Rev" column to the UI. + +### 2. Correspondence Detail Page +- **Issue:** Detail page was not displaying Subject/Description correctly (showing "-") because it wasn't resolving the `currentRevision` correctly or receiving unwrapped data. +- **Fix:** + - Updated `CorrespondenceDetail` to explicitly try finding `isCurrent` revision or fallback to index 0. + - Updated `useCorrespondence` (via `correspondence.service.ts`) to correctly unwrap the NestJS Interceptor response `{ data: { ... } }`. +- **Outcome:** Detail page now correctly shows Subject, Description, and Status from the current revision. + +### 3. Edit Functionality +- **Issue:** Clicking "Edit" led to a 404/Blank page. +- **Fix:** + - Created `app/(dashboard)/correspondences/[id]/edit/page.tsx`. + - Refactored `CorrespondenceForm` to accept `initialData` and supporting "Update" mode (switching between `createMutation` and `updateMutation`). +- **Outcome:** Users can now edit existing DRAFT correspondences. + +## 📂 Modified Files +- `backend/src/modules/correspondence/correspondence.service.ts` +- `frontend/types/correspondence.ts` +- `frontend/components/correspondences/list.tsx` +- `frontend/components/correspondences/detail.tsx` +- `frontend/components/correspondences/form.tsx` +- `frontend/lib/services/correspondence.service.ts` +- `frontend/app/(dashboard)/correspondences/[id]/edit/page.tsx` (Created) + +## ✅ Verification +- Validated List View shows revisions. +- Validated Detail View loads data. +- Validated Edit Page loads data and submits updates.