const $ = (sel, root = document) => root.querySelector(sel);
const root = $('#app');
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 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') },
});
renderList();
} catch {
renderLogin('نام کاربری یا رمز عبور اشتباه است');
}
});
}
function topbarHtml(active, primaryLabel) {
return `
پنل اندیشکده فولاد
`;
}
function wireTabs() {
document.querySelectorAll('.tabs button').forEach(btn => {
btn.addEventListener('click', () => {
const t = btn.dataset.tab;
if (t === 'articles') renderList();
else if (t === 'risks') renderRisksList();
else if (t === 'events') renderEventsList();
else if (t === 'radar') renderRadarList();
else if (t === 'radarPages') renderRadarPagesList();
else if (t === 'factoryReports') renderFactoryReportsList();
else if (t === 'integrations') renderIntegrationsList();
else if (t === 'stats') renderStatsList();
else if (t === 'mprices') renderMarketPricesList();
else if (t === 'charts') renderChartPointsList();
else if (t === 'vision') renderVisionList();
else if (t === 'advisory') renderAdvisoryList();
else if (t === 'team') renderTeamList(false);
else if (t === 'experts') renderTeamList(true);
else if (t === 'plans') renderPlansList();
else if (t === 'banners') renderBannersList();
else if (t === 'users') renderUsersList();
});
});
$('#logoutBtn').addEventListener('click', async () => {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {});
renderLogin();
});
}
async function renderList() {
root.innerHTML = `
${topbarHtml('articles', '+ مطلب جدید')}
مطالب
`;
wireTabs();
$('#newBtn').addEventListener('click', () => renderEditor(null));
try {
const items = await api('/api/articles?limit=200');
if (!items.length) {
$('#list').innerHTML = `هنوز مطلبی ثبت نشده است.
`;
return;
}
const safeCoverUrl = (u) => /^\/uploads\/[a-zA-Z0-9._-]+$/.test(String(u || '')) ? u : '';
$('#list').innerHTML = items.map((a) => `
${a.featured ? 'ویژه' : ''}
${a.category ? `${escapeHtml(a.category)}` : ''}
${a.publishDate ? `${escapeHtml(a.publishDate)}` : ''}
${escapeHtml(a.title)}
${escapeHtml(a.summary)}
`).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') {
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();
}
});
} catch (err) {
console.error(err);
$('#list').innerHTML = `خطا در بارگذاری مطالب.
`;
}
}
function renderEditor(article) {
const a = article || {
title: '', category: '', type: '', author: '', authorRole: '', authorInitial: '',
publishDate: '', pages: 0, price: 0, isFree: true, summary: '', body: '',
coverImage: '', tags: [], featured: false,
};
const isEdit = !!article;
root.innerHTML = `
`;
$('#backBtn').addEventListener('click', () => renderList());
$('#coverFile').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="coverImage"]').value = url;
$('#coverPreview').src = url;
} catch (err) {
alert('آپلود ناموفق بود: ' + err.message);
}
});
$('#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 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,
pages: Number(fd.get('pages') || 0),
price: Number(fd.get('price') || 0),
isFree: fd.get('isFree') === 'on',
featured: fd.get('featured') === 'on',
summary: fd.get('summary') || null,
body: fd.get('body') || null,
coverImage: fd.get('coverImage') || null,
tags: tagsRaw,
};
try {
if (isEdit) {
await api(`/api/articles/${article.id}`, { method: 'PUT', body: payload });
} else {
await api('/api/articles', { method: 'POST', body: payload });
}
renderList();
} 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 = `
`;
$('#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 = `
`;
$('#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;
}
// ═══════════════════════════════════════════════
// 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 = `
`;
$('#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 = `
`;
$('#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 = `
`;
$('#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 (رادار آینده)
// ═══════════════════════════════════════════════
async function renderRadarList() {
root.innerHTML = `
${topbarHtml('radar', '+ سیگنال جدید')}
رادار آینده
`;
wireTabs();
$('#newBtn').addEventListener('click', () => renderRadarEditor(null));
try {
const items = await api('/api/radar?limit=200');
if (!items.length) { $('#list').innerHTML = `هنوز سیگنالی ثبت نشده.
`; return; }
const safeImgUrl = (u) => /^(\/uploads\/[a-zA-Z0-9._-]+|\/[a-zA-Z0-9._\/-]+)$/.test(String(u || '')) ? u : '';
$('#list').innerHTML = items.map(r => `
${r.category ? `${escapeHtml(RADAR_CATEGORIES.find(c=>c.value===r.category)?.label||r.category)}` : ''}
${r.authorName ? `${escapeHtml(r.authorName)}` : ''}
${r.dateFa ? `${escapeHtml(r.dateFa)}` : ''}
${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') renderRadarEditor(await api(`/api/radar/${id}`));
else if (btn.dataset.action === 'delete') {
if (!confirm('این سیگنال حذف شود؟')) return;
await api(`/api/radar/${id}`, { method: 'DELETE' });
renderRadarList();
}
});
} catch (err) { $('#list').innerHTML = `خطا: ${escapeHtml(err.message)}
`; }
}
function renderRadarEditor(item) {
const r = item || { category:'', img:'', authorName:'', authorAvatar:'',
titleFa:'', dateFa:'', excerptFa:'', titleEn:'', dateEn:'', excerptEn:'', sortOrder:0 };
const isEdit = !!item;
root.innerHTML = `
`;
$('#backBtn').addEventListener('click', () => renderRadarList());
$('#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: fd.get('authorAvatar')||null,
dateFa: fd.get('dateFa')||null, dateEn: fd.get('dateEn')||null,
excerptFa: fd.get('excerptFa')||null, excerptEn: fd.get('excerptEn')||null,
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); }
});
}
// ═══════════════════════════════════════════════
// RADAR PAGES (صفحات رادار — hero + banner per category)
// ═══════════════════════════════════════════════
const RADAR_PAGE_SLUGS = [
{ slug: 'foresight', label: 'آیندهپژوهی و رصد هوشمند' },
{ slug: 'market', label: 'بازار و زنجیره فولاد' },
{ slug: 'sustainability', label: 'پایداری و فولاد سبز' },
{ slug: 'geopolitics', label: 'ژئوپلیتیک و اقتصاد جهانی' },
{ slug: 'technology', label: 'فناوری و نوآوری' },
];
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 = `
`;
$('#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:'', scrollDir:'up', sortOrder:0 };
const isEdit = !!report;
root.innerHTML = `
`;
$('#backBtn').addEventListener('click', () => renderFactoryReportsList());
$('#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,
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 = `
`;
$('#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 = `
`;
$('#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 = `
`;
$('#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 = `
`;
$('#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 = `
`;
$('#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 = `
`;
$('#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: 'hero', label: 'هیرو (بالای صفحه اصلی)' },
{ value: 'announcement', label: 'اطلاعرسانی (نوار بالا)' },
{ value: 'promo', 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 = `
`;
$('#backBtn').addEventListener('click', () => renderBannersList());
$('#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') || 'hero',
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); }
});
}
// ─────────────────────────────────────────────
api('/api/auth/me').then(() => renderList()).catch(() => renderLogin());