Add broadcast notifications and transit route approvals

Broadcast: ride/firebase/send_fcm.php is an internal service guarded by a
shared secret, so the browser cannot call it — holding that key client-side
would expose it, and the endpoint cannot tell who the sender is. A new
Admin/notifications/broadcast.php sits in front of it: it runs behind
connect.php, requires super_admin, restricts the target to the two topics the
apps actually subscribe to ('drivers'/'passengers') so it cannot be used to
push to an arbitrary topic or a single device token, bounds the title and
body, writes an audit entry before dispatching, and only then forwards the
call internally with the shared secret.

The composer shows a live push preview and an explicit confirmation naming
the audience, since a broadcast cannot be recalled.

Route approvals: draft routes render with their stops, distance and stop
count, and approve/reject posts to transit/route/approve.php behind a
confirmation stating the consequence. Available to admins and super admins,
matching the endpoint's own role check.

Also render user-supplied text with unicode-bidi: plaintext — Arabic names,
addresses and messages were being laid out left-to-right inside the
English UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-25 02:15:02 +03:00
co-authored by Claude Opus 5
parent 41a06bba0c
commit a0ab6c5155
3 changed files with 396 additions and 5 deletions
+73
View File
@@ -1253,3 +1253,76 @@ h1, h2, h3, h4, h5, h6 {
.tariff-field .form-input { padding-left: 1rem; font-size: 0.9rem; }
.tariff-field .form-input:disabled { opacity: 0.65; cursor: not-allowed; }
/* Route approvals */
.stop-list {
margin: 0;
padding-left: 1.2rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
color: var(--text-muted);
font-size: 0.85rem;
}
.stop-list li {
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
}
.stop-list li span:first-child { color: var(--text-main); }
/* Broadcast preview */
.push-preview {
max-width: 420px;
padding: 1rem 1.15rem;
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
}
.push-app {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-subtle);
margin-bottom: 0.5rem;
}
.push-app i { color: var(--primary); }
.push-title {
font-weight: 600;
color: var(--text-main);
margin-bottom: 0.2rem;
word-break: break-word;
}
.push-body {
font-size: 0.86rem;
color: var(--text-muted);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
/* Bidirectional text: names, addresses and messages are often Arabic while the
UI chrome is English. `plaintext` lets each value pick its own direction
from its first strong character instead of inheriting the page's LTR. */
.form-input,
.data-table td,
.push-title,
.push-body,
.kv-row strong,
.stop-list li span:first-child,
.kpi-tile-value {
unicode-bidi: plaintext;
}
textarea.form-input { text-align: start; }