refactor: decouple DepartmentTable logic into separate list and tree view components

This commit is contained in:
Yashwin 2026-05-11 19:14:47 +05:30
parent f87f552d52
commit 0510f15175
4 changed files with 459 additions and 383 deletions

View File

@ -28,7 +28,7 @@ export const DepartmentListView = ({
onLimitChange, onLimitChange,
}: DepartmentListViewProps): ReactElement => { }: DepartmentListViewProps): ReactElement => {
return ( return (
<> <div className="bg-white rounded-2xl border-2 border-slate-50 shadow-sm overflow-hidden">
<DataTable <DataTable
data={data} data={data}
columns={columns} columns={columns}
@ -48,6 +48,6 @@ export const DepartmentListView = ({
onLimitChange={onLimitChange} onLimitChange={onLimitChange}
/> />
)} )}
</> </div>
); );
}; };

View File

@ -4,6 +4,7 @@ import {
ChevronRight, ChevronRight,
ChevronDown, ChevronDown,
Folder, Folder,
Building2,
Plus, Plus,
Edit2, Edit2,
Trash2, Trash2,
@ -26,21 +27,34 @@ const TreeItem = ({
onDelete, onDelete,
}: TreeItemProps) => { }: TreeItemProps) => {
const { primaryColor } = useAppTheme(); const { primaryColor } = useAppTheme();
const [isExpanded, setIsExpanded] = useState(level === 0); const [isExpanded, setIsExpanded] = useState(false);
const hasChildren = item.children && item.children.length > 0; const hasChildren = item.children && item.children.length > 0;
const isPrimaryActive = level === 0 && hasChildren && isExpanded;
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div <div
className={`group flex items-center py-2.5 px-4 rounded-lg transition-all duration-200 ${ className={`group flex h-[44px] items-center gap-2 self-stretch transition-all duration-200 border ${
level === 0 level === 0
? "text-white shadow-md" ? isPrimaryActive
: "text-[#0f1724] hover:bg-[#f8fafc] border border-transparent hover:border-[#e2e8f0]" ? "border-transparent text-white rounded-t-md"
: "border-[#E5E7EB] text-[#111827] rounded-md"
: "border-transparent hover:border-[#E2E8F0] hover:bg-[#F8FAFC] text-[#111827] rounded-md"
} ${
level === 0 && isExpanded && hasChildren
? "border-b-transparent"
: ""
}`} }`}
style={{ style={{
backgroundColor: level === 0 ? primaryColor : undefined, backgroundColor:
marginLeft: level > 0 ? `${level * 28}px` : "0", level === 0
marginBottom: "4px", ? isPrimaryActive
? primaryColor
: "#FFFFFF"
: "transparent",
paddingLeft: level > 0 ? `${level * 28 + 12}px` : "12px",
paddingRight: "12px",
marginBottom: level === 0 && isExpanded && hasChildren ? "0" : "4px",
}} }}
> >
<div className="flex items-center gap-3 flex-1 min-w-0"> <div className="flex items-center gap-3 flex-1 min-w-0">
@ -52,7 +66,7 @@ const TreeItem = ({
setIsExpanded(!isExpanded); setIsExpanded(!isExpanded);
}} }}
className={`p-0.5 rounded transition-colors ${ className={`p-0.5 rounded transition-colors ${
level === 0 isPrimaryActive
? "hover:bg-white/10 text-white/70" ? "hover:bg-white/10 text-white/70"
: "hover:bg-gray-100 text-[#94a3b8]" : "hover:bg-gray-100 text-[#94a3b8]"
}`} }`}
@ -67,54 +81,76 @@ const TreeItem = ({
</div> </div>
<div <div
className={`p-1.5 rounded-md ${level === 0 ? "bg-white/10" : "bg-transparent"}`} className={`p-1.5 rounded-md ${isPrimaryActive ? "bg-white/10" : "bg-transparent"}`}
> >
<Folder {level === 0 ? (
className={`w-4 h-4 shrink-0 ${level === 0 ? "text-white" : "text-[#64748b]"}`} <Building2
/> className={`w-4 h-4 shrink-0 ${isPrimaryActive ? "text-white" : "text-[#64748b]"}`}
/>
) : (
<Folder
className={`w-4 h-4 shrink-0 ${isPrimaryActive ? "text-white" : "text-[#64748b]"}`}
/>
)}
</div> </div>
<div className="flex items-center gap-2 flex-1 min-w-0"> <div className="flex items-center gap-2 flex-1 min-w-0">
<span <span
className={`text-sm font-medium truncate ${level === 0 ? "text-white" : "text-[#1e293b]"}`} className={`text-[14px] font-medium truncate ${
isPrimaryActive ? "text-white" : "text-[#111827]"
}`}
> >
{item.name} {item.name}
</span> </span>
<span <span
className={`px-1.5 py-0.5 rounded text-[10px] font-bold font-mono shrink-0 uppercase ${ className={`px-1.5 py-[2px] rounded text-[10px] font-semibold ${
level === 0 isPrimaryActive
? "bg-white/20 text-white" ? "bg-white/15 text-white"
: "bg-[#f1f5f9] text-[#475569]" : "bg-[#F3F4F6] text-[#374151]"
}`} }`}
> >
{item.code} {item.code}
</span> </span>
{level === 0 ? ( {level === 0 ? (
<span className="text-xs text-white/60 font-normal ml-2"> <span
className={`text-xs ${
isPrimaryActive ? "text-white/70" : "text-[#9CA3AF]"
}`}
>
{item.user_count || 0} total {item.user_count || 0} total
</span> </span>
) : hasChildren ? ( ) : hasChildren ? (
<span className="text-xs text-[#94a3b8] font-normal ml-2"> <span
className={`text-xs ${
isPrimaryActive ? "text-white/70" : "text-[#9CA3AF]"
}`}
>
{item.child_count || 0} sub-departments {item.child_count || 0} sub-departments
</span> </span>
) : null} ) : null}
</div> </div>
</div> </div>
<div <div className="ml-auto flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
className={`flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-all duration-200 ${level === 0 ? "text-white" : "text-[#64748b]"}`}
>
<button <button
onClick={() => onAddSub(item)} onClick={() => onAddSub(item)}
className={`p-1.5 rounded transition-colors ${level === 0 ? "hover:bg-white/10" : "hover:bg-gray-100"}`} className={`p-1.5 rounded transition-colors ${
isPrimaryActive
? "hover:bg-white/10 text-white"
: "hover:bg-gray-100 text-[#64748B]"
}`}
title="Add Sub-department" title="Add Sub-department"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => onEdit(item)} onClick={() => onEdit(item)}
className={`p-1.5 rounded transition-colors ${level === 0 ? "hover:bg-white/10" : "hover:bg-gray-100"}`} className={`p-1.5 rounded transition-colors ${
isPrimaryActive
? "hover:bg-white/10 text-white"
: "hover:bg-gray-100 text-[#64748B]"
}`}
title="Edit" title="Edit"
> >
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
@ -122,7 +158,11 @@ const TreeItem = ({
{onDelete && ( {onDelete && (
<button <button
onClick={() => onDelete(item)} onClick={() => onDelete(item)}
className={`p-1.5 rounded transition-colors ${level === 0 ? "hover:bg-white/10 text-white/70" : "hover:bg-red-50 hover:text-red-600"}`} className={`p-1.5 rounded transition-colors ${
isPrimaryActive
? "hover:bg-white/10 text-white/70"
: "hover:bg-red-50 hover:text-red-600"
}`}
title="Delete" title="Delete"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
@ -132,7 +172,16 @@ const TreeItem = ({
</div> </div>
{isExpanded && hasChildren && ( {isExpanded && hasChildren && (
<div className="flex flex-col"> <div
className={`flex flex-col self-stretch p-3 gap-1 ${
level === 0
? "rounded-b-md border border-[#D1D5DB] border-t-0 bg-white"
: ""
}`}
style={{
marginTop: "0px",
}}
>
{item.children?.map((child) => ( {item.children?.map((child) => (
<TreeItem <TreeItem
key={child.id} key={child.id}
@ -195,7 +244,7 @@ export const DepartmentTreeView = ({
} }
return ( return (
<div className="p-4 flex flex-col gap-2 min-h-[400px]"> <div className="flex flex-col gap-4 p-4 border border-[#D1D5DB] bg-white rounded-lg self-stretch">
{data.map((item) => ( {data.map((item) => (
<TreeItem <TreeItem
key={item.id} key={item.id}

View File

@ -1,4 +1,10 @@
import { useState, useEffect, useImperativeHandle, forwardRef, type ReactElement } from "react"; import {
useState,
useEffect,
useImperativeHandle,
forwardRef,
type ReactElement,
} from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { import {
// PrimaryButton, // PrimaryButton,
@ -11,7 +17,6 @@ import {
SearchBox, SearchBox,
ActiveOnlyToggle, ActiveOnlyToggle,
type Column, type Column,
PrimaryButton,
} from "@/components/shared"; } from "@/components/shared";
import { import {
NewDepartmentModal, NewDepartmentModal,
@ -32,7 +37,6 @@ import type { RootState } from "@/store/store";
import { useAppTheme } from "@/hooks/useAppTheme"; import { useAppTheme } from "@/hooks/useAppTheme";
import { usePermissions } from "@/hooks/usePermissions"; import { usePermissions } from "@/hooks/usePermissions";
import CodeBadge from "../shared/CodeBadge"; import CodeBadge from "../shared/CodeBadge";
import { Plus } from "lucide-react";
interface DepartmentsTableProps { interface DepartmentsTableProps {
tenantId?: string | null; // If provided, use this tenantId (Super Admin mode) tenantId?: string | null; // If provided, use this tenantId (Super Admin mode)
@ -44,377 +48,383 @@ export interface DepartmentsTableRef {
openNewModal: () => void; openNewModal: () => void;
} }
export const DepartmentsTable = forwardRef<DepartmentsTableRef, DepartmentsTableProps>(({ export const DepartmentsTable = forwardRef<
tenantId: propsTenantId, DepartmentsTableRef,
compact = false, DepartmentsTableProps
showHeader = true, >(
}: DepartmentsTableProps, ref): ReactElement => { (
const { primaryColor } = useAppTheme(); {
const { canCreate, canUpdate } = usePermissions(); tenantId: propsTenantId,
const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId); compact = false,
const effectiveTenantId = propsTenantId || reduxTenantId; 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<Department[]>([]); const [departments, setDepartments] = useState<Department[]>([]);
const [treeData, setTreeData] = useState<Department[]>([]); const [treeData, setTreeData] = useState<Department[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'list' | 'tree'>('list'); const [viewMode, setViewMode] = useState<"list" | "tree">("list");
// Pagination state (Client-side since backend doesn't support it yet) // Pagination state (Client-side since backend doesn't support it yet)
const [currentPage, setCurrentPage] = useState<number>(1); const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(compact ? 10 : 5); const [limit, setLimit] = useState<number>(compact ? 10 : 5);
// Filter state // Filter state
const [activeOnly, setActiveOnly] = useState<boolean>(false); const [activeOnly, setActiveOnly] = useState<boolean>(false);
const [searchQuery, setSearchQuery] = useState<string>(""); const [searchQuery, setSearchQuery] = useState<string>("");
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState<string>(""); const [debouncedSearchQuery, setDebouncedSearchQuery] =
useState<string>("");
// Modal states // Modal states
const [isNewModalOpen, setIsNewModalOpen] = useState(false); const [isNewModalOpen, setIsNewModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isViewModalOpen, setIsViewModalOpen] = useState(false); const [isViewModalOpen, setIsViewModalOpen] = useState(false);
// const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); // const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedDepartment, setSelectedDepartment] = const [selectedDepartment, setSelectedDepartment] =
useState<Department | null>(null); useState<Department | null>(null);
const [isActionLoading, setIsActionLoading] = useState(false); const [isActionLoading, setIsActionLoading] = useState(false);
// Expose methods to parent // Expose methods to parent
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
openNewModal: () => setIsNewModalOpen(true), openNewModal: () => setIsNewModalOpen(true),
})); }));
const fetchDepartments = async () => { const fetchDepartments = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
if (viewMode === 'list') { if (viewMode === "list") {
const response = await departmentService.list(effectiveTenantId, { const response = await departmentService.list(effectiveTenantId, {
active_only: activeOnly, active_only: activeOnly,
search: debouncedSearchQuery, search: debouncedSearchQuery,
}); });
if (response.success) { if (response.success) {
setDepartments(response.data); setDepartments(response.data);
} else {
setError("Failed to load departments");
}
} else { } 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 { } catch (err: any) {
const response = await departmentService.getTree(effectiveTenantId, activeOnly); 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) { if (response.success) {
setTreeData(response.data); showToast.success("Department created successfully");
} else { setIsNewModalOpen(false);
setError("Failed to load department tree"); 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 const handleUpdate = async (id: string, data: UpdateDepartmentRequest) => {
useEffect(() => { try {
const timer = setTimeout(() => { setIsActionLoading(true);
setDebouncedSearchQuery(searchQuery); const response = await departmentService.update(
}, 500); id,
data,
return () => clearTimeout(timer); effectiveTenantId,
}, [searchQuery]); );
if (response.success) {
useEffect(() => { showToast.success("Department updated successfully");
fetchDepartments(); setIsEditModalOpen(false);
}, [effectiveTenantId, activeOnly, debouncedSearchQuery, viewMode]); fetchDepartments();
}
const handleCreate = async (data: CreateDepartmentRequest) => { } catch (err: any) {
try { showToast.error(
setIsActionLoading(true); err?.response?.data?.error?.message || "Failed to update department",
const response = await departmentService.create(data, effectiveTenantId); );
if (response.success) { } finally {
showToast.success("Department created successfully"); setIsActionLoading(false);
setIsNewModalOpen(false);
fetchDepartments();
} }
} catch (err: any) { };
showToast.error(
err?.response?.data?.error?.message || "Failed to create department",
);
} finally {
setIsActionLoading(false);
}
};
const handleUpdate = async (id: string, data: UpdateDepartmentRequest) => { // const handleDelete = async () => {
try { // if (!selectedDepartment) return;
setIsActionLoading(true); // try {
const response = await departmentService.update( // setIsActionLoading(true);
id, // const response = await departmentService.delete(
data, // selectedDepartment.id,
effectiveTenantId, // effectiveTenantId,
); // );
if (response.success) { // if (response.success) {
showToast.success("Department updated successfully"); // showToast.success("Department deleted successfully");
setIsEditModalOpen(false); // setIsDeleteModalOpen(false);
fetchDepartments(); // fetchDepartments();
} // }
} catch (err: any) { // } catch (err: any) {
showToast.error( // showToast.error(
err?.response?.data?.error?.message || "Failed to update department", // err?.response?.data?.error?.message || "Failed to delete department",
); // );
} finally { // } finally {
setIsActionLoading(false); // setIsActionLoading(false);
} // }
}; // };
// const handleDelete = async () => { // Client-side pagination logic
// if (!selectedDepartment) return; const totalItems = departments.length;
// try { const totalPages = Math.ceil(totalItems / limit);
// setIsActionLoading(true); const paginatedData = departments.slice(
// const response = await departmentService.delete( (currentPage - 1) * limit,
// selectedDepartment.id, currentPage * limit,
// 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 columns: Column<Department>[] = [
const totalItems = departments.length; {
const totalPages = Math.ceil(totalItems / limit); key: "name",
const paginatedData = departments.slice( label: "Department Name",
(currentPage - 1) * limit, render: (dept) => (
currentPage * limit, <span className="text-sm font-medium text-[#0f1724]">
); {dept.name}
</span>
const columns: Column<Department>[] = [ ),
{ },
key: "name", {
label: "Department Name", key: "code",
render: (dept) => ( label: "Code",
<span className="text-sm font-medium text-[#0f1724]">{dept.name}</span> render: (dept) => <CodeBadge label={dept.code} />,
), },
}, {
{ key: "parent_name",
key: "code", label: "Parent",
label: "Code", render: (dept) => (
render: (dept) => ( <span className="text-sm text-[#6b7280]">
<CodeBadge label={dept.code} /> {dept.parent_name || "-"}
), </span>
}, ),
{ },
key: "parent_name", {
label: "Parent", key: "level",
render: (dept) => ( label: "Level",
<span className="text-sm text-[#6b7280]"> render: (dept) => (
{dept.parent_name || "-"} <span className="text-sm text-[#6b7280]">{dept.level}</span>
</span> ),
), },
}, {
{ key: "sort_order",
key: "level", label: "Order",
label: "Level", render: (dept) => (
render: (dept) => ( <span className="text-sm text-[#6b7280]">{dept.sort_order}</span>
<span className="text-sm text-[#6b7280]">{dept.level}</span> ),
), },
}, {
{ key: "child_count",
key: "sort_order", label: "Sub-depts",
label: "Order", render: (dept) => (
render: (dept) => ( <span className="text-sm text-[#6b7280]">
<span className="text-sm text-[#6b7280]">{dept.sort_order}</span> {dept.child_count || 0}
), </span>
}, ),
{ },
key: "child_count", {
label: "Sub-depts", key: "user_count",
render: (dept) => ( label: "Users",
<span className="text-sm text-[#6b7280]">{dept.child_count || 0}</span> render: (dept) => (
), <span className="text-sm text-[#6b7280]">{dept.user_count || 0}</span>
}, ),
{ },
key: "user_count", {
label: "Users", key: "status",
render: (dept) => ( label: "Status",
<span className="text-sm text-[#6b7280]">{dept.user_count || 0}</span> render: (dept) => (
), <StatusBadge variant={dept.is_active ? "success" : "failure"}>
}, {dept.is_active ? "Active" : "Inactive"}
{ </StatusBadge>
key: "status", ),
label: "Status", },
render: (dept) => ( {
<StatusBadge variant={dept.is_active ? "success" : "failure"}> key: "actions",
{dept.is_active ? "Active" : "Inactive"} label: "Actions",
</StatusBadge> align: "right",
), render: (dept) => (
}, <div className="flex justify-end">
{ <ActionDropdown
key: "actions", onView={() => {
label: "Actions", setSelectedDepartment(dept);
align: "right", setIsViewModalOpen(true);
render: (dept) => ( }}
<div className="flex justify-end"> onEdit={
<ActionDropdown canUpdate("departments")
onView={() => { ? () => {
setSelectedDepartment(dept); setSelectedDepartment(dept);
setIsViewModalOpen(true); setIsEditModalOpen(true);
}} }
onEdit={ : undefined
canUpdate("departments") }
? () => {
setSelectedDepartment(dept);
setIsEditModalOpen(true);
}
: undefined
}
/>
</div>
),
},
];
return (
<div
className={`flex flex-col gap-4 ${!compact ? "bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-sm" : ""}`}
>
{showHeader && (
<div className="flex flex-col border-b border-[rgba(0,0,0,0.08)]">
{/* Tabs */}
<div className="px-4 pt-3 flex items-center justify-between border-b border-transparent">
<div className="flex items-center gap-6">
<button
className="pb-3 text-sm font-medium transition-all relative"
style={{ color: viewMode === 'list' ? primaryColor : '#64748b' }}
onClick={() => setViewMode('list')}
>
List View
{viewMode === 'list' && (
<div
className="absolute bottom-0 left-0 right-0 h-0.5"
style={{ backgroundColor: primaryColor }}
/>
)}
</button>
<button
className="pb-3 text-sm font-medium transition-all relative"
style={{ color: viewMode === 'tree' ? primaryColor : '#64748b' }}
onClick={() => setViewMode('tree')}
>
Tree View
{viewMode === 'tree' && (
<div
className="absolute bottom-0 left-0 right-0 h-0.5"
style={{ backgroundColor: primaryColor }}
/>
)}
</button>
</div>
{/* Active Only Toggle */}
<ActiveOnlyToggle
activeOnly={activeOnly}
onChange={setActiveOnly}
className="pb-3"
/> />
</div> </div>
),
},
];
<div className="p-4 flex flex-col sm:flex-row items-center justify-between gap-4"> return (
<div className="flex items-center gap-3 w-full sm:w-auto"> <div className={`flex flex-col gap-4 `}>
{viewMode === 'list' && ( {showHeader && (
<SearchBox <div className="flex flex-col border-b border-[rgba(0,0,0,0.08)]">
value={searchQuery} {/* Tabs */}
onChange={setSearchQuery} <div className="px-4 pt-3 flex items-center justify-between border-b border-transparent">
placeholder="Search by name or code..." <div className="flex items-center gap-6">
/> <button
)} className="pb-3 text-sm font-medium transition-all relative"
style={{
color: viewMode === "list" ? primaryColor : "#64748b",
}}
onClick={() => setViewMode("list")}
>
List View
{viewMode === "list" && (
<div
className="absolute bottom-0 left-0 right-0 h-0.5"
style={{ backgroundColor: primaryColor }}
/>
)}
</button>
<button
className="pb-3 text-sm font-medium transition-all relative"
style={{
color: viewMode === "tree" ? primaryColor : "#64748b",
}}
onClick={() => setViewMode("tree")}
>
Tree View
{viewMode === "tree" && (
<div
className="absolute bottom-0 left-0 right-0 h-0.5"
style={{ backgroundColor: primaryColor }}
/>
)}
</button>
</div>
<div>
<div className="flex items-center gap-3">
{viewMode === "list" && (
<SearchBox
value={searchQuery}
onChange={setSearchQuery}
placeholder="Search by name or code..."
/>
)}
{/* Active Only Toggle */}
<ActiveOnlyToggle
activeOnly={activeOnly}
onChange={setActiveOnly}
/>
</div>
</div>
</div> </div>
{canCreate("departments") && (
<PrimaryButton
size="default"
className="flex items-center gap-2 w-full sm:w-auto"
onClick={() => setIsNewModalOpen(true)}
>
<Plus className="w-4 h-4" />
<span>New Department</span>
</PrimaryButton>
)}
</div> </div>
</div> )}
)}
{viewMode === 'list' ? ( {viewMode === "list" ? (
<DepartmentListView <DepartmentListView
data={paginatedData} data={paginatedData}
columns={columns} columns={columns}
isLoading={isLoading} isLoading={isLoading}
error={error} error={error}
currentPage={currentPage} currentPage={currentPage}
totalPages={totalPages} totalPages={totalPages}
totalItems={totalItems} totalItems={totalItems}
limit={limit} limit={limit}
onPageChange={setCurrentPage} onPageChange={setCurrentPage}
onLimitChange={(newLimit) => { onLimitChange={(newLimit) => {
setLimit(newLimit); setLimit(newLimit);
setCurrentPage(1); setCurrentPage(1);
}} }}
/>
) : (
<DepartmentTreeView
data={treeData}
isLoading={isLoading}
error={error}
onAddSub={(item) => {
setSelectedDepartment(item);
setIsNewModalOpen(true);
}}
onEdit={(item) => {
setSelectedDepartment(item);
setIsEditModalOpen(true);
}}
/>
)}
<NewDepartmentModal
isOpen={isNewModalOpen}
onClose={() => setIsNewModalOpen(false)}
onSubmit={handleCreate}
isLoading={isActionLoading}
tenantId={effectiveTenantId}
/> />
) : (
<DepartmentTreeView <EditDepartmentModal
data={treeData} isOpen={isEditModalOpen}
isLoading={isLoading} onClose={() => {
error={error} setIsEditModalOpen(false);
onAddSub={(item) => { setSelectedDepartment(null);
setSelectedDepartment(item);
setIsNewModalOpen(true);
}}
onEdit={(item) => {
setSelectedDepartment(item);
setIsEditModalOpen(true);
}} }}
department={selectedDepartment}
onSubmit={handleUpdate}
isLoading={isActionLoading}
tenantId={effectiveTenantId}
/> />
)}
<NewDepartmentModal <ViewDepartmentModal
isOpen={isNewModalOpen} isOpen={isViewModalOpen}
onClose={() => setIsNewModalOpen(false)} onClose={() => {
onSubmit={handleCreate} setIsViewModalOpen(false);
isLoading={isActionLoading} setSelectedDepartment(null);
tenantId={effectiveTenantId} }}
/> department={selectedDepartment}
/>
<EditDepartmentModal {/* <DeleteConfirmationModal
isOpen={isEditModalOpen}
onClose={() => {
setIsEditModalOpen(false);
setSelectedDepartment(null);
}}
department={selectedDepartment}
onSubmit={handleUpdate}
isLoading={isActionLoading}
tenantId={effectiveTenantId}
/>
<ViewDepartmentModal
isOpen={isViewModalOpen}
onClose={() => {
setIsViewModalOpen(false);
setSelectedDepartment(null);
}}
department={selectedDepartment}
/>
{/* <DeleteConfirmationModal
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
onClose={() => { onClose={() => {
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
@ -426,6 +436,7 @@ export const DepartmentsTable = forwardRef<DepartmentsTableRef, DepartmentsTable
itemName={selectedDepartment?.name || ""} itemName={selectedDepartment?.name || ""}
isLoading={isActionLoading} isLoading={isActionLoading}
/> */} /> */}
</div> </div>
); );
}); },
);

View File

@ -1,17 +1,33 @@
import { type ReactElement } from 'react'; import { useRef, type ReactElement } from 'react';
import { Layout } from '@/components/layout/Layout'; 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 Departments = (): ReactElement => {
const tableRef = useRef<DepartmentsTableRef>(null);
const { canCreate } = usePermissions();
return ( return (
<Layout <Layout
currentPage="Departments" currentPage="Departments"
pageHeader={{ pageHeader={{
title: 'Department Management', title: 'Department Management',
description: 'View and manage all departments within your organization.', description: 'View and manage all departments within your organization.',
action: canCreate("departments") ? (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => tableRef.current?.openNewModal()}
>
<Plus className="w-4 h-4" />
<span>New Department</span>
</PrimaryButton>
) : null,
}} }}
> >
<DepartmentsTable /> <DepartmentsTable ref={tableRef} />
</Layout> </Layout>
); );
}; };