Qassure-frontend/src/components/superadmin/DesignationsTable.tsx

344 lines
10 KiB
TypeScript

import { useState, useEffect, type ReactElement } from "react";
import { useSelector } from "react-redux";
import {
PrimaryButton,
StatusBadge,
ActionDropdown,
DataTable,
Pagination,
FilterDropdown,
DeleteConfirmationModal,
type Column,
} from "@/components/shared";
import {
NewDesignationModal,
EditDesignationModal,
ViewDesignationModal,
} from "@/components/shared/DesignationModals";
import { Plus, Search } from "lucide-react";
import { designationService } from "@/services/designation-service";
import type {
Designation,
CreateDesignationRequest,
UpdateDesignationRequest,
} from "@/types/designation";
import { showToast } from "@/utils/toast";
import type { RootState } from "@/store/store";
interface DesignationsTableProps {
tenantId?: string | null; // If provided, use this tenantId (Super Admin mode)
compact?: boolean; // Compact mode for tabs
showHeader?: boolean;
}
const DesignationsTable = ({
tenantId: propsTenantId,
compact = false,
showHeader = true,
}: DesignationsTableProps): ReactElement => {
const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId);
const effectiveTenantId = propsTenantId || reduxTenantId;
const [designations, setDesignations] = useState<Designation[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Pagination state
const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(compact ? 10 : 5);
// Filter state
const [activeOnly, setActiveOnly] = useState<boolean>(false);
const [searchQuery, setSearchQuery] = useState<string>("");
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState<string>("");
// Modal states
const [isNewModalOpen, setIsNewModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedDesignation, setSelectedDesignation] =
useState<Designation | null>(null);
const [isActionLoading, setIsActionLoading] = useState(false);
const fetchDesignations = async () => {
try {
setIsLoading(true);
setError(null);
const response = await designationService.list(effectiveTenantId, {
active_only: activeOnly,
search: debouncedSearchQuery,
});
if (response.success) {
setDesignations(response.data);
} else {
setError("Failed to load designations");
}
} catch (err: any) {
setError(
err?.response?.data?.error?.message || "Failed to load designations",
);
} finally {
setIsLoading(false);
}
};
// Debouncing search query
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
}, 500);
return () => clearTimeout(timer);
}, [searchQuery]);
useEffect(() => {
fetchDesignations();
}, [effectiveTenantId, activeOnly, debouncedSearchQuery]);
const handleCreate = async (data: CreateDesignationRequest) => {
try {
setIsActionLoading(true);
const response = await designationService.create(data, effectiveTenantId);
if (response.success) {
showToast.success("Designation created successfully");
setIsNewModalOpen(false);
fetchDesignations();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message || "Failed to create designation",
);
} finally {
setIsActionLoading(false);
}
};
const handleUpdate = async (id: string, data: UpdateDesignationRequest) => {
try {
setIsActionLoading(true);
const response = await designationService.update(
id,
data,
effectiveTenantId,
);
if (response.success) {
showToast.success("Designation updated successfully");
setIsEditModalOpen(false);
fetchDesignations();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message || "Failed to update designation",
);
} finally {
setIsActionLoading(false);
}
};
const handleDelete = async () => {
if (!selectedDesignation) return;
try {
setIsActionLoading(true);
const response = await designationService.delete(
selectedDesignation.id,
effectiveTenantId,
);
if (response.success) {
showToast.success("Designation deleted successfully");
setIsDeleteModalOpen(false);
fetchDesignations();
}
} catch (err: any) {
showToast.error(
err?.response?.data?.error?.message || "Failed to delete designation",
);
} finally {
setIsActionLoading(false);
}
};
// Client-side pagination logic
const totalItems = designations.length;
const totalPages = Math.ceil(totalItems / limit);
const paginatedData = designations.slice(
(currentPage - 1) * limit,
currentPage * limit,
);
const columns: Column<Designation>[] = [
{
key: "name",
label: "Designation Name",
render: (desig) => (
<span className="text-sm font-medium text-[#0f1724]">{desig.name}</span>
),
},
{
key: "code",
label: "Code",
render: (desig) => (
<span className="text-sm text-[#6b7280]">{desig.code}</span>
),
},
{
key: "level",
label: "Level",
render: (desig) => (
<span className="text-sm text-[#6b7280]">{desig.level}</span>
),
},
{
key: "sort_order",
label: "Order",
render: (desig) => (
<span className="text-sm text-[#6b7280]">{desig.sort_order}</span>
),
},
{
key: "user_count",
label: "Users",
render: (desig) => (
<span className="text-sm text-[#6b7280]">{desig.user_count || 0}</span>
),
},
{
key: "status",
label: "Status",
render: (desig) => (
<StatusBadge variant={desig.is_active ? "success" : "failure"}>
{desig.is_active ? "Active" : "Inactive"}
</StatusBadge>
),
},
{
key: "actions",
label: "Actions",
align: "right",
render: (desig) => (
<div className="flex justify-end">
<ActionDropdown
onView={() => {
setSelectedDesignation(desig);
setIsViewModalOpen(true);
}}
onEdit={() => {
setSelectedDesignation(desig);
setIsEditModalOpen(true);
}}
onDelete={() => {
setSelectedDesignation(desig);
setIsDeleteModalOpen(true);
}}
/>
</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="p-4 border-b border-[rgba(0,0,0,0.08)] flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3 w-full sm:w-auto">
<div className="relative flex-1 sm:w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#9aa6b2]" />
<input
type="text"
placeholder="Search designations..."
className="w-full pl-9 pr-4 py-2 bg-[#f5f7fa] border border-[rgba(0,0,0,0.08)] rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-[#0052cc]"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<FilterDropdown
label="Status"
options={[
{ value: "all", label: "All Status" },
{ value: "active", label: "Active Only" },
]}
value={activeOnly ? "active" : "all"}
onChange={(value) => setActiveOnly(value === "active")}
/>
</div>
<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 Designation</span>
</PrimaryButton>
</div>
)}
<DataTable
data={paginatedData}
columns={columns}
keyExtractor={(desig) => desig.id}
isLoading={isLoading}
error={error}
emptyMessage="No designations found"
/>
{totalItems > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
limit={limit}
onPageChange={setCurrentPage}
onLimitChange={(newLimit) => {
setLimit(newLimit);
setCurrentPage(1);
}}
/>
)}
<NewDesignationModal
isOpen={isNewModalOpen}
onClose={() => setIsNewModalOpen(false)}
onSubmit={handleCreate}
isLoading={isActionLoading}
/>
<EditDesignationModal
isOpen={isEditModalOpen}
onClose={() => {
setIsEditModalOpen(false);
setSelectedDesignation(null);
}}
designation={selectedDesignation}
onSubmit={handleUpdate}
isLoading={isActionLoading}
/>
<ViewDesignationModal
isOpen={isViewModalOpen}
onClose={() => {
setIsViewModalOpen(false);
setSelectedDesignation(null);
}}
designation={selectedDesignation}
/>
<DeleteConfirmationModal
isOpen={isDeleteModalOpen}
onClose={() => {
setIsDeleteModalOpen(false);
setSelectedDesignation(null);
}}
onConfirm={handleDelete}
title="Delete Designation"
message="Are you sure you want to delete this designation? This action cannot be undone."
itemName={selectedDesignation?.name || ""}
isLoading={isActionLoading}
/>
</div>
);
};
export default DesignationsTable;