import { useState, useEffect, type ReactElement } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Layout } from "@/components/layout/Layout"; import { PrimaryButton, SecondaryButton, Modal, FormField, FormSelect, StatusBadge, DataTable, Pagination, DeleteConfirmationModal, type Column, } from "@/components/shared"; import { roleService } from "@/services/role-service"; import { permissionService } from "@/services/permission-service"; import type { Role, Permission } from "@/types/role"; import { showToast } from "@/utils/toast"; import { Shield, Plus, Trash2, Loader2, ShieldAlert, } from "lucide-react"; // Zod Schema for custom permissions const customPermissionSchema = z.object({ resource: z .string() .min(1, "Resource name is required") .regex( /^[a-z0-9_]+$/, "Resource name must be lowercase alphanumeric and can contain underscores" ), action: z.string().min(1, "Action is required"), }); type CustomPermissionFormData = z.infer; export default function TenantAdminRole(): ReactElement { const [role, setRole] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // All permissions list for tenant_admin const [permissions, setPermissions] = useState([]); // Pagination state for permissions table const [page, setPage] = useState(1); const [limit, setLimit] = useState(10); const [totalItems, setTotalItems] = useState(0); const [totalPages, setTotalPages] = useState(1); // Add Custom Permission Modal state const [isAddCustomOpen, setIsAddCustomOpen] = useState(false); const [isAddingCustom, setIsAddingCustom] = useState(false); // Delete/Revoke Permission state const [deleteTargetId, setDeleteTargetId] = useState(null); const [isDeleting, setIsDeleting] = useState(false); // React Hook Form setup const { register, handleSubmit, setValue, watch, reset, formState: { errors }, } = useForm({ resolver: zodResolver(customPermissionSchema), defaultValues: { resource: "", action: "read", }, }); const loadPermissions = async (roleId: string, pageNum: number, limitNum: number) => { try { const response = await permissionService.getAll(pageNum, limitNum, { role_id: roleId }); if (response.success) { setPermissions(response.data); if (response.pagination) { setTotalItems(response.pagination.total); setTotalPages(response.pagination.totalPages); } } } catch (err: any) { showToast.error("Failed to load permissions list."); } finally { setIsLoading(false); } }; const loadTenantAdminRole = async () => { try { setIsLoading(true); setError(null); // 1. Fetch roles to find tenant_admin const rolesResponse = await roleService.getAll(1, 100, null, "tenant_admin", null, true); if (!rolesResponse.success || rolesResponse.data.length === 0) { throw new Error("Tenant Admin role not found. Please verify that the system is seeded."); } const tenantAdminSummary = rolesResponse.data.find( (r) => r.code === "tenant_admin" ); if (!tenantAdminSummary) { throw new Error("Tenant Admin role not found among tenant roles."); } // Keep role metadata setRole(tenantAdminSummary); } catch (err: any) { setError(err.message || "Failed to load Tenant Admin role data."); setIsLoading(false); } }; useEffect(() => { loadTenantAdminRole(); }, []); // When page or limit changes, load updated permissions useEffect(() => { if (role?.id) { loadPermissions(role.id, page, limit); } }, [page, limit, role?.id]); // Add custom permission directly via Permission API (Always tenant scoped) const handleAddCustomPermission = async (data: CustomPermissionFormData) => { if (!role) return; try { setIsAddingCustom(true); const payload = { role_id: role.id, resource: data.resource.trim().toLowerCase(), action: data.action.trim().toLowerCase(), scope: "tenant" as const, // Always tenant scoped }; const response = await permissionService.create(payload); if (response.success) { showToast.success("Permission added successfully."); setIsAddCustomOpen(false); reset(); await loadPermissions(role.id, page, limit); } } catch (err: any) { showToast.error( err?.response?.data?.error?.message || err?.message || "Failed to add permission." ); } finally { setIsAddingCustom(false); } }; // Revoke permission directly via Permission API const handleRevokePermission = async () => { if (!deleteTargetId || !role) return; try { setIsDeleting(true); const response = await permissionService.delete(deleteTargetId); if (response.success) { showToast.success("Permission revoked successfully."); setDeleteTargetId(null); await loadPermissions(role.id, page, limit); } } catch (err: any) { showToast.error( err?.response?.data?.error?.message || err?.message || "Failed to revoke permission." ); } finally { setIsDeleting(false); } }; // DataTable column mapping for permissions const columns: Column[] = [ { key: "resource", label: "Resource", width: "40%", render: (item) => ( {item.resource} ), }, { key: "action", label: "Action", width: "25%", render: (item) => ( {item.action} ), }, { key: "scope", label: "Scope", width: "20%", render: (item) => ( {item.scope || "tenant"} ), }, { key: "actions", label: "Action", width: "15%", align: "right", render: (item) => ( ), }, ]; if (isLoading) { return (

Loading Tenant Admin configuration...

); } if (error || !role) { return (

Configuration Error

{error || "Role not found."}

Retry Loading
); } return (
{/* Role Meta Information Card */}

{role.name}

Tenant Scoped {role.is_system && ( System seeded )}

Role Code: {role.code}

This is a global system template. Changes here immediately affect the permission policies of all active tenants.

{/* Permissions Table Section */}

Tenant Admin Permissions

Manage granular, scope-specific system policies or actions assigned to this role.

setIsAddCustomOpen(true)} className="flex items-center gap-1.5 px-3 py-2 text-xs" > Add Custom Permission
item.id || `${item.resource}-${item.action}`} emptyMessage="No permissions configured for this role." /> {totalItems > 0 && ( setPage(p)} onLimitChange={(l: number) => { setLimit(l); setPage(1); }} /> )}
{/* Add Custom Permission Modal */} { setIsAddCustomOpen(false); reset(); }} title="Add Custom Permission" description="Define a granular resource policy mapping directly to the database permission rules." footer={ <> { setIsAddCustomOpen(false); reset(); }} disabled={isAddingCustom} className="px-4 py-2 text-sm" > Cancel {isAddingCustom ? "Adding..." : "Add Permission"} } >
setValue("action", val, { shouldValidate: true })} error={errors.action?.message} />
{/* Revoke confirmation modal */} setDeleteTargetId(null)} onConfirm={handleRevokePermission} title="Revoke Permission" message="Are you sure you want to revoke this advanced permission? This will take effect immediately across all tenants." isLoading={isDeleting} />
); }