// ============================================================ // ADS MANAGER - مدير الإعلانات (Meta / TikTok / YouTube) // يُحمَّل قبل index.html الرئيسي ويستخدم api / Icon / Btn المعرّفة هناك // ============================================================ const ADS_PLATFORM_META = { meta: { label: 'ميتا (فيسبوك + إنستغرام)', short: 'Meta', color: '#1877F2', manager: 'https://adsmanager.facebook.com/', studio: 'https://business.facebook.com/latest/home' }, tiktok: { label: 'تيك توك', short: 'TikTok', color: '#111111', manager: 'https://ads.tiktok.com/i18n/dashboard', studio: 'https://www.tiktok.com/creator-center' }, youtube: { label: 'يوتيوب', short: 'YouTube', color: '#FF0000', manager: 'https://ads.google.com/', studio: 'https://studio.youtube.com/' }, }; const ADS_STATUS = { draft: { label: 'مسودة', cls: 'bg-gray-100 text-gray-700' }, paused: { label: 'موقوفة', cls: 'bg-yellow-100 text-yellow-800' }, active: { label: 'تعمل', cls: 'bg-green-100 text-green-800' }, completed: { label: 'منتهية', cls: 'bg-blue-100 text-blue-800' }, failed: { label: 'فشل النشر', cls: 'bg-red-100 text-red-800' }, archived: { label: 'مؤرشفة', cls: 'bg-gray-100 text-gray-500' }, }; const ADS_ACTION_LABEL = { pause_ad: 'إيقاف إعلان', pause_campaign: 'إيقاف الحملة', resume: 'تشغيل', scale_budget: 'زيادة الميزانية', reduce_budget: 'خفض الميزانية', new_creative: 'إبداع جديد', change_targeting: 'تغيير الاستهداف', wait: 'انتظار', }; // Readable error text: a proxy/server error page (e.g. Render 502 during a // deploy) arrives as raw HTML, which must never be dumped into the UI. const adsErrMsg = (e) => { const m = String((e && e.message) || e || ''); const st = e && e.status; if (/^\s* 300 ? m.slice(0, 300) + '…' : m; }; const fmtN = (n) => (n == null ? '—' : Number(n).toLocaleString('en-US', { maximumFractionDigits: 2 })); const fmtD = (d) => (d ? new Date(d).toLocaleString('ar-LY', { dateStyle: 'short', timeStyle: 'short' }) : '—'); function AdsTab({ active, children, onClick, badge }) { return ( ); } function AdsStat({ label, value, sub, color }) { return (
{label}
{value}
{sub &&
{sub}
}
); } function AdsBars({ data, field, label }) { const max = Math.max(1, ...data.map(d => Number(d[field] || 0))); if (!data.length) return
لا توجد بيانات أداء بعد — اضغط «مزامنة» بعد النشر.
; return (
{label}
{data.map(d => (
{d.day.slice(5)}
))}
); } function AdsToast({ msg, kind, onClose }) { useEffect(() => { const t = setTimeout(onClose, 6000); return () => clearTimeout(t); }, [msg]); if (!msg) return null; return (
{msg}
); } // ============================================================ // MAIN // ============================================================ function AdsManager({ user, onClose, initialTab }) { const [tab, setTab] = useState(initialTab === 'new' ? 'campaigns' : (initialTab || 'dashboard')); const [platforms, setPlatforms] = useState([]); const [accounts, setAccounts] = useState([]); const [campaigns, setCampaigns] = useState([]); const [recs, setRecs] = useState([]); const [dash, setDash] = useState(null); const [options, setOptions] = useState({ objectives: [], ctas: [], countries: [], default_countries: [] }); const [selected, setSelected] = useState(null); // campaign id const [wizard, setWizard] = useState(initialTab === 'new'); const [toast, setToast] = useState(null); const [loading, setLoading] = useState(true); const say = (msg, kind = 'ok') => setToast({ msg, kind }); const reloadAll = useCallback(async () => { try { const [p, a, c, r, d, o] = await Promise.all([ api('/ads/platforms'), api('/ads/accounts'), api('/ads/campaigns'), api('/ads/recommendations?status=pending'), api('/ads/dashboard?days=7'), api('/ads/meta/options'), ]); setPlatforms(p.platforms); setAccounts(a.accounts); setCampaigns(c.campaigns); setRecs(r.recommendations); setDash(d); setOptions(o); } catch (e) { say('تعذّر تحميل بيانات مدير الإعلانات: ' + adsErrMsg(e), 'error'); } setLoading(false); }, []); useEffect(() => { reloadAll(); // رسائل عودة OAuth (#/ads?connected=meta أو error=...) const h = window.location.hash; if (h.includes('connected=')) { const m = h.match(/connected=([a-z]+)/); say(`تم ربط حساب ${ADS_PLATFORM_META[m?.[1]]?.label || ''} بنجاح`); setTab('accounts'); window.history.replaceState({}, '', window.location.pathname + '#/ads'); } if (h.includes('error=')) { const m = h.match(/error=([^&]+)/); say('فشل الربط: ' + decodeURIComponent(m?.[1] || ''), 'error'); setTab('accounts'); window.history.replaceState({}, '', window.location.pathname + '#/ads'); } }, [reloadAll]); const openCampaign = (id) => { setSelected(id); setWizard(false); setTab('campaigns'); }; return (

مدير الإعلانات

يولّد الإعلانات والبوسترات والفيديوهات، ينشرها عبر الواجهات الرسمية، يحلّل الأداء ويحسّنه تلقائياً

{loading ? (
) : tab === 'dashboard' ? ( { setTab('campaigns'); setWizard(true); setSelected(null); }} onOpen={openCampaign} onAccounts={() => setTab('accounts')} onRunNow={async () => { try { const r = await api('/ads/scheduler/run-now', { method: 'POST' }); say(`دورة كاملة: ${r.synced} حملة مُزامنة، ${r.recommendations} توصية جديدة`); reloadAll(); } catch (e) { say(adsErrMsg(e), 'error'); } }} /> ) : tab === 'campaigns' ? ( wizard ? { setWizard(false); reloadAll(); setSelected(c.id); }} onCancel={() => setWizard(false)} /> : selected ? { setSelected(null); reloadAll(); }} onChanged={reloadAll} /> : setWizard(true)} /> ) : tab === 'recs' ? ( ) : tab === 'accounts' ? ( ) : tab === 'monitor' ? ( ) : ( )}
setToast(null)} />
); } // ============================================================ // DASHBOARD // ============================================================ function AdsDashboard({ dash, campaigns, recs, accounts, platforms, onNew, onOpen, onAccounts, onRunNow }) { const t = dash?.totals || {}; const active = campaigns.filter(c => c.status === 'active'); const noAccounts = accounts.length === 0; const unconfigured = platforms.filter(p => !p.configured); return (
{noAccounts && (
لا يوجد حساب إعلاني مربوط بعد

اربط حساب ميتا (فيسبوك + إنستغرام) أو تيك توك أو يوتيوب من تبويب الحسابات. بدون ربط يمكنك التخطيط وتوليد الإبداعات لكن لا يمكن النشر.

{unconfigured.length > 0 &&

مفاتيح ناقصة في .env: {unconfigured.map(p => p.missing_config.join('، ')).join(' · ')}

}
ربط حساب
)}

آخر 7 أيام

{campaigns.length} حملة · {active.length} تعمل الآن · {recs.length} توصية معلّقة

مزامنة الكل الآن حملة جديدة بالذكاء الاصطناعي
حسب المنصة
{(dash?.by_platform || []).length === 0 &&
لا بيانات بعد
} {(dash?.by_platform || []).map(p => (
{ADS_PLATFORM_META[p.platform]?.short} {fmtN(p.clicks)} نقرة · {fmtN(p.spend)} إنفاق · CTR {fmtN(p.ctr)}%
))}

الحملات النشطة والأخيرة

{campaigns.length === 0 ?
لا توجد حملات بعد. ابدأ بوصف منتجك وسيكتب مدير الحملات الخطة والنسخ الإعلانية ويصمّم البوسترات.
:
{campaigns.slice(0, 6).map(c => )}
}
); } function AdsCampaignCard({ c, onOpen }) { const st = ADS_STATUS[c.status] || ADS_STATUS.draft; const pm = ADS_PLATFORM_META[c.platform]; const p = c.perf_7d || {}; return ( ); } function AdsCampaignList({ campaigns, onOpen, onNew }) { const [filter, setFilter] = useState('all'); const list = campaigns.filter(c => filter === 'all' || c.status === filter || c.platform === filter); return (
{[['all', 'الكل'], ['active', 'تعمل'], ['paused', 'موقوفة'], ['draft', 'مسودات'], ['meta', 'Meta'], ['tiktok', 'TikTok'], ['youtube', 'YouTube']].map(([k, l]) => ( ))}
حملة جديدة
{list.length === 0 ?
لا توجد حملات مطابقة.
:
{list.map(c => )}
}
); } // ============================================================ // WIZARD: brief → AI plan → review creatives → save // ============================================================ function AdsWizard({ user, accounts, options, platforms, say, onDone, onCancel }) { const [step, setStep] = useState(1); const [platform, setPlatform] = useState('meta'); const [brief, setBrief] = useState(''); const [objective, setObjective] = useState('traffic'); const [budget, setBudget] = useState(5); const [countries, setCountries] = useState(['LY']); const [landing, setLanding] = useState(''); const [accountId, setAccountId] = useState(''); const [plan, setPlan] = useState(null); const [busy, setBusy] = useState(false); const [autopilot, setAutopilot] = useState(false); const platAccounts = accounts.filter(a => a.platform === platform && a.is_active); useEffect(() => { setAccountId(platAccounts.find(a => a.is_default)?.id || platAccounts[0]?.id || ''); }, [platform, accounts.length]); const toggleCountry = (c) => setCountries(cs => cs.includes(c) ? cs.filter(x => x !== c) : [...cs, c]); const doPlan = async () => { if (brief.trim().length < 5) return say('اكتب وصفاً للمنتج أو الخدمة أولاً', 'error'); setBusy(true); try { const r = await api('/ads/campaigns/plan', { method: 'POST', body: JSON.stringify({ platform, brief, objective, daily_budget: Number(budget), countries, landing_url: landing || null, account_id: accountId || null }) }); setPlan(r.plan); setStep(2); say(r.plan._generated_by === 'template' ? 'لا يوجد مفتاح نموذج متاح — استُخدم قالب محلي. أضف مفتاح Claude/GPT/Gemini من الإعدادات لنتائج أقوى.' : `كتب الخطة: ${r.plan._generated_by.replace('ai:', '')}`); } catch (e) { say(adsErrMsg(e), 'error'); } setBusy(false); }; const updCreative = (i, k, v) => setPlan(p => ({ ...p, creatives: p.creatives.map((c, j) => j === i ? { ...c, [k]: v } : c) })); const save = async () => { setBusy(true); try { const c = await api('/ads/campaigns', { method: 'POST', body: JSON.stringify({ platform, account_id: accountId || null, name: plan.name, objective, brief, landing_url: landing || null, daily_budget: Number(plan.daily_budget || budget), targeting: plan.targeting, placements: platform === 'meta' ? ['facebook', 'instagram'] : [platform], creatives: plan.creatives.map(cr => ({ ...cr, type: platform === 'youtube' ? 'video' : (cr.type || 'image') })), autopilot, autopilot_rules: { min_confidence: 0.8, max_daily_budget: Number(plan.daily_budget || budget) * 3 }, plan, }) }); say('تم حفظ الحملة كمسودة. الخطوة التالية: توليد الوسائط ثم النشر.'); onDone(c); } catch (e) { say(adsErrMsg(e), 'error'); } setBusy(false); }; return (

حملة جديدة

{[1, 2].map(s => {s === 1 ? '١ الوصف' : '٢ مراجعة الخطة'})}
{step === 1 && (
{Object.entries(ADS_PLATFORM_META).map(([k, v]) => { const pinfo = platforms.find(p => p.platform === k); return ( ); })}