// 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 && (
{radiusKm.toFixed(1).replace(/\.0$/, '')} كم
)} {/* BOTTOM SHEET */}
{/* Drag handle — clear up/down affordance */} {/* 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 */} {/* Bell */}
); } // ──────────────────────────────────────────────────────────── // 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 => ( ))}
{/* Filter chips */}
{filterOpts.map(o => { const active = filter === o.id; return ( ); })}
{/* 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 */}
طلب وارد · DISPATCH
{/* 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.pickup}
{order.dropoff}
{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 */}
الاستلام
{order.pickup}
التسليم
{order.dropoff}
{/* 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 */}
الوصول خلال
4 دقائق
{/* 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 */}
الوصول خلال
6 دقائق
الخطوة ٢ من ٢
توجّه إلى الزبون
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 (
{label}
{value}
); } Object.assign(window, { HomeScreen, IncomingAlert, OrderDetail, ToPickup, AtPickup, ToDropoff, CashCollection, Completed, });