diff --git a/dashboard/siro-admin/index.html b/dashboard/siro-admin/index.html index 756f0e4c..44b9b248 100644 --- a/dashboard/siro-admin/index.html +++ b/dashboard/siro-admin/index.html @@ -15,7 +15,7 @@ - + @@ -280,7 +280,7 @@
@@ -288,11 +288,11 @@
@@ -332,7 +332,7 @@
@@ -1229,8 +1551,8 @@ host.innerHTML = mod.panels.map((p) => `
-

${esc(p.title)}

-
Loading…
+

${esc(t(p.title))}

+
${t('Loading…')}
`).join(''); await Promise.all(mod.panels.map(async (p) => { @@ -1253,11 +1575,11 @@

${esc(tool.title)}

- - + +
-
Enter a value above to query this endpoint.
+
${t('Enter a value above to query this endpoint.')}
`).join(''); @@ -1270,13 +1592,14 @@ const run = async () => { const value = input.value.trim(); if (!value) return; - body.innerHTML = '
Querying…
'; + body.innerHTML = `
${t('Querying…')}
`; try { const payload = await api(tool.path, { params: { [tool.field]: value } }); renderPayload(body, payload); } catch (err) { if (handleApiError(err, 'lookup')) return; body.innerHTML = `
${esc(err.message)}
`; + toast(err.message, 'danger'); } }; @@ -1312,7 +1635,7 @@ const sumSeries = (series) => series.reduce((a, p) => a + p.value, 0); async function renderGrowth(host) { - host.innerHTML = '
Loading growth…
'; + host.innerHTML = `
${t('Loading growth…')}
`; let payload; try { @@ -1333,31 +1656,31 @@ host.innerHTML = `
- ${growthTile(fmtInt(totals.passengers), 'Passengers in total', 'ph-users', 'warning')} - ${growthTile(fmtInt(totals.drivers), 'Captains in total', 'ph-steering-wheel', 'info')} - ${growthTile(`+${fmtInt(joinedP)}`, 'Passengers joined · 30 days', 'ph-user-plus', 'primary')} - ${growthTile(`+${fmtInt(joinedD)}`, 'Captains joined · 30 days', 'ph-user-plus', 'success')} + ${growthTile(fmtInt(totals.passengers), t('Passengers in total'), 'ph-users', 'warning')} + ${growthTile(fmtInt(totals.drivers), t('Captains in total'), 'ph-steering-wheel', 'info')} + ${growthTile(`+${fmtInt(joinedP)}`, t('Passengers joined · 30 days'), 'ph-user-plus', 'primary')} + ${growthTile(`+${fmtInt(joinedD)}`, t('Captains joined · 30 days'), 'ph-user-plus', 'success')}
-

Signups per day last 30 days

+

${t('Signups per day')} ${t('last 30 days')}

- Passengers - Captains + ${t('Passengers')} + ${t('Captains')}
${activeDays === 0 - ? '
Nobody signed up in the last 30 days.
' + ? `
${t('Nobody signed up in the last 30 days.')}
` : `
- ${activeDays} of 30 days had at least one signup + ${activeDays} / 30 ${lang === 'ar' ? 'يوماً بها تسجيل واحد على الأقل' : 'days had at least one signup'}
`}
`; drawMultiLine('growthChart', [ - { label: 'Passengers', color: '#6366f1', points: passengers }, - { label: 'Captains', color: '#10b981', points: drivers }, + { label: t('Passengers'), color: '#6366f1', points: passengers }, + { label: t('Captains'), color: '#10b981', points: drivers }, ]); } @@ -1379,11 +1702,10 @@ host.innerHTML = `
- Revenue is counted from completed rides only. Total is what passengers paid; - platform share is what remains after the captain's cut. + ${t('Revenue is counted from completed rides only.')}
-
Loading revenue…
-
Loading captain ranking…
`; +
${t('Loading revenue…')}
+
${t('Loading captain ranking…')}
`; // Revenue try { @@ -1395,18 +1717,18 @@ $('anRevenue').innerHTML = `
-

Revenue last 30 days

+

${t('Revenue')} ${t('last 30 days')}

-
${summary.total_revenue_all == null ? '—' : fmtMoney(summary.total_revenue_all)}
Total collected
-
${summary.total_profit_all == null ? '—' : fmtMoney(summary.total_profit_all)}
Platform share
-
${summary.avg_ride_price == null ? '—' : fmtMoney(summary.avg_ride_price)}
Average fare
-
${fmtInt(sumSeries(rides))}
Completed rides
+
${summary.total_revenue_all == null ? '—' : fmtMoney(summary.total_revenue_all)}
${t('Total collected')}
+
${summary.total_profit_all == null ? '—' : fmtMoney(summary.total_profit_all)}
${t('Platform share')}
+
${summary.avg_ride_price == null ? '—' : fmtMoney(summary.avg_ride_price)}
${t('Average fare')}
+
${fmtInt(sumSeries(rides))}
${t('Completed rides')}
- ${sumSeries(series) === 0 ? '
No completed rides carried a fare in this period.
' : ''}`; + ${sumSeries(series) === 0 ? `
${t('No completed rides carried a fare in this period.')}
` : ''}`; - drawMultiLine('revenueTrend', [{ label: 'Revenue', color: '#06b6d4', points: series }]); + drawMultiLine('revenueTrend', [{ label: t('Revenue'), color: '#06b6d4', points: series }]); } catch (err) { if (handleApiError(err, 'analytics-revenue')) return; $('anRevenue').innerHTML = `
${esc(err.message)}
`; @@ -1416,7 +1738,7 @@ try { const ranking = await api('/Admin/v2/analytics/driver_ranking.php'); const body = $('anRanking'); - body.innerHTML = '

Captain ranking

'; + body.innerHTML = `

${t('Captain ranking')}

`; renderPayload(body.querySelector('.panel-body'), ranking); } catch (err) { if (handleApiError(err, 'analytics-ranking')) return; @@ -1436,12 +1758,12 @@ host.innerHTML = `
- +
- +
-
Loading organisations…
`; +
${t('Loading organisations…')}
`; $('orgNewBtn').addEventListener('click', () => openOrgForm(null, host)); bindSearch($('orgSearch'), $('orgSearchBtn'), @@ -1452,26 +1774,26 @@ async function loadOrgs(host, search) { const panel = $('orgList'); - panel.innerHTML = '
Loading organisations…
'; + panel.innerHTML = `
${t('Loading organisations…')}
`; try { const payload = await api('/Admin/transit/org/list.php', { params: { search } }); const orgs = payload?.orgs || []; const total = payload?.pagination?.total ?? orgs.length; if (!orgs.length) { - panel.innerHTML = '
No organisations registered yet.
'; + panel.innerHTML = `
${t('No organisations registered yet.')}
`; return; } panel.innerHTML = `
-

Organisations ${fmtInt(total)} total

+

${t('Organisations')} ${fmtInt(total)} ${t('total')}

- - + + ${orgs.map((o, i) => ` @@ -1505,7 +1827,7 @@ async function openOrgDetails(orgId, host) { const body = $('modalBodyContent'); - body.innerHTML = '
Loading…
'; + body.innerHTML = `
${t('Loading…')}
`; $('detailsModal').classList.add('active'); try { @@ -1527,26 +1849,26 @@
-

Routes (${routes.length})

+

${t('Routes')} (${routes.length})

${routes.length ? `
${routes.map((r) => `
${esc(r.name_ar || r.name_en || `#${r.id}`)} ${esc(humanize(r.status || ''))} -
`).join('')}
` : '
No routes yet.
'} +
`).join('')}` : `
${t('No routes yet.')}
`}
-

Recent trips (${trips.length})

+

${t('Recent trips')} (${trips.length})

-

Administrators

-
Loading…
+

${t('Administrators')}

+
${t('Loading…')}
- - - + + +
`; @@ -1555,7 +1877,7 @@ $('orgAdminAdd').addEventListener('click', () => addOrgAdmin(orgId)); } catch (err) { if (handleApiError(err, 'org-details')) return; - body.innerHTML = ``; @@ -1627,64 +1949,64 @@ body.innerHTML = `
${editing ? '' : ` `} ${editing ? ` ` : ''}
`; @@ -1742,65 +2064,63 @@ host.innerHTML = `
- Removal is permanent. The account row is deleted and the phone number is - added to the blacklist so it cannot register again. There is no undo — only lifting the block, - which does not restore the deleted account. + ${t('Removal is permanent.')} + ${lang === 'ar' ? 'يتم حذف حساب السائق وإضافة رقم الهاتف إلى القائمة السوداء حتى لا يتمكن من التسجيل مرة أخرى. لا يمكن التراجع عن هذا الإجراء — فقط رفع الحظر، الذي لا يُعيد الحساب المحذوف.' : 'The account row is deleted and the phone number is added to the blacklist so it cannot register again. There is no undo — only lifting the block, which does not restore the deleted account.'}
-
Loading blacklist…
+
${t('Loading blacklist…')}
-

Remove & blacklist

+

${lang === 'ar' ? 'حذف و加入 القائمة السوداء' : 'Remove & blacklist'}

- +
-

Lift a block

-

Removes the phone number from the blacklist so it can register again. It does - not restore a deleted account.

+

${t('Lift a block')}

+

${lang === 'ar' ? 'يزيل رقم الهاتف من القائمة السوداء حتى يتمكن من التسجيل مرة أخرى. لا يُعيد الحساب المحذوف.' : 'Removes the phone number from the blacklist so it can register again. It does not restore a deleted account.'}

- +
`; @@ -1816,11 +2136,11 @@ const payload = await api('/Admin/v2/quality/blacklist_manager.php', { params: { action_type: 'get_all' }, }); - panel.innerHTML = '

Currently blocked

'; + panel.innerHTML = `

${t('Currently blocked')}

`; renderPayload(panel.querySelector('.panel-body'), payload); } catch (err) { if (handleApiError(err, 'blacklist')) return; - panel.innerHTML = `

Currently blocked

${esc(err.message)}
`; + panel.innerHTML = `

${t('Currently blocked')}

${esc(err.message)}
`; } } @@ -1909,43 +2229,43 @@ host.innerHTML = `
- Launching creates a real discount code and notifies every passenger in the selected country. - The promo stays valid for seven days. Always preview first. + ${lang === 'ar' ? 'الإطلاق ينشئ كود خصم حقيقي ويخبر كل راكب في الدولة المحددة.' : 'Launching creates a real discount code and notifies every passenger in the selected country.'} + ${lang === 'ar' ? 'يبقى كود الخصم صالحاً لمدة سبعة أيام. عاين دائماً أولاً.' : 'The promo stays valid for seven days. Always preview first.'}
-

Campaign parameters

+

${t('Campaign parameters')}

- - - Preview first to enable launching. + + + ${t('Preview first to enable launching.')}
-
No analysis run yet.
+
${t('No analysis run yet.')}
-
Loading campaign history…
`; +
${t('Loading campaign history…')}
`; $('cmpPreview').addEventListener('click', () => runCampaign(true)); $('cmpLaunch').addEventListener('click', () => runCampaign(false)); @@ -1982,15 +2302,15 @@ } const btn = isPreview ? $('cmpPreview') : $('cmpLaunch'); - busy(btn, true, isPreview ? 'Analysing…' : 'Launching…'); - $('cmpResult').innerHTML = '
Running market analysis…
'; + busy(btn, true, isPreview ? t('Analysing…') : t('Launching…')); + $('cmpResult').innerHTML = `
${t('Running market analysis…')}
`; try { const payload = await api('/Admin/marketing/trigger_campaign.php', { params: isPreview ? { ...params, dry_run: '1' } : params, }); - $('cmpResult').innerHTML = `

${isPreview ? 'Preview' : 'Launch result'}

`; + $('cmpResult').innerHTML = `

${isPreview ? t('Preview') : t('Launch result')}

`; renderPayload($('cmpResult').querySelector('.panel-body'), payload); if (isPreview) { @@ -2024,17 +2344,17 @@ const panel = $('cmpLog'); try { const payload = await api('/Admin/marketing/get_campaigns_log.php'); - panel.innerHTML = '

Campaign history

'; + panel.innerHTML = `

${t('Campaign history')}

`; renderPayload(panel.querySelector('.panel-body'), payload); } catch (err) { if (handleApiError(err, 'campaign-log')) return; - panel.innerHTML = `

Campaign history

${esc(err.message)}
`; + panel.innerHTML = `

${t('Campaign history')}

${esc(err.message)}
`; } } // ── App versions ───────────────────────────────────────────────────────── async function renderAppVersions(host) { - host.innerHTML = '
Loading package versions…
'; + host.innerHTML = `
${t('Loading package versions…')}
`; let packages = []; try { @@ -2046,28 +2366,27 @@ } if (!packages.length) { - host.innerHTML = '
No package rows configured.
'; + host.innerHTML = `
${t('No package rows configured.')}
`; return; } host.innerHTML = `
- Each app compares its own build against this number on launch. Raising it can force every - user of that app to update before they can continue. + ${lang === 'ar' ? 'كل تطبيق يقارن إصداره بهذا الرقم عند التشغيل. رفعه يجبر كل مستخدم على التحديث قبل المتابعة.' : 'Each app compares its own build against this number on launch. Raising it can force every user of that app to update before they can continue.'}
${packages.map((pkg, index) => `

${esc(pkg.name || pkg.packageName || pkg.app_name || `Package #${pkg.id}`)} - row #${esc(pkg.id)} + #${esc(pkg.id)}

- +
@@ -2113,7 +2432,7 @@ async function renderDriverDocs(host, offset = 0) { docsOffset = offset; - host.innerHTML = '
Loading captains awaiting review…
'; + host.innerHTML = `
${t('Loading captains awaiting review…')}
`; let drivers = []; try { @@ -2128,14 +2447,14 @@ } if (!drivers.length && offset === 0) { - host.innerHTML = '
No captains are awaiting document review.
'; + host.innerHTML = `
${t('No captains are awaiting document review.')}
`; return; } host.innerHTML = `
-

Awaiting review showing ${drivers.length} from #${offset + 1}

+

${t('Awaiting review')} ${lang === 'ar' ? `عرض ${drivers.length} من #${offset + 1}` : `showing ${drivers.length} from #${offset + 1}`}

@@ -2150,14 +2469,14 @@
- + `).join('')}
NameTypeCityContractCaptainsVehiclesRoutesEnrolled${t('Name')}${t('Type')}${t('City')}${t('Contract')}${t('Captains')}${t('Vehicles')}${t('Routes')}${t('Enrolled')}
#${esc(d.id)} ${esc(`${d.first_name || ''} ${d.last_name || ''}`.trim() || 'Unnamed')} ${esc(maskPhone(d.phone))}
-
Pick a captain above to inspect their documents.
+
${t('Pick a captain above to inspect their documents.')}
`; $('docsPrev')?.addEventListener('click', () => @@ -2198,12 +2517,12 @@ #${esc(driverId)} · ${esc(driver.status || 'unknown')}
-

Documents (${documents.length})

+

${lang === 'ar' ? 'الوثائق' : 'Documents'} (${documents.length})

${documents.length ? `
${documents.map((doc) => ` @@ -2212,18 +2531,18 @@ ? ` ${esc(doc.doc_type || 'document')} ` - : '
no file linked
'} + : `
${lang === 'ar' ? 'لا ملف مرتبط' : 'no file linked'}
`}
${esc(humanize(doc.doc_type || 'document'))} ${esc(doc.image_name || '—')}
`).join('')}
` - : '
This captain has uploaded no documents — approving now would activate an unverified account.
'} + : `
${lang === 'ar' ? 'هذا الكابتن لم يُرسل أي وثائق — الاعتماد الآن يعني تفعيل حساب غير موثّق.' : 'This captain has uploaded no documents — approving now would activate an unverified account.'}
`}
-

Record

+

${lang === 'ar' ? 'السجل' : 'Record'}

${facts.map(([k, v]) => `
@@ -2259,45 +2578,44 @@ // ── Staff management ───────────────────────────────────────────────────── async function renderStaff(host) { host.innerHTML = ` -
Loading pending accounts…
-
Loading employees…
+
${t('Loading pending accounts…')}
+
${t('Loading employees…')}
-

Add a staff account

+

${t('Add a staff account')}

- Creates a login for the Siro admin tools. Choose the password with the new member present, or have - them change it at first sign-in — it is stored hashed and cannot be read back. + ${lang === 'ar' ? 'يُنشئ حساب دخول لأدوات سير للمشرفين. اختر كلمة المرور مع العضو الجديد، أو دعهم يغيّروها عند أول تسجيل دخول —她是 مشفرة ولا يمكن قراءتها.' : 'Creates a login for the Siro admin tools. Choose the password with the new member present, or have them change it at first sign-in — it is stored hashed and cannot be read back.'}

- +
`; @@ -2320,12 +2638,12 @@ .join(''); panel.innerHTML = ` -

Pending activation

+

${t('Pending activation')}

${notes} ${rows.length ? `
- + ${rows.map((r) => ` @@ -2335,12 +2653,12 @@ `).join('')}
IDNamePhoneTypeRequested
${t('ID')}${t('Name')}${t('Phone')}${t('Type')}${lang === 'ar' ? 'طُلب' : 'Requested'}
${esc(r.type)} ${esc(fmtDate(r.created_at, true))}
-
` : (notes ? '' : '
No accounts are waiting for activation.
')}`; +
` : (notes ? '' : '
' + (lang === 'ar' ? 'لا توجد حسابات بانتظار التفعيل.' : 'No accounts are waiting for activation.') + '
')}`; panel.querySelectorAll('[data-activate]').forEach((btn) => btn.addEventListener('click', () => activateStaff(btn.dataset.activate, btn.dataset.type, host))); @@ -2354,11 +2672,11 @@ const panel = $('staffList'); try { const payload = await api('/Admin/employee/get.php'); - panel.innerHTML = '

Employees

'; + panel.innerHTML = `

${t('Employees')}

`; renderPayload(panel.querySelector('.panel-body'), payload); } catch (err) { if (handleApiError(err, 'employees')) return; - panel.innerHTML = `

Employees

${esc(err.message)}
`; + panel.innerHTML = `

${t('Employees')}

${esc(err.message)}
`; } } @@ -2425,7 +2743,7 @@ // transit/route/approve.php accepts approve | suspend | reject and refuses a // no-op transition, so each decision is confirmed against the route's stops. async function renderRouteApprovals(host) { - host.innerHTML = '
Loading draft routes…
'; + host.innerHTML = `
${t('Loading draft routes…')}
`; let routes = []; try { @@ -2438,7 +2756,7 @@ } if (!routes.length) { - host.innerHTML = '
No routes are waiting for approval.
'; + host.innerHTML = `
${t('No routes are waiting for approval.')}
`; return; } @@ -2446,35 +2764,35 @@

- ${esc(route.name_ar || route.name_en || 'Unnamed route')} - #${esc(route.id)} · ${esc(route.org_name || 'unknown organisation')} + ${esc(route.name_ar || route.name_en || (lang === 'ar' ? 'مسار بدون اسم' : 'Unnamed route'))} + #${esc(route.id)} · ${esc(route.org_name || (lang === 'ar' ? 'مؤسسة غير معروفة' : 'unknown organisation'))}

- - + +
-
${esc(route.direction || '—')}
Direction
-
${fmtNum(route.distance_km)} km
Distance
-
${fmtInt(route.duration_min)} min
Duration
-
${fmtInt(route.stops_count)}
Stops
-
${esc(route.country || '—')}
Country
-
${esc(fmtDate(route.created_at, true))}
Submitted
+
${esc(route.direction || '—')}
${t('Direction')}
+
${fmtNum(route.distance_km)} km
${t('Distance')}
+
${fmtInt(route.duration_min)} ${lang === 'ar' ? 'دقيقة' : 'min'}
${t('Duration')}
+
${fmtInt(route.stops_count)}
${t('Stops')}
+
${esc(route.country || '—')}
${t('Country')}
+
${esc(fmtDate(route.created_at, true))}
${t('Submitted')}
-

Stops

+

${t('Stops')}

${(route.stops || []).length ? `
    ${route.stops.map((s) => `
  1. - ${esc(s.name_ar || 'Unnamed stop')} - ${Number(s.is_major) ? 'major' : ''} + ${esc(s.name_ar || (lang === 'ar' ? 'محطة بدون اسم' : 'Unnamed stop'))} + ${Number(s.is_major) ? `${t('major')}` : ''} ${esc(shortCoord(`${s.latitude},${s.longitude}`))}
  2. `).join('')} -
` : '
This route has no stops recorded.
'} + ` : `
${t('This route has no stops recorded.')}
`}
`).join(''); @@ -2513,51 +2831,51 @@ host.innerHTML = `
- This reaches every device at once and cannot be recalled. - The message is recorded in the audit log against your account. + ${lang === 'ar' ? 'هذا يصل لكل الأجهزة دفعة واحدة ولا يمكن استرجاعه.' : 'This reaches every device at once and cannot be recalled.'} + ${lang === 'ar' ? 'يُسجَّل الرسالة في سجل العمليات ضد حسابك.' : 'The message is recorded in the audit log against your account.'}
-

Compose

+

${t('Compose')}

- +
- - + +
- - + +
- +
-

Preview

+

${t('Preview')}

Siro
-
Notification title
-
Message text
+
${t('Notification title')}
+
${t('Message text')}
`; const title = $('bcTitle'); const body = $('bcBody'); const sync = () => { - $('bcPreviewTitle').textContent = title.value.trim() || 'Notification title'; - $('bcPreviewBody').textContent = body.value.trim() || 'Message text'; + $('bcPreviewTitle').textContent = title.value.trim() || t('Notification title'); + $('bcPreviewBody').textContent = body.value.trim() || t('Message text'); }; title.addEventListener('input', sync); body.addEventListener('input', sync); @@ -2588,7 +2906,7 @@ 'It is delivered immediately and cannot be recalled.' )) return; - busy($('bcSend'), true, 'Sending…'); + busy($('bcSend'), true, t('Sending…')); status.textContent = ''; try { @@ -2633,7 +2951,7 @@ let tariffRows = []; async function renderTariffEditor(host) { - host.innerHTML = '
Loading tariff…
'; + host.innerHTML = `
${t('Loading tariff…')}
`; try { const payload = await api('/ride/kazan/get.php'); @@ -2645,7 +2963,7 @@ } if (!tariffRows.length) { - host.innerHTML = '
No tariff rows configured.
'; + host.innerHTML = `
${lang === 'ar' ? 'لا توجد صفوف تعرفة معدّة.' : 'No tariff rows configured.'}
`; return; } @@ -2654,11 +2972,11 @@ ${readOnly ? `
- You are signed in as an admin, so the tariff is shown read-only. Only a super admin can change prices. + ${lang === 'ar' ? 'أنت مسجّل كمدير، لذا تُعرض التعرفة للقراءة فقط. فقط المدير العام يمكنه تغيير الأسعار.' : 'You are signed in as an admin, so the tariff is shown read-only. Only a super admin can change prices.'}
` : `
- These values are live. Saving changes what every passenger is charged from the next ride onwards. Changes are recorded in the audit log against your account. + ${lang === 'ar' ? 'هذه القيم حيّة. الحفظ يغيّر ما يدفعه كل راكب من الرحلة التالية فصاعداً. يُسجَّل التغيير في سجل العمليات ضد حسابك.' : 'These values are live. Saving changes what every passenger is charged from the next ride onwards. Changes are recorded in the audit log against your account.'}
`} ${tariffRows.map((row, index) => tariffCard(row, index, readOnly)).join('')}`; @@ -2680,8 +2998,8 @@ ${readOnly ? '' : `
- - + +
`}
@@ -2758,14 +3076,14 @@ const frag = document.createDocumentFragment(); if (payload === null || payload === undefined || payload === '') { - frag.appendChild(msgNode('The endpoint responded successfully but returned no data yet.')); + frag.appendChild(msgNode(t('The endpoint responded successfully but returned no data yet.'))); return frag; } if (Array.isArray(payload)) { frag.appendChild(payload.length ? buildTable(payload) - : msgNode('No records recorded for this yet — the table is empty in the database.')); + : msgNode(t('No records recorded for this yet — the table is empty in the database.'))); return frag; } @@ -2841,7 +3159,7 @@ } function formatValue(value, column = '') { - if (typeof value === 'boolean') return value ? 'Yes' : 'No'; + if (typeof value === 'boolean') return value ? t('Yes') : t('No'); if (value === null || value === undefined || value === '') return '—'; const str = String(value); if (/^\d{4}-\d{2}-\d{2}[ T]/.test(str)) return fmtDate(str); @@ -3044,28 +3362,28 @@ function showRideDetails(r) { const body = $('modalBodyContent'); const rows = [ - ['Status', labelStatus(r.status)], - ['Passenger', r.passenger_full_name], - ['Passenger phone', maskPhone(r.p_phone)], - ['Completed trips (passenger)', fmtInt(r.p_completed)], - ['Captain', r.driver_full_name], - ['Captain phone', maskPhone(r.d_phone)], - ['Captain completed / cancelled', `${fmtInt(r.d_completed)} / ${fmtInt(r.d_canceled)}`], - ['Pickup', r.address_start || r.start_location], - ['Drop-off', r.address_end || r.end_location], - ['Distance', r.distance ? `${fmtNum(r.distance)} km` : '—'], - ['Passenger fare', fmtMoney(rideFare(r))], - ['Captain earning', r.price_for_driver ? fmtMoney(r.price_for_driver) : '—'], - ['Payment method', r.paymentMethod], - ['Service', r.carType], - ['Requested', rideTimestamp(r)], - ['Started', r.rideTimeStart ? fmtDate(r.rideTimeStart) : '—'], - ['Finished', r.rideTimeFinish ? fmtDate(r.rideTimeFinish) : '—'], - ['Cancellation note', r.cancel_reason], + [t('Status'), labelStatus(r.status)], + [t('Passenger'), r.passenger_full_name], + [t('Passenger phone'), maskPhone(r.p_phone)], + [t('Completed trips (passenger)'), fmtInt(r.p_completed)], + [t('Captain'), r.driver_full_name], + [t('Captain phone'), maskPhone(r.d_phone)], + [t('Captain completed / cancelled'), `${fmtInt(r.d_completed)} / ${fmtInt(r.d_canceled)}`], + [t('Pickup'), r.address_start || r.start_location], + [t('Drop-off'), r.address_end || r.end_location], + [t('Distance'), r.distance ? `${fmtNum(r.distance)} km` : '—'], + [t('Passenger fare'), fmtMoney(rideFare(r))], + [t('Captain earning'), r.price_for_driver ? fmtMoney(r.price_for_driver) : '—'], + [t('Payment method'), r.paymentMethod], + [t('Service'), r.carType], + [t('Requested'), rideTimestamp(r)], + [t('Started'), r.rideTimeStart ? fmtDate(r.rideTimeStart) : '—'], + [t('Finished'), r.rideTimeFinish ? fmtDate(r.rideTimeFinish) : '—'], + [t('Cancellation note'), r.cancel_reason], ]; body.innerHTML = `