810 lines
49 KiB
TypeScript
810 lines
49 KiB
TypeScript
import { For, Show, createMemo, createSignal, onMount, createEffect } from 'solid-js';
|
|
import { useSearchParams } from '@solidjs/router';
|
|
import AdminShell from '~/components/AdminShell';
|
|
import type { CrudRecord } from '~/lib/admin/types';
|
|
|
|
const API = '/api/gateway';
|
|
|
|
type ExternalRoleRecord = {
|
|
id: string;
|
|
name: string;
|
|
code: string;
|
|
vertical: 'jobs' | 'marketplace';
|
|
category: 'provider' | 'employer' | 'consumer' | 'specialist';
|
|
onboardingSchemaId: string;
|
|
modules: string[];
|
|
permissions: Record<string, string[]>;
|
|
requiresOnboardingApproval: boolean;
|
|
requiresLeadApproval: boolean;
|
|
requiresJobApproval: boolean;
|
|
featureLimits: string; // JSON string
|
|
status: 'ACTIVE' | 'INACTIVE';
|
|
assignedUsers: number;
|
|
assignedUserTypes: string[];
|
|
createdDate: string;
|
|
updatedAt: string;
|
|
};
|
|
|
|
const FALLBACK_ROLES: ExternalRoleRecord[] = [
|
|
{
|
|
id: 'er1', name: 'Professional Photographer', code: 'photographer', vertical: 'marketplace', category: 'provider',
|
|
onboardingSchemaId: 'photographer_onboarding_v1', modules: ['dashboard', 'profile', 'portfolio', 'services', 'leads', 'wallet'],
|
|
permissions: { dashboard: ['read'], profile: ['read', 'update'], leads: ['read', 'update'] },
|
|
requiresOnboardingApproval: true, requiresLeadApproval: true, requiresJobApproval: false,
|
|
featureLimits: '{}', status: 'ACTIVE', assignedUsers: 45, assignedUserTypes: ['PHOTOGRAPHER', 'CREATIVE'], createdDate: '2026-01-15', updatedAt: '2026-03-27'
|
|
},
|
|
{
|
|
id: 'er2', name: 'Verified Company', code: 'company', vertical: 'jobs', category: 'employer',
|
|
onboardingSchemaId: 'company_onboarding_v1', modules: ['dashboard', 'profile', 'jobs', 'applications'],
|
|
permissions: { jobs: ['read', 'create', 'update'], applications: ['read', 'approve'] },
|
|
requiresOnboardingApproval: true, requiresLeadApproval: false, requiresJobApproval: true,
|
|
featureLimits: '{"maxActiveJobs": 5}', status: 'ACTIVE', assignedUsers: 120, assignedUserTypes: ['COMPANY', 'ENTERPRISE'], createdDate: '2026-02-10', updatedAt: '2026-03-27'
|
|
},
|
|
];
|
|
|
|
const USER_TYPE_OPTIONS = ['COMPANY', 'CANDIDATE', 'PHOTOGRAPHER', 'MAKEUP_ARTIST', 'TUTOR', 'DEVELOPER', 'VIDEO_EDITOR', 'FITNESS_TRAINER', 'CATERER', 'GRAPHIC_DESIGNER', 'SOCIAL_MEDIA_MANAGER', 'CUSTOMER'];
|
|
|
|
const ONBOARDING_SCHEMAS = [
|
|
'company_onboarding_v1', 'job_seeker_onboarding_v1', 'customer_onboarding_v1', 'photographer_onboarding_v1',
|
|
'makeup_artist_onboarding_v1', 'tutor_onboarding_v1', 'developer_onboarding_v1', 'video_editor_onboarding_v1'
|
|
];
|
|
|
|
const MODULES_BY_VERTICAL = {
|
|
jobs: [
|
|
{ key: 'dashboard', label: 'Dashboard', desc: 'KPI summary and overview' },
|
|
{ key: 'profile', label: 'Profile', desc: 'Maintain role preferences' },
|
|
{ key: 'jobs', label: 'Jobs', desc: 'Manage job postings' },
|
|
{ key: 'applications', label: 'Applications', desc: 'Review hiring flow' },
|
|
{ key: 'settings', label: 'Settings', desc: 'Account security' }
|
|
],
|
|
marketplace: [
|
|
{ key: 'dashboard', label: 'Dashboard', desc: 'KPI summary and overview' },
|
|
{ key: 'profile', label: 'Profile', desc: 'Public-facing profile' },
|
|
{ key: 'portfolio', label: 'Portfolio', desc: 'Publish work samples' },
|
|
{ key: 'services', label: 'Services', desc: 'List service pricing' },
|
|
{ key: 'leads', label: 'Leads / Requests', desc: 'Handle incoming requests' },
|
|
{ key: 'wallet', label: 'Wallet', desc: 'Tracecoin balance' }
|
|
]
|
|
};
|
|
|
|
const PERMISSION_ACTIONS = [
|
|
{ key: 'read', label: 'Read' },
|
|
{ key: 'create', label: 'Create' },
|
|
{ key: 'update', label: 'Update' },
|
|
{ key: 'delete', label: 'Delete' },
|
|
{ key: 'approve', label: 'Approve' }
|
|
];
|
|
|
|
const FALLBACK_LOGS = [
|
|
{ id: 'l1', user: 'Admin Ashwin', action: 'Updated Permissions', target: 'Verified Company', date: '2026-03-27 10:30' },
|
|
{ id: 'l2', user: 'Admin Ashwin', action: 'Changed Status', target: 'Professional Photographer', date: '2026-03-26 14:15' },
|
|
{ id: 'l3', user: 'System', action: 'Auto-sync Schema', target: 'Verified Company', date: '2026-03-25 09:00' },
|
|
];
|
|
|
|
const FALLBACK_ASSIGNED_USERS = [
|
|
{ id: 'u1', name: 'John Doe', email: 'john@example.com', joined: '2026-01-20' },
|
|
{ id: 'u2', name: 'Jane Smith', email: 'jane@studios.com', joined: '2026-02-05' },
|
|
{ id: 'u3', name: 'Alice Wong', email: 'alice@photo.me', joined: '2026-03-12' },
|
|
];
|
|
|
|
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 FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) {
|
|
return (
|
|
<label style="display:block">
|
|
<span style="font-size:13px;font-weight:600;color:#374151">
|
|
{props.label}{props.required && <span style="margin-left:2px;color:#FF5E13">*</span>}
|
|
</span>
|
|
<Show when={props.type === 'textarea'} fallback={
|
|
<input
|
|
type={props.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;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
|
|
/>
|
|
}>
|
|
<textarea
|
|
value={props.value}
|
|
onInput={(e) => props.onInput(e.currentTarget.value)}
|
|
placeholder={props.placeholder}
|
|
style="display:block;margin-top:6px;min-height:100px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box;resize:vertical"
|
|
/>
|
|
</Show>
|
|
</label>
|
|
);
|
|
}
|
|
|
|
export default function ExternalRoleManagementPage() {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [view, setView] = createSignal<'list' | 'form'>('list');
|
|
const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
|
|
const [formTab, setFormTab] = createSignal<'general' | 'access' | 'settings'>('general');
|
|
const [detailTab, setDetailTab] = createSignal<'permissions' | 'users' | 'logs'>('permissions');
|
|
|
|
const [search, setSearch] = createSignal('');
|
|
const [statusFilter, setStatusFilter] = createSignal('all');
|
|
const [sortBy, setSortBy] = createSignal<'name_asc' | 'name_desc' | 'users_desc' | 'users_asc'>('name_asc');
|
|
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
|
|
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
|
|
const [rows, setRows] = createSignal<ExternalRoleRecord[]>([]);
|
|
const [viewingRole, setViewingRole] = createSignal<ExternalRoleRecord | null>(null);
|
|
const [editingId, setEditingId] = createSignal<string | null>(null);
|
|
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
|
|
|
// Form Signals
|
|
const [name, setName] = createSignal('');
|
|
const [code, setCode] = createSignal('');
|
|
const [vertical, setVertical] = createSignal<'jobs' | 'marketplace'>('marketplace');
|
|
const [category, setCategory] = createSignal<ExternalRoleRecord['category']>('provider');
|
|
const [assignedUserTypes, setAssignedUserTypes] = createSignal<string[]>([]);
|
|
const [onboardingId, setOnboardingId] = createSignal(ONBOARDING_SCHEMAS[0]);
|
|
const [enabledModules, setEnabledModules] = createSignal<string[]>([]);
|
|
const [permissions, setPermissions] = createSignal<Record<string, string[]>>({});
|
|
const [reqOnbAppr, setReqOnbAppr] = createSignal(true);
|
|
const [reqLeadAppr, setReqLeadAppr] = createSignal(false);
|
|
const [reqJobAppr, setReqJobAppr] = createSignal(false);
|
|
const [limitsJson, setLimitsJson] = createSignal('{}');
|
|
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
|
|
const [isSaving, setIsSaving] = createSignal(false);
|
|
const [error, setError] = createSignal('');
|
|
|
|
createEffect(() => {
|
|
if (searchParams.view === 'form') setView('form');
|
|
else if (searchParams.editingId) {
|
|
const row = rows().find(r => r.id === searchParams.editingId);
|
|
if (row) openEdit(row);
|
|
}
|
|
});
|
|
|
|
const load = async () => { setRows(FALLBACK_ROLES); };
|
|
onMount(() => void load());
|
|
|
|
const filteredRows = createMemo(() => {
|
|
let r = rows();
|
|
if (statusFilter() !== 'all') r = r.filter((d) => d.status === statusFilter().toUpperCase());
|
|
const q = search().toLowerCase();
|
|
if (q) {
|
|
r = r.filter(r => r.name.toLowerCase().includes(q) || r.code.toLowerCase().includes(q));
|
|
}
|
|
const sorted = [...r];
|
|
const mode = sortBy();
|
|
sorted.sort((a, b) => {
|
|
if (mode === 'name_desc') return b.name.localeCompare(a.name);
|
|
if (mode === 'users_desc') return b.assignedUsers - a.assignedUsers;
|
|
if (mode === 'users_asc') return a.assignedUsers - b.assignedUsers;
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
return sorted;
|
|
});
|
|
|
|
const moduleOptions = createMemo(() => MODULES_BY_VERTICAL[vertical()]);
|
|
|
|
const resetForm = () => {
|
|
setEditingId(null); setName(''); setCode(''); setVertical('marketplace'); setCategory('provider');
|
|
setAssignedUserTypes([]); setOnboardingId(ONBOARDING_SCHEMAS[0]); setEnabledModules([]); setPermissions({});
|
|
setReqOnbAppr(true); setReqLeadAppr(false); setReqJobAppr(false); setLimitsJson('{}');
|
|
setStatus('ACTIVE'); setFormTab('general');
|
|
setSearchParams({ view: undefined, editingId: undefined });
|
|
};
|
|
|
|
const openCreate = () => { resetForm(); setView('form'); };
|
|
const openEdit = (row: ExternalRoleRecord) => {
|
|
setEditingId(row.id); setName(row.name); setCode(row.code); setVertical(row.vertical);
|
|
setCategory(row.category); setAssignedUserTypes(row.assignedUserTypes || []); setOnboardingId(row.onboardingSchemaId);
|
|
setEnabledModules(row.modules); setPermissions(row.permissions);
|
|
setReqOnbAppr(row.requiresOnboardingApproval); setReqLeadAppr(row.requiresLeadApproval);
|
|
setReqJobAppr(row.requiresJobApproval); setLimitsJson(row.featureLimits);
|
|
setStatus(row.status); setView('form'); setOpenMenuId(null);
|
|
};
|
|
|
|
const toggleUserType = (type: string) => {
|
|
setAssignedUserTypes(prev => prev.includes(type) ? prev.filter(t => t !== type) : [...prev, type]);
|
|
};
|
|
|
|
const validateJson = (json: string) => {
|
|
try {
|
|
JSON.parse(json);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const toggleModule = (mod: string) => {
|
|
setEnabledModules(prev => {
|
|
const next = prev.includes(mod) ? prev.filter(m => m !== mod) : [...prev, mod];
|
|
if (!next.includes(mod)) {
|
|
const nextPerms = { ...permissions() };
|
|
delete nextPerms[mod];
|
|
setPermissions(nextPerms);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const togglePermission = (mod: string, act: string) => {
|
|
const current = permissions()[mod] || [];
|
|
const next = current.includes(act) ? current.filter(a => a !== act) : [...current, act];
|
|
setPermissions({ ...permissions(), [mod]: next });
|
|
};
|
|
|
|
const save = async () => {
|
|
if (!name().trim() || !code().trim()) {
|
|
setError('Role name and code are required.');
|
|
setFormTab('general');
|
|
return;
|
|
}
|
|
if (!validateJson(limitsJson())) {
|
|
setError('Invalid JSON in feature limits.');
|
|
setFormTab('settings');
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
setError('');
|
|
|
|
try {
|
|
// Simulate API call
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
resetForm();
|
|
setView('list');
|
|
} catch (err: any) {
|
|
setError(err?.message || 'Failed to save role.');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AdminShell>
|
|
<div class="w-full space-y-6 pb-8">
|
|
|
|
<div style="margin-bottom: 1.5rem">
|
|
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">External Role Management</h1>
|
|
<p class="mt-1 text-[14px] text-[#6B7280]">Configure granular permissions and workflows for external platform users</p>
|
|
</div>
|
|
|
|
{/* ── LIST VIEW ── */}
|
|
<Show when={view() === 'list'}>
|
|
<div>
|
|
{/* Tabs */}
|
|
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
|
{([
|
|
{ key: 'all', label: 'All External Roles', action: () => { setListTab('all'); setStatusFilter('all'); } },
|
|
{ key: 'create', label: 'Create External Role', action: () => { setListTab('create'); openCreate(); } },
|
|
{ key: 'view', label: 'View External Role', action: () => setListTab('view') },
|
|
] as const).map((tab) => (
|
|
<button
|
|
type="button"
|
|
onClick={tab.action}
|
|
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* View Role panel */}
|
|
<Show when={listTab() === 'view'}>
|
|
<Show
|
|
when={!viewingRole()}
|
|
>
|
|
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;padding:48px 24px;text-align:center">
|
|
<p style="font-size:15px;font-weight:600;color:#111827">No role selected</p>
|
|
<p style="margin-top:6px;font-size:13px;color:#6B7280">Click the <strong>⋮</strong> menu on any role row and choose <strong>View Details</strong>.</p>
|
|
</div>
|
|
</Show>
|
|
<Show when={viewingRole()}>
|
|
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
|
{/* Header */}
|
|
<div style="padding:20px 24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
|
|
<div>
|
|
<h2 style="font-size:18px;font-weight:700;color:#111827">{viewingRole()!.name}</h2>
|
|
<p style="margin-top:2px;font-size:13px;color:#6B7280">{viewingRole()!.vertical.toUpperCase()} • {viewingRole()!.category.toUpperCase()}</p>
|
|
</div>
|
|
<StatusBadge status={viewingRole()!.status} />
|
|
</div>
|
|
{/* Detail Tabs */}
|
|
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
|
|
{(['permissions', 'users', 'logs'] as const).map((tab, i) => {
|
|
const labels = ['Module Permissions', 'Assigned Users', 'Activity Logs'];
|
|
const active = () => detailTab() === tab;
|
|
return (
|
|
<button type="button" onClick={() => setDetailTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
|
|
{labels[i]}
|
|
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div style="padding:24px">
|
|
<Show when={detailTab() === 'permissions'}>
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;overflow:hidden;opacity:0.8">
|
|
<table style="width:100%;border-collapse:collapse">
|
|
<thead style="background:#F9FAFB">
|
|
<tr style="text-align:left">
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">Module</th>
|
|
<For each={PERMISSION_ACTIONS}>
|
|
{act => <th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase;text-align:center">{act.label}</th>}
|
|
</For>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<For each={MODULES_BY_VERTICAL[viewingRole()!.vertical]}>
|
|
{mod => {
|
|
const isEnabled = viewingRole()!.modules.includes(mod.key);
|
|
return (
|
|
<tr style={`border-top:1px solid #E5E7EB; ${!isEnabled ? 'background:#F9FAFB;opacity:0.5' : ''}`}>
|
|
<td style="padding:12px 16px">
|
|
<p style="font-size:13px;font-weight:600;color:#111827">{mod.label}</p>
|
|
</td>
|
|
<For each={PERMISSION_ACTIONS}>
|
|
{act => (
|
|
<td style="padding:12px 16px;text-align:center">
|
|
<input type="checkbox" checked={isEnabled && (viewingRole()!.permissions[mod.key] || []).includes(act.key)} disabled style="width:16px;height:16px;accent-color:#FF5E13" />
|
|
</td>
|
|
)}
|
|
</For>
|
|
</tr>
|
|
);
|
|
}}
|
|
</For>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Show>
|
|
|
|
<Show when={detailTab() === 'users'}>
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;overflow:hidden">
|
|
<table style="width:100%;border-collapse:collapse">
|
|
<thead style="background:#F9FAFB">
|
|
<tr style="text-align:left">
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">User Name</th>
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">Email</th>
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">Joined Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<For each={FALLBACK_ASSIGNED_USERS}>
|
|
{user => (
|
|
<tr style="border-top:1px solid #E5E7EB">
|
|
<td style="padding:12px 16px;font-size:13px;font-weight:600;color:#111827">{user.name}</td>
|
|
<td style="padding:12px 16px;font-size:13px;color:#6B7280">{user.email}</td>
|
|
<td style="padding:12px 16px;font-size:13px;color:#6B7280">{user.joined}</td>
|
|
</tr>
|
|
)}
|
|
</For>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Show>
|
|
|
|
<Show when={detailTab() === 'logs'}>
|
|
<div style="display:flex;flex-direction:column;gap:12px">
|
|
<For each={FALLBACK_LOGS}>
|
|
{log => (
|
|
<div style="display:flex;align-items:flex-start;gap:12px;padding:14px;border:1px solid #E5E7EB;border-radius:12px;background:#F9FAFB">
|
|
<div style="width:32px;height:32px;border-radius:50%;background:#FFF3EE;display:flex;align-items:center;justify-content:center;color:#FF5E13;flex-shrink:0">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|
</div>
|
|
<div style="flex:1">
|
|
<p style="font-size:13px;font-weight:600;color:#111827">{log.user} <span style="font-weight:400;color:#6B7280">{log.action}</span> for {log.target}</p>
|
|
<p style="font-size:11px;color:#9CA3AF;margin-top:2px">{log.date}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div style="display:flex;align-items:center;gap:10px;padding:14px 24px;border-top:1px solid #E5E7EB">
|
|
<button type="button" onClick={() => openEdit(viewingRole()!)} style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">Edit Role</button>
|
|
<button type="button" onClick={() => { setViewingRole(null); setListTab('all'); }} style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Back to List</button>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
</Show>
|
|
|
|
<div style={{ display: listTab() === 'view' ? 'none' : 'block' }}>
|
|
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
|
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
|
<input
|
|
value={search()}
|
|
onInput={(e) => setSearch(e.currentTarget.value)}
|
|
placeholder="Search roles..."
|
|
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
|
|
/>
|
|
<div style="position:relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => { setSortMenuOpen((v) => !v); setFilterMenuOpen(false); }}
|
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
|
>
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
|
|
Sort
|
|
</button>
|
|
<Show when={sortMenuOpen()}>
|
|
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
|
{(['name_asc', 'name_desc', 'users_desc', 'users_asc'] as const).map((s, i) => (
|
|
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
|
|
{['Name (A-Z)', 'Name (Z-A)', 'Users (High-Low)', 'Users (Low-High)'][i]}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
<div style="position:relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
|
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
|
>
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
|
Filters
|
|
</button>
|
|
<Show when={filterMenuOpen()}>
|
|
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
|
{(['all', 'active', 'inactive'] as const).map((s) => (
|
|
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
|
|
{s === 'all' ? 'All Status' : s === 'active' ? 'Active' : 'Inactive'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full">
|
|
<thead>
|
|
<tr style="background:#0D0D2A;text-align:left">
|
|
{['Role Name', 'Vertical', 'Category', 'Onboarding', 'Status', 'Users', 'Actions'].map(h => (
|
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<For each={filteredRows()}>
|
|
{(row) => (
|
|
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
|
|
<td style="padding:12px 20px">
|
|
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
|
|
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">
|
|
<For each={row.assignedUserTypes}>
|
|
{type => <span style="font-size:10px;padding:1px 6px;border-radius:4px;background:#F3F4F6;color:#6B7280;font-weight:600">{type}</span>}
|
|
</For>
|
|
</div>
|
|
</td>
|
|
<td style="padding:12px 20px;font-size:12px;text-transform:uppercase;font-weight:700;color:#6B7280">{row.vertical}</td>
|
|
<td style="padding:12px 20px;font-size:12px;text-transform:capitalize;color:#6B7280">{row.category}</td>
|
|
<td style="padding:12px 20px;font-size:12px;color:#6B7280">{row.onboardingSchemaId}</td>
|
|
<td style="padding:12px 20px"><StatusBadge status={row.status} /></td>
|
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.assignedUsers} users</td>
|
|
<td style="padding:12px 20px;position:relative">
|
|
<button type="button" onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)} style="display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;border-radius:8px;color:#9CA3AF;background:none;border:none;cursor:pointer">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
|
|
</button>
|
|
<Show when={openMenuId() === row.id}>
|
|
<div style="position:absolute;right:20px;top:44px;z-index:20;width:180px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.12)">
|
|
<button type="button" onClick={() => { setViewingRole(row); setListTab('view'); setOpenMenuId(null); }} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">View Details</button>
|
|
<button type="button" onClick={() => openEdit(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">Edit Role</button>
|
|
<button type="button" style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left">Delete Role</button>
|
|
</div>
|
|
</Show>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</For>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
|
|
{/* ── FORM VIEW ── */}
|
|
<Show when={view() === 'form'}>
|
|
<div>
|
|
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
|
<button type="button" onClick={() => resetForm()} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">All External Roles</button>
|
|
<button type="button" style="padding-bottom:12px;font-size:14px;font-weight:500;color:#FF5E13;border-bottom:2px solid #FF5E13;background:none;cursor:pointer;margin-bottom:-1px">{editingId() ? 'Edit External Role' : 'Create External Role'}</button>
|
|
</div>
|
|
|
|
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
|
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
|
|
{(['general', 'access', 'settings'] as const).map((tab, i) => {
|
|
const labels = ['General Config', 'Module Permissions', 'Workflow & Limits'];
|
|
const active = () => formTab() === tab;
|
|
return (
|
|
<button type="button" onClick={() => setFormTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
|
|
{labels[i]}
|
|
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div style="padding:24px">
|
|
<Show when={error()}>
|
|
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
|
|
{error()}
|
|
</div>
|
|
</Show>
|
|
|
|
<Show when={formTab() === 'general'}>
|
|
<div style="display:flex;flex-direction:column;gap:24px;max-width:800px">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
|
<FormInput label="Role Name" required value={name()} onInput={setName} placeholder="e.g. Verified Photographer" />
|
|
<FormInput label="Role Code" required value={code()} onInput={setCode} placeholder="e.g. photographer" />
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
|
<label style="display:block">
|
|
<span style="font-size:13px;font-weight:600;color:#374151">App Vertical *</span>
|
|
<select value={vertical()} onChange={e => setVertical(e.currentTarget.value as any)} style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;outline:none">
|
|
<option value="jobs">Jobs</option>
|
|
<option value="marketplace">Marketplace</option>
|
|
</select>
|
|
</label>
|
|
<label style="display:block">
|
|
<span style="font-size:13px;font-weight:600;color:#374151">Role Category *</span>
|
|
<select value={category()} onChange={e => setCategory(e.currentTarget.value as any)} style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;outline:none">
|
|
<option value="provider">Provider</option>
|
|
<option value="employer">Employer</option>
|
|
<option value="consumer">Consumer</option>
|
|
<option value="specialist">Specialist</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<div>
|
|
<span style="font-size:13px;font-weight:600;color:#374151">Assigned User Types *</span>
|
|
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px">
|
|
<For each={USER_TYPE_OPTIONS}>
|
|
{type => {
|
|
const active = () => assignedUserTypes().includes(type);
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleUserType(type)}
|
|
style={`height:32px;border-radius:8px;padding:0 12px;font-size:11px;font-weight:600;cursor:pointer;transition:all 0.2s;${active() ? 'background:#FF5E13;color:white;border:1px solid #FF5E13' : 'background:white;color:#6B7280;border:1px solid #E5E7EB'}`}
|
|
>
|
|
{type.replace('_', ' ')}
|
|
</button>
|
|
);
|
|
}}
|
|
</For>
|
|
</div>
|
|
</div>
|
|
|
|
<label style="display:block">
|
|
<span style="font-size:13px;font-weight:600;color:#374151">Onboarding Schema *</span>
|
|
<select value={onboardingId()} onChange={e => setOnboardingId(e.currentTarget.value)} style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;outline:none">
|
|
<For each={ONBOARDING_SCHEMAS}>{id => <option value={id}>{id}</option>}</For>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</Show>
|
|
|
|
<Show when={formTab() === 'access'}>
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;overflow:hidden">
|
|
<table style="width:100%;border-collapse:collapse">
|
|
<thead style="background:#F9FAFB">
|
|
<tr style="text-align:left">
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">Module</th>
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase;text-align:center">Enabled</th>
|
|
<For each={PERMISSION_ACTIONS}>
|
|
{act => <th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase;text-align:center">{act.label}</th>}
|
|
</For>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<For each={moduleOptions()}>
|
|
{mod => (
|
|
<tr style="border-top:1px solid #E5E7EB">
|
|
<td style="padding:12px 16px">
|
|
<p style="font-size:13px;font-weight:600;color:#111827">{mod.label}</p>
|
|
<p style="font-size:11px;color:#6B7280">{mod.desc}</p>
|
|
</td>
|
|
<td style="padding:12px 16px;text-align:center">
|
|
<input type="checkbox" checked={enabledModules().includes(mod.key)} onChange={() => toggleModule(mod.key)} style="width:16px;height:16px;accent-color:#FF5E13;cursor:pointer" />
|
|
</td>
|
|
<For each={PERMISSION_ACTIONS}>
|
|
{act => (
|
|
<td style="padding:12px 16px;text-align:center">
|
|
<input
|
|
type="checkbox"
|
|
disabled={!enabledModules().includes(mod.key)}
|
|
checked={(permissions()[mod.key] || []).includes(act.key)}
|
|
onChange={() => togglePermission(mod.key, act.key)}
|
|
style="width:16px;height:16px;accent-color:#FF5E13;cursor:pointer"
|
|
/>
|
|
</td>
|
|
)}
|
|
</For>
|
|
</tr>
|
|
)}
|
|
</For>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Show>
|
|
|
|
<Show when={formTab() === 'settings'}>
|
|
<div style="display:flex;flex-direction:column;gap:24px;max-width:800px">
|
|
<div>
|
|
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:12px">Workflow Approvals</h3>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
|
{[
|
|
{ l: 'Review onboarding submissions', v: reqOnbAppr, s: setReqOnbAppr },
|
|
{ l: 'Review incoming leads', v: reqLeadAppr, s: setReqLeadAppr },
|
|
{ l: 'Review job posts', v: reqJobAppr, s: setReqJobAppr }
|
|
].map(item => (
|
|
<label style="display:flex;align-items:center;gap:10px;padding:12px;border:1px solid #E5E7EB;border-radius:10px;cursor:pointer;background:#F9FAFB">
|
|
<input type="checkbox" checked={item.v()} onChange={e => item.s(e.currentTarget.checked)} style="width:16px;height:16px;accent-color:#FF5E13" />
|
|
<span style="font-size:13px;color:#374151">{item.l}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<FormInput label="Feature Limits (JSON)" type="textarea" value={limitsJson()} onInput={setLimitsJson} placeholder='{"maxActiveJobs": 5, "maxApplications": 10}' />
|
|
<Show when={limitsJson().trim() && !validateJson(limitsJson())}>
|
|
<p style="margin-top:4px;font-size:12px;color:#DC2626;font-weight:500">Invalid JSON format</p>
|
|
</Show>
|
|
</div>
|
|
|
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px;border-radius:10px;background:#F9FAFB;border:1px solid #E5E7EB;max-width:400px">
|
|
<div>
|
|
<p style="font-size:13px;font-weight:600;color:#111827">Active Status</p>
|
|
<p style="font-size:12px;color:#6B7280">Role is available for registration.</p>
|
|
</div>
|
|
<button onClick={() => setStatus(s => s === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE')} style={`width:40px;height:20px;border-radius:10px;position:relative;cursor:pointer;border:none;transition:background 0.2s;${status() === 'ACTIVE' ? 'background:#FF5E13' : 'background:#D1D5DB'}`}>
|
|
<div style={`width:16px;height:16px;background:white;border-radius:50%;position:absolute;top:2px;transition:left 0.2s;${status() === 'ACTIVE' ? 'left:22px' : 'left:2px'}`} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
|
|
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;border-top:1px solid #E5E7EB;padding:14px 24px">
|
|
<button type="button" onClick={() => resetForm()} style="height:38px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 20px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Cancel</button>
|
|
<button type="button" onClick={() => void save()} disabled={isSaving()} style={`height:38px;border-radius:10px;background:#0D0D2A;padding:0 24px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer;opacity:${isSaving() ? 0.7 : 1}`}>
|
|
{isSaving() ? 'Saving...' : editingId() ? 'Update Role' : 'Create Role'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
|
|
{/* ── DETAIL VIEW ── */}
|
|
<Show when={listTab() === 'view' && viewingRole()}>
|
|
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
|
<div style="padding:24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
|
|
<div>
|
|
<h2 style="font-size:20px;font-weight:700;color:#111827">{viewingRole()!.name}</h2>
|
|
<p style="font-size:14px;color:#6B7280;margin-top:2px">{viewingRole()!.vertical.toUpperCase()} • {viewingRole()!.category.toUpperCase()} • Schema: {viewingRole()!.onboardingSchemaId}</p>
|
|
</div>
|
|
<div style="display:flex;gap:10px">
|
|
<button type="button" onClick={() => openEdit(viewingRole()!)} style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">Edit Role</button>
|
|
<StatusBadge status={viewingRole()!.status} />
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
|
|
{(['permissions', 'users', 'logs'] as const).map((tab, i) => {
|
|
const labels = ['Module Permissions', 'Assigned Users', 'Activity Logs'];
|
|
const active = () => detailTab() === tab;
|
|
return (
|
|
<button type="button" onClick={() => setDetailTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
|
|
{labels[i]}
|
|
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div style="padding:24px">
|
|
<Show when={detailTab() === 'permissions'}>
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;overflow:hidden;opacity:0.8">
|
|
<table style="width:100%;border-collapse:collapse">
|
|
<thead style="background:#F9FAFB">
|
|
<tr style="text-align:left">
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">Module</th>
|
|
<For each={PERMISSION_ACTIONS}>
|
|
{act => <th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase;text-align:center">{act.label}</th>}
|
|
</For>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<For each={(MODULES_BY_VERTICAL as any)[viewingRole()!.vertical]}>
|
|
{(mod: any) => {
|
|
const isEnabled = viewingRole()!.modules.includes(mod.key);
|
|
return (
|
|
<tr style={`border-top:1px solid #E5E7EB; ${!isEnabled ? 'background:#F9FAFB;opacity:0.5' : ''}`}>
|
|
<td style="padding:12px 16px">
|
|
<p style="font-size:13px;font-weight:600;color:#111827">{mod.label}</p>
|
|
</td>
|
|
<For each={PERMISSION_ACTIONS}>
|
|
{act => (
|
|
<td style="padding:12px 16px;text-align:center">
|
|
<input type="checkbox" checked={isEnabled && (viewingRole()!.permissions[mod.key] || []).includes(act.key)} disabled style="width:16px;height:16px;accent-color:#FF5E13" />
|
|
</td>
|
|
)}
|
|
</For>
|
|
</tr>
|
|
);
|
|
}}
|
|
</For>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Show>
|
|
|
|
<Show when={detailTab() === 'users'}>
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;overflow:hidden">
|
|
<table style="width:100%;border-collapse:collapse">
|
|
<thead style="background:#F9FAFB">
|
|
<tr style="text-align:left">
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">User Name</th>
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">Email</th>
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase">Joined Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<For each={FALLBACK_ASSIGNED_USERS}>
|
|
{user => (
|
|
<tr style="border-top:1px solid #E5E7EB">
|
|
<td style="padding:12px 16px;font-size:13px;font-weight:600;color:#111827">{user.name}</td>
|
|
<td style="padding:12px 16px;font-size:13px;color:#6B7280">{user.email}</td>
|
|
<td style="padding:12px 16px;font-size:13px;color:#6B7280">{user.joined}</td>
|
|
</tr>
|
|
)}
|
|
</For>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Show>
|
|
|
|
<Show when={detailTab() === 'logs'}>
|
|
<div style="display:flex;flex-direction:column;gap:12px">
|
|
<For each={FALLBACK_LOGS}>
|
|
{log => (
|
|
<div style="display:flex;align-items:flex-start;gap:12px;padding:14px;border:1px solid #E5E7EB;border-radius:12px;background:#F9FAFB">
|
|
<div style="width:32px;height:32px;border-radius:50%;background:#FFF3EE;display:flex;align-items:center;justify-content:center;color:#FF5E13;flex-shrink:0">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|
</div>
|
|
<div style="flex:1">
|
|
<p style="font-size:13px;font-weight:600;color:#111827">{log.user} <span style="font-weight:400;color:#6B7280">{log.action}</span> for {log.target}</p>
|
|
<p style="font-size:11px;color:#9CA3AF;margin-top:2px">{log.date}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
|
|
<div style="padding:16px 24px;border-top:1px solid #E5E7EB;display:flex;justify-content:flex-end">
|
|
<button type="button" onClick={() => resetForm()} style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Back to List</button>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
|
|
</div>
|
|
</AdminShell>
|
|
);
|
|
}
|