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: 'ویژه' }, ]; async function api(path, { method = 'GET', body, isForm = false } = {}) { const headers = {}; if (body && !isForm) headers['Content-Type'] = 'application/json'; const res = await fetch(path, { method, credentials: 'include', headers, body: isForm ? body : (body ? JSON.stringify(body) : undefined), }); if (res.status === 401) { renderLogin('نیاز به ورود مجدد است'); throw new Error('unauthorized'); } if (!res.ok) { const text = await res.text(); throw new Error(text || res.statusText); } if (res.status === 204) return null; return res.json(); } function renderLogin(errorMsg = '') { root.innerHTML = `

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

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

${errorMsg ? `
${errorMsg}
` : ''}
`; $('#loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const fd = new FormData(e.target); try { const data = await api('/api/auth/login', { method: 'POST', body: { username: fd.get('username'), password: fd.get('password') }, }); renderList(); } catch { renderLogin('نام کاربری یا رمز عبور اشتباه است'); } }); } function topbarHtml(active, primaryLabel) { return `
پنل اندیشکده فولاد
`; } function wireTabs() { document.querySelectorAll('.tabs button').forEach(btn => { btn.addEventListener('click', () => { if (btn.dataset.tab === 'articles') renderList(); else if (btn.dataset.tab === 'risks') renderRisksList(); else if (btn.dataset.tab === '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 = `
${isEdit ? 'ویرایش مطلب' : 'مطلب جدید'}
`; $('#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 = `
${isEdit ? 'ویرایش سیگنال ریسک' : 'سیگنال ریسک جدید'}
`; $('#backBtn').addEventListener('click', () => renderRisksList()); $('#editorForm').addEventListener('submit', async (e) => { e.preventDefault(); const fd = new FormData(e.target); const payload = { name: fd.get('name'), quote: fd.get('quote'), date: fd.get('date') || null, level: fd.get('level'), sortOrder: Number(fd.get('sortOrder') || 0), }; try { if (isEdit) await api(`/api/risks/${risk.id}`, { method: 'PUT', body: payload }); else await api('/api/risks', { method: 'POST', body: payload }); renderRisksList(); } catch (err) { alert('ذخیره ناموفق بود: ' + err.message); } }); } // ---------- users ---------- async function renderUsersList() { root.innerHTML = ` ${topbarHtml('users', '+ کاربر جدید')}

کاربران پنل

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

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

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

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

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

`; } } function renderUserEditor(user) { const u = user || { username: '' }; const isEdit = !!user; root.innerHTML = `
${isEdit ? 'ویرایش کاربر' : 'کاربر جدید'}
`; $('#backBtn').addEventListener('click', () => renderUsersList()); $('#editorForm').addEventListener('submit', async (e) => { e.preventDefault(); const fd = new FormData(e.target); const username = String(fd.get('username') || '').trim(); const password = String(fd.get('password') || ''); const payload = { username }; if (password) payload.password = password; try { if (isEdit) await api(`/api/users/${user.id}`, { method: 'PUT', body: payload }); else await api('/api/users', { method: 'POST', body: { username, password } }); renderUsersList(); } catch (err) { alert('ذخیره ناموفق بود: ' + friendlyUserError(err.message)); } }); } function friendlyUserError(msg) { const m = String(msg || ''); if (m.includes('username_taken')) return 'این نام کاربری قبلاً ثبت شده است.'; if (m.includes('username_too_short')) return 'نام کاربری باید حداقل ۳ کاراکتر باشد.'; if (m.includes('password_too_short')) return 'رمز عبور باید حداقل ۸ کاراکتر باشد.'; if (m.includes('cannot_delete_self')) return 'نمی‌توانید حساب خودتان را حذف کنید.'; if (m.includes('cannot_delete_last_user')) return 'آخرین کاربر را نمی‌توان حذف کرد.'; return m; } api('/api/auth/me').then(() => renderList()).catch(() => renderLogin());