refactor: standardize RolesTable component formatting and improve code readability

This commit is contained in:
Yashwin 2026-05-08 14:34:24 +05:30
parent f4838422c2
commit 2cfce21323
6 changed files with 893 additions and 551 deletions

View File

@ -3,7 +3,7 @@ import type { ReactElement } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Loader2, ChevronDown, ChevronRight } from "lucide-react";
import { Loader2 } from "lucide-react";
import {
Modal,
FormField,
@ -116,9 +116,9 @@ export const EditRoleModal = ({
const [selectedPermissions, setSelectedPermissions] = useState<
Array<{ resource: string; action: string }>
>([]);
const [expandedResources, setExpandedResources] = useState<Set<string>>(
new Set(),
);
// const [expandedResources, setExpandedResources] = useState<Set<string>>(
// new Set(),
// );
const {
register,
@ -185,38 +185,38 @@ export const EditRoleModal = ({
return resourceMap;
}, [permissions]);
// Check if a resource has any selected actions
const hasSelectedActions = (
resource: string,
actions: Set<string>,
): boolean => {
return Array.from(actions).some((action) => {
return selectedPermissions.some((p) => {
// Check for exact match
if (p.resource === resource && p.action === action) return true;
// Check for wildcard resource with exact action
if (p.resource === "*" && p.action === action) return true;
// Check for exact resource with wildcard action
if (p.resource === resource && p.action === "*") return true;
// Check for wildcard resource with wildcard action
if (p.resource === "*" && p.action === "*") return true;
return false;
});
});
};
// // Check if a resource has any selected actions
// const hasSelectedActions = (
// resource: string,
// actions: Set<string>,
// ): boolean => {
// return Array.from(actions).some((action) => {
// return selectedPermissions.some((p) => {
// // Check for exact match
// if (p.resource === resource && p.action === action) return true;
// // Check for wildcard resource with exact action
// if (p.resource === "*" && p.action === action) return true;
// // Check for exact resource with wildcard action
// if (p.resource === resource && p.action === "*") return true;
// // Check for wildcard resource with wildcard action
// if (p.resource === "*" && p.action === "*") return true;
// return false;
// });
// });
// };
// Toggle resource expansion
const toggleResource = (resource: string) => {
setExpandedResources((prev) => {
const newSet = new Set(prev);
if (newSet.has(resource)) {
newSet.delete(resource);
} else {
newSet.add(resource);
}
return newSet;
});
};
// // Toggle resource expansion
// const toggleResource = (resource: string) => {
// setExpandedResources((prev) => {
// const newSet = new Set(prev);
// if (newSet.has(resource)) {
// newSet.delete(resource);
// } else {
// newSet.add(resource);
// }
// return newSet;
// });
// };
// Handle permission checkbox change
const handlePermissionChange = (
@ -252,37 +252,37 @@ export const EditRoleModal = ({
}, [selectedPermissions, setValue]);
// Expand resources that have selected permissions when role is loaded
useEffect(() => {
if (
selectedPermissions.length > 0 &&
availableResourcesAndActions.size > 0
) {
const resourcesWithPermissions = new Set<string>();
selectedPermissions.forEach((perm) => {
if (perm.resource === "*") {
// If wildcard resource, expand all available resources
availableResourcesAndActions.forEach((_, resource) => {
resourcesWithPermissions.add(resource);
});
} else if (availableResourcesAndActions.has(perm.resource)) {
// Only expand if resource exists in available resources
resourcesWithPermissions.add(perm.resource);
}
});
// Only update if we have resources to expand and they're not already expanded
if (resourcesWithPermissions.size > 0) {
setExpandedResources((prev) => {
const newSet = new Set(prev);
resourcesWithPermissions.forEach((resource) => {
if (!newSet.has(resource)) {
newSet.add(resource);
}
});
return newSet;
});
}
}
}, [selectedPermissions, availableResourcesAndActions]);
// useEffect(() => {
// if (
// selectedPermissions.length > 0 &&
// availableResourcesAndActions.size > 0
// ) {
// const resourcesWithPermissions = new Set<string>();
// selectedPermissions.forEach((perm) => {
// if (perm.resource === "*") {
// // If wildcard resource, expand all available resources
// availableResourcesAndActions.forEach((_, resource) => {
// resourcesWithPermissions.add(resource);
// });
// } else if (availableResourcesAndActions.has(perm.resource)) {
// // Only expand if resource exists in available resources
// resourcesWithPermissions.add(perm.resource);
// }
// });
// // Only update if we have resources to expand and they're not already expanded
// if (resourcesWithPermissions.size > 0) {
// setExpandedResources((prev) => {
// const newSet = new Set(prev);
// resourcesWithPermissions.forEach((resource) => {
// if (!newSet.has(resource)) {
// newSet.add(resource);
// }
// });
// return newSet;
// });
// }
// }
// }, [selectedPermissions, availableResourcesAndActions]);
// Load role data when modal opens - only load once per roleId
useEffect(() => {
@ -505,6 +505,160 @@ export const EditRoleModal = ({
{/* Permissions Section */}
<div className="pb-4">
<div
className="
flex flex-col items-start gap-3 self-stretch
p-4 rounded-[8px]
border border-[#D1D5DB]
bg-white
"
>
{/* Header Section */}
<div className="flex flex-col items-start self-stretch">
<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 module.
</p>
</div>
</div>
{errors.permissions && (
<p className="text-sm text-[#ef4444]">
{errors.permissions.message}
</p>
)}
{/* Table */}
<div className="w-full overflow-x-auto border border-[#E5E7EB] rounded-[8px]">
<table className="w-full border-collapse">
{/* Table Header */}
<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]"
>
<div className="flex flex-col items-center gap-1">
<span>{action}</span>
</div>
</th>
))}
</tr>
</thead>
{/* Table Body */}
<tbody>
{Array.from(availableResourcesAndActions.entries()).map(
([resource, actions]) => (
<tr key={resource}>
{/* Resource Name */}
<td
className="
w-[204px]
h-[53px]
px-3 py-[19px]
border-b border-[#E5E7EB]
text-[14px]
text-[#111827]
bg-white
"
>
{resource.replace(/_/g, " ")}
</td>
{/* Action Columns */}
{[
"read",
"create",
"update",
"delete",
// "approve",
// "admin",
].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>
{/* <div className="pb-4">
<label className="flex items-center gap-1.5 text-[13px] font-medium text-[#0e1b2a] mb-3">
<span>Permissions</span>
</label>
@ -631,7 +785,7 @@ export const EditRoleModal = ({
</div>
)}
</div>
</div>
</div> */}
</form>
)}
</Modal>

View File

@ -3,7 +3,7 @@ import type { ReactElement } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { ChevronDown, ChevronRight } from "lucide-react";
// import { ChevronDown, ChevronRight } from "lucide-react";
import {
Modal,
FormField,
@ -108,9 +108,9 @@ export const NewRoleModal = ({
const [selectedPermissions, setSelectedPermissions] = useState<
Array<{ resource: string; action: string }>
>([]);
const [expandedResources, setExpandedResources] = useState<Set<string>>(
new Set(),
);
// const [expandedResources, setExpandedResources] = useState<Set<string>>(
// new Set(),
// );
const {
register,
@ -193,37 +193,37 @@ export const NewRoleModal = ({
}, [permissions]);
// Check if a resource has any selected actions
const hasSelectedActions = (
resource: string,
actions: Set<string>,
): boolean => {
return Array.from(actions).some((action) => {
return selectedPermissions.some((p) => {
// Check for exact match
if (p.resource === resource && p.action === action) return true;
// Check for wildcard resource with exact action
if (p.resource === "*" && p.action === action) return true;
// Check for exact resource with wildcard action
if (p.resource === resource && p.action === "*") return true;
// Check for wildcard resource with wildcard action
if (p.resource === "*" && p.action === "*") return true;
return false;
});
});
};
// const hasSelectedActions = (
// resource: string,
// actions: Set<string>,
// ): boolean => {
// return Array.from(actions).some((action) => {
// return selectedPermissions.some((p) => {
// // Check for exact match
// if (p.resource === resource && p.action === action) return true;
// // Check for wildcard resource with exact action
// if (p.resource === "*" && p.action === action) return true;
// // Check for exact resource with wildcard action
// if (p.resource === resource && p.action === "*") return true;
// // Check for wildcard resource with wildcard action
// if (p.resource === "*" && p.action === "*") return true;
// return false;
// });
// });
// };
// Toggle resource expansion
const toggleResource = (resource: string) => {
setExpandedResources((prev) => {
const newSet = new Set(prev);
if (newSet.has(resource)) {
newSet.delete(resource);
} else {
newSet.add(resource);
}
return newSet;
});
};
// // Toggle resource expansion
// const toggleResource = (resource: string) => {
// setExpandedResources((prev) => {
// const newSet = new Set(prev);
// if (newSet.has(resource)) {
// newSet.delete(resource);
// } else {
// newSet.add(resource);
// }
// return newSet;
// });
// };
// Handle permission checkbox change
const handlePermissionChange = (
@ -397,6 +397,157 @@ export const NewRoleModal = ({
{/* Permissions Section */}
<div className="pb-4">
<div
className="
flex flex-col items-start gap-3 self-stretch
p-4 rounded-[8px]
border border-[#D1D5DB]
bg-white
"
>
{/* Header Section */}
<div className="flex flex-col items-start self-stretch">
<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 module.
</p>
</div>
</div>
{errors.permissions && (
<p className="text-sm text-[#ef4444]">
{errors.permissions.message}
</p>
)}
{/* Table */}
<div className="w-full overflow-x-auto border border-[#E5E7EB] rounded-[8px]">
<table className="w-full border-collapse">
{/* Table Header */}
<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]"
>
<div className="flex flex-col items-center gap-1">
<span>{action}</span>
</div>
</th>
))}
</tr>
</thead>
{/* Table Body */}
<tbody>
{Array.from(availableResourcesAndActions.entries()).map(
([resource, actions]) => (
<tr key={resource}>
{/* Resource Name */}
<td
className="
w-[204px]
h-[53px]
px-3 py-[19px]
border-b border-[#E5E7EB]
text-[14px]
text-[#111827]
bg-white
"
>
{resource.replace(/_/g, " ")}
</td>
{/* Action Columns */}
{[
"read",
"create",
"update",
"delete",
// "approve",
// "admin",
].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>
{/* <div className="pb-4">
<label className="flex items-center gap-1.5 text-[13px] font-medium text-[#0e1b2a] mb-3">
<span>Permissions</span>
</label>
@ -511,7 +662,7 @@ export const NewRoleModal = ({
</div>
)}
</div>
</div>
</div> */}
</form>
</Modal>
);

View File

@ -1,4 +1,10 @@
import { useState, useEffect, type ReactElement, forwardRef, useImperativeHandle } from "react";
import {
useState,
useEffect,
type ReactElement,
forwardRef,
useImperativeHandle,
} from "react";
import {
PrimaryButton,
StatusBadge,
@ -47,233 +53,275 @@ interface RolesTableProps {
compact?: boolean; // Compact mode for tabs (default: false)
}
export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(({
tenantId,
showHeader = true,
compact = false,
}, ref): ReactElement => {
// const { primaryColor } = useAppTheme();
const { canCreate, canUpdate, canDelete } = usePermissions();
const [roles, setRoles] = useState<Role[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [isCreating, setIsCreating] = useState<boolean>(false);
export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
({ tenantId, showHeader = true, compact = false }, ref): ReactElement => {
// const { primaryColor } = useAppTheme();
const { canCreate, canUpdate, canDelete } = usePermissions();
const [roles, setRoles] = useState<Role[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [isCreating, setIsCreating] = useState<boolean>(false);
// Expose imperative methods
useImperativeHandle(ref, () => ({
openNewModal: () => setIsModalOpen(true),
refresh: () => fetchRoles(currentPage, limit, orderBy, debouncedSearch),
}));
// Expose imperative methods
useImperativeHandle(ref, () => ({
openNewModal: () => setIsModalOpen(true),
refresh: () => fetchRoles(currentPage, limit, orderBy, debouncedSearch),
}));
// Pagination state
const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(5);
const [pagination, setPagination] = useState<{
page: number;
limit: number;
total: number;
totalPages: number;
hasMore: boolean;
}>({
page: 1,
limit: 5,
total: 0,
totalPages: 1,
hasMore: false,
});
// Pagination state
const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(5);
const [pagination, setPagination] = useState<{
page: number;
limit: number;
total: number;
totalPages: number;
hasMore: boolean;
}>({
page: 1,
limit: 5,
total: 0,
totalPages: 1,
hasMore: false,
});
// Filter state
// const [scopeFilter, setScopeFilter] = useState<string | null>(null);
const [orderBy, setOrderBy] = useState<string[] | null>(null);
// Filter state
// const [scopeFilter, setScopeFilter] = useState<string | null>(null);
const [orderBy, setOrderBy] = useState<string[] | null>(null);
// Search state
const [search, setSearch] = useState<string>("");
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
// Search state
const [search, setSearch] = useState<string>("");
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
// View, Edit, Delete modals
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
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);
// View, Edit, Delete modals
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
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);
const fetchRoles = async (
page: number,
itemsPerPage: number,
// scope: string | null = null,
sortBy: string[] | null = null,
searchQuery: string | null = null,
): Promise<void> => {
try {
setIsLoading(true);
setError(null);
const response = tenantId
? await roleService.getByTenant(tenantId, page, itemsPerPage, sortBy, searchQuery)
: await roleService.getAll(page, itemsPerPage, sortBy, searchQuery);
if (response.success) {
setRoles(response.data);
setPagination(response.pagination);
} else {
setError("Failed to load roles");
const fetchRoles = async (
page: number,
itemsPerPage: number,
// scope: string | null = null,
sortBy: string[] | null = null,
searchQuery: string | null = null,
): Promise<void> => {
try {
setIsLoading(true);
setError(null);
const response = tenantId
? await roleService.getByTenant(
tenantId,
page,
itemsPerPage,
sortBy,
searchQuery,
)
: await roleService.getAll(page, itemsPerPage, sortBy, searchQuery);
if (response.success) {
setRoles(response.data);
setPagination(response.pagination);
} else {
setError("Failed to load roles");
}
} catch (err: any) {
setError(err?.response?.data?.error?.message || "Failed to load roles");
} finally {
setIsLoading(false);
}
} catch (err: any) {
setError(err?.response?.data?.error?.message || "Failed to load roles");
} finally {
setIsLoading(false);
}
};
};
// Handle search debouncing
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearch(search);
if (search) setCurrentPage(1);
}, 500);
return () => clearTimeout(timer);
}, [search]);
// Handle search debouncing
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearch(search);
if (search) setCurrentPage(1);
}, 500);
return () => clearTimeout(timer);
}, [search]);
useEffect(() => {
fetchRoles(currentPage, limit, orderBy, debouncedSearch);
}, [currentPage, limit, orderBy, debouncedSearch, tenantId]);
useEffect(() => {
fetchRoles(currentPage, limit, orderBy, debouncedSearch);
}, [currentPage, limit, orderBy, debouncedSearch, tenantId]);
const handleCreateRole = async (data: CreateRoleRequest): Promise<void> => {
try {
setIsCreating(true);
const response = await roleService.create(data);
const message = response.message || `Role created successfully`;
const description = response.message ? undefined : `${data.name} has been added`;
showToast.success(message, description);
setIsModalOpen(false);
await fetchRoles(currentPage, limit, orderBy);
} catch (err: any) {
throw err;
} finally {
setIsCreating(false);
}
};
const handleCreateRole = async (data: CreateRoleRequest): Promise<void> => {
try {
setIsCreating(true);
const response = await roleService.create(data);
const message = response.message || `Role created successfully`;
const description = response.message
? undefined
: `${data.name} has been added`;
showToast.success(message, description);
setIsModalOpen(false);
await fetchRoles(currentPage, limit, orderBy);
} catch (err: any) {
throw err;
} finally {
setIsCreating(false);
}
};
// View role handler
const handleViewRole = (roleId: string): void => {
setSelectedRoleId(roleId);
setViewModalOpen(true);
};
// View role handler
const handleViewRole = (roleId: string): void => {
setSelectedRoleId(roleId);
setViewModalOpen(true);
};
// Edit role handler
const handleEditRole = (roleId: string, roleName: string): void => {
setSelectedRoleId(roleId);
setSelectedRoleName(roleName);
setEditModalOpen(true);
};
// Edit role handler
const handleEditRole = (roleId: string, roleName: string): void => {
setSelectedRoleId(roleId);
setSelectedRoleName(roleName);
setEditModalOpen(true);
};
// Update role handler
const handleUpdateRole = async (id: string, data: UpdateRoleRequest): Promise<void> => {
try {
setIsUpdating(true);
const response = await roleService.update(id, data);
const message = response.message || `Role updated successfully`;
const description = response.message ? undefined : `${data.name} has been updated`;
showToast.success(message, description);
setEditModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName('');
await fetchRoles(currentPage, limit, orderBy);
} catch (err: any) {
throw err;
} finally {
setIsUpdating(false);
}
};
// Update role handler
const handleUpdateRole = async (
id: string,
data: UpdateRoleRequest,
): Promise<void> => {
try {
setIsUpdating(true);
const response = await roleService.update(id, data);
const message = response.message || `Role updated successfully`;
const description = response.message
? undefined
: `${data.name} has been updated`;
showToast.success(message, description);
setEditModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName("");
await fetchRoles(currentPage, limit, orderBy);
} catch (err: any) {
throw err;
} finally {
setIsUpdating(false);
}
};
// Delete role handler
const handleDeleteRole = (roleId: string, roleName: string): void => {
setSelectedRoleId(roleId);
setSelectedRoleName(roleName);
setDeleteModalOpen(true);
};
// Delete role handler
const handleDeleteRole = (roleId: string, roleName: string): void => {
setSelectedRoleId(roleId);
setSelectedRoleName(roleName);
setDeleteModalOpen(true);
};
// Confirm delete handler
const handleConfirmDelete = async (): Promise<void> => {
if (!selectedRoleId) return;
// Confirm delete handler
const handleConfirmDelete = async (): Promise<void> => {
if (!selectedRoleId) return;
try {
setIsDeleting(true);
await roleService.delete(selectedRoleId);
setDeleteModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName('');
await fetchRoles(currentPage, limit, orderBy);
} catch (err: any) {
throw err; // Let the modal handle the error display
} finally {
setIsDeleting(false);
}
};
try {
setIsDeleting(true);
await roleService.delete(selectedRoleId);
setDeleteModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName("");
await fetchRoles(currentPage, limit, orderBy);
} catch (err: any) {
throw err; // Let the modal handle the error display
} finally {
setIsDeleting(false);
}
};
// Load role for view/edit
const loadRole = async (id: string): Promise<Role> => {
const response = await roleService.getById(id);
return response.data;
};
// Load role for view/edit
const loadRole = async (id: string): Promise<Role> => {
const response = await roleService.getById(id);
return response.data;
};
// Table columns
const columns: Column<Role>[] = [
{
key: "name",
label: "Name",
render: (role) => (
<span className="text-sm font-normal text-[#0f1724]">{role.name}</span>
),
},
{
key: "code",
label: "Code",
render: (role) => <CodeBadge label={role.code} />,
},
{
key: "scope",
label: "Scope",
render: (role) => (
<StatusBadge variant={getScopeVariant(role.scope)}>
{role.scope}
</StatusBadge>
),
},
{
key: "user_count",
label: "Users",
render: (role) => (
<span className="text-sm font-normal text-[#0f1724]">
{role.user_count || 0}
</span>
),
},
{
key: "description",
label: "Description",
render: (role) => (
<span className="text-sm font-normal text-[#6b7280]">
{role.description || "N/A"}
</span>
),
},
{
key: "created_at",
label: "Created Date",
render: (role) => (
<span className="text-sm font-normal text-[#6b7280]">
{formatDate(role.created_at)}
</span>
),
},
{
key: "actions",
label: "Actions",
align: "right",
render: (role) => (
<div className="flex justify-end">
// Table columns
const columns: Column<Role>[] = [
{
key: "name",
label: "Name",
render: (role) => (
<span className="text-sm font-normal text-[#0f1724]">
{role.name}
</span>
),
},
{
key: "code",
label: "Code",
render: (role) => <CodeBadge label={role.code} />,
},
{
key: "description",
label: "Description",
render: (role) => (
<span className="text-sm font-normal text-[#6b7280]">
{role.description || "N/A"}
</span>
),
},
{
key: "scope",
label: "Scope",
render: (role) => (
<StatusBadge variant={getScopeVariant(role.scope)}>
{role.scope}
</StatusBadge>
),
},
{
key: "user_count",
label: "Users",
render: (role) => (
<span className="text-sm font-normal text-[#0f1724]">
{role.user_count || 0}
</span>
),
},
{
key: "created_at",
label: "Created Date",
render: (role) => (
<span className="text-sm font-normal text-[#6b7280]">
{formatDate(role.created_at)}
</span>
),
},
{
key: "actions",
label: "Actions",
align: "right",
render: (role) => (
<div className="flex justify-end">
<ActionDropdown
onView={() => handleViewRole(role.id)}
onEdit={
canUpdate("roles")
? () => handleEditRole(role.id, role.name)
: undefined
}
onDelete={
canDelete("roles")
? () => handleDeleteRole(role.id, role.name)
: undefined
}
/>
</div>
),
},
];
// Mobile card renderer
const mobileCardRenderer = (role: Role) => (
<div className="p-4">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-[#0f1724] truncate">
{role.name}
</h3>
<p className="text-xs text-[#6b7280] mt-0.5 font-mono">
{role.code}
</p>
</div>
<ActionDropdown
onView={() => handleViewRole(role.id)}
onEdit={
@ -288,90 +336,214 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(({
}
/>
</div>
),
},
];
// Mobile card renderer
const mobileCardRenderer = (role: Role) => (
<div className="p-4">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-[#0f1724] truncate">
{role.name}
</h3>
<p className="text-xs text-[#6b7280] mt-0.5 font-mono">{role.code}</p>
</div>
<ActionDropdown
onView={() => handleViewRole(role.id)}
onEdit={
canUpdate("roles")
? () => handleEditRole(role.id, role.name)
: undefined
}
onDelete={
canDelete("roles")
? () => handleDeleteRole(role.id, role.name)
: undefined
}
/>
</div>
<div className="grid grid-cols-2 gap-3 text-xs">
<div>
<span className="text-[#9aa6b2]">Scope:</span>
<div className="mt-1">
<StatusBadge variant={getScopeVariant(role.scope)}>
{role.scope}
</StatusBadge>
</div>
</div>
<div>
<span className="text-[#9aa6b2]">Users:</span>
<p className="text-[#0f1724] font-normal mt-1">
{role.user_count || 0}
</p>
</div>
<div>
<span className="text-[#9aa6b2]">Created:</span>
<p className="text-[#0f1724] font-normal mt-1">
{formatDate(role.created_at)}
</p>
</div>
{role.description && (
<div className="col-span-2">
<span className="text-[#9aa6b2]">Description:</span>
<p className="text-[#0f1724] font-normal mt-1">{role.description}</p>
</div>
)}
</div>
</div>
);
if (compact) {
// Compact mode for tabs
return (
<>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4">
<h3 className="text-lg font-semibold text-[#0f1724]"></h3>
<div className="flex flex-wrap items-center gap-2">
<SearchBox
value={search}
onChange={setSearch}
placeholder="Search..."
/>
{canCreate("roles") && (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => setIsModalOpen(true)}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New Role</span>
</PrimaryButton>
)}
<div className="grid grid-cols-2 gap-3 text-xs">
<div>
<span className="text-[#9aa6b2]">Scope:</span>
<div className="mt-1">
<StatusBadge variant={getScopeVariant(role.scope)}>
{role.scope}
</StatusBadge>
</div>
</div>
<div>
<span className="text-[#9aa6b2]">Users:</span>
<p className="text-[#0f1724] font-normal mt-1">
{role.user_count || 0}
</p>
</div>
<div>
<span className="text-[#9aa6b2]">Created:</span>
<p className="text-[#0f1724] font-normal mt-1">
{formatDate(role.created_at)}
</p>
</div>
{role.description && (
<div className="col-span-2">
<span className="text-[#9aa6b2]">Description:</span>
<p className="text-[#0f1724] font-normal mt-1">
{role.description}
</p>
</div>
)}
</div>
</div>
);
if (compact) {
// Compact mode for tabs
return (
<>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4">
<h3 className="text-lg font-semibold text-[#0f1724]"></h3>
<div className="flex flex-wrap items-center gap-2">
<SearchBox
value={search}
onChange={setSearch}
placeholder="Search..."
/>
{canCreate("roles") && (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => setIsModalOpen(true)}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New Role</span>
</PrimaryButton>
)}
</div>
</div>
<DataTable
data={roles}
columns={columns}
keyExtractor={(role) => role.id}
mobileCardRenderer={mobileCardRenderer}
emptyMessage="No roles found"
isLoading={isLoading}
error={error}
/>
{pagination.totalPages > 0 && (
<Pagination
currentPage={currentPage}
totalPages={pagination.totalPages}
totalItems={pagination.total}
limit={limit}
onPageChange={(page: number) => {
setCurrentPage(page);
}}
onLimitChange={(newLimit: number) => {
setLimit(newLimit);
setCurrentPage(1);
}}
/>
)}
</div>
{/* Modals */}
<NewRoleModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSubmit={handleCreateRole}
isLoading={isCreating}
defaultTenantId={tenantId || undefined}
/>
<ViewRoleModal
isOpen={viewModalOpen}
onClose={() => {
setViewModalOpen(false);
setSelectedRoleId(null);
}}
roleId={selectedRoleId}
onLoadRole={loadRole}
/>
<EditRoleModal
isOpen={editModalOpen}
onClose={() => {
setEditModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName("");
}}
roleId={selectedRoleId}
onLoadRole={loadRole}
onSubmit={handleUpdateRole}
isLoading={isUpdating}
defaultTenantId={tenantId || undefined}
/>
<DeleteConfirmationModal
isOpen={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName("");
}}
onConfirm={handleConfirmDelete}
title="Delete Role"
message={`Are you sure you want to delete this role`}
itemName={selectedRoleName}
isLoading={isDeleting}
/>
</>
);
}
// Full mode with header
return (
<>
{/* Table Container */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
{/* Table Header with Filters */}
{showHeader && (
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
{/* Filters */}
<div className="flex flex-wrap items-center gap-3">
{/* Global Search */}
<SearchBox
value={search}
onChange={setSearch}
placeholder="Search by name or code..."
/>
{/* Sort Filter */}
<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: ["updated_at", "asc"], label: "Updated (Oldest)" },
{
value: ["updated_at", "desc"],
label: "Updated (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>
{/* Actions */}
<div className="flex items-center gap-2">
{/* Export Button */}
{/* <button
type="button"
className="flex items-center gap-1.5 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50 transition-colors cursor-pointer"
>
<Download className="w-3.5 h-3.5" />
<span>Export</span>
</button> */}
{/* New Role Button */}
{canCreate("roles") && (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => setIsModalOpen(true)}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New Role</span>
</PrimaryButton>
)}
</div>
</div>
)}
{/* Data Table */}
<DataTable
data={roles}
columns={columns}
@ -381,7 +553,9 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(({
isLoading={isLoading}
error={error}
/>
{pagination.totalPages > 0 && (
{/* Table Footer with Pagination */}
{pagination.total > 0 && (
<Pagination
currentPage={currentPage}
totalPages={pagination.totalPages}
@ -392,7 +566,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(({
}}
onLimitChange={(newLimit: number) => {
setLimit(newLimit);
setCurrentPage(1);
setCurrentPage(1); // Reset to first page when limit changes
}}
/>
)}
@ -446,150 +620,5 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(({
/>
</>
);
}
// Full mode with header
return (
<>
{/* Table Container */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
{/* Table Header with Filters */}
{showHeader && (
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
{/* Filters */}
<div className="flex flex-wrap items-center gap-3">
{/* Global Search */}
<SearchBox
value={search}
onChange={setSearch}
placeholder="Search by name or code..."
/>
{/* Sort Filter */}
<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: ["updated_at", "asc"], label: "Updated (Oldest)" },
{ value: ["updated_at", "desc"], label: "Updated (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>
{/* Actions */}
<div className="flex items-center gap-2">
{/* Export Button */}
{/* <button
type="button"
className="flex items-center gap-1.5 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50 transition-colors cursor-pointer"
>
<Download className="w-3.5 h-3.5" />
<span>Export</span>
</button> */}
{/* New Role Button */}
{canCreate("roles") && (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => setIsModalOpen(true)}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New Role</span>
</PrimaryButton>
)}
</div>
</div>
)}
{/* Data Table */}
<DataTable
data={roles}
columns={columns}
keyExtractor={(role) => role.id}
mobileCardRenderer={mobileCardRenderer}
emptyMessage="No roles found"
isLoading={isLoading}
error={error}
/>
{/* Table Footer with Pagination */}
{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); // Reset to first page when limit changes
}}
/>
)}
</div>
{/* Modals */}
<NewRoleModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSubmit={handleCreateRole}
isLoading={isCreating}
defaultTenantId={tenantId || undefined}
/>
<ViewRoleModal
isOpen={viewModalOpen}
onClose={() => {
setViewModalOpen(false);
setSelectedRoleId(null);
}}
roleId={selectedRoleId}
onLoadRole={loadRole}
/>
<EditRoleModal
isOpen={editModalOpen}
onClose={() => {
setEditModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName('');
}}
roleId={selectedRoleId}
onLoadRole={loadRole}
onSubmit={handleUpdateRole}
isLoading={isUpdating}
defaultTenantId={tenantId || undefined}
/>
<DeleteConfirmationModal
isOpen={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName('');
}}
onConfirm={handleConfirmDelete}
title="Delete Role"
message={`Are you sure you want to delete this role`}
itemName={selectedRoleName}
isLoading={isDeleting}
/>
</>
);
});
},
);

View File

@ -9,10 +9,10 @@ import {
} from "lucide-react";
import { useAppSelector } from "@/hooks/redux-hooks";
import type { QuickAction } from "@/types/dashboard";
import { useAppTheme } from "@/hooks/useAppTheme";
// import { useAppTheme } from "@/hooks/useAppTheme";
export const QuickActions = () => {
const { primaryColor } = useAppTheme();
// const { primaryColor } = useAppTheme();
const navigate = useNavigate();
const { roles, permissions } = useAppSelector((state) => state.auth);
@ -32,17 +32,20 @@ export const QuickActions = () => {
{
icon: Plus,
label: "New Tenant",
btncolor: "#4C89FA",
onClick: () => navigate("/tenants/create-wizard"),
},
{ icon: UserPlus, label: "Module", onClick: () => navigate("/modules") },
{ icon: UserPlus, label: "Module", btncolor: "#16C784", onClick: () => navigate("/modules") },
{
icon: Shield,
label: "Notification",
btncolor: "#FCA004",
onClick: () => navigate("/notifications"),
},
{
icon: Settings,
label: "Audit Logs",
btncolor: "#6B7280",
onClick: () => navigate("/audit-logs"),
},
];
@ -51,21 +54,25 @@ export const QuickActions = () => {
hasPermission("users", "create") && {
icon: UserPlus,
label: "New User",
btncolor: "#4C89FA",
onClick: () => navigate("/tenant/users"),
},
hasPermission("roles", "create") && {
icon: Shield,
label: "New Role",
btncolor: "#16C784",
onClick: () => navigate("/tenant/roles"),
},
hasPermission("departments", "create") && {
icon: Building2,
label: "New Dept",
btncolor: "#FCA004",
onClick: () => navigate("/tenant/departments"),
},
hasPermission("designations", "create") && {
icon: BadgeCheck,
label: "New Desig",
btncolor: "#6B7280",
onClick: () => navigate("/tenant/designations"),
},
].filter(Boolean) as QuickAction[];
@ -107,7 +114,7 @@ export const QuickActions = () => {
<div className="flex items-center justify-center">
<Icon
className="w-[20px] h-[20px]"
color={primaryColor}
color={action.btncolor}
strokeWidth={2}
/>
</div>

View File

@ -306,13 +306,13 @@ const LandingPage = (): ReactElement => {
</div>
</header>
<main className="flex-1 flex flex-col items-center px-6 py-16 md:py-24">
<main className="flex-1 flex flex-col items-center px-6 py-6 md:py-10">
{/* Welcome Section */}
<div className="text-center mb-16 md:mb-24 animate-in fade-in slide-in-from-bottom-4 duration-700">
<h1 className="text-4xl md:text-5xl font-bold text-[#0f1724] mb-4">
<div className="text-center mb-4 md:mb-12 animate-in fade-in slide-in-from-bottom-4 duration-700">
<h1 className="text-3xl font-bold text-[#0f1724] mb-4">
Welcome back, {getUserName()}
</h1>
<p className="text-[#64748b] text-lg max-w-2xl mx-auto leading-relaxed">
<p className="text-[#64748b] text-md max-w-2xl mx-auto leading-relaxed">
Select a module below to access your workspace. You can switch between modules anytime from the main navigation.
</p>
</div>

View File

@ -22,6 +22,7 @@ export interface QuickAction {
icon: LucideIcon;
label: string;
onClick: () => void;
btncolor: string;
}
export interface HealthMetric {