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 = `
`;
$('#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 => `
${g.label}
`).join('');
const subTabs = activeGroup.tabs.map(t => `
${TAB_LABELS[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 = `
تولید محتوا
بستن
نوع محتوا
در یک نگاه
رادار آینده
دسته
منابع و لینکها
در حال بارگذاری منابع...
ذخیره منابع
برای رادار آینده، انتخاب «همه» هر ۴ زیرسرفصل را draft میکند.
شروع تولید
`;
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 => `${escapeHtml(c.label)} `).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 => `${escapeHtml(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 => `${escapeHtml(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 => `
${ct.icon}
${ct.label}
${ct.desc}
`).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 = `
`;
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 ``;
}
return `
${ctrls}
${escapeHtml(b.value || '')}
`;
}).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 = `
عنوان / منبع
سطح
${RISK_LEVELS.map(l => `${l.label} `).join('')}
تاریخ / افق (شمسی)
ترتیب نمایش (عدد کوچکتر اول)
متن سیگنال
${escapeHtml(r.quote)}
${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 ? '(برای تغییر پر کنید، خالی = بدون تغییر)' : '(حداقل ۸ کاراکتر)'}
${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} خرید` : ''}
جزئیات
${m.isActive ? 'مسدود کردن' : 'رفع مسدودی'}
حذف
`).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})
فعالیت اخیر
${m.isActive ? 'مسدود کردن' : 'رفع مسدودی'}
حذف کاربر
`;
$('#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 = `` +
acts.map(a => `
${a.kind === 'click' ? 'کلیک' : 'بازدید'}
${a.kind === 'click' && a.label ? `«${escapeHtml(a.label)}» در ` : ''}${escapeHtml(a.path || '')}
— ${fmtDateTime(a.at)}
`).join('') + ` `;
}
} 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 = `
عنوان (فارسی)
عنوان (انگلیسی)
نوع
—
${EVENT_TYPES.map(t=>`${t.label} `).join('')}
ثبتنام باز است
تاریخ (فارسی)
تاریخ (انگلیسی)
ماه (فارسی)
ماه (انگلیسی)
سال
ترتیب نمایش
شهر (فارسی)
شهر (انگلیسی)
مکان (فارسی)
مکان (انگلیسی)
توضیحات (فارسی) ${escapeHtml(e.descriptionFa||'')}
توضیحات (انگلیسی) ${escapeHtml(e.descriptionEn||'')}
${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 = `
نام (فارسی)
نام (انگلیسی)
سمت (فارسی)
سمت (انگلیسی)
حرف آواتار
تعداد گزارش
ایمیل
ترتیب نمایش
تخصصها (با ویرگول جدا کنید)
${isExpert ? `
اتاق فکر (فارسی)
اتاق فکر (انگلیسی)
تلگرام
لینکدین
توییتر
وبسایت ` : ''}
بیوگرافی (فارسی) ${escapeHtml(m.bioFa||'')}
بیوگرافی (انگلیسی) ${escapeHtml(m.bioEn||'')}
${isEdit?'ذخیره':'ایجاد'}
`;
$('#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 = `
نام (فارسی)
نام (انگلیسی)
قیمت (ریال)
دوره (فارسی)
دوره (انگلیسی)
برچسب (فارسی)
برچسب (انگلیسی)
دکمه CTA (فارسی)
دکمه CTA (انگلیسی)
ترتیب نمایش
پلان ویژه (هایلایت)
ویژگیها (هر ویژگی در یک خط — فارسی)
${escapeHtml(featStr)}
${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 => `${c.label} `).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 = `
بخش سایت
انتخاب کنید…
${RADAR_CATEGORIES.map(c=>`${c.label} `).join('')}
نمایش در بخشهای سایت:
${sectionBadges(r.category)}
فارسی
عنوان (فارسی)
تاریخ (فارسی)
نام نویسنده
نام منبع
لینک منبع
هشتگها (با ویرگول جدا کنید)
چکیده (فارسی) ${escapeHtml(r.excerptFa||'')}
متن کامل (فارسی) ${escapeHtml(r.bodyFa||'')}
تحلیل استراتژیک (فارسی — برای اعضا) ${escapeHtml(r.analysisFa||'')}
English
Title (English)
Date (English)
Excerpt (English) ${escapeHtml(r.excerptEn||'')}
Body (English) ${escapeHtml(r.bodyEn||'')}
Strategic Analysis (English) ${escapeHtml(r.analysisEn||'')}
ترتیب نمایش
${isEdit?'ذخیره تغییرات':'انتشار مطلب رادار'}
`;
$('#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 = `
عنوان دسته (فارسی)
عنوان دسته (انگلیسی)
تیتر تازهترینها (فارسی)
تیتر تازهترینها (انگلیسی)
کارت ویژه (هیرو)
عنوان ویژه (فارسی)
عنوان ویژه (انگلیسی)
تاریخ ویژه (فارسی)
تاریخ ویژه (انگلیسی)
تصویر ویژه (URL)
بنر تحلیل ویژه
برچسب بنر (فارسی)
برچسب بنر (انگلیسی)
عنوان بنر (فارسی)
عنوان بنر (انگلیسی)
توضیح بنر (فارسی) ${escapeHtml(r.bannerDescFa||'')}
توضیح بنر (انگلیسی) ${escapeHtml(r.bannerDescEn||'')}
تصویر کتاب بنر (URL)
ترتیب نمایش
ذخیره
`;
$('#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 = `
عنوان (فارسی)
عنوان (انگلیسی)
برچسب (فارسی)
برچسب (انگلیسی)
تاریخ (فارسی)
تاریخ (انگلیسی)
جهت ورود
${SCROLL_DIRS.map(d=>`${d.label} `).join('')}
ترتیب نمایش
فایل ماهنامه (PDF) — مستقیم دانلود میشود
خلاصه (فارسی) ${escapeHtml(r.excerptFa||'')}
خلاصه (انگلیسی) ${escapeHtml(r.excerptEn||'')}
${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 = `
نام
آیکن
${INTEGRATION_ICONS.map(i=>`${i.label} `).join('')}
رنگ تأکید
ستون شبکه (۱-۶)
ردیف شبکه (۱-۵)
ترتیب نمایش
مسیر لوگو (اختیاری)
${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 = `
شناسه (لاتین)
نام محصول (فارسی)
مقدار / قیمت
واحد (مثل تومان/کیلو)
تغییر (مقدار)
تغییر (درصد)
روند
${TREND_TYPES.map(t => `${t.label} `).join('')}
ترتیب نمایش
${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 = `
سری داده
${CHART_SERIES.map(s => `${s.label} `).join('')}
ترتیب نمایش
برچسب (ماه فارسی / نام کشور)
مقدار (قیمت / تناژ)
متادیتا (فقط «مقایسه جهانی» — JSON: {"flag":"🇮🇷","currency":"IRR","unit":"تومان/کیلو"})
${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 = `
عنوان (فارسی)
عنوان (انگلیسی)
آیکون
—
${VISION_ICONS.map(ic=>`${ic} `).join('')}
ترتیب نمایش
توضیحات (فارسی) ${escapeHtml(v.descFa||'')}
توضیحات (انگلیسی) ${escapeHtml(v.descEn||'')}
${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 = `
برچسب بالای عنوان (فارسی) — مثلاً «ماهنامه شماره ۱۲ منتشر شد…»
برچسب بالای عنوان (انگلیسی)
عنوان (فارسی)
عنوان (انگلیسی)
زیرعنوان (فارسی)
زیرعنوان (انگلیسی)
متن دکمه (فارسی)
متن دکمه (انگلیسی)
لینک دکمه
آپلود فایل PDF گزارش (لینک دکمه را پر میکند)
موقعیت
${POSITIONS.map(p=>`${p.label} `).join('')}
ترتیب نمایش
فعال (نمایش داده شود)
${isEdit ? 'ذخیره' : 'ایجاد'}
`;
$('#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} مورد اضافه شد
` : ''}
`;
jsonTA.value = ''; countEl.textContent = '';
} catch (err) {
statusEl.innerHTML = `خطا: ${err.message}
`;
} finally {
btn.disabled = false; btn.textContent = 'ایمپورت';
}
};
}
// ─────────────────────────────────────────────
api('/api/auth/me').then(() => renderRadarList()).catch(() => renderLogin());