// ResDri driver app — screens.
// All order data is live from the backend (/orders/pool). There is no seeded
// demo array — an empty pool renders the real "empty" state.
const VERTICAL_LABEL_AR = { food: 'طعام', parcel: 'طرد', grocery: 'بقالة' };
// ────────────────────────────────────────────────────────────
// Home — three states (online | offline | empty). Designed for
// "what should I take now?" — minimal top chrome, sheet defaults
// high enough to show 3+ orders, sort/filter chips, top-pick
// highlight, inline accept.
// ────────────────────────────────────────────────────────────
function HomeScreen({
online, setOnline, homeState, openOrder, acceptOrder, today,
tabBarHeight = 72, setHomeState, radiusKm = 1.5, setRadiusKm,
orders, driverLatLng, driverName, config,
}) {
// Radius slider bounds — server-driven (admin Settings). No hardcoded range.
const RADIUS_MIN = (config && config.radiusMinKm) || 0.5;
const RADIUS_MAX = (config && config.maxRadiusKm) || 5;
const RADIUS_STEP = (config && config.radiusStepKm) || 0.5;
// Live API orders only — no demo fallback. Empty → the "empty" home state.
const pool = Array.isArray(orders) ? orders : [];
// Helper: where to place this order's pin on the static map source pixels.
// - prefer the live order's projected mapPos (from real lat/lng)
// - else the design's hand-placed PIN_COORDS by id
// - else fall back to the city centre
const pinFor = (o) => o.mapPos || PIN_COORDS[o.id] || PIN_COORDS.CENTER;
// Lower collapsed default so more map is visible behind the sheet
const [sheet, setSheet] = React.useState(0.50);
const [sort, setSort] = React.useState('best'); // best | nearest | highest
const [filter, setFilter] = React.useState('all'); // all | food | parcel | grocery
const coversTabBar = sheet > 0.7;
const isExpanded = sheet > 0.7;
const showPool = homeState === 'online' && online;
const showEmpty = homeState === 'empty' && online;
const showOffline = !online || homeState === 'offline';
const mapMode = showOffline || showEmpty ? 'driver-only' : 'pool';
const mapDim = showOffline;
// Sorted/filtered orders — also gate by radius (using toPickup distance)
const filtered = React.useMemo(() => {
let list = pool.filter(o => filter === 'all' ? true : o.kind === filter);
list = list.filter(o => (o.toPickup ?? o.distance) <= radiusKm);
const score = (o) => (o.payout + (o.bonus || 0)) / Math.max(0.5, o.distance);
if (sort === 'nearest') list = [...list].sort((a, b) => (a.toPickup ?? a.distance) - (b.toPickup ?? b.distance));
else if (sort === 'highest') list = [...list].sort((a, b) => (b.payout + b.bonus) - (a.payout + a.bonus));
else list = [...list].sort((a, b) => score(b) - score(a));
return list;
}, [pool, sort, filter, radiusKm]);
// Out-of-radius orders — shown as edge indicators on the map
const outOfRadius = React.useMemo(() => {
const cutoff = filter === 'all' ? pool : pool.filter(o => o.kind === filter);
return cutoff
.filter(o => (o.toPickup ?? o.distance) > radiusKm)
.map(o => ({
id: o.id,
kind: o.kind,
payout: o.payout,
bonus: o.bonus || 0,
distanceKm: o.toPickup ?? o.distance,
pos: pinFor(o),
}));
}, [pool, filter, radiusKm]);
const topPickId = filtered[0]?.id;
// Visible map area (above the bottom sheet)
const sheetTopPx = (1 - sheet) * 874;
const visibleArea = {
top: 110,
left: 0,
right: 402,
bottom: Math.max(140, sheetTopPx - 8),
};
return (
{/* MAP — fills behind */}
openOrder(id)}
radiusKm={radiusKm}
orders={filtered.map(o => ({
id: o.id, kind: o.kind, payout: o.payout, bonus: o.bonus || 0,
pos: pinFor(o),
}))}
outOfRadiusOrders={outOfRadius}
/>
{/* SLIM TOP — avatar + earnings pill + bell, all in one row */}
{/* RADIUS ADJUSTER — floating, top-right under the bell, only when online */}
{showPool && (
setRadiusKm && setRadiusKm(Math.max(RADIUS_MIN, +(radiusKm - RADIUS_STEP).toFixed(1)))} style={{
width: 32, height: 32, borderRadius: 10,
background: 'transparent', border: 'none',
color: 'var(--text-2)', fontFamily: 'inherit', fontWeight: 800, fontSize: 18,
display: 'grid', placeItems: 'center', cursor: 'pointer',
}}>−
{radiusKm.toFixed(1).replace(/\.0$/, '')}
كم
setRadiusKm && setRadiusKm(Math.min(RADIUS_MAX, +(radiusKm + RADIUS_STEP).toFixed(1)))} style={{
width: 32, height: 32, borderRadius: 10,
background: 'transparent', border: 'none',
color: 'var(--text-2)', fontFamily: 'inherit', fontWeight: 800, fontSize: 18,
display: 'grid', placeItems: 'center', cursor: 'pointer',
}}>+
)}
{/* BOTTOM SHEET */}
{/* Drag handle — clear up/down affordance */}
setSheet(isExpanded ? 0.42 : 0.82)}
style={{
padding: '10px 16px 6px', display: 'flex', flexDirection: 'column',
alignItems: 'center', gap: 4, flexShrink: 0,
background: 'transparent', border: 'none', width: '100%',
fontFamily: 'inherit', cursor: 'pointer',
}}>
{/* The bar */}
{/* Direction indicator */}
{isExpanded ? 'اسحب لتصغير' : 'اسحب لعرض الكل'}
{/* STICKY ONLINE LEVER */}
{
const next = !online;
setOnline(next);
if (setHomeState) setHomeState(next ? 'online' : 'offline');
}} />
{/* Content varies by state */}
{showOffline &&
}
{showEmpty &&
}
{showPool && (
setSheet(isExpanded ? 0.42 : 0.82)}
/>
)}
);
}
// ────────────────────────────────────────────────────────────
// SlimTopBar — compact single-row top chrome
// Layout (RTL): [avatar+greeting] [earnings pill] [bell]
// ────────────────────────────────────────────────────────────
function SlimTopBar({ online, today, orders, driverName }) {
const initial = (driverName || 'م').trim().charAt(0) || 'م';
return (
{/* Avatar with online dot */}
{initial}
{/* Earnings pill — tappable, expands to Earnings tab */}
اليوم
{orders}
طلب
{/* Bell */}
{online && (
)}
);
}
// ────────────────────────────────────────────────────────────
// PoolHomeContent — list with sort/filter chips + featured top pick
// ────────────────────────────────────────────────────────────
function PoolHomeContent({ orders, topPickId, sort, setSort, filter, setFilter, openOrder, acceptOrder, isExpanded, onToggleExpand }) {
const sortOpts = [
{ id: 'best', lbl: 'الأفضل' },
{ id: 'nearest', lbl: 'الأقرب' },
{ id: 'highest', lbl: 'الأعلى ربحاً' },
];
const filterOpts = [
{ id: 'all', lbl: 'الكل', icon: null },
{ id: 'food', lbl: 'طعام', icon: 'food' },
{ id: 'parcel', lbl: 'طرود', icon: 'package' },
{ id: 'grocery', lbl: 'بقالة', icon: 'cart' },
];
return (
<>
{/* Sort + filter row */}
{/* Sort segmented */}
{sortOpts.map(o => (
setSort(o.id)} style={{
flex: 1, height: 32, border: 'none',
background: sort === o.id ? 'var(--surface)' : 'transparent',
color: sort === o.id ? 'var(--text)' : 'var(--text-2)',
fontFamily: 'inherit', fontWeight: sort === o.id ? 800 : 600, fontSize: 12,
borderRadius: 9,
boxShadow: sort === o.id ? '0 1px 3px rgba(0,0,0,0.06)' : 'none',
}}>{o.lbl}
))}
{/* Filter chips */}
{filterOpts.map(o => {
const active = filter === o.id;
return (
setFilter(o.id)} style={{
height: 32, padding: '0 12px', borderRadius: 999,
background: active ? 'var(--primary)' : 'var(--surface)',
color: active ? '#fff' : 'var(--text)',
border: active ? 'none' : '1px solid var(--line)',
fontFamily: 'inherit', fontWeight: 700, fontSize: 12,
display: 'inline-flex', alignItems: 'center', gap: 5,
whiteSpace: 'nowrap', flexShrink: 0,
}}>
{o.icon && }
{o.lbl}
);
})}
{/* Pool list */}
{orders.length === 0 ? (
لا طلبات بهذا التصنيف
جرّب تصنيفاً آخر
) : orders.map((o) => (
openOrder(o.id)}
onAccept={() => acceptOrder ? acceptOrder(o.id) : openOrder(o.id)}
/>
))}
>
);
}
function OfflineHomeContent({ today }) {
return (
أنت غير متّصل حالياً
لن تتلقى أي طلبات. اضغط الزر أعلاه عندما تكون جاهزاً للعمل.
{/* Pre-check list */}
);
}
function CheckRow({ ok, label, detail }) {
return (
{label}
{detail &&
{detail} }
);
}
function EmptyHomeContent() {
return (
{/* Friendly "listening" illustration — concentric radar circles + icon */}
{[0, 0.6, 1.2].map(d => (
))}
لا توجد طلبات في الوقت الحالي
سنخبرك فور توفّر طلب في منطقتك. لا تحتاج إلى إبقاء الشاشة مفتوحة.
);
}
// ────────────────────────────────────────────────────────────
// Incoming order alert — HERO screen. Calm dispatch-radio chirp.
// - Two RTL placements via prop `primarySide` ('right' = leading, 'left' = trailing)
// - 15-second countdown ring
// - Soft radar pulse animation
// ────────────────────────────────────────────────────────────
function IncomingAlert({ order, onAccept, onSkip, primarySide = 'right' }) {
const [t, setT] = React.useState(15);
React.useEffect(() => {
if (t <= 0) { onSkip(); return; }
const id = setTimeout(() => setT(t - 1), 1000);
return () => clearTimeout(id);
}, [t]);
const verticalC = { food: '#E11D48', parcel: '#2563EB', grocery: '#16A34A' }[order.kind];
const ringP = (15 - t) / 15;
const acceptBtn = (
قبول
);
const skipBtn = (
);
return (
{/* Subtle vertical-stripe texture for "workhorse" feel */}
{/* Top banner: dispatch label + sound icon */}
{/* Center: vertical chip + huge payout + countdown ring */}
{/* Vertical badge + label */}
{VERTICAL_LABEL_AR[order.kind]} · {order.business}
{/* Payout — HUGE, with countdown ring around it */}
{/* Soft radar pulse (only first few seconds) */}
{t > 11 && (
)}
{/* Countdown ring */}
أرباحك
{order.bonus > 0 && (
+₪{order.bonus}
مكافأة
)}
{t} ثانية
{/* Route summary */}
{order.distance.toFixed(1)} كم
~{order.eta} دقيقة
طلب #{order.shortId}
{/* Bottom: Accept + Skip — primary on configurable side */}
{acceptBtn}
{skipBtn}
);
}
// ────────────────────────────────────────────────────────────
// Order Detail (pre-accept) — full route on map + giant Accept
// ────────────────────────────────────────────────────────────
function OrderDetail({ order, onAccept, onBack, onSkip }) {
return (
{/* Back chip */}
{/* Distance/eta floating chip */}
المسافة الكلية
{order.distance.toFixed(1)} كم
{/* Bottom sheet — details */}
{/* Handle */}
{/* Vertical + business + payout */}
{order.business}
{VERTICAL_LABEL_AR[order.kind]} · للزبون {order.customer}
{order.bonus > 0 && (
{'+₪' + order.bonus} مكافأة
)}
{/* Route */}
{/* Payout breakdown */}
تفصيل الأرباح
{order.bonus > 0 &&
}
{order.cash > 0 && (
<>
اقبض نقداً من الزبون
>
)}
{/* Notes */}
{order.notes && (
ملاحظة من المرسل
"{order.notes}"
)}
{/* Compatibility */}
متوافق مع مركبتك (سيارة)
{/* Bottom action bar — pinned */}
);
}
function MoneyRow({ label, value, bold, accent }) {
return (
{label}
{value} ₪
);
}
// ────────────────────────────────────────────────────────────
// Active order — heading to pickup
// ────────────────────────────────────────────────────────────
function ToPickup({ order, onArrived, onBack }) {
return (
{/* Top card: pickup info */}
الاستلام من
{order.business}
{order.pickup}
{/* ETA banner */}
{/* Bottom action sheet */}
الخطوة ١ من ٢
توجّه إلى نقطة الاستلام
{order.distance.toFixed(1)} كم
);
}
// ────────────────────────────────────────────────────────────
// At pickup — confirm handover, optional photo
// ────────────────────────────────────────────────────────────
function AtPickup({ order, onPicked }) {
const [photo, setPhoto] = React.useState(false);
return (
وصلت إلى الاستلام
{order.business}
تأكيد الاستلام
هل استلمت الطلب من المتجر؟
{/* Photo affordance */}
setPhoto(!photo)} style={{
height: 92, borderRadius: 14,
background: photo ? 'color-mix(in oklch, var(--success) 14%, transparent)' : 'var(--surface-2)',
border: photo ? '1.5px solid color-mix(in oklch, var(--success) 32%, transparent)' : '1.5px dashed var(--line-strong)',
display: 'flex', alignItems: 'center', gap: 12,
padding: '0 16px', marginBottom: 14,
}}>
{photo ? 'تم التقاط صورة الإيصال' : 'صورة الإيصال (اختياري)'}
{photo ? 'اضغط لإعادة التقاط' : 'يساعد في حل النزاعات لاحقاً'}
تأكيد الاستلام والمتابعة
);
}
// ────────────────────────────────────────────────────────────
// Heading to dropoff
// ────────────────────────────────────────────────────────────
function ToDropoff({ order, onArrived }) {
return (
الزبون
{order.customer}
{order.dropoff}
{/* ETA */}
الخطوة ٢ من ٢
توجّه إلى الزبون
1.4 كم
);
}
// ────────────────────────────────────────────────────────────
// CASH COLLECTION — hero. Customer is standing right there.
// - Singular focus: the amount, large and unambiguous
// - Slide-to-confirm (deliberate, not one-tap)
// - Minimal competing UI
// ────────────────────────────────────────────────────────────
function CashCollection({ order, onCollected, onBack }) {
return (
{/* Minimal header — just back + step */}
{/* Customer banner */}
اقبض من
{order.customer}
طلب #{order.shortId}
{/* The big number — visually dominant, calm */}
{/* Soft halo behind the number */}
المبلغ المطلوب نقداً (شامل الضريبة)
{/* Breakdown — quiet, secondary */}
{/* Slide-to-confirm — deliberate */}
عدّ المبلغ أمام الزبون قبل التأكيد. لا يمكن التراجع.
);
}
// ────────────────────────────────────────────────────────────
// Completed summary
// ────────────────────────────────────────────────────────────
function Completed({ order, onClose, totalToday }) {
return (
{/* Check mark */}
تم تسليم الطلب
طلب #{order.shortId} · للزبون {order.customer}
{/* Earnings card */}
ربحك من هذا الطلب
{order.bonus > 0 &&
}
{/* Quick stats */}
العودة إلى الطلبات
);
}
function Stat({ label, value }) {
return (
);
}
Object.assign(window, {
HomeScreen, IncomingAlert, OrderDetail,
ToPickup, AtPickup, ToDropoff,
CashCollection, Completed,
});