refactor: decouple DepartmentTable logic into separate list and tree view components
This commit is contained in:
parent
f87f552d52
commit
0510f15175
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user