🍽️ حلا فود — وثيقة البناء الكاملة
معلومات المشروع
| الموقع | halafood.wardyat.net |
| Stack | React 18 + TypeScript + Vite + Tailwind CSS |
| Backend | Supabase (PostgreSQL + RPC + RLS) |
| الألعاب | games.halafood.wardyat.net (تطبيق منفصل) |
| 966550688470 (المطبخ) | |
| Supabase URL | qicpzqbemuvzcgwpujru.supabase.co |
نموذج العمل
| وجبة | حلى | |
|---|---|---|
| سعر الزميلة | SR 15 | SR 10 |
| سعر الجملة | SR 12 | SR 8 |
| ربح الموزّعة | SR 3 | SR 2 |
- بونص الموزّعة: كل 10 وجبات = وجبتان مجانيتان
- ولاء العميلة: كل SR 70 إنفاق = وجبة مجانية
- حد الإرسال: SR 150 قبل الإرسال للمطبخ
أنواع المستخدمين
| المستخدم | رابط الدخول | الدور |
|---|---|---|
| 🔴 المطبخ | /?admin=hala2026 |
تحديد المنيو، تأكيد الطلبات |
| 🟡 الموزّعة | /?dashboard=code |
جمع الطلبات، الإرسال للمطبخ |
| 🟢 العميلة | /?me=phone |
الطلبات، النقاط، المستويات |
| ⚪ زميلة | /?ref=code |
طلب مباشر عبر الموزّعة |
| 🎮 لاعبة | games.../?phone=رقم |
الألعاب والكوينز |
حلا فود — وثيقة إعادة البناء الكاملة
أولاً: قوانين الكود بالعربية (مصدر الحقيقة)
🏗️ الهيكل العام
القانون 1 — التوجيه بالـ URL فقط
لا يوجد React Router. التوجيه يعتمد على query parameters في URL:
- ?ref=كود أو ?order=كود → صفحة طلب الزميلة (ColleagueOrderPage)
- ?dashboard=كود → لوحة الموزّعة (AmbassadorDashboard)
- ?admin=كود → لوحة المطبخ (AdminPanel)
- ?me=رقم_الهاتف → صفحة العميلة الشخصية (CustomerPage)
- لا شيء → الصفحة الرئيسية العامة (منيو + سلة)
القانون 2 — مصدر البيانات: Supabase فقط
كل العمليات تمر عبر Supabase RPC functions. لا يوجد REST API مخصص. الـ anon key هو الوحيد المستخدم في الواجهة. الأمان يعتمد على RLS + SECURITY DEFINER.
القانون 3 — لا نظام تسجيل دخول
- المطبخ يدخل بكود سري (hala2026) موجود في جدول admin_config (لا أحد يقرأه من الخارج)
- الموزّعة تعرّفها بكودها مثل maria في الرابط
- العميلة تُعرَّف برقم هاتفها
- كل هذا بدون JWT أو sessions
💰 نموذج العمل (Business Logic)
القانون 4 — نظام السعرين
- سعر العميل: وجبة = SR 15، حلى = SR 10
- سعر الجملة (للموزّعة): وجبة = SR 12، حلى = SR 8
- ربح الموزّعة = الفرق: SR 3 للوجبة، SR 2 للحلى
- هذا الحساب يجري في الخادم عبر trigger، ليس في الواجهة (منع الاحتيال)
القانون 5 — نظام البونص (وجبات مجانية)
- كل 10 وجبات تبيعها الموزّعة → تكسب 2 وجبة مجانية
- المعادلة: (إجمالي_الوجبات ÷ 10) × 2 = وجبات_مجانية_مكتسبة
- الباقي للبونص التالي: 10 - (إجمالي_الوجبات % 10)
- يوجد أيضاً منح يدوية من المطبخ (عمود bonus_free_meals)
- لا يُحتسب حتى يؤكّد المطبخ الطلب (status = confirmed أو delivered فقط)
القانون 6 — الحد الأدنى للإرسال
لا يمكن للموزّعة إرسال الطلبات للمطبخ حتى يصل مجموع تجزئة الطلبات إلى SR 150 أو أكثر. هذا الحد مخزون في ثابت: MIN_WHOLESALE_TOTAL = 150
القانون 7 — طلب الزميلة مقابل طلب الجملة
- طلب الزميلة (colleague_orders): يُسجّل بسعر التجزئة — هذا ما تدفعه الزميلة للموزّعة
- طلب الجملة (orders مع is_wholesale = true): يُنشأ عند إرسال المجموعة للمطبخ — هذا ما تدفعه الموزّعة للمطبخ
القانون 8 — نظام ولاء العميلة
- العميلة تكسب وجبة مجانية كل SR 70 تنفقها
- الحساب: Math.floor(رصيد_الإنفاق / 70) = عدد الوجبات المجانية المكتسبة
- يُحسب من مجموع طلباتها (بسعر التجزئة) عبر dالة get_customer_profile
🗄️ قاعدة البيانات
القانون 9 — الجداول الأساسية
menu_items — أصناف المنيو (ثابتة، 10 وجبات + 6 حلويات فلبينية)
ambassadors — الموزّعات (code، display_name، phone، hospital، active)
orders — الطلبات (جملة + مباشرة)
colleague_orders — طلبات الزميلات (مجمّعة تحت موزّعة)
admin_config — صف واحد فقط (الكود السري + رقم واتساب المطبخ)
coin_ledger — سجل كوينز العميلات (الألعاب)
bonus_ledger — سجل الوجبات المجانية للموزّعات
القانون 10 — حقل is_today
الصفحة الرئيسية تعرض فقط الأصناف التي is_today = true. المطبخ يختار طبخة اليوم عبر دالة set_today_menu. لا يوجد "منيو دائم" للعملاء.
القانون 11 — الأمان بطبقتين
- طبقة 1: RLS على كل الجداول. colleague_orders لا توجد لها سياسة قراءة عامة على الإطلاق
- طبقة 2: دوال SECURITY DEFINER — المنطق الحساس (إحصاءات، إرسال مجموعة، منح مجاني) يجري على الخادم بصلاحيات كاملة
القانون 12 — Trigger على الأرقام
كل طلب (سواء في orders أو colleague_orders) يمر عبر trigger يعيد حساب:
- meal_count — عدد الوجبات من menu_items (ليس من العميل)
- total_amount — المجموع الصحيح من menu_items
- expected_profit — الربح المتوقع (للطلبات الجملة فقط)
الهدف: منع أي تلاعب من الواجهة الأمامية.
📱 WhatsApp والمشاركة
القانون 13 — لا إرسال تلقائي
التطبيق يبني نص الرسالة ويفتح https://wa.me/رقم?text=... في نافذة جديدة. المستخدم هو من يضغط إرسال. لا توجد واجهة API لواتساب.
القانون 14 — رقم المطبخ ثابت
رقم واتساب المطبخ: 966550688470 — مخزون في HALA_FOOD_WHATSAPP كثابت في whatsapp.ts
القانون 15 — صور الحالة بالـ Canvas
لوحة اليوم (1080×1350 بكسل) تُبنى ديناميكياً بـ Canvas API:
- خلفية برتقالية بتدرّج
- شبكة 2×2 لصور الأطباق
- الاسم الفلبيني + السعر على كل صورة
- لوغو حلا فود + تاريخ اليوم + رابط الموزّعة (اختياري)
- تُحمَّل كـ Blob ثم تُشارك عبر Web Share API (أو تُنزَّل إن لم تكن مدعومة)
🎮 نظام الكوينز والألعاب
القانون 16 — مستويات العميلة (4 مستويات)
- مبتدئة 🌱 (0 — 9,999 كوين)
- منتظمة ⭐ (10,000 — 49,999 كوين)
- VIP 💎 (50,000 — 199,999 كوين)
- أسطورة 👑 (200,000+ كوين)
القانون 17 — أنواع الألعاب
wheel | scratch | tap | daily | memory | mission | slot | weekly | mystery | envelope
كلها تستدعي نفس دالة play_game(phone, gameType) في Supabase. القيود اليومية في الخادم.
💻 واجهة المستخدم
القانون 18 — الألوان الثابتة
- برتقالي غامق: #FF4500 — للهيدر والأزرار الرئيسية
- برتقالي فاتح: #FF8C00 — للتدرّج
- ذهبي: #F7B12B — للتمييز والتركيز
- أخضر: #148C3C — للنجاح وزر الطلب
- خلفية: #FFF0DC — كريمي دافئ
القانون 19 — الاتجاهات
- صفحات العملاء (ColleagueOrderPage): dir="ltr" (الإنجليزية أولاً)
- لوحة الموزّعة والمطبخ: dir="rtl" (العربية)
- خلط اتجاهات داخل نفس الصفحة عند الضرورة
القانون 20 — هيكل الصفحة الرئيسية
Header ثابت في الأعلى → قائمة المنيو (شبكة 2 عمود) → بانر التجنيد في الأسفل → سلة + زر الطلب تطفو فوق كل شيء (fixed bottom)
القانون 21 — DishCard
بطاقة صنف واحدة مع: صورة + اسم فلبيني + اسم عربي (اختياري) + سعر + زر إضافة (أو +/-). تعمل في وضعين: عادي (سعر العميل) أو جملة (سعر الموزّعة).
📲 PWA
القانون 22 — تثبيت التطبيق
يُستمع لـ beforeinstallprompt event. عند توفّره يُعرض زر تثبيت في صفحة النجاح. للـ iOS يُعرض تعليم نصي (مشاركة → أضف للشاشة الرئيسية).
القانون 23 — إعادة توجيه PWA
عند فتح التطبيق في وضع standalone (مثبّت على الهاتف) بدون query params:
- يقرأ localStorage["hf_me"] للحصول على كود الموزّعة المحفوظة
- إن وجد → window.location.replace("/?ref=كود") تلقائياً
💾 localStorage
القانون 24 — مفتاح hf_me
المفتاح الوحيد المستخدم: hf_me
يخزن: { name, phone, code, ts }
- name: اسم العميلة
- phone: رقم هاتفها
- code: كود آخر موزّعة زارت عبرها
- ts: timestamp للإنشاء
يُقرأ عند بدء تشغيل أي صفحة لتعبئة الحقول مسبقاً وتحقيق تجربة "العودة".
🔢 تطبيع الأرقام
القانون 25 — أرقام الهاتف السعودية
normPhone(raw) — قانون موحّد:
- 05XXXXXXXX → 9665XXXXXXXX
- 5XXXXXXXX (9 أرقام) → 9665XXXXXXXX
- 966... يُترك كما هو
هذا الرقم المُطبَّع هو المفتاح الفريد للعميلة في الـ URL (?me=) وفي قاعدة البيانات.
ثانياً: وثيقة المنيو الكاملة
الوجبات (10 وجبات فلبينية) — SR 15 للعميل / SR 12 للموزّعة
| الاسم الفلبيني | الاسم العربي |
|---|---|
| Chopsuey with Rice | تشوب سوي مع رز |
| Fried Chicken with Rice | دجاج مقلي مع رز |
| Lumpia Shanghai with Rice | لمبية شنغهاي مع رز |
| Monggo with Fish and Rice | مونقو مع سمك ورز |
| Ginataang Laing with Rice | جيناتانغ لاينغ مع رز |
| Chicken Inasal with Rice | دجاج إناسال مشوي مع رز |
| Kaldereta with Rice | كالديريتا لحم مع رز |
| Spaghetti with Chicken | سباغيتي مع دجاج |
| Aroskaldo with Shanghai | أروز كالدو مع لمبية |
| Sopas with Puto | شوربة سوباس مع بوتو |
الحلويات (6 حلويات فلبينية) — SR 10 للعميل / SR 8 للموزّعة
| الاسم الفلبيني | الاسم العربي |
|---|---|
| Biko | بيكو · رز حلو بجوز الهند |
| Turon | تورون · موز مقلي مقرمش |
| Bilo Bilo | بيلو بيلو · كرات أرز بالحليب |
| Banana Puto | بوتو موز |
| Kutsinta | كوتسينتا · كيك أرز |
| Putong Bigas | بوتونغ بيقاس · كيك أرز مبخّر |
ثالثاً: البرومبت الكامل لإعادة البناء
═══════════════════════════════════════════════════
HALA FOOD — COMPLETE REBUILD PROMPT
═══════════════════════════════════════════════════
أريد بناء تطبيق ويب كامل من الصفر اسمه Hala Food — منصة طلب طعام منزلي فلبيني تعمل في السعودية. التطبيق يستهدف عاملات المنازل الفلبينيات في المستشفيات السعودية. نموذج العمل يعتمد على موزّعات (ambassadors) يجمّعن طلبات زميلاتهن.
⚙️ المكدس التقني
- Frontend: React 18 + TypeScript + Vite
- Styling: Tailwind CSS v3
- Icons: lucide-react
- Backend: Supabase (PostgreSQL + RPC functions + RLS)
- بدون: React Router أو أي state management خارجي
🗺️ نظام التوجيه
لا React Router — التوجيه يعتمد على query parameters فقط:
```typescript
const params = new URLSearchParam
حلا فود — وثيقة إعادة البناء الكاملة
أولاً: قوانين الكود بالعربية (مصدر الحقيقة)
🏗️ الهيكل العام
القانون 1 — التوجيه بالـ URL فقط
لا يوجد React Router. التوجيه يعتمد على query parameters في URL:
- ?ref=كود أو ?order=كود → صفحة طلب الزميلة (ColleagueOrderPage)
- ?dashboard=كود → لوحة الموزّعة (AmbassadorDashboard)
- ?admin=كود → لوحة المطبخ (AdminPanel)
- ?me=رقم_الهاتف → صفحة العميلة الشخصية (CustomerPage)
- لا شيء → الصفحة الرئيسية العامة (منيو + سلة)
القانون 2 — مصدر البيانات: Supabase فقط
كل العمليات تمر عبر Supabase RPC functions. لا يوجد REST API مخصص. الـ anon key هو الوحيد المستخدم في الواجهة. الأمان يعتمد على RLS + SECURITY DEFINER.
القانون 3 — لا نظام تسجيل دخول
- المطبخ يدخل بكود سري (hala2026) موجود في جدول admin_config (لا أحد يقرأه من الخارج)
- الموزّعة تعرّفها بكودها مثل maria في الرابط
- العميلة تُعرَّف برقم هاتفها
- كل هذا بدون JWT أو sessions
💰 نموذج العمل (Business Logic)
القانون 4 — نظام السعرين
- سعر العميل: وجبة = SR 15، حلى = SR 10
- سعر الجملة (للموزّعة): وجبة = SR 12، حلى = SR 8
- ربح الموزّعة = الفرق: SR 3 للوجبة، SR 2 للحلى
- هذا الحساب يجري في الخادم عبر trigger، ليس في الواجهة (منع الاحتيال)
القانون 5 — نظام البونص (وجبات مجانية)
- كل 10 وجبات تبيعها الموزّعة → تكسب 2 وجبة مجانية
- المعادلة: (إجمالي_الوجبات ÷ 10) × 2 = وجبات_مجانية_مكتسبة
- الباقي للبونص التالي: 10 - (إجمالي_الوجبات % 10)
- يوجد أيضاً منح يدوية من المطبخ (عمود bonus_free_meals)
- لا يُحتسب حتى يؤكّد المطبخ الطلب (status = confirmed أو delivered فقط)
القانون 6 — الحد الأدنى للإرسال
لا يمكن للموزّعة إرسال الطلبات للمطبخ حتى يصل مجموع تجزئة الطلبات إلى SR 150 أو أكثر. هذا الحد مخزون في ثابت: MIN_WHOLESALE_TOTAL = 150
القانون 7 — طلب الزميلة مقابل طلب الجملة
- طلب الزميلة (colleague_orders): يُسجّل بسعر التجزئة — هذا ما تدفعه الزميلة للموزّعة
- طلب الجملة (orders مع is_wholesale = true): يُنشأ عند إرسال المجموعة للمطبخ — هذا ما تدفعه الموزّعة للمطبخ
القانون 8 — نظام ولاء العميلة
- العميلة تكسب وجبة مجانية كل SR 70 تنفقها
- الحساب: Math.floor(رصيد_الإنفاق / 70) = عدد الوجبات المجانية المكتسبة
- يُحسب من مجموع طلباتها (بسعر التجزئة) عبر dالة get_customer_profile
🗄️ قاعدة البيانات
القانون 9 — الجداول الأساسية
menu_items — أصناف المنيو (ثابتة، 10 وجبات + 6 حلويات فلبينية)
ambassadors — الموزّعات (code، display_name، phone، hospital، active)
orders — الطلبات (جملة + مباشرة)
colleague_orders — طلبات الزميلات (مجمّعة تحت موزّعة)
admin_config — صف واحد فقط (الكود السري + رقم واتساب المطبخ)
coin_ledger — سجل كوينز العميلات (الألعاب)
bonus_ledger — سجل الوجبات المجانية للموزّعات
القانون 10 — حقل is_today
الصفحة الرئيسية تعرض فقط الأصناف التي is_today = true. المطبخ يختار طبخة اليوم عبر دالة set_today_menu. لا يوجد "منيو دائم" للعملاء.
القانون 11 — الأمان بطبقتين
- طبقة 1: RLS على كل الجداول. colleague_orders لا توجد لها سياسة قراءة عامة على الإطلاق
- طبقة 2: دوال SECURITY DEFINER — المنطق الحساس (إحصاءات، إرسال مجموعة، منح مجاني) يجري على الخادم بصلاحيات كاملة
القانون 12 — Trigger على الأرقام
كل طلب (سواء في orders أو colleague_orders) يمر عبر trigger يعيد حساب:
- meal_count — عدد الوجبات من menu_items (ليس من العميل)
- total_amount — المجموع الصحيح من menu_items
- expected_profit — الربح المتوقع (للطلبات الجملة فقط)
الهدف: منع أي تلاعب من الواجهة الأمامية.
📱 WhatsApp والمشاركة
القانون 13 — لا إرسال تلقائي
التطبيق يبني نص الرسالة ويفتح https://wa.me/رقم?text=... في نافذة جديدة. المستخدم هو من يضغط إرسال. لا توجد واجهة API لواتساب.
القانون 14 — رقم المطبخ ثابت
رقم واتساب المطبخ: 966550688470 — مخزون في HALA_FOOD_WHATSAPP كثابت في whatsapp.ts
القانون 15 — صور الحالة بالـ Canvas
لوحة اليوم (1080×1350 بكسل) تُبنى ديناميكياً بـ Canvas API:
- خلفية برتقالية بتدرّج
- شبكة 2×2 لصور الأطباق
- الاسم الفلبيني + السعر على كل صورة
- لوغو حلا فود + تاريخ اليوم + رابط الموزّعة (اختياري)
- تُحمَّل كـ Blob ثم تُشارك عبر Web Share API (أو تُنزَّل إن لم تكن مدعومة)
🎮 نظام الكوينز والألعاب
القانون 16 — مستويات العميلة (4 مستويات)
- مبتدئة 🌱 (0 — 9,999 كوين)
- منتظمة ⭐ (10,000 — 49,999 كوين)
- VIP 💎 (50,000 — 199,999 كوين)
- أسطورة 👑 (200,000+ كوين)
القانون 17 — أنواع الألعاب
wheel | scratch | tap | daily | memory | mission | slot | weekly | mystery | envelope
كلها تستدعي نفس دالة play_game(phone, gameType) في Supabase. القيود اليومية في الخادم.
💻 واجهة المستخدم
القانون 18 — الألوان الثابتة
- برتقالي غامق: #FF4500 — للهيدر والأزرار الرئيسية
- برتقالي فاتح: #FF8C00 — للتدرّج
- ذهبي: #F7B12B — للتمييز والتركيز
- أخضر: #148C3C — للنجاح وزر الطلب
- خلفية: #FFF0DC — كريمي دافئ
القانون 19 — الاتجاهات
- صفحات العملاء (ColleagueOrderPage): dir="ltr" (الإنجليزية أولاً)
- لوحة الموزّعة والمطبخ: dir="rtl" (العربية)
- خلط اتجاهات داخل نفس الصفحة عند الضرورة
القانون 20 — هيكل الصفحة الرئيسية
Header ثابت في الأعلى → قائمة المنيو (شبكة 2 عمود) → بانر التجنيد في الأسفل → سلة + زر الطلب تطفو فوق كل شيء (fixed bottom)
القانون 21 — DishCard
بطاقة صنف واحدة مع: صورة + اسم فلبيني + اسم عربي (اختياري) + سعر + زر إضافة (أو +/-). تعمل في وضعين: عادي (سعر العميل) أو جملة (سعر الموزّعة).
📲 PWA
القانون 22 — تثبيت التطبيق
يُستمع لـ beforeinstallprompt event. عند توفّره يُعرض زر تثبيت في صفحة النجاح. للـ iOS يُعرض تعليم نصي (مشاركة → أضف للشاشة الرئيسية).
القانون 23 — إعادة توجيه PWA
عند فتح التطبيق في وضع standalone (مثبّت على الهاتف) بدون query params:
- يقرأ localStorage["hf_me"] للحصول على كود الموزّعة المحفوظة
- إن وجد → window.location.replace("/?ref=كود") تلقائياً
💾 localStorage
القانون 24 — مفتاح hf_me
المفتاح الوحيد المستخدم: hf_me
يخزن: { name, phone, code, ts }
- name: اسم العميلة
- phone: رقم هاتفها
- code: كود آخر موزّعة زارت عبرها
- ts: timestamp للإنشاء
يُقرأ عند بدء تشغيل أي صفحة لتعبئة الحقول مسبقاً وتحقيق تجربة "العودة".
🔢 تطبيع الأرقام
**القانون 25
حلا فود — الكود الكامل لكل المكونات
(التطبيق الأساسي + تطبيق الألعاب منفصل)
═══════════════════════════════════════
التطبيق الأساسي — المكونات الكاملة
═══════════════════════════════════════
1. AdminPanel.tsx (5 تبويبات — بدون ألعاب)
// src/components/AdminPanel.tsx
import { useEffect, useState } from 'react';
import {
Check, RefreshCw, Save, ClipboardList, X, UserPlus, Copy,
Users, Gift, Home, ListTodo, ShoppingBag, Link as LinkIcon,
} from 'lucide-react';
import React from 'react';
import {
MenuItem, OrderRow, OrderStatus, AmbassadorRow,
DistributorSummary, KitchenStats,
verifyAdmin, getKitchenStats, loadAllMenu, setTodayMenu,
loadRecentOrders, loadAllCollections, setOrderStatus,
createAmbassador, listAmbassadors, redeemFreeMeals,
getAllColleagueOrders, AllColleagueOrder,
setAmbassadorActive,
} from '../lib/supabase';
const SITE = 'https://halafood.wardyat.net';
const GAMES_SITE = 'https://games.halafood.wardyat.net'; // تطبيق الألعاب المنفصل
// ─── شريط التنقل السفلي ────────────────────────────────
function AdminNavBar({ page, setPage, pendingCount }: {
page: string;
setPage: (p: 'kitchen'|'tasks'|'team'|'orders'|'links') => void;
pendingCount: number;
}) {
const items = [
{ key: 'kitchen', icon: '🍳', label: 'المطبخ' },
{ key: 'tasks', icon: '✅', label: 'المهام' },
{ key: 'team', icon: '👩', label: 'الفريق' },
{ key: 'orders', icon: '📦', label: 'الطلبات', badge: pendingCount },
{ key: 'links', icon: '🔗', label: 'الروابط' },
] as const;
return (
<nav className="fixed bottom-0 inset-x-0 z-50 bg-white/95 backdrop-blur-md
border-t border-gray-100 shadow-[0_-4px_24px_rgba(0,0,0,0.10)]"
style={{ paddingBottom: 'max(10px,env(safe-area-inset-bottom))' }} dir="rtl">
<div className="max-w-2xl mx-auto flex px-2 pt-2 pb-1 gap-0.5">
{items.map(item => (
<button key={item.key} onClick={() => setPage(item.key as any)}
className={`flex-1 flex flex-col items-center gap-1 rounded-xl py-2 px-1 transition-all relative
${page === item.key
? 'bg-gradient-to-b from-[#FF4500] to-[#FF6A00] text-white shadow-lg'
: 'text-gray-400'}`}>
<span className="relative text-lg leading-none">{item.icon}
{'badge' in item && item.badge > 0 && (
<span className="absolute -top-1 -right-2 min-w-[16px] h-4 bg-[#148C3C] text-white
text-[9px] rounded-full flex items-center justify-center px-1 font-bold">
{item.badge}
</span>
)}
</span>
<span className="text-[9px] font-bold leading-none">{item.label}</span>
</button>
))}
</div>
</nav>
);
}
// ─── شريط إحصاءات المطبخ ────────────────────────────────
function KitchenStatsBar({ stats }: { stats: KitchenStats }) {
return (
<div className="grid grid-cols-4 gap-2 mb-4">
{[
['🍽️', stats.today_meals, 'وجبة اليوم'],
['📦', stats.today_orders, 'طلب اليوم'],
['🌸', stats.active_ambassadors, 'موزّعة نشطة'],
['⏳', stats.pending_collections, 'مجموعة معلقة'],
].map(([icon, val, label]) => (
<div key={label as string}
className="bg-white rounded-2xl p-2.5 text-center shadow-sm">
<p className="text-base leading-none mb-0.5">{icon}</p>
<p className="font-extrabold text-[#FF4500] text-lg leading-none">{val}</p>
<p className="text-[9px] text-gray-400 mt-0.5">{label}</p>
</div>
))}
</div>
);
}
// ─── المكوّن الرئيسي ────────────────────────────────────
export function AdminPanel({ code }: { code: string }) {
const [ok, setOk] = useState<boolean | null>(null);
const [menu, setMenu] = useState<MenuItem[]>([]);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [orders, setOrders] = useState<OrderRow[]>([]);
const [allOrders, setAllOrders] = useState<AllColleagueOrder[]>([]);
const [collections, setCollections] = useState<DistributorSummary[]>([]);
const [stats, setStats] = useState<KitchenStats | null>(null);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [page, setPage] = useState<'kitchen'|'tasks'|'team'|'orders'|'links'>('kitchen');
const [ordersTab, setOrdersTab] = useState<'customers'|'wholesale'>('customers');
useEffect(() => {
verifyAdmin(code).then(async valid => {
setOk(valid);
if (!valid) return;
const [m, o, c, st, ao] = await Promise.all([
loadAllMenu(), loadRecentOrders(code), loadAllCollections(code),
getKitchenStats(code), getAllColleagueOrders(code),
]);
setMenu(m);
setSelected(new Set(m.filter(x => x.is_today).map(x => x.id)));
setOrders(o); setCollections(c); setStats(st); setAllOrders(ao);
});
}, [code]);
if (ok === null)
return <Spinner />;
// ── شاشة إدخال الكود إن كان خاطئاً ──
if (!ok)
return (
<div className="min-h-screen bg-[#FFF0DC] flex items-center justify-center p-6" dir="rtl">
<div className="bg-white rounded-3xl shadow-xl p-8 max-w-xs w-full text-center">
<p className="text-5xl mb-4">🔒</p>
<h2 className="font-extrabold text-xl text-gray-800 mb-1">الدخول مقيّد</h2>
<p className="text-sm text-gray-500 mb-4">رمز الدخول غير صحيح.</p>
<p className="text-xs text-gray-400 font-mono">استخدمي: /?admin=الرمز</p>
</div>
</div>
);
const meals = menu.filter(m => m.category === 'meal');
const desserts = menu.filter(m => m.category === 'dessert');
const selMeals = meals.filter(m => selected.has(m.id)).length;
const selDesserts = desserts.filter(m => selected.has(m.id)).length;
const pendingCount = orders.filter(o => o.is_wholesale && o.status === 'new').length;
function toggle(id: string) {
setSelected(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
setSaved(false);
}
async function save() {
setSaving(true);
const ok = await setTodayMenu(code, [...selected]);
setSaving(false);
if (ok) { setSaved(true); setMenu(m => m.map(x => ({ ...x, is_today: selected.has(x.id) }))); }
}
async function changeStatus(id: string, status: OrderStatus) {
const ok = await setOrderStatus(code, id, status);
if (ok) setOrders(os => os.map(o => o.id === id ? { ...o, status } : o));
}
return (
<div className="min-h-screen bg-[#FFF0DC] pb-20" dir="rtl">
{/* ── Header ── */}
<header className="bg-gradient-to-r from-[#FF4500] to-[#FF8C00] text-white text-center py-5 px-4">
<img src="/logo.jpg" alt="حلا فود"
className="w-11 h-11 rounded-full ring-2 ring-[#F7B12B] mx-auto mb-1" />
<h1 className="text-xl font-extrabold">لوحة المطبخ</h1>
<p className="text-[#FBD9A0] text-xs">حلا فود — إدارة المطبخ والموزّعات</p>
</header>
{/* ── تبويب المطبخ ── */}
{page === 'kitchen' && (
<main className="max-w-2xl mx-auto p-4">
{stats && <KitchenStatsBar stats={stats} />}
<div className="bg-white rounded-2xl shadow-sm p-4">
<h2 className="font-bold text-gray-800 mb-1">🔥 طبخة اليوم</h2>
<p className="text-xs text-gray-500 mb-3">
اختاري الأصناف التي ستطبخينها اليوم.
مختار: {selMeals} وجبة · {selDesserts} حلى
</p>
<MenuGroup title="🍽️ الوجبات" items={meals} selected={selected} onToggle={toggle} />
<MenuGroup title="🍰 الحلويات" items={desserts} selected={selected} onToggle={toggle} />
<button onClick={save} disabled={saving}
className={`w-full mt-3 font-bold py-4 rounded-2xl flex items-center justify-center gap-2
active:scale-[0.99] disabled:opacity-70
${saved ? 'bg-[#148C3C]' : 'bg-[#FF4500]'} text-white`}>
{saved
? <><Check size={20} /> تم الحفظ — المنيو محدَّث</>
: saving ? '⏳ جاري الحفظ...'
: <><Save size={20} /> حفظ طبخة اليوم</>}
</button>
</div>
</main>
)}
{/* ── تبويب المهام ── */}
{page === 'tasks' && (
<main className="max-w-2xl mx-auto p-4 space-y-3">
{[
{
emoji: '🍽️',
title: 'اختيار طبخة اليوم',
desc: saved ? `${selected.size} صنف محدد ✅` : 'حددي الأصناف من تبويب المطبخ',
done: saved,
},
{
emoji: '✅',
title: `تأكيد طلبات الجملة (${pendingCount})`,
desc: pendingCount === 0 ? 'كل الطلبات مؤكَّدة ✅' : `${pendingCount} طلب ينتظر تأكيدك`,
done: pendingCount === 0,
action: pendingCount > 0 ? (
<div className="mt-2 space-y-1.5">
{orders.filter(o => o.is_wholesale && o.status === 'new').slice(0,3).map(o => (
<div key={o.id} className="flex items-center justify-between bg-amber-50 rounded-xl px-3 py-2">
<span className="text-xs text-gray-700">{o.meal_count} وجبة · SR {o.total_amount}</span>
<button onClick={() => changeStatus(o.id, 'confirmed')}
className="text-xs bg-[#148C3C] text-white px-3 py-1 rounded-full active:scale-95">
✅ تأكيد
</button>
</div>
))}
</div>
) : null,
},
{
emoji: '📊',
title: 'تقرير اليوم',
desc: stats
? `${stats.today_meals} وجبة · ${stats.today_orders} طلب · ${stats.active_ambassadors} موزّعة`
: 'لا توجد بيانات بعد',
done: false,
action: (
<button onClick={() => {
const r = `📊 تقرير حلا فود\n🍽️ وجبات: ${stats?.today_meals}\n📦 طلبات: ${stats?.today_orders}\n👩 موزّعات: ${stats?.active_ambassadors}\n📅 أسبوع: ${stats?.week_meals}`;
navigator.clipboard.writeText(r);
}} className="mt-2 w-full bg-[#FF4500] text-white py-2.5 rounded-xl text-sm font-bold active:scale-[0.98]">
📋 نسخ تقرير اليوم
</button>
),
},
].map((t, i) => (
<div key={i} className={`bg-white rounded-2xl p-4 shadow-sm border-r-4
${t.done ? 'border-[#148C3C] opacity-80' : 'border-[#F7B12B]'}`}>
<div className="flex items-start gap-3">
<span className="text-xl">{t.emoji}</span>
<div className="flex-1">
<p className={`font-bold ${t.done ? 'text-gray-400 line-through' : 'text-gray-800'}`}>{t.title}</p>
<p className="text-xs text-gray-500 mt-0.5">{t.desc}</p>
{(t as any).action}
</div>
{t.done && <span className="text-[10px] bg-green-100 text-green-700 px-2 py-0.5 rounded-full shrink-0">✅ مكتمل</span>}
</div>
</div>
))}
</main>
)}
{/* ── تبويب الفريق ── */}
{page === 'team' && (
<main className="max-w-2xl mx-auto p-4 space-y-4">
<AmbassadorsManager adminCode={code} />
</main>
)}
{/* ── تبويب الطلبات ── */}
{page === 'orders' && (
<main className="max-w-2xl mx-auto p-4 space-y-3">
{/* سويتش العملاء / الجملة */}
<div className="bg-white rounded-2xl p-1 flex gap-1 shadow-sm">
<button onClick={() => setOrdersTab('customers')}
className={`flex-1 py-2.5 rounded-xl text-sm font-bold transition
${ordersTab === 'customers' ? 'bg-[#FF4500] text-white' : 'text-gray-400'}`}>
طلبات الزميلات ({allOrders.filter(o => o.status === 'open').length})
</button>
<button onClick={() => setOrdersTab('wholesale')}
className={`flex-1 py-2.5 rounded-xl text-sm font-bold transition
${ordersTab === 'wholesale' ? 'bg-[#148C3C] text-white' : 'text-gray-400'}`}>
جملة الموزّعات ({pendingCount})
</button>
</div>
{/* طلبات الزميلات */}
{ordersTab === 'customers' && (
<div className="space-y-2">
<div className="flex justify-between items-center">
<h3 className="font-bold text-gray-800">طلبات مفتوحة</h3>
<button onClick={async () => setAllOrders(await getAllColleagueOrders(code))}
className="text-xs text-[#FF4500] flex items-center gap-1">
<RefreshCw size={13} /> تحديث
</button>
</div>
{allOrders.length === 0
? <EmptyState text="لا توجد طلبات بعد" />
: allOrders.map(o => (
<div key={o.id} className="bg-white rounded-2xl p-3.5 shadow-sm">
<div className="flex justify-between items-start mb-1.5">
<div>
<p className="font-bold text-gray-800 text-sm">{o.colleague_name}</p>
{o.colleague_phone && (
<p className="text-xs text-gray-400" dir="ltr">{o.colleague_phone}</p>
)}
<p className="text-[10px] text-gray-400">{o.amb_name} • {fmtDate(o.created_at)}</p>
</div>
<span className="font-extrabold text-[#148C3C]">SR {o.total}</span>
</div>
<p className="text-xs text-gray-500">
{(o.items || []).map(i => `${i.name} ×${i.quantity}`).join(' · ')}
</p>
</div>
))
}
</div>
)}
{/* طلبات الجملة */}
{ordersTab === 'wholesale' && (
<div className="space-y-2">
<div className="flex justify-between items-center">
<h3 className="font-bold text-gray-800">طلبات الجملة</h3>
<button onClick={async () => setOrders(await loadRecentOrders(code))}
className="text-xs text-[#FF4500] flex items-center gap-1">
<RefreshCw size={13} /> تحديث
</button>
</div>
{orders.filter(o => o.is_wholesale).length === 0
? <EmptyState text="لا توجد طلبات جملة" />
: orders.filter(o => o.is_wholesale).map(o => (
<div key={o.id} className="bg-white rounded-2xl p-3.5 shadow-sm">
<div className="flex justify-between items-start mb-2">
<div>
<p className="font-bold text-gray-800 text-sm">{o.meal_count} وجبة</p>
<p className="text-[10px] text-gray-400">{fmtDate(o.created_at)}</p>
</div>
<div className="text-left">
<p className="font-extrabold text-[#148C3C]">SR {o.total_amount}</p>
<StatusBadge status={o.status} />
</div>
</div>
<p className="text-xs text-gray-500 mb-2">
{(o.items || []).map(i => `${i.name} ×${i.quantity}`).join(' · ')}
</p>
<div className="flex gap-2">
{o.status === 'new' && (
<button onClick={() => changeStatus(o.id, 'confirmed')}
className="flex-1 bg-[#148C3C] text-white text-xs font-bold py-2 rounded-xl active:scale-95">
✅ تأكيد
</button>
)}
{o.status === 'confirmed' && (
<button onClick={() => changeStatus(o.id, 'delivered')}
className="flex-1 bg-blue-500 text-white text-xs font-bold py-2 rounded-xl active:scale-95">
🚚 تم التسليم
</button>
)}
{o.status !== 'cancelled' && o.status !== 'delivered' && (
<button onClick={() => changeStatus(o.id, 'cancelled')}
className="flex-1 bg-gray-100 text-gray-600 text-xs font-bold py-2 rounded-xl active:scale-95">
❌ إلغاء
</button>
)}
</div>
</div>
))
}
</div>
)}
</main>
)}
{/* ── تبويب الروابط ── */}
{page === 'links' && (
<LinksPage adminCode={code} />
)}
<AdminNavBar page={page} setPage={setPage} pendingCount={pendingCount} />
</div>
);
}
// ─── صفحة الروابط ────────────────────────────────────
function LinksPage({ adminCode }: { adminCode: string }) {
const [list, setList] = useState<AmbassadorRow[]>([]);
const [copied, setCopied] = useState<string | null>(null);
useEffect(() => {
listAmbassadors(adminCode).then(setList);
}, [adminCode]);
function copy(text: string, key: string) {
navigator.clipboard.writeText(text);
setCopied(key);
setTimeout(() => setCopied(k => k === key ? null : k), 1500);
}
return (
<main className="max-w-2xl mx-auto p-4 space-y-3">
<h2 className="font-bold text-gray-800 flex items-center gap-1">
<LinkIcon size={18} /> روابط الموزّعات
</h2>
{list.map(a => (
<div key={a.code} className="bg-white rounded-2xl p-4 shadow-sm">
<div className="flex items-center justify-between mb-3">
<div>
<p className="font-bold text-gray-800">{a.display_name}</p>
<p className="text-xs text-gray-400 font-mono">{a.code}{!a.active && ' · موقوفة'}</p>
</div>
{a.hospital && <p className="text-xs text-gray-400 text-left">{a.hospital}</p>}
</div>
<div className="space-y-2">
{[
{ label: '🛒 رابط الطلبات', url: `${SITE}/?ref=${a.code}`, key: `ref-${a.code}` },
{ label: '📊 رابط اللوحة', url: `${SITE}/?dashboard=${a.code}`, key: `dash-${a.code}` },
].map(({ label, url, key }) => (
<div key={key} className="flex items-center gap-2 bg-[#FFF0DC] rounded-xl px-3 py-2">
<span className="text-xs text-gray-500 flex-1 truncate font-mono" dir="ltr">{url}</span>
<button onClick={() => copy(url, key)}
className="text-xs bg-[#FF4500] text-white px-2.5 py-1 rounded-lg shrink-0 active:scale-95">
{copied === key ? '✅' : <><Copy size={11} /> {label.split(' ')[0]}</>}
</button>
</div>
))}
{/* رابط إرسال عبر واتساب */}
<a href={`https://wa.me/${a.phone?.replace(/\D/g,'')}?text=${encodeURIComponent(
`مرحباً ${a.display_name} 🌸\n\nروابطك في حلا فود:\n\n🛒 رابط الطلبات:\n${SITE}/?ref=${a.code}\n\n📊 لوحتك:\n${SITE}/?dashboard=${a.code}\n\nSalamat! 🌟`
)}`}
target="_blank" rel="noreferrer"
className="block text-center text-xs bg-[#25D366] text-white py-2 rounded-xl font-bold active:scale-95">
📤 إرسال روابطها على واتساب
</a>
</div>
</div>
))}
</main>
);
}
// ─── إدارة الموزّعات (تبويب الفريق) ────────────────
function AmbassadorsManager({ adminCode }: { adminCode: string }) {
const [list, setList] = useState<AmbassadorRow[]>([]);
const [form, setForm] = useState({ code:'', name:'', phone:'', hospital:'' });
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState<string | null>(null);
useEffect(() => { listAmbassadors(adminCode).then(setList); }, [adminCode]);
const cleanCode = (v: string) => v.toLowerCase().replace(/[^a-z0-9_-]/g,'');
async function add() {
const c = cleanCode(form.code);
if (c.length < 2) { setMsg('الكود قصير (حروف إنجليزية مثل nora)'); return; }
setBusy(true); setMsg(null);
const res = await createAmbassador(adminCode, { ...form, code: c });
setBusy(false);
if (res.ok) {
setForm({ code:'', name:'', phone:'', hospital:'' });
setMsg(`✅ أُضيفت: ${res.code}`);
setList(await listAmbassadors(adminCode));
} else {
setMsg(res.error.includes('duplicate') ? 'الكود مستخدم — اختاري غيره' : 'تعذّرت الإضافة');
}
}
async function redeem(a: AmbassadorRow) {
if (a.free_meals_balance <= 0) return;
if (!confirm(`سلّمتِ وجبة مجانية لـ${a.display_name}؟`)) return;
const newBal = await redeemFreeMeals(adminCode, a.code, 1);
if (newBal !== null)
setList(l => l.map(x => x.code === a.code ? { ...x, free_meals_balance: newBal } : x));
}
async function toggleActive(a: AmbassadorRow) {
const ok = await setAmbassadorActive(adminCode, a.code, !a.active);
if (ok) setList(l => l.map(x => x.code === a.code ? { ...x, active: !a.active } : x));
}
return (
<div className="space-y-4">
{/* نموذج الإضافة */}
<div className="bg-white rounded-2xl shadow-sm p-4">
<h2 className="font-bold text-gray-800 flex items-center gap-1 mb-3">
<UserPlus size={18} /> إضافة موزّعة جديدة
</h2>
<div className="grid grid-cols-2 gap-2">
<input value={form.code} onChange={e => setForm({...form, code: cleanCode(e.target.value)})}
placeholder="الكود (nora)" dir="ltr"
className="col-span-2 px-3 py-2.5 rounded-xl border border-gray-200 text-sm" />
<input value={form.name} onChange={e => setForm({...form, name: e.target.value})}
placeholder="الاسم الكامل"
className="px-3 py-2.5 rounded-xl border border-gray-200 text-sm" />
<input value={form.phone} onChange={e => setForm({...form, phone: e.target.value})}
placeholder="الجوال" dir="ltr"
className="px-3 py-2.5 rounded-xl border border-gray-200 text-sm" />
<input value={form.hospital} onChange={e => setForm({...form, hospital: e.target.value})}
placeholder="المستشفى / المنطقة (اختياري)"
className="col-span-2 px-3 py-2.5 rounded-xl border border-gray-200 text-sm" />
<button onClick={add} disabled={busy}
className="col-span-2 bg-[#148C3C] text-white font-bold py-3 rounded-xl
flex items-center justify-center gap-1 active:scale-95 disabled:opacity-60">
<UserPlus size={17} /> {busy ? 'جاري الإضافة...' : 'إضافة موزّعة'}
</button>
</div>
{msg && <p className="text-sm mt-2 text-gray-700 text-center">{msg}</p>}
</div>
{/* قائمة الموزّعات */}
<div className="bg-white rounded-2xl shadow-sm p-4">
<h2 className="font-bold text-gray-800 flex items-center gap-1 mb-3">
<Users size={18} /> الموزّعات ({list.length})
</h2>
{list.length === 0 ? <EmptyState text="لا توجد موزّعات بعد" /> : (
<div className="space-y-3">
{list.map(a => (
<div key={a.code} className="border border-gray-100 rounded-2xl p-3">
<div className="flex items-center justify-between mb-2">
<div>
<p className="font-bold text-gray-800">
{a.display_name}
<span className="text-gray-400 font-normal text-xs mr-1">({a.code})</span>
</p>
{a.hospital && <p className="text-xs text-gray-400">{a.hospital}</p>}
{a.phone && <p className="text-xs text-gray-400" dir="ltr">{a.phone}</p>}
</div>
<button onClick={() => toggleActive(a)}
className={`text-xs px-2.5 py-1 rounded-full font-semibold active:scale-95
${a.active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'}`}>
{a.active ? '✅ نشطة' : '⏸ موقوفة'}
</button>
</div>
{/* وجبات مجانية */}
{a.free_meals_balance > 0 && (
<div className="flex items-center justify-between bg-pink-50 rounded-xl px-2.5 py-1.5 mb-2">
<span className="text-xs text-pink-700">
🎁 {a.free_meals_balance} وجبة مجانية مستحقّة
</span>
<button onClick={() => redeem(a)}
className="text-xs bg-pink-600 text-white px-2.5 py-1 rounded-full active:scale-95">
سلّمت وجبة −1
</button>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}
// ─── مكوّنات مساعدة ────────────────────────────────
function MenuGroup({ title, items, selected, onToggle }: {
title: string; items: MenuItem[]; selected: Set<string>; onToggle: (id: string) => void;
}) {
return (
<div className="mb-3">
<p className="text-sm font-semibold text-gray-600 mb-2">{title}</p>
<div className="grid grid-cols-2 gap-2">
{items.map(m => {
const on = selected.has(m.id);
return (
<button key={m.id} onClick={() => onToggle(m.id)}
className={`text-right rounded-xl border-2 p-2 flex items-center gap-2 transition
${on ? 'border-[#148C3C] bg-green-50' : 'border-gray-200 bg-white'}`}>
<span className={`w-5 h-5 rounded-md flex items-center justify-center shrink-0
${on ? 'bg-[#148C3C] text-white' : 'bg-gray-200'}`}>
{on && <Check size={14} />}
</span>
<span className="text-sm font-semibold text-gray-800 leading-tight">
{m.name_ar || m.name}
</span>
</button>
);
})}
</div>
</div>
);
}
function StatusBadge({ status }: { status: string }) {
const map: Record<string,[string,string]> = {
new: ['bg-yellow-100 text-yellow-700', 'جديد'],
confirmed: ['bg-blue-100 text-blue-700', 'مؤكَّد'],
delivered: ['bg-green-100 text-green-700', 'تم التسليم'],
cancelled: ['bg-red-100 text-red-700', 'ملغي'],
};
const [cls, label] = map[status] || ['bg-gray-100 text-gray-500', status];
return <span className={`text-[10px] font-bold px-2 py-0.5 rounded-full ${cls}`}>{label}</span>;
}
function EmptyState({ text }: { text: string }) {
return <p className="text-center text-gray-400 text-sm py-6">{text}</p>;
}
function Spinner() {
return (
<div className="min-h-screen bg-[#FF4500] flex items-center justify-center">
<div className="animate-spin rounded-full h-14 w-14 border-b-4 border-[#F7B12B]" />
</div>
);
}
function fmtDate(iso: string) {
return new Date(iso).toLocaleDateString('ar-SA', { month:'short', day:'numeric', hour:'2-digit', minute:'2-digit' });
}
2. AmbassadorDashboard.tsx (3 تبويبات — بدون ألعاب)
```typescript
// src/components/AmbassadorDashboard.tsx
import { useEffect, useState } from 'react';
import {
Wallet, Gift, Share2, Send, Plus, Minus, Trash2, Check,
MessageCircle, Bell, Copy, Home, Inbox, ClipboardList,
} from 'lucide-react';
import React from 'react';
import {
AmbassadorStats, MenuItem, ColleagueOrder, OrderedItem,
MIN_WHOLESALE_TOTAL,
loadAmbassador, loadAmbassadorStats, loadTodayMenu,
getCollection, getOrderedCollection, markColleagueDelivered,
placeColleagueOrder, setColleaguePaid, cancelColleagueOrder,
sendCollectionToKitchen, getRecentCustomers, RecentCustomer,
} from '../lib/supabase';
import { buildTodayStatusImage, buildStatusCaption, shareToStatus } from '../utils/statusImage';
import { openWhatsAppTo } from '../utils/whatsapp';
import { normPhone } from '../utils/phone';
const BASE_URL = 'https://halafood.wardyat.net';
const GAMES_URL = 'https://games.halafood.wardyat.net'; // رابط تطبيق الألعاب المنفصل
const TIPS = [
'نصيحة: شاركي المنيو في حالة الواتساب كل صباح 🌅',
'نصيحة: اسألي زميلاتك ماذا يردن غداً 📋',
'نصيحة: اجمعي طلبات بقيمة SR 150+ لإرسالها للمطبخ 🚀',
'نصيحة: شاركي الرابط في مجموعة الجناح 💬',
'نصيحة: ذكّري من لم يسددن بعد 💸',
'نصيحة: كلما جمعتِ أبكر كلما أكلتِ أسرع 💛',
'نصيحة: صورة الطبخة تشحّ الشهية — استعمليها 📸',
];
export function AmbassadorDashboard({ code }: { code: string }) {
const [stats, setStats] = useState
const [menu, setMenu] = useState
useEffect(() => {
if ('Notification' in window && Notification.permission === 'default')
Notification.requestPermission();
Promise.all([loadAmbassadorStats(code), loadTodayMenu()]).then(([s, m]) => {
setStats(s); setMenu(m); setLoading(false);
});
loadAmbassador(code).then(a => { if (a?.phone) setAmbPhone(normPhone(a.phone)); });
}, [code]);
async function shareStatus() {
setSharing(true);
try {
const blob = await buildTodayStatusImage(menu, code);
const res = await shareToStatus(blob, buildStatusCaption(menu, code));
if (res === 'downloaded')
alert('تم تحميل الصورة ونسخ النص ✅\nافتحي واتساب → الحالة → اختاري الصورة والصقي النص.');
} finally { setSharing(false); }
}
if (loading) return
if (!stats)
return (
الرابط غير صحيح أو الموزّعة غير نشطة.
);
const tip = TIPS[new Date().getDate() % TIPS.length];
return (
<header className="bg-gradient-to-r from-[#FF4500] to-[#FF8C00] text-white sticky top-0 z-40 shadow-md">
<div className="max-w-md mx-auto px-4 py-3 flex items-center gap-3">
<img src="/logo.jpg" alt="Hala Food" className="w-10 h-10 rounded-full ring-2 ring-[#F7B12B] shrink-0" />
<div className="flex-1 min-w-0">
<h1 className="text-base font-extrabold leading-tight truncate">{stats.display_name} 🌸</h1>
<p className="text-[#FBD9A0] text-[11px]">لوحة الموزّعة · حلا فود</p>
</div>
<a href={`/?ref=${code}`} target="_blank" rel="noreferrer"
className="shrink-0 bg-white/20 rounded-full px-2.5 py-1.5 text-[11px] font-bold">
🔗 رابطي
</a>
</div>
</header>
{/* ── الرئيسية ── */}
{page === 'home' && (
<div className="max-w-md mx-auto p-4 space-y-3">
{/* بطاقة الأرباح */}
<div className="bg-white rounded-2xl shadow-md p-5 text-center">
<p className="text-gray-400 text-xs flex items-center justify-center gap-1 mb-1">
<Wallet size={14} className="text-[#148C3C]" /> أرباحك المتراكمة
</p>
<p className="text-5xl font-extrabold text-[#148C3C]">
<span className="text-2xl">SR </span>{stats.expected_profit}
</p>
<div className="grid grid-cols-3 gap-2 mt-3">
{[
[stats.total_meals, 'وجبة', '#FF4500'],
[stats.total_orders, 'طلب', '#FF4500'],
[stats.free_meals_balance,'وجبة مجانية', 'pink-600'],
].map(([val, label, color]) => (
<div key={label as string} className="bg-[#FFF0DC] rounded-xl p-2 text-center">
<p className={`font-bold text-[${color}] text-base`}>{val}</p>
<p className="text-xs text-gray-400">{label}</p>
</div>
))}
</div>
{stats.free_meals_balance > 0 && (
<div className="mt-3 bg-pink-50 text-pink-700 rounded-xl py-2 px-3 text-sm font-semibold flex items-center justify-center gap-1">
<Gift size={15} /> {stats.free_meals_balance} وجبة مجانية مستحقة 🎁
</div>
)}
</div>
{/* بار التقدم نحو البونص */}
<div className="bg-white rounded-2xl p-4 shadow-sm">
<div className="flex justify-between text-xs text-gray-500 mb-1.5">
<span>وجبة {stats.total_meals % 10} / 10 للبونص التالي</span>
<span>🎁 كل 10 وجبات = وجبتان مجانيتان</span>
</div>
<div className="h-2.5 bg-gray-100 rounded-full overflow-hidden">
<div className="h-full bg-gradient-to-r from-[#F7B12B] to-[#FF4500] rounded-full transition-all"
style={{ width: `${(stats.total_meals % 10) * 10}%` }} />
</div>
<p className="text-[10px] text-gray-400 mt-1 text-center">
باقي {stats.meals_to_next_bonus} وجبة للبونص القادم
</p>
</div>
{/* أزرار المشاركة */}
<div className="space-y-2.5">
<button onClick={shareStatus} disabled={sharing || menu.length === 0}
className="w-full bg-[#F7B12B] text-[#CC2D00] font-extrabold py-4 rounded-2xl shadow
flex items-center justify-center gap-2 active:scale-[0.99] disabled:opacity-60">
<Share2 size={20} /> {sharing ? 'جاري التحضير...' : 'شاركي المنيو في الحالة'}
</button>
<button onClick={() => {
const caption = buildStatusCaption(menu, code);
window.open(`https://wa.me/?text=${encodeURIComponent(caption)}`, '_blank');
}} disabled={menu.length === 0}
className="w-full bg-[#25D366] text-white font-bold py-3.5 rounded-2xl shadow
flex items-center justify-center gap-2 active:scale-[0.99] disabled:opacity-60">
<MessageCircle size={20} /> شاركي كرسالة واتساب
</button>
</div>
{/* بطاقة الرابط */}
<ReferralCard code={code} name={stats.display_name} />
{/* رابط الألعاب */}
{ambPhone && (
<a href={`${GAMES_URL}/?phone=${ambPhone}`} target="_blank" rel="noreferrer"
className="block w-full bg-gradient-to-r from-purple-500 to-indigo-600 text-white
font-bold py-3 rounded-2xl text-center shadow active:scale-[0.99]">
🎮 ألعابي وكوينزي — افتحي عالم الألعاب
</a>
)}
{/* نصيحة اليوم */}
<d
iv className="bg-amber-50 border border-[#F7B12B]/40 rounded-2xl p-3 flex items-center gap-2">
💡
{tip}