🍽️ حلا فود — وثيقة البناء الكاملة

معلومات المشروع

الموقع halafood.wardyat.net
Stack React 18 + TypeScript + Vite + Tailwind CSS
Backend Supabase (PostgreSQL + RPC + RLS)
الألعاب games.halafood.wardyat.net (تطبيق منفصل)
WhatsApp 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) — قانون موحّد:
- 05XXXXXXXX9665XXXXXXXX
- 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 — التوجيه بالـ 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(null);
const [menu, setMenu] = useState([]);
const [loading, setLoading] = useState(true);
const [sharing, setSharing] = useState(false);
const [alert, setAlert] = useState(false);
const [page, setPage] = useState<'home'|'tasks'|'orders'>('home');
const [ambPhone, setAmbPhone] = 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}




)}

  {/* ── المهام ── */}
  {page === 'tasks' && (
    <TasksTab code={code} menu={menu} />
  )}

  {/* ── الطلبات ── */}
  {page === 'orders' && (
    <div className="max-w-md mx-auto p-4 space-y-4">
      {alert && (
        <div className="bg-[#148C3C] text-white rounded-2xl p-3 flex items-center gap-2 animate-bounce">
          <Bell size={18} /> طلب جديد وصل! 🌸
          <button onClick={() => setAlert(false)} className="mr-auto text-white/70">✕</button>
        </div>
      )}
      <CollectionPanel code={code} menu={menu} onNewOrder={() => { setAlert(true); playBeep(); }} />
      <DeliveredPanel  code={code} />
    </div>
  )}

  <AmbNavBar page={page} setPage={setPage} hasAlert={alert} />
</div>

);
}

// ─── شريط التنقل (3 تبويبات) ────────────────────
function AmbNavBar({ page, setPage, hasAlert }: {
page: string; setPage: (p: 'home'|'tasks'|'orders') => void; hasAlert: boolean;
}) {
const items = [
{ key: 'home', icon: '🏠', label: 'الرئيسية' },
{ key: 'tasks', icon: '✅', label: 'مهامي' },
{ key: 'orders', icon: '📦', label: 'الطلبات', badge: hasAlert },
] as const;
return (


);
}

// ─── بطاقة الرابط ────────────────────────────────
function ReferralCard({ code, name }: { code: string; name: string }) {
const url = ${BASE_URL}/?ref=${code};
const [copied, setCopied] = React.useState(false);

return (



رابط طلبات زميلاتك


{url}




https://wa.me/?text=${encodeURIComponent(اطلبي من ${name} 🌸\n${url})}} target="_blank" rel="noreferrer" className="flex-1 bg-[#25D366] text-white font-bold py-2 rounded-xl text-sm text-center active:scale-95">
📤 شاركي



);
}

// ─── تبويب المهام ────────────────────────────────
function TasksTab({ code, menu }: { code: string; menu: MenuItem[] }) {
const [done, setDone] = React.useState>(new Set());
const tasks = [
{ emoji: '📸', text: 'نشرتِ المنيو في حالة واتساب' },
{ emoji: '💬', text: 'أرسلتِ الرابط في مجموعة الجناح' },
{ emoji: '📦', text: 'جمعتِ SR 150 وأرسلتِ للمطبخ' },
{ emoji: '💰', text: 'جمعتِ الدفعات من الزميلات' },
{ emoji: '🚚', text: 'وزّعتِ الطلبات لصاحباتها' },
];
return (



مهامك اليومية



{done.size}/{tasks.length}


{tasks.map((t, i) => (

))}

);
}

// ─── لوحة التجميع ────────────────────────────────
function CollectionPanel({ code, menu, onNewOrder }: {
code: string; menu: MenuItem[]; onNewOrder?: () => void;
}) {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
const [showAdd, setShowAdd] = useState(false);
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [qtys, setQtys] = useState>({});
const [sending, setSending] = useState(false);
const [sent, setSent] = useState(false);
const prevCount = React.useRef(-1);

async function reload(notify = false) {
const fresh = await getCollection(code);
if (notify && prevCount.current >= 0 && fresh.length > prevCount.current) onNewOrder?.();
prevCount.current = fresh.length;
setOrders(fresh); setLoading(false);
}

useEffect(() => {
reload();
const t = setInterval(() => reload(true), 30_000);
return () => clearInterval(t);
}, [code]);

const cart = menu.filter(m => qtys[m.id] > 0).map(m => ({ ...m, quantity: qtys[m.id] }));
const retailTotal = orders.reduce((s,o) => s+o.total, 0);
const canSend = retailTotal >= MIN_WHOLESALE_TOTAL;

async function addManual() {
if (!name.trim() || cart.length === 0) return;
setSending(true);
const res = await placeColleagueOrder(code, name.trim(), phone.trim(), cart.map(c => ({id:c.id,quantity:c.quantity})));
setSending(false);
if (res.ok) { setName(''); setPhone(''); setQtys({}); setShowAdd(false); await reload(); }
}

async function sendToKitchen() {
setSent(false);
const res = await sendCollectionToKitchen(code);
if (!res.ok) { alert(res.error === 'min_not_reached' ? الحد الأدنى SR ${MIN_WHOLESALE_TOTAL} : res.error); return; }
const lines = (res.items || []).map(i => • ${i.name} ×${i.quantity}).join('\n');
const waMsg = *🛒 طلب جملة — حلا فود*\n━━━━━━━━━━\n${lines}\n━━━━━━━━━━\n*${res.meal_count} وجبة · SR ${res.wholesale_total}*\n${res.free_used > 0 ?🎁 ${res.free_used} وجبة مجانية = خصم SR ${res.discount}: ''}\nSalamat 🌟;
window.open(https://wa.me/966550688470?text=${encodeURIComponent(waMsg)}, '_blank');
setSent(true);
await reload();
}

if (loading) return

جاري التحميل...
;

return (




📦 التجميع
({orders.length} طلب)



  {/* نموذج الإضافة اليدوية */}
  {showAdd && (
    <div className="bg-[#FFF0DC] rounded-xl p-3 mb-3 space-y-2">
      <input value={name} onChange={e => setName(e.target.value)}
        placeholder="اسم الزميلة *"
        className="w-full px-3 py-2 rounded-xl border border-amber-200 text-sm bg-white" />
      <input value={phone} onChange={e => setPhone(e.target.value)}
        placeholder="رقم جوالها (اختياري)" dir="ltr" inputMode="tel"
        className="w-full px-3 py-2 rounded-xl border border-amber-200 text-sm bg-white" />
      <div className="grid grid-cols-2 gap-2">
        {menu.map(m => (
          <div key={m.id} className="flex items-center justify-between bg-white rounded-xl px-2 py-1.5 border border-gray-100">
            <span className="text-xs truncate text-gray-700">{m.name_ar || m.name}</span>
            <div className="flex items-center gap-1 shrink-0">
              <button onClick={() => setQtys(q => ({...q,[m.id]:Math.max(0,(q[m.id]||0)-1)}))}>
                <Minus size={14} className="text-gray-400" />
              </button>
              <span className="text-xs font-bold w-4 text-center">{qtys[m.id]||0}</span>
              <button onClick={() => setQtys(q => ({...q,[m.id]:(q[m.id]||0)+1}))}>
                <Plus size={14} className="text-[#FF4500]" />
              </button>
            </div>
          </div>
        ))}
      </div>
      <button onClick={addManual} disabled={sending || !name.trim() || cart.length === 0}
        className="w-full bg-[#148C3C] text-white font-bold py-2.5 rounded-xl text-sm active:scale-95 disabled:opacity-60">
        {sending ? '⏳ جاري الإضافة...' : '✅ إضافة الطلب'}
      </button>
    </div>
  )}

  {/* قائمة الطلبات */}
  {orders.length === 0
    ? <p className="text-center text-gray-400 text-sm py-4">لا توجد طلبات بعد 🌸</p>
    : (
      <div className="space-y-2 mb-3">
        {orders.map(o => (
          <div key={o.id} className="border border-gray-100 rounded-xl p-3">
            <div className="flex items-center justify-between mb-1">
              <div>
                <p className="font-semibold 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>
                )}
              </div>
              <span className="font-extrabold text-[#148C3C] text-sm">SR {o.total}</span>
            </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">
              <button onClick={async () => {
                  await setColleaguePaid(code, o.id, !o.paid);
                  await reload();
                }}
                className={`flex-1 text-xs font-bold py-1.5 rounded-lg active:scale-95
                  ${o.paid ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'}`}>
                {o.paid ? '✅ دفعت' : '⬜ لم تدفع'}
              </button>
              <button onClick={async () => { await cancelColleagueOrder(code, o.id); await reload(); }}
                className="text-xs font-bold py-1.5 px-3 bg-red-50 text-red-500 rounded-lg active:scale-95">
                <Trash2 size={13} />
              </button>
              {o.colleague_phone && (
                <button onClick={() => openWhatsAppTo(o.colleague_phone!, `مرحباً ${o.colleague_name}، طلبك جاهز! 🌸`)}
                  className="text-xs font-bold py-1.5 px-3 bg-[#25D366]/10 text-[#25D366] rounded-lg active:scale-95">
                  📱
                </button>
              )}
            </div>
          </div>
        ))}
      </div>
    )
  }

  {/* شريط الإجمالي + زر الإرسال */}
  <div className="border-t border-gray-100 pt-3">
    <div className="flex justify-between text-xs text-gray-500 mb-1.5">
      <span>الإجمالي</span>
      <span className="font-bold text-gray-800">SR {retailTotal}</span>
    </div>
    <div className="h-2.5 bg-gray-100 rounded-full overflow-hidden mb-3">
      <div className="h-full bg-gradient-to-r from-[#F7B12B] to-[#148C3C] rounded-full transition-all"
           style={{ width: `${Math.min(retailTotal / MIN_WHOLESALE_TOTAL * 100, 100)}%` }} />
    </div>
    <button onClick={sendToKitchen} disabled={!canSend}
      className={`w-full font-extrabold py-4 rounded-2xl flex items-center justify-center gap-2 transition
        ${canSend
          ? 'bg-gradient-to-r from-[#148C3C] to-[#1aaa4a] text-white shadow-lg active:scale-[0.99]'
          : 'bg-gray-200 text-gray-400 cursor-not-allowed'}`}>
      <Send size={20} />
      {canSend
        ? `🚀 إرسال المجموعة للمطبخ`
        : `تحتاجين SR ${MIN_WHOLESALE_TOTAL - retailTotal} إضافية`}
    </button>
    {sent && <p className="text-center text-xs text-[#148C3C] font-semibold mt-2">✅ تم فتح واتساب المطبخ!</p>}
  </div>
</div>

);
}

// ─── لوحة الانتظار (طلبات أُرسلت) ────────────────
function DeliveredPanel({ code }: { code: string }) {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
getOrderedCollection(code).then(o => { setOrders(o); setLoading(false); });
}, [code]);

if (loading || orders.length === 0) return null;

return (


⏳ قيد التوصيل ({orders.length})



{orders.map(o => (



{o.colleague_name}



{o.items.map(i => ${i.name} ×${i.quantity}).join(' · ')}



SR {o.total}



{o.colleague_phone && (

)}


))}


);
}

function playBeep() {
try {
const ctx = new AudioContext();
const osc = ctx.createOscillator(), g = ctx.createGain();
osc.connect(g); g.connect(ctx.destination);
osc.frequency.value = 880;
g.gain.setValueAtTime(0.4, ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.4);
osc.start(); osc.stop(ctx.currentTime + 0.4);
} catch {}
}

function Spinner() {
return (




);
}


---

## 3. CustomerPage.tsx (3 تبويبات — بدون ألعاب)

```typescript
// src/components/CustomerPage.tsx
import { useEffect, useState } from 'react';
import { getCustomerProfile, CustomerProfile } from '../lib/supabase';
import { getLevelInfo, fmtK } from '../utils/coins';

const GAMES_URL = 'https://games.halafood.wardyat.net';

export function CustomerPage({ phone }: { phone: string }) {
  const [profile, setProfile] = useState<CustomerProfile | null>(null);
  const [loading, setLoading] = useState(true);
  const [page,    setPage]    = useState<'orders'|'loyalty'|'level'>('orders');

  useEffect(() => {
    getCustomerProfile(phone).then(p => { setProfile(p); setLoading(false); });
  }, [phone]);

  if (loading)
    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>
    );

  if (!profile)
    return (
      <div className="min-h-screen bg-[#FFF0DC] flex items-center justify-center p-6 text-center" dir="rtl">
        <div>
          <p className="text-4xl mb-3">🌸</p>
          <p className="text-gray-600">لم نجد بيانات لهذا الرقم بعد.</p>
          <p className="text-sm text-gray-400 mt-1">اطلبي أول طلب عبر رابط موزّعتك!</p>
        </div>
      </div>
    );

  const level = getLevelInfo(profile.total_coins);

  return (
    <div className="min-h-screen bg-[#FFF0DC] pb-20" dir="rtl">

      <header className="bg-gradient-to-r from-[#FF4500] to-[#FF8C00] text-white py-5 px-4 text-center">
        <img src="/logo.jpg" alt="Hala Food" className="w-11 h-11 rounded-full ring-2 ring-[#F7B12B] mx-auto mb-2" />
        <h1 className="font-extrabold text-xl">لوحتي الشخصية</h1>
        <p className="text-[#FBD9A0] text-xs">{phone}</p>
      </header>

      {/* مستوى + كوينز */}
      <div className="max-w-md mx-auto px-4 pt-4">
        <div className="bg-white rounded-2xl shadow-md p-4 flex items-center gap-4">
          <div className="text-4xl">{level.icon}</div>
          <div className="flex-1">
            <p className="font-extrabold text-gray-800">{level.label}</p>
            <p className="text-sm text-gray-500">{fmtK(profile.total_coins)} كوين</p>
            {level.next && (
              <div className="mt-1.5">
                <div className="h-2 bg-gray-100 rounded-full overflow-hidden">
                  <div className="h-full rounded-full transition-all"
                       style={{ width:`${level.pct}%`, background:level.color }} />
                </div>
                <p className="text-[10px] text-gray-400 mt-0.5">
                  {fmtK(profile.total_coins)} / {fmtK(level.next)}
                </p>
              </div>
            )}
          </div>
        </div>
      </div>

      {/* شريط التنقل العلوي */}
      <div className="max-w-md mx-auto px-4 pt-3">
        <div className="bg-white rounded-2xl p-1 flex gap-1 shadow-sm">
          {([
            ['orders',  '📦 طلباتي'],
            ['loyalty', '⭐ نقاطي'],
            ['level',   '🏆 مستوياتي'],
          ] as const).map(([key, label]) => (
            <button key={key} onClick={() => setPage(key)}
              className={`flex-1 py-2.5 rounded-xl text-sm font-bold transition
                ${page === key ? 'bg-[#FF4500] text-white shadow-sm' : 'text-gray-400'}`}>
              {label}
            </button>
          ))}
        </div>
      </div>

      <main className="max-w-md mx-auto px-4 pt-3 space-y-3">

        {/* ── طلباتي ── */}
        {page === 'orders' && (
          profile.orders.length === 0
            ? <div className="text-center py-16 text-gray-400">
                <p className="text-4xl mb-3">🛒</p>
                <p>لا توجد طلبات بعد</p>
              </div>
            : profile.orders.map(o => (
              <div key={o.id} className="bg-white rounded-2xl p-4 shadow-sm">
                <div className="flex justify-between items-start mb-2">
                  <div>
                    <p className="font-bold text-gray-800">
                      {new Date(o.created_at).toLocaleDateString('ar-SA', { month:'short', day:'numeric' })}
                    </p>
                    <p className="text-xs text-gray-400">
                      عبر {o.amb_name}
                    </p>
                  </div>
                  <div className="text-left">
                    <p className="font-extrabold text-[#148C3C]">SR {o.total}</p>
                    <DeliveryBadge status={o.delivery_status} />
                  </div>
                </div>
                <p className="text-xs text-gray-500">
                  {o.items.map(i => `${i.name} ×${i.quantity}`).join(' · ')}
                </p>
              </div>
            ))
        )}

        {/* ── نقاطي ── */}
        {page === 'loyalty' && (
          <div className="space-y-3">
            <div className="bg-white rounded-2xl p-5 text-center shadow-md">
              <p className="text-5xl font-extrabold text-[#F7B12B]">{fmtK(profile.total_coins)}</p>
              <p className="text-gray-500 text-sm mt-1">إجمالي الكوينز</p>
            </div>
            {/* شريط التقدم نحو الوجبة المجانية */}
            <div className="bg-white rounded-2xl p-4 shadow-sm">
              <p className="font-bold text-gray-800 mb-3">🎁 نظام الولاء (وجبات مجانية)</p>
              {(() => {
                const spent = profile.orders.reduce((s, o) => s + o.total, 0);
                const prog  = spent % 70;
                const earned = Math.floor(spent / 70);
                return (
                  <>
                    <div className="flex justify-between text-xs text-gray-500 mb-1">
                      <span>أنفقتِ: SR {Math.round(spent)}</span>
                      <span>🎁 {earned} وجبة مجانية مكتسبة</span>
                    </div>
                    <div className="h-3 bg-gray-100 rounded-full overflow-hidden">
                      <div className="h-full bg-gradient-to-r from-[#F7B12B] to-[#FF4500] rounded-full"
                           style={{ width:`${(prog/70)*100}%` }} />
                    </div>
                    <p className="text-xs text-gray-400 text-center mt-1.5">
                      SR {Math.round(prog)} / SR 70 · بقي SR {Math.round(70-prog)} للوجبة القادمة
                    </p>
                  </>
                );
              })()}
            </div>
            {/* رابط الألعاب */}
            <a href={`${GAMES_URL}/?phone=${phone}`} target="_blank" rel="noreferrer"
              className="block w-full bg-gradient-to-r from-purple-500 to-indigo-600
                         text-white font-bold py-4 rounded-2xl text-center shadow active:scale-[0.99]">
              🎮 العبي واكسبي كوينز أكثر!
            </a>
          </div>
        )}

        {/* ── مستوياتي ── */}
        {page === 'level' && (
          <div className="space-y-2">
            {[
              { n:1, icon:'🌱', label:'مبتدئة',  coins:'0',        color:'#888888' },
              { n:2, icon:'⭐', label:'منتظمة',  coins:'10,000',   color:'#3498DB' },
              { n:3, icon:'💎', label:'VIP',      coins:'50,000',   color:'#9B59B6' },
              { n:4, icon:'👑', label:'أسطورة',  coins:'200,000',  color:'#F7B12B' },
            ].map(l => (
              <div key={l.n}
                className={`bg-white rounded-2xl p-4 shadow-sm flex items-center gap-4
                  ${profile.level === l.n ? 'ring-2 ring-[#F7B12B]' : ''}`}>
                <div className="text-3xl">{l.icon}</div>
                <div className="flex-1">
                  <p className="font-bold text-gray-800">
                    {l.label}
                    {profile.level === l.n && <span className="text-xs text-[#F7B12B] mr-2">← أنتِ هنا</span>}
                  </p>
                  <p className="text-xs text-gray-400">{l.coins}+ كوين</p>
                </div>
                <span className={`w-6 h-6 rounded-full ${profile.level >= l.n ? 'bg-[#148C3C]' : 'bg-gray-200'} flex items-center justify-center`}>
                  {profile.level >= l.n && <span className="text-white text-xs">✓</span>}
                </span>
              </div>
            ))}
          </div>
        )}

      </main>
    </div>
  );
}

function DeliveryBadge({ status }: { status: string }) {
  const map: Record<string,[string,string]> = {
    collecting: ['bg-yellow-100 text-yellow-700', '📦 قيد الجمع'],
    on_way:     ['bg-blue-100   text-blue-700',   '🚗 في الطريق'],
    delivered:  ['bg-green-100  text-green-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>;
}

═══════════════════════════════════════

تطبيق الألعاب — مستقل تماماً

═══════════════════════════════════════

هيكل مشروع الألعاب

halafood-games/
├── src/
│   ├── main.tsx
│   ├── App.tsx                 ← ?phone=رقم
│   ├── index.css
│   ├── lib/
│   │   └── supabase.ts         ← نفس الـ credentials + دوال الألعاب فقط
│   ├── utils/
│   │   ├── phone.ts            ← نسخة من utils/phone.ts
│   │   └── coins.ts            ← نسخة من utils/coins.ts
│   └── components/
│       ├── GamesHub.tsx        ← الصفحة الرئيسية
│       ├── WheelGame.tsx       ← عجلة الحظ
│       ├── ScratchCard.tsx     ← بطاقة الكشط
│       ├── DailyBonus.tsx      ← المكافأة اليومية
│       ├── SlotMachine.tsx     ← ماكينة الحظ
│       ├── MysteryBox.tsx      ← صندوق الغموض
│       ├── Leaderboard.tsx     ← لوحة الترتيب
│       └── PlayerProfile.tsx   ← ملف اللاعب
├── public/
│   └── logo.jpg
├── .env
├── package.json
├── vite.config.ts
└── tailwind.config.js

App.tsx (الألعاب)

// halafood-games/src/App.tsx
import { useState, useEffect } from 'react';
import { GamesHub } from './components/GamesHub';
import { normPhone } from './utils/phone';

export default function App() {
  const params = new URLSearchParams(window.location.search);
  const rawPhone = params.get('phone') || '';
  const phone = rawPhone ? normPhone(rawPhone) : '';

  // حفظ الرقم في localStorage
  useEffect(() => {
    if (phone) localStorage.setItem('hfg_phone', phone);
  }, [phone]);

  // استرداد إن لم يكن في الـ URL
  const [activePhone] = useState(() => {
    if (phone) return phone;
    return localStorage.getItem('hfg_phone') || '';
  });

  if (!activePhone)
    return (
      <div className="min-h-screen bg-[#1a1a2e] flex items-center justify-center p-6 text-center">
        <div>
          <p className="text-5xl mb-4">🎮</p>
          <h1 className="text-white font-extrabold text-2xl mb-2">عالم حلا فود</h1>
          <p className="text-gray-400 text-sm">افتحي هذه الصفحة من رابط لوحتك الشخصية</p>
          <a href="https://halafood.wardyat.net"
             className="mt-4 block bg-[#FF4500] text-white font-bold py-3 px-6 rounded-2xl">
            اطلبي من حلا فود →
          </a>
        </div>
      </div>
    );

  return <GamesHub phone={activePhone} />;
}

GamesHub.tsx

// halafood-games/src/components/GamesHub.tsx
import { useEffect, useState } from 'react';
import { getPlayerProfile, PlayerProfile } from '../lib/supabase';
import { getLevelInfo, fmtK } from '../utils/coins';
import { WheelGame }    from './WheelGame';
import { ScratchCard }  from './ScratchCard';
import { DailyBonus }   from './DailyBonus';
import { SlotMachine }  from './SlotMachine';
import { MysteryBox }   from './MysteryBox';
import { Leaderboard }  from './Leaderboard';

const ORDER_URL = 'https://halafood.wardyat.net';

export function GamesHub({ phone }: { phone: string }) {
  const [profile, setProfile]  = useState<PlayerProfile | null>(null);
  const [loading, setLoading]  = useState(true);
  const [active,  setActive]   = useState<string | null>(null);

  const reload = () => getPlayerProfile(phone).then(p => { setProfile(p); setLoading(false); });

  useEffect(() => { reload(); }, [phone]);

  const onWin = (coins: number) => {
    setProfile(p => p ? { ...p, total_coins: p.total_coins + coins } : p);
  };

  if (loading)
    return (
      <div className="min-h-screen flex items-center justify-center"
           style={{ background: 'linear-gradient(135deg,#1a1a2e,#16213e)' }}>
        <div className="animate-spin rounded-full h-14 w-14 border-b-4 border-[#F7B12B]" />
      </div>
    );

  const level = getLevelInfo(profile?.total_coins ?? 0);

  const games = [
    { id:'wheel',   icon:'🎡', label:'عجلة الحظ',       sub:'مرة/يوم',    color:'from-orange-500 to-red-500' },
    { id:'scratch',  icon:'🃏', label:'بطاقة الكشط',     sub:'مرة/يوم',    color:'from-purple-500 to-pink-500' },
    { id:'daily',    icon:'🌟', label:'مكافأة يومية',    sub:'20 كوين',    color:'from-yellow-500 to-orange-500' },
    { id:'slot',     icon:'🎰', label:'ماكينة الحظ',     sub:'3 مرات/يوم', color:'from-blue-500 to-cyan-500' },
    { id:'mystery',  icon:'🎁', label:'صندوق الغموض',   sub:'مرة/يوم',    color:'from-green-500 to-teal-500' },
  ];

  return (
    <div className="min-h-screen pb-6" style={{ background:'linear-gradient(135deg,#1a1a2e,#16213e)' }}>

      {/* Header */}
      <header className="px-4 pt-6 pb-4">
        <div className="flex items-center gap-3">
          <div className={`w-12 h-12 rounded-2xl flex items-center justify-center text-2xl`}
               style={{ background: level.color }}>
            {level.icon}
          </div>
          <div className="flex-1">
            <p className="text-white font-extrabold text-lg leading-none">{level.label}</p>
            <p className="text-gray-400 text-sm">{fmtK(profile?.total_coins ?? 0)} كوين</p>
          </div>
          <a href={ORDER_URL} target="_blank" rel="noreferrer"
            className="bg-[#FF4500] text-white text-xs font-bold px-3 py-2 rounded-xl">
            🍽️ اطلبي
          </a>
        </div>
        {/* Progress bar */}
        {level.next && (
          <div className="mt-3">
            <div className="h-2 bg-white/10 rounded-full overflow-hidden">
              <div className="h-full rounded-full transition-all"
                   style={{ width:`${level.pct}%`, background:level.color }} />
            </div>
            <p className="text-gray-500 text-[10px] mt-1 text-center">
              {fmtK(profile?.total_coins ?? 0)} / {fmtK(level.next)} للمستوى التالي
            </p>
          </div>
        )}
      </header>

      {/* Grid الألعاب */}
      <div className="px-4 grid grid-cols-2 gap-3 mb-4">
        {games.map(g => (
          <button key={g.id} onClick={() => setActive(g.id)}
            className={`rounded-2xl p-4 flex flex-col items-center gap-2 active:scale-95 transition
                        bg-gradient-to-br ${g.color}`}>
            <span className="text-4xl">{g.icon}</span>
            <p className="text-white font-extrabold text-sm">{g.label}</p>
            <p className="text-white/70 text-xs">{g.sub}</p>
          </button>
        ))}
        {/* لوحة الترتيب — عرض كامل */}
        <button onClick={() => setActive('leaderboard')}
          className="rounded-2xl p-4 flex flex-col items-center gap-2 active:scale-95
                     bg-gradient-to-br from-amber-500 to-yellow-500">
          <span className="text-4xl">🏆</span>
          <p className="text-white font-extrabold text-sm">لوحة الترتيب</p>
          <p className="text-white/70 text-xs">أفضل اللاعبين</p>
        </button>
      </div>

      {/* سجل آخر الكوينز */}
      {profile?.history && profile.history.length > 0 && (
        <div className="px-4">
          <p className="text-gray-400 text-xs mb-2">آخر الجوائز</p>
          <div className="space-y-1.5">
            {profile.history.slice(0,5).map((h, i) => (
              <div key={i} className="bg-white/5 rounded-xl px-3 py-2 flex items-center justify-between">
                <span className="text-gray-300 text-xs">{GAME_LABELS[h.game_type] || h.game_type}</span>
                <span className="text-[#F7B12B] font-bold text-sm">+{fmtK(h.coins_won)}</span>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Modals الألعاب */}
      {active === 'wheel'       && <WheelGame    phone={phone} onWin={onWin} onClose={() => { setActive(null); reload(); }} />}
      {active === 'scratch'     && <ScratchCard  phone={phone} onWin={onWin} onClose={() => { setActive(null); reload(); }} />}
      {active === 'daily'       && <DailyBonus   phone={phone} onWin={onWin} onClose={() => { setActive(null); reload(); }} />}
      {active === 'slot'        && <SlotMachine  phone={phone} onWin={onWin} onClose={() => { setActive(null); reload(); }} />}
      {active === 'mystery'     && <MysteryBox   phone={phone} onWin={onWin} onClose={() => { setActive(null); reload(); }} />}
      {active === 'leaderboard' && <Leaderboard               onClose={() => setActive(null)} />}
    </div>
  );
}

const GAME_LABELS: Record<string,string> = {
  wheel:'عجلة الحظ', scratch:'كشط', daily:'يومية',
  slot:'ماكينة', mystery:'صندوق', weekly:'أسبوعية',
};

WheelGame.tsx (مثال كامل)

```typescript
// halafood-games/src/components/WheelGame.tsx
import { useState, useRef, useEffect } from 'react';
import { playGame } from '../lib/supabase';

const SLICES = [
{ label: '10', coins: 10, color: '#FF4500' },
{ label: '25', coins: 25, color: '#F7B12B' },
{ label: '50', coins: 50, color: '#148C3C' },
{ label: '100', coins: 100, color: '#9B59B6' },
{ label: '5', coins: 5, color: '#3498DB' },
{ label: '75', coins: 75, color: '#E74C3C' },
{ label: '15', coins: 15, color: '#FF8C00' },
{ label: '30', coins: 30, color: '#1ABC9C' },
];

export function WheelGame({ phone, onWin, onClose }: {
phone: string; onWin: (coins: number) => void; onClose: () => void;
}) {
const canvasRef = useRef(null);
const [spinning, setSpinning] = useState(false);
const [result, setResult] = useState(null);
const [played, setPlayed] = useState(false);
const [angle, setAngle] = useState(0);

// رسم العجلة
useEffect(() => {
const canvas = canvasRef.current; if (!canvas) return;
const ctx = canvas.getContext('2d')!;
const cx = canvas.width / 2, cy = canvas.height / 2, r = cx - 10;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const arc = (2 * Math.PI) / SLICES.length;

SLICES.forEach((slice, i) => {
  const start = angle + i * arc - Math.PI / 2;
  ctx.beginPath();
  ctx.moveTo(cx, cy);
  ctx.arc(cx, cy, r, start, start + arc);
  ctx.fillStyle = slice.color;
  ctx.

fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();

  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(start + arc / 2);
  ctx.textAlign = 'right';
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 18px sans-serif';
  ctx.fillText(slice.label, r - 15, 6);
  ctx.restore();
});

// المركز
ctx.beginPath();
ctx.arc(cx, cy, 20, 0, 2 * Math.PI);
ctx.fillStyle = '#fff';
ctx.fill();

}, [angle]);

async function spin() {
if (spinning || played) return;
setSpinning(true);
const res = await playGame(phone, 'wheel');
if (!res.can_play) { setPlayed(true); setSpinning(false); return; }

const targetCoins = res.coins_won;
const sliceIdx    = SLICES.findIndex(s => s.coins === targetCoins) ?? 0;
const sliceAngle  = (2 * Math.PI) / SLICES.length;
const targetAngle = -(sliceIdx * sliceAngle) + (5 * 2 * Math.PI); // 5 دورات كاملة

const start    = Date.now();
const duration = 4000;
const startAngle = angle;

function animate() {
  const elapsed = Date.now() - start;
  const progress = Math.min(elapsed / duration, 1);
  const eased = 1 - Math.pow(1 - progress, 3); // ease out cubic
  setAngle(startAngle + targetAngle * eased);
  if (progress < 1) requestAnimationFrame(animate);
  else {
    setSpinning(false);
    setResult(targetCoins);
    setPlayed(true);
    onWin(targetCoins);
  }
}
requestAnimationFrame(animate);

}

return (



    <h2 className="text-white font-extrabold text-xl mb-4">🎡 عجلة الحظ</h2>

    {/* مؤشر العجلة */}
    <div className="relative inline-block mb-4">
      <div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-2 z-10
                      w-0 h-0 border-l-[10px] border-r-[10px] border-t-[20px]
                      border-l-transparent border-r-transparent border-t-[#F7B12B]" />
      <canvas ref={canvasRef} width={280} height={280} className="rounded-full" />
    </div>

    {result !== null && (
      <div className="mb-4 text-center">
        <p className="text-[#F7B12B] text-4xl font-extrabold">+{result}</p>
        <p className="text-gray-400 text-sm">كوين ربحتِ!</p>
      </div>
    )}

    {played && result === null && (
      <p className="text-gray-400 text-sm mb-4">لعبتِ اليوم بالفعل. عودي غداً! 🌙</p>
    )}

    <button onClick={spin} disabled={spinning || played}
      className="w-full py-4 rounded-2xl font-extrabold text-white text-lg active:scale-95
                 disabled:opacity-50 transition"
      style={{ background: spinning || played ? '#333' : 'linear-gradient(135deg,#FF4500,#F7B12B)' }}>
      {spinning ? '⏳ يدور...' : played ? '✅ تم اللعب اليوم' : '🎡 أدوّري العجلة!'}
    </button>

    {result && (
      <button onClick={onClose} className="mt-3 w-full text-gray-400 text-sm py-2">
        إغلاق
      </button>
    )}
  </div>
</div>

);
}


---

### lib/supabase.ts (الألعاب — مستقل)

```typescript
// halafood-games/src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js';

// نفس مشروع Supabase — credentials مشتركة
const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY
);

export interface GameResult {
  coins_won: number;
  can_play:  boolean;
  played_today?: boolean;
  error?: string;
}

export interface PlayerProfile {
  total_coins:  number;
  display_name: string | null;
  level:        number;
  history: Array<{ game_type: string; coins_won: number; played_at: string }>;
}

export type GameType =
  'wheel' | 'scratch' | 'daily' | 'slot' | 'mystery' | 'weekly' | 'tap';

export async function playGame(phone: string, gameType: GameType): Promise<GameResult> {
  const { data, error } = await supabase.rpc('play_game', {
    p_phone: phone, p_game_type: gameType,
  });
  if (error) return { coins_won: 0, can_play: false, error: error.message };
  return data as GameResult;
}

export async function getPlayerProfile(phone: string): Promise<PlayerProfile> {
  const { data, error } = await supabase.rpc('get_player_profile', { p_phone: phone });
  if (error || !data) return { total_coins: 0, display_name: null, level: 1, history: [] };
  return data as PlayerProfile;
}

export async function getLeaderboard(): Promise<Array<{ phone: string; display_name: string; total_coins: number }>> {
  const { data, error } = await supabase.rpc('get_leaderboard');
  if (error || !data) return [];
  return data;
}

═══════════════════════════════════════

statusImage.ts — الكود الكامل

═══════════════════════════════════════

// src/utils/statusImage.ts
import { MenuItem } from '../lib/supabase';

const BASE_URL = 'https://halafood.wardyat.net';
const W = 1080, H = 1350;

export async function buildTodayStatusImage(
  menu: MenuItem[],
  ambCode?: string
): Promise<Blob> {
  const canvas = document.createElement('canvas');
  canvas.width = W; canvas.height = H;
  const ctx = canvas.getContext('2d')!;

  // ── خلفية متدرجة ──
  const bg = ctx.createLinearGradient(0, 0, 0, H);
  bg.addColorStop(0, '#FF4500');
  bg.addColorStop(0.5, '#FF6A00');
  bg.addColorStop(1, '#FF8C00');
  ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);

  // ── نقش شبكي خفيف ──
  ctx.strokeStyle = 'rgba(255,255,255,0.05)';
  ctx.lineWidth = 1;
  for (let x = 0; x < W; x += 60) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
  for (let y = 0; y < H; y += 60) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }

  // ── الهيدر ──
  ctx.fillStyle = 'rgba(0,0,0,0.25)';
  ctx.fillRect(0, 0, W, 180);

  // لوغو
  try {
    const logo = await loadImage('/logo.jpg');
    ctx.save();
    ctx.beginPath(); ctx.arc(W/2, 90, 55, 0, Math.PI*2); ctx.clip();
    ctx.drawImage(logo, W/2-55, 35, 110, 110);
    ctx.restore();
    // حلقة ذهبية
    ctx.beginPath(); ctx.arc(W/2, 90, 57, 0, Math.PI*2);
    ctx.strokeStyle = '#F7B12B'; ctx.lineWidth = 4; ctx.stroke();
  } catch {}

  ctx.fillStyle = '#FFFFFF';
  ctx.font = 'bold 64px Arial';
  ctx.textAlign = 'center';
  ctx.fillText('Hala Food 🇵🇭', W/2, 210);

  ctx.fillStyle = '#FBD9A0';
  ctx.font = '36px Arial';
  ctx.fillText('Luto Ngayong Araw · طبخة اليوم', W/2, 260);

  // ── شبكة الأطباق 2×2 ──
  const items = menu.slice(0, 4);
  const cols = 2, rows = Math.ceil(items.length / cols);
  const cellW = 500, cellH = 450;
  const startX = (W - cols * cellW) / 2;
  const startY = 300;

  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    const col  = i % cols, row = Math.floor(i / cols);
    const x = startX + col * (cellW + 20);
    const y = startY + row * (cellH + 20);

    // بطاقة الصنف
    ctx.fillStyle = 'rgba(0,0,0,0.35)';
    roundRect(ctx, x, y, cellW, cellH, 30);
    ctx.fill();

    // صورة الصنف
    if (item.image_url) {
      try {
        const img = await loadImage(item.image_url);
        ctx.save();
        roundRect(ctx, x, y, cellW, cellH - 100, 30);
        ctx.clip();
        ctx.drawImage(img, x, y, cellW, cellH - 100);
        ctx.restore();
      } catch {
        // placeholder برتقالي
        ctx.fillStyle = '#FF6A00';
        roundRect(ctx, x, y, cellW, cellH - 100, 30);
        ctx.fill();
        ctx.fillStyle = 'rgba(255,255,255,0.5)';
        ctx.font = '80px Arial'; ctx.textAlign = 'center';
        ctx.fillText('🍽️', x + cellW/2, y + (cellH-100)/2 + 30);
      }
    }

    // اسم الصنف
    ctx.fillStyle = '#FFFFFF';
    ctx.font = 'bold 30px Arial'; ctx.textAlign = 'center';
    const name = item.name_ar || item.name;
    wrapText(ctx, name, x + cellW/2, y + cellH - 85, cellW - 20, 35);

    // السعر
    ctx.fillStyle = '#F7B12B';
    ctx.font = 'bold 42px Arial';
    ctx.fillText(`SR ${item.customer_price}`, x + cellW/2, y + cellH - 20);
  }

  // ── الفوتر ──
  const footerY = startY + rows * (cellH + 20) + 30;
  ctx.fillStyle = 'rgba(0,0,0,0.3)';
  ctx.fillRect(0, footerY, W, H - footerY);

  const today = new Date().toLocaleDateString('ar-SA', { weekday:'long', month:'long', day:'numeric' });
  ctx.fillStyle = '#FBD9A0'; ctx.font = '32px Arial'; ctx.textAlign = 'center';
  ctx.fillText(today, W/2, footerY + 55);

  if (ambCode) {
    ctx.fillStyle = '#FFFFFF'; ctx.font = 'bold 36px Arial';
    ctx.fillText(`🔗 اطلبي: ${BASE_URL}/?ref=${ambCode}`, W/2, footerY + 105);
  }

  ctx.fillStyle = '#F7B12B'; ctx.font = 'bold 44px Arial';
  ctx.fillText('halafood.wardyat.net', W/2, footerY + 165);

  return new Promise(resolve => canvas.toBlob(b => resolve(b!), 'image/jpeg', 0.93));
}

// ── مساعدات ──
function loadImage(src: string): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.onload  = () => resolve(img);
    img.onerror = reject;
    img.src = src;
  });
}

function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  ctx.lineTo(x + w, y + h - r);
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  ctx.lineTo(x + r, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  ctx.lineTo(x, y + r);
  ctx.quadraticCurveTo(x, y, x + r, y);
  ctx.closePath();
}

function wrapText(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, maxW: number, lineH: number) {
  const words = text.split(' ');
  let line = '';
  for (const word of words) {
    const test = line + word + ' ';
    if (ctx.measureText(test).width > maxW && line) {
      ctx.fillText(line.trim(), x, y);
      line = word + ' '; y += lineH;
    } else { line = test; }
  }
  if (line.trim()) ctx.fillText(line.trim(), x, y);
}

export function buildStatusCaption(menu: MenuItem[], ambCode?: string): string {
  const items = menu.map(m => `• ${m.name_ar || m.name} — SR ${m.customer_price}`).join('\n');
  const link  = ambCode ? `\n\n🔗 اطلبي الآن:\n${BASE_URL}/?ref=${ambCode}` : '';
  return `🍽️ طبخة اليوم — Hala Food 🇵🇭\n━━━━━━━━━━\n${items}\n━━━━━━━━━━\nLutong Bahay · طعام منزلي أصيل${link}`;
}

export function buildRecruitmentCaption(name?: string): string {
  const greeting = name ? `مرحباً ${name}! ` : '';
  return `${greeting}💰 انضمي لفريق حلا فود!\n\n✅ SR 3 ربح لكل وجبة\n✅ SR 30+ يومياً\n✅ وجبتان مجانيتان كل 10 طلبات\n\n🔗 للانضمام وتبدأ الآن:\nhalafood.wardyat.net\n\nSalamat! 🌸`;
}

export async function shareToStatus(blob: Blob, caption: string): Promise<'shared' | 'downloaded'> {
  try { await navigator.clipboard.writeText(caption); } catch {}

  if (navigator.share) {
    try {
      await navigator.share({
        files: [new File([blob], 'hala-food-menu.jpg', { type:'image/jpeg' })],
        text: caption,
      });
      return 'shared';
    } catch {}
  }

  // Fallback: تنزيل
  const url = URL.createObjectURL(blob);
  const a   = Object.assign(document.createElement('a'), { href:url, download:'hala-food-menu.jpg' });
  a.click();
  URL.revokeObjectURL(url);
  return 'downloaded';
}

═══════════════════════════════════════

دليل النشر (Deployment)

═══════════════════════════════════════

1. Supabase — مرة واحدة

# نفّذ كل الـ SQL في Supabase SQL Editor:
# 1. الجداول + Triggers + RLS
# 2. RPC functions (30+ دالة)
# 3. البيانات الأولية (المنيو الفلبيني)
# 4. جداول الألعاب (coin_ledger, game_players)

2. نشر التطبيق الأساسي

cd halafood-main
cp .env.example .env
# أضف: VITE_SUPABASE_URL + VITE_SUPABASE_ANON_KEY

npm install
npm run build
# dist/ → nginx على halafood.wardyat.net

3. نشر تطبيق الألعاب

cd halafood-games
cp .env.example .env
# نفس الـ credentials

npm install
npm run build
# dist/ → nginx على games.halafood.wardyat.net

4. nginx config

# التطبيق الأساسي
server {
    listen 80;
    server_name halafood.wardyat.net;
    root /var/www/halafood;
    index index.html;
    location / { try_files $uri $uri/ /index.html; }
}

# تطبيق الألعاب
server {
    listen 80;
    server_name games.halafood.wardyat.net;
    root /var/www/halafood-games;
    index index.html;
    location / { try_files $uri $uri/ /index.html; }
}

5. روابط الاختبار بعد النشر

halafood.wardyat.net                      ← الصفحة الرئيسية
halafood.wardyat.net/?ref=maria           ← طلب عبر موزّعة
halafood.wardyat.net/?dashboard=maria     ← لوحة الموزّعة (3 تبويبات)
halafood.wardyat.net/?admin=hala2026      ← لوحة المطبخ (5 تبويبات)
halafood.wardyat.net/?me=966512345678     ← صفحة العميلة (3 تبويبات)
games.halafood.wardyat.net/?phone=966...  ← عالم الألعاب

حلا فود — الهيكلة المعمارية الكاملة

فصل الأنظمة + الدخول الآمن + سيناريو كل عضو


═══════════════════════════════════════

القسم الأول: فصل النظامين

═══════════════════════════════════════

المشكلة في الكود القديم

نظام الألعاب والكوينز مدمج بشكل عشوائي في كل المكونات:
- GameWorldPanel يُستدعى من AmbassadorDashboard و CustomerPage
- coin_ledger و play_game ملتصقان بمنطق الطلبات
- التطوير صعب لأن تعديل الألعاب يمكن أن يكسر نظام الطلبات


الحل: تطبيقان منفصلان على نفس الـ Supabase

┌─────────────────────────────────────────────────────────┐
│                    نفس الـ Supabase Project              │
│                  (قاعدة بيانات مشتركة)                   │
├─────────────────────────┬───────────────────────────────┤
│   🍽️ التطبيق الأساسي     │      🎮 تطبيق الألعاب          │
│   halafood.wardyat.net  │  games.halafood.wardyat.net   │
│   (أو /app الأساسي)      │   (أو /games subdomain)       │
├─────────────────────────┼───────────────────────────────┤
│ • منيو اليوم            │ • عجلة الحظ                    │
│ • طلبات الزميلات        │ • بطاقة الكشط                  │
│ • لوحة الموزّعة         │ • الألعاب اليومية              │
│ • لوحة المطبخ           │ • لوحة الترتيب                 │
│ • صفحة العميلة          │ • رصيد الكوينز                 │
│ • PWA للطلبات           │ • عرض المستويات                │
└─────────────────────────┴───────────────────────────────┘

هيكل مشروع التطبيق الأساسي (بعد الفصل)

halafood-main/
├── src/
│   ├── App.tsx                    ← التوجيه الرئيسي
│   ├── lib/supabase.ts            ← واجهة البيانات (بدون coins/games)
│   ├── utils/
│   │   ├── phone.ts
│   │   ├── whatsapp.ts
│   │   └── statusImage.ts
│   └── components/
│       ├── DishCard.tsx
│       ├── ColleagueOrderPage.tsx
│       ├── AmbassadorDashboard.tsx  ← بدون تبويب الألعاب
│       ├── AdminPanel.tsx           ← بدون تبويب الألعاب
│       └── CustomerPage.tsx         ← بدون تبويب الألعاب

هيكل مشروع تطبيق الألعاب (مستقل)

halafood-games/
├── src/
│   ├── App.tsx                    ← التوجيه: ?phone=رقم
│   ├── lib/supabase.ts            ← نفس الـ credentials (قراءة coins فقط)
│   ├── utils/
│   │   ├── phone.ts               ← نسخة مشتركة
│   │   └── coins.ts               ← fmtK + getLevelInfo
│   └── components/
│       ├── WheelGame.tsx          ← عجلة الحظ
│       ├── ScratchCard.tsx        ← بطاقة الكشط
│       ├── DailyBonus.tsx         ← المكافأة اليومية
│       ├── Leaderboard.tsx        ← لوحة الترتيب
│       ├── PlayerProfile.tsx      ← ملف اللاعب + المستوى
│       └── GamesHub.tsx           ← الصفحة الرئيسية للألعاب

قاعدة بيانات الألعاب (جداول منفصلة — نفس الـ Supabase)

-- جدول الكوينز (مستقل عن orders)
CREATE TABLE coin_ledger (
  id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  phone      text NOT NULL,
  game_type  text NOT NULL,
  coins_won  integer NOT NULL DEFAULT 0,
  played_at  timestamptz DEFAULT now()
);
CREATE INDEX idx_coin_phone ON coin_ledger(phone);
CREATE INDEX idx_coin_daily ON coin_ledger(phone, game_type, (played_at::date));

-- ملفات اللاعبين (مستقل عن العملاء)
CREATE TABLE game_players (
  phone       text PRIMARY KEY,
  display_name text,
  total_coins integer DEFAULT 0,    -- يُحدَّث بـ trigger
  created_at  timestamptz DEFAULT now(),
  last_active timestamptz DEFAULT now()
);

-- Trigger: تحديث total_coins تلقائياً
CREATE OR REPLACE FUNCTION update_player_coins() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  INSERT INTO game_players (phone, total_coins)
  VALUES (NEW.phone, NEW.coins_won)
  ON CONFLICT (phone) DO UPDATE
    SET total_coins = game_players.total_coins + NEW.coins_won,
        last_active = now();
  RETURN NEW;
END; $$;
CREATE TRIGGER trg_update_coins AFTER INSERT ON coin_ledger
  FOR EACH ROW EXECUTE FUNCTION update_player_coins();

RPC لتطبيق الألعاب (منفصلة)

-- تشغيل لعبة
CREATE OR REPLACE FUNCTION play_game(p_phone text, p_game_type text)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
DECLARE v_won int; v_played boolean; v_daily_limit int;
BEGIN
  -- الحد اليومي حسب نوع اللعبة
  v_daily_limit := CASE p_game_type
    WHEN 'wheel'   THEN 1
    WHEN 'scratch' THEN 1
    WHEN 'daily'   THEN 1
    WHEN 'slot'    THEN 3
    WHEN 'weekly'  THEN 1  -- مرة بالأسبوع
    ELSE 5
  END;

  -- التحقق من اللعب السابق
  IF p_game_type = 'weekly' THEN
    SELECT EXISTS(SELECT 1 FROM coin_ledger
      WHERE phone=p_phone AND game_type=p_game_type
      AND played_at >= date_trunc('week', now()))
    INTO v_played;
  ELSE
    SELECT (count(*) >= v_daily_limit) FROM coin_ledger
    WHERE phone=p_phone AND game_type=p_game_type AND played_at::date=current_date
    INTO v_played;
  END IF;

  IF v_played THEN
    RETURN jsonb_build_object('coins_won',0,'can_play',false,'played_today',true);
  END IF;

  -- حساب الكوينز
  v_won := CASE p_game_type
    WHEN 'wheel'   THEN (ARRAY[10,25,50,100,5,75])[floor(random()*6+1)::int]
    WHEN 'scratch' THEN floor(random()*91+10)::int
    WHEN 'daily'   THEN 20
    WHEN 'weekly'  THEN floor(random()*201+100)::int
    WHEN 'slot'    THEN floor(random()*51+5)::int
    ELSE floor(random()*31+5)::int
  END;

  INSERT INTO coin_ledger (phone, game_type, coins_won) VALUES (p_phone, p_game_type, v_won);
  RETURN jsonb_build_object('coins_won',v_won,'can_play',true,'played_today',false);
END; $$;

-- لوحة الترتيب
CREATE OR REPLACE FUNCTION get_leaderboard()
RETURNS TABLE (phone text, display_name text, total_coins int)
LANGUAGE sql SECURITY DEFINER SET search_path = public AS $$
  SELECT phone,
    COALESCE(display_name, substring(phone, length(phone)-3)) AS display_name,
    total_coins
  FROM game_players
  ORDER BY total_coins DESC
  LIMIT 20;
$$;

-- ملف اللاعب
CREATE OR REPLACE FUNCTION get_player_profile(p_phone text)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
DECLARE v_player game_players; v_history jsonb;
BEGIN
  SELECT * INTO v_player FROM game_players WHERE phone=p_phone;
  IF NOT FOUND THEN
    RETURN jsonb_build_object('total_coins',0,'level',1,'history','[]'::jsonb);
  END IF;
  SELECT jsonb_agg(jsonb_build_object('game_type',game_type,'coins_won',coins_won,'played_at',played_at)
    ORDER BY played_at DESC) INTO v_history
  FROM coin_ledger WHERE phone=p_phone LIMIT 20;
  RETURN jsonb_build_object(
    'total_coins', v_player.total_coins,
    'display_name', v_player.display_name,
    'level', CASE
      WHEN v_player.total_coins >= 200000 THEN 4
      WHEN v_player.total_coins >= 50000  THEN 3
      WHEN v_player.total_coins >= 10000  THEN 2
      ELSE 1 END,
    'history', coalesce(v_history,'[]'::jsonb)
  );
END; $$;

ربط التطبيقين (نقاط الاتصال الوحيدة)

التطبيق الأساسي  ──────────►  تطبيق الألعاب
                    رابط من صفحة نجاح الطلب:
                    games.halafood.wardyat.net/?phone=966XXXXXXXXX

تطبيق الألعاب   ──────────►  التطبيق الأساسي
                    رابط "اطلبي الآن":
                    halafood.wardyat.net/?ref=كود_الموزّعة

لا يوجد استدعاء مباشر بين الكودين. الربط فقط عبر روابط.


═══════════════════════════════════════

القسم الثاني: طرق الدخول الآمنة

═══════════════════════════════════════

خريطة الدخول الكاملة

                        ┌─────────────────────────────────┐
                        │         halafood.wardyat.net     │
                        └─────────────┬───────────────────┘
                                      │
              ┌───────────────┬───────┴────────┬───────────────┐
              ▼               ▼                ▼               ▼
    ┌──────────────┐  ┌──────────────┐ ┌────────────┐ ┌─────────────┐
    │  ?admin=CODE │  │?dashboard=   │ │  ?ref=CODE │ │  ?me=PHONE  │
    │              │  │     CODE     │ │            │ │             │
    │  لوحة المطبخ │  │لوحة الموزّعة │ │طلب زميلة  │ │صفحة العميلة │
    │   (Admin)    │  │(Ambassador)  │ │(ColleagueO)│ │  (Customer) │
    └──────┬───────┘  └──────┬───────┘ └─────┬──────┘ └──────┬──────┘
           │                 │               │               │
           ▼                 ▼               ▼               ▼
    يتحقق من        يتحقق من        يتحقق من        يعرض بيانات
    admin_config    ambassadors     ambassadors     العميلة من
    في الخادم       (code+active)   (code+active)   رقم هاتفها

طريقة الدخول لكل لوحة

🔑 لوحة المطبخ — الأكثر حساسية

الرابط:   halafood.wardyat.net/?admin=hala2026
الكود:    hala2026  (مخزون في admin_config، لا يُقرأ بـ anon)
الحماية:  كل دالة تبدأ بـ  IF NOT admin_ok(p_code) THEN RAISE EXCEPTION 'unauthorized';

طريقة الحفاظ على الكود سري:
1. الكود لا يُعرض أبداً في الواجهة
2. كل الوصول عبر RPC functions محمية
3. إن حاول أحد قراءة admin_config مباشرة → RLS يمنعه
4. الكود يُرسَل للمطبخ مرة واحدة عبر واتساب خاص

طريقة أكثر أماناً (للتطوير المستقبلي):

// بدلاً من الكود في الـ URL — استخدم PIN محلي
// يُخزّن في localStorage بعد أول إدخال ناجح
const ADMIN_KEY = 'hf_admin_session';

async function adminLogin(inputCode: string): Promise<boolean> {
  const ok = await verifyAdmin(inputCode);
  if (ok) {
    // تخزين مشفّر في localStorage (ليس الكود الأصلي — فقط token مؤقت)
    const session = btoa(inputCode + ':' + Date.now());
    localStorage.setItem(ADMIN_KEY, session);
    return true;
  }
  return false;
}

الرابط الآمن للمطبخ (بدون كود في الـ URL):

halafood.wardyat.net/?admin=1   ← يعرض شاشة إدخال PIN

🎀 لوحة الموزّعة — متوسطة الحساسية

الرابط:   halafood.wardyat.net/?dashboard=maria
الكود:    maria  (اسم الموزّعة — ليس سراً)
الحماية:  get_ambassador_stats تُرجع فقط بيانات هذه الموزّعة

ليس الهدف إخفاء الرابط — الهدف منع التلاعب:
- لا يمكن تعديل طلبات موزّعة أخرى حتى لو عرفت كودها
- جميع الدوال تربط بين الكود والـ ambassador_id في الخادم
- لا يوجد بيانات مالية حساسة ظاهرة للـ anon

تحسين اختياري — إضافة PIN للوحة:

ALTER TABLE ambassadors ADD COLUMN IF NOT EXISTS pin text;

CREATE OR REPLACE FUNCTION verify_ambassador_pin(p_code text, p_pin text)
RETURNS boolean LANGUAGE sql SECURITY DEFINER SET search_path = public AS $$
  SELECT EXISTS (SELECT 1 FROM ambassadors WHERE code=p_code AND pin=p_pin AND active=true);
$$;

👤 صفحة العميلة — منخفضة الحساسية

الرابط:   halafood.wardyat.net/?me=966512345678
المفتاح:  رقم الهاتف (المُطبَّع)
الحماية:  get_customer_profile تُرجع فقط بيانات هذا الرقم

الرابط يُعطى للعميلة مباشرة — هي التي تحفظه في متصفحها.
لا توجد بيانات حساسة (بطاقة ائتمان، عنوان...) — فقط قائمة الطلبات ورصيد النقاط.


🛒 رابط الطلب — عام بالكامل

الرابط:   halafood.wardyat.net/?ref=maria
المفتاح:  كود الموزّعة (يُنشَر في واتساب)
الحماية:  لا يوجد بيانات سرية — يعرض فقط المنيو وصورة الموزّعة

جدول مقارنة مستويات الأمان

┌─────────────────┬──────────────┬────────────────────┬──────────────────────┐
│    اللوحة       │ مستوى الأمان │   طريقة التحقق     │  ماذا يرى الـ anon  │
├─────────────────┼──────────────┼────────────────────┼──────────────────────┤
│ لوحة المطبخ    │ 🔴 عالي جداً │ admin_ok(code)     │ خطأ 'unauthorized'  │
│ لوحة الموزّعة  │ 🟡 متوسط     │ code+active check  │ إحصاءاتها فقط       │
│ صفحة العميلة   │ 🟢 منخفض     │ رقم الهاتف فقط    │ طلباتها فقط          │
│ رابط الطلب     │ ⚪ عام       │ لا يوجد            │ المنيو فقط           │
└─────────────────┴──────────────┴────────────────────┴──────────────────────┘

═══════════════════════════════════════

القسم الثالث: سيناريو كل عضو

═══════════════════════════════════════

نظرة شاملة على الأعضاء

                    ┌──────────────┐
                    │  المطبخ 👩‍🍳   │  (شخص واحد — الطبّاخة وصاحبة العمل)
                    └──────┬───────┘
                           │ تضيف موزّعات وتختار المنيو
                           ▼
              ┌─────────────────────────┐
              │    الموزّعات 🌸 (N)      │  (زميلات في مستشفيات مختلفة)
              └──────┬──────────────────┘
                     │ تجمع طلبات
                     ▼
         ┌─────────────────────────────┐
         │    الزميلات / العملاء 👩    │  (زميلات في نفس المستشفى)
         └─────────────────────────────┘

سيناريو 1: المطبخ 👩‍🍳 (Kitchen / Admin)

الدور: صاحبة حلا فود. تطبخ وتدير كل شيء.

رابط الدخول: /?admin=hala2026

الصباح الباكر (7:00 صباحاً)

1. تفتح التطبيق على هاتفها
   URL: halafood.wardyat.net/?admin=hala2026

2. تبدأ بتبويب "المطبخ"
   ┌─────────────────────────────────────┐
   │  ما هي طبخة اليوم؟                  │
   │  ☐ Chopsuey with Rice               │
   │  ☑ Fried Chicken with Rice  ← تختار │
   │  ☐ Kaldereta with Rice               │
   │  ☑ Chicken Inasal with Rice ← تختار │
   │  ─────────────                       │
   │  ☑ Biko                    ← تختار  │
   │  ☑ Turon                   ← تختار  │
   │                                      │
   │  [ حفظ طبخة اليوم ✅ ]              │
   └─────────────────────────────────────┘
   → تستدعي: set_today_menu(code, [id1, id2, id3, id4])

3. تبويب "الفريق" → ترسل روابط الموزّعات على الواتساب
   → كل موزّعة تحصل على رابطين:
     • رابط الطلب: /?ref=maria
     • رابط اللوحة: /?dashboard=maria

في منتصف النهار (12:00 ظهراً)

4. تبويب "الطلبات" تابعة
   ┌─────────────────────────────────────────────────────┐
   │  [ 42 وجبة ] [ 8 طلبات ] [ 5 موزّعات ] [ 3 معلقة ] │
   ├─────────────────────────────────────────────────────┤
   │  طلبات الجملة (من الموزّعات)                         │
   │  ────────────────────────────                        │
   │  Maria — 18 وجبة — SR 216                           │
   │  [✅ تأكيد] [❌ إلغاء]                               │
   │                                                      │
   │  Sara  — 12 وجبة — SR 144                           │
   │  [✅ تأكيد] [❌ إلغاء]                               │
   └─────────────────────────────────────────────────────┘
   → تستدعي: set_order_status(adminCode, orderId, 'confirmed')

5. تبويب "الفريق" — منح وجبات مجانية
   ┌──────────────────────────────────────────┐
   │  Maria — 3 وجبة مجانية مستحقة            │
   │  [استبدال ✓]  → يطرح من free_meals_balance│
   └──────────────────────────────────────────┘
   → تستدعي: redeem_free_meals(adminCode, 'maria', 3)

المساء (بعد التوصيل)

6. تبويب "الطلبات" → تغيير الحالة إلى "تم التسليم"
   → set_order_status(adminCode, orderId, 'delivered')

   ✅ عند هذه اللحظة تُحتسب أرباح الموزّعة وبونصها

ما لا تراه المطبخ:
- لا تستطيع رؤية رصيد الكوينز للعملاء (نظام منفصل)
- لا تستطيع رؤية محادثات الواتساب الخاصة


سيناريو 2: الموزّعة 🌸 (Ambassador / Distributor)

الدور: ممرضة في مستشفى. تجمع طلبات زميلاتها يومياً وتوصّلها.

رابط الدخول: /?dashboard=maria

الصباح (7:30 صباحاً)

1. تستلم رسالة من المطبخ: "الطبخة جاهزة!"
   تفتح لوحتها: halafood.wardyat.net/?dashboard=maria

2. تبويب الرئيسية
   ┌──────────────────────────────────────┐
   │  Maria 🌸           SR 156 أرباحك    │
   │  ────────────────────────────────── │
   │  47 وجبة │ 12 طلب │ 1 وجبة مجانية  │
   │  ────────────────────────────────── │
   │  [📤 شاركي المنيو في الحالة]        │
   │  [💬 أرسلي كرسالة واتساب]           │
   │  ────────────────────────────────── │
   │  🔗 رابطك: halafood.../?ref=maria   │
   │  [نسخ الرابط]                        │
   └──────────────────────────────────────┘

3. تضغط "شاركي المنيو في الحالة"
   → يبني صورة Canvas 1080×1350 للطبخة
   → يفتح Web Share API أو يُنزّل الصورة
   → تنشر في حالة الواتساب: "طبخة اليوم 🍽️"

طوال اليوم (تستقبل طلبات)

4. كل زميلة تفتح رابطها وتطلب
   → يُسجَّل الطلب في colleague_orders
   → تصل إشعار واتساب للموزّعة (من صفحة نجاح الزميلة)

5. الموزّعة تفتح تبويب "الطلبات" → قسم التجميع
   ┌────────────────────────────────────────────────┐
   │  📦 التجميع (6 طلبات — SR 180 تجزئة)          │
   │  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━          │
   │  نورا العتيبي              SR 30               │
   │  • Fried Chicken ×2                            │
   │  [✅ دفعت] [🗑️ حذف]                           │
   │                                                │
   │  سارة محمد                 SR 25               │
   │  • Chicken Inasal + Biko                       │
   │  [✅ دفعت] [🗑️ حذف]                           │
   │                                                │
   │  + إضافة طلب يدوي                              │
   │  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━          │
   │  ████████████████░░░ SR 180 / 150              │
   │                                                │
   │  ✅ [إرسال المجموعة للمطبخ]  (مفعّل!)         │
   └────────────────────────────────────────────────┘
   → send_collection_to_kitchen('maria')
   → يُنشئ طلب جملة واحد في orders (is_wholesale=true)
   → تفتح واتساب المطبخ مع ملخص الطلب

عند التوصيل (المساء)

6. تستلم الطلبات من المطبخ

7. تبويب "الطلبات" → قسم الانتظار
   ┌───────────────────────────────────────────┐
   │  ⏳ الانتظار (تم الإرسال للمطبخ)          │
   │  ─────────────────────────────────────── │
   │  نورا العتيبي              SR 30          │
   │  [📱 واتساب نورا] [✅ تم التسليم]        │
   │                                           │
   │  سارة محمد                 SR 25          │
   │  [📱 واتساب سارة] [✅ تم التسليم]        │
   └───────────────────────────────────────────┘
   → markColleagueDelivered('maria', orderId)

8. تبويب الرئيسية → ترى أرباحها المحدّثة بعد تأكيد المطبخ
   (كل 10 وجبات = وجبتان مجانيتان تُضاف لرصيدها)

ما لا تراه الموزّعة:
- لا ترى طلبات موزّعات أخريات
- لا ترى هاتف الزميلات في قاعدة البيانات (تحفظه من الطلب فقط)
- لا ترى كود الـ admin


سيناريو 3: الزميلة / العميلة الجديدة 👩 (New Customer)

الدور: تسمع عن حلا فود من زميلتها الموزّعة.

رابط الدخول: /?ref=maria (أرسلته لها الموزّعة)

أول طلب في حياتها

1. تفتح الرابط: halafood.wardyat.net/?ref=maria

2. تشاهد الصفحة
   ┌──────────────────────────────────────┐
   │  🍽️ Order from Maria                 │
   │  Today's menu · Hala Food 🇵🇭        │
   │  ────────────────────────────────── │
   │  Your Details                        │
   │  ┌──────────────────────────────┐   │
   │  │ Your Name *        [اسمك    ]│   │
   │  │ Mobile Number (Optional) 🎁  │   │
   │  │ [05XXXXXXXX               ]  │   │
   │  └──────────────────────────────┘   │
   │  ────────────────────────────────── │
   │  🍽️ Meals                           │
   │  ┌──────┐ ┌──────┐                  │
   │  │ 🍗   │ │ 🍲   │                  │
   │  │Fried │ │Kald. │                  │
   │  │SR 15 │ │SR 15 │                  │
   │  │ [+]  │ │ [+]  │                  │
   │  └──────┘ └──────┘                  │
   └──────────────────────────────────────┘

3. تختار: Fried Chicken ×1 + Biko ×1
   الإجمالي: SR 25

4. تضغط "Place Order" (SR 25)
   → يستدعي place_colleague_order('maria', 'نورا', '0512345678', items)
   → يفتح واتساب ماريا مع رسالة الطلب
   → تحفظ اسمها ورقمها في localStorage

صفحة نجاح الطلب (أهم لحظة في تجربتها)

5. SuccessScreen تعرض:
   ┌──────────────────────────────────────────┐
   │  ✅  Order Sent! 🎉                      │
   │  SR 25 — Amount due upon delivery        │
   │  ──────────────────────────────────────  │
   │  🌸 نظام الولاء                          │
   │  أنفقتِ حتى الآن: SR 25                 │
   │  ⭐ بقي SR 45 للوجبة المجانية           │
   │  [████░░░░░░░░░] 25/70                   │
   │  ──────────────────────────────────────  │
   │  📊 رابط لوحتك الشخصية                  │
   │  halafood.../?me=966512345678            │
   │  [فتح]                                   │
   │  ──────────────────────────────────────  │
   │  📲 ثبّتي تطبيق حلا فود على هاتفك      │
   │  [⬇️ تثبيت]                              │
   │  ──────────────────────────────────────  │
   │  💰 كوني موزّعة حلا فود!                │
   │  SR 3 ربح/وجبة │ SR 30+ يومياً           │
   │  [اسمك][جوالك][مستشفاك]                 │
   │  [🚀 سجّلي الآن]                        │
   └──────────────────────────────────────────┘

سيناريو 4: العميلة العائدة 🔄 (Returning Customer)

الدور: نورا طلبت أكثر من مرة. تريد تتبع طلباتها.

1. تفتح رابطها الشخصي: halafood.wardyat.net/?me=966512345678

2. أو تفتح رابط ماريا مباشرة ← التطبيق يتعرف عليها من localStorage
   ┌──────────────────────────────────────┐
   │  👋 أهلاً نورا — تم التعرف عليكِ ✓  │
   │  [تغيير]                              │
   └──────────────────────────────────────┘

3. صفحتها الشخصية /?me=966512345678
   ┌──────────────────────────────────────────┐
   │  تبويبات: [طلباتي] [نقاطي] [ألعابي]    │
   │  ──────────────────────────────────────  │
   │  تبويب طلباتي:                           │
   │  ─────────────                           │
   │  📦 اليوم — SR 25                        │
   │  Fried Chicken + Biko                    │
   │  حالة: في الطريق 🚗                     │
   │                                          │
   │  📦 أمس — SR 30                          │
   │  Kaldereta ×2                            │
   │  حالة: تم التسليم ✅                    │
   │  ──────────────────────────────────────  │
   │  تبويب نقاطي:                            │
   │  إجمالي الإنفاق: SR 145                  │
   │  [████████████████████░░░░░░] 145/210    │
   │  ⭐ بقي SR 65 للوجبتين المجانيتين       │
   └──────────────────────────────────────────┘

سيناريو 5: الموزّعة الجديدة (Self-Registration) 🌱

الدور: فاطمة عميلة عادية، قررت تصبح موزّعة بعد تجربة الطلب.

1. بعد طلبها الأول — في SuccessScreen ترى:
   ┌────────────────────────────────────┐
   │  💰 كوني موزّعة حلا فود!           │
   │  SR 3/وجبة │ SR 30+ يومياً          │
   │  ─────────────────────────────    │
   │  ✓ فاطمة العمري ← (من طلبها)     │
   │  ✓ 0556789012   ← (من طلبها)     │
   │  [المستشفى: ________________]      │
   │                                    │
   │  [🚀 سجّلي فاطمة كموزّعة]         │
   └────────────────────────────────────┘

2. تضغط التسجيل
   → self_register_ambassador('فاطمة العمري', '0556789012', 'مستشفى الملك فهد')
   → يُنشئ كود: fatimah (من اسمها تلقائياً)
   → يُرجع: { ok: true, code: 'fatimah', isNew: true }

3. تظهر روابطها فوراً:
   ┌────────────────────────────────────────────────────┐
   │  🎉 تم التسجيل! مرحباً بكِ في حلا فود 🌸          │
   │  ─────────────────────────────────────────────     │
   │  🔗 رابط طلبات زميلاتك:                            │
   │  halafood.wardyat.net/?ref=fatimah                  │
   │  [نسخ] [📤 أرسلي على واتساب]                      │
   │                                                    │
   │  [افتحي لوحتك الآن →]                             │
   │  halafood.wardyat.net/?dashboard=fatimah           │
   └────────────────────────────────────────────────────┘

4. في نفس الوقت — يظهر في لوحة المطبخ:
   "موزّعة جديدة: فاطمة العمري (fatimah) — مستشفى الملك فهد"

ملخص رحلة كل عضو (الخريطة الكاملة)

                                     حلا فود — خريطة الأعضاء
─────────────────────────────────────────────────────────────────────────────────

المطبخ 👩‍🍳
  الصباح:  /?admin=hala2026  →  تختار طبخة اليوم  →  تُرسل للموزّعات
  النهار:  /?admin=hala2026  →  تؤكّد طلبات الجملة  →  تطبخ
  المساء:  /?admin=hala2026  →  تُسلّم + تضع "تم التسليم"

         ↓ تضيف موزّعات / تمنح وجبات مجانية

الموزّعة 🌸 (maria, fatimah, sara...)
  الصباح:  /?dashboard=كودها  →  تشارك المنيو في حالة واتساب
  النهار:  تستقبل طلبات زميلاتها على /?ref=كودها
  الظهر:   /?dashboard=كودها  →  تجمع SR 150+  →  ترسل للمطبخ
  المساء:  /?dashboard=كودها  →  تُسلّم وتضع "تم التسليم"

         ↓ ترسل الرابط لزميلاتها

الزميلة / العميلة 👩
  أول مرة:   /?ref=كود_الموزّعة  →  تطلب  →  تسجّل نفسها اختيارياً
  عائدة:     /?ref=كود_الموزّعة  →  يتعرف عليها التطبيق من localStorage
  لوحتها:    /?me=رقمها  →  تتابع طلباتها + رصيد الولاء
  ألعاب:     games.halafood.wardyat.net/?phone=رقمها  ← (النظام المنفصل)

         ↓ إن أرادت تصبح موزّعة

الموزّعة الجديدة 🌱 (تسجيل ذاتي)
  في صفحة النجاح  →  تملأ الاستمارة  →  تحصل على رابطيها فوراً
  ثم: /?dashboard=كودها_الجديد
─────────────────────────────────────────────────────────────────────────────────

═══════════════════════════════════════

القسم الرابع: تطبيق الألعاب — سيناريو منفصل

═══════════════════════════════════════

الدخول لتطبيق الألعاب

الرابط: games.halafood.wardyat.net/?phone=966512345678

لا يوجد تسجيل دخول — رقم الهاتف هو المعرّف الوحيد

صفحة الألعاب

/?phone=966512345678

┌────────────────────────────────────────────┐
│  🎮 عالم حلا فود                           │
│  نورا ⭐ منتظمة — 12,450 كوين             │
│  [████████████░░░░░░░] 12,450 / 50,000     │
│  ──────────────────────────────────────    │
│  الألعاب اليومية                           │
│  ┌────────┐ ┌────────┐ ┌────────┐         │
│  │ 🎡     │ │ 🃏     │ │ 📅     │         │
│  │ عجلة  │ │ كشط    │ │ يومية  │         │
│  │[العب] │ │ [✓تم]  │ │[العب] │         │
│  └────────┘ └────────┘ └────────┘         │
│  ──────────────────────────────────────    │
│  🏆 لوحة الترتيب                          │
│  1. ريم     👑 أسطورة   245,800 كوين      │
│  2. فاطمة   💎 VIP      89,200 كوين       │
│  3. نورا    ⭐ منتظمة   12,450 كوين       │
│  ──────────────────────────────────────    │
│  [🍽️ اطلبي الآن] ← رابط للتطبيق الأساسي │
└────────────────────────────────────────────┘

كيف يصل المستخدم لتطبيق الألعاب؟

التطبيق الأساسي يضع روابط بسيطة:

1. في صفحة العميلة (?me=رقم):
   زر "ألعابي 🎮" → يفتح: games.halafood.wardyat.net/?phone=رقمها

2. في لوحة الموزّعة:
   زر "ألعابي 🎮" → يفتح: games.halafood.wardyat.net/?phone=رقمها

3. في صفحة نجاح الطلب:
   "🎮 العبي واكسبي كوينز!" → games.halafood.wardyat.net/?phone=رقمها

═══════════════════════════════════════

القسم الخامس: ملخص قرارات الهندسة

═══════════════════════════════════════

القرار السبب
فصل الألعاب في تطبيق مستقل التطوير مستقل، لا يأثر على نظام الطلبات
نفس قاعدة البيانات (Supabase) لا حاجة لـ API بين التطبيقين، التكلفة أقل
الربط عبر روابط فقط بسيط، لا coupling بين الكودين
الأمان بـ SECURITY DEFINER كل العمليات الحساسة تجري على الخادم
لا JWT/sessions بساطة كاملة — التطبيق للهواتف بالمستشفيات، لا يحتاج تعقيد
كود المطبخ في URL (مشكلة) للتطوير المستقبلي: استبدله بشاشة PIN محلية
التوجيه بـ query params بدون React Router — تبسيط البناء


│ ────────────────────────────────── │
│ 🍽️ Meals │
│ ┌──────┐ ┌──────┐ │
│ │ 🍗 │ │ 🍲 │ │
│ │Fried │ │Kald. │ │
│ │SR 15 │ │SR 15 │ │
│ │ [+] │ │ [+] │ │
│ └──────┘ └──────┘ │
└──────────────────────────────────────┘

  1. تختار: Fried Chicken ×1 + Biko ×1
    الإجمالي: SR 25

  2. تضغط "Place Order" (SR 25)
    → يستدعي place_colleague_order('maria', 'نورا', '0512345678', items)
    → يفتح واتساب ماريا مع رسالة الطلب
    → تحفظ اسمها ورقمها في localStorage


#### صفحة نجاح الطلب (أهم لحظة في تجربتها)

  1. SuccessScreen تعرض:
    ┌──────────────────────────────────────────┐
    │ ✅ Order Sent! 🎉 │
    │ SR 25 — Amount due upon delivery │
    │ ────────────────────────────────────── │
    │ 🌸 نظام الولاء │
    │ أنفقتِ حتى الآن: SR 25 │
    │ ⭐ بقي SR 45 للوجبة المجانية │
    │ [████░░░░░░░░░] 25/70 │
    │ ────────────────────────────────────── │
    │ 📊 رابط لوحتك الشخصية │
    │ halafood.../?me=966512345678 │
    │ [فتح] │
    │ ────────────────────────────────────── │
    │ 📲 ثبّتي تطبيق حلا فود على هاتفك │
    │ [⬇️ تثبيت] │
    │ ────────────────────────────────────── │
    │ 💰 كوني موزّعة حلا فود! │
    │ SR 3 ربح/وجبة │ SR 30+ يومياً │
    │ [اسمك][جوالك][مستشفاك] │
    │ [🚀 سجّلي الآن] │
    └──────────────────────────────────────────┘

---

### سيناريو 4: العميلة العائدة 🔄 (Returning Customer)

**الدور:** نورا طلبت أكثر من مرة. تريد تتبع طلباتها.

  1. تفتح رابطها الشخصي: halafood.wardyat.net/?me=966512345678

  2. أو تفتح رابط ماريا مباشرة ← التطبيق يتعرف عليها من localStorage
    ┌──────────────────────────────────────┐
    │ 👋 أهلاً نورا — تم التعرف عليكِ ✓ │
    │ [تغيير] │
    └──────────────────────────────────────┘

  3. صفحتها الشخصية /?me=966512345678
    ┌──────────────────────────────────────────┐
    │ تبويبات: [طلباتي] [نقاطي] [ألعابي] │
    │ ────────────────────────────────────── │
    │ تبويب طلباتي: │
    │ ───────────── │
    │ 📦 اليوم — SR 25 │
    │ Fried Chicken + Biko │
    │ حالة: في الطريق 🚗 │
    │ │
    │ 📦 أمس — SR 30 │
    │ Kaldereta ×2 │
    │ حالة: تم التسليم ✅ │
    │ ────────────────────────────────────── │
    │ تبويب نقاطي: │
    │ إجمالي الإنفاق: SR 145 │
    │ [████████████████████░░░░░░] 145/210 │
    │ ⭐ بقي SR 65 للوجبتين المجانيتين │
    └──────────────────────────────────────────┘


---

### سيناريو 5: الموزّعة الجديدة (Self-Registration) 🌱

**الدور:** فاطمة عميلة عادية، قررت تصبح موزّعة بعد تجربة الطلب.

  1. بعد طلبها الأول — في SuccessScreen ترى:
    ┌────────────────────────────────────┐
    │ 💰 كوني موزّعة حلا فود! │
    │ SR 3/وجبة │ SR 30+ يومياً │
    │ ───────────────────────────── │
    │ ✓ فاطمة العمري ← (من طلبها) │
    │ ✓ 0556789012 ← (من طلبها) │
    │ [المستشفى: ____] │
    │ │
    │ [🚀 سجّلي فاطمة كموزّعة] │
    └────────────────────────────────────┘

  2. تضغط التسجيل
    → self_register_ambassador('فاطمة العمري', '0556789012', 'مستشفى الملك فهد')
    → يُنشئ كود: fatimah (من اسمها تلقائياً)
    → يُرجع: { ok: true, code: 'fatimah', isNew: true }

  3. تظهر روابطها فوراً:
    ┌────────────────────────────────────────────────────┐
    │ 🎉 تم التسجيل! مرحباً بكِ في حلا فود 🌸 │
    │ ───────────────────────────────────────────── │
    │ 🔗 رابط طلبات زميلاتك: │
    │ halafood.wardyat.net/?ref=fatimah │
    │ [نسخ] [📤 أرسلي على واتساب] │
    │ │
    │ [افتحي لوحتك الآن →] │
    │ halafood.wardyat.net/?dashboard=fatimah │
    └────────────────────────────────────────────────────┘

  4. في نفس الوقت — يظهر في لوحة المطبخ:
    "موزّعة جديدة: فاطمة العمري (fatimah) — مستشفى الملك فهد"


---

### ملخص رحلة كل عضو (الخريطة الكاملة)

                                 حلا فود — خريطة الأعضاء

─────────────────────────────────────────────────────────────────────────────────

المطبخ 👩‍🍳
الصباح: /?admin=hala2026 → تختار طبخة اليوم → تُرسل للموزّعات
النهار: /?admin=hala2026 → تؤكّد طلبات الجملة → تطبخ
المساء: /?admin=hala2026 → تُسلّم + تضع "تم التسليم"

     ↓ تضيف موزّعات / تمنح وجبات مجانية

الموزّعة 🌸 (maria, fatimah, sara...)
الصباح: /?dashboard=كودها → تشارك المنيو في حالة واتساب
النهار: تستقبل طلبات زميلاتها على /?ref=كودها
الظهر: /?dashboard=كودها → تجمع SR 150+ → ترسل للمطبخ
المساء: /?dashboard=كودها → تُسلّم وتضع "تم التسليم"

     ↓ ترسل الرابط لزميلاتها

الزميلة / العميلة 👩
أول مرة: /?ref=كود_الموزّعة → تطلب → تسجّل نفسها اختيارياً
عائدة: /?ref=كود_الموزّعة → يتعرف عليها التطبيق من localStorage
لوحتها: /?me=رقمها → تتابع طلباتها + رصيد الولاء
ألعاب: games.halafood.wardyat.net/?phone=رقمها ← (النظام المنفصل)

     ↓ إن أرادت تصبح موزّعة

الموزّعة الجديدة 🌱 (تسجيل ذاتي)
في صفحة النجاح → تملأ الاستمارة → تحصل على رابطيها فوراً
ثم: /?dashboard=كودها_الجديد
─────────────────────────────────────────────────────────────────────────────────


---

## ═══════════════════════════════════════
## القسم الرابع: تطبيق الألعاب — سيناريو منفصل
## ═══════════════════════════════════════

### الدخول لتطبيق الألعاب

الرابط: games.halafood.wardyat.net/?phone=966512345678

لا يوجد تسجيل دخول — رقم الهاتف هو المعرّف الوحيد


### صفحة الألعاب

/?phone=966512345678

┌────────────────────────────────────────────┐
│ 🎮 عالم حلا فود │
│ نورا ⭐ منتظمة — 12,450 كوين │
│ [████████████░░░░░░░] 12,450 / 50,000 │
│ ────────────────────────────────────── │
│ الألعاب اليومية │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 🎡 │ │ 🃏 │ │ 📅 │ │
│ │ عجلة │ │ كشط │ │ يومية │ │
│ │[العب] │ │ [✓تم] │ │[العب] │ │
│ └────────┘ └────────┘ └────────┘ │
│ ────────────────────────────────────── │
│ 🏆 لوحة الترتيب │
│ 1. ريم 👑 أسطورة 245,800 كوين │
│ 2. فاطمة 💎 VIP 89,200 كوين │
│ 3. نورا ⭐ منتظمة 12,450 كوين │
│ ────────────────────────────────────── │
│ [🍽️ اطلبي الآن] ← رابط للتطبيق الأساسي │
└────────────────────────────────────────────┘


### كيف يصل المستخدم لتطبيق الألعاب؟

التطبيق الأساسي يضع روابط بسيطة:

  1. في صفحة العميلة (?me=رقم):
    زر "ألعابي 🎮" → يفتح: games.halafood.wardyat.net/?phone=رقمها

  2. في لوحة الموزّعة:
    زر "ألعابي 🎮" → يفتح: games.halafood.wardyat.net/?phone=رقمها

  3. في صفحة نجاح الطلب:
    "🎮 العبي واكسبي كوينز!" → games.halafood.wardyat.net/?phone=رقمها
    ```


═══════════════════════════════════════

القسم الخامس: ملخص قرارات الهندسة

═══════════════════════════════════════

القرار السبب
فصل الألعاب في تطبيق مستقل التطوير مستقل، لا يأثر على نظام الطلبات
نفس قاعدة البيانات (Supabase) لا حاجة لـ API بين التطبيقين، التكلفة أقل
الربط عبر روابط فقط بسيط، لا coupling بين الكودين
الأمان بـ SECURITY DEFINER كل العمليات الحساسة تجري على الخادم
لا JWT/sessions بساطة كاملة — التطبيق للهواتف بالمستشفيات، لا يحتاج تعقيد
كود المطبخ في URL (مشكلة) للتطوير المستقبلي: استبدله بشاشة PIN محلية
التوجيه بـ query params بدون React Router — تبسيط البناء

SQL الكامل لجداول الألعاب

(يُضاف لنفس مشروع Supabase)

═══════════════════════════════════════

-- ══════════════════════════════════════════════
-- MIGRATION 10: نظام الألعاب (جداول منفصلة)
-- ══════════════════════════════════════════════

-- جدول اللاعبين (مرتبط بالجوال فقط، لا يحتاج ambassador)
CREATE TABLE IF NOT EXISTS game_players (
  id           BIGSERIAL PRIMARY KEY,
  phone        TEXT NOT NULL UNIQUE,
  display_name TEXT,          -- يُملأ من colleague_name أو ambassador
  total_coins  BIGINT NOT NULL DEFAULT 0,
  level        INTEGER NOT NULL DEFAULT 1,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE game_players ENABLE ROW LEVEL SECURITY;

-- أنون يقرأ ويكتب عبر RPC فقط — لا مباشر
CREATE POLICY "game_players_deny_all" ON game_players FOR ALL TO anon USING (false);

-- ──────────────────────────────────────────────
-- جدول سجل الكوينز (كل عملية تُسجَّل)
-- ──────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS coin_ledger (
  id          BIGSERIAL PRIMARY KEY,
  player_id   BIGINT NOT NULL REFERENCES game_players(id) ON DELETE CASCADE,
  phone       TEXT NOT NULL,
  game_type   TEXT NOT NULL,
  coins_won   INTEGER NOT NULL DEFAULT 0,
  played_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE coin_ledger ENABLE ROW LEVEL SECURITY;
CREATE POLICY "coin_ledger_deny_all" ON coin_ledger FOR ALL TO anon USING (false);

-- ──────────────────────────────────────────────
-- Trigger: تحديث total_coins تلقائياً
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION coins_recompute()
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
  UPDATE game_players
  SET
    total_coins = (SELECT COALESCE(SUM(coins_won), 0) FROM coin_ledger WHERE player_id = NEW.player_id),
    level = CASE
      WHEN (SELECT COALESCE(SUM(coins_won),0) FROM coin_ledger WHERE player_id = NEW.player_id) >= 200000 THEN 4
      WHEN (SELECT COALESCE(SUM(coins_won),0) FROM coin_ledger WHERE player_id = NEW.player_id) >= 50000  THEN 3
      WHEN (SELECT COALESCE(SUM(coins_won),0) FROM coin_ledger WHERE player_id = NEW.player_id) >= 10000  THEN 2
      ELSE 1
    END
  WHERE id = NEW.player_id;
  RETURN NEW;
END;
$$;

CREATE TRIGGER after_coin_insert
  AFTER INSERT ON coin_ledger
  FOR EACH ROW EXECUTE FUNCTION coins_recompute();

-- ──────────────────────────────────────────────
-- RPC: play_game — نقطة دخول واحدة لكل الألعاب
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION play_game(
  p_phone     TEXT,
  p_game_type TEXT
)
RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE
  v_player_id BIGINT;
  v_today     DATE := CURRENT_DATE;
  v_count     INTEGER;
  v_max_plays INTEGER;
  v_coins_won INTEGER;
  v_prize     TEXT;

  -- جدول جوائز كل لعبة (أوزان مرجّحة)
  wheel_prizes  INTEGER[] := ARRAY[5,5,10,10,15,25,50,100];
  scratch_prizes INTEGER[] := ARRAY[0,0,0,10,10,25,50,100,200];
  slot_prizes   INTEGER[] := ARRAY[0,0,5,10,15,30,50];
  mystery_prizes INTEGER[] := ARRAY[10,20,30,50,75,100];
BEGIN
  -- الحد الأقصى لعدد اللعب يومياً لكل لعبة
  v_max_plays := CASE p_game_type
    WHEN 'wheel'   THEN 1
    WHEN 'scratch' THEN 1
    WHEN 'daily'   THEN 1
    WHEN 'slot'    THEN 3
    WHEN 'mystery' THEN 1
    WHEN 'weekly'  THEN 1
    WHEN 'tap'     THEN 5
    ELSE 1
  END;

  -- إيجاد/إنشاء اللاعب
  SELECT id INTO v_player_id FROM game_players WHERE phone = p_phone;
  IF v_player_id IS NULL THEN
    INSERT INTO game_players (phone)
    VALUES (p_phone)
    RETURNING id INTO v_player_id;
  END IF;

  -- التحقق من عدد المرات اليوم
  SELECT COUNT(*) INTO v_count
  FROM coin_ledger
  WHERE player_id = v_player_id
    AND game_type = p_game_type
    AND played_at::DATE = v_today;

  IF v_count >= v_max_plays THEN
    RETURN json_build_object(
      'can_play',     false,
      'coins_won',    0,
      'played_today', true,
      'plays_left',   0
    );
  END IF;

  -- حساب الجائزة
  v_coins_won := CASE p_game_type
    WHEN 'wheel'   THEN wheel_prizes[1 + floor(random() * array_length(wheel_prizes,1))::int]
    WHEN 'scratch' THEN scratch_prizes[1 + floor(random() * array_length(scratch_prizes,1))::int]
    WHEN 'daily'   THEN 20  -- ثابت
    WHEN 'slot'    THEN slot_prizes[1 + floor(random() * array_length(slot_prizes,1))::int]
    WHEN 'mystery' THEN mystery_prizes[1 + floor(random() * array_length(mystery_prizes,1))::int]
    WHEN 'weekly'  THEN 200 + floor(random()*300)::int
    WHEN 'tap'     THEN 5 + floor(random()*10)::int
    ELSE 10
  END;

  -- تسجيل اللعبة
  INSERT INTO coin_ledger (player_id, phone, game_type, coins_won)
  VALUES (v_player_id, p_phone, p_game_type, v_coins_won);

  RETURN json_build_object(
    'can_play',    true,
    'coins_won',   v_coins_won,
    'plays_left',  v_max_plays - v_count - 1
  );
END;
$$;

-- ──────────────────────────────────────────────
-- RPC: get_player_profile
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION get_player_profile(p_phone TEXT)
RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE
  v_player game_players%ROWTYPE;
  v_hist   JSON;
BEGIN
  SELECT * INTO v_player FROM game_players WHERE phone = p_phone;
  IF v_player.id IS NULL THEN
    -- إنشاء لاعب جديد
    INSERT INTO game_players (phone) VALUES (p_phone) RETURNING * INTO v_player;
  END IF;

  SELECT json_agg(
    json_build_object(
      'game_type',  game_type,
      'coins_won',  coins_won,
      'played_at',  played_at
    ) ORDER BY played_at DESC
  ) INTO v_hist
  FROM coin_ledger
  WHERE player_id = v_player.id
  LIMIT 20;

  RETURN json_build_object(
    'total_coins',  v_player.total_coins,
    'display_name', v_player.display_name,
    'level',        v_player.level,
    'history',      COALESCE(v_hist, '[]'::json)
  );
END;
$$;

-- ──────────────────────────────────────────────
-- RPC: get_leaderboard — أفضل 20 لاعب
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION get_leaderboard()
RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
  RETURN (
    SELECT json_agg(
      json_build_object(
        'phone',        phone,
        'display_name', COALESCE(display_name, '***' || right(phone, 4)),
        'total_coins',  total_coins,
        'level',        level
      ) ORDER BY total_coins DESC
    )
    FROM (
      SELECT phone, display_name, total_coins, level
      FROM game_players
      ORDER BY total_coins DESC
      LIMIT 20
    ) t
  );
END;
$$;

-- ──────────────────────────────────────────────
-- RPC: update_player_display_name (يُستدعى بعد أول طلب)
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION update_player_display_name(
  p_phone TEXT,
  p_name  TEXT
)
RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
  INSERT INTO game_players (phone, display_name)
  VALUES (p_phone, p_name)
  ON CONFLICT (phone) DO UPDATE
  SET display_name = EXCLUDED.display_name
  WHERE game_players.display_name IS NULL;  -- لا تغيّر إن كان محدداً
END;
$$;

-- Revoke direct access — كل شيء عبر RPC
REVOKE ALL ON game_players, coin_ledger FROM anon;
REVOKE ALL ON game_players, coin_ledger FROM authenticated;
GRANT EXECUTE ON FUNCTION play_game(TEXT,TEXT)       TO anon;
GRANT EXECUTE ON FUNCTION get_player_profile(TEXT)   TO anon;
GRANT EXECUTE ON FUNCTION get_leaderboard()          TO anon;
GRANT EXECUTE ON FUNCTION update_player_display_name(TEXT,TEXT) TO anon;

═══════════════════════════════════════

نظام PIN الآمن


═══════════════════════════════════════

SQL الكامل لجداول الألعاب

(يُضاف لنفس مشروع Supabase)

═══════════════════════════════════════

-- ══════════════════════════════════════════════
-- MIGRATION 10: نظام الألعاب (جداول منفصلة)
-- ══════════════════════════════════════════════

-- جدول اللاعبين (مرتبط بالجوال فقط، لا يحتاج ambassador)
CREATE TABLE IF NOT EXISTS game_players (
  id           BIGSERIAL PRIMARY KEY,
  phone        TEXT NOT NULL UNIQUE,
  display_name TEXT,          -- يُملأ من colleague_name أو ambassador
  total_coins  BIGINT NOT NULL DEFAULT 0,
  level        INTEGER NOT NULL DEFAULT 1,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE game_players ENABLE ROW LEVEL SECURITY;

-- أنون يقرأ ويكتب عبر RPC فقط — لا مباشر
CREATE POLICY "game_players_deny_all" ON game_players FOR ALL TO anon USING (false);

-- ──────────────────────────────────────────────
-- جدول سجل الكوينز (كل عملية تُسجَّل)
-- ──────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS coin_ledger (
  id          BIGSERIAL PRIMARY KEY,
  player_id   BIGINT NOT NULL REFERENCES game_players(id) ON DELETE CASCADE,
  phone       TEXT NOT NULL,
  game_type   TEXT NOT NULL,
  coins_won   INTEGER NOT NULL DEFAULT 0,
  played_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE coin_ledger ENABLE ROW LEVEL SECURITY;
CREATE POLICY "coin_ledger_deny_all" ON coin_ledger FOR ALL TO anon USING (false);

-- ──────────────────────────────────────────────
-- Trigger: تحديث total_coins تلقائياً
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION coins_recompute()
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
  UPDATE game_players
  SET
    total_coins = (SELECT COALESCE(SUM(coins_won), 0) FROM coin_ledger WHERE player_id = NEW.player_id),
    level = CASE
      WHEN (SELECT COALESCE(SUM(coins_won),0) FROM coin_ledger WHERE player_id = NEW.player_id) >= 200000 THEN 4
      WHEN (SELECT COALESCE(SUM(coins_won),0) FROM coin_ledger WHERE player_id = NEW.player_id) >= 50000  THEN 3
      WHEN (SELECT COALESCE(SUM(coins_won),0) FROM coin_ledger WHERE player_id = NEW.player_id) >= 10000  THEN 2
      ELSE 1
    END
  WHERE id = NEW.player_id;
  RETURN NEW;
END;
$$;

CREATE TRIGGER after_coin_insert
  AFTER INSERT ON coin_ledger
  FOR EACH ROW EXECUTE FUNCTION coins_recompute();

-- ──────────────────────────────────────────────
-- RPC: play_game — نقطة دخول واحدة لكل الألعاب
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION play_game(
  p_phone     TEXT,
  p_game_type TEXT
)
RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE
  v_player_id BIGINT;
  v_today     DATE := CURRENT_DATE;
  v_count     INTEGER;
  v_max_plays INTEGER;
  v_coins_won INTEGER;
  v_prize     TEXT;

  -- جدول جوائز كل لعبة (أوزان مرجّحة)
  wheel_prizes  INTEGER[] := ARRAY[5,5,10,10,15,25,50,100];
  scratch_prizes INTEGER[] := ARRAY[0,0,0,10,10,25,50,100,200];
  slot_prizes   INTEGER[] := ARRAY[0,0,5,10,15,30,50];
  mystery_prizes INTEGER[] := ARRAY[10,20,30,50,75,100];
BEGIN
  -- الحد الأقصى لعدد اللعب يومياً لكل لعبة
  v_max_plays := CASE p_game_type
    WHEN 'wheel'   THEN 1
    WHEN 'scratch' THEN 1
    WHEN 'daily'   THEN 1
    WHEN 'slot'    THEN 3
    WHEN 'mystery' THEN 1
    WHEN 'weekly'  THEN 1
    WHEN 'tap'     THEN 5
    ELSE 1
  END;

  -- إيجاد/إنشاء اللاعب
  SELECT id INTO v_player_id FROM game_players WHERE phone = p_phone;
  IF v_player_id IS NULL THEN
    INSERT INTO game_players (phone)
    VALUES (p_phone)
    RETURNING id INTO v_player_id;
  END IF;

  -- التحقق من عدد المرات اليوم
  SELECT COUNT(*) INTO v_count
  FROM coin_ledger
  WHERE player_id = v_player_id
    AND game_type = p_game_type
    AND played_at::DATE = v_today;

  IF v_count >= v_max_plays THEN
    RETURN json_build_object(
      'can_play',     false,
      'coins_won',    0,
      'played_today', true,
      'plays_left',   0
    );
  END IF;

  -- حساب الجائزة
  v_coins_won := CASE p_game_type
    WHEN 'wheel'   THEN wheel_prizes[1 + floor(random() * array_length(wheel_prizes,1))::int]
    WHEN 'scratch' THEN scratch_prizes[1 + floor(random() * array_length(scratch_prizes,1))::int]
    WHEN 'daily'   THEN 20  -- ثابت
    WHEN 'slot'    THEN slot_prizes[1 + floor(random() * array_length(slot_prizes,1))::int]
    WHEN 'mystery' THEN mystery_prizes[1 + floor(random() * array_length(mystery_prizes,1))::int]
    WHEN 'weekly'  THEN 200 + floor(random()*300)::int
    WHEN 'tap'     THEN 5 + floor(random()*10)::int
    ELSE 10
  END;

  -- تسجيل اللعبة
  INSERT INTO coin_ledger (player_id, phone, game_type, coins_won)
  VALUES (v_player_id, p_phone, p_game_type, v_coins_won);

  RETURN json_build_object(
    'can_play',    true,
    'coins_won',   v_coins_won,
    'plays_left',  v_max_plays - v_count - 1
  );
END;
$$;

-- ──────────────────────────────────────────────
-- RPC: get_player_profile
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION get_player_profile(p_phone TEXT)
RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE
  v_player game_players%ROWTYPE;
  v_hist   JSON;
BEGIN
  SELECT * INTO v_player FROM game_players WHERE phone = p_phone;
  IF v_player.id IS NULL THEN
    -- إنشاء لاعب جديد
    INSERT INTO game_players (phone) VALUES (p_phone) RETURNING * INTO v_player;
  END IF;

  SELECT json_agg(
    json_build_object(
      'game_type',  game_type,
      'coins_won',  coins_won,
      'played_at',  played_at
    ) ORDER BY played_at DESC
  ) INTO v_hist
  FROM coin_ledger
  WHERE player_id = v_player.id
  LIMIT 20;

  RETURN json_build_object(
    'total_coins',  v_player.total_coins,
    'display_name', v_player.display_name,
    'level',        v_player.level,
    'history',      COALESCE(v_hist, '[]'::json)
  );
END;
$$;

-- ──────────────────────────────────────────────
-- RPC: get_leaderboard — أفضل 20 لاعب
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION get_leaderboard()
RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
  RETURN (
    SELECT json_agg(
      json_build_object(
        'phone',        phone,
        'display_name', COALESCE(display_name, '***' || right(phone, 4)),
        'total_coins',  total_coins,
        'level',        level
      ) ORDER BY total_coins DESC
    )
    FROM (
      SELECT phone, display_name, total_coins, level
      FROM game_players
      ORDER BY total_coins DESC
      LIMIT 20
    ) t
  );
END;
$$;

-- ──────────────────────────────────────────────
-- RPC: update_player_display_name (يُستدعى بعد أول طلب)
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION update_player_display_name(
  p_phone TEXT,
  p_name  TEXT
)
RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
  INSERT INTO game_players (phone, display_name)
  VALUES (p_phone, p_name)
  ON CONFLICT (phone) DO UPDATE
  SET display_name = EXCLUDED.display_name
  WHERE game_players.display_name IS NULL;  -- لا تغيّر إن كان محدداً
END;
$$;

-- Revoke direct access — كل شيء عبر RPC
REVOKE ALL ON game_players, coin_ledger FROM anon;
REVOKE ALL ON game_players, coin_ledger FROM authenticated;
GRANT EXECUTE ON FUNCTION play_game(TEXT,TEXT)       TO anon;
GRANT EXECUTE ON FUNCTION get_player_profile(TEXT)   TO anon;
GRANT EXECUTE ON FUNCTION get_leaderboard()          TO anon;
GRANT EXECUTE ON FUNCTION update_player_display_name(TEXT,TEXT) TO anon;

═══════════════════════════════════════

DishCard.tsx

═══════════════════════════════════════

// src/components/DishCard.tsx
import React from 'react';
import { MenuItem } from '../lib/supabase';

interface Props {
  item: MenuItem;
  quantity: number;
  onAdd: (id: string) => void;
  onRemove: (id: string) => void;
}

export function DishCard({ item, quantity, onAdd, onRemove }: Props) {
  return (
    <div className={`bg-white rounded-2xl shadow-sm overflow-hidden flex flex-col
                     transition-all duration-200 active:scale-[0.98]
                     ${quantity > 0 ? 'ring-2 ring-[#148C3C] shadow-md' : ''}`}
         dir="rtl">
      {/* صورة الصنف */}
      <div className="relative aspect-[4/3] bg-[#FFF0DC] overflow-hidden">
        {item.image_url
          ? (
            <img src={item.image_url} alt={item.name_ar || item.name}
                 className="w-full h-full object-cover"
                 loading="lazy"
                 onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} />
          )
          : (
            <div className="w-full h-full flex items-center justify-center">
              <span className="text-5xl">
                {item.category === 'dessert' ? '🍰' : '🍽️'}
              </span>
            </div>
          )
        }
        {/* شارة الفئة */}
        <span className={`absolute top-2 right-2 text-[10px] font-bold px-2 py-0.5 rounded-full
          ${item.category === 'dessert'
            ? 'bg-pink-100 text-pink-700'
            : 'bg-orange-100 text-orange-700'}`}>
          {item.category === 'dessert' ? '🍰 حلى' : '🍽️ وجبة'}
        </span>
        {/* عداد الكمية — يظهر فوق الصورة */}
        {quantity > 0 && (
          <div className="absolute top-2 left-2 min-w-[28px] h-7 bg-[#148C3C] text-white
                          rounded-full flex items-center justify-center px-2
                          text-sm font-extrabold shadow-lg">
            {quantity}
          </div>
        )}
      </div>

      {/* محتوى البطاقة */}
      <div className="p-3 flex flex-col gap-2 flex-1">
        <div className="flex-1">
          <h3 className="font-bold text-gray-800 text-sm leading-tight line-clamp-2">
            {item.name_ar || item.name}
          </h3>
          {item.name !== (item.name_ar || item.name) && (
            <p className="text-xs text-gray-400 mt-0.5" dir="ltr">{item.name}</p>
          )}
        </div>

        {/* السعر */}
        <div className="flex items-center justify-between">
          <span className="font-extrabold text-[#FF4500] text-base">
            SR {item.customer_price}
          </span>
          <span className="text-[10px] text-gray-300">جملة SR {item.wholesale_price}</span>
        </div>

        {/* أزرار الكمية */}
        <div className="flex items-center gap-1">
          {quantity === 0 ? (
            <button onClick={() => onAdd(item.id)}
              className="w-full bg-[#FF4500] text-white font-bold py-2.5 rounded-xl text-sm
                         active:scale-95 transition flex items-center justify-center gap-1.5">
              <span className="text-base leading-none">+</span>
              أضيفي للسلة
            </button>
          ) : (
            <>
              <button onClick={() => onRemove(item.id)}
                className="w-9 h-9 rounded-xl bg-red-50 text-red-500 font-bold text-lg
                           flex items-center justify-center active:scale-90 transition">
                −
              </button>
              <div className="flex-1 bg-[#FFF0DC] rounded-xl py-2 text-center font-extrabold
                              text-[#148C3C] text-base">
                {quantity}
              </div>
              <button onClick={() => onAdd(item.id)}
                className="w-9 h-9 rounded-xl bg-[#148C3C] text-white font-bold text-lg
                           flex items-center justify-center active:scale-90 transition">
                +
              </button>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

═══════════════════════════════════════

نظام PIN آمن (بديل عن الكود في URL)

═══════════════════════════════════════

// src/lib/adminAuth.ts
const ADMIN_SESSION_KEY = 'hf_admin_session';
const SESSION_DURATION  = 8 * 60 * 60 * 1000; // 8 ساعات

export function saveAdminSession(code: string) {
  const payload = { code, ts: Date.now() };
  localStorage.setItem(ADMIN_SESSION_KEY, btoa(JSON.stringify(payload)));
}

export function getAdminSession(): string | null {
  const raw = localStorage.getItem(ADMIN_SESSION_KEY);
  if (!raw) return null;
  try {
    const { code, ts } = JSON.parse(atob(raw));
    if (Date.now() - ts > SESSION_DURATION) {
      localStorage.removeItem(ADMIN_SESSION_KEY);
      return null;
    }
    return code;
  } catch {
    localStorage.removeItem(ADMIN_SESSION_KEY);
    return null;
  }
}

export function clearAdminSession() {
  localStorage.removeItem(ADMIN_SESSION_KEY);
}
// src/components/AdminLogin.tsx — شاشة إدخال PIN
import { useState } from 'react';
import { saveAdminSession } from '../lib/adminAuth';
import { verifyAdmin } from '../lib/supabase';

export function AdminLogin({ onLogin }: { onLogin: (code: string) => void }) {
  const [pin,   setPin]   = useState('');
  const [error, setError] = useState('');
  const [busy,  setBusy]  = useState(false);

  async function submit() {
    if (!pin.trim()) return;
    setBusy(true); setError('');
    const ok = await verifyAdmin(pin.trim());
    setBusy(false);
    if (ok) { saveAdminSession(pin.trim()); onLogin(pin.trim()); }
    else     setError('رمز المطبخ غير صحيح ❌');
  }

  return (
    <div className="min-h-screen bg-[#FF4500] flex items-center justify-center p-6" dir="rtl">
      <div className="bg-white rounded-3xl shadow-2xl p-8 max-w-xs w-full text-center">
        <img src="/logo.jpg" alt="Hala Food"
             className="w-16 h-16 rounded-full ring-4 ring-[#F7B12B] mx-auto mb-4" />
        <h2 className="font-extrabold text-xl text-gray-800 mb-1">دخول المطبخ</h2>
        <p className="text-gray-400 text-xs mb-5">أدخلي رمز المطبخ للوصول</p>
        <input
          type="password" value={pin} onChange={e => setPin(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && submit()}
          placeholder="••••••••"
          className="w-full text-center text-2xl tracking-widest border-2 border-gray-200 rounded-2xl
                     px-4 py-3 mb-3 focus:border-[#

حلا فود — SQL الألعاب + DishCard + نظام PIN الآمن


═══════════════════════════════════════

SQL الكامل لجداول الألعاب

(يُضاف لنفس مشروع Supabase)

═══════════════════════════════════════

```sql
-- ══════════════════════════════════════════════
-- MIGRATION 10: نظام الألعاب (جداول منفصلة)
-- ══════════════════════════════════════════════

-- جدول اللاعبين (مرتبط بالجوال فقط، لا يحتاج ambassador)
CREATE TABLE IF NOT EXISTS game_players (
id BIGSERIAL PRIMARY KEY,
phone TEXT NOT NULL UNIQUE,
display_name TEXT, -- يُملأ من colleague_name أو ambassador
total_coins BIGINT NOT NULL DEFAULT 0,
level INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE game_players ENABLE ROW LEVEL SECURITY;

-- أنون يقرأ ويكتب عبر RPC فقط — لا مباشر
CREATE POLICY "game_players_deny_all" ON game_players FOR ALL TO anon USING (false);

-- ──────────────────────────────────────────────
-- جدول سجل الكوينز (كل عملية تُسجَّل)
-- ──────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS coin_ledger (
id BIGSERIAL PRIMARY KEY,
player_id BIGINT NOT NULL REFERENCES game_players(id) ON DELETE CASCADE,
phone TEXT NOT NULL,
game_type TEXT NOT NULL,
coins_won INTEGER NOT NULL DEFAULT 0,
played_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE coin_ledger ENABLE ROW LEVEL SECURITY;
CREATE POLICY "coin_ledger_deny_all" ON coin_ledger FOR ALL TO anon USING (false);

-- ──────────────────────────────────────────────
-- Trigger: تحديث total_coins تلقائياً
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION coins_recompute()
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
UPDATE game_players
SET
total_coins = (SELECT COALESCE(SUM(coins_won), 0) FROM coin_ledger WHERE player_id = NEW.player_id),
level = CASE
WHEN (SELECT COALESCE(SUM(coins_won),0) FROM coin_ledger WHERE player_id = NEW.player_id) >= 200000 THEN 4
WHEN (SELECT COALESCE(SUM(coins_won),0) FROM coin_ledger WHERE player_id = NEW.player_id) >= 50000 THEN 3
WHEN (SELECT COALESCE(SUM(coins_won),0) FROM coin_ledger WHERE player_id = NEW.player_id) >= 10000 THEN 2
ELSE 1
END
WHERE id = NEW.player_id;
RETURN NEW;
END;
$$;

CREATE TRIGGER after_coin_insert
AFTER INSERT ON coin_ledger
FOR EACH ROW EXECUTE FUNCTION coins_recompute();

-- ──────────────────────────────────────────────
-- RPC: play_game — نقطة دخول واحدة لكل الألعاب
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION play_game(
p_phone TEXT,
p_game_type TEXT
)
RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE
v_player_id BIGINT;
v_today DATE := CURRENT_DATE;
v_count INTEGER;
v_max_plays INTEGER;
v_coins_won INTEGER;
v_prize TEXT;

-- جدول جوائز كل لعبة (أوزان مرجّحة)
wheel_prizes INTEGER[] := ARRAY[5,5,10,10,15,25,50,100];
scratch_prizes INTEGER[] := ARRAY[0,0,0,10,10,25,50,100,200];
slot_prizes INTEGER[] := ARRAY[0,0,5,10,15,30,50];
mystery_prizes INTEGER[] := ARRAY[10,20,30,50,75,100];
BEGIN
-- الحد الأقصى لعدد اللعب يومياً لكل لعبة
v_max_plays := CASE p_game_type
WHEN 'wheel' THEN 1
WHEN 'scratch' THEN 1
WHEN 'daily' THEN 1
WHEN 'slot' THEN 3
WHEN 'mystery' THEN 1
WHEN 'weekly' THEN 1
WHEN 'tap' THEN 5
ELSE 1
END;

-- إيجاد/إنشاء اللاعب
SELECT id INTO v_player_id FROM game_players WHERE phone = p_phone;
IF v_player_id IS NULL THEN
INSERT INTO game_players (phone)
VALUES (p_phone)
RETURNING id INTO v_player_id;
END IF;

-- التحقق من عدد المرات اليوم
SELECT COUNT(*) INTO v_count
FROM coin_ledger
WHERE player_id = v_player_id
AND game_type = p_game_type
AND played_at::DATE = v_today;

IF v_count >= v_max_plays THEN
RETURN json_build_object(
'can_play', false,
'coins_won', 0,
'played_today', true,
'plays_left', 0
);
END IF;

-- حساب الجائزة
v_coins_won := CASE p_game_type
WHEN 'wheel' THEN wheel_prizes[1 + floor(random() * array_length(wheel_prizes,1))::int]
WHEN 'scratch' THEN scratch_prizes[1 + floor(random() * array_length(scratch_prizes,1))::int]
WHEN 'daily' THEN 20 -- ثابت
WHEN 'slot' THEN slot_prizes[1 + floor(random() * array_length(slot_prizes,1))::int]
WHEN 'mystery' THEN mystery_prizes[1 + floor(random() * array_length(mystery_prizes,1))::int]
WHEN 'weekly' THEN 200 + floor(random()300)::int
WHEN 'tap' THEN 5 + floor(random()
10)::int
ELSE 10
END;

-- تسجيل اللعبة
INSERT INTO coin_ledger (player_id, phone, game_type, coins_won)
VALUES (v_player_id, p_phone, p_game_type, v_coins_won);

RETURN json_build_object(
'can_play', true,
'coins_won', v_coins_won,
'plays_left', v_max_plays - v_count - 1
);
END;
$$;

-- ──────────────────────────────────────────────
-- RPC: get_player_profile
-- ──────────────────────────────────────────────
CREATE OR REPLACE FUNCTION get_player_profile(p_phone TEXT)
RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE
v_player game_players%ROWTYPE;
v_hist JSON;
BEGIN
SELECT * INTO v_player FROM game_players WHERE phone = p_phone;
IF v_player.id IS NULL THEN
-- إنشاء لاعب جديد
INSERT INTO game_players (phone) VALUES (p_phone) RETURNING * INTO v_player;
END IF;

SELECT json_agg(
json_build_object(
'game_type', game_type,
'coins_won', coins_won,
'played_at', played_at
) ORDER BY played_at DESC
) INTO v_hist
FROM coin_ledger
WHERE player_id = v_player.id
LIMIT 20;

RETURN json_build_object(
'total_coins', v_player.total_coins,
'display_name', v_player.display_name,
'level', v_player.level,
'history', COALESCE(v_hist, '[]'::json)
);
END;
$$;

-- ──────────────────────────────────────────────
-- RPC: get_leaderboard — أفضل 20 لاعب
-- ───────────────────────────