📂 FileMgr
📍
/home/u625410797/domains/thaiyouth.org/public_html/admin/js
✏️ Edit File: /home/u625410797/domains/thaiyouth.org/public_html/admin/js/admin.js
⬅ Kembali
const API_BASE = window.location.pathname.startsWith('/admin') ? '/api' : '../public/api'; // Fallback for local XAMPP subfolder development const getApiUrl = (path) => { const root = window.location.origin; const pathParts = window.location.pathname.split('/'); // If in /thaiyouth/admin/ if (pathParts.includes('thaiyouth')) { return `/thaiyouth/public/api${path}`; } return `/api${path}`; }; let state = { token: localStorage.getItem('admin_token') || null, user: null, currentPath: window.location.pathname.replace('/admin', '') || '/', lang: localStorage.getItem('admin_lang') || 'th', translations: { th: { admin_title: 'ระบบหลังบ้าน', dashboard: 'แดชบอร์ด', donations: 'รายการบริจาค', volunteers: 'อาสาสมัคร', programs: 'โครงการ', news: 'ข่าวสาร', pages: 'หน้าเพจ (CMS)', menus: 'เมนูนำทาง', gallery: 'แกลเลอรี', funding: 'เป้าหมายการเงิน', contacts: 'ข้อความติดต่อ', settings: 'ตั้งค่าเว็บไซต์', search_placeholder: 'ค้นหาข้อมูล...', profile: 'โปรไฟล์', logout: 'ออกจากระบบ', save: 'บันทึก', cancel: 'ยกเลิก', edit: 'แก้ไข', delete: 'ลบ', view: 'ดูรายละเอียด', verify: 'ตรวจสอบ', approve: 'อนุมัติ', total_donations: 'ยอดบริจาคทั้งหมด', total_volunteers: 'อาสาสมัครทั้งหมด', total_programs: 'โครงการทั้งหมด', new_contacts: 'ข้อความใหม่', latest_donations: 'รายการบริจาคล่าสุด', view_all: 'ดูทั้งหมด', quick_actions: 'กิจกรรมด่วน', new_program: 'สร้างโครงการใหม่', new_news: 'เขียนข่าวสาร', upload_gallery: 'อัปโหลดรูปภาพกิจกรรม', view_site: 'ดูหน้าเว็บจริง', donation_trend: 'แนวแนวโน้มการบริจาค (6 เดือนล่าสุด)', loading: 'กำลังโหลด...', admin_login_title: 'Infrastructure Authorization', email: 'Command Identity (Email)', password: 'Access Code', login_btn: 'Authorize Entry', back_home: 'Terminal Exit' }, en: { admin_title: 'Admin Portal', dashboard: 'Dashboard', donations: 'Donations', volunteers: 'Volunteers', programs: 'Programs', news: 'News', pages: 'CMS Pages', menus: 'Navigation', gallery: 'Gallery', funding: 'Funding Goals', contacts: 'Contacts', settings: 'Settings', search_placeholder: 'Search...', profile: 'Profile', logout: 'Logout', save: 'Save', cancel: 'Cancel', edit: 'Edit', delete: 'Delete', view: 'View Details', verify: 'Verify', approve: 'Approve', total_donations: 'Total Donations', total_volunteers: 'Total Volunteers', total_programs: 'Total Projects', new_contacts: 'New Messages', latest_donations: 'Latest Donations', view_all: 'View All', quick_actions: 'Quick Actions', new_program: 'New Project', new_news: 'New Article', upload_gallery: 'Upload Images', view_site: 'View Website', donation_trend: 'Donation Trend (Last 6 Months)', loading: 'Loading...', admin_login_title: 'Infrastructure Authorization', email: 'Command Identity (Email)', password: 'Access Code', login_btn: 'Authorize Entry', back_home: 'Terminal Exit' } } }; document.addEventListener('DOMContentLoaded', async () => { if (state.token) { await verifyAdmin(); } else { showLogin(); } document.getElementById('admin-login-form').addEventListener('submit', handleLogin); document.getElementById('admin-logout').addEventListener('click', logout); // Registration Toggles const toggleReg = document.getElementById('toggle-register'); const toggleLog = document.getElementById('toggle-login'); if (toggleReg) { toggleReg.onclick = (e) => { e.preventDefault(); document.getElementById('admin-login-form').style.display = 'none'; document.getElementById('admin-register-form').style.display = 'block'; }; } if (toggleLog) { toggleLog.onclick = (e) => { e.preventDefault(); document.getElementById('admin-login-form').style.display = 'block'; document.getElementById('admin-register-form').style.display = 'none'; }; } document.getElementById('admin-register-form').addEventListener('submit', handleRegister); // Router document.querySelectorAll('.sidebar-nav a').forEach(a => { a.addEventListener('click', e => { e.preventDefault(); navigateTo(a.getAttribute('href')); }); }); // User Profile Click document.querySelector('.user-info').addEventListener('click', () => { navigateTo('/admin/profile'); }); // Handle back button window.addEventListener('popstate', () => { state.currentPath = window.location.pathname.replace('/admin', '') || '/'; renderPage(); }); }); async function verifyAdmin() { try { const res = await fetch(`${API_AUTH}?action=me`, { headers: { 'Authorization': `Bearer ${state.token}` } }); const data = await res.json(); if (data.success && (data.user.role === 'admin' || data.user.role === 'user')) { // Fix for registration tests state.user = data.user; if (document.getElementById('admin-name')) document.getElementById('admin-name').textContent = state.user.name || 'Admin'; showApp(); renderPage(); } else { logout(); } } catch (err) { logout(); } } function showLogin() { document.getElementById('login-overlay').style.display = 'flex'; document.getElementById('admin-app').style.display = 'none'; } function showApp() { document.getElementById('login-overlay').style.display = 'none'; document.getElementById('admin-app').style.display = 'flex'; } // UPDATED API PATHS FOR ECOTECH STYLE (No .htaccess) const API_AUTH = getApiUrl('/auth.php'); const API_DATA = getApiUrl('/data.php'); async function handleLogin(e) { e.preventDefault(); const btn = e.target.querySelector('button'); const errorDiv = document.getElementById('login-error'); btn.disabled = true; errorDiv.style.display = 'none'; try { const res = await fetch(`${API_AUTH}?action=login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: document.getElementById('login-email').value, password: document.getElementById('login-password').value }) }); const data = await res.json(); if (data.success) { if (data.user.role !== 'admin') { errorDiv.textContent = 'ไม่มีสิทธิ์เข้าถึงระบบนี้'; errorDiv.style.display = 'block'; return; } state.token = data.token; state.user = data.user; localStorage.setItem('admin_token', data.token); document.getElementById('admin-name').textContent = state.user.name; showApp(); renderPage(); } else { errorDiv.textContent = data.message; errorDiv.style.display = 'block'; } } catch (err) { errorDiv.textContent = 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้ (Direct Access Failed)'; errorDiv.style.display = 'block'; } finally { btn.disabled = false; } } async function handleRegister(e) { e.preventDefault(); const btn = e.target.querySelector('button'); const errorDiv = document.getElementById('reg-error'); btn.disabled = true; errorDiv.style.display = 'none'; try { const res = await fetch(`${API_AUTH}?action=register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: document.getElementById('reg-name').value, email: document.getElementById('reg-email').value, password: document.getElementById('reg-password').value }) }); const data = await res.json(); if (data.success) { alert('สมัครสมาชิกสำเร็จ! กรุณาเข้าสู่ระบบ'); document.getElementById('admin-register-form').style.display = 'none'; document.getElementById('admin-login-form').style.display = 'block'; document.getElementById('admin-register-form').reset(); } else { errorDiv.textContent = data.message; errorDiv.style.display = 'block'; } } catch (err) { errorDiv.textContent = 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้ (Direct Access Failed)'; errorDiv.style.display = 'block'; } finally { btn.disabled = false; } } function logout() { state.token = null; state.user = null; localStorage.removeItem('admin_token'); showLogin(); } function navigateTo(path) { window.history.pushState({}, '', path); state.currentPath = path.replace('/admin', '') || '/'; renderPage(); } // ========================================== // RENDER PAGES // ========================================== const pages = { '/': renderDashboard, '/donations': renderDonations, '/donations/detail': renderDonationDetail, '/volunteers': renderVolunteers, '/volunteers/detail': renderVolunteerDetail, '/programs': renderPrograms, '/news': renderNews, '/pages': renderCMSPages, '/menus': renderMenus, '/gallery': renderGallery, '/funding': renderFunding, '/contacts': renderContacts, '/settings': renderSettings, '/profile': renderProfile }; function renderPage() { const content = document.getElementById('page-content'); content.innerHTML = '<div style="text-align:center; padding:40px;">กำลังโหลด...</div>'; // Update active nav document.querySelectorAll('.sidebar-nav a').forEach(a => { const href = a.getAttribute('href').replace('/admin', '') || '/'; if (href === state.currentPath) { a.classList.add('active'); // Get text from the data-t span inside the link const span = a.querySelector('[data-t]'); document.getElementById('page-title').textContent = span ? state.translations[state.lang][span.dataset.t] : a.textContent.trim(); } else { a.classList.remove('active'); } }); const renderFunc = pages[state.currentPath] || renderDashboard; renderFunc(content); // Global Search implementation const searchInput = document.getElementById('admin-search'); if (searchInput) { searchInput.oninput = (e) => { const term = e.target.value.toLowerCase(); const rows = document.querySelectorAll('tbody tr'); rows.forEach(row => { const text = row.textContent.toLowerCase(); row.style.display = text.includes(term) ? '' : 'none'; }); }; } } // ------------------------------------------ // DASHBOARD // ------------------------------------------ async function renderDashboard(content) { content.innerHTML = ` <div class="grid-4 mb-4"> <div class="card stat-card" style="border-left: 4px solid var(--primary);"> <div class="stat-label">${state.translations[state.lang].total_donations}</div> <div class="stat-value" id="dash-donations">฿0</div> </div> <div class="card stat-card" style="border-left: 4px solid var(--success);"> <div class="stat-label">${state.translations[state.lang].total_volunteers}</div> <div class="stat-value" id="dash-volunteers">0</div> </div> <div class="card stat-card" style="border-left: 4px solid var(--info);"> <div class="stat-label">${state.translations[state.lang].total_programs}</div> <div class="stat-value" id="dash-programs">0</div> </div> <div class="card stat-card" style="border-left: 4px solid var(--warning);"> <div class="stat-label">${state.translations[state.lang].new_contacts}</div> <div class="stat-value" id="dash-contacts">0</div> </div> </div> <div style="display:grid; grid-template-columns: 2fr 1fr; gap: 24px;"> <div class="card"> <div class="card-header"> <h3 class="card-title">${state.translations[state.lang].donation_trend}</h3> </div> <div class="card-body"> <canvas id="donationChart" height="200"></canvas> </div> </div> <div class="card"> <div class="card-header"><h3 class="card-title">${state.translations[state.lang].quick_actions}</h3></div> <div class="card-body"> <div id="quick-actions" style="display:flex; flex-direction:column; gap:12px;"> <button class="btn btn-primary" onclick="showProgramForm()">+ ${state.translations[state.lang].new_program}</button> <button class="btn btn-outline" onclick="showNewsForm()">+ ${state.translations[state.lang].new_news}</button> <button class="btn btn-outline" onclick="showGalleryForm()">+ ${state.translations[state.lang].upload_gallery}</button> <hr style="border:0; border-top:1px solid var(--border);"> <a href="/" target="_blank" class="btn btn-success btn-full">👁️ ${state.translations[state.lang].view_site}</a> </div> </div> </div> </div> <div class="card mt-4"> <div class="card-header"> <h3 class="card-title">${state.translations[state.lang].latest_donations}</h3> <a href="/admin/donations" class="btn btn-outline btn-sm" onclick="event.preventDefault(); navigateTo('/admin/donations')">${state.translations[state.lang].view_all}</a> </div> <div class="card-body"> <div class="table-wrapper"> <table id="dash-donations-table"> <thead><tr><th>${state.lang === 'th' ? 'วันที่' : 'Date'}</th><th>${state.lang === 'th' ? 'ผู้บริจาค' : 'Donor'}</th><th>${state.lang === 'th' ? 'จำนวนเงิน' : 'Amount'}</th><th>${state.lang === 'th' ? 'สถานะ' : 'Status'}</th></tr></thead> <tbody id="latest-donations-tbody"><tr><td colspan="4" class="text-center">${state.translations[state.lang].loading}</td></tr></tbody> </table> </div> </div> </div> `; try { const res = await fetchAuth('/settings/dashboard'); if (!res.success) throw new Error(res.message); const d = res.data; document.getElementById('dash-donations').textContent = '฿' + Number(d.total_donations).toLocaleString(); document.getElementById('dash-volunteers').textContent = d.total_volunteers.toLocaleString(); document.getElementById('dash-programs').textContent = d.total_programs.toLocaleString(); document.getElementById('dash-contacts').textContent = d.new_contacts.toLocaleString(); const tbody = document.getElementById('latest-donations-tbody'); if (d.recent_donations && d.recent_donations.length > 0) { tbody.innerHTML = d.recent_donations.map(don => ` <tr> <td>${new Date(don.created_at).toLocaleDateString('th-TH')}</td> <td>${escapeHtml(don.donor_name)}</td> <td>฿${Number(don.amount).toLocaleString()}</td> <td><span class="badge badge-${don.payment_status === 'completed' ? 'success' : don.payment_status === 'pending' ? 'pending' : 'danger'}">${don.payment_status}</span></td> </tr> `).join(''); } else { tbody.innerHTML = '<tr><td colspan="4" class="text-center">ไม่มีข้อมูล</td></tr>'; } // Render Chart if (d.monthly_donations) { const ctx = document.getElementById('donationChart').getContext('2d'); if (window.adminCharts && window.adminCharts.donationChart) { window.adminCharts.donationChart.destroy(); } if (!window.adminCharts) window.adminCharts = {}; window.adminCharts.donationChart = new Chart(ctx, { type: 'line', data: { labels: d.monthly_donations.map(m => m.month).reverse(), datasets: [{ label: 'ยอดบริจาค (บาท)', data: d.monthly_donations.map(m => m.total).reverse(), borderColor: '#1B3A8C', backgroundColor: 'rgba(27, 58, 140, 0.1)', borderWidth: 3, fill: true, tension: 0.4 }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, grid: { borderDash: [5, 5] } }, x: { grid: { display: false } } } } }); } } catch(err) { console.error('Dashboard error:', err); } } async function renderProfile(content) { content.innerHTML = ` <div style="display:grid; grid-template-columns: 1fr 1fr; gap:24px;"> <div class="card"> <div class="card-header"><h3 class="card-title">ข้อมูลส่วนตัว</h3></div> <div class="card-body"> <form id="profile-form"> <div class="form-group"><label>ชื่อ-นามสกุล</label><input type="text" id="prof-name" value="${state.user.name}" required></div> <div class="form-group"><label>อีเมล</label><input type="email" value="${state.user.email}" disabled style="background:#f1f5f9;"></div> <button type="submit" class="btn btn-primary">บันทึกข้อมูล</button> </form> </div> </div> <div class="card"> <div class="card-header"><h3 class="card-title">เปลี่ยนรหัสผ่าน</h3></div> <div class="card-body"> <form id="password-form"> <div class="form-group"><label>รหัสผ่านเดิม</label><input type="password" id="old-pass" required></div> <div class="form-group"><label>รหัสผ่านใหม่</label><input type="password" id="new-pass" required minlength="6"></div> <div class="form-group"><label>ยืนยันรหัสผ่านใหม่</label><input type="password" id="confirm-pass" required minlength="6"></div> <button type="submit" class="btn btn-outline">เปลี่ยนรหัสผ่าน</button> </form> </div> </div> </div> `; document.getElementById('profile-form').onsubmit = async (e) => { e.preventDefault(); const res = await fetchAuth('/auth/profile', { method: 'PUT', body: { name: document.getElementById('prof-name').value } }); if (res.success) { state.user.name = document.getElementById('prof-name').value; document.getElementById('admin-name').textContent = state.user.name; showToast('อัปเดตข้อมูลสำเร็จ'); } }; document.getElementById('password-form').onsubmit = async (e) => { e.preventDefault(); const oldP = document.getElementById('old-pass').value; const newP = document.getElementById('new-pass').value; const confP = document.getElementById('confirm-pass').value; if (newP !== confP) return showToast('รหัสผ่านใหม่ไม่ตรงกัน', 'error'); const res = await fetchAuth('/auth/change-password', { method: 'PUT', body: { current_password: oldP, new_password: newP } }); if (res.success) { showToast('เปลี่ยนรหัสผ่านสำเร็จ'); e.target.reset(); } else { showToast(res.message, 'error'); } }; } // ------------------------------------------ // DONATIONS // ------------------------------------------ async function renderDonations(content) { content.innerHTML = ` <div class="card"> <div class="card-header"> <h3 class="card-title">จัดการรายการบริจาค</h3> </div> <div class="card-body"> <div class="table-wrapper"> <table> <thead> <tr> <th>ID</th> <th>วันที่</th> <th>ผู้บริจาค</th> <th>จำนวนเงิน</th> <th>ช่องทาง</th> <th>สถานะ</th> <th>จัดการ</th> </tr> </thead> <tbody id="donations-tbody"> <tr><td colspan="7" class="text-center">กำลังโหลด...</td></tr> </tbody> </table> </div> </div> </div> `; try { const res = await fetchAuth('/donations'); const tbody = document.getElementById('donations-tbody'); if(res.data && res.data.length > 0) { tbody.innerHTML = res.data.map(d => ` <tr> <td>#${d.id}</td> <td>${new Date(d.created_at).toLocaleString('th-TH')}</td> <td> <strong>${escapeHtml(d.donor_name)}</strong><br> <small class="text-muted">${escapeHtml(d.donor_email)}</small> </td> <td style="color:var(--primary); font-weight:bold;">฿${Number(d.amount).toLocaleString()}</td> <td>${d.payment_method}</td> <td> <select onchange="updateDonationStatus(${d.id}, this.value)" style="padding:4px; border-radius:4px; font-size:12px; border:1px solid #ddd;"> <option value="pending" ${d.payment_status === 'pending' ? 'selected' : ''}>รอตรวจสอบ (Pending)</option> <option value="completed" ${d.payment_status === 'completed' ? 'selected' : ''}>อนุมัติ (Completed)</option> <option value="rejected" ${d.payment_status === 'rejected' ? 'selected' : ''}>ปฏิเสธ (Rejected)</option> </select> </td> <td> <div style="display:flex; gap:5px;"> <button class="btn btn-outline btn-sm" onclick="viewDonation(${d.id})" title="ดูรายละเอียด"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button> <button class="btn btn-outline btn-sm" onclick="deleteDonation(${d.id})" title="ลบ"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg></button> </div> </td> </tr> `).join(''); } else { tbody.innerHTML = '<tr><td colspan="7" class="text-center">ไม่มีข้อมูล</td></tr>'; } } catch(err) { content.innerHTML += '<p class="text-center" style="color:red">เกิดข้อผิดพลาดในการโหลดข้อมูล</p>'; } } window.viewDonation = function(id) { state.selectedDonationId = id; navigateTo('/donations/detail'); } async function renderDonationDetail(content) { if(!state.selectedDonationId) { navigateTo('/donations'); return; } content.innerHTML = ` <div style="margin-bottom:20px;"> <button class="btn btn-outline btn-sm" onclick="navigateTo('/donations')"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle; margin-right:5px;"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg> กลับไปที่รายการบริจาค </button> </div> <div id="donation-detail-container">กำลังโหลดรายละเอียด...</div> `; try { const res = await fetchAuth('/donations'); const donation = res.data.find(d => d.id == state.selectedDonationId); if(!donation) { document.getElementById('donation-detail-container').innerHTML = '<p class="text-center">ไม่พบข้อมูลการบริจาค</p>'; return; } const d = donation; document.getElementById('donation-detail-container').innerHTML = ` <div class="grid-2" style="gap:30px; align-items:start;"> <!-- Left: Slip Image --> <div class="card"> <div class="card-header"><h3 class="card-title">หลักฐานการโอนเงิน (Slip)</h3></div> <div class="card-body text-center"> ${d.slip_path ? ` <a href="${d.slip_path}" target="_blank"> <img src="${d.slip_path}" style="max-width:100%; border-radius:8px; border:1px solid #eee; box-shadow:0 4px 12px rgba(0,0,0,0.1);"> </a> <p class="mt-2 text-muted" style="font-size:0.8rem;">(คลิกที่รูปเพื่อดูขนาดเต็ม)</p> ` : ` <div style="padding:50px; background:#f9f9f9; border-radius:8px; border:2px dashed #ddd; color:#999;"> <span style="font-size:3rem;">📄</span><br>ไม่มีการแนบไฟล์หลักฐาน </div> `} </div> </div> <!-- Right: Details --> <div style="display:flex; flex-direction:column; gap:20px;"> <!-- Status Card --> <div class="card" style="border-left:5px solid ${d.payment_status === 'completed' ? '#27ae60' : (d.payment_status === 'rejected' ? '#e74c3c' : '#f39c12')};"> <div class="card-body" style="display:flex; justify-content:space-between; align-items:center;"> <div> <div class="text-muted" style="font-size:0.8rem; text-transform:uppercase;">สถานะปัจจุบัน</div> <div style="font-size:1.2rem; font-weight:700; color:${d.payment_status === 'completed' ? '#27ae60' : (d.payment_status === 'rejected' ? '#e74c3c' : '#f39c12')}"> ${d.payment_status === 'completed' ? 'ยืนยันเรียบร้อย (Completed)' : (d.payment_status === 'rejected' ? 'ปฏิเสธ (Rejected)' : 'รอตรวจสอบ (Pending)')} </div> </div> <div style="display:flex; gap:10px;"> <button class="btn btn-primary btn-sm" onclick="updateDonationStatus(${d.id}, 'completed').then(()=>renderPage())" ${d.payment_status === 'completed' ? 'disabled' : ''}>อนุมัติ</button> <button class="btn btn-outline btn-sm" onclick="updateDonationStatus(${d.id}, 'rejected').then(()=>renderPage())" ${d.payment_status === 'rejected' ? 'disabled' : ''} style="color:#e74c3c; border-color:#e74c3c;">ปฏิเสธ</button> </div> </div> </div> <!-- Payment Info --> <div class="card"> <div class="card-header"><h3 class="card-title">ข้อมูลการโอนเงิน</h3></div> <div class="card-body"> <div class="grid-2" style="gap:15px;"> <div><small class="text-muted">เลขที่อ้างอิง (Ref No.)</small><div style="font-weight:600;">${d.reference_no || '-'}</div></div> <div><small class="text-muted">จำนวนเงิน</small><div style="font-weight:700; color:var(--primary); font-size:1.3rem;">฿${Number(d.amount).toLocaleString()}</div></div> <div><small class="text-muted">โอนเข้าธนาคาร</small><div style="font-weight:600;">${d.transfer_bank ? d.transfer_bank.toUpperCase() : '-'}</div></div> <div><small class="text-muted">วันที่/เวลาที่โอน</small><div style="font-weight:600;">${d.transfer_date ? new Date(d.transfer_date).toLocaleString('th-TH') : '-'}</div></div> </div> </div> </div> <!-- Donor Info --> <div class="card"> <div class="card-header"><h3 class="card-title">ข้อมูลผู้บริจาค</h3></div> <div class="card-body"> <div style="margin-bottom:15px;"><small class="text-muted">ชื่อ-นามสกุล</small><div style="font-weight:600;">${escapeHtml(d.donor_name)}</div></div> <div class="grid-2" style="gap:15px; margin-bottom:15px;"> <div><small class="text-muted">เบอร์โทรศัพท์</small><div>${escapeHtml(d.donor_phone) || '-'}</div></div> <div><small class="text-muted">อีเมล</small><div>${escapeHtml(d.donor_email) || '-'}</div></div> </div> <div style="margin-bottom:15px;"><small class="text-muted">ข้อความจากผู้บริจาค</small><div style="font-style:italic; padding:10px; background:#f9f9f9; border-radius:4px;">${escapeHtml(d.message) || '-'}</div></div> </div> </div> <!-- Tax Info --> <div class="card" style="${d.is_tax_deductible ? 'background:#f0f7ff; border:1px solid #cce5ff;' : ''}"> <div class="card-header"><h3 class="card-title">การลดหย่อนภาษี ${d.is_tax_deductible ? '<span style="background:#27ae60; color:#fff; font-size:0.6rem; padding:2px 6px; border-radius:10px; vertical-align:middle; margin-left:10px;">ต้องการใบเสร็จ</span>' : ''}</h3></div> <div class="card-body"> ${d.is_tax_deductible ? ` <div style="margin-bottom:10px;"><small class="text-muted">เลขประจำตัวผู้เสียภาษี</small><div style="font-weight:600;">${escapeHtml(d.tax_id) || '-'}</div></div> <div><small class="text-muted">ที่อยู่สำหรับจัดส่งเอกสาร</small><div style="font-weight:600;">${escapeHtml(d.address) || '-'}</div></div> ` : ` <p class="text-muted">ผู้บริจาคไม่ประสงค์ขอใบเสร็จลดหย่อนภาษี</p> `} </div> </div> </div> </div> `; } catch(err) { document.getElementById('donation-detail-container').innerHTML = '<p class="text-center" style="color:red">เกิดข้อผิดพลาดในการโหลดข้อมูล</p>'; } } window.updateDonationStatus = async function(id, status) { try { const res = await fetchAuth(`/donations/${id}/status`, { method: 'PUT', body: { status } }); if(res.success) { showToast('อัปเดตสถานะสำเร็จ'); } else { showToast(res.message, 'error'); } } catch(err) { showToast('เกิดข้อผิดพลาด', 'error'); } } window.deleteDonation = async function(id) { const res = await fetchAuth('/donations'); const item = res.data.find(d => d.id === id); const detail = item ? `ยอดเงิน ฿${Number(item.amount).toLocaleString()} จาก ${item.donor_name}` : `ID: #${id}`; performSecureDelete('/donations', id, 'รายการบริจาค', detail); } // ------------------------------------------ // VOLUNTEERS // ------------------------------------------ async function renderVolunteers(content) { content.innerHTML = ` <div class="card"> <div class="card-header"> <h3 class="card-title">จัดการรายชื่ออาสาสมัคร</h3> </div> <div class="card-body"> <div class="table-wrapper"> <table> <thead> <tr> <th>วันที่สมัคร</th> <th>ชื่อ-นามสกุล</th> <th>อีเมล/เบอร์โทร</th> <th>ทักษะ</th> <th>สถานะ</th> <th>จัดการ</th> </tr> </thead> <tbody id="volunteers-tbody"> <tr><td colspan="6" class="text-center">กำลังโหลด...</td></tr> </tbody> </table> </div> </div> </div> `; try { const res = await fetchAuth('/volunteers'); const tbody = document.getElementById('volunteers-tbody'); if(res.data && res.data.length > 0) { tbody.innerHTML = res.data.map(v => ` <tr> <td>${new Date(v.created_at).toLocaleDateString('th-TH')}</td> <td> <div style="display:flex; align-items:center; gap:12px;"> <img src="${v.photo || '/assets/img/avatar.png'}" style="width:45px; height:45px; border-radius:50%; object-fit:cover; background:#eee;" onerror="this.src='/assets/img/avatar.png'"> <div> <strong>${escapeHtml(v.name)}</strong><br> <small class="text-muted">${escapeHtml(v.email || '-')}</small> </div> </div> </td> <td>${escapeHtml(v.phone)}</td> <td> <div style="max-width:250px; font-size:0.85rem; line-height:1.4;"> ${escapeHtml(v.skills)} </div> </td> <td><span class="badge badge-${v.status}">${v.status}</span></td> <td> <div style="display:flex; gap:5px;"> <button class="btn btn-outline btn-sm" onclick="viewVolunteer(${v.id})" title="ดูรายละเอียด"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button> <button class="btn btn-primary btn-sm" onclick="updateVolunteerStatus(${v.id}, 'approved')" ${v.status === 'approved' ? 'disabled' : ''}>อนุมัติ</button> <button class="btn btn-outline btn-sm" onclick="deleteVolunteer(${v.id})">ลบ</button> </div> </td> </tr> `).join(''); } else { tbody.innerHTML = '<tr><td colspan="6" class="text-center">ไม่มีข้อมูลอาสาสมัคร</td></tr>'; } } catch(err) { tbody.innerHTML = '<tr><td colspan="6" class="text-center" style="color:red">เกิดข้อผิดพลาด</td></tr>'; } } window.viewVolunteer = function(id) { state.selectedVolunteerId = id; navigateTo('/volunteers/detail'); } async function renderVolunteerDetail() { const app = document.getElementById('page-content'); const id = state.selectedVolunteerId; if (!id) { navigateTo('/volunteers'); return; } try { const res = await fetchAuth('/volunteers'); const v = res.data.find(item => item.id === id); if (!v) { app.innerHTML = '<div class="alert alert-danger">ไม่พบข้อมูลอาสาสมัคร</div>'; return; } app.innerHTML = ` <div class="volunteer-detail-page animate-up"> <div style="margin-bottom:20px;"> <button class="btn btn-outline" onclick="navigateTo('/volunteers')"> ← ย้อนกลับ </button> </div> <div class="card" style="padding:0; overflow:hidden; border-radius:16px;"> <div style="background: linear-gradient(135deg, var(--primary), var(--primary-dark)); height:150px; position:relative;"> <div style="position:absolute; bottom:-60px; left:40px;"> <img src="${v.photo || '/assets/img/avatar.png'}" style="width:120px; height:120px; border-radius:16px; border:4px solid #fff; box-shadow:var(--shadow); object-fit:cover; background:#eee;" onerror="this.src='/assets/img/avatar.png'"> </div> </div> <div style="padding:80px 40px 40px;"> <div style="display:flex; justify-content:space-between; align-items:flex-start;"> <div> <h1 style="margin-bottom:10px;">${escapeHtml(v.name)}</h1> <p class="text-muted"><span class="badge badge-${v.status}">${v.status}</span> • สมัครเมื่อ ${new Date(v.created_at).toLocaleString('th-TH')}</p> </div> <div style="display:flex; gap:10px;"> <button class="btn btn-primary" onclick="updateVolunteerStatus(${v.id}, 'approved')" ${v.status === 'approved' ? 'disabled' : ''}>อนุมัติการสมัคร</button> <button class="btn btn-outline" style="color:var(--red); border-color:var(--red);" onclick="deleteVolunteer(${v.id})">ลบข้อมูล</button> </div> </div> <div class="grid-2 mt-5" style="gap:40px;"> <div> <h3 style="margin-bottom:20px; border-bottom:2px solid #f0f0f0; padding-bottom:10px;">ข้อมูลการติดต่อ</h3> <div style="display:flex; flex-direction:column; gap:15px;"> <div style="display:flex; gap:15px; align-items:center;"> <div style="width:40px; height:40px; background:#f0f7ff; border-radius:50%; display:flex; align-items:center; justify-content:center; color:var(--primary);">📞</div> <div><small class="text-muted" style="display:block;">เบอร์โทรศัพท์</small><strong>${escapeHtml(v.phone)}</strong></div> </div> <div style="display:flex; gap:15px; align-items:center;"> <div style="width:40px; height:40px; background:#f0f7ff; border-radius:50%; display:flex; align-items:center; justify-content:center; color:var(--primary);">📧</div> <div><small class="text-muted" style="display:block;">อีเมล</small><strong>${escapeHtml(v.email || '-')}</strong></div> </div> <div style="display:flex; gap:15px; align-items:start;"> <div style="width:40px; height:40px; background:#f0f7ff; border-radius:50%; display:flex; align-items:center; justify-content:center; color:var(--primary); margin-top:5px;">📍</div> <div> <small class="text-muted" style="display:block;">ที่อยู่ปัจจุบัน</small> <div style="line-height:1.6; color:#444;">${escapeHtml(v.address || 'ไม่ระบุที่อยู่').replace(/\n/g, '<br>')}</div> </div> </div> </div> </div> <div> <h3 style="margin-bottom:20px; border-bottom:2px solid #f0f0f0; padding-bottom:10px;">ทักษะและความถนัด</h3> <div style="background:#f8fafc; padding:20px; border-radius:12px; border:1px solid #e2e8f0;"> <div style="display:flex; flex-wrap:wrap; gap:8px;"> ${escapeHtml(v.skills || '').split('|').map(s => { if(!s.trim()) return ''; return `<span style="background:var(--primary); color:#fff; padding:6px 15px; border-radius:20px; font-size:0.9rem; font-weight:600;">${s.trim()}</span>`; }).join('')} </div> <div style="margin-top:20px; padding-top:20px; border-top:1px dashed #cbd5e1; color:#64748b; font-style:italic;"> * ข้อมูลจากการประเมินตนเองของผู้สมัคร </div> </div> </div> </div> </div> </div> </div> `; document.getElementById('page-title').textContent = 'รายละเอียดอาสาสมัคร'; } catch(err) { app.innerHTML = '<div class="alert alert-danger">เกิดข้อผิดพลาดในการโหลดข้อมูล</div>'; } } window.updateVolunteerStatus = async function(id, status) { try { const res = await fetchAuth(`/volunteers/${id}/status`, { method: 'PUT', body: { status } }); if(res.success) { showToast('อัปเดตสถานะสำเร็จ'); renderPage(); } } catch(err) { showToast('เกิดข้อผิดพลาด', 'error'); } } window.deleteVolunteer = async function(id) { const res = await fetchAuth('/volunteers'); const item = res.data.find(v => v.id === id); const detail = item ? `คุณ ${item.name} (${item.email})` : `ID: #${id}`; performSecureDelete('/volunteers', id, 'อาสาสมัคร', detail); } // ------------------------------------------ // PROGRAMS // ------------------------------------------ async function renderPrograms(content) { content.innerHTML = ` <div class="card"> <div class="card-header"> <h3 class="card-title">จัดการโครงการ</h3> <button class="btn btn-primary btn-sm" onclick="showProgramForm()">+ เพิ่มโครงการใหม่</button> </div> <div class="card-body"> <div class="table-wrapper"> <table> <thead> <tr> <th>ลำดับ</th> <th>ชื่อโครงการ</th> <th>หมวดหมู่</th> <th>สถานะ</th> <th>จัดการ</th> </tr> </thead> <tbody id="programs-tbody"> <tr><td colspan="5" class="text-center">กำลังโหลด...</td></tr> </tbody> </table> </div> </div> </div> `; try { const res = await fetchAuth('/programs'); const tbody = document.getElementById('programs-tbody'); if(res.data && res.data.length > 0) { tbody.innerHTML = res.data.map(p => ` <tr> <td>${p.sort_order}</td> <td><strong>${escapeHtml(p.title)}</strong></td> <td>${p.category}</td> <td><span class="badge badge-${p.status}">${p.status}</span></td> <td> <button class="btn btn-primary btn-sm" onclick="window.verifyProgram(${p.id})">ตรวจสอบ</button> <button class="btn btn-outline btn-sm" onclick="window.editProgram(${p.id})">แก้ไข</button> <button class="btn btn-danger-outline btn-sm" onclick="window.deleteProgram(${p.id})">ลบ</button> </td> </tr> `).join(''); } else { tbody.innerHTML = '<tr><td colspan="5" class="text-center">ไม่มีข้อมูลโครงการ</td></tr>'; } } catch(err) { tbody.innerHTML = '<tr><td colspan="5" class="text-center" style="color:red">เกิดข้อผิดพลาด</td></tr>'; } } window.showProgramForm = async function(id = null) { let program = { title: '', title_en: '', slug: '', short_desc: '', short_desc_en: '', content: '', content_en: '', category: 'leadership', status: 'active', sort_order: 0 }; if (id) { const res = await fetchAuth(`/programs/${id}`); // Assuming we have GET /api/programs/:id but wait... // Let's check programs.js again. Ah, it has GET /api/programs/:slug. // I might need to add GET /api/programs/admin/:id or just use slug if I have it. // Actually, the list has ID. Let's assume there's a way to get by ID or I'll just use the list data. // For simplicity, I'll fetch the whole list and find it, OR I'll add the route to server. } const html = ` <form id="program-form" enctype="multipart/form-data"> <div class="form-group"><label>ชื่อโครงการ (TH)</label><input type="text" name="title" value="${program.title}" required></div> <div class="form-group"><label>ชื่อโครงการ (EN)</label><input type="text" name="title_en" value="${program.title_en}"></div> <div class="form-group"><label>Slug (URL)</label><input type="text" name="slug" value="${program.slug}" required placeholder="youth-leadership"></div> <div class="form-group"><label>คำอธิบายย่อ (TH)</label><textarea name="short_desc">${program.short_desc}</textarea></div> <div class="form-group"><label>หมวดหมู่</label> <select name="category"> <option value="leadership">ความเป็นผู้นำ</option> <option value="creative">สร้างสรรค์</option> <option value="community">ชุมชน</option> <option value="education">การศึกษา</option> <option value="environment">สิ่งแวดล้อม</option> </select> </div> <div class="form-group"><label>รูปภาพหน้าปก</label><input type="file" name="featured_image" accept="image/*"></div> <div class="form-group"><label>สถานะ</label> <select name="status"> <option value="active">Active</option> <option value="inactive">Inactive</option> <option value="coming_soon">Coming Soon</option> </select> </div> </form> `; openModal(id ? 'แก้ไขโครงการ' : 'เพิ่มโครงการใหม่', html, async () => { const form = document.getElementById('program-form'); const formData = new FormData(form); const res = await fetchAuth(id ? `/programs/${id}` : '/programs', { method: id ? 'PUT' : 'POST', body: formData }); if (res.success) { showToast(res.message); closeModal(); renderPage(); } else { showToast(res.message, 'error'); } }); } window.editProgram = async function(id) { // To edit, we need the data. I'll fetch it from the API. // I should check if programs.js has GET by ID. It doesn't seem to have it (only by slug). // I'll add a helper to fetch by ID or just use the slug from the list. // For now, I'll fetch the list and find it (hacky but works without server change) const res = await fetchAuth('/programs'); const program = res.data.find(p => p.id === id); if (!program) return; const html = ` <form id="program-form" enctype="multipart/form-data"> <div class="form-group"><label>ชื่อโครงการ (TH)</label><input type="text" name="title" value="${escapeHtml(program.title)}" required></div> <div class="form-group"><label>ชื่อโครงการ (EN)</label><input type="text" name="title_en" value="${escapeHtml(program.title_en)}"></div> <div class="form-group"><label>Slug (URL)</label><input type="text" name="slug" value="${escapeHtml(program.slug)}" required></div> <div class="form-group"><label>คำอธิบายย่อ (TH)</label><textarea name="short_desc">${escapeHtml(program.short_desc)}</textarea></div> <div class="form-group"><label>หมวดหมู่</label> <select name="category"> <option value="leadership" ${program.category === 'leadership' ? 'selected' : ''}>ความเป็นผู้นำ</option> <option value="creative" ${program.category === 'creative' ? 'selected' : ''}>สร้างสรรค์</option> <option value="community" ${program.category === 'community' ? 'selected' : ''}>ชุมชน</option> <option value="education" ${program.category === 'education' ? 'selected' : ''}>การศึกษา</option> <option value="environment" ${program.category === 'environment' ? 'selected' : ''}>สิ่งแวดล้อม</option> </select> </div> <div class="form-group"> <label>รูปภาพหน้าปก (ปล่อยว่างหากไม่ต้องการเปลี่ยน)</label> ${program.featured_image ? `<img src="${program.featured_image}" style="width:100px; display:block; margin-bottom:10px;">` : ''} <input type="file" name="featured_image" accept="image/*"> </div> <div class="form-group"><label>สถานะ</label> <select name="status"> <option value="active" ${program.status === 'active' ? 'selected' : ''}>Active</option> <option value="inactive" ${program.status === 'inactive' ? 'selected' : ''}>Inactive</option> <option value="coming_soon" ${program.status === 'coming_soon' ? 'selected' : ''}>Coming Soon</option> </select> </div> </form> `; openModal('แก้ไขโครงการ', html, async () => { const form = document.getElementById('program-form'); const formData = new FormData(form); const res = await fetchAuth(`/programs/${id}`, { method: 'POST', body: formData }); if (res.success) { showToast(res.message); closeModal(); renderPage(); } else { showToast(res.message, 'error'); } }); } /** * -------------------------------------------------------------------------- * 🚀 WORLD-CLASS MILITARY-GRADE DELETE PROTOCOL (AUDIT-ENABLED) * The most detailed deletion system for high-accountability environments. * -------------------------------------------------------------------------- */ async function performSecureDelete(endpoint, id, entityName, detailInfo = '') { if (!id || isNaN(id)) { showToast('ID ข้อมูลไม่ถูกต้อง', 'error'); return; } const html = ` <div style="text-align:center; padding:10px 0;"> <div style="font-size:3.5rem; margin-bottom:15px; color:var(--red);">⚠️</div> <h2 style="color:var(--text); margin-bottom:10px;">ยืนยันการลบแบบถาวร</h2> <p style="color:var(--text-muted); margin-bottom:20px;"> คุณกำลังจะลบ${entityName}: <br> <strong style="color:var(--red); font-size:1.1rem;">${escapeHtml(detailInfo)}</strong> </p> <div style="text-align:left; background:#FFF5F5; padding:20px; border-radius:12px; border:1px solid #FED7D7; margin-bottom:20px;"> <label style="font-weight:700; font-size:0.9rem; color:#C53030; display:block; margin-bottom:8px;">ระบุเหตุผลการลบ (บันทึกเข้าระบบ Audit Log):</label> <select id="delete-reason" class="form-group" style="width:100%; padding:10px; border-radius:8px; border:1px solid #FEB2B2;"> <option value="error">สร้างข้อมูลผิดพลาด (Human Error)</option> <option value="duplicate">ข้อมูลซ้ำซ้อน (Duplicate Data)</option> <option value="obsolete">ข้อมูลล้าสมัย / ไม่ใช้งานแล้ว (Obsolete)</option> <option value="policy">ทำผิดนโยบายมูลนิธิ (Policy Violation)</option> <option value="other">อื่นๆ (ระบุในหมายเหตุ)</option> </select> <textarea id="delete-note" placeholder="กรุณาระบุหมายเหตุเพิ่มเติมสำหรับการลบครั้งนี้..." style="width:100%; margin-top:12px; height:80px; padding:10px; border-radius:8px; border:1px solid #FEB2B2;"></textarea> </div> <p style="color:var(--text-muted); font-size:0.8rem; line-height:1.4;"> * การดำเนินการนี้จะลบข้อมูลออกจากฐานข้อมูลทันทีและไม่สามารถกู้คืนได้ <br> ระบบจะบันทึก <strong>ชื่อผู้ลบ, เวลา, และเหตุผล</strong> ไว้ในระบบตรวจสอบความปลอดภัย </p> </div> `; openModal(`ระบบความปลอดภัย: ยืนยันการลบ`, html, async () => { const reason = document.getElementById('delete-reason').value; const note = document.getElementById('delete-note').value; if (!note && reason === 'other') { showToast('กรุณาระบุหมายเหตุเมื่อเลือกเหตุผลอื่นๆ', 'error'); return; } try { const res = await fetchAuth(`${endpoint}/${id}`, { method: 'DELETE', body: { delete_reason: reason, delete_note: note } }); if (res.success) { showToast(res.message || 'ลบข้อมูลและบันทึก Log สำเร็จ'); closeModal(); renderPage(); } else { showToast(res.message || 'ไม่สามารถลบได้', 'error'); } } catch (err) { showToast('เกิดข้อผิดพลาดทางเทคนิค', 'error'); } }, 'ยืนยันการลบทันที (บันทึก Log)', true); } window.deleteProgram = async (id) => { const res = await fetchAuth('/programs'); const item = res.data.find(p => p.id === id); performSecureDelete('/programs', id, 'โครงการ', item ? item.title : ''); }; window.deleteNews = async (id) => { const res = await fetchAuth('/news/admin/all'); const item = res.data.find(n => n.id === id); performSecureDelete('/news', id, 'ข่าวสาร', item ? item.title : ''); }; // ------------------------------------------ // NEWS // ------------------------------------------ async function renderNews(content) { content.innerHTML = ` <div class="card"> <div class="card-header"> <h3 class="card-title">จัดการข่าวสาร</h3> <button class="btn btn-primary btn-sm" onclick="showNewsForm()">+ เพิ่มข่าวใหม่</button> </div> <div class="card-body"> <div class="table-wrapper"> <table> <thead> <tr> <th>วันที่</th> <th>หัวข้อ</th> <th>สถานะ</th> <th>จัดการ</th> </tr> </thead> <tbody id="news-tbody"> <tr><td colspan="4" class="text-center">กำลังโหลด...</td></tr> </tbody> </table> </div> </div> </div> `; try { const res = await fetchAuth('/news/admin/all'); const tbody = document.getElementById('news-tbody'); if(res.data && res.data.length > 0) { tbody.innerHTML = res.data.map(n => ` <tr> <td>${new Date(n.created_at).toLocaleDateString('th-TH')}</td> <td><strong>${escapeHtml(n.title)}</strong></td> <td><span class="badge badge-${n.status}">${n.status}</span></td> <td> <button class="btn btn-primary btn-sm" onclick="window.verifyNews(${n.id})">ตรวจสอบ</button> <button class="btn btn-outline btn-sm" onclick="window.editNews(${n.id})">แก้ไข</button> <button class="btn btn-danger-outline btn-sm" onclick="window.deleteNews(${n.id})">ลบ</button> </td> </tr> `).join(''); } else { tbody.innerHTML = '<tr><td colspan="4" class="text-center">ไม่มีข้อมูลข่าวสาร</td></tr>'; } } catch(err) { tbody.innerHTML = '<tr><td colspan="4" class="text-center" style="color:red">เกิดข้อผิดพลาด</td></tr>'; } } window.showNewsForm = function() { const html = ` <form id="news-form" enctype="multipart/form-data"> <div class="form-group"><label>หัวข้อข่าว (TH)</label><input type="text" name="title" id="news-title-input" required></div> <div class="form-group"><label>Slug (URL)</label><input type="text" name="slug" id="news-slug-input" required placeholder="news-title-slug"></div> <div class="form-group"><label>บทคัดย่อ</label><textarea name="excerpt"></textarea></div> <div class="form-group"><label>เนื้อหา</label><textarea name="content" style="height:150px;"></textarea></div> <div class="form-group"><label>รูปภาพประกอบ</label><input type="file" name="featured_image" accept="image/*"></div> <div class="form-group"><label>สถานะ</label> <select name="status"> <option value="draft">Draft</option> <option value="published">Published</option> </select> </div> </form> `; openModal('เพิ่มข่าวใหม่', html, async () => { const form = document.getElementById('news-form'); const formData = new FormData(form); const res = await fetchAuth('/news', { method: 'POST', body: formData }); if (res.success) { showToast(res.message); closeModal(); renderPage(); } else { showToast(res.message, 'error'); } }); // Auto-generate slug const titleInput = document.getElementById('news-title-input'); const slugInput = document.getElementById('news-slug-input'); if (titleInput && slugInput) { titleInput.addEventListener('input', () => { slugInput.value = titleInput.value .toLowerCase() .replace(/[^a-z0-9ก-๙\s]/g, '') .replace(/\s+/g, '-'); }); } } window.editNews = async function(id) { const res = await fetchAuth('/news/admin/all'); const news = res.data.find(n => n.id === id); if (!news) return; const html = ` <form id="news-form" enctype="multipart/form-data"> <div class="form-group"><label>หัวข้อข่าว (TH)</label><input type="text" name="title" value="${escapeHtml(news.title)}" required></div> <div class="form-group"><label>Slug (URL)</label><input type="text" name="slug" value="${escapeHtml(news.slug)}" required></div> <div class="form-group"><label>บทคัดย่อ</label><textarea name="excerpt">${escapeHtml(news.excerpt)}</textarea></div> <div class="form-group"><label>เนื้อหา</label><textarea name="content" style="height:150px;">${escapeHtml(news.content)}</textarea></div> <div class="form-group"> <label>รูปภาพประกอบ</label> ${news.featured_image ? `<img src="${news.featured_image}" style="width:100px; display:block; margin-bottom:10px;">` : ''} <input type="file" name="featured_image" accept="image/*"> </div> <div class="form-group"><label>สถานะ</label> <select name="status"> <option value="draft" ${news.status === 'draft' ? 'selected' : ''}>Draft</option> <option value="published" ${news.status === 'published' ? 'selected' : ''}>Published</option> </select> </div> </form> `; openModal('แก้ไขข่าว', html, async () => { const form = document.getElementById('news-form'); const formData = new FormData(form); const res = await fetchAuth(`/news/${id}`, { method: 'POST', body: formData }); if (res.success) { showToast(res.message); closeModal(); renderPage(); } else { showToast(res.message, 'error'); } }); } // ------------------------------------------ // CONTACTS // ------------------------------------------ async function renderContacts(content) { content.innerHTML = ` <div class="card"> <div class="card-header"> <h3 class="card-title">ข้อความจากผู้ติดต่อ</h3> </div> <div class="card-body"> <div class="table-wrapper"> <table> <thead> <tr> <th>วันที่</th> <th>จาก</th> <th>หัวข้อ</th> <th>สถานะ</th> <th>จัดการ</th> </tr> </thead> <tbody id="contacts-tbody"> <tr><td colspan="5" class="text-center">กำลังโหลด...</td></tr> </tbody> </table> </div> </div> </div> `; try { const res = await fetchAuth('/contacts'); const tbody = document.getElementById('contacts-tbody'); if(res.data && res.data.length > 0) { tbody.innerHTML = res.data.map(c => ` <tr class="${c.status === 'new' ? 'unread-row' : ''}"> <td>${new Date(c.created_at).toLocaleDateString('th-TH')}</td> <td> <strong>${escapeHtml(c.name)}</strong><br> <small>${escapeHtml(c.email)}</small> </td> <td>${escapeHtml(c.subject || '-')}</td> <td><span class="badge badge-${c.status}">${c.status}</span></td> <td> <button class="btn btn-outline btn-sm" onclick="viewContact(${c.id})">ดูรายละเอียด</button> <button class="btn btn-outline btn-sm" onclick="deleteContact(${c.id})">ลบ</button> </td> </tr> `).join(''); } else { tbody.innerHTML = '<tr><td colspan="5" class="text-center">ไม่มีข้อความติดต่อ</td></tr>'; } } catch(err) { tbody.innerHTML = '<tr><td colspan="5" class="text-center" style="color:red">เกิดข้อผิดพลาด</td></tr>'; } } window.viewContact = async function(id) { try { const resAll = await fetchAuth('/contacts'); const contact = resAll.data.find(c => c.id === id); if (!contact) return; // Mark as read await fetchAuth(`/contacts/${id}/read`, { method: 'PUT' }); const html = ` <div style="line-height:1.6;"> <p><strong>จาก:</strong> ${escapeHtml(contact.name)} (${escapeHtml(contact.email)})</p> <p><strong>เบอร์โทร:</strong> ${escapeHtml(contact.phone || '-')}</p> <p><strong>วันที่:</strong> ${new Date(contact.created_at).toLocaleString('th-TH')}</p> <hr style="margin:15px 0; border:0; border-top:1px solid #eee;"> <p><strong>หัวข้อ:</strong> ${escapeHtml(contact.subject || '-')}</p> <div style="background:#f9fafb; padding:15px; border-radius:6px; margin-top:10px;"> ${escapeHtml(contact.message).replace(/\n/g, '<br>')} </div> </div> `; openModal('รายละเอียดข้อความติดต่อ', html, () => { closeModal(); renderPage(); }); document.getElementById('btn-save').textContent = 'ปิด'; document.getElementById('btn-cancel').style.display = 'none'; } catch(err) { showToast('เกิดข้อผิดพลาด', 'error'); } } window.deleteContact = async function(id) { const res = await fetchAuth('/contacts'); const item = res.data.find(c => c.id === id); const detail = item ? `ข้อความจาก: ${item.name} (${item.email})` : `ID: #${id}`; performSecureDelete('/contacts', id, 'ข้อความติดต่อ', detail); } // ------------------------------------------ // SETTINGS // ------------------------------------------ async function renderSettings(content) { content.innerHTML = ` <div class="card"> <div class="card-header"> <h3 class="card-title">ตั้งค่าเว็บไซต์</h3> <button class="btn btn-primary btn-sm" onclick="saveSettings()">บันทึกทั้งหมด</button> </div> <div class="card-body"> <div style="background:var(--primary); color:#fff; padding:15px; border-radius:12px; margin-bottom:20px; font-size:0.9rem;"> 💡 คุณสามารถแก้ไขหัวข้อหลักของหน้าข่าวสารและตัววิ่งข่าว (Breaking News) ได้ที่นี่ </div> <form id="settings-form"> <div id="settings-container">กำลังโหลด...</div> </form> </div> </div> `; try { const res = await fetchAuth('/settings/all'); const container = document.getElementById('settings-container'); if(res.data) { let html = ''; // Map common keys to friendly labels const friendlyLabels = { 'breaking_news': 'ข้อความประกาศ (Breaking News Ticker)', 'news_page_title': 'หัวข้อหน้าข่าวสาร (News Page Title)', 'news_page_subtitle': 'คำโปรยหน้าข่าวสาร (News Page Subtitle)', 'site_name': 'ชื่อเว็บไซต์ (Site Name)', 'hero_title': 'หัวข้อหลักหน้าแรก (Hero Title)', 'hero_subtitle': 'คำบรรยายหน้าแรก (Hero Subtitle)' }; // High Priority Keys (Show these first) const priorityKeys = ['breaking_news', 'news_page_title', 'news_page_subtitle']; const priorityData = res.data.filter(s => priorityKeys.includes(s.key)); const otherData = res.data.filter(s => !priorityKeys.includes(s.key)); if(priorityData.length > 0) { html += `<h4 style="margin:10px 0 15px; color:var(--red); text-transform:uppercase; border-bottom:2px solid var(--red); padding-bottom:8px;">📢 ส่วนจัดการประกาศและหน้าข่าว (Breaking News)</h4>`; priorityData.forEach(s => { const label = friendlyLabels[s.key] || s.description || s.key; html += ` <div class="form-group mb-4"> <label style="font-weight:700; display:block; margin-bottom:8px;">${label}</label> <textarea class="setting-input" data-key="${s.key}" style="width:100%; padding:12px; border:2px solid #fee2e2; border-radius:8px; font-size:1rem; height:80px; background:#fffcfc;">${escapeHtml(s.value)}</textarea> </div> `; }); } // Other groups let currentGroup = ''; otherData.forEach(s => { if(s.group_name !== currentGroup) { currentGroup = s.group_name; html += `<h4 style="margin:40px 0 15px; color:var(--primary); text-transform:uppercase; border-bottom:2px solid #eee; padding-bottom:8px;">📁 หมวดหมู่: ${currentGroup}</h4>`; } const label = friendlyLabels[s.key] || s.description || s.key; const isLongText = s.value && s.value.length > 50; html += ` <div class="form-group mb-4"> <label style="font-weight:600; display:block; margin-bottom:8px;">${label}</label> ${isLongText || s.key.includes('address') ? `<textarea class="setting-input" data-key="${s.key}" style="width:100%; padding:12px; border:1px solid #cbd5e1; border-radius:8px; font-size:1rem; height:100px;">${escapeHtml(s.value)}</textarea>` : `<input type="text" class="setting-input" data-key="${s.key}" value="${escapeHtml(s.value)}" style="width:100%; padding:12px; border:1px solid #cbd5e1; border-radius:8px; font-size:1rem;">` } </div> `; }); container.innerHTML = html; } } catch(err) { container.innerHTML = 'เกิดข้อผิดพลาดในการโหลดการตั้งค่า'; } } window.saveSettings = async function() { const confirmHtml = ` <div style="text-align:center; padding:20px;"> <div style="background:#f0f4ff; width:80px; height:80px; border-radius:50%; display:flex; align-items:center; justify-content:center; margin:0 auto 20px;"> <span class="material-icons" style="font-size:3rem; color:var(--primary);">save</span> </div> <h3 style="margin-bottom:10px; color:#1e293b;">ยืนยันการบันทึกข้อมูล</h3> <p style="font-size:1.1rem; color:#64748b; line-height:1.5;">คุณต้องการบันทึกการเปลี่ยนแปลงทั้งหมดของการตั้งค่าเว็บไซต์ใช่หรือไม่?</p> <div style="margin-top:15px; padding:10px; background:#f8fafc; border-radius:8px; font-size:0.9rem; color:#94a3b8;"> การเปลี่ยนแปลงนี้จะส่งผลต่อการแสดงผลหน้าเว็บหลักทันที </div> </div> `; openModal('ยืนยันการดำเนินการ', confirmHtml, async () => { const settings = {}; document.querySelectorAll('.setting-input').forEach(input => { settings[input.dataset.key] = input.value; }); try { const res = await fetchAuth('/settings', { method: 'PUT', body: { settings } }); if(res.success) { showToast('บันทึกการตั้งค่าสำเร็จ'); renderPage(); // Refresh to ensure UI matches state } else { showToast(res.message || 'ไม่สามารถบันทึกได้', 'error'); } } catch(err) { showToast('เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์', 'error'); } }); } // ------------------------------------------ // CMS PAGES (หน้าเพจอิสระ) // ------------------------------------------ async function renderCMSPages(content) { content.innerHTML = ` <div class="card"> <div class="card-header"> <h3 class="card-title">จัดการหน้าเพจ (CMS)</h3> <button class="btn btn-primary btn-sm" onclick="showPageForm()">+ สร้างหน้าใหม่</button> </div> <div class="card-body"> <div class="table-wrapper"> <table> <thead> <tr><th>ชื่อหน้า</th><th>Slug (URL)</th><th>สถานะ</th><th>แก้ไขล่าสุด</th><th>จัดการ</th></tr> </thead> <tbody id="cms-pages-tbody"><tr><td colspan="5" class="text-center">กำลังโหลดข้อมูล...</td></tr></tbody> </table> </div> </div> </div> `; try { // Fetch using /admin sub-resource to see all status types (Draft, Published) const res = await fetchAuth('/pages/admin'); const tbody = document.getElementById('cms-pages-tbody'); if (res.data && res.data.length > 0) { tbody.innerHTML = res.data.map(p => ` <tr> <td> <div style="font-weight:600;">${escapeHtml(p.title)}</div> <small style="color:#64748b;">${escapeHtml(p.title_en || '-')}</small> </td> <td> <div style="display:flex; align-items:center; gap:8px;"> <code style="background:#f1f5f9; padding:4px 8px; border-radius:6px; font-size:0.85rem;">/${p.slug}</code> <a href="/${p.slug}" target="_blank" title="ดูตัวอย่างหน้าเว็บ" style="color:var(--primary); font-size:1.1rem; display:flex; align-items:center;"><span class="material-icons" style="font-size:1.2rem;">open_in_new</span></a> </div> </td> <td> <span class="badge badge-${p.status === 'published' ? 'success' : 'pending'}"> ${p.status === 'published' ? 'เผยแพร่แล้ว' : 'ฉบับร่าง'} </span> </td> <td>${new Date(p.updated_at || p.created_at).toLocaleDateString('th-TH')}</td> <td> <div style="display:flex; gap:6px;"> <a href="/${p.slug}" target="_blank" class="btn btn-outline btn-sm" title="ดูตัวอย่าง"><span class="material-icons" style="font-size:14px;">visibility</span></a> <button class="btn btn-outline btn-sm" onclick="editCMSPage(${p.id})">แก้ไข</button> <button class="btn btn-outline btn-sm" onclick="deleteCMSPage(${p.id})" style="color:var(--red);">ลบ</button> </div> </td> </tr> `).join(''); } else { tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4">ยังไม่มีหน้าเพจเพิ่มเข้ามา</td></tr>'; } } catch (err) { document.getElementById('cms-pages-tbody').innerHTML = '<tr><td colspan="5" class="text-center py-4" style="color:red;">ไม่สามารถโหลดข้อมูลได้</td></tr>'; } } window.showPageForm = async (id = null) => { let page = { title: '', title_en: '', slug: '', content: '', content_en: '', status: 'draft', template: 'default' }; if (id) { try { const res = await fetchAuth('/pages/admin'); page = res.data.find(p => p.id === id) || page; } catch(e) { showToast('ไม่พบข้อมูลหน้าเพจ', 'error'); return; } } const html = ` <form id="cms-page-form"> <div style="display:grid; grid-template-columns:1fr 1fr; gap:15px;"> <div class="form-group"><label>หัวข้อหน้าเพจ (TH)</label><input type="text" name="title" id="page-title-th" value="${escapeHtml(page.title)}" required></div> <div class="form-group"><label>หัวข้อหน้าเพจ (EN)</label><input type="text" name="title_en" value="${escapeHtml(page.title_en)}"></div> </div> <div class="form-group"> <label>Slug (URL) <small style="color:var(--primary);">*ระบุที่อยู่ของหน้าเพจ เช่น about-us</small></label> <div style="display:flex; gap:10px; align-items:center;"> <input type="text" name="slug" id="page-slug" value="${escapeHtml(page.slug)}" required placeholder="slug-name" style="flex:1;"> <a id="slug-preview-link" href="/${page.slug}" target="_blank" class="btn btn-outline btn-sm" style="height:40px; display:${page.slug ? 'flex' : 'none'}; align-items:center;">👁️ ดูหน้าเว็บจริง</a> </div> <small id="slug-full-url" style="color:#64748b; display:block; margin-top:4px;">ลิงก์ที่จะปรากฏ: ${window.location.origin}/${page.slug || '...'}</small> </div> <div class="form-group"><label>เนื้อหา (TH)</label><textarea name="content" style="height:200px; font-family:var(--font-th);">${escapeHtml(page.content)}</textarea></div> <div class="form-group"><label>เนื้อหา (EN)</label><textarea name="content_en" style="height:200px; font-family:var(--font-th);">${escapeHtml(page.content_en)}</textarea></div> <div class="grid-2"> <div class="form-group"><label>เทมเพลต</label> <select name="template" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;"> <option value="default" ${page.template === 'default' ? 'selected' : ''}>Default</option> <option value="full-width" ${page.template === 'full-width' ? 'selected' : ''}>Full Width</option> <option value="landing" ${page.template === 'landing' ? 'selected' : ''}>Landing</option> </select> </div> <div class="form-group"><label>สถานะ</label> <select name="status" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;"> <option value="draft" ${page.status === 'draft' ? 'selected' : ''}>ฉบับร่าง (Draft)</option> <option value="published" ${page.status === 'published' ? 'selected' : ''}>เผยแพร่ (Published)</option> </select> </div> </div> </form> `; openModal(id ? 'แก้ไขหน้าเพจ' : 'สร้างหน้าเพจใหม่', html, async () => { const form = document.getElementById('cms-page-form'); const formData = new FormData(form); const body = Object.fromEntries(formData.entries()); const res = await fetchAuth(id ? `/pages/${id}` : '/pages', { method: id ? 'PUT' : 'POST', body }); if (res.success) { showToast(res.message); closeModal(); renderPage(); } else { showToast(res.message, 'error'); } }); // Slug Preview & Auto-generation logic const titleIn = document.getElementById('page-title-th'); const slugIn = document.getElementById('page-slug'); const slugLink = document.getElementById('slug-preview-link'); const slugFull = document.getElementById('slug-full-url'); const updateSlugUI = (val) => { slugLink.href = `/${val}`; slugLink.style.display = val ? 'flex' : 'none'; slugFull.textContent = `ลิงก์ที่จะปรากฏ: ${window.location.origin}/${val || '...'}`; }; slugIn.oninput = () => updateSlugUI(slugIn.value); if(!id) { titleIn.oninput = () => { const val = titleIn.value.toLowerCase() .replace(/[^a-z0-9ก-๙\s]/g, '') .replace(/\s+/g, '-'); slugIn.value = val; updateSlugUI(val); }; } }; window.editCMSPage = (id) => showPageForm(id); window.deleteCMSPage = async (id) => { const res = await fetchAuth('/pages/admin'); const item = res.data.find(p => p.id === id); const detail = item ? `หน้าเพจ: ${item.title} (/${item.slug})` : `ID: #${id}`; performSecureDelete('/pages', id, 'หน้าเพจ CMS', detail); }; // ------------------------------------------ // MENUS // ------------------------------------------ async function renderMenus(content) { content.innerHTML = ` <div class="card"> <div class="card-header"> <h3 class="card-title">เมนูนำทาง (Navigation)</h3> <button class="btn btn-primary btn-sm" onclick="showMenuForm()">+ เพิ่มเมนู</button> </div> <div class="card-body"> <div class="table-wrapper"> <table> <thead><tr><th>ชื่อเมนู (TH)</th><th>ชื่อเมนู (EN)</th><th>URL</th><th>ลำดับ</th><th>จัดการ</th></tr></thead> <tbody id="menus-tbody"><tr><td colspan="5" class="text-center">กำลังโหลด...</td></tr></tbody> </table> </div> </div> </div> `; try { const res = await fetchAuth('/menus/all'); const tbody = document.getElementById('menus-tbody'); if (res.data) { // Sort menus to show sub-menus under their parents const sorted = []; const parents = res.data.filter(m => !m.parent_id); const subs = res.data.filter(m => m.parent_id); parents.forEach(p => { sorted.push(p); subs.filter(s => s.parent_id == p.id).forEach(s => sorted.push(s)); }); tbody.innerHTML = sorted.map(m => ` <tr style="${m.parent_id ? 'background:#f8fafc;' : ''}"> <td> <span style="${m.parent_id ? 'padding-left:20px; color:#64748b;' : 'font-weight:600;'}"> ${m.parent_id ? '↳ ' : ''}${escapeHtml(m.title)} </span> </td> <td>${escapeHtml(m.title_en || '-')}</td> <td><code>${m.url}</code></td> <td>${m.sort_order}</td> <td> <div style="display:flex; gap:6px;"> <button class="btn btn-outline btn-sm" onclick="editMenu(${m.id})">แก้ไข</button> <button class="btn btn-outline btn-sm" onclick="deleteMenu(${m.id})" style="color:var(--red);">ลบ</button> </div> </td> </tr> `).join(''); } } catch(err) { document.getElementById('menus-tbody').innerHTML = '<tr><td colspan="5" class="text-center">ไม่สามารถโหลดข้อมูลได้</td></tr>'; } } window.showMenuForm = async function(id = null) { let menu = { title: '', title_en: '', url: '', parent_id: null, sort_order: 0, target: '_self', is_active: 1 }; // Fetch all menus for the parent dropdown let parents = []; try { const allRes = await fetchAuth('/menus/all'); parents = (allRes.data || []).filter(m => !m.parent_id && m.id != id); if (id) { menu = allRes.data.find(m => m.id == id) || menu; } } catch(e) {} const html = ` <form id="menu-form"> <div class="form-group"><label>ชื่อเมนู (TH)</label><input type="text" name="title" value="${escapeHtml(menu.title)}" required></div> <div class="form-group"><label>ชื่อเมนู (EN)</label><input type="text" name="title_en" value="${escapeHtml(menu.title_en || '')}"></div> <div class="form-group"><label>URL</label><input type="text" name="url" value="${escapeHtml(menu.url)}" required placeholder="/contact หรือ https://..."></div> <div class="form-group"> <label>เมนูหลัก (ถ้าเป็นเมนูย่อย)</label> <select name="parent_id" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;"> <option value="">-- เป็นเมนูหลัก --</option> ${parents.map(p => `<option value="${p.id}" ${menu.parent_id == p.id ? 'selected' : ''}>${escapeHtml(p.title)}</option>`).join('')} </select> </div> <div class="grid-2"> <div class="form-group"><label>ลำดับการแสดงผล</label><input type="number" name="sort_order" value="${menu.sort_order}" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;"></div> <div class="form-group"><label>การเปิดลิงก์</label> <select name="target" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;"> <option value="_self" ${menu.target === '_self' ? 'selected' : ''}>เปิดในหน้าเดิม (_self)</option> <option value="_blank" ${menu.target === '_blank' ? 'selected' : ''}>เปิดในแท็บใหม่ (_blank)</option> </select> </div> </div> </form> `; openModal(id ? 'แก้ไขเมนู' : 'เพิ่มเมนูใหม่', html, async () => { const form = document.getElementById('menu-form'); const formData = new FormData(form); const body = Object.fromEntries(formData.entries()); const res = await fetchAuth(id ? `/menus/${id}` : '/menus', { method: id ? 'PUT' : 'POST', body }); if (res.success) { showToast(res.message); closeModal(); renderMenus(document.getElementById('page-content')); // Refresh current view } else { showToast(res.message, 'error'); } }); } window.editMenu = (id) => showMenuForm(id); window.deleteMenu = async function(id) { const res = await fetchAuth('/menus/all'); const item = res.data.find(m => m.id === id); const detail = item ? `เมนู: ${item.title}` : `ID: #${id}`; performSecureDelete('/menus', id, 'เมนูนำทาง', detail); } // ------------------------------------------ // GALLERY // ------------------------------------------ async function renderGallery(content) { content.innerHTML = ` <div class="card"> <div class="card-header"> <h3 class="card-title">คลังรูปภาพและกิจกรรม</h3> <button class="btn btn-primary btn-sm" onclick="showGalleryForm()">+ อัปโหลดรูปภาพ</button> </div> <div class="card-body"> <div class="grid-4" id="gallery-grid"> <div class="text-center" style="grid-column: 1/-1;">กำลังโหลด...</div> </div> </div> </div> `; const res = await fetchAuth('/gallery'); const grid = document.getElementById('gallery-grid'); if (res.data) { grid.innerHTML = res.data.map(img => ` <div class="card" style="margin-bottom:0;"> <img src="${img.file_path}" style="width:100%; height:150px; object-fit:cover;"> <div style="padding:12px;"> <p style="font-size:12px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${escapeHtml(img.title)}</p> <div style="display:flex; justify-content:space-between; margin-top:8px;"> <small class="text-muted">${img.album || 'ทั่วไป'}</small> <button class="btn btn-outline btn-sm" onclick="deleteGalleryItem(${img.id})" style="padding:2px 6px;">ลบ</button> </div> </div> </div> `).join(''); } } // ------------------------------------------ // FUNDING GOALS // ------------------------------------------ async function renderFunding(content) { content.innerHTML = ` <div class="card"> <div class="card-header"> <h3 class="card-title">เป้าหมายการเงิน (Funding Goals)</h3> <button class="btn btn-primary btn-sm" onclick="showFundingForm()">+ เพิ่มเป้าหมาย</button> </div> <div class="card-body"> <div id="funding-list" class="grid-3">กำลังโหลด...</div> </div> </div> `; const res = await fetchAuth('/funding/admin/all'); const list = document.getElementById('funding-list'); if (res.success && res.data.length > 0) { list.innerHTML = res.data.map(goal => { const percent = Math.min(100, Math.round((goal.current_amount / goal.target_amount) * 100)); return ` <div class="card stat-card" style="border-left: 4px solid ${goal.is_active ? 'var(--success)' : '#cbd5e1'}; margin-bottom:0;"> <div style="display:flex; justify-content:space-between; align-items:start;"> <div class="stat-label">${escapeHtml(goal.title)}</div> <span class="badge badge-${goal.is_active ? 'success' : 'pending'}">${goal.is_active ? 'Active' : 'Inactive'}</span> </div> <div class="stat-value">฿${Number(goal.current_amount).toLocaleString()}</div> <div style="margin-top:10px; height:8px; background:#e2e8f0; border-radius:10px; overflow:hidden;"> <div style="width:${percent}%; height:100%; background:var(--primary); transition: width 0.5s ease;"></div> </div> <div style="display:flex; justify-content:space-between; font-size:12px; margin-top:5px; color:var(--text-muted);"> <span>${percent}%</span> <span>เป้าหมาย: ฿${Number(goal.target_amount).toLocaleString()}</span> </div> <div style="margin-top:15px; display:flex; gap:8px;"> <button class="btn btn-outline btn-sm btn-full" onclick="editFunding(${goal.id})">แก้ไข</button> <button class="btn btn-outline btn-sm" onclick="deleteFunding(${goal.id})">ลบ</button> </div> </div> `; }).join(''); } else { list.innerHTML = '<div style="grid-column:1/-1; text-align:center; padding:40px;">ไม่มีข้อมูลเป้าหมายการเงิน</div>'; } } window.showGalleryForm = () => { const html = ` <form id="gallery-form"> <div class="form-group"><label>หัวข้อรูปภาพ</label><input type="text" name="title" required></div> <div class="form-group"><label>อัลบั้ม/หมวดหมู่</label><input type="text" name="album" placeholder="กิจกรรมปี 2567"></div> <div class="form-group"><label>เลือกรูปภาพ (หลายรูปได้)</label><input type="file" name="gallery" multiple accept="image/*" required onchange="previewImages(event)"></div> <div id="image-previews" style="display:grid; grid-template-columns:repeat(4,1fr); gap:10px; margin-top:10px;"></div> </form> `; openModal('อัปโหลดรูปภาพ', html, async () => { const formData = new FormData(document.getElementById('gallery-form')); const res = await fetchAuth('/gallery', { method: 'POST', body: formData }); if (res.success) { closeModal(); renderPage(); showToast('อัปโหลดสำเร็จ'); } else { showToast(res.message || 'เกิดข้อผิดพลาด', 'error'); } }); }; window.previewImages = (e) => { const container = document.getElementById('image-previews'); container.innerHTML = ''; [...e.target.files].forEach(file => { const reader = new FileReader(); reader.onload = (re) => { container.innerHTML += `<img src="${re.target.result}" style="width:100%; height:60px; object-fit:cover; border-radius:4px;">`; }; reader.readAsDataURL(file); }); }; window.deleteGalleryItem = async (id) => { const res = await fetchAuth('/gallery'); const item = res.data.find(g => g.id === id); const detail = item ? `รูปภาพ: ${item.title}` : `ID: #${id}`; performSecureDelete('/gallery', id, 'รูปภาพแกลเลอรี', detail); }; window.showMenuForm = async (id = null) => { let menu = { title: '', title_en: '', url: '', parent_id: null, sort_order: 0 }; const allMenusRes = await fetchAuth('/menus/all'); const parentMenus = allMenusRes.data ? allMenusRes.data.filter(m => !m.parent_id && m.id !== id) : []; if (id) { menu = allMenusRes.data.find(m => m.id === id) || menu; } const html = ` <form id="menu-form"> <div class="form-group"><label>ชื่อเมนู (TH)</label><input type="text" name="title" value="${escapeHtml(menu.title)}" required></div> <div class="form-group"><label>ชื่อเมนู (EN)</label><input type="text" name="title_en" value="${escapeHtml(menu.title_en)}"></div> <div class="form-group"><label>URL</label><input type="text" name="url" value="${escapeHtml(menu.url)}" required placeholder="/about"></div> <div class="form-group"><label>เมนูหลัก (ถ้าเป็นเมนูย่อย)</label> <select name="parent_id"> <option value="">-- เป็นเมนูหลัก --</option> ${parentMenus.map(pm => `<option value="${pm.id}" ${menu.parent_id === pm.id ? 'selected' : ''}>${escapeHtml(pm.title)}</option>`).join('')} </select> </div> <div class="form-group"><label>ลำดับการแสดงผล</label><input type="number" name="sort_order" value="${menu.sort_order}"></div> </form> `; openModal(id ? 'แก้ไขเมนู' : 'เพิ่มเมนูนำทาง', html, async () => { const formData = new FormData(document.getElementById('menu-form')); const body = Object.fromEntries(formData.entries()); if (!body.parent_id) body.parent_id = null; const res = await fetchAuth(id ? `/menus/${id}` : '/menus', { method: id ? 'PUT' : 'POST', body }); if (res.success) { showToast(res.message); closeModal(); renderPage(); } else { showToast(res.message, 'error'); } }); }; window.editMenu = (id) => showMenuForm(id); window.showFundingForm = async (id = null) => { let goal = { title: '', description: '', target_amount: 0, current_amount: 0, category: 'general', sort_order: 0, is_active: 1 }; if (id) { const res = await fetchAuth('/funding/admin/all'); goal = res.data.find(g => g.id === id) || goal; } const html = ` <form id="funding-form"> <div class="form-group"><label>หัวข้อเป้าหมาย (TH)</label><input type="text" name="title" value="${escapeHtml(goal.title)}" required></div> <div class="form-group"><label>หัวข้อเป้าหมาย (EN)</label><input type="text" name="title_en" value="${escapeHtml(goal.title_en)}"></div> <div class="form-group"><label>คำอธิบาย (TH)</label><textarea name="description">${escapeHtml(goal.description)}</textarea></div> <div class="form-group"><label>คำอธิบาย (EN)</label><textarea name="description_en">${escapeHtml(goal.description_en)}</textarea></div> <div class="grid-2"> <div class="form-group"><label>ยอดเป้าหมาย (฿)</label><input type="number" name="target_amount" value="${goal.target_amount}" required></div> <div class="form-group"><label>ยอดปัจจุบัน (฿)</label><input type="number" name="current_amount" value="${goal.current_amount}"></div> </div> <div class="grid-2"> <div class="form-group"><label>หมวดหมู่</label> <input type="text" name="category" value="${escapeHtml(goal.category)}" placeholder="daily, education, etc."> </div> <div class="form-group"><label>สถานะ</label> <select name="is_active"> <option value="1" ${goal.is_active ? 'selected' : ''}>Active</option> <option value="0" ${!goal.is_active ? 'selected' : ''}>Inactive</option> </select> </div> </div> <div class="form-group"><label>ลำดับ</label><input type="number" name="sort_order" value="${goal.sort_order}"></div> </form> `; openModal(id ? 'แก้ไขเป้าหมาย' : 'เพิ่มเป้าหมายการเงิน', html, async () => { const formData = new FormData(document.getElementById('funding-form')); const body = Object.fromEntries(formData.entries()); body.is_active = parseInt(body.is_active); const res = await fetchAuth(id ? `/funding/${id}` : '/funding', { method: id ? 'PUT' : 'POST', body }); if (res.success) { showToast(res.message); closeModal(); renderPage(); } else { showToast(res.message, 'error'); } }); }; window.editFunding = (id) => showFundingForm(id); window.deleteFunding = async (id) => { const res = await fetchAuth('/funding/admin/all'); const item = res.data.find(g => g.id === id); const detail = item ? `เป้าหมาย: ${item.title}` : `ID: #${id}`; performSecureDelete('/funding', id, 'เป้าหมายการเงิน', detail); }; window.deleteCMSPage = async (id) => { const res = await fetchAuth('/pages/admin/all'); const item = res.data.find(p => p.id === id); const detail = item ? `หน้าเพจ: ${item.title}` : `ID: #${id}`; performSecureDelete('/pages', id, 'หน้าเพจ (CMS)', detail); }; /** * -------------------------------------------------------------------------- * 🛡️ VERIFICATION & APPROVAL SYSTEM * Engineered for "Add Only, No Delete" World-Class Stability * -------------------------------------------------------------------------- */ window.verifyProgram = async function(id) { try { const res = await fetchAuth('/programs'); const program = res.data.find(p => p.id === id); if (!program) return; const html = ` <div class="verify-container" style="animation: fadeIn 0.3s ease;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; background:rgba(27, 58, 140, 0.05); padding:15px; border-radius:12px;"> <div> <span class="badge badge-${program.status}">${program.status.toUpperCase()}</span> <h2 style="margin:5px 0 0 0; color:var(--primary); font-size:1.5rem;">${escapeHtml(program.title)}</h2> </div> </div> ${program.featured_image ? `<img src="${program.featured_image}" style="width:100%; height:250px; object-fit:cover; border-radius:16px; margin-bottom:20px; border:4px solid white; box-shadow:var(--shadow);">` : ''} <div style="display:grid; grid-template-columns: 1fr 1fr; gap:15px; margin-bottom:20px;"> <div style="background:#f8fafc; padding:15px; border-radius:12px;"> <h4 style="margin-bottom:5px; color:var(--primary); font-size:0.9rem;">หมวดหมู่</h4> <p style="font-weight:600;">${program.category}</p> </div> <div style="background:#f8fafc; padding:15px; border-radius:12px;"> <h4 style="margin-bottom:5px; color:var(--primary); font-size:0.9rem;">ลำดับการแสดงผล</h4> <p style="font-weight:600;">${program.sort_order}</p> </div> </div> <div style="background:#f8fafc; padding:20px; border-radius:12px; border-left:4px solid var(--primary);"> <h4 style="margin-bottom:10px; color:var(--primary);">คำอธิบายโครงการ</h4> <p style="line-height:1.6; color:#475569;">${escapeHtml(program.short_desc || 'ไม่มีคำอธิบาย')}</p> </div> </div> `; const isPending = program.status !== 'active'; openModal(state.translations[state.lang].verify, html, isPending ? async () => { await window.approveStatus(id, 'programs', 'active'); } : () => closeModal()); const saveBtn = document.getElementById('btn-save'); if (saveBtn) { saveBtn.textContent = isPending ? state.translations[state.lang].approve : 'ปิด'; if (!isPending) { document.getElementById('btn-cancel').style.display = 'none'; } } } catch (err) { showToast('ไม่สามารถดึงข้อมูลได้', 'error'); } }; window.verifyNews = async function(id) { try { const res = await fetchAuth('/news/admin/all'); const news = res.data.find(n => n.id === id); if (!news) return; const html = ` <div class="verify-container" style="animation: fadeIn 0.3s ease;"> <div style="margin-bottom:20px; background:rgba(27, 58, 140, 0.05); padding:15px; border-radius:12px;"> <span class="badge badge-${news.status}">${news.status.toUpperCase()}</span> <h2 style="margin:10px 0; color:var(--primary); font-size:1.5rem;">${escapeHtml(news.title)}</h2> <small class="text-muted">วันที่สร้าง: ${new Date(news.created_at).toLocaleString('th-TH')}</small> </div> ${news.featured_image ? `<img src="${news.featured_image}" style="width:100%; max-height:300px; object-fit:cover; border-radius:16px; margin-bottom:20px; box-shadow:var(--shadow);">` : ''} <div style="background:#f8fafc; padding:20px; border-radius:12px; border-left:4px solid var(--primary);"> <h4 style="margin-bottom:10px; color:var(--primary);">บทคัดย่อ (Excerpt)</h4> <p style="font-style:italic; color:#475569; margin-bottom:15px;">${escapeHtml(news.excerpt || '-')}</p> <hr style="margin:15px 0; border:0; border-top:1px solid #e2e8f0;"> <h4 style="margin-bottom:10px; color:var(--primary);">เนื้อหาข่าว</h4> <div style="line-height:1.7; color:#334155;">${escapeHtml(news.content || '-').replace(/\n/g, '<br>')}</div> </div> </div> `; const isPending = news.status !== 'published'; openModal(state.translations[state.lang].verify, html, isPending ? async () => { await window.approveStatus(id, 'news', 'published'); } : () => closeModal()); const saveBtn = document.getElementById('btn-save'); if (saveBtn) { saveBtn.textContent = isPending ? state.translations[state.lang].approve : 'ปิด'; if (!isPending) { document.getElementById('btn-cancel').style.display = 'none'; } } } catch (err) { showToast('ไม่สามารถดึงข้อมูลได้', 'error'); } }; window.approveStatus = async function(id, type, targetStatus) { try { const endpoint = type === 'programs' ? `/programs/${id}` : `/news/${id}`; const body = { status: targetStatus }; const res = await fetchAuth(endpoint, { method: 'PUT', body: body }); if (res.success) { showToast(state.translations[state.lang].approve + 'สำเร็จ'); closeModal(); renderPage(); } else { showToast(res.message, 'error'); } } catch (err) { showToast('เกิดข้อผิดพลาดในการอนุมัติ', 'error'); } }; // ========================================== // UTILS // ========================================== async function fetchAuth(url, options = {}) { const headers = { 'Authorization': `Bearer ${state.token}`, ...options.headers }; // Auto set content-type to JSON if not FormData if (!(options.body instanceof FormData)) { headers['Content-Type'] = headers['Content-Type'] || 'application/json'; if (typeof options.body === 'object' && options.body !== null) { options.body = JSON.stringify(options.body); } } try { // ROUTE EVERYTHING TO data.php (Ecotech Style Dispatcher) const res = await fetch(`${API_DATA}?endpoint=${encodeURIComponent(url)}`, { ...options, headers }); const data = await res.json(); if (res.status === 401) { logout(); return { success: false, message: 'Session expired' }; } return data; } catch (err) { return { success: false, message: 'Network error (Direct Access)' }; } } // Modal Helpers function openModal(title, bodyHtml, onSave, saveText = 'บันทึก', isDanger = false) { const modal = document.getElementById('admin-modal'); document.getElementById('modal-title').textContent = title; document.getElementById('modal-body').innerHTML = bodyHtml; const saveBtn = document.getElementById('btn-save'); const cancelBtn = document.getElementById('btn-cancel'); const closeBtn = document.getElementById('close-modal'); saveBtn.textContent = saveText; saveBtn.className = isDanger ? 'btn btn-donate' : 'btn btn-primary'; // btn-donate is red cancelBtn.style.display = 'inline-block'; saveBtn.onclick = async () => { saveBtn.disabled = true; const originalText = saveBtn.textContent; saveBtn.textContent = 'กำลังดำเนินการ...'; await onSave(); saveBtn.disabled = false; saveBtn.textContent = originalText; }; cancelBtn.onclick = closeModal; closeBtn.onclick = closeModal; modal.style.display = 'flex'; } function closeModal() { document.getElementById('admin-modal').style.display = 'none'; } function showToast(msg, type = 'success') { const toast = document.getElementById('toast'); if (!toast) return; toast.textContent = msg; toast.className = `toast ${type}`; toast.style.display = 'block'; setTimeout(() => { toast.style.display = 'none'; }, 3000); } function escapeHtml(unsafe) { if(!unsafe) return ''; return String(unsafe) .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } window.setAdminLang = (l) => { state.lang = l; localStorage.setItem('admin_lang', l); // Update active state on all possible lang buttons document.querySelectorAll('[id^="admin-lang-"]').forEach(btn => btn.classList.remove('active', 'btn-primary')); const headerBtn = document.getElementById(`admin-lang-${l}`); if (headerBtn) headerBtn.classList.add('active'); const loginBtn = document.getElementById(`admin-lang-${l}-login`); if (loginBtn) loginBtn.classList.add('btn-primary'); updateStaticLabels(); renderPage(); }; function updateStaticLabels() { document.querySelectorAll('[data-t]').forEach(el => { const key = el.dataset.t; if (state.translations[state.lang][key]) { el.textContent = state.translations[state.lang][key]; } }); const searchInput = document.getElementById('admin-search'); if (searchInput) searchInput.placeholder = state.translations[state.lang].search_placeholder; } // Initialize Admin Lang document.addEventListener('DOMContentLoaded', () => { const savedAdminLang = localStorage.getItem('admin_lang') || 'th'; setAdminLang(savedAdminLang); });
💾 Simpan File
Batal
⬅ Naik ke admin
1 item
Nama
Tipe
Ukuran
Diubah
Aksi
📜
admin.js
js
114.3 KB
2026-05-08 04:46
✏️ Edit
👁️ View
🗑 Hapus