Update: 2026-07-25 18:19:51

This commit is contained in:
Hamza-Ayed
2026-07-25 18:19:52 +03:00
parent 27369b8ac1
commit 5e103f60f2
+95 -95
View File
@@ -1387,13 +1387,13 @@
el.driversTableBody.innerHTML = drivers.map((d) => `
<tr class="row-clickable" data-captain="${esc(d.id)}">
<td><strong>#${esc(d.id)}</strong></td>
<td>${esc(`${d.first_name || ''} ${d.last_name || ''}`.trim() || 'Unnamed')}</td>
<td>${esc(`${d.first_name || ''} ${d.last_name || ''}`.trim() || t('Unnamed'))}</td>
<td>${esc(maskPhone(d.phone))}</td>
<td>${esc(d.email || '—')}</td>
<td>${rating(d.passengerAverageRating)}</td>
<td>${fmtInt(d.countPassengerRide)}</td>
<td>${fmtInt(d.countPassengerCancel)}</td>
<td><span class="badge ${badgeClass(d.status)}">${esc(d.status || 'unknown')}</span></td>
<td><span class="badge ${badgeClass(d.status)}">${esc(d.status || t('Unknown'))}</span></td>
</tr>
`).join('');
@@ -1409,13 +1409,13 @@
el.passengersTableBody.innerHTML = rows.map((p) => `
<tr class="row-clickable" data-passenger="${esc(p.id)}">
<td><strong>#${esc(p.id)}</strong></td>
<td>${esc(`${p.first_name || ''} ${p.last_name || ''}`.trim() || 'Unnamed')}</td>
<td>${esc(`${p.first_name || ''} ${p.last_name || ''}`.trim() || t('Unnamed'))}</td>
<td>${esc(p.email || maskPhone(p.phone))}</td>
<td>${fmtInt(p.countPassengerRide)}</td>
<td>${rating(p.passengerAverageRating)}</td>
<td>${fmtInt(p.countPassengerCancel)}</td>
<td>${esc(fmtDate(p.created_at, true))}</td>
<td><span class="badge ${badgeClass(p.status)}">${esc(p.status || 'unknown')}</span></td>
<td><span class="badge ${badgeClass(p.status)}">${esc(p.status || t('Unknown'))}</span></td>
</tr>
`).join('');
@@ -1455,13 +1455,13 @@
}
async function decideApproval(adminId, action) {
if (!confirm(`Set admin #${adminId} to "${action}"?`)) return;
if (!confirm(t('Set admin') + ` #${adminId} ` + t('to') + ` "${action}"?`)) return;
try {
const form = new FormData();
form.append('admin_id', adminId);
form.append('action', action);
await api('/Admin/auth/approve_admin.php', { method: 'POST', body: form });
toast(`Admin #${adminId} ${action}.`, 'success');
toast(`${t('Admin')} #${adminId} ${t(action)}.`, 'success');
loadApprovals();
} catch (err) {
if (!handleApiError(err, 'approve')) toast(err.message, 'danger');
@@ -2307,25 +2307,25 @@
const label = type === 'driver' ? 'captain' : 'passenger';
if (!phone) {
toast('Enter the phone number of the account to remove.', 'warning');
toast(t('Enter the phone number of the account to remove.'), 'warning');
return;
}
if (confirmPhone !== phone) {
toast('The retyped phone does not match — nothing was deleted.', 'danger');
toast(t('The retyped phone does not match — nothing was deleted.'), 'danger');
return;
}
if (type === 'driver' && !id) {
toast('Removing a captain requires their account ID.', 'warning');
toast(t('Removing a captain requires their account ID.'), 'warning');
return;
}
if (!confirm(
`Permanently delete the ${label} on ${phone} and blacklist that number?\n\n` +
`${reason ? `Reason: ${reason}\n\n` : ''}` +
t('Permanently delete') + ` ${label} ${t('on')} ${phone} ` + t('and blacklist that number?') + '\n\n' +
`${reason ? t('Reason') + `: ${reason}\n\n` : ''}` +
'The account is deleted from the database. This cannot be undone.'
)) return;
busy($('rmSubmit'), true, 'Deleting…');
busy($('rmSubmit'), true, t('Deleting…'));
status.textContent = '';
try {
if (type === 'driver') {
@@ -2335,14 +2335,14 @@
params: { id, phone, reason },
});
}
status.textContent = `Removed and blacklisted ${phone}`;
toast(`${label === 'captain' ? 'Captain' : 'Passenger'} removed and blacklisted.`, 'success');
status.textContent = t('Removed and blacklisted') + ` ${phone}`;
toast(label === 'captain' ? t('Captain removed and blacklisted.') : t('Passenger removed and blacklisted.'), 'success');
['rmPhone', 'rmId', 'rmReason', 'rmConfirmPhone'].forEach((f) => { $(f).value = ''; });
loadBlacklist();
} catch (err) {
if (!handleApiError(err, 'remove-account')) toast(err.message, 'danger');
} finally {
busy($('rmSubmit'), false, 'Delete and blacklist');
busy($('rmSubmit'), false, t('Delete and blacklist'));
}
}
@@ -2352,25 +2352,25 @@
const status = $('ubStatus');
if (!phone) {
toast('Enter the blocked phone number.', 'warning');
toast(t('Enter the blocked phone number.'), 'warning');
return;
}
if (!confirm(`Lift the block on ${phone}? They will be able to register again.`)) return;
if (!confirm(t('Lift the block on') + ` ${phone}? ` + t('They will be able to register again.'))) return;
busy($('ubSubmit'), true, 'Working…');
busy($('ubSubmit'), true, t('Working…'));
try {
const path = type === 'driver'
? '/Admin/driver/remove_from_blacklist.php'
: '/Admin/passenger/admin_unblacklist.php';
await api(path, { params: { phone } });
status.textContent = `Block lifted for ${phone}`;
toast('Block lifted.', 'success');
status.textContent = t('Block lifted for') + ` ${phone}`;
toast(t('Block lifted.'), 'success');
$('ubPhone').value = '';
loadBlacklist();
} catch (err) {
if (!handleApiError(err, 'lift-block')) toast(err.message, 'danger');
} finally {
busy($('ubSubmit'), false, 'Lift block');
busy($('ubSubmit'), false, t('Lift block'));
}
}
@@ -2443,13 +2443,13 @@
if (!isPreview) {
if (signature !== previewedCampaign) {
toast('Parameters changed since the preview — preview again before launching.', 'warning');
toast(t('Parameters changed since the preview') + ' — ' + t('preview again before launching'), 'warning');
$('cmpLaunch').disabled = true;
return;
}
if (!confirm(
`Launch this campaign in ${params.country_code}?\n\n` +
'It creates a discount code valid for 7 days and pushes a notification to every passenger there.\n\n' +
t('Launch this campaign in') + ` ${params.country_code}?\n\n` +
t('It creates a discount code valid for 7 days and pushes a notification to every passenger there.') + '\n\n' +
'This cannot be undone.'
)) return;
}
@@ -2471,13 +2471,13 @@
previewedCampaign = signature;
$('cmpLaunch').disabled = false;
$('cmpStatus').textContent = opportunity === false
? 'The AI found no opportunity — launching would still send.'
? t('The AI found no opportunity — launching would still send.')
: `Previewed ${params.country_code}. Launch is now enabled.`;
} else {
previewedCampaign = null;
$('cmpLaunch').disabled = true;
$('cmpStatus').textContent = `Launched at ${new Date().toLocaleTimeString()}`;
toast('Campaign dispatched.', 'success');
$('cmpStatus').textContent = t('Launched at') + ` ${new Date().toLocaleTimeString()}`;
toast(t('Campaign dispatched.'), 'success');
loadCampaignLog();
}
} catch (err) {
@@ -2486,7 +2486,7 @@
toast(err.message, 'danger');
}
} finally {
busy(btn, false, isPreview ? 'Preview' : 'Launch campaign');
busy(btn, false, isPreview ? t('Preview') : t('Launch campaign'));
// busy() clears `disabled`, so re-apply the arming rule afterwards:
// launching stays locked until the current parameters are previewed.
$('cmpLaunch').disabled = previewedCampaign !== JSON.stringify(campaignParams());
@@ -2554,22 +2554,22 @@
const version = input.value.trim();
if (version === String(pkg.version ?? '')) {
toast('Version unchanged.', 'info');
toast(t('Version unchanged.'), 'info');
return;
}
// Mirrors the server-side check so a typo is caught before the request.
if (!/^\d+(\.\d+){0,3}$/.test(version)) {
toast('Use digits separated by dots, e.g. 1.4.2', 'warning');
toast(t('Use digits separated by dots, e.g. 1.4.2'), 'warning');
return;
}
if (!confirm(
`Set ${pkg.name || `package #${pkg.id}`} to version ${version} (was ${pkg.version ?? '—'})?\n\n` +
t('Version set to') + ` ${pkg.name || `package #${pkg.id}`} \u2192 ${version} (${pkg.version ?? '—'})?\n\n` +
'Users on an older build may be prompted or forced to update.'
)) return;
try {
await api('/serviceapp/updatePackages.php', { params: { id: pkg.id, version } });
toast(`Version set to ${version}.`, 'success');
toast(t('Version set to') + ` ${version}.`, 'success');
renderAppVersions(host);
} catch (err) {
if (!handleApiError(err, 'package-save')) toast(err.message, 'danger');
@@ -2615,7 +2615,7 @@
</div>
<div class="table-responsive">
<table class="data-table">
<thead><tr><th>ID</th><th>Name</th><th>Phone</th><th></th></tr></thead>
<thead><tr><th>${t('ID')}</th><th>${t('Name')}</th><th>${t('Phone')}</th><th></th></tr></thead>
<tbody>
${drivers.map((d) => `
<tr>
@@ -2643,7 +2643,7 @@
async function showDriverDocs(driverId, host) {
const panel = $('docsDetail');
panel.innerHTML = '<div class="table-msg">Loading documents…</div>';
panel.innerHTML = `<div class="table-msg">${t('Loading documents…')}</div>`;
let driver = {};
let documents = [];
@@ -2666,8 +2666,8 @@
panel.innerHTML = `
<div class="card-header">
<h3 class="card-title">
${esc(`${driver.first_name || ''} ${driver.last_name || ''}`.trim() || `Captain #${driverId}`)}
<span class="card-sub">#${esc(driverId)} · ${esc(driver.status || 'unknown')}</span>
${esc(`${driver.first_name || ''} ${driver.last_name || ''}`.trim() || t('Captain #') + driverId)}
<span class="card-sub">#${esc(driverId)} · ${esc(driver.status || t('Unknown'))}</span>
</h3>
<button class="btn btn-primary btn-sm" data-approve-driver="${esc(driverId)}">
<i class="ph ph-check"></i> <span>${t('Approve & activate')}</span>
@@ -2712,16 +2712,16 @@
async function approveDriver(driverId, driver, docCount, host) {
const name = `${driver.first_name || ''} ${driver.last_name || ''}`.trim() || `#${driverId}`;
const warning = docCount === 0
? '\n\nWARNING: no documents are on file for this captain.'
: `\n\n${docCount} document(s) reviewed.`;
? '\n\n' + t('WARNING: no documents are on file for this captain.')
: `\n\n${docCount} ` + t('document(s) reviewed') + '.';
if (!confirm(`Activate captain ${name}?${warning}\n\nThey will be able to accept rides immediately.`)) return;
if (!confirm(t('Activate captain') + ` ${name}?${warning}\n\n` + t('They will be able to accept rides immediately.'))) return;
try {
await api('/Admin/driver/updateDriverFromAdmin.php', {
params: { id: driverId, status: 'active' },
});
toast(`Captain ${name} activated.`, 'success');
toast(`${t('Captain')} ${name} ${t('activated.')}`, 'success');
renderDriverDocs(host, docsOffset);
} catch (err) {
if (!handleApiError(err, 'driver-approve')) toast(err.message, 'danger');
@@ -2834,10 +2834,10 @@
}
async function activateStaff(userId, type, host) {
if (!confirm(`Activate ${type} account #${userId}? They will be able to sign in immediately.`)) return;
if (!confirm(t('Activate') + ` ${type} ` + t('account') + ` #${userId}? ` + t('They will be able to sign in immediately.'))) return;
try {
await api('/Admin/Staff/activate.php', { params: { user_id: userId, type } });
toast(`Account #${userId} activated.`, 'success');
toast(t('Account') + ` #${userId} ${t('activated.')}`, 'success');
loadStaffPending(host);
} catch (err) {
if (!handleApiError(err, 'staff-activate')) toast(err.message, 'danger');
@@ -2854,41 +2854,41 @@
const status = $('staffStatus');
if (!name || !password) {
toast('Name and password are required.', 'warning');
toast(t('Name and password are required.'), 'warning');
return;
}
if (password.length < 8) {
toast('Use a password of at least 8 characters.', 'warning');
toast(t('Use a password of at least 8 characters.'), 'warning');
return;
}
if (role === 'admin' && !isSuperAdmin()) {
toast('Only a super admin can create administrator accounts.', 'warning');
toast(t('Only a super admin can create administrator accounts.'), 'warning');
return;
}
const roleLabel = role === 'admin' ? 'ADMINISTRATOR' : 'customer service';
if (!confirm(
`Create a ${roleLabel} account for "${name}"?\n\n` +
t('Create a account for') + ` ${roleLabel} \"${name}\"?\n\n` +
`Phone: ${phone || '—'}\nEmail: ${email || '—'}\n\n` +
(role === 'admin'
? 'Administrators can see and change platform data.'
: 'Customer service staff can view operational data.')
? t('Administrators can see and change platform data.')
: t('Customer service staff can view operational data.'))
)) return;
busy($('staffAdd'), true, 'Creating…');
busy($('staffAdd'), true, t('Creating…'));
status.textContent = '';
try {
await api('/Admin/Staff/add.php', {
params: { name, phone, email, password, role, country },
});
status.textContent = `Created ${roleLabel} account for ${name}`;
toast('Staff account created.', 'success');
status.textContent = t('Created') + ` ${roleLabel} ` + t('account') + ` ${t('for')} ${name}`;
toast(t('Staff account created.'), 'success');
['staffName', 'staffPhone', 'staffEmail', 'staffPassword'].forEach((id) => { $(id).value = ''; });
loadStaffPending(host);
} catch (err) {
if (!handleApiError(err, 'staff-add')) toast(`Could not create account: ${err.message}`, 'danger');
if (!handleApiError(err, 'staff-add')) toast(t('Could not create account:') + ' ' + err.message, 'danger');
} finally {
busy($('staffAdd'), false, 'Create account');
busy($('staffAdd'), false, t('Create account'));
}
}
@@ -2957,11 +2957,11 @@
async function decideRoute(route, action, host) {
const verb = action === 'approve' ? 'approve' : 'reject';
const consequence = action === 'approve'
? 'The route goes live and passengers can ride it.'
: 'The organisation will have to resubmit the route.';
? t('The route goes live and passengers can ride it.')
: t('The organisation will have to resubmit the route.');
if (!confirm(
`${verb === 'approve' ? 'Approve' : 'Reject'} route "${route.name_ar || route.id}" ` +
`${verb === 'approve' ? t('Approve') : t('Reject')} ` + t('route') + ` \"${route.name_ar || route.id}\" ` +
`from ${route.org_name || 'this organisation'}?\n\n` +
`${fmtInt(route.stops_count)} stops · ${fmtNum(route.distance_km)} km\n\n${consequence}`
)) return;
@@ -2970,7 +2970,7 @@
await api('/Admin/transit/route/approve.php', {
params: { route_id: route.id, action },
});
toast(`Route #${route.id} ${verb}ed.`, 'success');
toast(t('Route') + ` #${route.id} ${t(verb + 'ed')}.`, 'success');
renderRouteApprovals(host);
} catch (err) {
if (!handleApiError(err, 'route-decision')) toast(err.message, 'danger');
@@ -3038,7 +3038,7 @@
async function sendBroadcast(host) {
if (!isSuperAdmin()) {
toast('Broadcasting is restricted to super admins.', 'warning');
toast(t('Broadcasting is restricted to super admins.'), 'warning');
return;
}
@@ -3049,12 +3049,12 @@
const audienceLabel = audience === 'drivers' ? 'every captain' : 'every passenger';
if (!title || !body) {
toast('Enter both a title and a message.', 'warning');
toast(t('Enter both a title and a message.'), 'warning');
return;
}
if (!confirm(
`Send this notification to ${audienceLabel} on the platform?\n\n` +
t('Send this notification to') + ` ${audienceLabel} ` + t('on the platform?') + '\n\n' +
`${title}\n${body}\n\n` +
'It is delivered immediately and cannot be recalled.'
)) return;
@@ -3066,17 +3066,17 @@
const result = await api('/Admin/notifications/broadcast.php', {
params: { audience, title, body },
});
status.textContent = `Sent to ${audienceLabel} at ${new Date().toLocaleTimeString()}`;
toast(`Notification delivered to ${audienceLabel}.`, 'success');
status.textContent = t('Sent to') + ` ${audienceLabel} ` + t('at') + ` ${new Date().toLocaleTimeString()}`;
toast(t('Notification delivered to') + ` ${audienceLabel}.`, 'success');
$('bcTitle').value = '';
$('bcBody').value = '';
$('bcPreviewTitle').textContent = 'Notification title';
$('bcPreviewBody').textContent = 'Message text';
$('bcPreviewTitle').textContent = t('Notification title');
$('bcPreviewBody').textContent = t('Message text');
console.info('[broadcast]', result);
} catch (err) {
if (!handleApiError(err, 'broadcast')) toast(`Send failed: ${err.message}`, 'danger');
if (!handleApiError(err, 'broadcast')) toast(t('Send failed:') + ' ' + err.message, 'danger');
} finally {
busy($('bcSend'), false, 'Review & send');
busy($('bcSend'), false, t('Review & send'));
}
}
@@ -3169,7 +3169,7 @@
async function saveTariff(index, host) {
if (!isSuperAdmin()) {
toast('Only a super admin can change pricing.', 'warning');
toast(t('Only a super admin can change pricing.'), 'warning');
return;
}
@@ -3185,7 +3185,7 @@
});
if (!Object.keys(changes).length) {
toast('Nothing changed on this tariff row.', 'info');
toast(t('Nothing changed on this tariff row.'), 'info');
return;
}
@@ -3207,10 +3207,10 @@
await api('/ride/kazan/update.php', {
params: { id: row.id, adminId: session?.id ?? '', ...changes },
});
toast('Tariff updated and recorded in the audit log.', 'success');
toast(t('Tariff updated and recorded in the audit log.'), 'success');
renderTariffEditor(host);
} catch (err) {
if (!handleApiError(err, 'tariff-save')) toast(`Update failed: ${err.message}`, 'danger');
if (!handleApiError(err, 'tariff-save')) toast(t('Update failed:') + ' ' + err.message, 'danger');
}
}
@@ -3369,11 +3369,11 @@
async function runDiagnostics() {
const out = el.diagnosticsOutput;
if (!out) return;
busy(el.runDiagnosticsBtn, true, 'Running…');
busy(el.runDiagnosticsBtn, true, t('Running…'));
const lines = [
`Siro Admin diagnostics — ${new Date().toISOString()}`,
`Console build: ${BUILD}`,
`${t('Console build:')} ${BUILD}`,
`Page origin : ${location.origin}`,
`API base : ${API_BASE}`,
`Fingerprint : ${deviceFingerprint.slice(0, 20)}…`,
@@ -3411,22 +3411,22 @@
lines.push(
`✘ ${name}`,
` ${url}`,
` NETWORK FAILURE — ${err.message}`,
' (blocked by CORS, DNS, mixed content, or the host is unreachable)',
` ${t('NETWORK FAILURE —')} ${err.message}`,
t('(blocked by CORS, DNS, mixed content, or the host is unreachable)'),
''
);
}
}
out.textContent = lines.join('\n');
busy(el.runDiagnosticsBtn, false, 'Run diagnostics');
busy(el.runDiagnosticsBtn, false, t('Run diagnostics'));
}
// Outbound message — confirmed explicitly because it reaches a real person
// and cannot be recalled.
async function sendWhatsApp() {
if (!isSuperAdmin()) {
toast('Sending messages is restricted to super admins.', 'warning');
toast(t('Sending messages is restricted to super admins.'), 'warning');
return;
}
const receiver = $('waReceiver').value.trim();
@@ -3434,44 +3434,44 @@
const status = $('waStatus');
if (!receiver || !message) {
toast('Enter both a recipient and a message.', 'warning');
toast(t('Enter both a recipient and a message.'), 'warning');
return;
}
if (!confirm(`Send this WhatsApp message to ${receiver}?\n\n${message}`)) return;
if (!confirm(t('Send this WhatsApp message to') + ` ${receiver}?\n\n${message}`)) return;
status.textContent = 'Sending…';
status.textContent = t('Sending…');
try {
await api('/Admin/send_whatsapp_message.php', { params: { receiver, message } });
status.textContent = `Sent to ${receiver}`;
status.textContent = t('Sent to') + ` ${receiver}`;
$('waMessage').value = '';
toast('Message sent.', 'success');
toast(t('Message sent.'), 'success');
} catch (err) {
status.textContent = '';
toast(`Send failed: ${err.message}`, 'danger');
toast(t('Send failed:') + ' ' + err.message, 'danger');
}
}
// Super-admin only: mirrors the Flutter EncryptToolPage (Admin/ggg.php).
async function runCryptoTool(action) {
if (!isSuperAdmin()) {
toast('This tool is restricted to super admins.', 'warning');
toast(t('This tool is restricted to super admins.'), 'warning');
return;
}
const text = el.decryptInput.value.trim();
const adminPhone = $('decryptPhone').value.trim();
if (!text || !adminPhone) {
toast('Enter both your admin phone and the value.', 'warning');
toast(t('Enter both your admin phone and the value.'), 'warning');
return;
}
el.decryptOutput.textContent = 'Working…';
el.decryptOutput.textContent = t('Working…');
try {
const payload = await api('/Admin/ggg.php', {
params: { action, text, admin_phone: adminPhone },
});
el.decryptOutput.textContent = payload?.result ?? JSON.stringify(payload, null, 2);
} catch (err) {
el.decryptOutput.textContent = `Failed: ${err.message}`;
el.decryptOutput.textContent = t('Failed:') + ' ' + err.message;
}
}
@@ -3501,7 +3501,7 @@
if (!chosen) return;
API_BASE = chosen;
localStorage.setItem(API_BASE_KEY, chosen);
toast(`API base set to ${chosen}. Reloading data…`, 'success');
toast(t('API base set to') + ` ${chosen}. ${t('Reloading data…')}`, 'success');
renderSessionInfo();
if (session) loadEverything();
});
@@ -3515,9 +3515,9 @@
el.copyDiagnosticsBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(el.diagnosticsOutput.textContent);
toast('Diagnostics report copied.', 'success');
toast(t('Diagnostics report copied.'), 'success');
} catch {
toast('Copy failed — select the text manually.', 'warning');
toast(t('Copy failed — select the text manually.'), 'warning');
}
});
}
@@ -3744,7 +3744,7 @@
ctx.fillStyle = '#64748b';
ctx.font = '12px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('No ride data', cx, cy);
ctx.fillText(t('No ride data'), cx, cy);
} else {
let angle = -Math.PI / 2;
slices.forEach((s) => {
@@ -3763,7 +3763,7 @@
ctx.fillText(fmtInt(total), cx, cy + 2);
ctx.fillStyle = '#64748b';
ctx.font = '10px Inter, sans-serif';
ctx.fillText('TOTAL RIDES', cx, cy + 18);
ctx.fillText(t('TOTAL RIDES'), cx, cy + 18);
}
if (legendEl) {
@@ -3913,7 +3913,7 @@
}
function markKpisUnavailable() {
document.querySelectorAll('[data-kpi]').forEach((n) => { n.textContent = 'n/a'; });
document.querySelectorAll('[data-kpi]').forEach((n) => { n.textContent = t('n/a'); });
}
function tableMessage(tbody, cols, message, isError = false) {
@@ -3951,7 +3951,7 @@
function rating(v) {
const r = Number(v);
if (!r) return '<span class="stamp">unrated</span>';
if (!r) return `<span class="stamp">${t('unrated')}</span>`;
return `<i class="ph-fill ph-star" style="color:var(--warning)"></i> ${r.toFixed(2)}`;
}
@@ -3983,7 +3983,7 @@
};
function labelStatus(status) {
if (!status) return 'Unknown';
if (!status) return t('Unknown');
return t(STATUS_LABELS[String(status).toLowerCase()] || humanize(status));
}
@@ -3997,7 +3997,7 @@
}
function formatRole(role) {
return String(role || '').replace(/_/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase()) || 'Admin';
return String(role || '').replace(/_/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase()) || t('Admin');
}
function initials(name) {