From 5e4d5ee36105176968ab3eb1025bf84cf9f68a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CPeteroche=E2=80=9D?= <“petergoddey08l@gmail.com”> Date: Mon, 23 Feb 2026 08:25:41 +0100 Subject: [PATCH 1/2] feat: Add departments and categories management page and updated type definitions for categories and departments. --- frontend/app/(dashboard)/departments/page.tsx | 204 ++++++++++++++++++ frontend/lib/api/assets.ts | 33 ++- frontend/lib/query/types/asset.ts | 13 +- 3 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 frontend/app/(dashboard)/departments/page.tsx diff --git a/frontend/app/(dashboard)/departments/page.tsx b/frontend/app/(dashboard)/departments/page.tsx new file mode 100644 index 00000000..20e9951a --- /dev/null +++ b/frontend/app/(dashboard)/departments/page.tsx @@ -0,0 +1,204 @@ +'use client'; + +import React, { useState } from 'react'; +import { + useDepartmentsList, + useCreateDepartment, + useDeleteDepartment, + useCategories, + useCreateCategory, + useDeleteCategory +} from '@/lib/query/hooks/query.hook'; +import { DepartmentWithCount, CategoryWithCount } from '@/lib/api/assets'; +import { Plus, Trash2, LayoutGrid, Tags, Loader2, AlertCircle } from 'lucide-react'; +import { toast } from 'react-toastify'; + +type TabType = 'departments' | 'categories'; + +export default function DepartmentsPage() { + const [activeTab, setActiveTab] = useState('departments'); + const [isAdding, setIsAdding] = useState(false); + const [formData, setFormData] = useState({ name: '', description: '' }); + + const { data: departments, isLoading: isLoadingDepts } = useDepartmentsList(); + const { data: categories, isLoading: isLoadingCats } = useCategories(); + + const createDept = useCreateDepartment(); + const deleteDept = useDeleteDepartment(); + const createCat = useCreateCategory(); + const deleteCat = useDeleteCategory(); + + const handleCreate = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.name.trim()) return; + + try { + if (activeTab === 'departments') { + await createDept.mutateAsync(formData); + } else { + await createCat.mutateAsync(formData); + } + toast.success(`${activeTab === 'departments' ? 'Department' : 'Category'} created successfully`); + setFormData({ name: '', description: '' }); + setIsAdding(false); + } catch (err: any) { + toast.error(err.message || `Failed to create ${activeTab}`); + } + }; + + const handleDelete = async (id: string, name: string) => { + const confirmMessage = `Are you sure you want to delete ${name}? Assets in this ${activeTab === 'departments' ? 'department' : 'category'} will need to be reassigned/recategorised.`; + if (!window.confirm(confirmMessage)) return; + + try { + if (activeTab === 'departments') { + await deleteDept.mutateAsync(id); + } else { + await deleteCat.mutateAsync(id); + } + toast.success(`${activeTab === 'departments' ? 'Department' : 'Category'} deleted successfully`); + } catch (err: any) { + toast.error(err.message || `Failed to delete ${activeTab}`); + } + }; + + const items = activeTab === 'departments' ? departments : categories; + const isLoading = activeTab === 'departments' ? isLoadingDepts : isLoadingCats; + + return ( +
+
+

Management

+ +
+ + +
+
+ +
+

+ {activeTab} ({items?.length || 0}) +

+ +
+ + {isAdding && ( +
+
+
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all" + /> + {activeTab === 'departments' ? createDept.isError && ( +

+ {createDept.error?.message} +

+ ) : createCat.isError && ( +

+ {createCat.error?.message} +

+ )} +
+
+ + setFormData({ ...formData, description: e.target.value })} + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all" + /> +
+
+
+ + +
+
+ )} + + {isLoading ? ( +
+ +

Loading {activeTab}...

+
+ ) : ( +
+ {items?.map((item: any) => ( +
+
+

{item.name}

+ +
+

+ {item.description || 'No description provided.'} +

+
+ Asset Count + {item.assetCount || 0} +
+
+ ))} + {!isLoading && items?.length === 0 && ( +
+ +

No {activeTab} found. Create one to get started.

+
+ )} +
+ )} +
+ ); +} diff --git a/frontend/lib/api/assets.ts b/frontend/lib/api/assets.ts index 17515cbb..ec3a778a 100644 --- a/frontend/lib/api/assets.ts +++ b/frontend/lib/api/assets.ts @@ -6,9 +6,12 @@ import { AssetHistoryFilters, AssetNote, AssetUser, + Category, + CategoryWithCount, CreateMaintenanceInput, CreateNoteInput, Department, + DepartmentWithCount, MaintenanceRecord, TransferAssetInput, UpdateAssetStatusInput, @@ -46,8 +49,34 @@ export const assetApiClient = { return apiClient.request(`/assets/${id}/notes`); }, - getDepartments(): Promise { - return apiClient.request('/departments'); + getDepartments(): Promise { + return apiClient.request('/departments'); + }, + + createDepartment(data: { name: string; description?: string }): Promise { + return apiClient.request('/departments', { + method: 'POST', + body: JSON.stringify(data), + }); + }, + + deleteDepartment(id: string): Promise { + return apiClient.request(`/departments/${id}`, { method: 'DELETE' }); + }, + + getCategories(): Promise { + return apiClient.request('/categories'); + }, + + createCategory(data: { name: string; description?: string }): Promise { + return apiClient.request('/categories', { + method: 'POST', + body: JSON.stringify(data), + }); + }, + + deleteCategory(id: string): Promise { + return apiClient.request(`/categories/${id}`, { method: 'DELETE' }); }, getUsers(): Promise { diff --git a/frontend/lib/query/types/asset.ts b/frontend/lib/query/types/asset.ts index 7ed789e4..869be4a5 100644 --- a/frontend/lib/query/types/asset.ts +++ b/frontend/lib/query/types/asset.ts @@ -17,15 +17,24 @@ export enum AssetCondition { DAMAGED = 'DAMAGED', } -export interface AssetCategory { +export interface Category { id: string; name: string; description?: string; } +export interface CategoryWithCount extends Category { + assetCount: number; +} + export interface Department { id: string; name: string; + description?: string; +} + +export interface DepartmentWithCount extends Department { + assetCount: number; } export interface AssetUser { @@ -39,7 +48,7 @@ export interface Asset { assetId: string; name: string; description: string | null; - category: AssetCategory; + category: Category; serialNumber: string | null; purchaseDate: string | null; purchasePrice: number | null; From dc824798a35284fafc6a87a6f8e5a102a8d0c376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CPeteroche=E2=80=9D?= <“petergoddey08l@gmail.com”> Date: Mon, 23 Feb 2026 08:41:57 +0100 Subject: [PATCH 2/2] feat: Implement asset transfer functionality with a new dedicated dialog and integrate it into the asset details page. --- frontend/app/(dashboard)/assets/[id]/page.tsx | 28 +++- .../components/assets/transfer-dialog.tsx | 141 ++++++++++++++++++ 2 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 frontend/components/assets/transfer-dialog.tsx diff --git a/frontend/app/(dashboard)/assets/[id]/page.tsx b/frontend/app/(dashboard)/assets/[id]/page.tsx index 3de49715..c53d409d 100644 --- a/frontend/app/(dashboard)/assets/[id]/page.tsx +++ b/frontend/app/(dashboard)/assets/[id]/page.tsx @@ -8,6 +8,9 @@ import { Button } from "@/components/ui/button"; import { StatusBadge } from "@/components/assets/status-badge"; import { ConditionBadge } from "@/components/assets/condition-badge"; import { useAsset, useAssetHistory } from "@/lib/query/hooks/useAsset"; +import { useAuthStore } from "@/store/auth.store"; +import { TransferAssetDialog } from "@/components/assets/transfer-dialog"; +import { MoveHorizontal } from "lucide-react"; type Tab = "overview" | "history" | "documents"; @@ -15,6 +18,8 @@ export default function AssetDetailPage() { const { id } = useParams<{ id: string }>(); const router = useRouter(); const [tab, setTab] = useState("overview"); + const [isTransferOpen, setIsTransferOpen] = useState(false); + const { user } = useAuthStore(); const { data: asset, isLoading } = useAsset(id); const { data: history = [] } = useAssetHistory(id); @@ -73,6 +78,16 @@ export default function AssetDetailPage() { + + {(user?.role === 'ADMIN' || user?.role === 'MANAGER') && ( + + )} @@ -82,11 +97,10 @@ export default function AssetDetailPage() { + + +
+ {/* Department */} +
+ + + {errors.departmentId &&

{errors.departmentId.message}

} +
+ + {/* Assigned To */} +
+ + +
+ + + +
+ +