Update: 2026-07-25 18:19:51
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user