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

1221 lines
38 KiB
TypeScript

import {
useState,
useEffect,
type ReactElement,
forwardRef,
useImperativeHandle,
useMemo,
useRef,
} from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Plus, ArrowUpDown, Loader2 } from "lucide-react";
import {
PrimaryButton,
SecondaryButton,
ActionDropdown,
DataTable,
Pagination,
FilterDropdown,
SearchBox,
Modal,
FormField,
FormTextArea,
// DeleteConfirmationModal,
type Column,
} from "@/components/shared";
import { roleService } from "@/services/role-service";
import type { Role, CreateRoleRequest, UpdateRoleRequest } from "@/types/role";
import { showToast } from "@/utils/toast";
import { formatDate } from "@/utils/format-date";
import { useAppSelector } from "@/hooks/redux-hooks";
import CodeBadge from "@/components/shared/CodeBadge";
import { usePermissions } from "@/hooks/usePermissions";
// Utility function to generate code from name
const generateCodeFromName = (name: string): string => {
return name
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s]/g, "") // Remove special characters except spaces
.replace(/\s+/g, "_") // Replace spaces with underscores
.replace(/_+/g, "_") // Replace multiple underscores with single underscore
.replace(/^_|_$/g, ""); // Remove leading/trailing underscores
};
// All available resources
const ALL_RESOURCES = [
"users",
"roles",
"tenants",
"modules",
// "permissions",
// "projects",
"document",
// "audit",
"security",
"workflow",
// "training",
// "capa",
"supplier",
// "reports",
"notifications",
"files",
"settings",
"audit_logs",
"audit_resources",
// "qms_connections",
// "qms_sync_jobs",
// "qms_sync_conflicts",
// "qms_entity_mappings",
"ai",
// "qms",
];
// All available actions
const ALL_ACTIONS = ["create", "read", "update", "delete"];
// Resources that should not be visible or managed on the platform roles UI
const EXCLUDED_RESOURCES = new Set([
"usage",
"licenses",
"pricing_plans",
"pricing-plans",
"pricing plans",
"invoices",
"billing",
"payments",
]);
// Validation schema for creation
const newPlatformRoleSchema = z.object({
name: z.string().min(1, "Role name is required"),
code: z
.string()
.min(1, "Code is required")
.regex(
/^[a-z]+(_[a-z]+)*$/,
"Code must be lowercase and use '_' for separation (e.g. abc_def)",
),
description: z.string().min(1, "Description is required"),
permissions: z
.array(
z.object({
resource: z.string(),
action: z.string(),
}),
)
.optional()
.nullable(),
});
type NewPlatformRoleFormData = z.infer<typeof newPlatformRoleSchema>;
// Validation schema for update
const editPlatformRoleSchema = z.object({
name: z.string().min(1, "Role name is required"),
code: z
.string()
.min(1, "Code is required")
.regex(
/^[a-z]+(_[a-z]+)*$/,
"Code must be lowercase and use '_' for separation (e.g. abc_def)",
),
description: z.string().min(1, "Description is required"),
permissions: z
.array(
z.object({
resource: z.string(),
action: z.string(),
}),
)
.optional()
.nullable(),
});
type EditPlatformRoleFormData = z.infer<typeof editPlatformRoleSchema>;
// Dedicated Creation Modal
interface NewPlatformRoleModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (data: CreateRoleRequest) => Promise<void>;
isLoading: boolean;
}
const NewPlatformRoleModal = ({
isOpen,
onClose,
onSubmit,
isLoading = false,
}: NewPlatformRoleModalProps): ReactElement | null => {
const permissions = useAppSelector((state) => state.auth.permissions);
const [selectedPermissions, setSelectedPermissions] = useState<
Array<{ resource: string; action: string }>
>([]);
const {
register,
handleSubmit,
setValue,
watch,
reset,
setError,
clearErrors,
formState: { errors },
} = useForm<NewPlatformRoleFormData>({
resolver: zodResolver(newPlatformRoleSchema),
defaultValues: {
code: undefined,
permissions: [],
},
});
const nameValue = watch("name");
// Auto-generate code from name
useEffect(() => {
if (nameValue) {
const generatedCode = generateCodeFromName(nameValue);
setValue("code", generatedCode, { shouldValidate: true });
}
}, [nameValue, setValue]);
// Reset form when modal closes
useEffect(() => {
if (!isOpen) {
reset({
name: "",
code: undefined,
description: "",
permissions: [],
});
setSelectedPermissions([]);
clearErrors();
}
}, [isOpen, reset, clearErrors]);
// Build available resources and actions based on user permissions
const availableResourcesAndActions = useMemo(() => {
const resourceMap = new Map<string, Set<string>>();
permissions.forEach((perm) => {
const { resource, action } = perm;
if (EXCLUDED_RESOURCES.has(resource)) return;
if (resource === "*") {
ALL_RESOURCES.forEach((res) => {
if (EXCLUDED_RESOURCES.has(res)) return;
if (!resourceMap.has(res)) {
resourceMap.set(res, new Set());
}
const actions = resourceMap.get(res)!;
if (action === "*") {
ALL_ACTIONS.forEach((act) => actions.add(act));
} else {
actions.add(action);
}
});
} else {
if (!resourceMap.has(resource)) {
resourceMap.set(resource, new Set());
}
const actions = resourceMap.get(resource)!;
if (action === "*") {
ALL_ACTIONS.forEach((act) => actions.add(act));
} else {
actions.add(action);
}
}
});
return resourceMap;
}, [permissions]);
// Handle permission checkbox change
const handlePermissionChange = (
resource: string,
action: string,
checked: boolean,
) => {
setSelectedPermissions((prev) => {
const newPerms = [...prev];
if (checked) {
if (
!newPerms.some((p) => p.resource === resource && p.action === action)
) {
newPerms.push({ resource, action });
}
} else {
return newPerms.filter(
(p) => !(p.resource === resource && p.action === action),
);
}
return newPerms;
});
};
useEffect(() => {
setValue(
"permissions",
selectedPermissions.length > 0 ? selectedPermissions : [],
);
}, [selectedPermissions, setValue]);
const handleFormSubmit = async (
data: NewPlatformRoleFormData,
): Promise<void> => {
clearErrors();
try {
await onSubmit(data as CreateRoleRequest);
} catch (error: any) {
if (
error?.response?.data?.details &&
Array.isArray(error.response.data.details)
) {
const validationErrors = error.response.data.details;
validationErrors.forEach(
(detail: { path: string; message: string }) => {
if (
detail.path === "name" ||
detail.path === "code" ||
detail.path === "description" ||
detail.path === "permissions"
) {
setError(detail.path as keyof NewPlatformRoleFormData, {
type: "server",
message: detail.message,
});
}
},
);
} else {
const errorObj = error?.response?.data?.error;
const errorMessage =
(typeof errorObj === "object" &&
errorObj !== null &&
"message" in errorObj
? errorObj.message
: null) ||
(typeof errorObj === "string" ? errorObj : null) ||
error?.response?.data?.message ||
error?.message ||
"Failed to create platform role. Please try again.";
setError("root", {
type: "server",
message:
typeof errorMessage === "string"
? errorMessage
: "Failed to create platform role. Please try again.",
});
}
}
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="Create Platform Role"
description="Define a new platform-level role and its allowed permissions."
maxWidth="lg"
footer={
<>
<SecondaryButton
type="button"
onClick={onClose}
disabled={isLoading}
className="px-4 py-2.5 text-sm"
>
Cancel
</SecondaryButton>
<PrimaryButton
type="button"
onClick={handleSubmit(handleFormSubmit)}
disabled={isLoading}
size="default"
className="px-4 py-2.5 text-sm"
>
{isLoading ? "Creating..." : "Create Role"}
</PrimaryButton>
</>
}
>
<form
onSubmit={handleSubmit(handleFormSubmit)}
className="flex flex-col gap-0"
>
{errors.root && (
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
<p className="text-sm text-[#ef4444]">{errors.root.message}</p>
</div>
)}
<div className="grid grid-cols-2 gap-5 pb-4">
<FormField
label="Role Name"
required
placeholder="Enter Role Name"
error={errors.name?.message}
{...register("name")}
/>
<FormField
label="Role Code"
required
placeholder="Auto-generated from name"
error={errors.code?.message}
{...register("code")}
disabled
className="bg-[#f3f4f6] cursor-not-allowed"
/>
</div>
<FormTextArea
label="Description"
required
placeholder="Enter Role Description"
error={errors.description?.message}
{...register("description")}
rows={4}
/>
<div className="pb-4">
<div className="flex flex-col items-start gap-3 self-stretch p-4 rounded-[8px] border border-[#D1D5DB] bg-white">
<div className="flex flex-col items-start self-stretch">
<h3 className="text-[13px] font-semibold text-[#111827]">
Permissions
</h3>
<p className="text-[11px] text-[#6B7280]">
Select allowed actions for this role by platform service.
</p>
</div>
{errors.permissions && (
<p className="text-sm text-[#ef4444]">
{errors.permissions.message}
</p>
)}
<div className="w-full overflow-x-auto border border-[#E5E7EB] rounded-[8px]">
<table className="w-full border-collapse">
<thead>
<tr>
<th className="w-[204px] h-[53px] px-3 py-[10px] text-left text-[11px] font-medium uppercase text-[#6B7280] border-b border-[#D1D5DB] bg-[#F9F9F9]">
Platform Services
</th>
{["View", "Create", "Edit", "Delete"].map((action) => (
<th
key={action}
className="w-[120px] px-3 py-[10px] text-center text-[11px] font-medium text-[#6B7280] border-b border-[#D1D5DB] bg-[#F9F9F9]"
>
{action}
</th>
))}
</tr>
</thead>
<tbody>
{Array.from(availableResourcesAndActions.entries()).map(
([resource, actions]) => (
<tr key={resource}>
<td className="w-[204px] h-[53px] px-3 py-[19px] border-b border-[#E5E7EB] text-[14px] text-[#111827] bg-white capitalize">
{resource.replace(/_/g, " ")}
</td>
{["read", "create", "update", "delete"].map(
(action) => {
const isChecked = selectedPermissions.some((p) => {
if (
p.resource === resource &&
p.action === action
)
return true;
if (p.resource === "*" && p.action === action)
return true;
if (p.resource === resource && p.action === "*")
return true;
if (p.resource === "*" && p.action === "*")
return true;
return false;
});
const isAvailable = actions.has(action);
return (
<td
key={`${resource}-${action}`}
className="w-[120px] px-3 py-[10px] text-center border-b border-[#E5E7EB] bg-white"
>
<div className="flex justify-center items-center">
<input
type="checkbox"
disabled={!isAvailable}
checked={isAvailable ? isChecked : false}
onChange={(e) =>
handlePermissionChange(
resource,
action,
e.target.checked,
)
}
className="w-4 h-4 rounded border-[#D1D5DB] text-[#0F3CC9] focus:ring-[#0F3CC9] disabled:opacity-30 disabled:cursor-not-allowed"
/>
</div>
</td>
);
},
)}
</tr>
),
)}
</tbody>
</table>
</div>
</div>
</div>
</form>
</Modal>
);
};
// Dedicated Edit Modal
interface EditPlatformRoleModalProps {
isOpen: boolean;
onClose: () => void;
roleId: string | null;
onLoadRole: (id: string) => Promise<Role>;
onSubmit: (id: string, data: UpdateRoleRequest) => Promise<void>;
isLoading: boolean;
}
const EditPlatformRoleModal = ({
isOpen,
onClose,
roleId,
onLoadRole,
onSubmit,
isLoading = false,
}: EditPlatformRoleModalProps): ReactElement | null => {
const [isLoadingRole, setIsLoadingRole] = useState<boolean>(false);
const [loadError, setLoadError] = useState<string | null>(null);
const loadedRoleIdRef = useRef<string | null>(null);
const permissions = useAppSelector((state) => state.auth.permissions);
const [selectedPermissions, setSelectedPermissions] = useState<
Array<{ resource: string; action: string }>
>([]);
const {
register,
handleSubmit,
setValue,
reset,
setError,
clearErrors,
formState: { errors },
} = useForm<EditPlatformRoleFormData>({
resolver: zodResolver(editPlatformRoleSchema),
defaultValues: {
permissions: [],
},
});
// Build available resources and actions based on user permissions
const availableResourcesAndActions = useMemo(() => {
const resourceMap = new Map<string, Set<string>>();
permissions.forEach((perm) => {
const { resource, action } = perm;
if (EXCLUDED_RESOURCES.has(resource)) return;
if (resource === "*") {
ALL_RESOURCES.forEach((res) => {
if (EXCLUDED_RESOURCES.has(res)) return;
if (!resourceMap.has(res)) {
resourceMap.set(res, new Set());
}
const actions = resourceMap.get(res)!;
if (action === "*") {
ALL_ACTIONS.forEach((act) => actions.add(act));
} else {
actions.add(action);
}
});
} else {
if (!resourceMap.has(resource)) {
resourceMap.set(resource, new Set());
}
const actions = resourceMap.get(resource)!;
if (action === "*") {
ALL_ACTIONS.forEach((act) => actions.add(act));
} else {
actions.add(action);
}
}
});
return resourceMap;
}, [permissions]);
// Handle permission checkbox change
const handlePermissionChange = (
resource: string,
action: string,
checked: boolean,
) => {
setSelectedPermissions((prev) => {
const newPerms = [...prev];
if (checked) {
if (
!newPerms.some((p) => p.resource === resource && p.action === action)
) {
newPerms.push({ resource, action });
}
} else {
return newPerms.filter(
(p) => !(p.resource === resource && p.action === action),
);
}
return newPerms;
});
};
useEffect(() => {
setValue(
"permissions",
selectedPermissions.length > 0 ? selectedPermissions : [],
);
}, [selectedPermissions, setValue]);
// Load role details
useEffect(() => {
if (isOpen && roleId) {
if (loadedRoleIdRef.current !== roleId) {
const loadRoleData = async () => {
try {
setIsLoadingRole(true);
setLoadError(null);
clearErrors();
const role = await onLoadRole(roleId);
loadedRoleIdRef.current = roleId;
const rolePermissions = role.permissions || [];
setSelectedPermissions(rolePermissions);
setValue("permissions", rolePermissions);
reset({
name: role.name,
code: role.code,
description: role.description || "",
permissions: rolePermissions,
});
} catch (err: any) {
setLoadError(
err?.response?.data?.error?.message ||
"Failed to load role details",
);
} finally {
setIsLoadingRole(false);
}
};
loadRoleData();
}
} else if (!isOpen) {
loadedRoleIdRef.current = null;
setSelectedPermissions([]);
reset({
name: "",
code: "",
description: "",
permissions: [],
});
setLoadError(null);
clearErrors();
}
}, [isOpen, roleId, onLoadRole, reset, clearErrors, setValue]);
const handleFormSubmit = async (
data: EditPlatformRoleFormData,
): Promise<void> => {
if (!roleId) return;
clearErrors();
try {
const { code, ...rest } = data;
await onSubmit(roleId, rest as UpdateRoleRequest);
} catch (error: any) {
if (
error?.response?.data?.details &&
Array.isArray(error.response.data.details)
) {
const validationErrors = error.response.data.details;
validationErrors.forEach(
(detail: { path: string; message: string }) => {
if (
detail.path === "name" ||
detail.path === "code" ||
detail.path === "description" ||
detail.path === "permissions"
) {
setError(detail.path as keyof EditPlatformRoleFormData, {
type: "server",
message: detail.message,
});
}
},
);
} else {
const errorObj = error?.response?.data?.error;
const errorMessage =
(typeof errorObj === "object" &&
errorObj !== null &&
"message" in errorObj
? errorObj.message
: null) ||
(typeof errorObj === "string" ? errorObj : null) ||
error?.response?.data?.message ||
error?.message ||
"Failed to update role. Please try again.";
setError("root", {
type: "server",
message:
typeof errorMessage === "string"
? errorMessage
: "Failed to update role. Please try again.",
});
}
throw error;
}
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="Edit Platform Role"
description="Update platform role name, description, and permissions."
maxWidth="lg"
footer={
<>
<SecondaryButton
type="button"
onClick={onClose}
disabled={isLoading || isLoadingRole}
className="px-4 py-2.5 text-sm"
>
Cancel
</SecondaryButton>
<PrimaryButton
type="button"
onClick={handleSubmit(handleFormSubmit)}
disabled={isLoading || isLoadingRole}
size="default"
className="px-4 py-2.5 text-sm"
>
{isLoading ? "Updating..." : "Update Role"}
</PrimaryButton>
</>
}
>
{isLoadingRole && (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 text-[#112868] animate-spin" />
</div>
)}
{errors.root && (
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4 mx-5 mt-5">
<p className="text-sm text-[#ef4444]">{errors.root.message}</p>
</div>
)}
{loadError && (
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4 mx-5">
<p className="text-sm text-[#ef4444]">{loadError}</p>
</div>
)}
{!isLoadingRole && (
<form
onSubmit={handleSubmit(handleFormSubmit)}
className="flex flex-col gap-0"
>
<div className="grid grid-cols-2 gap-5 pb-4">
<FormField
label="Role Name"
required
placeholder="Enter Role Name"
error={errors.name?.message}
{...register("name")}
/>
<FormField
label="Role Code"
required
placeholder="Auto-generated from name"
error={errors.code?.message}
{...register("code")}
disabled
className="bg-[#f3f4f6] cursor-not-allowed"
/>
</div>
<FormTextArea
label="Description"
required
placeholder="Enter Role Description"
error={errors.description?.message}
{...register("description")}
rows={4}
/>
<div className="pb-4">
<div className="flex flex-col items-start gap-3 self-stretch p-4 rounded-[8px] border border-[#D1D5DB] bg-white">
<div className="flex flex-col items-start self-stretch">
<h3 className="text-[13px] font-semibold text-[#111827]">
Permissions
</h3>
<p className="text-[11px] text-[#6B7280]">
Select allowed actions for this role by platform service.
</p>
</div>
{errors.permissions && (
<p className="text-sm text-[#ef4444]">
{errors.permissions.message}
</p>
)}
<div className="w-full overflow-x-auto border border-[#E5E7EB] rounded-[8px]">
<table className="w-full border-collapse">
<thead>
<tr>
<th className="w-[204px] h-[53px] px-3 py-[19px] text-left text-[11px] font-medium uppercase text-[#6B7280] border-b border-[#D1D5DB] bg-[#F9F9F9]">
Platform Services
</th>
{["View", "Create", "Edit", "Delete"].map((action) => (
<th
key={action}
className="w-[120px] px-3 py-[10px] text-center text-[11px] font-medium text-[#6B7280] border-b border-[#D1D5DB] bg-[#F9F9F9]"
>
{action}
</th>
))}
</tr>
</thead>
<tbody>
{Array.from(availableResourcesAndActions.entries()).map(
([resource, actions]) => (
<tr key={resource}>
<td className="w-[204px] h-[53px] px-3 py-[19px] border-b border-[#E5E7EB] text-[14px] text-[#111827] bg-white capitalize">
{resource.replace(/_/g, " ")}
</td>
{["read", "create", "update", "delete"].map(
(action) => {
const isChecked = selectedPermissions.some(
(p) => {
if (
p.resource === resource &&
p.action === action
)
return true;
if (p.resource === "*" && p.action === action)
return true;
if (
p.resource === resource &&
p.action === "*"
)
return true;
if (p.resource === "*" && p.action === "*")
return true;
return false;
},
);
const isAvailable = actions.has(action);
return (
<td
key={`${resource}-${action}`}
className="w-[120px] px-3 py-[10px] text-center border-b border-[#E5E7EB] bg-white"
>
<div className="flex justify-center items-center">
<input
type="checkbox"
disabled={!isAvailable}
checked={isAvailable ? isChecked : false}
onChange={(e) =>
handlePermissionChange(
resource,
action,
e.target.checked,
)
}
className="w-4 h-4 rounded border-[#D1D5DB] text-[#0F3CC9] focus:ring-[#0F3CC9] disabled:opacity-30 disabled:cursor-not-allowed"
/>
</div>
</td>
);
},
)}
</tr>
),
)}
</tbody>
</table>
</div>
</div>
</div>
</form>
)}
</Modal>
);
};
export interface PlatformRolesTableRef {
openNewModal: () => void;
refresh: () => void;
}
export const PlatformRolesTable = forwardRef<PlatformRolesTableRef, {}>(
(_, ref): ReactElement => {
const tenantIdFromAuth = useAppSelector((state) => state.auth.tenantId);
const { canCreate, canUpdate } = usePermissions();
const [roles, setRoles] = useState<Role[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [isCreateOpen, setIsCreateOpen] = useState<boolean>(false);
const [isCreating, setIsCreating] = useState<boolean>(false);
// Pagination state
const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(10);
const [pagination, setPagination] = useState<{
page: number;
limit: number;
total: number;
totalPages: number;
hasMore: boolean;
}>({
page: 1,
limit: 10,
total: 0,
totalPages: 1,
hasMore: false,
});
// Filter and Search state
const [orderBy, setOrderBy] = useState<string[] | null>(null);
const [search, setSearch] = useState<string>("");
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
// Modal state
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
// const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
// const [selectedRoleName, setSelectedRoleName] = useState<string>("");
const [isUpdating, setIsUpdating] = useState<boolean>(false);
// const [isDeleting, setIsDeleting] = useState<boolean>(false);
// Expose methods to parent
useImperativeHandle(ref, () => ({
openNewModal: () => setIsCreateOpen(true),
refresh: () =>
fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch),
}));
const fetchPlatformRoles = async (
page: number,
itemsPerPage: number,
sortBy: string[] | null = null,
searchQuery: string | null = null,
) => {
try {
setIsLoading(true);
setError(null);
// Load roles filtered by scope=platform
const response = await roleService.getAll(
page,
itemsPerPage,
sortBy,
searchQuery,
"platform",
);
if (response.success) {
setRoles(response.data);
setPagination(response.pagination);
} else {
setError("Failed to load platform roles");
}
} catch (err: any) {
setError(
err?.response?.data?.error?.message ||
"Failed to load platform roles",
);
} finally {
setIsLoading(false);
}
};
// Debounce search
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearch(search);
if (search) setCurrentPage(1);
}, 500);
return () => clearTimeout(timer);
}, [search]);
useEffect(() => {
fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch);
}, [currentPage, limit, orderBy, debouncedSearch]);
const handleCreateRole = async (data: CreateRoleRequest) => {
try {
setIsCreating(true);
const payload = {
...data,
scope: "platform",
tenant_id: tenantIdFromAuth || "00000000-0000-0000-0000-000000000001",
};
const response = await roleService.create(payload);
showToast.success(
response.message || "Platform role created successfully",
);
setIsCreateOpen(false);
fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch);
} catch (err: any) {
throw err;
} finally {
setIsCreating(false);
}
};
const handleUpdateRole = async (id: string, data: UpdateRoleRequest) => {
try {
setIsUpdating(true);
const payload = {
...data,
scope: "platform",
tenant_id: tenantIdFromAuth || "00000000-0000-0000-0000-000000000001",
};
const response = await roleService.update(id, payload);
showToast.success(
response.message || "Platform role updated successfully",
);
setEditModalOpen(false);
setSelectedRoleId(null);
fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch);
} catch (err: any) {
throw err;
} finally {
setIsUpdating(false);
}
};
// const handleConfirmDelete = async () => {
// if (!selectedRoleId) return;
// try {
// setIsDeleting(true);
// await roleService.delete(selectedRoleId);
// showToast.success("Platform role deleted successfully");
// // setDeleteModalOpen(false);
// setSelectedRoleId(null);
// setSelectedRoleName("");
// fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch);
// } catch (err: any) {
// showToast.error(
// err?.response?.data?.error?.message ||
// "Failed to delete platform role",
// );
// } finally {
// setIsDeleting(false);
// }
// };
const loadRole = async (id: string): Promise<Role> => {
const response = await roleService.getById(id);
return response.data;
};
// Columns config
const columns: Column<Role>[] = [
{
key: "name",
label: "Role Name",
render: (role) => (
<span className="font-medium text-[#0f1724]">{role.name}</span>
),
},
{
key: "code",
label: "Role Code",
render: (role) => <CodeBadge label={role.code} />,
},
{
key: "description",
label: "Description",
render: (role) => (
<span className="text-sm text-[#4b5563] line-clamp-1 max-w-[400px]">
{role.description || "N/A"}
</span>
),
},
{
key: "user_count",
label: "Assigned Users",
render: (role) => (
<span className="text-sm text-[#0f1724]">{role.user_count || 0}</span>
),
},
{
key: "created_at",
label: "Created Date",
render: (role) => (
<span className="text-sm text-[#6b7280]">
{formatDate(role.created_at)}
</span>
),
},
{
key: "actions",
label: "Actions",
align: "right",
render: (role) => (
<div className="flex justify-end">
<ActionDropdown
onEdit={
canUpdate("roles")
? () => {
setSelectedRoleId(role.id);
setEditModalOpen(true);
}
: undefined
}
// onDelete={
// canDelete("roles")
// ? () => {
// setSelectedRoleId(role.id);
// setSelectedRoleName(role.name);
// setDeleteModalOpen(true);
// }
// : undefined
// }
/>
</div>
),
},
];
return (
<>
<div className="pb-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-3">
<SearchBox
value={search}
onChange={setSearch}
placeholder="Search platform roles..."
/>
<FilterDropdown
label="Sort by"
options={[
{ value: ["name", "asc"], label: "Name (A-Z)" },
{ value: ["name", "desc"], label: "Name (Z-A)" },
{ value: ["code", "asc"], label: "Code (A-Z)" },
{ value: ["code", "desc"], label: "Code (Z-A)" },
{ value: ["created_at", "asc"], label: "Created (Oldest)" },
{ value: ["created_at", "desc"], label: "Created (Newest)" },
]}
value={orderBy}
onChange={(value) => {
setOrderBy(value as string[] | null);
setCurrentPage(1);
}}
placeholder="Default"
showIcon
icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
/>
</div>
<div className="flex items-center gap-2">
{canCreate("roles") && (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => setIsCreateOpen(true)}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New Platform Role</span>
</PrimaryButton>
)}
</div>
</div>
<DataTable
data={roles}
columns={columns}
keyExtractor={(role) => role.id}
emptyMessage="No platform roles found"
isLoading={isLoading}
error={error}
/>
{pagination.total > 0 && (
<Pagination
currentPage={currentPage}
totalPages={pagination.totalPages}
totalItems={pagination.total}
limit={limit}
onPageChange={(page: number) => {
setCurrentPage(page);
}}
onLimitChange={(newLimit: number) => {
setLimit(newLimit);
setCurrentPage(1);
}}
/>
)}
<NewPlatformRoleModal
isOpen={isCreateOpen}
onClose={() => setIsCreateOpen(false)}
onSubmit={handleCreateRole}
isLoading={isCreating}
/>
<EditPlatformRoleModal
isOpen={editModalOpen}
onClose={() => {
setEditModalOpen(false);
setSelectedRoleId(null);
}}
roleId={selectedRoleId}
onLoadRole={loadRole}
onSubmit={handleUpdateRole}
isLoading={isUpdating}
/>
{/* <DeleteConfirmationModal
isOpen={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName("");
}}
onConfirm={handleConfirmDelete}
title="Delete Platform Role"
message="Are you sure you want to delete this platform role? This action cannot be undone."
itemName={selectedRoleName}
isLoading={isDeleting}
/> */}
</>
);
},
);
PlatformRolesTable.displayName = "PlatformRolesTable";