const $ = (sel, root = document) => root.querySelector(sel); const root = $('#app'); const TAB_GROUPS = [ { label: 'محتوا', tabs: ['glance', 'radarFuture', 'factoryReports', 'media', 'events', 'banners'] }, { label: 'کاربران', tabs: ['members', 'newsletter'] }, ]; const TAB_LABELS = { glance: 'در یک نگاه', radarFuture: 'رادار آینده', factoryReports: 'ماهنامه کارخانه', media: 'ویدیوکست و پادکست', events: 'رویدادها', banners: 'بنرها', members: 'کاربران سایت', newsletter: 'خبرنامه', }; const CATEGORIES = [ 'بازار جهانی', 'سیاست‌گذاری', 'انرژی و ESG', 'صادرات', 'تولید داخلی', 'ریسک و بحران', 'رادار آینده', 'اخبار صنعت', 'گزارش کارخانه', ]; const TYPES = [ { value: 'quarterly', label: 'فصلی' }, { value: 'flash', label: 'فلش' }, { value: 'risk', label: 'هشدار ریسک' }, { value: 'free', label: 'رایگان' }, { value: 'special', label: 'ویژه' }, { value: 'video', label: 'ویدیوکست' }, { value: 'podcast', label: 'پادکست' }, { value: 'news', label: 'خبر' }, { value: 'radar', label: 'رادار آینده' }, ]; const CONTENT_TYPES = [ { key: 'report', label: 'گزارش تحلیلی', icon: '📊', desc: 'گزارش فصلی، فلش یا ویژه با تحلیل بازار', defaultType: 'quarterly', defaultCategory: '' }, { key: 'news', label: 'خبر', icon: '📰', desc: 'اخبار صنعت فولاد', defaultType: 'news', defaultCategory: 'اخبار صنعت' }, { key: 'video', label: 'ویدیوکست', icon: '🎬', desc: 'ویدیوی تحلیلی یا مصاحبه', defaultType: 'video', defaultCategory: '' }, { key: 'podcast', label: 'پادکست', icon: '🎙️', desc: 'اپیزود پادکست صوتی', defaultType: 'podcast', defaultCategory: '' }, { key: 'factory', label: 'گزارش کارخانه', icon: '🏭', desc: 'گزارش تولید و عملیات کارخانه', defaultType: 'factoryReport', defaultCategory: 'گزارش کارخانه' }, { key: 'radarFuture', label: 'رادار آینده', icon: '🔭', desc: 'مقالهٔ بلوکی (متن/عکس) برای ۵ زیرمنوی رادار آینده', defaultType: 'radarfuture', defaultCategory: '' }, ]; // the 5 رادار آینده sub-sections (block-article categories) const RADAR_FUTURE_CATEGORIES = [ 'پایداری و فولاد سبز', 'ژئوپلیتیک و اقتصاد جهانی', 'فناوری و نوآوری', 'بازار و زنجیره فولاد', ]; function inferContentType(a) { if (!a) return 'report'; if (a.type === 'video') return 'video'; if (a.type === 'podcast') return 'podcast'; if (a.type === 'radarfuture') return 'radarFuture'; if (a.type === 'factoryReport' || a.category === 'گزارش کارخانه') return 'factory'; if (a.type === 'news') return 'news'; return 'report'; } const EVENT_TYPES = [ { value: 'conference', label: 'کنگره' }, { value: 'exhibition', label: 'نمایشگاه' }, { value: 'seminar', label: 'سمینار' }, { value: 'international', label: 'جهانی' }, ]; const RADAR_CATEGORIES = [ { value: 'market', label: 'بازارهای جهانی و اقتصادِ کلان' }, { value: 'tech', label: 'نوآوری و فناوری‌هایِ تحول‌آفرین' }, { value: 'commodity', label: 'مواد معدنیِ حیاتی و زنجیره‌ی تامین' }, { value: 'geo', label: 'ژئوپلیتیک و امنیتِ اقتصادی' }, { value: 'energy', label: 'تجارت و کسب‌وکارِ بین‌الملل' }, ]; const SCROLL_DIRS = [ { value: 'up', label: 'از پایین' }, { value: 'left', label: 'از چپ' }, { value: 'right', label: 'از راست' }, ]; const INTEGRATION_ICONS = [ { value: 'exchange', label: 'صرافی' }, { value: 'globe', label: 'جهانی' }, { value: 'chart', label: 'نمودار' }, { value: 'activity', label: 'فعالیت' }, { value: 'database', label: 'پایگاه داده' }, { value: 'newspaper', label: 'خبرگزاری' }, { value: 'building', label: 'سازمان' }, { value: 'trending', label: 'روند' }, { value: 'coins', label: 'کالا' }, ]; const TREND_TYPES = [ { value: 'up', label: 'صعودی ↑' }, { value: 'down', label: 'نزولی ↓' }, { value: 'flat', label: 'ثابت →' }, ]; const CHART_SERIES = [ { value: 'history', label: 'روند قیمت' }, { value: 'export', label: 'صادرات ماهانه' }, { value: 'comparison', label: 'مقایسه جهانی' }, ]; const VISION_ICONS = ['FlaskConical', 'Network', 'Leaf', 'Telescope', 'Sparkles']; async function api(path, { method = 'GET', body, isForm = false } = {}) { const headers = {}; if (body && !isForm) headers['Content-Type'] = 'application/json'; const res = await fetch(path, { method, credentials: 'include', headers, body: isForm ? body : (body ? JSON.stringify(body) : undefined), }); if (res.status === 401) { renderLogin('نیاز به ورود مجدد است'); throw new Error('unauthorized'); } if (!res.ok) { const text = await res.text(); throw new Error(text || res.statusText); } if (res.status === 204) return null; return res.json(); } function renderLogin(errorMsg = '') { root.innerHTML = `

ورود به پنل مدیریت

اندیشکده فولاد

${errorMsg ? `
${escapeHtml(errorMsg)}
` : ''}
`; $('#loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const fd = new FormData(e.target); try { const data = await api('/api/auth/login', { method: 'POST', body: { username: fd.get('username'), password: fd.get('password') }, }); renderRadarList(); } catch { renderLogin('نام کاربری یا رمز عبور اشتباه است'); } }); } function topbarHtml(active, primaryLabel) { const activeGroup = TAB_GROUPS.find(g => g.tabs.includes(active)) || TAB_GROUPS[0]; const groupBtns = TAB_GROUPS.map(g => ` `).join(''); const subTabs = activeGroup.tabs.map(t => ` `).join(''); return `
پنل اندیشکده فولاد
`; } function wireTabs() { document.querySelectorAll('.tabs button, .groups button').forEach(btn => { btn.addEventListener('click', () => { const t = btn.dataset.tab; if (t === 'glance') renderRadarList(); else if (t === 'radarFuture') renderRadarFutureList(); else if (t === 'factoryReports') renderFactoryReportsList(); else if (t === 'media') renderMediaList(); else if (t === 'events') renderEventsList(); else if (t === 'banners') renderBannersList(); else if (t === 'members') renderMembersList(); else if (t === 'newsletter') renderNewsletterList(); }); }); $('#logoutBtn').addEventListener('click', async () => { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {}); renderLogin(); }); $('#genBtn')?.addEventListener('click', () => openGeneratorDialog()); } function openGeneratorDialog(defaultKind = 'glance') { const existing = document.getElementById('generatorModal'); if (existing) existing.remove(); const overlay = document.createElement('div'); overlay.id = 'generatorModal'; overlay.style.cssText = 'position:fixed;inset:0;z-index:1000;display:grid;place-items:center;'; overlay.innerHTML = ` `; document.body.appendChild(overlay); const kindEl = overlay.querySelector('#genKind'); const catEl = overlay.querySelector('#genCategory'); const statusEl = overlay.querySelector('#genStatus'); const hintEl = overlay.querySelector('#genHint'); const sourceEl = overlay.querySelector('#genSources'); const sourceStatusEl = overlay.querySelector('#sourceStatus'); const categories = { glance: [{ value: 'market', label: 'بازارهای جهانی و اقتصاد کلان' }, { value: 'tech', label: 'نوآوری و فناوری‌های تحول‌آفرین' }, { value: 'commodity', label: 'مواد معدنی حیاتی و زنجیره تامین' }, { value: 'geo', label: 'ژئوپلیتیک و امنیت اقتصادی' }, { value: 'energy', label: 'تجارت و کسب‌وکار بین‌الملل' }], radarFuture: [{ value: 'all', label: 'همه' }, { value: 'بازار و زنجیره فولاد', label: 'بازار و زنجیره فولاد' }, { value: 'پایداری و فولاد سبز', label: 'پایداری و فولاد سبز' }, { value: 'ژئوپلیتیک و اقتصاد جهانی', label: 'ژئوپلیتیک و اقتصاد جهانی' }, { value: 'فناوری و نوآوری', label: 'فناوری و نوآوری' }], }; const fillCats = () => { const list = categories[kindEl.value] || categories.glance; catEl.innerHTML = list.map(c => ``).join(''); hintEl.textContent = kindEl.value === 'radarFuture' ? 'برای رادار آینده، انتخاب «همه» هر ۴ زیرسرفصل را draft می‌کند.' : 'در یک نگاه، چند آیتم کوتاه و فشرده ساخته می‌شود.'; }; kindEl.value = defaultKind; fillCats(); kindEl.addEventListener('change', fillCats); overlay.querySelector('#closeGen').addEventListener('click', () => overlay.remove()); overlay.querySelector('.modal-backdrop').addEventListener('click', () => overlay.remove()); const saveSources = async () => { const text = sourceEl.value || ''; const data = await api('/api/admin/generator-sources', { method: 'PUT', body: { text } }); sourceStatusEl.textContent = `${data.count || 0} منبع ذخیره شد`; return data; }; api('/api/admin/generator-sources').then((data) => { sourceEl.value = data.text || ''; sourceStatusEl.textContent = `${data.sources?.length || 0} منبع ذخیره شده`; }).catch((err) => { sourceStatusEl.textContent = `خطا در بارگذاری منابع: ${err.message}`; }); overlay.querySelector('#saveSources').addEventListener('click', async () => { try { await saveSources(); } catch (err) { sourceStatusEl.textContent = `خطا در ذخیره منابع: ${err.message}`; } }); overlay.querySelector('#startGen').addEventListener('click', async () => { statusEl.textContent = 'در حال ذخیره منابع...'; try { await saveSources(); } catch (err) { statusEl.textContent = `خطا در ذخیره منابع: ${err.message}`; return; } statusEl.textContent = 'در حال شروع...'; const job = await api('/api/admin/generate', { method: 'POST', body: { kind: kindEl.value, category: catEl.value, sourcesText: sourceEl.value || '' } }); statusEl.textContent = `${job.message} | ${job.step} | ${job.progress}%`; const timer = setInterval(async () => { try { const fresh = await api(`/api/admin/generate/jobs/${job.id}`); statusEl.textContent = `${fresh.message} | ${fresh.step} | ${fresh.progress}%`; if (fresh.status === 'done') { clearInterval(timer); statusEl.textContent = 'تمام شد'; setTimeout(() => { overlay.remove(); if (kindEl.value === 'radarFuture') renderRadarFutureList(); else renderRadarList(); }, 700); } if (fresh.status === 'error') { clearInterval(timer); statusEl.textContent = `خطا: ${fresh.error || fresh.message}`; } } catch (err) { clearInterval(timer); statusEl.textContent = `خطا: ${err.message}`; } }, 1000); }); } async function renderList() { root.innerHTML = ` ${topbarHtml('articles', '+ مطلب جدید')}

مطالب

${CATEGORIES.map(c => ``).join('')}

در حال بارگذاری…

`; wireTabs(); $('#newBtn').addEventListener('click', () => renderEditor(null)); let allItems = []; let activeCat = ''; const safeCoverUrl = (u) => /^\/uploads\/[a-zA-Z0-9._-]+$/.test(String(u || '')) ? u : ''; function renderItems() { const filtered = activeCat ? allItems.filter(a => a.category === activeCat) : allItems; if (!filtered.length) { $('#list').innerHTML = `

مطلبی در این دسته‌بندی یافت نشد.

`; return; } $('#list').innerHTML = filtered.map((a) => `
${a.featured ? 'ویژه' : ''} ${a.category ? `${escapeHtml(a.category)}` : ''} ${a.publishDate ? `${escapeHtml(a.publishDate)}` : ''}

${escapeHtml(a.title)}

${escapeHtml(a.summary)}

`).join(''); } $('#cat-bar').addEventListener('click', (e) => { const btn = e.target.closest('.cat-btn'); if (!btn) return; activeCat = btn.dataset.cat; document.querySelectorAll('.cat-btn').forEach(b => b.classList.toggle('active', b === btn)); renderItems(); }); $('#list').addEventListener('click', async (e) => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const row = btn.closest('.row'); const id = row.dataset.id; if (btn.dataset.action === 'edit') { const a = await api(`/api/articles/${id}`); renderEditor(a); } else if (btn.dataset.action === 'delete') { if (!confirm('این مطلب حذف شود؟')) return; await api(`/api/articles/${id}`, { method: 'DELETE' }); renderList(); } }); try { allItems = await api('/api/articles?limit=500'); if (!allItems.length) { $('#list').innerHTML = `

هنوز مطلبی ثبت نشده است.

`; return; } renderItems(); } catch (err) { console.error(err); $('#list').innerHTML = `

خطا در بارگذاری مطالب.

`; } } async function renderRadarFutureList() { root.innerHTML = ` ${topbarHtml('radarFuture', '+ رادار آینده جدید')}

رادار آینده

محتوای صفحات «رادار آینده» سایت (‎/radar/*‎) — مقالهٔ بلوکی به تفکیک ۵ زیرمنو.

${RADAR_FUTURE_CATEGORIES.map(c => ``).join('')}

در حال بارگذاری…

`; wireTabs(); $('#newBtn').addEventListener('click', () => renderEditor(null, { contentType: 'radarFuture', type: 'radarfuture', category: '' })); let allItems = [], activeCat = ''; function renderItems() { const filtered = activeCat ? allItems.filter(a => a.category === activeCat) : allItems; if (!filtered.length) { $('#list').innerHTML = `

مطلبی در این بخش یافت نشد.

`; return; } $('#list').innerHTML = filtered.map(a => `
${a.category?`${escapeHtml(a.category)}`:''}${a.publishDate?`${escapeHtml(a.publishDate)}`:''}

${escapeHtml(a.title)}

${escapeHtml(a.summary||'')}

`).join(''); } $('#cat-bar').addEventListener('click', e => { const b = e.target.closest('.cat-btn'); if (!b) return; activeCat = b.dataset.cat; document.querySelectorAll('.cat-btn').forEach(x => x.classList.toggle('active', x === b)); renderItems(); }); $('#list').addEventListener('click', async e => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderEditor(await api(`/api/articles/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این مطلب حذف شود؟')) return; await api(`/api/articles/${id}`, { method: 'DELETE' }); renderRadarFutureList(); } }); try { const all = await api('/api/articles?limit=500'); allItems = all.filter(a => a.type === 'radarfuture'); if (!allItems.length) { $('#list').innerHTML = `

هنوز مطلبی ثبت نشده — با «+ رادار آینده جدید» شروع کن.

`; return; } renderItems(); } catch (err) { $('#list').innerHTML = `

خطا در بارگذاری.

`; } } async function renderMediaList() { root.innerHTML = ` ${topbarHtml('media', '+ ویدیوکست جدید')}

ویدیوکست و پادکست

در حال بارگذاری…

`; wireTabs(); $('#newBtn').addEventListener('click', () => renderEditor(null, { contentType: 'video', type: 'video', category: '' })); $('#newPod').addEventListener('click', () => renderEditor(null, { contentType: 'podcast', type: 'podcast', category: '' })); $('#list').addEventListener('click', async e => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderEditor(await api(`/api/articles/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این مورد حذف شود؟')) return; await api(`/api/articles/${id}`, { method: 'DELETE' }); renderMediaList(); } }); try { const all = await api('/api/articles?limit=500'); const items = all.filter(a => a.type === 'video' || a.type === 'podcast'); if (!items.length) { $('#list').innerHTML = `

هنوز موردی ثبت نشده.

`; return; } $('#list').innerHTML = items.map(a => `
${a.type==='video'?'ویدیوکست':'پادکست'}${a.publishDate?`${escapeHtml(a.publishDate)}`:''}

${escapeHtml(a.title)}

${escapeHtml(a.summary||'')}

`).join(''); } catch (err) { $('#list').innerHTML = `

خطا در بارگذاری.

`; } } function renderTypeSelector() { root.innerHTML = `
مطلب جدید

چه نوع محتوایی می‌خواهید منتشر کنید؟

${CONTENT_TYPES.map(ct => ` `).join('')}
`; $('#backBtn').addEventListener('click', () => renderList()); document.querySelectorAll('.type-card').forEach(btn => { btn.addEventListener('click', () => { const ct = CONTENT_TYPES.find(c => c.key === btn.dataset.ctype); if (ct.externalTab === 'radar') { renderRadarList(); return; } renderEditor(null, { contentType: ct.key, type: ct.defaultType, category: ct.defaultCategory }); }); }); } function renderEditor(article, draft = {}) { if (!article && !draft.contentType) { renderTypeSelector(); return; } const contentType = draft.contentType || inferContentType(article); const ctInfo = CONTENT_TYPES.find(c => c.key === contentType) || CONTENT_TYPES[0]; const isEdit = !!article; const a = article || { title: '', category: draft.category || ctInfo.defaultCategory, type: draft.type || ctInfo.defaultType, author: '', authorRole: '', authorInitial: '', publishDate: '', episodeNumber: '', pages: 0, price: 0, isFree: true, summary: '', body: '', coverImage: '', tags: [], featured: false, }; // content blocks (interleaved text + chart/image) and citation sources let blocks = (Array.isArray(a.blocks) && a.blocks.length) ? a.blocks.map(b => ({ ...b })) : (a.body ? [{ type: 'text', value: a.body }] : []); let sources = Array.isArray(a.sources) ? a.sources.map(s => ({ ...s })) : []; const REPORT_TYPES = TYPES.filter(t => ['quarterly','flash','special','risk','free'].includes(t.value)); const isRadarFuture = contentType === 'radarFuture'; const hasCover = ['report','news','video','podcast','factory','radarFuture'].includes(contentType); const hasCategory = ['report','news','radarFuture'].includes(contentType); const hasAuthor = ['report','video','podcast'].includes(contentType); const hasPages = ['report','factory'].includes(contentType); const hasPrice = contentType === 'report'; const hasBody = ['report','news','factory','radarFuture'].includes(contentType); const hasMediaLink = ['video','podcast'].includes(contentType); const hasReportType = contentType === 'report'; const categoryOptions = isRadarFuture ? RADAR_FUTURE_CATEGORIES : CATEGORIES; root.innerHTML = `
${ctInfo.icon} ${isEdit ? 'ویرایش' : 'ایجاد'} — ${ctInfo.label}
${hasCategory ? ` ` : ``} ${hasReportType ? ` ` : ``} ${contentType === 'podcast' ? ` ` : ``} ${hasAuthor ? ` ` : ``} ${hasPages ? ` ` : ``} ${hasPrice ? ` ` : ``} ${hasCover ? `
` : ``}
${hasBody ? `
` : ``} ${hasMediaLink ? `
` : (hasBody ? `` : ``)}
${!isEdit ? `` : ''}
`; const backToList = () => { if (contentType === 'radarFuture') return renderRadarFutureList(); if (contentType === 'video' || contentType === 'podcast') return renderMediaList(); return renderRadarFutureList(); }; $('#backBtn').addEventListener('click', backToList); document.getElementById('changeTypeBtn')?.addEventListener('click', () => renderTypeSelector()); const coverFile = document.getElementById('coverFile'); if (coverFile) { coverFile.addEventListener('change', async (e) => { const file = e.target.files?.[0]; if (!file) return; const fd = new FormData(); fd.append('file', file); try { const data = await api('/api/uploads', { method: 'POST', body: fd, isForm: true }); const url = data.url; const input = $('input[name="coverImage"]'); if (input) input.value = url; const prev = document.getElementById('coverPreview'); if (prev) { prev.src = url; prev.style.display = ''; } } catch (err) { alert('آپلود ناموفق بود: ' + err.message); } }); } // video / podcast media file → object storage (fills the media URL field) const mediaFile = document.getElementById('mediaFile'); if (mediaFile) { mediaFile.addEventListener('change', async (e) => { const file = e.target.files?.[0]; if (!file) return; const status = document.getElementById('mediaStatus'); if (status) status.textContent = 'در حال آپلود فایل… (بسته به حجم چند لحظه طول می‌کشد)'; const fd = new FormData(); fd.append('file', file); try { const { url } = await api('/api/uploads/raw', { method: 'POST', body: fd, isForm: true }); document.getElementById('mediaUrl').value = url; if (status) status.textContent = '✓ آپلود شد و در آبجکت استوریج ذخیره شد'; } catch (err) { if (status) status.textContent = ''; alert('آپلود فایل ناموفق بود: ' + err.message); } }); } // ── content blocks (text + chart/image, reorderable) ────────────────── function renderBlocks() { const el = document.getElementById('blocksEditor'); if (!el) return; if (!blocks.length) { el.innerHTML = `
هنوز محتوایی اضافه نشده — یک پاراگراف یا تصویر/نمودار اضافه کنید.
`; return; } el.innerHTML = blocks.map((b, i) => { const ctrls = `
`; if (b.type === 'image') { return `
${ctrls}
${b.url ? `` : `
تصویر / نمودار
`}
`; } return `
${ctrls}
`; }).join(''); el.querySelectorAll('[data-txt]').forEach(t => t.addEventListener('input', e => { blocks[+e.target.dataset.txt].value = e.target.value; })); el.querySelectorAll('[data-cap]').forEach(t => t.addEventListener('input', e => { blocks[+e.target.dataset.cap].caption = e.target.value; })); el.querySelectorAll('[data-imgfile]').forEach(f => f.addEventListener('change', async e => { const file = e.target.files?.[0]; if (!file) return; const i = +e.target.dataset.imgfile; const fd2 = new FormData(); fd2.append('file', file); try { const { url } = await api('/api/uploads', { method: 'POST', body: fd2, isForm: true }); blocks[i].url = url; renderBlocks(); } catch (err) { alert('آپلود ناموفق بود: ' + err.message); } })); el.querySelectorAll('button[data-act]').forEach(btn => btn.addEventListener('click', e => { const i = +e.currentTarget.dataset.i, act = e.currentTarget.dataset.act; if (act === 'del') blocks.splice(i, 1); else if (act === 'up' && i > 0) { [blocks[i - 1], blocks[i]] = [blocks[i], blocks[i - 1]]; } else if (act === 'down' && i < blocks.length - 1) { [blocks[i + 1], blocks[i]] = [blocks[i], blocks[i + 1]]; } renderBlocks(); })); } document.getElementById('addTextBlock')?.addEventListener('click', () => { blocks.push({ type: 'text', value: '' }); renderBlocks(); }); document.getElementById('addImageBlock')?.addEventListener('click', () => { blocks.push({ type: 'image', url: '', caption: '' }); renderBlocks(); }); renderBlocks(); // ── sources / citations ─────────────────────────────────────────────── function renderSources() { const el = document.getElementById('sourcesEditor'); if (!el) return; el.innerHTML = sources.length ? sources.map((s, i) => `
`).join('') : `
منبعی اضافه نشده.
`; el.querySelectorAll('[data-stitle]').forEach(t => t.addEventListener('input', e => { sources[+e.target.dataset.stitle].title = e.target.value; })); el.querySelectorAll('[data-surl]').forEach(t => t.addEventListener('input', e => { sources[+e.target.dataset.surl].url = e.target.value; })); el.querySelectorAll('[data-sdel]').forEach(b => b.addEventListener('click', e => { sources.splice(+e.currentTarget.dataset.sdel, 1); renderSources(); })); } document.getElementById('addSource')?.addEventListener('click', () => { sources.push({ title: '', url: '' }); renderSources(); }); renderSources(); $('#editorForm').addEventListener('submit', async (e) => { e.preventDefault(); const fd = new FormData(e.target); const tagsRaw = String(fd.get('tags') || '').split(/[,،]/).map(s => s.trim()).filter(Boolean); const isFreeVal = fd.get('isFree'); const payload = { title: fd.get('title'), category: fd.get('category') || null, type: fd.get('type') || null, author: fd.get('author') || null, authorRole: fd.get('authorRole') || null, authorInitial:fd.get('authorInitial') || null, publishDate: fd.get('publishDate') || null, episodeNumber:fd.get('episodeNumber') || null, pages: Number(fd.get('pages') || 0), price: Number(fd.get('price') || 0), isFree: isFreeVal === 'on' || isFreeVal === 'true', featured: fd.get('featured') === 'on', summary: fd.get('summary') || null, body: hasBody ? blocks.filter(b => b.type === 'text' && b.value && b.value.trim()).map(b => b.value.trim()).join('\n\n') : (fd.get('body') || null), coverImage: fd.get('coverImage') || null, blocks: hasBody ? blocks.filter(b => (b.type === 'text' && b.value && b.value.trim()) || (b.type === 'image' && b.url)) : [], sources: sources.filter(s => (s.title && s.title.trim()) || (s.url && s.url.trim())), tags: tagsRaw, }; try { if (isEdit) { await api(`/api/articles/${article.id}`, { method: 'PUT', body: payload }); } else { await api('/api/articles', { method: 'POST', body: payload }); } backToList(); } catch (err) { alert('ذخیره ناموفق بود: ' + err.message); } }); } function escapeAttr(s) { return String(s ?? '').replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); } const RISK_LEVELS = [ { value: 'critical', label: 'بحرانی' }, { value: 'high', label: 'بالا' }, { value: 'medium', label: 'متوسط' }, { value: 'low', label: 'پایین' }, { value: 'opportunity', label: 'فرصت' }, ]; async function renderRisksList() { root.innerHTML = ` ${topbarHtml('risks', '+ سیگنال جدید')}

سیگنال‌های ریسک

در حال بارگذاری…

`; wireTabs(); $('#newBtn').addEventListener('click', () => renderRiskEditor(null)); try { const items = await api('/api/risks?limit=200'); if (!items.length) { $('#list').innerHTML = `

هنوز سیگنالی ثبت نشده است.

`; return; } const SAFE_LEVELS = new Set(['critical', 'high', 'medium', 'low', 'opportunity']); const labelOf = (lvl) => (RISK_LEVELS.find(r => r.value === lvl)?.label) || escapeHtml(lvl); $('#list').innerHTML = items.map(r => `
${labelOf(r.level)} ${escapeHtml(r.name)} ${r.date ? `${escapeHtml(r.date)}` : ''}

${escapeHtml(r.quote)}

`).join(''); $('#list').addEventListener('click', async (e) => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') { const r = await api(`/api/risks/${id}`); renderRiskEditor(r); } else if (btn.dataset.action === 'delete') { if (!confirm('این سیگنال حذف شود؟')) return; await api(`/api/risks/${id}`, { method: 'DELETE' }); renderRisksList(); } }); } catch (err) { console.error(err); $('#list').innerHTML = `

خطا در بارگذاری سیگنال‌ها.

`; } } function renderRiskEditor(risk) { const r = risk || { quote: '', name: '', date: '', level: 'high', sortOrder: 0 }; const isEdit = !!risk; root.innerHTML = `
${isEdit ? 'ویرایش سیگنال ریسک' : 'سیگنال ریسک جدید'}
`; $('#backBtn').addEventListener('click', () => renderRisksList()); $('#editorForm').addEventListener('submit', async (e) => { e.preventDefault(); const fd = new FormData(e.target); const payload = { name: fd.get('name'), quote: fd.get('quote'), date: fd.get('date') || null, level: fd.get('level'), sortOrder: Number(fd.get('sortOrder') || 0), }; try { if (isEdit) await api(`/api/risks/${risk.id}`, { method: 'PUT', body: payload }); else await api('/api/risks', { method: 'POST', body: payload }); renderRisksList(); } catch (err) { alert('ذخیره ناموفق بود: ' + err.message); } }); } // ---------- users ---------- async function renderUsersList() { root.innerHTML = ` ${topbarHtml('users', '+ کاربر جدید')}

کاربران پنل

در حال بارگذاری…

`; wireTabs(); $('#newBtn').addEventListener('click', () => renderUserEditor(null)); try { const items = await api('/api/users'); if (!items.length) { $('#list').innerHTML = `

هنوز کاربری ثبت نشده است.

`; return; } $('#list').innerHTML = items.map(u => `
${escapeHtml(u.username)} ${u.createdAt ? `عضویت: ${escapeHtml(u.createdAt)}` : ''}
`).join(''); $('#list').addEventListener('click', async (e) => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const row = btn.closest('.row'); const id = row.dataset.id; if (btn.dataset.action === 'edit') { renderUserEditor({ id, username: row.dataset.username }); } else if (btn.dataset.action === 'delete') { if (!confirm('این کاربر حذف شود؟')) return; try { await api(`/api/users/${id}`, { method: 'DELETE' }); renderUsersList(); } catch (err) { alert('حذف ناموفق بود: ' + friendlyUserError(err.message)); } } }); } catch (err) { console.error(err); $('#list').innerHTML = `

خطا در بارگذاری کاربران.

`; } } function renderUserEditor(user) { const u = user || { username: '' }; const isEdit = !!user; root.innerHTML = `
${isEdit ? 'ویرایش کاربر' : 'کاربر جدید'}
`; $('#backBtn').addEventListener('click', () => renderUsersList()); $('#editorForm').addEventListener('submit', async (e) => { e.preventDefault(); const fd = new FormData(e.target); const username = String(fd.get('username') || '').trim(); const password = String(fd.get('password') || ''); const payload = { username }; if (password) payload.password = password; try { if (isEdit) await api(`/api/users/${user.id}`, { method: 'PUT', body: payload }); else await api('/api/users', { method: 'POST', body: { username, password } }); renderUsersList(); } catch (err) { alert('ذخیره ناموفق بود: ' + friendlyUserError(err.message)); } }); } function friendlyUserError(msg) { const m = String(msg || ''); if (m.includes('username_taken')) return 'این نام کاربری قبلاً ثبت شده است.'; if (m.includes('username_too_short')) return 'نام کاربری باید حداقل ۳ کاراکتر باشد.'; if (m.includes('password_too_short')) return 'رمز عبور باید حداقل ۸ کاراکتر باشد.'; if (m.includes('cannot_delete_self')) return 'نمی‌توانید حساب خودتان را حذف کنید.'; if (m.includes('cannot_delete_last_user')) return 'آخرین کاربر را نمی‌توان حذف کرد.'; return m; } // ═══════════════════════════════════════════════ // SITE MEMBERS (phone/OTP users) // ═══════════════════════════════════════════════ function fmtDateTime(ts) { if (!ts) return '—'; // pg returns timestamptz as "2026-06-27 08:01:07.749392+00" — space separator, // microseconds, and a colon-less "+00" offset that Date can't parse. Normalize. const iso = String(ts).replace(' ', 'T') .replace(/(\.\d{3})\d+/, '$1') // trim micro→milliseconds .replace(/([+-]\d{2})(\d{2})?$/, (_, h, m) => `${h}:${m || '00'}`); // +00 → +00:00 const d = new Date(iso); if (isNaN(d)) return escapeHtml(String(ts)); try { return new Intl.DateTimeFormat('fa-IR', { dateStyle: 'medium', timeStyle: 'short', calendar: 'persian', }).format(d); } catch { return d.toISOString(); } } async function renderMembersList() { root.innerHTML = ` ${topbarHtml('members', '↻ بروزرسانی')}

کاربران سایت

کاربران ثبت‌نامی سایت (موبایل/کد یکبارمصرف). آخرین ورود، شماره تماس، مسدودسازی و حذف.

در حال بارگذاری…

`; wireTabs(); $('#newBtn').addEventListener('click', () => renderMembersList()); let all = []; function paint(items) { if (!items.length) { $('#list').innerHTML = `

کاربری یافت نشد.

`; return; } $('#list').innerHTML = items.map(m => `
${escapeHtml(m.name || 'بی‌نام')} ${m.isActive ? '' : 'مسدود'} ${m.phone ? `${escapeHtml(m.phone)}` : 'بدون شماره'}

آخرین ورود: ${fmtDateTime(m.lastLogin)} · عضویت: ${fmtDateTime(m.createdAt)}${m.purchases ? ` · ${m.purchases} خرید` : ''}

`).join(''); } $('#list').addEventListener('click', async (e) => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; const m = all.find(x => String(x.id) === String(id)); if (btn.dataset.action === 'view') { renderMemberDetail(id); } else if (btn.dataset.action === 'toggle') { try { await api(`/api/members/${id}`, { method: 'PATCH', body: { isActive: !m.isActive } }); renderMembersList(); } catch (err) { alert('عملیات ناموفق بود: ' + err.message); } } else if (btn.dataset.action === 'delete') { if (!confirm('این کاربر برای همیشه حذف شود؟')) return; try { await api(`/api/members/${id}`, { method: 'DELETE' }); renderMembersList(); } catch (err) { alert('حذف ناموفق بود' + (String(err.message).includes('forbidden') ? ' (فقط مالک پنل می‌تواند حذف کند)' : ': ' + err.message)); } } }); $('#memberSearch').addEventListener('input', (e) => { const q = e.target.value.trim().toLowerCase(); paint(!q ? all : all.filter(m => (m.name || '').toLowerCase().includes(q) || (m.email || '').toLowerCase().includes(q) || (m.phone || '').includes(q))); }); try { all = await api('/api/members'); paint(all); } catch (err) { console.error(err); $('#list').innerHTML = `

خطا در بارگذاری کاربران.

`; } } async function renderMemberDetail(id) { root.innerHTML = `
جزئیات کاربر

در حال بارگذاری…

`; $('#backBtn').addEventListener('click', () => renderMembersList()); try { const m = await api(`/api/members/${id}`); const field = (label, val, ltr) => `
${label} ${val}
`; const buys = m.purchases?.length ? m.purchases.map(p => `
  • ${escapeHtml(p.title || p.articleId)} — ${fmtDateTime(p.purchasedAt)}${p.amount ? ` (${p.amount.toLocaleString('fa-IR')} تومان)` : ''}
  • `).join('') : '
  • خریدی ثبت نشده
  • '; $('#detail').innerHTML = `

    ${escapeHtml(m.name || 'بی‌نام')} ${m.isActive ? '' : 'مسدود'}

    ${field('شماره موبایل:', m.phone ? escapeHtml(m.phone) : '—', true)} ${field('ایمیل:', m.email ? escapeHtml(m.email) : '—', true)} ${field('آخرین ورود:', fmtDateTime(m.lastLogin))} ${field('تاریخ عضویت:', fmtDateTime(m.createdAt))} ${field('وضعیت:', m.isActive ? 'فعال' : 'مسدود')}

    خریدها (${m.purchases?.length || 0})

    فعالیت اخیر

    در حال بارگذاری…

    `; $('#toggleBtn').addEventListener('click', async () => { try { await api(`/api/members/${id}`, { method: 'PATCH', body: { isActive: !m.isActive } }); renderMemberDetail(id); } catch (err) { alert('عملیات ناموفق بود: ' + err.message); } }); $('#delBtn').addEventListener('click', async () => { if (!confirm('این کاربر برای همیشه حذف شود؟')) return; try { await api(`/api/members/${id}`, { method: 'DELETE' }); renderMembersList(); } catch (err) { alert('حذف ناموفق بود: ' + err.message); } }); // activity timeline (loaded separately so the card renders instantly) try { const acts = await api(`/api/members/${id}/activity`); if (!acts.length) { $('#activity').innerHTML = `

    فعالیتی ثبت نشده.

    `; } else { $('#activity').innerHTML = ``; } } catch { $('#activity').innerHTML = `

    خطا در بارگذاری فعالیت.

    `; } } catch (err) { $('#detail').innerHTML = `

    خطا در بارگذاری.

    `; } } // ═══════════════════════════════════════════════ // NEWSLETTER SUBSCRIBERS // ═══════════════════════════════════════════════ async function renderNewsletterList() { root.innerHTML = ` ${topbarHtml('newsletter', '↻ بروزرسانی')}

    عضویت در خبرنامه

    ایمیل‌هایی که از فرم فوتر سایت ثبت شده‌اند.

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderNewsletterList()); try { const items = await api('/api/newsletter'); if (!items.length) { $('#list').innerHTML = `

    هنوز ایمیلی ثبت نشده.

    `; return; } $('#list').innerHTML = `

    ${items.length.toLocaleString('fa-IR')} ایمیل

    ` + items.map(s => `
    ${escapeHtml(s.email)} ${fmtDateTime(s.createdAt)}
    `).join(''); $('#list').addEventListener('click', async (e) => { const btn = e.target.closest('button[data-action="delete"]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (!confirm('این ایمیل حذف شود؟')) return; try { await api(`/api/newsletter/${id}`, { method: 'DELETE' }); renderNewsletterList(); } catch (err) { alert('حذف ناموفق بود: ' + err.message); } }); } catch (err) { console.error(err); $('#list').innerHTML = `

    خطا در بارگذاری.

    `; } } // ═══════════════════════════════════════════════ // EVENTS // ═══════════════════════════════════════════════ async function renderEventsList() { root.innerHTML = ` ${topbarHtml('events', '+ رویداد جدید')}

    رویدادها

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderEventEditor(null)); try { const items = await api('/api/events?limit=200'); if (!items.length) { $('#list').innerHTML = `

    هنوز رویدادی ثبت نشده.

    `; return; } $('#list').innerHTML = items.map(e => `
    ${e.type ? `${escapeHtml(EVENT_TYPES.find(t=>t.value===e.type)?.label||e.type)}` : ''} ${e.cityFa ? `${escapeHtml(e.cityFa)}` : ''} ${e.dateFa ? `${escapeHtml(e.dateFa)}` : ''} ${e.registrationOpen?'ثبت‌نام باز':'بسته'}

    ${escapeHtml(e.titleFa)}

    ${escapeHtml(e.descriptionFa||'')}

    `).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderEventEditor(await api(`/api/events/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این رویداد حذف شود؟')) return; await api(`/api/events/${id}`, { method: 'DELETE' }); renderEventsList(); } }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderEventEditor(event) { const e = event || { titleFa:'', titleEn:'', dateFa:'', dateEn:'', monthFa:'', monthEn:'', year:'', type:'', locationFa:'', locationEn:'', cityFa:'', cityEn:'', descriptionFa:'', descriptionEn:'', registrationOpen:true, sortOrder:0 }; const isEdit = !!event; root.innerHTML = `
    ${isEdit?'ویرایش رویداد':'رویداد جدید'}
    `; $('#backBtn').addEventListener('click', () => renderEventsList()); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const payload = { titleFa: fd.get('titleFa'), titleEn: fd.get('titleEn')||null, dateFa: fd.get('dateFa')||null, dateEn: fd.get('dateEn')||null, monthFa: fd.get('monthFa')||null, monthEn: fd.get('monthEn')||null, year: fd.get('year')||null, type: fd.get('type')||null, locationFa: fd.get('locationFa')||null, locationEn: fd.get('locationEn')||null, cityFa: fd.get('cityFa')||null, cityEn: fd.get('cityEn')||null, descriptionFa: fd.get('descriptionFa')||null, descriptionEn: fd.get('descriptionEn')||null, registrationOpen: fd.get('registrationOpen') === 'on', sortOrder: Number(fd.get('sortOrder')||0), }; try { if (isEdit) await api(`/api/events/${event.id}`, { method:'PUT', body:payload }); else await api('/api/events', { method:'POST', body:payload }); renderEventsList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ═══════════════════════════════════════════════ // TEAM & EXPERTS (shared UI, isExpert flag) // ═══════════════════════════════════════════════ async function renderTeamList(isExpert) { const tab = isExpert ? 'experts' : 'team'; const label = isExpert ? 'خبرگان' : 'تیم'; root.innerHTML = ` ${topbarHtml(tab, `+ ${isExpert?'خبره':'عضو'} جدید`)}

    ${isExpert?'شبکه خبرگان':'اعضای تیم'}

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderTeamEditor(null, isExpert)); try { const items = await api(`/api/team?expert=${isExpert?1:0}`); if (!items.length) { $('#list').innerHTML = `

    هنوز ${label}ی ثبت نشده.

    `; return; } $('#list').innerHTML = items.map(m => `
    ${escapeHtml(m.initial||m.nameFa?.charAt(0)||'؟')}
    ${m.roleFa ? `${escapeHtml(m.roleFa)}` : ''} ${m.expertRoomFa ? `${escapeHtml(m.expertRoomFa)}` : ''} ${m.reportCount ? `${m.reportCount} گزارش` : ''}

    ${escapeHtml(m.nameFa)}

    ${escapeHtml(m.bioFa||'')}

    `).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderTeamEditor(await api(`/api/team/${id}`), isExpert); else if (btn.dataset.action === 'delete') { if (!confirm('این عضو حذف شود؟')) return; await api(`/api/team/${id}`, { method:'DELETE' }); renderTeamList(isExpert); } }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderTeamEditor(member, isExpert) { const m = member || { nameFa:'', nameEn:'', roleFa:'', roleEn:'', bioFa:'', bioEn:'', expertise:[], email:'', initial:'', photo:'', reportCount:0, expertRoomFa:'', expertRoomEn:'', telegram:'', linkedin:'', twitter:'', website:'', sortOrder:0 }; const isEdit = !!member; const expertiseStr = Array.isArray(m.expertise) ? m.expertise.join('، ') : ''; root.innerHTML = `
    ${isEdit?'ویرایش':'جدید'} — ${isExpert?'خبره':'عضو تیم'}
    ${isExpert ? ` ` : ''}
    `; $('#backBtn').addEventListener('click', () => renderTeamList(isExpert)); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const expertiseRaw = String(fd.get('expertise')||'').split(/[,،]/).map(s=>s.trim()).filter(Boolean); const payload = { nameFa: fd.get('nameFa'), nameEn: fd.get('nameEn')||null, roleFa: fd.get('roleFa')||null, roleEn: fd.get('roleEn')||null, bioFa: fd.get('bioFa')||null, bioEn: fd.get('bioEn')||null, expertise: expertiseRaw, email: fd.get('email')||null, initial: fd.get('initial')||null, reportCount: Number(fd.get('reportCount')||0), isExpert: isExpert, expertRoomFa: fd.get('expertRoomFa')||null, expertRoomEn: fd.get('expertRoomEn')||null, telegram: fd.get('telegram')||null, linkedin: fd.get('linkedin')||null, twitter: fd.get('twitter')||null, website: fd.get('website')||null, sortOrder: Number(fd.get('sortOrder')||0), }; try { if (isEdit) await api(`/api/team/${member.id}`, { method:'PUT', body:payload }); else await api('/api/team', { method:'POST', body:payload }); renderTeamList(isExpert); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ═══════════════════════════════════════════════ // PLANS // ═══════════════════════════════════════════════ async function renderPlansList() { root.innerHTML = ` ${topbarHtml('plans', '+ پلان جدید')}

    پلان‌های اشتراک

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderPlanEditor(null)); try { const items = await api('/api/plans'); if (!items.length) { $('#list').innerHTML = `

    هنوز پلانی ثبت نشده.

    `; return; } $('#list').innerHTML = items.map(p => `
    ${p.isFeatured ? 'ویژه' : ''} ${p.badgeFa ? `${escapeHtml(p.badgeFa)}` : ''} ${Number(p.price).toLocaleString()} ریال / ${escapeHtml(p.periodFa||'')}

    ${escapeHtml(p.nameFa)}

    ${Array.isArray(p.features) ? p.features.length : 0} ویژگی

    `).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderPlanEditor(await api(`/api/plans/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این پلان حذف شود؟')) return; await api(`/api/plans/${id}`, { method:'DELETE' }); renderPlansList(); } }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderPlanEditor(plan) { const p = plan || { nameFa:'', nameEn:'', price:0, periodFa:'', periodEn:'', features:[], badgeFa:'', badgeEn:'', isFeatured:false, ctaFa:'', ctaEn:'', sortOrder:0 }; const isEdit = !!plan; const featStr = Array.isArray(p.features) ? p.features.map(f => typeof f === 'object' ? f.fa||'' : f).join('\n') : ''; root.innerHTML = `
    ${isEdit?'ویرایش پلان':'پلان جدید'}
    `; $('#backBtn').addEventListener('click', () => renderPlansList()); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const featLines = String(fd.get('features')||'').split('\n').map(s=>s.trim()).filter(Boolean); const payload = { nameFa: fd.get('nameFa'), nameEn: fd.get('nameEn')||null, price: Number(fd.get('price')||0), periodFa: fd.get('periodFa')||null, periodEn: fd.get('periodEn')||null, features: featLines, badgeFa: fd.get('badgeFa')||null, badgeEn: fd.get('badgeEn')||null, isFeatured: fd.get('isFeatured') === 'on', ctaFa: fd.get('ctaFa')||null, ctaEn: fd.get('ctaEn')||null, sortOrder: Number(fd.get('sortOrder')||0), }; try { if (isEdit) await api(`/api/plans/${plan.id}`, { method:'PUT', body:payload }); else await api('/api/plans', { method:'POST', body:payload }); renderPlansList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ═══════════════════════════════════════════════ // RADAR (رادار آینده) // ═══════════════════════════════════════════════ // section pages on the site → which raw categories feed them const RADAR_PAGE_SLUGS = [ { slug: 'market', label: 'بازار و زنجیره فولاد', cats: ['market','commodity'] }, { slug: 'sustainability', label: 'پایداری و فولاد سبز', cats: ['energy','commodity'] }, { slug: 'geopolitics', label: 'ژئوپلیتیک و اقتصاد جهانی', cats: ['geo','market'] }, { slug: 'technology', label: 'فناوری و نوآوری', cats: ['tech'] }, ]; function sectionLabelsForCategory(cat) { return RADAR_PAGE_SLUGS.filter(s => s.cats.includes(cat)).map(s => s.label); } async function renderRadarList() { root.innerHTML = ` ${topbarHtml('glance', '+ در یک نگاه جدید')}

    در یک نگاه

    محتوای صفحهٔ «در یک نگاه» سایت (‎/at-a-glance‎) — به تفکیک دسته.

    ${RADAR_CATEGORIES.map(c => ``).join('')}

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderRadarEditor(null)); let allItems = []; let activeCat = ''; const safeImgUrl = (u) => /^(\/uploads\/[a-zA-Z0-9._-]+|\/[a-zA-Z0-9._\/-]+)$/.test(String(u || '')) ? u : ''; const catLabel = (v) => (RADAR_CATEGORIES.find(c => c.value === v) || {}).label || v; function renderItems() { const filtered = activeCat ? allItems.filter(r => r.category === activeCat) : allItems; if (!filtered.length) { $('#list').innerHTML = `

    مطلبی در این دسته یافت نشد.

    `; return; } $('#list').innerHTML = filtered.map(r => { return `
    ${r.status === 'draft' ? `پیش‌نویس` : ''} ${r.category ? `${escapeHtml(catLabel(r.category))}` : ''} ${r.dateFa ? `${escapeHtml(r.dateFa)}` : ''}

    ${escapeHtml(r.titleFa||'')}

    ${escapeHtml(r.excerptFa||'')}

    ${r.status === 'draft' ? `` : ''}
    `; }).join(''); } $('#section-bar').addEventListener('click', e => { const btn = e.target.closest('.cat-btn'); if (!btn) return; activeCat = btn.dataset.cat; document.querySelectorAll('#section-bar .cat-btn').forEach(b => b.classList.toggle('active', b === btn)); renderItems(); }); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderRadarEditor(await api(`/api/radar/${id}`)); else if (btn.dataset.action === 'publish') { await api(`/api/radar/${id}/publish`, { method: 'POST' }); renderRadarList(); } else if (btn.dataset.action === 'delete') { if (!confirm('این مطلب حذف شود؟')) return; await api(`/api/radar/${id}`, { method: 'DELETE' }); renderRadarList(); } }); try { allItems = await api('/api/admin/radar?limit=500'); if (!allItems.length) { $('#list').innerHTML = `

    هنوز مطلبی ثبت نشده.

    `; return; } renderItems(); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderRadarEditor(item) { const r = item || { category:'', img:'', authorName:'', authorAvatar:'', titleFa:'', dateFa:'', excerptFa:'', bodyFa:'', analysisFa:'', titleEn:'', dateEn:'', excerptEn:'', bodyEn:'', analysisEn:'', tags:[], source:'', sourceUrl:'', sortOrder:0 }; const isEdit = !!item; function sectionBadges(cat) { const labels = sectionLabelsForCategory(cat); if (!labels.length) return ''; return labels.map(l => `${escapeHtml(l)}`).join(' '); } root.innerHTML = `
    📌 ${isEdit?'ویرایش':'مطلب جدید'} — در یک نگاه
    نمایش در بخش‌های سایت: ${sectionBadges(r.category)}
    فارسی
    English
    `; $('#backBtn').addEventListener('click', () => renderRadarList()); document.getElementById('catSelect').addEventListener('change', e => { const cat = e.target.value; const badgesEl = document.getElementById('sectionBadges'); badgesEl.innerHTML = `نمایش در بخش‌های سایت: ${sectionBadges(cat)}`; }); document.getElementById('radarCoverFile').addEventListener('change', async e => { const file = e.target.files?.[0]; if (!file) return; const fd = new FormData(); fd.append('file', file); try { const { url } = await api('/api/uploads', { method: 'POST', body: fd, isForm: true }); $('input[name="img"]').value = url; const prev = document.getElementById('radarCoverPreview'); prev.src = url; prev.style.display = ''; } catch (err) { alert('آپلود ناموفق: ' + err.message); } }); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const payload = { titleFa: fd.get('titleFa'), titleEn: fd.get('titleEn')||null, category: fd.get('category')||null, img: fd.get('img')||null, authorName: fd.get('authorName')||null, authorAvatar: r.authorAvatar || null, dateFa: fd.get('dateFa')||null, dateEn: fd.get('dateEn')||null, excerptFa: fd.get('excerptFa')||null, excerptEn: fd.get('excerptEn')||null, bodyFa: fd.get('bodyFa')||null, bodyEn: fd.get('bodyEn')||null, analysisFa: fd.get('analysisFa')||null, analysisEn: fd.get('analysisEn')||null, source: fd.get('source')||null, sourceUrl: fd.get('sourceUrl')||null, tags: String(fd.get('tags')||'').split(/[,،]/).map(s=>s.trim()).filter(Boolean), sortOrder: Number(fd.get('sortOrder')||0), }; try { if (isEdit) await api(`/api/radar/${item.id}`, { method:'PUT', body:payload }); else await api('/api/radar', { method:'POST', body:payload }); renderRadarList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } async function renderRadarPagesList() { root.innerHTML = ` ${topbarHtml('radarPages', '')}

    صفحات رادار آینده

    ۵ صفحه ثابت. محتوای هیرو (کارت ویژه) و بنر هر دسته را اینجا ویرایش کنید.

    در حال بارگذاری…

    `; wireTabs(); const newBtn = $('#newBtn'); if (newBtn) newBtn.style.display = 'none'; try { const saved = await api('/api/radar-pages'); const bySlug = {}; saved.forEach(p => { bySlug[p.slug] = p; }); $('#list').innerHTML = RADAR_PAGE_SLUGS.map(s => { const p = bySlug[s.slug]; return `
    ${escapeHtml(s.slug)} ${p ? 'ذخیره‌شده' : 'پیش‌فرض (هنوز ذخیره نشده)'}

    ${escapeHtml((p && p.labelFa) || s.label)}

    ${escapeHtml((p && p.bannerTitleFa) || '')}

    `; }).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const slug = btn.closest('.row').dataset.slug; const def = RADAR_PAGE_SLUGS.find(x => x.slug === slug); renderRadarPageEditor(bySlug[slug] || { slug, labelFa: def.label }, def); }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderRadarPageEditor(p, def) { const r = Object.assign({ slug: def.slug, sourceCategories: [], labelFa: def.label, labelEn: '', latestHeadingFa: '', latestHeadingEn: '', featuredTitleFa: '', featuredTitleEn: '', featuredDateFa: '', featuredDateEn: '', featuredImg: '', bannerKickerFa: 'تحلیل ویژه', bannerKickerEn: 'Special Analysis', bannerTitleFa: '', bannerTitleEn: '', bannerDescFa: '', bannerDescEn: '', bannerBookImg: '', sortOrder: 0, }, p || {}); const catChecked = (c) => (r.sourceCategories || []).includes(c) ? 'checked' : ''; root.innerHTML = `
    ویرایش صفحه: ${escapeHtml(r.labelFa)}

    کارت ویژه (هیرو)

    بنر تحلیل ویژه

    `; $('#backBtn').addEventListener('click', () => renderRadarPagesList()); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const payload = { sourceCategories: fd.getAll('srcCat'), labelFa: fd.get('labelFa'), labelEn: fd.get('labelEn')||null, latestHeadingFa: fd.get('latestHeadingFa')||null, latestHeadingEn: fd.get('latestHeadingEn')||null, featuredTitleFa: fd.get('featuredTitleFa')||null, featuredTitleEn: fd.get('featuredTitleEn')||null, featuredDateFa: fd.get('featuredDateFa')||null, featuredDateEn: fd.get('featuredDateEn')||null, featuredImg: fd.get('featuredImg')||null, bannerKickerFa: fd.get('bannerKickerFa')||null, bannerKickerEn: fd.get('bannerKickerEn')||null, bannerTitleFa: fd.get('bannerTitleFa')||null, bannerTitleEn: fd.get('bannerTitleEn')||null, bannerDescFa: fd.get('bannerDescFa')||null, bannerDescEn: fd.get('bannerDescEn')||null, bannerBookImg: fd.get('bannerBookImg')||null, sortOrder: Number(fd.get('sortOrder')||0), }; try { await api(`/api/radar-pages/${r.slug}`, { method:'PUT', body:payload }); renderRadarPagesList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ═══════════════════════════════════════════════ // FACTORY REPORTS (گزارش کارخانه) // ═══════════════════════════════════════════════ async function renderFactoryReportsList() { root.innerHTML = ` ${topbarHtml('factoryReports', '+ گزارش جدید')}

    گزارش کارخانه

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderFactoryReportEditor(null)); try { const items = await api('/api/factory-reports?limit=50'); if (!items.length) { $('#list').innerHTML = `

    هنوز گزارشی ثبت نشده.

    `; return; } $('#list').innerHTML = items.map(r => `
    ${r.tagFa ? `${escapeHtml(r.tagFa)}` : ''} ${r.dateFa ? `${escapeHtml(r.dateFa)}` : ''} ${escapeHtml(SCROLL_DIRS.find(d=>d.value===r.scrollDir)?.label||r.scrollDir||'')}

    ${escapeHtml(r.titleFa)}

    ${escapeHtml(r.excerptFa||'')}

    `).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderFactoryReportEditor(await api(`/api/factory-reports/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این گزارش حذف شود؟')) return; await api(`/api/factory-reports/${id}`, { method: 'DELETE' }); renderFactoryReportsList(); } }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderFactoryReportEditor(report) { const r = report || { img:'', tagFa:'', tagEn:'', titleFa:'', titleEn:'', dateFa:'', dateEn:'', excerptFa:'', excerptEn:'', fileUrl:'', scrollDir:'up', sortOrder:0 }; const isEdit = !!report; root.innerHTML = `
    ${isEdit?'ویرایش گزارش':'گزارش جدید'}
    `; $('#backBtn').addEventListener('click', () => renderFactoryReportsList()); document.getElementById('factoryCoverFile').addEventListener('change', async e => { const file = e.target.files?.[0]; if (!file) return; const fd = new FormData(); fd.append('file', file); try { const { url } = await api('/api/uploads', { method:'POST', body:fd, isForm:true }); $('input[name="img"]').value = url; const prev = document.getElementById('factoryCoverPreview'); if (prev) { prev.src = url; prev.style.display = ''; } } catch (err) { alert('آپلود تصویر ناموفق: ' + err.message); } }); document.getElementById('factoryFile').addEventListener('change', async e => { const file = e.target.files?.[0]; if (!file) return; const status = document.getElementById('factoryFileStatus'); if (status) status.textContent = 'در حال آپلود فایل…'; const fd = new FormData(); fd.append('file', file); try { const { url } = await api('/api/uploads/raw', { method:'POST', body:fd, isForm:true }); $('input[name="fileUrl"]').value = url; if (status) status.textContent = '✓ آپلود شد'; } catch (err) { if (status) status.textContent = ''; alert('آپلود ناموفق: ' + err.message); } }); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const payload = { img: fd.get('img')||null, tagFa: fd.get('tagFa')||null, tagEn: fd.get('tagEn')||null, titleFa: fd.get('titleFa'), titleEn: fd.get('titleEn')||null, dateFa: fd.get('dateFa')||null, dateEn: fd.get('dateEn')||null, excerptFa: fd.get('excerptFa')||null, excerptEn: fd.get('excerptEn')||null, fileUrl: fd.get('fileUrl')||null, scrollDir: fd.get('scrollDir')||'up', sortOrder: Number(fd.get('sortOrder')||0), }; try { if (isEdit) await api(`/api/factory-reports/${report.id}`, { method:'PUT', body:payload }); else await api('/api/factory-reports', { method:'POST', body:payload }); renderFactoryReportsList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ═══════════════════════════════════════════════ // INTEGRATIONS (منابع داده) // ═══════════════════════════════════════════════ async function renderIntegrationsList() { root.innerHTML = ` ${topbarHtml('integrations', '+ منبع جدید')}

    منابع داده

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderIntegrationEditor(null)); try { const items = await api('/api/integrations?limit=50'); if (!items.length) { $('#list').innerHTML = `

    هنوز منبعی ثبت نشده.

    `; return; } $('#list').innerHTML = items.map(it => `
    ${it.icon ? `${escapeHtml(INTEGRATION_ICONS.find(i=>i.value===it.icon)?.label||it.icon)}` : ''} سلول ${it.gridCol}×${it.gridRow} ${it.accent ? ` ` : ''}

    ${escapeHtml(it.name)}

    ${it.logo ? `

    ${escapeHtml(it.logo)}

    ` : ''}
    `).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderIntegrationEditor(await api(`/api/integrations/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این منبع حذف شود؟')) return; await api(`/api/integrations/${id}`, { method: 'DELETE' }); renderIntegrationsList(); } }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderIntegrationEditor(integration) { const it = integration || { name:'', icon:'globe', logo:'', accent:'', gridCol:1, gridRow:1, sortOrder:0 }; const isEdit = !!integration; root.innerHTML = `
    ${isEdit?'ویرایش منبع':'منبع جدید'}
    `; $('#backBtn').addEventListener('click', () => renderIntegrationsList()); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const payload = { name: fd.get('name'), icon: fd.get('icon')||'globe', logo: fd.get('logo')||null, accent: fd.get('accent')||null, gridCol: Number(fd.get('gridCol')||1), gridRow: Number(fd.get('gridRow')||1), sortOrder: Number(fd.get('sortOrder')||0), }; try { if (isEdit) await api(`/api/integrations/${integration.id}`, { method:'PUT', body:payload }); else await api('/api/integrations', { method:'POST', body:payload }); renderIntegrationsList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ═══════════════════════════════════════════════ // INSTITUTE STATS (آمار) // ═══════════════════════════════════════════════ async function renderStatsList() { root.innerHTML = ` ${topbarHtml('stats', '+ آمار جدید')}

    آمار اندیشکده

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderStatEditor(null)); try { const items = await api('/api/institute-stats'); if (!items.length) { $('#list').innerHTML = `

    هنوز آماری ثبت نشده.

    `; return; } $('#list').innerHTML = items.map(s => `
    ${s.value}${s.suffixFa ? escapeHtml(s.suffixFa) : ''}

    ${escapeHtml(s.labelFa)}

    ${s.labelEn ? `

    ${escapeHtml(s.labelEn)}

    ` : ''}
    `).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderStatEditor(await api(`/api/institute-stats/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این آمار حذف شود؟')) return; await api(`/api/institute-stats/${id}`, { method: 'DELETE' }); renderStatsList(); } }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderStatEditor(stat) { const s = stat || { id: '', labelFa: '', labelEn: '', value: 0, suffixFa: '', sortOrder: 0 }; const isEdit = !!stat; root.innerHTML = `
    ${isEdit ? 'ویرایش آمار' : 'آمار جدید'}
    `; $('#backBtn').addEventListener('click', () => renderStatsList()); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const payload = { id: fd.get('id'), labelFa: fd.get('labelFa'), labelEn: fd.get('labelEn') || null, value: Number(fd.get('value') || 0), suffixFa: fd.get('suffixFa') || null, sortOrder: Number(fd.get('sortOrder') || 0), }; try { if (isEdit) await api(`/api/institute-stats/${stat.id}`, { method: 'PUT', body: payload }); else await api('/api/institute-stats', { method: 'POST', body: payload }); renderStatsList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ═══════════════════════════════════════════════ // MARKET PRICES (قیمت‌ها) // ═══════════════════════════════════════════════ async function renderMarketPricesList() { root.innerHTML = ` ${topbarHtml('mprices', '+ قیمت جدید')}

    قیمت‌های بازار

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderMarketPriceEditor(null)); try { const items = await api('/api/market-prices'); if (!items.length) { $('#list').innerHTML = `

    هنوز قیمتی ثبت نشده.

    `; return; } const trendLabel = (t) => (TREND_TYPES.find(x => x.value === t)?.label) || t; $('#list').innerHTML = items.map(p => `
    ${p.value} ${escapeHtml(p.unit || '')} ${escapeHtml(trendLabel(p.trend))} ${p.changePercent}%

    ${escapeHtml(p.name)}

    `).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderMarketPriceEditor(await api(`/api/market-prices/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این قیمت حذف شود؟')) return; await api(`/api/market-prices/${id}`, { method: 'DELETE' }); renderMarketPricesList(); } }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderMarketPriceEditor(price) { const p = price || { id: '', name: '', value: 0, unit: '', change: 0, changePercent: 0, trend: 'flat', sortOrder: 0 }; const isEdit = !!price; root.innerHTML = `
    ${isEdit ? 'ویرایش قیمت' : 'قیمت جدید'}
    `; $('#backBtn').addEventListener('click', () => renderMarketPricesList()); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const payload = { id: fd.get('id'), name: fd.get('name'), value: Number(fd.get('value') || 0), unit: fd.get('unit') || null, change: Number(fd.get('change') || 0), changePercent: Number(fd.get('changePercent') || 0), trend: fd.get('trend'), sortOrder: Number(fd.get('sortOrder') || 0), }; try { if (isEdit) await api(`/api/market-prices/${price.id}`, { method: 'PUT', body: payload }); else await api('/api/market-prices', { method: 'POST', body: payload }); renderMarketPricesList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ═══════════════════════════════════════════════ // MARKET CHART POINTS (نمودارها) // ═══════════════════════════════════════════════ async function renderChartPointsList() { root.innerHTML = ` ${topbarHtml('charts', '+ نقطه جدید')}

    نقاط نمودار بازار

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderChartPointEditor(null)); try { const items = await api('/api/market-chart-points?limit=500'); if (!items.length) { $('#list').innerHTML = `

    هنوز نقطه‌ای ثبت نشده.

    `; return; } const seriesLabel = (s) => (CHART_SERIES.find(x => x.value === s)?.label) || s; $('#list').innerHTML = items.map(c => `
    ${escapeHtml(seriesLabel(c.series))} ${c.value} ${c.meta ? `${escapeHtml(c.meta)}` : ''}

    ${escapeHtml(c.label)}

    `).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderChartPointEditor(await api(`/api/market-chart-points/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این نقطه حذف شود؟')) return; await api(`/api/market-chart-points/${id}`, { method: 'DELETE' }); renderChartPointsList(); } }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderChartPointEditor(point) { const c = point || { id: '', series: 'history', label: '', value: 0, meta: '', sortOrder: 0 }; const isEdit = !!point; root.innerHTML = `
    ${isEdit ? 'ویرایش نقطه نمودار' : 'نقطه نمودار جدید'}
    `; $('#backBtn').addEventListener('click', () => renderChartPointsList()); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const payload = { series: fd.get('series'), label: fd.get('label'), value: Number(fd.get('value') || 0), meta: fd.get('meta') || null, sortOrder: Number(fd.get('sortOrder') || 0), }; try { if (isEdit) await api(`/api/market-chart-points/${point.id}`, { method: 'PUT', body: payload }); else await api('/api/market-chart-points', { method: 'POST', body: payload }); renderChartPointsList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ═══════════════════════════════════════════════ // VISION ITEMS (چشم‌انداز — About) // ═══════════════════════════════════════════════ async function renderVisionList() { root.innerHTML = ` ${topbarHtml('vision', '+ آیتم جدید')}

    چشم‌انداز

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderVisionEditor(null)); try { const items = await api('/api/vision-items?limit=200'); if (!items.length) { $('#list').innerHTML = `

    هنوز آیتمی ثبت نشده.

    `; return; } $('#list').innerHTML = items.map(v => `
    ${v.icon ? `${escapeHtml(v.icon)}` : ''} ${v.titleEn ? `${escapeHtml(v.titleEn)}` : ''}

    ${escapeHtml(v.titleFa)}

    ${escapeHtml(v.descFa||'')}

    `).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderVisionEditor(await api(`/api/vision-items/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این آیتم حذف شود؟')) return; await api(`/api/vision-items/${id}`, { method: 'DELETE' }); renderVisionList(); } }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderVisionEditor(item) { const v = item || { icon:'', titleFa:'', titleEn:'', descFa:'', descEn:'', sortOrder:0 }; const isEdit = !!item; root.innerHTML = `
    ${isEdit?'ویرایش آیتم':'آیتم جدید'}
    `; $('#backBtn').addEventListener('click', () => renderVisionList()); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const payload = { icon: fd.get('icon')||null, titleFa: fd.get('titleFa'), titleEn: fd.get('titleEn')||null, descFa: fd.get('descFa')||null, descEn: fd.get('descEn')||null, sortOrder: Number(fd.get('sortOrder')||0), }; try { if (isEdit) await api(`/api/vision-items/${item.id}`, { method:'PUT', body:payload }); else await api('/api/vision-items', { method:'POST', body:payload }); renderVisionList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ═══════════════════════════════════════════════ // ADVISORY BOARD (شورای مشورتی — About) // ═══════════════════════════════════════════════ async function renderAdvisoryList() { root.innerHTML = ` ${topbarHtml('advisory', '+ عضو جدید')}

    شورای مشورتی

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderAdvisoryEditor(null)); try { const items = await api('/api/advisory-board?limit=200'); if (!items.length) { $('#list').innerHTML = `

    هنوز عضوی ثبت نشده.

    `; return; } $('#list').innerHTML = items.map(m => `
    ${escapeHtml(m.nameFa?.charAt(0)||'؟')}
    ${m.roleFa ? `${escapeHtml(m.roleFa)}` : ''} ${m.nameEn ? `${escapeHtml(m.nameEn)}` : ''}

    ${escapeHtml(m.nameFa)}

    `).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderAdvisoryEditor(await api(`/api/advisory-board/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این عضو حذف شود؟')) return; await api(`/api/advisory-board/${id}`, { method: 'DELETE' }); renderAdvisoryList(); } }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderAdvisoryEditor(member) { const m = member || { nameFa:'', nameEn:'', roleFa:'', roleEn:'', sortOrder:0 }; const isEdit = !!member; root.innerHTML = `
    ${isEdit?'ویرایش عضو':'عضو جدید'}
    `; $('#backBtn').addEventListener('click', () => renderAdvisoryList()); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const payload = { nameFa: fd.get('nameFa'), nameEn: fd.get('nameEn')||null, roleFa: fd.get('roleFa')||null, roleEn: fd.get('roleEn')||null, sortOrder: Number(fd.get('sortOrder')||0), }; try { if (isEdit) await api(`/api/advisory-board/${member.id}`, { method:'PUT', body:payload }); else await api('/api/advisory-board', { method:'POST', body:payload }); renderAdvisoryList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ═══════════════════════════════════════════════ // BANNERS // ═══════════════════════════════════════════════ const POSITIONS = [ { value: 'featured', label: 'صفحه اصلی (زیر رویدادها)' }, { value: 'glance', label: 'صفحه در یک نگاه (پایین)' }, { value: 'media-split', label: 'صفحه رسانه (بین ویدیو و پادکست)' }, { value: 'radar-sustainability', label: 'صفحه رادار: پایداری و فولاد سبز' }, { value: 'radar-geopolitics', label: 'صفحه رادار: ژئوپلیتیک و اقتصاد جهانی' }, { value: 'radar-technology', label: 'صفحه رادار: فناوری و نوآوری' }, { value: 'radar-market', label: 'صفحه رادار: بازار و زنجیره فولاد' }, ]; async function renderBannersList() { root.innerHTML = ` ${topbarHtml('banners', '+ بنر جدید')}

    بنرها

    در حال بارگذاری…

    `; wireTabs(); $('#newBtn').addEventListener('click', () => renderBannerEditor(null)); try { const items = await api('/api/banners'); if (!items.length) { $('#list').innerHTML = `

    هنوز بنری ثبت نشده.

    `; return; } $('#list').innerHTML = items.map(b => `
    ${b.image ? `
    ` : ''}
    ${b.active ? 'فعال' : 'غیرفعال'} ${escapeHtml(POSITIONS.find(p=>p.value===b.position)?.label || b.position)}

    ${escapeHtml(b.titleFa)}

    ${escapeHtml(b.subtitleFa||'')}

    `).join(''); $('#list').addEventListener('click', async ev => { const btn = ev.target.closest('button[data-action]'); if (!btn) return; const id = btn.closest('.row').dataset.id; if (btn.dataset.action === 'edit') renderBannerEditor(await api(`/api/banners/${id}`)); else if (btn.dataset.action === 'delete') { if (!confirm('این بنر حذف شود؟')) return; await api(`/api/banners/${id}`, { method: 'DELETE' }); renderBannersList(); } }); } catch (err) { $('#list').innerHTML = `

    خطا: ${escapeHtml(err.message)}

    `; } } function renderBannerEditor(banner) { const b = banner || { overlineFa:'', overlineEn:'', titleFa:'', titleEn:'', subtitleFa:'', subtitleEn:'', ctaLabelFa:'', ctaLabelEn:'', ctaUrl:'', image:'', position:'featured', active:true, sortOrder:0 }; const isEdit = !!banner; root.innerHTML = `
    ${isEdit ? 'ویرایش بنر' : 'بنر جدید'}
    ${b.image ? `` : ''}
    `; $('#backBtn').addEventListener('click', () => renderBannersList()); $('#pdfFile').addEventListener('change', async ev => { const file = ev.target.files[0]; if (!file) return; const fd = new FormData(); fd.append('file', file); try { const res = await api('/api/uploads/raw', { method:'POST', body:fd, isForm:true }); $('#ctaUrl').value = res.url; } catch (err) { alert('آپلود PDF ناموفق: ' + err.message); } }); $('#imageFile').addEventListener('change', async ev => { const file = ev.target.files[0]; if (!file) return; const fd = new FormData(); fd.append('file', file); try { const res = await api('/api/uploads', { method:'POST', body:fd, isForm:true }); $('#imageUrl').value = res.url; } catch (err) { alert('آپلود ناموفق: ' + err.message); } }); $('#editorForm').addEventListener('submit', async ev => { ev.preventDefault(); const fd = new FormData(ev.target); const payload = { overlineFa: fd.get('overlineFa') || null, overlineEn: fd.get('overlineEn') || null, titleFa: fd.get('titleFa'), titleEn: fd.get('titleEn') || null, subtitleFa: fd.get('subtitleFa') || null, subtitleEn: fd.get('subtitleEn') || null, ctaLabelFa: fd.get('ctaLabelFa') || null, ctaLabelEn: fd.get('ctaLabelEn') || null, ctaUrl: fd.get('ctaUrl') || null, image: fd.get('imageUrl') || null, position: fd.get('position') || 'featured', active: fd.get('active') === 'on', sortOrder: Number(fd.get('sortOrder') || 0), }; try { if (isEdit) await api(`/api/banners/${banner.id}`, { method:'PUT', body:payload }); else await api('/api/banners', { method:'POST', body:payload }); renderBannersList(); } catch (err) { alert('ذخیره ناموفق: ' + err.message); } }); } // ───────────────────────────────────────────── function renderImport() { root.innerHTML = ` ${topbarHtml('import', '')}

    ⬇️ ایمپورت از Gemini

    خروجی JSON که Gemini داد رو اینجا paste کن و دکمه ایمپورت رو بزن. گزارش توی مطالب و آیتم‌های رادار توی رادار ذخیره می‌شن.

    `; setupTopbar(); const jsonTA = document.getElementById('import-json'); const statusEl = document.getElementById('import-status'); const resultEl = document.getElementById('import-result'); const countEl = document.getElementById('import-count'); jsonTA.addEventListener('input', () => { try { const d = JSON.parse(jsonTA.value); const ra = d.radar_items?.length || 0; countEl.textContent = `✓ JSON معتبر — گزارش: ${d.report?.title ? '1' : '0'} | رادار: ${ra}`; countEl.style.color = '#22c55e'; } catch { countEl.textContent = 'JSON نامعتبر'; countEl.style.color = '#ef4444'; } }); document.getElementById('import-clear').onclick = () => { jsonTA.value = ''; countEl.textContent = ''; resultEl.innerHTML = ''; }; document.getElementById('import-btn').onclick = async () => { let data; try { data = JSON.parse(jsonTA.value); } catch { statusEl.innerHTML = '
    JSON نامعتبر است
    '; return; } const btn = document.getElementById('import-btn'); btn.disabled = true; btn.textContent = 'در حال ایمپورت...'; statusEl.innerHTML = ''; try { const result = await api('/api/admin/import', { method:'POST', body: data }); resultEl.innerHTML = `
    ✓ ایمپورت موفق
    📄 گزارش: ${result.article}
    ${result.radar.length ? `
    🔭 آیتم‌های رادار: ${result.radar.length} مورد اضافه شد
    ` : ''}
    مشاهده در مطالب ← ${result.radar.length ? `مشاهده در رادار ←` : ''}
    `; jsonTA.value = ''; countEl.textContent = ''; } catch (err) { statusEl.innerHTML = `
    خطا: ${err.message}
    `; } finally { btn.disabled = false; btn.textContent = 'ایمپورت'; } }; } // ───────────────────────────────────────────── api('/api/auth/me').then(() => renderRadarList()).catch(() => renderLogin());