212 lines
8.5 KiB
TypeScript
212 lines
8.5 KiB
TypeScript
|
|
import { useNavigate } from '@solidjs/router';
|
||
|
|
import { createMemo, createResource, createSignal, Show } from 'solid-js';
|
||
|
|
import AdminShell from '~/components/AdminShell';
|
||
|
|
|
||
|
|
const API = '/api/gateway';
|
||
|
|
|
||
|
|
type Permission = { id: string; module: string; action: string };
|
||
|
|
|
||
|
|
async function loadPermissions(): Promise<Permission[]> {
|
||
|
|
const res = await fetch(`${API}/api/admin/permissions`);
|
||
|
|
if (!res.ok) return [];
|
||
|
|
const data = await res.json();
|
||
|
|
const rows = Array.isArray(data) ? data : (data.permissions || []);
|
||
|
|
return rows;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function CreateInternalRolePage() {
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const [permissions] = createResource(loadPermissions);
|
||
|
|
|
||
|
|
const [roleName, setRoleName] = createSignal('');
|
||
|
|
const [description, setDescription] = createSignal('');
|
||
|
|
const [assignedModules, setAssignedModules] = createSignal<string[]>([]);
|
||
|
|
const [permissionIds, setPermissionIds] = createSignal<string[]>([]);
|
||
|
|
const [saving, setSaving] = createSignal(false);
|
||
|
|
const [error, setError] = createSignal('');
|
||
|
|
|
||
|
|
const permsByModule = createMemo(() => {
|
||
|
|
const map: Record<string, Permission[]> = {};
|
||
|
|
(permissions() || []).forEach((p) => {
|
||
|
|
if (!map[p.module]) map[p.module] = [];
|
||
|
|
map[p.module].push(p);
|
||
|
|
});
|
||
|
|
return map;
|
||
|
|
});
|
||
|
|
|
||
|
|
const allModules = createMemo(() => Object.keys(permsByModule()).sort());
|
||
|
|
|
||
|
|
const toggleModule = (mod: string) => {
|
||
|
|
const current = assignedModules();
|
||
|
|
if (current.includes(mod)) {
|
||
|
|
setAssignedModules(current.filter((m) => m !== mod));
|
||
|
|
// remove permissions for this module
|
||
|
|
const idsToRemove = (permsByModule()[mod] || []).map((p) => p.id);
|
||
|
|
setPermissionIds(permissionIds().filter((id) => !idsToRemove.includes(id)));
|
||
|
|
} else {
|
||
|
|
setAssignedModules([...current, mod]);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const togglePermission = (id: string) => {
|
||
|
|
const current = permissionIds();
|
||
|
|
if (current.includes(id)) {
|
||
|
|
setPermissionIds(current.filter((p) => p !== id));
|
||
|
|
} else {
|
||
|
|
setPermissionIds([...current, id]);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSave = async () => {
|
||
|
|
if (!roleName().trim()) { setError('Role name is required'); return; }
|
||
|
|
if (assignedModules().length === 0) { setError('Select at least one module'); return; }
|
||
|
|
if (permissionIds().length === 0) { setError('Assign at least one permission'); return; }
|
||
|
|
|
||
|
|
try {
|
||
|
|
setSaving(true);
|
||
|
|
setError('');
|
||
|
|
const res = await fetch(`${API}/api/admin/roles`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({
|
||
|
|
name: roleName().trim(),
|
||
|
|
description: description().trim(),
|
||
|
|
audience: 'INTERNAL',
|
||
|
|
modules: assignedModules(),
|
||
|
|
permissionIds: permissionIds(),
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const body = await res.json().catch(() => ({}));
|
||
|
|
throw new Error(body.message || 'Failed to create role');
|
||
|
|
}
|
||
|
|
navigate('/admin/roles');
|
||
|
|
} catch (err: any) {
|
||
|
|
setError(err.message || 'Failed to create role');
|
||
|
|
} finally {
|
||
|
|
setSaving(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<AdminShell>
|
||
|
|
<div class="page-actions">
|
||
|
|
<div>
|
||
|
|
<h1 class="page-title">Create Internal Role</h1>
|
||
|
|
<p class="page-subtitle">Add a new internal role with module access and permissions.</p>
|
||
|
|
</div>
|
||
|
|
<a class="btn" href="/admin/roles">Back to Roles</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Show when={error()}>
|
||
|
|
<div class="error-box">{error()}</div>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
{/* Role Details */}
|
||
|
|
<div class="role-form-section">
|
||
|
|
<h3>Role Details</h3>
|
||
|
|
<p>Start by giving the role a simple business name.</p>
|
||
|
|
<div class="field-grid-2">
|
||
|
|
<div class="field">
|
||
|
|
<label>Name of the Role <span style="color:#ef4444">*</span></label>
|
||
|
|
<input
|
||
|
|
value={roleName()}
|
||
|
|
onInput={(e) => setRoleName(e.currentTarget.value)}
|
||
|
|
placeholder="e.g. Customer Support Rep"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div class="field">
|
||
|
|
<label>Description</label>
|
||
|
|
<input
|
||
|
|
value={description()}
|
||
|
|
onInput={(e) => setDescription(e.currentTarget.value)}
|
||
|
|
placeholder="Short description of this role"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Module Access */}
|
||
|
|
<div class="role-form-section">
|
||
|
|
<h3>Module Access</h3>
|
||
|
|
<p>Select which modules this role can access. Only selected modules will appear in the permission table below.</p>
|
||
|
|
<Show when={permissions.loading}>
|
||
|
|
<p class="notice">Loading modules...</p>
|
||
|
|
</Show>
|
||
|
|
<Show when={!permissions.loading && allModules().length > 0}>
|
||
|
|
<div class="module-picker">
|
||
|
|
{allModules().map((mod) => (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
class={`module-chip ${assignedModules().includes(mod) ? 'selected' : ''}`}
|
||
|
|
onClick={() => toggleModule(mod)}
|
||
|
|
>
|
||
|
|
<span style={`width:14px;height:14px;border-radius:3px;border:2px solid ${assignedModules().includes(mod) ? '#c2410c' : '#cbd5e1'};background:${assignedModules().includes(mod) ? '#c2410c' : '#fff'};flex-shrink:0;display:inline-block`} />
|
||
|
|
{mod}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</Show>
|
||
|
|
<Show when={!permissions.loading && allModules().length === 0}>
|
||
|
|
<p class="notice">No modules available. Is the backend running?</p>
|
||
|
|
</Show>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Permission Table */}
|
||
|
|
<Show when={assignedModules().length > 0}>
|
||
|
|
<div class="role-form-section">
|
||
|
|
<h3>Permissions</h3>
|
||
|
|
<p>Set Read / Create / Update / Delete access for each assigned module.</p>
|
||
|
|
<div class="table-wrap">
|
||
|
|
<table class="perm-table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th style="width:45%">Name of the module</th>
|
||
|
|
<th style="width:11%">No Access</th>
|
||
|
|
<th style="width:11%">Read</th>
|
||
|
|
<th style="width:11%">Create</th>
|
||
|
|
<th style="width:11%">Update</th>
|
||
|
|
<th style="width:11%">Delete</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
{assignedModules().sort().map((mod) => {
|
||
|
|
const perms = permsByModule()[mod] || [];
|
||
|
|
const actionMap: Record<string, string> = {};
|
||
|
|
perms.forEach((p) => { actionMap[p.action] = p.id; });
|
||
|
|
const hasRead = !!actionMap['Read'] && permissionIds().includes(actionMap['Read']);
|
||
|
|
const hasCreate = !!actionMap['Create'] && permissionIds().includes(actionMap['Create']);
|
||
|
|
const hasUpdate = !!actionMap['Update'] && permissionIds().includes(actionMap['Update']);
|
||
|
|
const hasDelete = !!actionMap['Delete'] && permissionIds().includes(actionMap['Delete']);
|
||
|
|
const noAccess = !hasRead && !hasCreate && !hasUpdate && !hasDelete;
|
||
|
|
return (
|
||
|
|
<tr>
|
||
|
|
<td style="font-weight:500">{mod}</td>
|
||
|
|
<td><input type="checkbox" checked={noAccess} disabled aria-label={`${mod} no access`} /></td>
|
||
|
|
<td>{actionMap['Read'] ? <input type="checkbox" checked={hasRead} onChange={() => togglePermission(actionMap['Read'])} aria-label={`${mod} read`} /> : <span style="color:#cbd5e1">—</span>}</td>
|
||
|
|
<td>{actionMap['Create'] ? <input type="checkbox" checked={hasCreate} onChange={() => togglePermission(actionMap['Create'])} aria-label={`${mod} create`} /> : <span style="color:#cbd5e1">—</span>}</td>
|
||
|
|
<td>{actionMap['Update'] ? <input type="checkbox" checked={hasUpdate} onChange={() => togglePermission(actionMap['Update'])} aria-label={`${mod} update`} /> : <span style="color:#cbd5e1">—</span>}</td>
|
||
|
|
<td>{actionMap['Delete'] ? <input type="checkbox" checked={hasDelete} onChange={() => togglePermission(actionMap['Delete'])} aria-label={`${mod} delete`} /> : <span style="color:#cbd5e1">—</span>}</td>
|
||
|
|
</tr>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
{/* Save */}
|
||
|
|
<div style="display:flex;justify-content:flex-end;margin-top:8px">
|
||
|
|
<button
|
||
|
|
class="btn navy"
|
||
|
|
onClick={handleSave}
|
||
|
|
disabled={saving() || !roleName().trim()}
|
||
|
|
>
|
||
|
|
{saving() ? 'Creating...' : 'Create Role'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</AdminShell>
|
||
|
|
);
|
||
|
|
}
|