Secure management suite

Sign in

Access is restricted to authorised Repair & Assure users.

Prototype access control: suitable for developer handoff and testing. Live security still requires server-side authentication and protected APIs.

Management Reporting

Repair & Assure Management Dashboard

Central access point for operational and commercial reporting.

Management Hub Active

Available management reports

Select a dashboard below to open its detailed reporting view.

↻

Repair & Assure SmartDebit Tracker

Monitor recurring service-plan collections, collection performance, run-off trends and the 12x12 / 83x3 reporting views.

Service plan performance Open dashboard →
⚙

Manufacturer Repair Partnerships

Review repair volumes, revenue, contribution, SLA dependency, engineer usage and manufacturer-level operational performance.

Partnership performance Open dashboard →
⌂

Estate Agent Partnerships

Track service requests, partner usage, revenue, direct contribution, fully loaded profitability and agency-level commercial performance.

Estate agency performance Open dashboard →
Designed as the main reporting hub. New reports can be added as additional tiles without changing the overall layout.

Add user

Report allowances

',policyCss+policyScript+''); } function openAdmin(){ if(!canAdmin(currentUser)) return; document.getElementById('adminOverlay').classList.add('open'); document.getElementById('adminOverlay').setAttribute('aria-hidden','false'); renderAdminUsers(); renderAdminRoles(); renderAudit(); renderMigration(); renderSettings(); } function closeAdmin(){ document.getElementById('adminOverlay')?.classList.remove('open'); document.getElementById('adminOverlay')?.setAttribute('aria-hidden','true'); } function showAdminTab(tab){ ['users','roles','audit','migration','settings'].forEach(t=>{ document.getElementById('admin'+t[0].toUpperCase()+t.slice(1)+'Tab').style.display=t===tab?'block':'none'; document.getElementById('nav'+t[0].toUpperCase()+t.slice(1)).classList.toggle('active',t===tab); }); if(tab==='users') renderAdminUsers(); if(tab==='roles') renderAdminRoles(); if(tab==='audit') renderAudit(); if(tab==='migration') renderMigration(); if(tab==='settings') renderSettings(); } function renderAdminUsers(){ const state=getAuthState(); const active=state.users.filter(u=>u.active).length; const admins=state.users.filter(u=>u.role==='Super Admin'&&u.active).length; const reportUsers=state.users.filter(u=>u.active&&Object.values(userPermissions(u)).some(p=>p.view)).length; const rows=state.users.map(u=>{ const reportCount=Object.values(userPermissions(u)).filter(p=>p.view).length; return ` ${escHtml(u.name)}
${escHtml(u.email)} ${escHtml(u.role)} ${u.active?'Active':'Inactive'} ${reportCount} / ${Object.keys(REPORT_META).length} ${u.lastLogin?new Date(u.lastLogin).toLocaleString('en-GB'):'Never'} `; }).join(''); document.getElementById('adminUsersTab').innerHTML=`
Developer handoff mode: this interface is complete, but users/passwords are currently stored in this browser. Production should move authentication, sessions and permission checks to the server.

Users

${state.users.length}
Total user accounts
${active}
Active users
${admins}
Active Super Admins
${rows||''}
UserRoleStatusReportsLast login
No users.
`; } function buildPermissionEditor(perms){ return `
Report
View
Manage
Data
Export
`+ Object.entries(REPORT_META).map(([key,meta])=>{ const p=perms[key]||{view:false,manage:false,data:false,export:false}; return `
${meta.name}
`; }).join(''); } function openUserEditor(id=null){ if(!canAdmin(currentUser)) return; const state=getAuthState(); const user=id?state.users.find(u=>u.id===id):null; editingUserId=user?.id||null; document.getElementById('userEditorTitle').textContent=user?'Edit user':'Add user'; document.getElementById('editUserName').value=user?.name||''; document.getElementById('editUserEmail').value=user?.email||''; const roleSel=document.getElementById('editUserRole'); roleSel.innerHTML=Object.keys(ROLE_DEFAULTS).map(r=>``).join(''); if(!user) roleSel.value='Read Only'; document.getElementById('editUserActive').value=String(user?.active??true); document.getElementById('editUserPassword').value=''; document.getElementById('editPasswordLabel').textContent=user?'New password (optional)':'Temporary password'; document.getElementById('permissionEditor').innerHTML=buildPermissionEditor(user?userPermissions(user):clone(ROLE_DEFAULTS[roleSel.value])); setAuthError('userEditError',''); document.getElementById('userEditorOverlay').classList.add('open'); } function closeUserEditor(){document.getElementById('userEditorOverlay')?.classList.remove('open');editingUserId=null} function applyRoleDefaultsToEditor(){ const role=document.getElementById('editUserRole').value; document.getElementById('permissionEditor').innerHTML=buildPermissionEditor(clone(ROLE_DEFAULTS[role]||ROLE_DEFAULTS['Read Only'])); } function readEditorPermissions(){ const perms={}; Object.keys(REPORT_META).forEach(k=>perms[k]={view:false,manage:false,data:false,export:false}); document.querySelectorAll('#permissionEditor input[data-perm]').forEach(cb=>{ const [key,field]=cb.dataset.perm.split(':'); perms[key][field]=cb.checked; }); return perms; } async function saveUserEditor(){ if(!canAdmin(currentUser)) return; const state=getAuthState(); const name=document.getElementById('editUserName').value.trim(); const email=document.getElementById('editUserEmail').value.trim().toLowerCase(); const role=document.getElementById('editUserRole').value; const active=document.getElementById('editUserActive').value==='true'; const pw=document.getElementById('editUserPassword').value; if(!name||!email){setAuthError('userEditError','Name and email are required.');return} if(state.users.some(u=>u.email===email&&u.id!==editingUserId)){setAuthError('userEditError','That email address is already in use.');return} if(!editingUserId && pw.length<10){setAuthError('userEditError','New users require a temporary password of at least 10 characters.');return} if(pw && pw.length<10){setAuthError('userEditError','Passwords must be at least 10 characters.');return} let user=editingUserId?state.users.find(u=>u.id===editingUserId):null; if(!user){ user={id:uid(),createdAt:nowIso(),lastLogin:null}; state.users.push(user); } const wasCurrent=user.id===currentUser.id; user.name=name;user.email=email;user.role=role;user.active=active;user.permissions=readEditorPermissions(); if(role==='Super Admin') user.permissions=clone(ROLE_DEFAULTS['Super Admin']); if(pw) user.passwordHash=await sha256(pw); if(wasCurrent && (!active||role!=='Super Admin')){ setAuthError('userEditError','You cannot remove your own active Super Admin access while signed in.');return; } saveAuthState(state); audit(editingUserId?'User updated':'User created',`${email} · ${role}`); closeUserEditor();renderAdminUsers();applyAccessUI(); } function renderAdminRoles(){ document.getElementById('adminRolesTab').innerHTML=`

Role defaults

Roles provide starting permissions. Individual user allowances can then be adjusted in Users.

${Object.keys(ROLE_DEFAULTS).map(role=>{ const names=Object.entries(ROLE_DEFAULTS[role]).filter(([,p])=>p.view).map(([k])=>REPORT_META[k].name); return ``; }).join('')}
RoleDefault access
${role}${names.length?names.join(' · '):'No reports by default'}
`; } function renderAudit(){ const state=getAuthState(); document.getElementById('adminAuditTab').innerHTML=`

Audit Log

${state.audit.length?state.audit.map(a=>`
${new Date(a.ts).toLocaleString('en-GB')}
${escHtml(a.user)}
${escHtml(a.action)}${a.detail?' · '+escHtml(a.detail):''}
`).join(''):'

No activity yet.

'}
`; } function buildMigrationPayload(includeAudit=false){ const state=getAuthState(); return { schema:'repair-assure-management-suite-user-migration', schemaVersion:1, exportedAt:nowIso(), source:'Repair & Assure Management Suite prototype', passwordMigration:false, passwordInstruction:'Passwords are intentionally excluded. Create/reset passwords securely on the production system.', reports:REPORT_META, roles:Object.keys(ROLE_DEFAULTS), settings:{ sessionMinutes:state.settings?.sessionMinutes||SESSION_TIMEOUT_MINUTES }, users:state.users.map(u=>({ legacyId:u.id, name:u.name, email:u.email, role:u.role, active:!!u.active, permissions:userPermissions(u), createdAt:u.createdAt||null, lastLogin:u.lastLogin||null, requiresPasswordSetup:true })), ...(includeAudit?{audit:state.audit||[]}:{}) }; } function downloadJsonFile(filename,obj){ const blob=new Blob([JSON.stringify(obj,null,2)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url;a.download=filename; document.body.appendChild(a);a.click();a.remove(); setTimeout(()=>URL.revokeObjectURL(url),1000); } function exportUsersPermissions(){ const payload=buildMigrationPayload(false); const stamp=new Date().toISOString().slice(0,10); downloadJsonFile(`repair_assure_users_permissions_${stamp}.json`,payload); audit('User migration export',`${payload.users.length} users exported without passwords`); renderMigration(); } function exportDeveloperMigration(){ const payload=buildMigrationPayload(true); payload.developerNotes={ recommendedImportOrder:[ 'Create roles/report registry', 'Create users by email', 'Apply active/inactive status', 'Apply per-report permissions', 'Require each imported user to set a new password', 'Enable secure server-side sessions and MFA for administrators' ], requiredServerControls:[ 'Store passwords with a production password-hashing algorithm such as Argon2id or bcrypt', 'Use HTTPS', 'Use secure HTTP-only session cookies', 'Enforce permissions on server/API routes, not only in the browser', 'Do not expose report data in unauthorised HTML/JavaScript', 'Log authentication and permission changes server-side' ] }; const stamp=new Date().toISOString().slice(0,10); downloadJsonFile(`repair_assure_developer_migration_${stamp}.json`,payload); audit('Developer migration export',`${payload.users.length} users + permissions + audit exported`); renderMigration(); } function triggerUserImport(){ document.getElementById('migrationImportFile').click(); } async function importUsersPermissions(input){ if(!canAdmin(currentUser)) return; const file=input.files?.[0]; if(!file) return; try{ const text=await file.text(); const payload=JSON.parse(text); if(payload.schema!=='repair-assure-management-suite-user-migration'||!Array.isArray(payload.users)){ throw new Error('This is not a recognised Repair & Assure migration file.'); } const state=getAuthState(); let added=0,updated=0; for(const incoming of payload.users){ if(!incoming.email) continue; const email=String(incoming.email).trim().toLowerCase(); let u=state.users.find(x=>x.email===email); if(u){ u.name=incoming.name||u.name; u.role=incoming.role||u.role; u.active=incoming.active!==false; u.permissions=incoming.permissions||u.permissions; updated++; }else{ u={ id:uid(), name:incoming.name||email, email, role:incoming.role||'Read Only', active:incoming.active!==false, permissions:incoming.permissions||clone(ROLE_DEFAULTS[incoming.role]||ROLE_DEFAULTS['Read Only']), passwordHash:'', createdAt:nowIso(), lastLogin:null, importedRequiresPasswordSetup:true }; state.users.push(u); added++; } } if(payload.settings?.sessionMinutes){ state.settings=state.settings||{}; state.settings.sessionMinutes=payload.settings.sessionMinutes; } saveAuthState(state); audit('User migration import',`${added} added · ${updated} updated · passwords not imported`); alert(`Import complete.\n\n${added} users added\n${updated} users updated\n\nPasswords were not imported. New/imported users require secure password setup.`); renderMigration();renderAdminUsers();applyAccessUI(); }catch(err){ alert('Import failed: '+err.message); }finally{ input.value=''; } } function renderMigration(){ const state=getAuthState(); const payload=buildMigrationPayload(false); const preview={ schema:payload.schema, schemaVersion:payload.schemaVersion, userCount:payload.users.length, users:payload.users.map(u=>({ name:u.name,email:u.email,role:u.role,active:u.active,permissions:u.permissions,requiresPasswordSetup:true })) }; document.getElementById('adminMigrationTab').innerHTML=`

Migration & Backup

Designed for handoff: create your users and permissions here now, then export them for your developers to import into the production server. Passwords are deliberately excluded from migration.

Export Users & Permissions

Exports names, emails, roles, account status and individual report allowances. Best for transferring the account setup to your developers.

Developer Migration Package

Includes the user/permission structure, report registry, session settings, audit history and implementation notes for the production backend.

Import / Restore User Setup

Restore a previous export or transfer a configured user set into another copy of this management suite. Existing users are matched by email.

Production Password Setup

Passwords are not exported. Imported users are flagged to require password setup. Your live system should send each user a secure setup/reset link.

Migration preview

${state.users.length} user account${state.users.length===1?'':'s'} currently configured. This preview excludes password hashes and audit details.

${escHtml(JSON.stringify(preview,null,2))}
`; } function renderSettings(){ const state=getAuthState(); document.getElementById('adminSettingsTab').innerHTML=`

Settings

Session security

Production handoff requirements

Use the Migration & Backup export to seed the production users/permissions. Replace localStorage user records with a server-side users table or identity provider; replace sessionStorage with secure HTTP-only sessions; enforce report permissions on every API route; serve dashboards/data only after authentication; use HTTPS; add password-setup/reset email and MFA for administrators.

`; } function saveSecuritySettings(){ const state=getAuthState(); state.settings=state.settings||{}; state.settings.sessionMinutes=+document.getElementById('sessionMinutesSetting').value||45; saveAuthState(state);audit('Security settings updated',`Session timeout ${state.settings.sessionMinutes} minutes`); renderSettings(); } function showHome(){ document.body.classList.remove("report-open"); document.getElementById("reportFrame").srcdoc=""; window.scrollTo(0,0); } function showReport(key){ if(!REPORTS[key]||!currentUser) return; const perm=userPermissions(currentUser)[key]; if(!perm?.view){ audit('Access denied',REPORT_META[key]?.name||key); alert('You do not have permission to view this report.'); return; } document.body.classList.add("report-open"); const reportDoc=injectReportPolicy(decodeB64Utf8(REPORTS[key]),key,perm); document.getElementById("reportFrame").srcdoc=reportDoc; audit('Report opened',REPORT_META[key]?.name||key); window.scrollTo(0,0); } function openSelectedReport(){ showReport(document.getElementById("reportSelect").value); } window.addEventListener("message",e=>{ if(e.data==="ra-main-home"){ showHome(); return; } },false); document.querySelectorAll(".card").forEach(card=>{ card.setAttribute("tabindex","0"); card.setAttribute("role","button"); card.addEventListener("keydown",e=>{ if(e.key==="Enter"||e.key===" "){e.preventDefault();card.click();} }); }); const updated=document.getElementById("updated"); if(updated){ const d=new Date(); updated.textContent="Hub updated: "+d.toLocaleDateString("en-GB",{day:"2-digit",month:"short",year:"numeric"}); } restoreSession();