nxtgauge-admin-solid/src/routes/admin/roles/index.tsx

465 lines
24 KiB
TypeScript
Raw Normal View History

import { For, Show, createSignal } from 'solid-js';
import AdminShell from '~/components/AdminShell';
import { ChevronDown, SlidersHorizontal, Download, MoreVertical } from 'lucide-solid';
type Role = {
id: string;
name: string;
department: string;
usersAssigned: number;
permissionsCount: number;
status: 'ACTIVE' | 'INACTIVE';
createdDate: string;
};
const FALLBACK_ROLES: Role[] = [
{ id: 'r1', name: 'Engineering Lead', department: 'Engineering', usersAssigned: 12, permissionsCount: 28, status: 'ACTIVE', createdDate: '2026-01-15' },
{ id: 'r2', name: 'Marketing Manager', department: 'Marketing', usersAssigned: 8, permissionsCount: 18, status: 'ACTIVE', createdDate: '2026-01-20' },
{ id: 'r3', name: 'Sales Director', department: 'Sales', usersAssigned: 15, permissionsCount: 32, status: 'ACTIVE', createdDate: '2026-02-01' },
{ id: 'r4', name: 'HR Admin', department: 'Human Resources', usersAssigned: 5, permissionsCount: 24, status: 'ACTIVE', createdDate: '2026-02-05' },
{ id: 'r5', name: 'Finance Controller', department: 'Finance', usersAssigned: 6, permissionsCount: 20, status: 'ACTIVE', createdDate: '2026-02-10' },
{ id: 'r6', name: 'Operations Head', department: 'Operations', usersAssigned: 4, permissionsCount: 16, status: 'INACTIVE', createdDate: '2026-03-01' },
{ id: 'r7', name: 'Support Lead', department: 'Customer Support', usersAssigned: 9, permissionsCount: 16, status: 'ACTIVE', createdDate: '2026-03-05' },
{ id: 'r8', name: 'Product Owner', department: 'Product', usersAssigned: 7, permissionsCount: 26, status: 'ACTIVE', createdDate: '2026-03-10' },
];
const MODULES = [
'Department Management',
'Designation Management',
'Internal Role Management',
'Employee Management',
'External Role Management',
'External Onboarding Management',
'Internal Dashboard Management',
'External Dashboard Management',
'Verification Management',
'Approval Management',
'Users Management',
'Company Management',
'Candidate Management',
'Customer Management',
'Jobs Management',
'Leads Management',
'Pricing Management',
'Credit Management',
'Coupon Management',
'Discount Management',
'Tax Management',
'Order Management',
'Invoice Management',
'Review Management',
'Support Management',
'Report Management',
'Ledger Management',
];
type PermKey = 'view' | 'create' | 'update' | 'delete';
const PERM_KEYS: PermKey[] = ['view', 'create', 'update', 'delete'];
type ModulePerms = Record<PermKey, boolean>;
type PermissionsMap = Record<string, ModulePerms>;
function defaultPerms(): PermissionsMap {
const map: PermissionsMap = {};
for (const m of MODULES) {
map[m] = { view: false, create: false, update: false, delete: false };
}
return map;
}
function StatusBadge(props: { status: string }) {
const active = () => props.status === 'ACTIVE';
return (
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? '#FFD8C2' : '#D1D5DB'};background:${active() ? '#FFF1EB' : '#F3F4F6'};color:${active() ? '#FF5E13' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}>
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${active() ? '#FF5E13' : '#9CA3AF'};margin-right:5px;flex-shrink:0`} />
{active() ? 'Active' : 'Inactive'}
</span>
);
}
function Toggle(props: { on: boolean; onChange: (v: boolean) => void }) {
return (
<button
type="button"
onClick={() => props.onChange(!props.on)}
style={`width:40px;height:22px;border-radius:11px;background:${props.on ? '#FF5E13' : '#E5E7EB'};position:relative;cursor:pointer;border:none;padding:0;transition:background 0.2s`}
>
<span style={`position:absolute;width:18px;height:18px;border-radius:50%;background:white;top:2px;transition:left 0.2s;left:${props.on ? '20px' : '2px'}`} />
</button>
);
}
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string }) {
return (
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
{props.label}{props.required && <span style="color:#FF5E13;margin-left:2px">*</span>}
</span>
<input
type="text"
value={props.value}
onInput={(e) => props.onInput(e.currentTarget.value)}
placeholder={props.placeholder}
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;padding:0 14px;font-size:13px;outline:none;box-sizing:border-box;color:#111827;background:white"
/>
</label>
);
}
export default function RolesPage() {
const [mainTab, setMainTab] = createSignal<'all' | 'create'>('all');
const [formTab, setFormTab] = createSignal<'general' | 'access' | 'settings'>('general');
// All Roles state
const [search, setSearch] = createSignal('');
// Create Role state
const [roleName, setRoleName] = createSignal('');
const [roleCode, setRoleCode] = createSignal('');
const [department, setDepartment] = createSignal('');
const [description, setDescription] = createSignal('');
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
const [approveRequests, setApproveRequests] = createSignal(true);
const [manageSettings, setManageSettings] = createSignal(false);
const [permissions, setPermissions] = createSignal<PermissionsMap>(defaultPerms());
const filteredRoles = () => {
const q = search().toLowerCase();
if (!q) return FALLBACK_ROLES;
return FALLBACK_ROLES.filter(
(r) => r.name.toLowerCase().includes(q) || r.department.toLowerCase().includes(q)
);
};
const togglePerm = (mod: string, key: PermKey) => {
setPermissions((prev) => ({
...prev,
[mod]: { ...prev[mod], [key]: !prev[mod][key] },
}));
};
const toggleSelectAll = (mod: string) => {
const p = permissions()[mod];
const allOn = PERM_KEYS.every((k) => p[k]);
setPermissions((prev) => ({
...prev,
[mod]: { view: !allOn, create: !allOn, update: !allOn, delete: !allOn },
}));
};
const formatDate = (d: string) => {
if (!d) return '—';
const dt = new Date(d);
return dt.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
};
const shown = () => filteredRoles();
return (
<AdminShell>
<div style="padding:24px">
{/* Page Header */}
<div>
<h1 style="font-size:24px;font-weight:700;color:#111827;margin:0">Internal Role Management</h1>
<p style="font-size:13px;color:#6B7280;margin-top:4px;margin-bottom:0">Manage internal roles and permissions</p>
</div>
{/* Main Tabs */}
<div style="display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB;margin-top:24px">
<button
type="button"
onClick={() => setMainTab('all')}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${mainTab() === 'all' ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280;border-bottom:none'}`}
>
All Roles
</button>
<button
type="button"
onClick={() => setMainTab('create')}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${mainTab() === 'create' ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280;border-bottom:none'}`}
>
Create Role
</button>
</div>
{/* ── ALL ROLES TAB ── */}
<Show when={mainTab() === 'all'}>
{/* Edge-to-edge card */}
<div style="margin-top:20px;margin-left:-24px;margin-right:-24px;border-radius:0;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white">
{/* Filter Bar */}
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
<input
type="text"
placeholder="Search roles..."
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
style="height:34px;flex:1;max-width:240px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;outline:none;color:#111827"
/>
<button type="button" style="height:34px;padding:0 12px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer;display:inline-flex;align-items:center;gap:4px">
<ChevronDown size={14} />
Sort
</button>
<button type="button" style="height:34px;padding:0 12px;border-radius:8px;border:1px solid #E5E7EB;background:white;font-size:13px;color:#374151;cursor:pointer;display:inline-flex;align-items:center;gap:4px">
<SlidersHorizontal size={14} />
Filters
</button>
<button type="button" style="height:34px;padding:0 14px;border-radius:8px;background:#0D0D2A;color:white;font-size:13px;font-weight:500;border:none;cursor:pointer;display:inline-flex;align-items:center;gap:6px">
<Download size={14} />
Export
</button>
</div>
{/* Table */}
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse">
<thead>
<tr style="background:#0D0D2A">
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Role Name</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Department</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Users Assigned</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Permissions Count</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Status</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap">Created Date</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;text-align:left;white-space:nowrap"></th>
</tr>
</thead>
<tbody>
<For each={shown()}>
{(role) => (
<tr style="border-bottom:1px solid #F3F4F6">
<td style="padding:12px 20px">
<span style="font-size:14px;font-weight:600;color:#111827">{role.name}</span>
</td>
<td style="padding:12px 20px">
<span style="font-size:14px;color:#374151">{role.department}</span>
</td>
<td style="padding:12px 20px">
<span style="font-size:14px;font-weight:600;color:#111827">{role.usersAssigned}</span>
</td>
<td style="padding:12px 20px">
<span style="display:inline-flex;border-radius:8px;background:#FFF1EB;border:1px solid #FFD8C2;color:#FF5E13;padding:2px 10px;font-size:12px;font-weight:600">
{role.permissionsCount} Permissions
</span>
</td>
<td style="padding:12px 20px">
<StatusBadge status={role.status} />
</td>
<td style="padding:12px 20px">
<span style="font-size:13px;color:#6B7280">{formatDate(role.createdDate)}</span>
</td>
<td style="padding:12px 20px;text-align:center">
<button type="button" style="background:none;border:none;cursor:pointer;color:#6B7280;display:inline-flex;align-items:center">
<MoreVertical size={16} />
</button>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
{/* Pagination */}
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-top:1px solid #F3F4F6">
<span style="font-size:13px;color:#6B7280">
Showing 1{shown().length} of {shown().length} roles
</span>
<div style="display:flex;align-items:center;gap:4px">
<button type="button" style="height:30px;min-width:30px;padding:0 10px;border-radius:6px;border:1px solid #E5E7EB;background:#FF5E13;color:white;font-size:13px;font-weight:600;cursor:pointer">1</button>
</div>
</div>
</div>
</Show>
{/* ── CREATE ROLE TAB ── */}
<Show when={mainTab() === 'create'}>
<div style="margin-top:20px;border-radius:16px;border:1px solid #E5E7EB;background:white;overflow:hidden">
{/* Form Sub-tabs */}
<div style="display:flex;align-items:center;gap:0;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(
[
{ key: 'general', label: 'General Information' },
{ key: 'access', label: 'Module Access' },
{ key: 'settings', label: 'Role Settings' },
] as const
).map(({ key, label }) => (
<button
type="button"
onClick={() => setFormTab(key)}
style={`padding:14px 0;margin-right:24px;font-size:14px;background:none;border:none;cursor:pointer;${formTab() === key ? 'color:#0D0D2A;border-bottom:2px solid #0D0D2A;margin-bottom:-1px;font-weight:600' : 'color:#6B7280;font-weight:400'}`}
>
{label}
</button>
))}
</div>
{/* ── General Information ── */}
<Show when={formTab() === 'general'}>
<div style="padding:24px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormInput
label="Role Name"
required
value={roleName()}
onInput={setRoleName}
placeholder="e.g. Engineering Lead"
/>
<FormInput
label="Role Code"
required
value={roleCode()}
onInput={setRoleCode}
placeholder="e.g. ENG_LEAD"
/>
</div>
<div style="margin-top:20px">
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
Department<span style="color:#FF5E13;margin-left:2px">*</span>
</span>
<input
type="text"
value={department()}
onInput={(e) => setDepartment(e.currentTarget.value)}
placeholder="e.g. Engineering"
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;padding:0 14px;font-size:13px;outline:none;box-sizing:border-box;color:#111827;background:white"
/>
</label>
</div>
<div style="margin-top:20px">
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
<textarea
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
placeholder="Describe this role's responsibilities..."
style="display:block;margin-top:6px;height:100px;width:100%;border-radius:10px;border:1px solid #E5E7EB;padding:10px 14px;font-size:13px;outline:none;box-sizing:border-box;color:#111827;background:white;resize:vertical;font-family:inherit"
/>
</label>
</div>
</div>
</Show>
{/* ── Module Access ── */}
<Show when={formTab() === 'access'}>
<div style="padding:24px">
<p style="font-size:13px;color:#6B7280;margin-top:0;margin-bottom:16px">
Configure module access permissions for this role.
</p>
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;border-radius:12px;overflow:hidden;border:1px solid #E5E7EB">
<thead>
<tr style="background:#0D0D2A">
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:left">Module Name</th>
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">View</th>
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Create</th>
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Update</th>
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Delete</th>
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;color:white;text-align:center">Select All</th>
</tr>
</thead>
<tbody>
<For each={MODULES}>
{(mod) => {
const p = () => permissions()[mod];
const allOn = () => PERM_KEYS.every((k) => p()[k]);
return (
<tr style="border-bottom:1px solid #F3F4F6">
<td style="padding:12px 16px;font-size:13px;color:#111827;font-weight:500;text-align:left">{mod}</td>
<For each={PERM_KEYS}>
{(key) => (
<td style="text-align:center;padding:12px">
<input
type="checkbox"
checked={p()[key]}
onChange={() => togglePerm(mod, key)}
style="width:16px;height:16px;accent-color:#FF5E13;cursor:pointer"
/>
</td>
)}
</For>
<td style="text-align:center;padding:12px">
<input
type="checkbox"
checked={allOn()}
onChange={() => toggleSelectAll(mod)}
style="width:16px;height:16px;accent-color:#FF5E13;cursor:pointer"
/>
</td>
</tr>
);
}}
</For>
</tbody>
</table>
</div>
</div>
</Show>
{/* ── Role Settings ── */}
<Show when={formTab() === 'settings'}>
<div style="padding:24px;display:flex;flex-direction:column;gap:24px">
{/* Role Status */}
<div>
<p style="font-size:13px;font-weight:600;color:#374151;margin:0 0 10px 0">Role Status</p>
<div style="display:flex;gap:8px">
<button
type="button"
onClick={() => setStatus('ACTIVE')}
style={`height:36px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;${status() === 'ACTIVE' ? 'border:1px solid #FF5E13;background:#FF5E13;color:white' : 'border:1px solid #E5E7EB;background:white;color:#6B7280'}`}
>
Active
</button>
<button
type="button"
onClick={() => setStatus('INACTIVE')}
style={`height:36px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;${status() === 'INACTIVE' ? 'border:1px solid #6B7280;background:#6B7280;color:white' : 'border:1px solid #E5E7EB;background:white;color:#6B7280'}`}
>
Inactive
</button>
</div>
</div>
{/* Toggle: Approve Requests */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
<div>
<p style="font-size:14px;font-weight:600;color:#111827;margin:0">Allow Role to Approve Requests</p>
<p style="font-size:13px;color:#6B7280;margin:4px 0 0 0">Grant this role the ability to approve or reject submitted requests.</p>
</div>
<Toggle on={approveRequests()} onChange={setApproveRequests} />
</div>
{/* Toggle: Manage Settings */}
<div style="border-radius:12px;border:1px solid #E5E7EB;padding:16px 20px;display:flex;align-items:center;justify-content:space-between">
<div>
<p style="font-size:14px;font-weight:600;color:#111827;margin:0">Allow Role to Manage System Settings</p>
<p style="font-size:13px;color:#6B7280;margin:4px 0 0 0">Grant this role access to configure and modify system-wide settings.</p>
</div>
<Toggle on={manageSettings()} onChange={setManageSettings} />
</div>
</div>
</Show>
{/* Form Footer */}
<div style="display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #E5E7EB">
<button
type="button"
onClick={() => setMainTab('all')}
style="height:38px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;border:1px solid #E5E7EB;background:white;color:#374151;cursor:pointer"
>
Cancel
</button>
<button
type="button"
style="background:#0D0D2A;color:white;height:38px;padding:0 20px;border-radius:8px;font-size:14px;font-weight:500;border:none;cursor:pointer"
>
Create Role
</button>
</div>
</div>
</Show>
</div>
</AdminShell>
);
}