From 0510f15175d1fcc2a8e9f7c92c8ccbe39960aa48 Mon Sep 17 00:00:00 2001 From: Yashwin Date: Mon, 11 May 2026 19:14:47 +0530 Subject: [PATCH] refactor: decouple DepartmentTable logic into separate list and tree view components --- .../superadmin/DepartmentListView.tsx | 4 +- .../superadmin/DepartmentTreeView.tsx | 105 ++- .../superadmin/DepartmentsTable.tsx | 711 +++++++++--------- src/pages/tenant/Departments.tsx | 22 +- 4 files changed, 459 insertions(+), 383 deletions(-) diff --git a/src/components/superadmin/DepartmentListView.tsx b/src/components/superadmin/DepartmentListView.tsx index e6ffb9b..372946a 100644 --- a/src/components/superadmin/DepartmentListView.tsx +++ b/src/components/superadmin/DepartmentListView.tsx @@ -28,7 +28,7 @@ export const DepartmentListView = ({ onLimitChange, }: DepartmentListViewProps): ReactElement => { return ( - <> +
)} - +
); }; diff --git a/src/components/superadmin/DepartmentTreeView.tsx b/src/components/superadmin/DepartmentTreeView.tsx index bccd53b..6e34b90 100644 --- a/src/components/superadmin/DepartmentTreeView.tsx +++ b/src/components/superadmin/DepartmentTreeView.tsx @@ -4,6 +4,7 @@ import { ChevronRight, ChevronDown, Folder, + Building2, Plus, Edit2, Trash2, @@ -26,21 +27,34 @@ const TreeItem = ({ onDelete, }: TreeItemProps) => { const { primaryColor } = useAppTheme(); - const [isExpanded, setIsExpanded] = useState(level === 0); + const [isExpanded, setIsExpanded] = useState(false); const hasChildren = item.children && item.children.length > 0; + const isPrimaryActive = level === 0 && hasChildren && isExpanded; return (
0 ? `${level * 28}px` : "0", - marginBottom: "4px", + backgroundColor: + level === 0 + ? isPrimaryActive + ? primaryColor + : "#FFFFFF" + : "transparent", + paddingLeft: level > 0 ? `${level * 28 + 12}px` : "12px", + paddingRight: "12px", + marginBottom: level === 0 && isExpanded && hasChildren ? "0" : "4px", }} >
@@ -52,7 +66,7 @@ const TreeItem = ({ setIsExpanded(!isExpanded); }} className={`p-0.5 rounded transition-colors ${ - level === 0 + isPrimaryActive ? "hover:bg-white/10 text-white/70" : "hover:bg-gray-100 text-[#94a3b8]" }`} @@ -67,54 +81,76 @@ const TreeItem = ({
- + {level === 0 ? ( + + ) : ( + + )}
{item.name} {item.code} {level === 0 ? ( - + {item.user_count || 0} total ) : hasChildren ? ( - + {item.child_count || 0} sub-departments ) : null}
-
+
{isExpanded && hasChildren && ( -
+
{item.children?.map((child) => ( -
@@ -195,7 +244,7 @@ export const DepartmentTreeView = ({ } return ( -
+
{data.map((item) => ( void; } -export const DepartmentsTable = forwardRef(({ - tenantId: propsTenantId, - compact = false, - showHeader = true, -}: DepartmentsTableProps, ref): ReactElement => { - const { primaryColor } = useAppTheme(); - const { canCreate, canUpdate } = usePermissions(); - const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId); - const effectiveTenantId = propsTenantId || reduxTenantId; +export const DepartmentsTable = forwardRef< + DepartmentsTableRef, + DepartmentsTableProps +>( + ( + { + tenantId: propsTenantId, + compact = false, + showHeader = true, + }: DepartmentsTableProps, + ref, + ): ReactElement => { + const { primaryColor } = useAppTheme(); + const { canUpdate } = usePermissions(); + const reduxTenantId = useSelector( + (state: RootState) => state.auth.tenantId, + ); + const effectiveTenantId = propsTenantId || reduxTenantId; - const [departments, setDepartments] = useState([]); - const [treeData, setTreeData] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [viewMode, setViewMode] = useState<'list' | 'tree'>('list'); + const [departments, setDepartments] = useState([]); + const [treeData, setTreeData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [viewMode, setViewMode] = useState<"list" | "tree">("list"); - // Pagination state (Client-side since backend doesn't support it yet) - const [currentPage, setCurrentPage] = useState(1); - const [limit, setLimit] = useState(compact ? 10 : 5); + // Pagination state (Client-side since backend doesn't support it yet) + const [currentPage, setCurrentPage] = useState(1); + const [limit, setLimit] = useState(compact ? 10 : 5); - // Filter state - const [activeOnly, setActiveOnly] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(""); + // Filter state + const [activeOnly, setActiveOnly] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [debouncedSearchQuery, setDebouncedSearchQuery] = + useState(""); - // Modal states - const [isNewModalOpen, setIsNewModalOpen] = useState(false); - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [isViewModalOpen, setIsViewModalOpen] = useState(false); - // const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selectedDepartment, setSelectedDepartment] = - useState(null); - const [isActionLoading, setIsActionLoading] = useState(false); + // Modal states + const [isNewModalOpen, setIsNewModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isViewModalOpen, setIsViewModalOpen] = useState(false); + // const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selectedDepartment, setSelectedDepartment] = + useState(null); + const [isActionLoading, setIsActionLoading] = useState(false); - // Expose methods to parent - useImperativeHandle(ref, () => ({ - openNewModal: () => setIsNewModalOpen(true), - })); + // Expose methods to parent + useImperativeHandle(ref, () => ({ + openNewModal: () => setIsNewModalOpen(true), + })); - const fetchDepartments = async () => { - try { - setIsLoading(true); - setError(null); - if (viewMode === 'list') { - const response = await departmentService.list(effectiveTenantId, { - active_only: activeOnly, - search: debouncedSearchQuery, - }); - if (response.success) { - setDepartments(response.data); + const fetchDepartments = async () => { + try { + setIsLoading(true); + setError(null); + if (viewMode === "list") { + const response = await departmentService.list(effectiveTenantId, { + active_only: activeOnly, + search: debouncedSearchQuery, + }); + if (response.success) { + setDepartments(response.data); + } else { + setError("Failed to load departments"); + } } else { - setError("Failed to load departments"); + const response = await departmentService.getTree( + effectiveTenantId, + activeOnly, + ); + if (response.success) { + setTreeData(response.data); + } else { + setError("Failed to load department tree"); + } } - } else { - const response = await departmentService.getTree(effectiveTenantId, activeOnly); + } catch (err: any) { + setError( + err?.response?.data?.error?.message || "Failed to load departments", + ); + } finally { + setIsLoading(false); + } + }; + + // Debouncing search query + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchQuery(searchQuery); + }, 500); + + return () => clearTimeout(timer); + }, [searchQuery]); + + useEffect(() => { + fetchDepartments(); + }, [effectiveTenantId, activeOnly, debouncedSearchQuery, viewMode]); + + const handleCreate = async (data: CreateDepartmentRequest) => { + try { + setIsActionLoading(true); + const response = await departmentService.create( + data, + effectiveTenantId, + ); if (response.success) { - setTreeData(response.data); - } else { - setError("Failed to load department tree"); + showToast.success("Department created successfully"); + setIsNewModalOpen(false); + fetchDepartments(); } + } catch (err: any) { + showToast.error( + err?.response?.data?.error?.message || "Failed to create department", + ); + } finally { + setIsActionLoading(false); } - } catch (err: any) { - setError( - err?.response?.data?.error?.message || "Failed to load departments", - ); - } finally { - setIsLoading(false); - } - }; + }; - // Debouncing search query - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearchQuery(searchQuery); - }, 500); - - return () => clearTimeout(timer); - }, [searchQuery]); - - useEffect(() => { - fetchDepartments(); - }, [effectiveTenantId, activeOnly, debouncedSearchQuery, viewMode]); - - const handleCreate = async (data: CreateDepartmentRequest) => { - try { - setIsActionLoading(true); - const response = await departmentService.create(data, effectiveTenantId); - if (response.success) { - showToast.success("Department created successfully"); - setIsNewModalOpen(false); - fetchDepartments(); + const handleUpdate = async (id: string, data: UpdateDepartmentRequest) => { + try { + setIsActionLoading(true); + const response = await departmentService.update( + id, + data, + effectiveTenantId, + ); + if (response.success) { + showToast.success("Department updated successfully"); + setIsEditModalOpen(false); + fetchDepartments(); + } + } catch (err: any) { + showToast.error( + err?.response?.data?.error?.message || "Failed to update department", + ); + } finally { + setIsActionLoading(false); } - } catch (err: any) { - showToast.error( - err?.response?.data?.error?.message || "Failed to create department", - ); - } finally { - setIsActionLoading(false); - } - }; + }; - const handleUpdate = async (id: string, data: UpdateDepartmentRequest) => { - try { - setIsActionLoading(true); - const response = await departmentService.update( - id, - data, - effectiveTenantId, - ); - if (response.success) { - showToast.success("Department updated successfully"); - setIsEditModalOpen(false); - fetchDepartments(); - } - } catch (err: any) { - showToast.error( - err?.response?.data?.error?.message || "Failed to update department", - ); - } finally { - setIsActionLoading(false); - } - }; + // const handleDelete = async () => { + // if (!selectedDepartment) return; + // try { + // setIsActionLoading(true); + // const response = await departmentService.delete( + // selectedDepartment.id, + // effectiveTenantId, + // ); + // if (response.success) { + // showToast.success("Department deleted successfully"); + // setIsDeleteModalOpen(false); + // fetchDepartments(); + // } + // } catch (err: any) { + // showToast.error( + // err?.response?.data?.error?.message || "Failed to delete department", + // ); + // } finally { + // setIsActionLoading(false); + // } + // }; - // const handleDelete = async () => { - // if (!selectedDepartment) return; - // try { - // setIsActionLoading(true); - // const response = await departmentService.delete( - // selectedDepartment.id, - // effectiveTenantId, - // ); - // if (response.success) { - // showToast.success("Department deleted successfully"); - // setIsDeleteModalOpen(false); - // fetchDepartments(); - // } - // } catch (err: any) { - // showToast.error( - // err?.response?.data?.error?.message || "Failed to delete department", - // ); - // } finally { - // setIsActionLoading(false); - // } - // }; + // Client-side pagination logic + const totalItems = departments.length; + const totalPages = Math.ceil(totalItems / limit); + const paginatedData = departments.slice( + (currentPage - 1) * limit, + currentPage * limit, + ); - // Client-side pagination logic - const totalItems = departments.length; - const totalPages = Math.ceil(totalItems / limit); - const paginatedData = departments.slice( - (currentPage - 1) * limit, - currentPage * limit, - ); - - const columns: Column[] = [ - { - key: "name", - label: "Department Name", - render: (dept) => ( - {dept.name} - ), - }, - { - key: "code", - label: "Code", - render: (dept) => ( - - ), - }, - { - key: "parent_name", - label: "Parent", - render: (dept) => ( - - {dept.parent_name || "-"} - - ), - }, - { - key: "level", - label: "Level", - render: (dept) => ( - {dept.level} - ), - }, - { - key: "sort_order", - label: "Order", - render: (dept) => ( - {dept.sort_order} - ), - }, - { - key: "child_count", - label: "Sub-depts", - render: (dept) => ( - {dept.child_count || 0} - ), - }, - { - key: "user_count", - label: "Users", - render: (dept) => ( - {dept.user_count || 0} - ), - }, - { - key: "status", - label: "Status", - render: (dept) => ( - - {dept.is_active ? "Active" : "Inactive"} - - ), - }, - { - key: "actions", - label: "Actions", - align: "right", - render: (dept) => ( -
- { - setSelectedDepartment(dept); - setIsViewModalOpen(true); - }} - onEdit={ - canUpdate("departments") - ? () => { - setSelectedDepartment(dept); - setIsEditModalOpen(true); - } - : undefined - } - /> -
- ), - }, - ]; - - return ( -
- {showHeader && ( -
- {/* Tabs */} -
-
- - -
- - {/* Active Only Toggle */} - [] = [ + { + key: "name", + label: "Department Name", + render: (dept) => ( + + {dept.name} + + ), + }, + { + key: "code", + label: "Code", + render: (dept) => , + }, + { + key: "parent_name", + label: "Parent", + render: (dept) => ( + + {dept.parent_name || "-"} + + ), + }, + { + key: "level", + label: "Level", + render: (dept) => ( + {dept.level} + ), + }, + { + key: "sort_order", + label: "Order", + render: (dept) => ( + {dept.sort_order} + ), + }, + { + key: "child_count", + label: "Sub-depts", + render: (dept) => ( + + {dept.child_count || 0} + + ), + }, + { + key: "user_count", + label: "Users", + render: (dept) => ( + {dept.user_count || 0} + ), + }, + { + key: "status", + label: "Status", + render: (dept) => ( + + {dept.is_active ? "Active" : "Inactive"} + + ), + }, + { + key: "actions", + label: "Actions", + align: "right", + render: (dept) => ( +
+ { + setSelectedDepartment(dept); + setIsViewModalOpen(true); + }} + onEdit={ + canUpdate("departments") + ? () => { + setSelectedDepartment(dept); + setIsEditModalOpen(true); + } + : undefined + } />
+ ), + }, + ]; -
-
- {viewMode === 'list' && ( - - )} + return ( +
+ {showHeader && ( +
+ {/* Tabs */} +
+
+ + +
+
+
+ {viewMode === "list" && ( + + )} + {/* Active Only Toggle */} + +
+
- - {canCreate("departments") && ( - setIsNewModalOpen(true)} - > - - New Department - - )}
-
- )} + )} - {viewMode === 'list' ? ( - { - setLimit(newLimit); - setCurrentPage(1); - }} + {viewMode === "list" ? ( + { + setLimit(newLimit); + setCurrentPage(1); + }} + /> + ) : ( + { + setSelectedDepartment(item); + setIsNewModalOpen(true); + }} + onEdit={(item) => { + setSelectedDepartment(item); + setIsEditModalOpen(true); + }} + /> + )} + + setIsNewModalOpen(false)} + onSubmit={handleCreate} + isLoading={isActionLoading} + tenantId={effectiveTenantId} /> - ) : ( - { - setSelectedDepartment(item); - setIsNewModalOpen(true); - }} - onEdit={(item) => { - setSelectedDepartment(item); - setIsEditModalOpen(true); + + { + setIsEditModalOpen(false); + setSelectedDepartment(null); }} + department={selectedDepartment} + onSubmit={handleUpdate} + isLoading={isActionLoading} + tenantId={effectiveTenantId} /> - )} - setIsNewModalOpen(false)} - onSubmit={handleCreate} - isLoading={isActionLoading} - tenantId={effectiveTenantId} - /> + { + setIsViewModalOpen(false); + setSelectedDepartment(null); + }} + department={selectedDepartment} + /> - { - setIsEditModalOpen(false); - setSelectedDepartment(null); - }} - department={selectedDepartment} - onSubmit={handleUpdate} - isLoading={isActionLoading} - tenantId={effectiveTenantId} - /> - - { - setIsViewModalOpen(false); - setSelectedDepartment(null); - }} - department={selectedDepartment} - /> - - {/* { setIsDeleteModalOpen(false); @@ -426,6 +436,7 @@ export const DepartmentsTable = forwardRef */} -
- ); -}); +
+ ); + }, +); diff --git a/src/pages/tenant/Departments.tsx b/src/pages/tenant/Departments.tsx index 6498e85..4d0cca1 100644 --- a/src/pages/tenant/Departments.tsx +++ b/src/pages/tenant/Departments.tsx @@ -1,17 +1,33 @@ -import { type ReactElement } from 'react'; +import { useRef, type ReactElement } from 'react'; import { Layout } from '@/components/layout/Layout'; -import { DepartmentsTable } from '@/components/superadmin'; +import { DepartmentsTable, type DepartmentsTableRef } from '@/components/superadmin/DepartmentsTable'; +import { PrimaryButton } from '@/components/shared'; +import { Plus } from 'lucide-react'; +import { usePermissions } from '@/hooks/usePermissions'; const Departments = (): ReactElement => { + const tableRef = useRef(null); + const { canCreate } = usePermissions(); + return ( tableRef.current?.openNewModal()} + > + + New Department + + ) : null, }} > - + ); };