refactor: standardize RolesTable component formatting and improve code readability
This commit is contained in:
parent
f4838422c2
commit
2cfce21323
@ -3,7 +3,7 @@ import type { ReactElement } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Loader2, ChevronDown, ChevronRight } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
FormField,
|
FormField,
|
||||||
@ -116,9 +116,9 @@ export const EditRoleModal = ({
|
|||||||
const [selectedPermissions, setSelectedPermissions] = useState<
|
const [selectedPermissions, setSelectedPermissions] = useState<
|
||||||
Array<{ resource: string; action: string }>
|
Array<{ resource: string; action: string }>
|
||||||
>([]);
|
>([]);
|
||||||
const [expandedResources, setExpandedResources] = useState<Set<string>>(
|
// const [expandedResources, setExpandedResources] = useState<Set<string>>(
|
||||||
new Set(),
|
// new Set(),
|
||||||
);
|
// );
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@ -185,38 +185,38 @@ export const EditRoleModal = ({
|
|||||||
return resourceMap;
|
return resourceMap;
|
||||||
}, [permissions]);
|
}, [permissions]);
|
||||||
|
|
||||||
// Check if a resource has any selected actions
|
// // Check if a resource has any selected actions
|
||||||
const hasSelectedActions = (
|
// const hasSelectedActions = (
|
||||||
resource: string,
|
// resource: string,
|
||||||
actions: Set<string>,
|
// actions: Set<string>,
|
||||||
): boolean => {
|
// ): boolean => {
|
||||||
return Array.from(actions).some((action) => {
|
// return Array.from(actions).some((action) => {
|
||||||
return selectedPermissions.some((p) => {
|
// return selectedPermissions.some((p) => {
|
||||||
// Check for exact match
|
// // Check for exact match
|
||||||
if (p.resource === resource && p.action === action) return true;
|
// if (p.resource === resource && p.action === action) return true;
|
||||||
// Check for wildcard resource with exact action
|
// // Check for wildcard resource with exact action
|
||||||
if (p.resource === "*" && p.action === action) return true;
|
// if (p.resource === "*" && p.action === action) return true;
|
||||||
// Check for exact resource with wildcard action
|
// // Check for exact resource with wildcard action
|
||||||
if (p.resource === resource && p.action === "*") return true;
|
// if (p.resource === resource && p.action === "*") return true;
|
||||||
// Check for wildcard resource with wildcard action
|
// // Check for wildcard resource with wildcard action
|
||||||
if (p.resource === "*" && p.action === "*") return true;
|
// if (p.resource === "*" && p.action === "*") return true;
|
||||||
return false;
|
// return false;
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
// Toggle resource expansion
|
// // Toggle resource expansion
|
||||||
const toggleResource = (resource: string) => {
|
// const toggleResource = (resource: string) => {
|
||||||
setExpandedResources((prev) => {
|
// setExpandedResources((prev) => {
|
||||||
const newSet = new Set(prev);
|
// const newSet = new Set(prev);
|
||||||
if (newSet.has(resource)) {
|
// if (newSet.has(resource)) {
|
||||||
newSet.delete(resource);
|
// newSet.delete(resource);
|
||||||
} else {
|
// } else {
|
||||||
newSet.add(resource);
|
// newSet.add(resource);
|
||||||
}
|
// }
|
||||||
return newSet;
|
// return newSet;
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
// Handle permission checkbox change
|
// Handle permission checkbox change
|
||||||
const handlePermissionChange = (
|
const handlePermissionChange = (
|
||||||
@ -252,37 +252,37 @@ export const EditRoleModal = ({
|
|||||||
}, [selectedPermissions, setValue]);
|
}, [selectedPermissions, setValue]);
|
||||||
|
|
||||||
// Expand resources that have selected permissions when role is loaded
|
// Expand resources that have selected permissions when role is loaded
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (
|
// if (
|
||||||
selectedPermissions.length > 0 &&
|
// selectedPermissions.length > 0 &&
|
||||||
availableResourcesAndActions.size > 0
|
// availableResourcesAndActions.size > 0
|
||||||
) {
|
// ) {
|
||||||
const resourcesWithPermissions = new Set<string>();
|
// const resourcesWithPermissions = new Set<string>();
|
||||||
selectedPermissions.forEach((perm) => {
|
// selectedPermissions.forEach((perm) => {
|
||||||
if (perm.resource === "*") {
|
// if (perm.resource === "*") {
|
||||||
// If wildcard resource, expand all available resources
|
// // If wildcard resource, expand all available resources
|
||||||
availableResourcesAndActions.forEach((_, resource) => {
|
// availableResourcesAndActions.forEach((_, resource) => {
|
||||||
resourcesWithPermissions.add(resource);
|
// resourcesWithPermissions.add(resource);
|
||||||
});
|
// });
|
||||||
} else if (availableResourcesAndActions.has(perm.resource)) {
|
// } else if (availableResourcesAndActions.has(perm.resource)) {
|
||||||
// Only expand if resource exists in available resources
|
// // Only expand if resource exists in available resources
|
||||||
resourcesWithPermissions.add(perm.resource);
|
// resourcesWithPermissions.add(perm.resource);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
// Only update if we have resources to expand and they're not already expanded
|
// // Only update if we have resources to expand and they're not already expanded
|
||||||
if (resourcesWithPermissions.size > 0) {
|
// if (resourcesWithPermissions.size > 0) {
|
||||||
setExpandedResources((prev) => {
|
// setExpandedResources((prev) => {
|
||||||
const newSet = new Set(prev);
|
// const newSet = new Set(prev);
|
||||||
resourcesWithPermissions.forEach((resource) => {
|
// resourcesWithPermissions.forEach((resource) => {
|
||||||
if (!newSet.has(resource)) {
|
// if (!newSet.has(resource)) {
|
||||||
newSet.add(resource);
|
// newSet.add(resource);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
return newSet;
|
// return newSet;
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}, [selectedPermissions, availableResourcesAndActions]);
|
// }, [selectedPermissions, availableResourcesAndActions]);
|
||||||
|
|
||||||
// Load role data when modal opens - only load once per roleId
|
// Load role data when modal opens - only load once per roleId
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -505,6 +505,160 @@ export const EditRoleModal = ({
|
|||||||
|
|
||||||
{/* Permissions Section */}
|
{/* Permissions Section */}
|
||||||
<div className="pb-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
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{/* 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">
|
<label className="flex items-center gap-1.5 text-[13px] font-medium text-[#0e1b2a] mb-3">
|
||||||
<span>Permissions</span>
|
<span>Permissions</span>
|
||||||
</label>
|
</label>
|
||||||
@ -631,7 +785,7 @@ export const EditRoleModal = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import type { ReactElement } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
// import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
FormField,
|
FormField,
|
||||||
@ -108,9 +108,9 @@ export const NewRoleModal = ({
|
|||||||
const [selectedPermissions, setSelectedPermissions] = useState<
|
const [selectedPermissions, setSelectedPermissions] = useState<
|
||||||
Array<{ resource: string; action: string }>
|
Array<{ resource: string; action: string }>
|
||||||
>([]);
|
>([]);
|
||||||
const [expandedResources, setExpandedResources] = useState<Set<string>>(
|
// const [expandedResources, setExpandedResources] = useState<Set<string>>(
|
||||||
new Set(),
|
// new Set(),
|
||||||
);
|
// );
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@ -193,37 +193,37 @@ export const NewRoleModal = ({
|
|||||||
}, [permissions]);
|
}, [permissions]);
|
||||||
|
|
||||||
// Check if a resource has any selected actions
|
// Check if a resource has any selected actions
|
||||||
const hasSelectedActions = (
|
// const hasSelectedActions = (
|
||||||
resource: string,
|
// resource: string,
|
||||||
actions: Set<string>,
|
// actions: Set<string>,
|
||||||
): boolean => {
|
// ): boolean => {
|
||||||
return Array.from(actions).some((action) => {
|
// return Array.from(actions).some((action) => {
|
||||||
return selectedPermissions.some((p) => {
|
// return selectedPermissions.some((p) => {
|
||||||
// Check for exact match
|
// // Check for exact match
|
||||||
if (p.resource === resource && p.action === action) return true;
|
// if (p.resource === resource && p.action === action) return true;
|
||||||
// Check for wildcard resource with exact action
|
// // Check for wildcard resource with exact action
|
||||||
if (p.resource === "*" && p.action === action) return true;
|
// if (p.resource === "*" && p.action === action) return true;
|
||||||
// Check for exact resource with wildcard action
|
// // Check for exact resource with wildcard action
|
||||||
if (p.resource === resource && p.action === "*") return true;
|
// if (p.resource === resource && p.action === "*") return true;
|
||||||
// Check for wildcard resource with wildcard action
|
// // Check for wildcard resource with wildcard action
|
||||||
if (p.resource === "*" && p.action === "*") return true;
|
// if (p.resource === "*" && p.action === "*") return true;
|
||||||
return false;
|
// return false;
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
// Toggle resource expansion
|
// // Toggle resource expansion
|
||||||
const toggleResource = (resource: string) => {
|
// const toggleResource = (resource: string) => {
|
||||||
setExpandedResources((prev) => {
|
// setExpandedResources((prev) => {
|
||||||
const newSet = new Set(prev);
|
// const newSet = new Set(prev);
|
||||||
if (newSet.has(resource)) {
|
// if (newSet.has(resource)) {
|
||||||
newSet.delete(resource);
|
// newSet.delete(resource);
|
||||||
} else {
|
// } else {
|
||||||
newSet.add(resource);
|
// newSet.add(resource);
|
||||||
}
|
// }
|
||||||
return newSet;
|
// return newSet;
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
// Handle permission checkbox change
|
// Handle permission checkbox change
|
||||||
const handlePermissionChange = (
|
const handlePermissionChange = (
|
||||||
@ -397,6 +397,157 @@ export const NewRoleModal = ({
|
|||||||
|
|
||||||
{/* Permissions Section */}
|
{/* Permissions Section */}
|
||||||
<div className="pb-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
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{/* 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">
|
<label className="flex items-center gap-1.5 text-[13px] font-medium text-[#0e1b2a] mb-3">
|
||||||
<span>Permissions</span>
|
<span>Permissions</span>
|
||||||
</label>
|
</label>
|
||||||
@ -511,7 +662,7 @@ export const NewRoleModal = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import { useState, useEffect, type ReactElement, forwardRef, useImperativeHandle } from "react";
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
type ReactElement,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
@ -47,233 +53,275 @@ interface RolesTableProps {
|
|||||||
compact?: boolean; // Compact mode for tabs (default: false)
|
compact?: boolean; // Compact mode for tabs (default: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(({
|
export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
|
||||||
tenantId,
|
({ tenantId, showHeader = true, compact = false }, ref): ReactElement => {
|
||||||
showHeader = true,
|
// const { primaryColor } = useAppTheme();
|
||||||
compact = false,
|
const { canCreate, canUpdate, canDelete } = usePermissions();
|
||||||
}, ref): ReactElement => {
|
const [roles, setRoles] = useState<Role[]>([]);
|
||||||
// const { primaryColor } = useAppTheme();
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const { canCreate, canUpdate, canDelete } = usePermissions();
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [roles, setRoles] = useState<Role[]>([]);
|
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isCreating, setIsCreating] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
|
||||||
const [isCreating, setIsCreating] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// Expose imperative methods
|
// Expose imperative methods
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
openNewModal: () => setIsModalOpen(true),
|
openNewModal: () => setIsModalOpen(true),
|
||||||
refresh: () => fetchRoles(currentPage, limit, orderBy, debouncedSearch),
|
refresh: () => fetchRoles(currentPage, limit, orderBy, debouncedSearch),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
const [limit, setLimit] = useState<number>(5);
|
const [limit, setLimit] = useState<number>(5);
|
||||||
const [pagination, setPagination] = useState<{
|
const [pagination, setPagination] = useState<{
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
total: number;
|
total: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
}>({
|
}>({
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
total: 0,
|
total: 0,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
// const [scopeFilter, setScopeFilter] = useState<string | null>(null);
|
// const [scopeFilter, setScopeFilter] = useState<string | null>(null);
|
||||||
const [orderBy, setOrderBy] = useState<string[] | null>(null);
|
const [orderBy, setOrderBy] = useState<string[] | null>(null);
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
const [search, setSearch] = useState<string>("");
|
const [search, setSearch] = useState<string>("");
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
|
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
|
||||||
|
|
||||||
// View, Edit, Delete modals
|
// View, Edit, Delete modals
|
||||||
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
|
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
|
||||||
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
|
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
|
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
|
||||||
const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
|
const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
|
||||||
const [selectedRoleName, setSelectedRoleName] = useState<string>("");
|
const [selectedRoleName, setSelectedRoleName] = useState<string>("");
|
||||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||||
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||||
|
|
||||||
const fetchRoles = async (
|
const fetchRoles = async (
|
||||||
page: number,
|
page: number,
|
||||||
itemsPerPage: number,
|
itemsPerPage: number,
|
||||||
// scope: string | null = null,
|
// scope: string | null = null,
|
||||||
sortBy: string[] | null = null,
|
sortBy: string[] | null = null,
|
||||||
searchQuery: string | null = null,
|
searchQuery: string | null = null,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = tenantId
|
const response = tenantId
|
||||||
? await roleService.getByTenant(tenantId, page, itemsPerPage, sortBy, searchQuery)
|
? await roleService.getByTenant(
|
||||||
: await roleService.getAll(page, itemsPerPage, sortBy, searchQuery);
|
tenantId,
|
||||||
if (response.success) {
|
page,
|
||||||
setRoles(response.data);
|
itemsPerPage,
|
||||||
setPagination(response.pagination);
|
sortBy,
|
||||||
} else {
|
searchQuery,
|
||||||
setError("Failed to load roles");
|
)
|
||||||
|
: 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
|
// Handle search debouncing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setDebouncedSearch(search);
|
setDebouncedSearch(search);
|
||||||
if (search) setCurrentPage(1);
|
if (search) setCurrentPage(1);
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRoles(currentPage, limit, orderBy, debouncedSearch);
|
fetchRoles(currentPage, limit, orderBy, debouncedSearch);
|
||||||
}, [currentPage, limit, orderBy, debouncedSearch, tenantId]);
|
}, [currentPage, limit, orderBy, debouncedSearch, tenantId]);
|
||||||
|
|
||||||
const handleCreateRole = async (data: CreateRoleRequest): Promise<void> => {
|
const handleCreateRole = async (data: CreateRoleRequest): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
const response = await roleService.create(data);
|
const response = await roleService.create(data);
|
||||||
const message = response.message || `Role created successfully`;
|
const message = response.message || `Role created successfully`;
|
||||||
const description = response.message ? undefined : `${data.name} has been added`;
|
const description = response.message
|
||||||
showToast.success(message, description);
|
? undefined
|
||||||
setIsModalOpen(false);
|
: `${data.name} has been added`;
|
||||||
await fetchRoles(currentPage, limit, orderBy);
|
showToast.success(message, description);
|
||||||
} catch (err: any) {
|
setIsModalOpen(false);
|
||||||
throw err;
|
await fetchRoles(currentPage, limit, orderBy);
|
||||||
} finally {
|
} catch (err: any) {
|
||||||
setIsCreating(false);
|
throw err;
|
||||||
}
|
} finally {
|
||||||
};
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// View role handler
|
// View role handler
|
||||||
const handleViewRole = (roleId: string): void => {
|
const handleViewRole = (roleId: string): void => {
|
||||||
setSelectedRoleId(roleId);
|
setSelectedRoleId(roleId);
|
||||||
setViewModalOpen(true);
|
setViewModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Edit role handler
|
// Edit role handler
|
||||||
const handleEditRole = (roleId: string, roleName: string): void => {
|
const handleEditRole = (roleId: string, roleName: string): void => {
|
||||||
setSelectedRoleId(roleId);
|
setSelectedRoleId(roleId);
|
||||||
setSelectedRoleName(roleName);
|
setSelectedRoleName(roleName);
|
||||||
setEditModalOpen(true);
|
setEditModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update role handler
|
// Update role handler
|
||||||
const handleUpdateRole = async (id: string, data: UpdateRoleRequest): Promise<void> => {
|
const handleUpdateRole = async (
|
||||||
try {
|
id: string,
|
||||||
setIsUpdating(true);
|
data: UpdateRoleRequest,
|
||||||
const response = await roleService.update(id, data);
|
): Promise<void> => {
|
||||||
const message = response.message || `Role updated successfully`;
|
try {
|
||||||
const description = response.message ? undefined : `${data.name} has been updated`;
|
setIsUpdating(true);
|
||||||
showToast.success(message, description);
|
const response = await roleService.update(id, data);
|
||||||
setEditModalOpen(false);
|
const message = response.message || `Role updated successfully`;
|
||||||
setSelectedRoleId(null);
|
const description = response.message
|
||||||
setSelectedRoleName('');
|
? undefined
|
||||||
await fetchRoles(currentPage, limit, orderBy);
|
: `${data.name} has been updated`;
|
||||||
} catch (err: any) {
|
showToast.success(message, description);
|
||||||
throw err;
|
setEditModalOpen(false);
|
||||||
} finally {
|
setSelectedRoleId(null);
|
||||||
setIsUpdating(false);
|
setSelectedRoleName("");
|
||||||
}
|
await fetchRoles(currentPage, limit, orderBy);
|
||||||
};
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Delete role handler
|
// Delete role handler
|
||||||
const handleDeleteRole = (roleId: string, roleName: string): void => {
|
const handleDeleteRole = (roleId: string, roleName: string): void => {
|
||||||
setSelectedRoleId(roleId);
|
setSelectedRoleId(roleId);
|
||||||
setSelectedRoleName(roleName);
|
setSelectedRoleName(roleName);
|
||||||
setDeleteModalOpen(true);
|
setDeleteModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Confirm delete handler
|
// Confirm delete handler
|
||||||
const handleConfirmDelete = async (): Promise<void> => {
|
const handleConfirmDelete = async (): Promise<void> => {
|
||||||
if (!selectedRoleId) return;
|
if (!selectedRoleId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
await roleService.delete(selectedRoleId);
|
await roleService.delete(selectedRoleId);
|
||||||
setDeleteModalOpen(false);
|
setDeleteModalOpen(false);
|
||||||
setSelectedRoleId(null);
|
setSelectedRoleId(null);
|
||||||
setSelectedRoleName('');
|
setSelectedRoleName("");
|
||||||
await fetchRoles(currentPage, limit, orderBy);
|
await fetchRoles(currentPage, limit, orderBy);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw err; // Let the modal handle the error display
|
throw err; // Let the modal handle the error display
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load role for view/edit
|
// Load role for view/edit
|
||||||
const loadRole = async (id: string): Promise<Role> => {
|
const loadRole = async (id: string): Promise<Role> => {
|
||||||
const response = await roleService.getById(id);
|
const response = await roleService.getById(id);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Table columns
|
// Table columns
|
||||||
const columns: Column<Role>[] = [
|
const columns: Column<Role>[] = [
|
||||||
{
|
{
|
||||||
key: "name",
|
key: "name",
|
||||||
label: "Name",
|
label: "Name",
|
||||||
render: (role) => (
|
render: (role) => (
|
||||||
<span className="text-sm font-normal text-[#0f1724]">{role.name}</span>
|
<span className="text-sm font-normal text-[#0f1724]">
|
||||||
),
|
{role.name}
|
||||||
},
|
</span>
|
||||||
{
|
),
|
||||||
key: "code",
|
},
|
||||||
label: "Code",
|
{
|
||||||
render: (role) => <CodeBadge label={role.code} />,
|
key: "code",
|
||||||
},
|
label: "Code",
|
||||||
{
|
render: (role) => <CodeBadge label={role.code} />,
|
||||||
key: "scope",
|
},
|
||||||
label: "Scope",
|
{
|
||||||
render: (role) => (
|
key: "description",
|
||||||
<StatusBadge variant={getScopeVariant(role.scope)}>
|
label: "Description",
|
||||||
{role.scope}
|
render: (role) => (
|
||||||
</StatusBadge>
|
<span className="text-sm font-normal text-[#6b7280]">
|
||||||
),
|
{role.description || "N/A"}
|
||||||
},
|
</span>
|
||||||
{
|
),
|
||||||
key: "user_count",
|
},
|
||||||
label: "Users",
|
{
|
||||||
render: (role) => (
|
key: "scope",
|
||||||
<span className="text-sm font-normal text-[#0f1724]">
|
label: "Scope",
|
||||||
{role.user_count || 0}
|
render: (role) => (
|
||||||
</span>
|
<StatusBadge variant={getScopeVariant(role.scope)}>
|
||||||
),
|
{role.scope}
|
||||||
},
|
</StatusBadge>
|
||||||
{
|
),
|
||||||
key: "description",
|
},
|
||||||
label: "Description",
|
{
|
||||||
render: (role) => (
|
key: "user_count",
|
||||||
<span className="text-sm font-normal text-[#6b7280]">
|
label: "Users",
|
||||||
{role.description || "N/A"}
|
render: (role) => (
|
||||||
</span>
|
<span className="text-sm font-normal text-[#0f1724]">
|
||||||
),
|
{role.user_count || 0}
|
||||||
},
|
</span>
|
||||||
{
|
),
|
||||||
key: "created_at",
|
},
|
||||||
label: "Created Date",
|
{
|
||||||
render: (role) => (
|
key: "created_at",
|
||||||
<span className="text-sm font-normal text-[#6b7280]">
|
label: "Created Date",
|
||||||
{formatDate(role.created_at)}
|
render: (role) => (
|
||||||
</span>
|
<span className="text-sm font-normal text-[#6b7280]">
|
||||||
),
|
{formatDate(role.created_at)}
|
||||||
},
|
</span>
|
||||||
{
|
),
|
||||||
key: "actions",
|
},
|
||||||
label: "Actions",
|
{
|
||||||
align: "right",
|
key: "actions",
|
||||||
render: (role) => (
|
label: "Actions",
|
||||||
<div className="flex justify-end">
|
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
|
<ActionDropdown
|
||||||
onView={() => handleViewRole(role.id)}
|
onView={() => handleViewRole(role.id)}
|
||||||
onEdit={
|
onEdit={
|
||||||
@ -288,90 +336,214 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||||
},
|
<div>
|
||||||
];
|
<span className="text-[#9aa6b2]">Scope:</span>
|
||||||
|
<div className="mt-1">
|
||||||
// Mobile card renderer
|
<StatusBadge variant={getScopeVariant(role.scope)}>
|
||||||
const mobileCardRenderer = (role: Role) => (
|
{role.scope}
|
||||||
<div className="p-4">
|
</StatusBadge>
|
||||||
<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>
|
</div>
|
||||||
</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
|
<DataTable
|
||||||
data={roles}
|
data={roles}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -381,7 +553,9 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
{pagination.totalPages > 0 && (
|
|
||||||
|
{/* Table Footer with Pagination */}
|
||||||
|
{pagination.total > 0 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={pagination.totalPages}
|
totalPages={pagination.totalPages}
|
||||||
@ -392,7 +566,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(({
|
|||||||
}}
|
}}
|
||||||
onLimitChange={(newLimit: number) => {
|
onLimitChange={(newLimit: number) => {
|
||||||
setLimit(newLimit);
|
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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAppSelector } from "@/hooks/redux-hooks";
|
import { useAppSelector } from "@/hooks/redux-hooks";
|
||||||
import type { QuickAction } from "@/types/dashboard";
|
import type { QuickAction } from "@/types/dashboard";
|
||||||
import { useAppTheme } from "@/hooks/useAppTheme";
|
// import { useAppTheme } from "@/hooks/useAppTheme";
|
||||||
|
|
||||||
export const QuickActions = () => {
|
export const QuickActions = () => {
|
||||||
const { primaryColor } = useAppTheme();
|
// const { primaryColor } = useAppTheme();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { roles, permissions } = useAppSelector((state) => state.auth);
|
const { roles, permissions } = useAppSelector((state) => state.auth);
|
||||||
|
|
||||||
@ -32,17 +32,20 @@ export const QuickActions = () => {
|
|||||||
{
|
{
|
||||||
icon: Plus,
|
icon: Plus,
|
||||||
label: "New Tenant",
|
label: "New Tenant",
|
||||||
|
btncolor: "#4C89FA",
|
||||||
onClick: () => navigate("/tenants/create-wizard"),
|
onClick: () => navigate("/tenants/create-wizard"),
|
||||||
},
|
},
|
||||||
{ icon: UserPlus, label: "Module", onClick: () => navigate("/modules") },
|
{ icon: UserPlus, label: "Module", btncolor: "#16C784", onClick: () => navigate("/modules") },
|
||||||
{
|
{
|
||||||
icon: Shield,
|
icon: Shield,
|
||||||
label: "Notification",
|
label: "Notification",
|
||||||
|
btncolor: "#FCA004",
|
||||||
onClick: () => navigate("/notifications"),
|
onClick: () => navigate("/notifications"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Settings,
|
icon: Settings,
|
||||||
label: "Audit Logs",
|
label: "Audit Logs",
|
||||||
|
btncolor: "#6B7280",
|
||||||
onClick: () => navigate("/audit-logs"),
|
onClick: () => navigate("/audit-logs"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -51,21 +54,25 @@ export const QuickActions = () => {
|
|||||||
hasPermission("users", "create") && {
|
hasPermission("users", "create") && {
|
||||||
icon: UserPlus,
|
icon: UserPlus,
|
||||||
label: "New User",
|
label: "New User",
|
||||||
|
btncolor: "#4C89FA",
|
||||||
onClick: () => navigate("/tenant/users"),
|
onClick: () => navigate("/tenant/users"),
|
||||||
},
|
},
|
||||||
hasPermission("roles", "create") && {
|
hasPermission("roles", "create") && {
|
||||||
icon: Shield,
|
icon: Shield,
|
||||||
label: "New Role",
|
label: "New Role",
|
||||||
|
btncolor: "#16C784",
|
||||||
onClick: () => navigate("/tenant/roles"),
|
onClick: () => navigate("/tenant/roles"),
|
||||||
},
|
},
|
||||||
hasPermission("departments", "create") && {
|
hasPermission("departments", "create") && {
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
label: "New Dept",
|
label: "New Dept",
|
||||||
|
btncolor: "#FCA004",
|
||||||
onClick: () => navigate("/tenant/departments"),
|
onClick: () => navigate("/tenant/departments"),
|
||||||
},
|
},
|
||||||
hasPermission("designations", "create") && {
|
hasPermission("designations", "create") && {
|
||||||
icon: BadgeCheck,
|
icon: BadgeCheck,
|
||||||
label: "New Desig",
|
label: "New Desig",
|
||||||
|
btncolor: "#6B7280",
|
||||||
onClick: () => navigate("/tenant/designations"),
|
onClick: () => navigate("/tenant/designations"),
|
||||||
},
|
},
|
||||||
].filter(Boolean) as QuickAction[];
|
].filter(Boolean) as QuickAction[];
|
||||||
@ -107,7 +114,7 @@ export const QuickActions = () => {
|
|||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<Icon
|
<Icon
|
||||||
className="w-[20px] h-[20px]"
|
className="w-[20px] h-[20px]"
|
||||||
color={primaryColor}
|
color={action.btncolor}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -306,13 +306,13 @@ const LandingPage = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</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 */}
|
{/* Welcome Section */}
|
||||||
<div className="text-center mb-16 md:mb-24 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
<div className="text-center mb-4 md:mb-12 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||||
<h1 className="text-4xl md:text-5xl font-bold text-[#0f1724] mb-4">
|
<h1 className="text-3xl font-bold text-[#0f1724] mb-4">
|
||||||
Welcome back, {getUserName()}
|
Welcome back, {getUserName()}
|
||||||
</h1>
|
</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.
|
Select a module below to access your workspace. You can switch between modules anytime from the main navigation.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export interface QuickAction {
|
|||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
label: string;
|
label: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
btncolor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HealthMetric {
|
export interface HealthMetric {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user