2026-03-30 04:48:09 +02:00
|
|
|
import { For, Show, createEffect, createMemo, createResource, createSignal, onMount } from 'solid-js';
|
2026-03-27 05:35:18 +01:00
|
|
|
import type { CrudRecord } from '~/lib/admin/types';
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
2026-04-07 22:12:52 +02:00
|
|
|
const API = '';
|
2026-03-30 04:48:09 +02:00
|
|
|
const ACTIONS = ['View', 'Create', 'Update', 'Delete'] as const;
|
|
|
|
|
const STATIC_MODULES = [
|
|
|
|
|
'Department Management', 'Designation Management', 'Internal Role Management',
|
2026-04-08 22:12:38 +02:00
|
|
|
'Employee Management', 'External Role Management',
|
2026-03-30 04:48:09 +02:00
|
|
|
'Internal Dashboard Management', 'External Dashboard Management', 'Verification Management',
|
|
|
|
|
'Approval Management', 'Users Management', 'Company Management', 'Candidate Management',
|
|
|
|
|
'Customer Management', 'Photographer Management', 'Makeup Artist Management',
|
|
|
|
|
'Tutor Management', 'Developer Management', 'Fitness Trainer Management',
|
|
|
|
|
'Graphic Designer Management', 'Social Media Management', 'Video Editor Management',
|
|
|
|
|
'Catering Services Management', 'Jobs Management', 'Leads Management',
|
|
|
|
|
'Applications Management', 'Responses Management', 'Review Management',
|
|
|
|
|
'Pricing Management', 'Credit Management', 'Coupon Management', 'Discount Management',
|
|
|
|
|
'Tax Management', 'Order Management', 'Invoice Management', 'Ledger Management',
|
|
|
|
|
'Knowledge Base Management', 'Support Management', 'Report Management', 'Notifications',
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
function formatRoleKey(input: string): string {
|
|
|
|
|
return input
|
|
|
|
|
.trim()
|
|
|
|
|
.toUpperCase()
|
|
|
|
|
.replace(/[^A-Z0-9]+/g, '_')
|
|
|
|
|
.replace(/^_+|_+$/g, '')
|
|
|
|
|
.replace(/_{2,}/g, '_');
|
|
|
|
|
}
|
2026-03-27 05:35:18 +01:00
|
|
|
|
|
|
|
|
type RoleRecord = CrudRecord & {
|
2026-03-27 19:27:35 +01:00
|
|
|
key?: string;
|
2026-03-27 05:35:18 +01:00
|
|
|
department?: string;
|
2026-03-27 19:27:35 +01:00
|
|
|
departmentId?: string;
|
|
|
|
|
description?: string;
|
2026-03-27 05:35:18 +01:00
|
|
|
usersAssigned?: number;
|
|
|
|
|
permissionsCount?: number;
|
2026-03-27 19:27:35 +01:00
|
|
|
canApproveRequests?: boolean;
|
|
|
|
|
canManageSystemSettings?: boolean;
|
2026-03-27 05:35:18 +01:00
|
|
|
createdDate?: string;
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
};
|
|
|
|
|
|
2026-03-27 19:27:35 +01:00
|
|
|
type DepartmentOption = { id: string; name: string };
|
2026-03-30 04:48:09 +02:00
|
|
|
type Permission = { key: string; module: string; action: string };
|
|
|
|
|
|
|
|
|
|
function makeKey(module: string, action: string) {
|
|
|
|
|
return `${module.replace(/ /g, '_').toLowerCase()}:${action.toLowerCase()}`;
|
|
|
|
|
}
|
2026-03-27 02:28:34 +01:00
|
|
|
|
2026-03-30 04:48:09 +02:00
|
|
|
async function loadPermissions(): Promise<Permission[]> {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API}/api/admin/permissions`);
|
|
|
|
|
if (!res.ok) throw new Error();
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
return Array.isArray(data)
|
|
|
|
|
? data
|
|
|
|
|
: [];
|
|
|
|
|
} catch {
|
|
|
|
|
return STATIC_MODULES.flatMap((module) =>
|
|
|
|
|
ACTIONS.map((action) => ({
|
|
|
|
|
key: makeKey(module, action),
|
|
|
|
|
module,
|
|
|
|
|
action,
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-27 19:27:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeRole(item: any, idx: number): RoleRecord {
|
|
|
|
|
return {
|
|
|
|
|
id: String(item.id ?? `role-${idx + 1}`),
|
|
|
|
|
name: String(item.name ?? ''),
|
|
|
|
|
key: item.key ? String(item.key) : undefined,
|
|
|
|
|
department: item.department_name ? String(item.department_name) : undefined,
|
|
|
|
|
departmentId: item.department_id ? String(item.department_id) : undefined,
|
|
|
|
|
description: item.description ? String(item.description) : undefined,
|
|
|
|
|
usersAssigned: Number(item.users_assigned ?? 0),
|
|
|
|
|
permissionsCount: Number(item.permissions_count ?? 0),
|
|
|
|
|
canApproveRequests: Boolean(item.can_approve_requests ?? false),
|
|
|
|
|
canManageSystemSettings: Boolean(item.can_manage_system_settings ?? false),
|
|
|
|
|
status: item.is_active === false ? 'INACTIVE' : 'ACTIVE',
|
|
|
|
|
updatedAt: String(item.updated_at ?? item.created_at ?? ''),
|
|
|
|
|
createdDate: String(item.created_at ?? ''),
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-03-25 22:15:06 +01:00
|
|
|
|
2026-03-27 02:28:34 +01:00
|
|
|
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>
|
|
|
|
|
);
|
feat(admin): redesign sidebar, dashboard, dept, designation & roles UI
- Sidebar: white bg, rounded pill nav items, orange left indicator for active
- Dashboard: remove Export/View All buttons, add Customise Dashboard + drag handles on widgets
- Department/Designation/Roles: new design system with orange label header, stat cards, light table header, 3-dot action menus, status badges
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 08:01:23 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 02:28:34 +01:00
|
|
|
function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string }) {
|
feat(admin): pixel-perfect UI overhaul for department, designation, roles, employees pages
- Rewrote all layout/spacing with inline styles (Tailwind v4 doesn't generate most utility classes)
- AdminSidebar: all 37 modules in 9 groups, scrollable, 220px/64px collapse, no bottom user section
- AdminShell: header height 64px, user avatar top-right (Gmail-style), removed search bar
- Department: orange-only status badges, dark navy table header (white text), edge-to-edge table, View/Create/All tabs, View action in row menu, form with inline styles
- Designation: full rewrite matching department pattern — same tabs, filter bar, table, form
- Roles/Employees: compact filter bar and table cell sizing fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:56:55 +01:00
|
|
|
return (
|
2026-03-27 02:28:34 +01:00
|
|
|
<label style="display:block">
|
|
|
|
|
<span style="font-size:13px;font-weight:600;color:#374151">
|
2026-03-27 05:35:18 +01:00
|
|
|
{props.label}{props.required && <span style="margin-left:2px;color:#FF5E13">*</span>}
|
2026-03-27 02:28:34 +01:00
|
|
|
</span>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={props.value}
|
|
|
|
|
onInput={(e) => props.onInput(e.currentTarget.value)}
|
|
|
|
|
placeholder={props.placeholder}
|
2026-03-27 05:35:18 +01:00
|
|
|
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"
|
2026-03-27 02:28:34 +01:00
|
|
|
/>
|
|
|
|
|
</label>
|
feat(admin): pixel-perfect UI overhaul for department, designation, roles, employees pages
- Rewrote all layout/spacing with inline styles (Tailwind v4 doesn't generate most utility classes)
- AdminSidebar: all 37 modules in 9 groups, scrollable, 220px/64px collapse, no bottom user section
- AdminShell: header height 64px, user avatar top-right (Gmail-style), removed search bar
- Department: orange-only status badges, dark navy table header (white text), edge-to-edge table, View/Create/All tabs, View action in row menu, form with inline styles
- Designation: full rewrite matching department pattern — same tabs, filter bar, table, form
- Roles/Employees: compact filter bar and table cell sizing fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:56:55 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
export default function RoleManagementPage() {
|
|
|
|
|
const [view, setView] = createSignal<'list' | 'form' | 'detail'>('list');
|
2026-03-30 04:48:09 +02:00
|
|
|
const [permissions] = createResource(loadPermissions);
|
2026-03-27 05:35:18 +01:00
|
|
|
const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
|
|
|
|
|
const [formTab, setFormTab] = createSignal<'general' | 'permissions' | 'settings'>('general');
|
|
|
|
|
const [detailTab, setDetailTab] = createSignal<'permissions' | 'users' | 'logs'>('permissions');
|
2026-03-27 19:27:35 +01:00
|
|
|
|
2026-03-25 22:15:06 +01:00
|
|
|
const [search, setSearch] = createSignal('');
|
2026-03-27 05:35:18 +01:00
|
|
|
const [rows, setRows] = createSignal<RoleRecord[]>([]);
|
|
|
|
|
const [viewingRole, setViewingRole] = createSignal<RoleRecord | null>(null);
|
2026-03-27 19:27:35 +01:00
|
|
|
const [viewingPermissions, setViewingPermissions] = createSignal<string[]>([]);
|
2026-03-27 05:35:18 +01:00
|
|
|
const [editingId, setEditingId] = createSignal<string | null>(null);
|
|
|
|
|
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
2026-03-27 19:27:35 +01:00
|
|
|
const [isLoading, setIsLoading] = createSignal(false);
|
|
|
|
|
const [error, setError] = createSignal('');
|
2026-03-30 04:48:09 +02:00
|
|
|
const [statusFilter, setStatusFilter] = createSignal<'all' | 'ACTIVE' | 'INACTIVE'>('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);
|
2026-03-25 22:15:06 +01:00
|
|
|
|
2026-03-27 19:27:35 +01:00
|
|
|
// Form state
|
2026-03-27 05:35:18 +01:00
|
|
|
const [name, setName] = createSignal('');
|
2026-03-27 19:27:35 +01:00
|
|
|
const [roleKey, setRoleKey] = createSignal('');
|
|
|
|
|
const [description, setDescription] = createSignal('');
|
|
|
|
|
const [departmentId, setDepartmentId] = createSignal('');
|
|
|
|
|
const [status, setStatus] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE');
|
|
|
|
|
const [canApproveRequests, setCanApproveRequests] = createSignal(false);
|
|
|
|
|
const [canManageSystemSettings, setCanManageSystemSettings] = createSignal(false);
|
|
|
|
|
const [selectedPermissions, setSelectedPermissions] = createSignal<Set<string>>(new Set());
|
|
|
|
|
const [isSaving, setIsSaving] = createSignal(false);
|
|
|
|
|
const [formError, setFormError] = createSignal('');
|
|
|
|
|
const [departments, setDepartments] = createSignal<DepartmentOption[]>([]);
|
2026-03-30 04:48:09 +02:00
|
|
|
const isViewingSuperAdmin = createMemo(() => (viewingRole()?.key || '').toUpperCase() === 'SUPER_ADMIN');
|
2026-03-27 02:28:34 +01:00
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
const load = async () => {
|
2026-03-27 19:27:35 +01:00
|
|
|
setIsLoading(true);
|
|
|
|
|
setError('');
|
|
|
|
|
try {
|
|
|
|
|
const params = new URLSearchParams({ audience: 'INTERNAL', per_page: '100', q: search().trim() });
|
2026-03-27 21:25:00 +01:00
|
|
|
const accessToken = typeof sessionStorage !== 'undefined'
|
|
|
|
|
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
|
|
|
|
: '';
|
|
|
|
|
const res = await fetch(`${API}/api/admin/roles?${params}`, {
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/json',
|
|
|
|
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
|
|
|
},
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
2026-03-27 19:27:35 +01:00
|
|
|
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
|
|
|
|
const payload = await res.json().catch(() => null);
|
|
|
|
|
const list: any[] = Array.isArray(payload)
|
|
|
|
|
? payload
|
|
|
|
|
: Array.isArray(payload?.roles)
|
|
|
|
|
? payload.roles
|
|
|
|
|
: [];
|
|
|
|
|
setRows(list.map(normalizeRole));
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
setError(err?.message || 'Could not reach roles API.');
|
|
|
|
|
setRows([]);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
2026-03-25 22:15:06 +01:00
|
|
|
};
|
|
|
|
|
|
2026-03-27 19:27:35 +01:00
|
|
|
const loadDepartments = async () => {
|
|
|
|
|
try {
|
2026-03-27 21:25:00 +01:00
|
|
|
const accessToken = typeof sessionStorage !== 'undefined'
|
|
|
|
|
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
|
|
|
|
: '';
|
|
|
|
|
const res = await fetch(`${API}/api/admin/departments?per_page=100`, {
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/json',
|
|
|
|
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
|
|
|
},
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
2026-03-27 19:27:35 +01:00
|
|
|
if (!res.ok) return;
|
|
|
|
|
const payload = await res.json().catch(() => null);
|
2026-04-08 22:12:38 +02:00
|
|
|
const list: any[] = Array.isArray(payload)
|
|
|
|
|
? payload
|
|
|
|
|
: Array.isArray(payload?.departments)
|
|
|
|
|
? payload.departments
|
|
|
|
|
: Array.isArray(payload?.data)
|
|
|
|
|
? payload.data
|
|
|
|
|
: Array.isArray(payload?.items)
|
|
|
|
|
? payload.items
|
|
|
|
|
: [];
|
2026-03-27 19:27:35 +01:00
|
|
|
setDepartments(list.map((d: any) => ({ id: String(d.id), name: String(d.name) })));
|
|
|
|
|
} catch { /* dropdown just empty */ }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMount(() => { void load(); void loadDepartments(); });
|
2026-03-25 22:15:06 +01:00
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
const filteredRows = createMemo(() => {
|
|
|
|
|
const q = search().toLowerCase();
|
2026-03-30 04:48:09 +02:00
|
|
|
let list = rows();
|
|
|
|
|
if (statusFilter() !== 'all') {
|
|
|
|
|
list = list.filter((r) => r.status === statusFilter());
|
|
|
|
|
}
|
|
|
|
|
if (q) {
|
|
|
|
|
list = list.filter(r =>
|
|
|
|
|
r.name.toLowerCase().includes(q) || (r.key || '').toLowerCase().includes(q)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const sorted = [...list];
|
|
|
|
|
sorted.sort((a, b) => {
|
|
|
|
|
if (sortBy() === 'name_desc') return b.name.localeCompare(a.name);
|
|
|
|
|
if (sortBy() === 'users_desc') return Number(b.usersAssigned || 0) - Number(a.usersAssigned || 0);
|
|
|
|
|
if (sortBy() === 'users_asc') return Number(a.usersAssigned || 0) - Number(b.usersAssigned || 0);
|
|
|
|
|
return a.name.localeCompare(b.name);
|
|
|
|
|
});
|
|
|
|
|
return sorted;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const exportCsv = () => {
|
|
|
|
|
const headers = ['Role Name', 'Role Key', 'Department', 'Users', 'Permissions', 'Status'];
|
|
|
|
|
const rowsData = filteredRows().map((row) => [
|
|
|
|
|
row.name || '',
|
|
|
|
|
row.key || '',
|
|
|
|
|
row.department || '',
|
|
|
|
|
String(row.usersAssigned ?? 0),
|
|
|
|
|
(row.key || '').toUpperCase() === 'SUPER_ADMIN' ? 'All' : String(row.permissionsCount ?? 0),
|
|
|
|
|
row.status || '',
|
|
|
|
|
]);
|
|
|
|
|
const csv = [headers, ...rowsData]
|
|
|
|
|
.map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
|
|
|
|
.join('\n');
|
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = `internal-roles-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const permissionKeyByModuleAction = createMemo(() => {
|
|
|
|
|
const map = new Map<string, string>();
|
|
|
|
|
const src = permissions() ?? [];
|
|
|
|
|
src.forEach((p) => {
|
|
|
|
|
const moduleKey = String(p.module || '').trim().toUpperCase();
|
|
|
|
|
const actionKey = String(p.action || '').trim().toUpperCase();
|
|
|
|
|
if (!moduleKey || !actionKey) return;
|
|
|
|
|
map.set(`${moduleKey}::${actionKey}`, String(p.key || '').trim());
|
|
|
|
|
});
|
|
|
|
|
return map;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const orderedModules = createMemo(() => {
|
|
|
|
|
const fromApi = Array.from(
|
|
|
|
|
new Set((permissions() ?? []).map((p) => String(p.module || '').trim()).filter(Boolean)),
|
2026-03-27 19:27:35 +01:00
|
|
|
);
|
2026-03-30 04:48:09 +02:00
|
|
|
const ordered = [...STATIC_MODULES.filter((m) => fromApi.includes(m))];
|
|
|
|
|
const extras = fromApi.filter((m) => !ordered.includes(m)).sort();
|
|
|
|
|
return [...ordered, ...extras];
|
2026-03-27 05:35:18 +01:00
|
|
|
});
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
2026-03-30 04:48:09 +02:00
|
|
|
const permissionKeyFor = (module: string, action: string) =>
|
|
|
|
|
permissionKeyByModuleAction().get(`${module.toUpperCase()}::${action.toUpperCase()}`) || '';
|
|
|
|
|
|
2026-03-27 19:27:35 +01:00
|
|
|
const togglePermission = (key: string) => {
|
|
|
|
|
setSelectedPermissions(prev => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
if (next.has(key)) next.delete(key);
|
|
|
|
|
else next.add(key);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
const resetForm = () => {
|
2026-03-27 19:27:35 +01:00
|
|
|
setEditingId(null); setName(''); setRoleKey(''); setDescription('');
|
|
|
|
|
setDepartmentId(''); setStatus('ACTIVE'); setCanApproveRequests(false);
|
|
|
|
|
setCanManageSystemSettings(false); setSelectedPermissions(new Set());
|
|
|
|
|
setFormTab('general'); setFormError('');
|
2026-03-27 02:28:34 +01:00
|
|
|
};
|
|
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
const openCreate = () => { resetForm(); setView('form'); };
|
2026-03-27 19:27:35 +01:00
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
const openEdit = (row: RoleRecord) => {
|
2026-03-27 19:27:35 +01:00
|
|
|
setEditingId(row.id);
|
|
|
|
|
setName(row.name); setRoleKey(row.key || '');
|
|
|
|
|
setDescription(row.description || '');
|
|
|
|
|
setDepartmentId(row.departmentId || '');
|
|
|
|
|
setStatus(row.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE');
|
|
|
|
|
setCanApproveRequests(Boolean(row.canApproveRequests));
|
|
|
|
|
setCanManageSystemSettings(Boolean(row.canManageSystemSettings));
|
|
|
|
|
setSelectedPermissions(new Set());
|
|
|
|
|
setFormTab('general'); setView('form'); setOpenMenuId(null);
|
|
|
|
|
// Fetch permission_keys for this role
|
2026-03-27 21:25:00 +01:00
|
|
|
fetch(`${API}/api/admin/roles/${row.id}`).then(r => r.json()).then((detail) => {
|
2026-03-27 19:27:35 +01:00
|
|
|
if (Array.isArray(detail?.permission_keys)) {
|
2026-03-27 21:25:00 +01:00
|
|
|
const keys = (detail.permission_keys as any[]).map((k) => String(k));
|
|
|
|
|
setSelectedPermissions(new Set<string>(keys));
|
2026-03-27 19:27:35 +01:00
|
|
|
}
|
|
|
|
|
}).catch(() => {});
|
2026-03-27 05:35:18 +01:00
|
|
|
};
|
2026-03-27 19:27:35 +01:00
|
|
|
|
|
|
|
|
const openDetail = async (row: RoleRecord) => {
|
2026-03-27 05:35:18 +01:00
|
|
|
setViewingRole(row); setView('detail'); setListTab('view'); setOpenMenuId(null);
|
2026-03-27 19:27:35 +01:00
|
|
|
setViewingPermissions([]);
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API}/api/admin/roles/${row.id}`);
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const detail = await res.json();
|
2026-03-27 21:25:00 +01:00
|
|
|
setViewingPermissions(Array.isArray(detail?.permission_keys) ? (detail.permission_keys as any[]).map((k: any) => String(k)) : []);
|
2026-03-27 19:27:35 +01:00
|
|
|
}
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const save = async () => {
|
2026-03-30 04:48:09 +02:00
|
|
|
if (isSaving()) return;
|
|
|
|
|
const normalizedRoleKey = editingId() ? formatRoleKey(roleKey()) : formatRoleKey(name());
|
|
|
|
|
if (!name().trim() || !normalizedRoleKey) {
|
2026-03-27 19:27:35 +01:00
|
|
|
setFormError('Role name and role key are required.');
|
|
|
|
|
setFormTab('general');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setIsSaving(true);
|
|
|
|
|
setFormError('');
|
|
|
|
|
try {
|
2026-03-27 21:25:00 +01:00
|
|
|
const accessToken = typeof sessionStorage !== 'undefined'
|
2026-03-30 04:48:09 +02:00
|
|
|
? (sessionStorage.getItem('nxtgauge_admin_access_token') || '').trim()
|
2026-03-27 21:25:00 +01:00
|
|
|
: '';
|
2026-03-27 19:27:35 +01:00
|
|
|
const isCreate = !editingId();
|
|
|
|
|
const endpoint = isCreate
|
|
|
|
|
? `${API}/api/admin/roles`
|
|
|
|
|
: `${API}/api/admin/roles/${editingId()}`;
|
|
|
|
|
const method = isCreate ? 'POST' : 'PATCH';
|
|
|
|
|
const body: Record<string, unknown> = {
|
|
|
|
|
name: name().trim(),
|
|
|
|
|
description: description().trim() || null,
|
|
|
|
|
is_active: status() === 'ACTIVE',
|
|
|
|
|
can_approve_requests: canApproveRequests(),
|
|
|
|
|
can_manage_system_settings: canManageSystemSettings(),
|
2026-03-27 21:25:00 +01:00
|
|
|
permission_keys: Array.from(selectedPermissions()) as string[],
|
2026-03-27 19:27:35 +01:00
|
|
|
};
|
|
|
|
|
if (departmentId().trim()) body.department_id = departmentId().trim();
|
|
|
|
|
if (isCreate) {
|
2026-03-30 04:48:09 +02:00
|
|
|
body.key = normalizedRoleKey;
|
2026-03-27 19:27:35 +01:00
|
|
|
body.audience = 'INTERNAL';
|
|
|
|
|
}
|
|
|
|
|
const res = await fetch(endpoint, {
|
|
|
|
|
method,
|
2026-03-27 21:25:00 +01:00
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
Accept: 'application/json',
|
|
|
|
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
|
|
|
},
|
|
|
|
|
credentials: 'include',
|
2026-03-27 19:27:35 +01:00
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
});
|
2026-03-30 04:48:09 +02:00
|
|
|
const raw = await res.text();
|
|
|
|
|
let message = '';
|
|
|
|
|
if (raw) {
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(raw) as { message?: string; error?: string };
|
|
|
|
|
message = parsed?.message || parsed?.error || '';
|
|
|
|
|
} catch {
|
|
|
|
|
message = raw;
|
|
|
|
|
}
|
2026-03-27 19:27:35 +01:00
|
|
|
}
|
2026-03-30 04:48:09 +02:00
|
|
|
if (!res.ok) throw new Error(message || `Request failed (${res.status})`);
|
2026-03-27 19:27:35 +01:00
|
|
|
setView('list');
|
|
|
|
|
resetForm();
|
|
|
|
|
await load();
|
|
|
|
|
} catch (err: any) {
|
2026-03-30 04:48:09 +02:00
|
|
|
setFormError(String(err?.message || '').trim() || 'Failed to save role.');
|
2026-03-27 19:27:35 +01:00
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-30 04:48:09 +02:00
|
|
|
createEffect(() => {
|
|
|
|
|
if (editingId()) return;
|
|
|
|
|
setRoleKey(formatRoleKey(name()));
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-27 19:27:35 +01:00
|
|
|
const deleteRole = async (id: string, roleName: string) => {
|
|
|
|
|
if (!window.confirm(`Delete role "${roleName}"?`)) return;
|
|
|
|
|
setOpenMenuId(null);
|
|
|
|
|
try {
|
2026-03-27 21:25:00 +01:00
|
|
|
const accessToken = typeof sessionStorage !== 'undefined'
|
2026-03-30 04:48:09 +02:00
|
|
|
? (sessionStorage.getItem('nxtgauge_admin_access_token') || '').trim()
|
2026-03-27 21:25:00 +01:00
|
|
|
: '';
|
|
|
|
|
const res = await fetch(`${API}/api/admin/roles/${id}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/json',
|
|
|
|
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
|
|
|
},
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
2026-03-27 19:27:35 +01:00
|
|
|
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
|
|
|
|
await load();
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
setError(err?.message || 'Failed to delete role.');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleStatus = async (row: RoleRecord) => {
|
|
|
|
|
setOpenMenuId(null);
|
|
|
|
|
try {
|
2026-03-27 21:25:00 +01:00
|
|
|
const accessToken = typeof sessionStorage !== 'undefined'
|
2026-03-30 04:48:09 +02:00
|
|
|
? (sessionStorage.getItem('nxtgauge_admin_access_token') || '').trim()
|
2026-03-27 21:25:00 +01:00
|
|
|
: '';
|
2026-03-27 19:27:35 +01:00
|
|
|
const res = await fetch(`${API}/api/admin/roles/${row.id}`, {
|
|
|
|
|
method: 'PATCH',
|
2026-03-27 21:25:00 +01:00
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
Accept: 'application/json',
|
|
|
|
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
|
|
|
},
|
|
|
|
|
credentials: 'include',
|
2026-03-27 19:27:35 +01:00
|
|
|
body: JSON.stringify({ is_active: row.status !== 'ACTIVE' }),
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
|
|
|
|
await load();
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
setError(err?.message || 'Failed to update status.');
|
|
|
|
|
}
|
2026-03-27 05:35:18 +01:00
|
|
|
};
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
|
|
|
|
|
return (
|
2026-03-27 05:35:18 +01:00
|
|
|
<div class="w-full space-y-6 pb-8">
|
2026-03-27 19:27:35 +01:00
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
<div style="margin-bottom: 1.5rem">
|
|
|
|
|
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Internal Role Management</h1>
|
|
|
|
|
<p class="mt-1 text-[14px] text-[#6B7280]">Define and manage organizational access levels with granular permission control</p>
|
feat(admin): redesign sidebar, dashboard, dept, designation & roles UI
- Sidebar: white bg, rounded pill nav items, orange left indicator for active
- Dashboard: remove Export/View All buttons, add Customise Dashboard + drag handles on widgets
- Department/Designation/Roles: new design system with orange label header, stat cards, light table header, 3-dot action menus, status badges
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 08:01:23 +01:00
|
|
|
</div>
|
2026-03-25 22:15:06 +01:00
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
{/* ── LIST VIEW ── */}
|
|
|
|
|
<Show when={view() === 'list'}>
|
|
|
|
|
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
|
|
|
|
{([
|
2026-03-27 19:27:35 +01:00
|
|
|
{ key: 'all', label: 'All Roles', action: () => { setListTab('all'); void load(); } },
|
2026-03-27 05:35:18 +01:00
|
|
|
{ key: 'create', label: 'Create Role', action: () => { setListTab('create'); openCreate(); } },
|
|
|
|
|
{ key: 'view', label: 'View 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>
|
2026-03-26 00:06:47 +01:00
|
|
|
|
2026-03-27 19:27:35 +01:00
|
|
|
<Show when={error()}>
|
|
|
|
|
<div style="margin-top:16px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">{error()}</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;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)">
|
2026-03-27 02:28:34 +01:00
|
|
|
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
|
|
|
|
<input
|
|
|
|
|
value={search()}
|
2026-03-27 19:27:35 +01:00
|
|
|
onInput={(e) => { setSearch(e.currentTarget.value); void load(); }}
|
2026-03-27 05:35:18 +01:00
|
|
|
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"
|
2026-03-27 02:28:34 +01:00
|
|
|
/>
|
2026-03-30 04:48:09 +02:00
|
|
|
<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 (A-Z)'],
|
|
|
|
|
['name_desc', 'Name (Z-A)'],
|
|
|
|
|
['users_desc', 'Users (High-Low)'],
|
|
|
|
|
['users_asc', 'Users (Low-High)'],
|
|
|
|
|
] as const).map(([key, label]) => (
|
|
|
|
|
<button type="button" onClick={() => { setSortBy(key); 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() === key ? '#FF5E13' : '#374151'};background:${sortBy() === key ? '#FFF1EB' : 'transparent'}`}>{label}</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', 'All Status'],
|
|
|
|
|
['ACTIVE', 'Active'],
|
|
|
|
|
['INACTIVE', 'Inactive'],
|
|
|
|
|
] as const).map(([key, label]) => (
|
2026-04-08 22:12:38 +02:00
|
|
|
<button type="button" onClick={() => { setStatusFilter(key as any); setFilterMenuOpen(false); void load(); }} 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() === key ? '#FF5E13' : '#374151'};background:${statusFilter() === key ? '#FFF1EB' : 'transparent'}`}>{label}</button>
|
2026-03-30 04:48:09 +02:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
<button type="button" onClick={exportCsv} style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
|
|
|
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
|
|
|
Export
|
|
|
|
|
</button>
|
2026-03-27 02:28:34 +01:00
|
|
|
</div>
|
feat(admin): redesign sidebar, dashboard, dept, designation & roles UI
- Sidebar: white bg, rounded pill nav items, orange left indicator for active
- Dashboard: remove Export/View All buttons, add Customise Dashboard + drag handles on widgets
- Department/Designation/Roles: new design system with orange label header, stat cards, light table header, 3-dot action menus, status badges
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 08:01:23 +01:00
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
<div class="overflow-x-auto">
|
|
|
|
|
<table class="min-w-full">
|
2026-03-27 02:28:34 +01:00
|
|
|
<thead>
|
2026-03-27 05:35:18 +01:00
|
|
|
<tr style="background:#0D0D2A;text-align:left">
|
2026-03-27 19:27:35 +01:00
|
|
|
{['Role Name', 'Role Key', 'Department', 'Users', 'Permissions', 'Status', 'Actions'].map(h => (
|
2026-03-27 05:35:18 +01:00
|
|
|
<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>
|
|
|
|
|
))}
|
feat(admin): redesign sidebar, dashboard, dept, designation & roles UI
- Sidebar: white bg, rounded pill nav items, orange left indicator for active
- Dashboard: remove Export/View All buttons, add Customise Dashboard + drag handles on widgets
- Department/Designation/Roles: new design system with orange label header, stat cards, light table header, 3-dot action menus, status badges
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 08:01:23 +01:00
|
|
|
</tr>
|
2026-03-27 02:28:34 +01:00
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
2026-03-27 19:27:35 +01:00
|
|
|
<Show
|
|
|
|
|
when={filteredRows().length > 0}
|
|
|
|
|
fallback={
|
|
|
|
|
<tr>
|
|
|
|
|
<td colspan="7" style="padding:48px 24px;text-align:center">
|
|
|
|
|
<Show when={isLoading()} fallback={
|
|
|
|
|
<>
|
|
|
|
|
<p style="font-size:15px;font-weight:600;color:#111827">No internal roles found</p>
|
|
|
|
|
<p style="margin-top:6px;font-size:13px;color:#6B7280">Create your first role to get started.</p>
|
|
|
|
|
<button type="button" onClick={openCreate} style="margin-top:16px;display:inline-flex;align-items:center;gap:6px;border-radius:10px;background:#0D0D2A;padding:8px 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">Create Role</button>
|
|
|
|
|
</>
|
|
|
|
|
}>
|
|
|
|
|
<p style="font-size:13px;color:#6B7280">Loading...</p>
|
2026-03-27 05:35:18 +01:00
|
|
|
</Show>
|
feat(admin): pixel-perfect UI overhaul for department, designation, roles, employees pages
- Rewrote all layout/spacing with inline styles (Tailwind v4 doesn't generate most utility classes)
- AdminSidebar: all 37 modules in 9 groups, scrollable, 220px/64px collapse, no bottom user section
- AdminShell: header height 64px, user avatar top-right (Gmail-style), removed search bar
- Department: orange-only status badges, dark navy table header (white text), edge-to-edge table, View/Create/All tabs, View action in row menu, form with inline styles
- Designation: full rewrite matching department pattern — same tabs, filter bar, table, form
- Roles/Employees: compact filter bar and table cell sizing fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:56:55 +01:00
|
|
|
</td>
|
feat(admin): redesign sidebar, dashboard, dept, designation & roles UI
- Sidebar: white bg, rounded pill nav items, orange left indicator for active
- Dashboard: remove Export/View All buttons, add Customise Dashboard + drag handles on widgets
- Department/Designation/Roles: new design system with orange label header, stat cards, light table header, 3-dot action menus, status badges
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 08:01:23 +01:00
|
|
|
</tr>
|
2026-03-27 19:27:35 +01:00
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<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>
|
|
|
|
|
<Show when={row.description}>
|
|
|
|
|
<p style="font-size:12px;color:#9CA3AF;margin-top:1px">{row.description}</p>
|
|
|
|
|
</Show>
|
|
|
|
|
</td>
|
|
|
|
|
<td style="padding:12px 20px;font-size:12px;font-family:monospace;color:#6B7280">{row.key || '—'}</td>
|
|
|
|
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{row.department || '—'}</td>
|
|
|
|
|
<td style="padding:12px 20px;font-size:13px;color:#374151">{Number(row.usersAssigned || 0)} users</td>
|
2026-03-30 04:48:09 +02:00
|
|
|
<td style="padding:12px 20px;font-size:13px;color:#374151">
|
|
|
|
|
{(row.key || '').toUpperCase() === 'SUPER_ADMIN' ? 'All' : Number(row.permissionsCount || 0)}
|
|
|
|
|
</td>
|
2026-03-27 19:27:35 +01:00
|
|
|
<td style="padding:12px 20px"><StatusBadge status={row.status} /></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:200px;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={() => void openDetail(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
|
|
|
|
<svg style="width:15px;height:15px;color:#FF5E13;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
|
|
|
View Role
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" onClick={() => openEdit(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
|
|
|
|
<svg style="width:15px;height:15px;color:#FF5E13;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="m16.5 3.5 4 4L7 21H3v-4L16.5 3.5Z"/></svg>
|
|
|
|
|
Edit Role
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" onClick={() => void toggleStatus(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
|
|
|
|
<svg style="width:15px;height:15px;color:#FF5E13;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg>
|
|
|
|
|
{row.status === 'ACTIVE' ? 'Deactivate' : 'Activate'}
|
|
|
|
|
</button>
|
|
|
|
|
<div style="height:1px;background:#F3F4F6;margin:4px 0" />
|
|
|
|
|
<button type="button" onClick={() => void deleteRole(row.id, row.name)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left">
|
|
|
|
|
<svg style="width:15px;height:15px;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6"/></svg>
|
|
|
|
|
Delete Role
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</Show>
|
2026-03-27 02:28:34 +01:00
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
feat(admin): redesign sidebar, dashboard, dept, designation & roles UI
- Sidebar: white bg, rounded pill nav items, orange left indicator for active
- Dashboard: remove Export/View All buttons, add Customise Dashboard + drag handles on widgets
- Department/Designation/Roles: new design system with orange label header, stat cards, light table header, 3-dot action menus, status badges
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 08:01:23 +01:00
|
|
|
</div>
|
2026-03-27 02:28:34 +01:00
|
|
|
</Show>
|
2026-03-25 22:15:06 +01:00
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
{/* ── FORM VIEW ── */}
|
|
|
|
|
<Show when={view() === 'form'}>
|
|
|
|
|
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
2026-03-27 19:27:35 +01:00
|
|
|
<button type="button" onClick={() => { setView('list'); resetForm(); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">All Roles</button>
|
2026-03-27 05:35:18 +01:00
|
|
|
<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 Role' : 'Create 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', 'permissions', 'settings'] as const).map((tab, i) => {
|
|
|
|
|
const labels = ['General Information', 'Module Access', 'Role Settings'];
|
|
|
|
|
const active = () => formTab() === tab;
|
|
|
|
|
return (
|
|
|
|
|
<button type="button" onClick={() => setFormTab(tab)} style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;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>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-03-27 02:28:34 +01:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
<div style="padding:24px">
|
2026-03-27 19:27:35 +01:00
|
|
|
<Show when={formError()}>
|
|
|
|
|
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">{formError()}</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
{/* General */}
|
2026-03-27 05:35:18 +01:00
|
|
|
<Show when={formTab() === 'general'}>
|
2026-03-27 19:27:35 +01:00
|
|
|
<div style="display:flex;flex-direction:column;gap:20px">
|
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
|
|
|
|
<FormInput label="Role Name" required value={name()} onInput={setName} placeholder="e.g. HR Manager" />
|
2026-03-30 04:48:09 +02:00
|
|
|
<label style="display:block">
|
|
|
|
|
<span style="font-size:13px;font-weight:600;color:#374151">
|
|
|
|
|
Role Key
|
|
|
|
|
<span style="margin-left:2px;color:#FF5E13">*</span>
|
|
|
|
|
</span>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Auto-generated from role name"
|
|
|
|
|
value={roleKey()}
|
|
|
|
|
readOnly
|
|
|
|
|
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
|
|
|
|
|
/>
|
|
|
|
|
<p style="margin-top:6px;font-size:11px;color:#6B7280">
|
|
|
|
|
Generated automatically from Role Name.
|
|
|
|
|
</p>
|
|
|
|
|
</label>
|
2026-03-27 19:27:35 +01:00
|
|
|
</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">Department</span>
|
|
|
|
|
<select
|
|
|
|
|
value={departmentId()}
|
|
|
|
|
onChange={(e) => setDepartmentId(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;color:#111827;outline:none;box-sizing:border-box;appearance:none"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Select department</option>
|
|
|
|
|
<For each={departments()}>{(d) => <option value={d.id}>{d.name}</option>}</For>
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
<label style="display:block">
|
|
|
|
|
<span style="font-size:13px;font-weight:600;color:#374151">Status</span>
|
|
|
|
|
<div style="margin-top:6px;display:flex;gap:8px">
|
|
|
|
|
{(['ACTIVE', 'INACTIVE'] as const).map((s) => (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setStatus(s)}
|
|
|
|
|
style={`flex:1;height:40px;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${status() === s ? '#FF5E13' : '#E5E7EB'};background:${status() === s ? '#FFF3EE' : 'white'};color:${status() === s ? '#FF5E13' : '#6B7280'}`}
|
|
|
|
|
>
|
|
|
|
|
{s === 'ACTIVE' ? 'Active' : 'Inactive'}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<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="Brief description of this role's responsibilities..."
|
|
|
|
|
rows="3"
|
|
|
|
|
style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;resize:none;box-sizing:border-box;font-family:inherit"
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
2026-03-27 02:28:34 +01:00
|
|
|
</div>
|
2026-03-27 05:35:18 +01:00
|
|
|
</Show>
|
2026-03-27 02:28:34 +01:00
|
|
|
|
2026-03-27 19:27:35 +01:00
|
|
|
{/* Permissions */}
|
2026-03-27 05:35:18 +01:00
|
|
|
<Show when={formTab() === 'permissions'}>
|
2026-03-27 19:27:35 +01:00
|
|
|
<div>
|
|
|
|
|
<p style="margin-bottom:16px;font-size:13px;color:#6B7280">Select the module access permissions for this role.</p>
|
|
|
|
|
<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>
|
|
|
|
|
<For each={ACTIONS}>
|
|
|
|
|
{(action) => (
|
2026-03-30 04:48:09 +02:00
|
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase;text-align:center">{action}</th>
|
2026-03-27 19:27:35 +01:00
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
2026-03-30 04:48:09 +02:00
|
|
|
<For each={orderedModules()}>
|
|
|
|
|
{(module) => (
|
2026-03-27 19:27:35 +01:00
|
|
|
<tr style="border-top:1px solid #E5E7EB">
|
2026-03-30 04:48:09 +02:00
|
|
|
<td style="padding:12px 16px;font-size:13px;font-weight:600;color:#111827">{module}</td>
|
2026-03-27 19:27:35 +01:00
|
|
|
<For each={ACTIONS}>
|
|
|
|
|
{(action) => {
|
2026-03-30 04:48:09 +02:00
|
|
|
const pk = permissionKeyFor(module, action);
|
|
|
|
|
const disabled = !pk;
|
2026-03-27 19:27:35 +01:00
|
|
|
return (
|
|
|
|
|
<td style="padding:12px 16px;text-align:center">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
2026-03-30 04:48:09 +02:00
|
|
|
checked={pk ? selectedPermissions().has(pk) : false}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
onChange={() => pk && togglePermission(pk)}
|
2026-03-27 19:27:35 +01:00
|
|
|
style="width:16px;height:16px;accent-color:#FF5E13;cursor:pointer"
|
|
|
|
|
/>
|
|
|
|
|
</td>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
</For>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
2026-03-27 02:28:34 +01:00
|
|
|
</div>
|
2026-03-27 05:35:18 +01:00
|
|
|
</Show>
|
2026-03-27 02:28:34 +01:00
|
|
|
|
2026-03-27 19:27:35 +01:00
|
|
|
{/* Settings */}
|
2026-03-27 05:35:18 +01:00
|
|
|
<Show when={formTab() === 'settings'}>
|
|
|
|
|
<div style="display:flex;flex-direction:column;gap:20px">
|
2026-03-27 19:27:35 +01:00
|
|
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-radius:12px;background:#F9FAFB;border:1px solid #E5E7EB">
|
2026-03-27 05:35:18 +01:00
|
|
|
<div>
|
2026-03-27 19:27:35 +01:00
|
|
|
<p style="font-size:13px;font-weight:600;color:#111827">Can Approve Requests</p>
|
|
|
|
|
<p style="font-size:12px;color:#6B7280;margin-top:2px">Users with this role can make final decisions in Approval Management.</p>
|
2026-03-27 05:35:18 +01:00
|
|
|
</div>
|
2026-03-27 19:27:35 +01:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setCanApproveRequests(v => !v)}
|
|
|
|
|
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canApproveRequests() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`}
|
|
|
|
|
>
|
|
|
|
|
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canApproveRequests() ? '22px' : '2px'}`} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-radius:12px;background:#F9FAFB;border:1px solid #E5E7EB">
|
|
|
|
|
<div>
|
|
|
|
|
<p style="font-size:13px;font-weight:600;color:#111827">Can Manage System Settings</p>
|
|
|
|
|
<p style="font-size:12px;color:#6B7280;margin-top:2px">Users with this role can access and modify system-level configurations.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setCanManageSystemSettings(v => !v)}
|
|
|
|
|
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canManageSystemSettings() ? '#FF5E13' : '#E5E7EB'};transition:background 0.2s;flex-shrink:0`}
|
|
|
|
|
>
|
|
|
|
|
<span style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canManageSystemSettings() ? '22px' : '2px'}`} />
|
|
|
|
|
</button>
|
2026-03-27 02:28:34 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-27 05:35:18 +01:00
|
|
|
</Show>
|
|
|
|
|
</div>
|
feat(admin): pixel-perfect UI overhaul for department, designation, roles, employees pages
- Rewrote all layout/spacing with inline styles (Tailwind v4 doesn't generate most utility classes)
- AdminSidebar: all 37 modules in 9 groups, scrollable, 220px/64px collapse, no bottom user section
- AdminShell: header height 64px, user avatar top-right (Gmail-style), removed search bar
- Department: orange-only status badges, dark navy table header (white text), edge-to-edge table, View/Create/All tabs, View action in row menu, form with inline styles
- Designation: full rewrite matching department pattern — same tabs, filter bar, table, form
- Roles/Employees: compact filter bar and table cell sizing fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:56:55 +01:00
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;border-top:1px solid #E5E7EB;padding:14px 24px">
|
2026-03-27 19:27:35 +01:00
|
|
|
<button type="button" onClick={() => { setView('list'); 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">
|
|
|
|
|
{isSaving() ? 'Saving...' : editingId() ? 'Update Role' : 'Create Role'}
|
2026-03-27 05:35:18 +01:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
feat(admin): redesign sidebar, dashboard, dept, designation & roles UI
- Sidebar: white bg, rounded pill nav items, orange left indicator for active
- Dashboard: remove Export/View All buttons, add Customise Dashboard + drag handles on widgets
- Department/Designation/Roles: new design system with orange label header, stat cards, light table header, 3-dot action menus, status badges
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 08:01:23 +01:00
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
{/* ── DETAIL VIEW ── */}
|
|
|
|
|
<Show when={view() === 'detail' && viewingRole()}>
|
2026-03-27 19:27:35 +01:00
|
|
|
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
|
|
|
|
<button type="button" onClick={() => { setView('list'); setListTab('all'); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">All 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">View Role</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
<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>
|
|
|
|
|
<div style="display:flex;align-items:center;gap:12px">
|
|
|
|
|
<h2 style="font-size:20px;font-weight:700;color:#111827">{viewingRole()!.name}</h2>
|
|
|
|
|
<StatusBadge status={viewingRole()!.status} />
|
2026-03-27 02:28:34 +01:00
|
|
|
</div>
|
2026-03-27 19:27:35 +01:00
|
|
|
<p style="font-size:13px;color:#6B7280;margin-top:4px">
|
|
|
|
|
Key: <code style="font-family:monospace;background:#F3F4F6;padding:1px 6px;border-radius:4px">{viewingRole()!.key || '—'}</code>
|
|
|
|
|
{viewingRole()!.department ? ` • ${viewingRole()!.department}` : ''}
|
|
|
|
|
{` • ${Number(viewingRole()!.usersAssigned || 0)} users assigned`}
|
|
|
|
|
</p>
|
|
|
|
|
<Show when={viewingRole()!.description}>
|
|
|
|
|
<p style="font-size:13px;color:#6B7280;margin-top:2px">{viewingRole()!.description}</p>
|
|
|
|
|
</Show>
|
2026-03-27 02:28:34 +01:00
|
|
|
</div>
|
2026-03-27 05:35:18 +01:00
|
|
|
<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>
|
|
|
|
|
</div>
|
feat(admin): pixel-perfect UI overhaul for department, designation, roles, employees pages
- Rewrote all layout/spacing with inline styles (Tailwind v4 doesn't generate most utility classes)
- AdminSidebar: all 37 modules in 9 groups, scrollable, 220px/64px collapse, no bottom user section
- AdminShell: header height 64px, user avatar top-right (Gmail-style), removed search bar
- Department: orange-only status badges, dark navy table header (white text), edge-to-edge table, View/Create/All tabs, View action in row menu, form with inline styles
- Designation: full rewrite matching department pattern — same tabs, filter bar, table, form
- Roles/Employees: compact filter bar and table cell sizing fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:56:55 +01:00
|
|
|
|
2026-03-27 05:35:18 +01:00
|
|
|
<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 = ['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'}>
|
2026-03-27 19:27:35 +01:00
|
|
|
<div style="border:1px solid #E5E7EB;border-radius:12px;overflow:hidden">
|
2026-03-27 05:35:18 +01:00
|
|
|
<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>
|
2026-03-27 19:27:35 +01:00
|
|
|
<For each={ACTIONS}>
|
|
|
|
|
{(action) => (
|
2026-03-30 04:48:09 +02:00
|
|
|
<th style="padding:12px 16px;font-size:11px;font-weight:600;color:#6B7280;text-transform:uppercase;text-align:center">{action}</th>
|
2026-03-27 19:27:35 +01:00
|
|
|
)}
|
|
|
|
|
</For>
|
2026-03-27 05:35:18 +01:00
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
2026-03-30 04:48:09 +02:00
|
|
|
<For each={orderedModules()}>
|
|
|
|
|
{(module) => (
|
2026-03-27 05:35:18 +01:00
|
|
|
<tr style="border-top:1px solid #E5E7EB">
|
2026-03-30 04:48:09 +02:00
|
|
|
<td style="padding:12px 16px;font-size:13px;font-weight:600;color:#111827">{module}</td>
|
2026-03-27 19:27:35 +01:00
|
|
|
<For each={ACTIONS}>
|
|
|
|
|
{(action) => {
|
2026-03-30 04:48:09 +02:00
|
|
|
const pk = permissionKeyFor(module, action);
|
|
|
|
|
const has = () => isViewingSuperAdmin() || viewingPermissions().includes(pk);
|
2026-03-27 19:27:35 +01:00
|
|
|
return (
|
|
|
|
|
<td style="padding:12px 16px;text-align:center">
|
|
|
|
|
<input type="checkbox" checked={has()} disabled style={`width:16px;height:16px;accent-color:#FF5E13;cursor:default;opacity:${has() ? '1' : '0.4'}`} />
|
|
|
|
|
</td>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
</For>
|
2026-03-27 05:35:18 +01:00
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
2026-03-27 19:27:35 +01:00
|
|
|
<div style="margin-top:16px;display:flex;gap:16px">
|
|
|
|
|
<div style="display:flex;align-items:center;gap:6px;padding:10px 14px;border-radius:10px;background:#F9FAFB;border:1px solid #E5E7EB">
|
|
|
|
|
<p style="font-size:13px;font-weight:600;color:#111827">Can Approve Requests:</p>
|
|
|
|
|
<span style={`font-size:13px;font-weight:600;color:${viewingRole()!.canApproveRequests ? '#FF5E13' : '#9CA3AF'}`}>{viewingRole()!.canApproveRequests ? 'Yes' : 'No'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;align-items:center;gap:6px;padding:10px 14px;border-radius:10px;background:#F9FAFB;border:1px solid #E5E7EB">
|
|
|
|
|
<p style="font-size:13px;font-weight:600;color:#111827">Can Manage Settings:</p>
|
|
|
|
|
<span style={`font-size:13px;font-weight:600;color:${viewingRole()!.canManageSystemSettings ? '#FF5E13' : '#9CA3AF'}`}>{viewingRole()!.canManageSystemSettings ? 'Yes' : 'No'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-27 05:35:18 +01:00
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={detailTab() === 'users'}>
|
2026-03-27 19:27:35 +01:00
|
|
|
<div style="padding:32px;text-align:center">
|
|
|
|
|
<p style="font-size:14px;font-weight:600;color:#111827">{Number(viewingRole()!.usersAssigned || 0)} users assigned</p>
|
|
|
|
|
<p style="margin-top:6px;font-size:13px;color:#6B7280">Detailed user list will be available when employee management is connected.</p>
|
|
|
|
|
</div>
|
2026-03-27 05:35:18 +01:00
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={detailTab() === 'logs'}>
|
2026-03-27 19:27:35 +01:00
|
|
|
<div style="padding:32px;text-align:center">
|
|
|
|
|
<p style="font-size:13px;color:#6B7280">Activity logs will appear here.</p>
|
2026-03-27 05:35:18 +01:00
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="padding:16px 24px;border-top:1px solid #E5E7EB;display:flex;justify-content:flex-end">
|
|
|
|
|
<button type="button" onClick={() => { setView('list'); 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>
|
feat(admin): pixel-perfect UI overhaul for department, designation, roles, employees pages
- Rewrote all layout/spacing with inline styles (Tailwind v4 doesn't generate most utility classes)
- AdminSidebar: all 37 modules in 9 groups, scrollable, 220px/64px collapse, no bottom user section
- AdminShell: header height 64px, user avatar top-right (Gmail-style), removed search bar
- Department: orange-only status badges, dark navy table header (white text), edge-to-edge table, View/Create/All tabs, View action in row menu, form with inline styles
- Designation: full rewrite matching department pattern — same tabs, filter bar, table, form
- Roles/Employees: compact filter bar and table cell sizing fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 01:56:55 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-27 02:28:34 +01:00
|
|
|
</Show>
|
2026-03-27 05:35:18 +01:00
|
|
|
|
ui(step-1): match reference layout for dept/designation/employees/roles pages
- All pages: white sticky page header + tab bar with orange underline,
-mx-6 -mt-6 negative margin to stretch headers edge-to-edge
- department: full columns (ID, Name, Description, Created By, etc.),
icon-only action buttons, navy Add Department button
- designation: Designations List / Add Designation tabs, status filter
dropdown, inline create/edit form, full columns with status badge
- employees: View/Add tabs, icon-only action buttons, status badges
- roles/index: clean table with Name+code subtext, Description, actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 04:47:05 +01:00
|
|
|
</div>
|
feat(admin): build complete admin panel with UI parity and search/filter
- Implement all admin management pages (employees, users, jobs, leads, orders, companies, customers, candidates, approval, invoices, reviews, support, KB, pricing, coupons, credits, discounts, tax, reports, ledger)
- Implement 9 professional vertical pages (developers, designers, tutors, video editors, photographers, makeup artists, graphic designers, social media managers, fitness trainers)
- Implement internal/external dashboard and role management with builder UI
- Fix tab styling: replace inline border-bottom styles with admin-tab CSS class across 8+ pages
- Add search/filter functionality to invoice and review pages
- Add toggle status (activate/deactivate) to employees page with PATCH /api/admin/employees/{id}
- Align UI styling with NextJS admin panel for visual parity
- Add stat cards to approval page showing counts by status
- Implement graceful empty states for all list views
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-19 13:04:10 +01:00
|
|
|
);
|
|
|
|
|
}
|