| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"/> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| <title>Admin Panel — New Age Portal</title> |
| <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet"> |
| <style> |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| :root { |
| --bg: #F5F4F0; --surface: #FFFFFF; --border: #E2E0DA; |
| --text: #1A1916; --muted: #6B6A65; --hint: #9E9C97; |
| --accent: #1A1916; --radius: 10px; --radius-sm: 6px; |
| --shadow: 0 1px 3px rgba(0,0,0,0.06), 0 4px 16px rgba(0,0,0,0.06); |
| } |
| body { font-family: 'DM Sans', sans-serif; background: var(--bg); min-height: 100vh; } |
| body::before { |
| content: ''; position: fixed; inset: 0; |
| background-image: radial-gradient(circle, #C8C6BF 1px, transparent 1px); |
| background-size: 24px 24px; opacity: 0.45; pointer-events: none; z-index: 0; |
| } |
| .navbar { |
| position: relative; z-index: 10; |
| background: var(--surface); border-bottom: 1px solid var(--border); |
| padding: 0 2rem; height: 56px; |
| display: flex; align-items: center; justify-content: space-between; |
| } |
| .nav-brand { font-size: 0.9375rem; font-weight: 600; color: var(--text); } |
| .nav-brand span { color: var(--hint); font-weight: 400; margin-left: 6px; font-size: 0.8125rem; } |
| .nav-right a { |
| font-size: 0.8125rem; color: var(--muted); text-decoration: none; |
| border: 1px solid var(--border); border-radius: var(--radius-sm); |
| padding: 6px 12px; transition: color 0.15s; |
| } |
| .nav-right a:hover { color: var(--text); } |
| .container { position: relative; z-index: 1; max-width: 900px; margin: 2rem auto; padding: 0 1.5rem; } |
| h1 { font-size: 1.25rem; font-weight: 600; color: var(--text); margin-bottom: 0.25rem; } |
| .subtitle { font-size: 0.875rem; color: var(--muted); margin-bottom: 1.5rem; } |
| .tabs { display: flex; gap: 4px; margin-bottom: 1.5rem; } |
| .tab { |
| padding: 7px 16px; border-radius: var(--radius-sm); font-size: 0.8125rem; |
| font-weight: 500; cursor: pointer; border: 1px solid var(--border); |
| background: var(--surface); color: var(--muted); transition: all 0.15s; |
| font-family: 'DM Sans', sans-serif; |
| } |
| .tab.active { background: var(--accent); color: #fff; border-color: var(--accent); } |
| .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; } |
| table { width: 100%; border-collapse: collapse; } |
| thead th { padding: 12px 16px; text-align: left; font-size: 0.75rem; font-weight: 600; color: var(--hint); text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); background: #FAFAF8; } |
| tbody td { padding: 14px 16px; font-size: 0.875rem; color: var(--text); border-bottom: 1px solid var(--border); } |
| tbody tr:last-child td { border-bottom: none; } |
| tbody tr:hover td { background: #FAFAF8; } |
| .badge { |
| display: inline-flex; align-items: center; gap: 5px; |
| padding: 3px 9px; border-radius: 99px; font-size: 0.75rem; font-weight: 600; |
| } |
| .badge.pending { background: #FEF9C3; color: #854D0E; } |
| .badge.approved { background: #DCFCE7; color: #166534; } |
| .badge.rejected { background: #FEE2E2; color: #991B1B; } |
| .btn-approve, .btn-reject { |
| padding: 5px 12px; border-radius: var(--radius-sm); font-size: 0.8125rem; |
| font-weight: 500; cursor: pointer; border: 1px solid; font-family: 'DM Sans', sans-serif; |
| transition: opacity 0.15s; |
| } |
| .btn-approve { background: #DCFCE7; color: #166534; border-color: #BBF7D0; } |
| .btn-approve:hover { opacity: 0.8; } |
| .btn-reject { background: #FEE2E2; color: #991B1B; border-color: #FECACA; margin-left: 6px; } |
| .btn-reject:hover { opacity: 0.8; } |
| .empty { padding: 3rem; text-align: center; color: var(--hint); font-size: 0.875rem; } |
| .toast { |
| position: fixed; bottom: 2rem; right: 2rem; z-index: 999; |
| background: var(--accent); color: #fff; |
| padding: 12px 20px; border-radius: var(--radius-sm); |
| font-size: 0.875rem; font-weight: 500; |
| animation: slideIn 0.2s ease; display: none; |
| } |
| @keyframes slideIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } } |
| </style> |
| |
| <script type="text/javascript"> |
| (function(c,l,a,r,i,t,y){ |
| c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; |
| t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; |
| y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); |
| })(window, document, "clarity", "script", "x13hxsbvnw"); |
| </script> |
| </head> |
| <body> |
|
|
| <nav class="navbar"> |
| <div class="nav-brand">New Age Portal <span>Admin Panel</span></div> |
| <div class="nav-right"><a href="/chat">← Back to Chat</a></div> |
| </nav> |
|
|
| <div class="container"> |
| <h1>User Management</h1> |
| <p class="subtitle">Approve or reject employee access requests</p> |
|
|
| <div class="tabs"> |
| <button class="tab active" onclick="filterUsers('Pending')">Pending</button> |
| <button class="tab" onclick="filterUsers('Approved')">Approved</button> |
| <button class="tab" onclick="filterUsers('Rejected')">Rejected</button> |
| <button class="tab" onclick="filterUsers('All')">All</button> |
| </div> |
|
|
| <div class="card" id="usersCard"> |
| <table> |
| <thead> |
| <tr> |
| <th>Name</th> |
| <th>Email</th> |
| <th>Role</th> |
| <th>Requested</th> |
| <th>Status</th> |
| <th>Actions</th> |
| </tr> |
| </thead> |
| <tbody id="usersTable"> |
| <tr><td colspan="6" class="empty">Loading...</td></tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| |
| <div class="container" style="margin-top:2rem;"> |
| <h1>Knowledge Base</h1> |
| <p class="subtitle">Add content the AI bot will use to answer employee questions</p> |
|
|
| |
| <div class="card" style="padding:1.5rem; margin-bottom:1.5rem;"> |
| <div style="display:flex; flex-direction:column; gap:1rem;"> |
| <div> |
| <label style="font-size:0.8125rem; font-weight:500; color:var(--text); display:block; margin-bottom:5px;">Title *</label> |
| <input type="text" id="kTitle" placeholder="e.g. Leave Policy, Product Info" style="width:100%; height:40px; padding:0 12px; font-family:'DM Sans',sans-serif; font-size:0.875rem; border:1px solid var(--border); border-radius:6px; outline:none; color:var(--text);" /> |
| </div> |
| <div> |
| <label style="font-size:0.8125rem; font-weight:500; color:var(--text); display:block; margin-bottom:5px;">URL (optional — auto fetches content)</label> |
| <input type="url" id="kUrl" placeholder="https://notion.so/your-page or any public URL" style="width:100%; height:40px; padding:0 12px; font-family:'DM Sans',sans-serif; font-size:0.875rem; border:1px solid var(--border); border-radius:6px; outline:none; color:var(--text);" /> |
| </div> |
| <div> |
| <label style="font-size:0.8125rem; font-weight:500; color:var(--text); display:block; margin-bottom:5px;">Content (paste text if no URL)</label> |
| <textarea id="kContent" placeholder="Paste the knowledge content here..." rows="5" style="width:100%; padding:10px 12px; font-family:'DM Sans',sans-serif; font-size:0.875rem; border:1px solid var(--border); border-radius:6px; outline:none; color:var(--text); resize:vertical;"></textarea> |
| </div> |
| <button onclick="addKnowledge()" style="height:40px; background:var(--accent); color:#fff; border:none; border-radius:6px; font-family:'DM Sans',sans-serif; font-size:0.875rem; font-weight:600; cursor:pointer; transition:opacity 0.15s;" onmouseover="this.style.opacity=0.85" onmouseout="this.style.opacity=1"> |
| + Add to Knowledge Base |
| </button> |
|
|
| <div style="border-top:1px solid var(--border); padding-top:1rem; margin-top:0.5rem;"> |
| <label style="font-size:0.8125rem; font-weight:600; color:var(--text); display:block; margin-bottom:8px;">📄 Or Upload a PDF</label> |
| <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;"> |
| <input type="text" id="pdfTitle" placeholder="PDF title (e.g. Job Leveling)" style="flex:1; min-width:180px; height:40px; padding:0 12px; font-family:'DM Sans',sans-serif; font-size:0.875rem; border:1px solid var(--border); border-radius:6px; outline:none; color:var(--text);" /> |
| <input type="file" id="pdfFile" accept=".pdf" style="flex:1; min-width:180px; font-family:'DM Sans',sans-serif; font-size:0.8125rem; color:var(--muted);" /> |
| <button onclick="uploadPdf()" style="height:40px; padding:0 20px; background:#1A1916; color:#fff; border:none; border-radius:6px; font-family:'DM Sans',sans-serif; font-size:0.875rem; font-weight:600; cursor:pointer; white-space:nowrap;" onmouseover="this.style.opacity=0.85" onmouseout="this.style.opacity=1"> |
| Upload PDF |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="card" id="knowledgeList"> |
| <table style="width:100%; border-collapse:collapse;"> |
| <thead> |
| <tr> |
| <th style="padding:12px 16px; text-align:left; font-size:0.75rem; font-weight:600; color:var(--hint); text-transform:uppercase; letter-spacing:0.05em; border-bottom:1px solid var(--border); background:#FAFAF8;">Title</th> |
| <th style="padding:12px 16px; text-align:left; font-size:0.75rem; font-weight:600; color:var(--hint); text-transform:uppercase; letter-spacing:0.05em; border-bottom:1px solid var(--border); background:#FAFAF8;">Content Preview</th> |
| <th style="padding:12px 16px; text-align:left; font-size:0.75rem; font-weight:600; color:var(--hint); text-transform:uppercase; letter-spacing:0.05em; border-bottom:1px solid var(--border); background:#FAFAF8;">Added</th> |
| <th style="padding:12px 16px; text-align:left; font-size:0.75rem; font-weight:600; color:var(--hint); text-transform:uppercase; letter-spacing:0.05em; border-bottom:1px solid var(--border); background:#FAFAF8;">Actions</th> |
| </tr> |
| </thead> |
| <tbody id="kTableBody"> |
| <tr><td colspan="4" style="padding:3rem; text-align:center; color:var(--hint); font-size:0.875rem;">No knowledge entries yet.</td></tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| <div class="toast" id="toast"></div> |
|
|
| <script> |
| let allUsers = []; |
| let currentFilter = 'Pending'; |
| |
| async function loadUsers() { |
| const res = await fetch('/api/admin/users'); |
| const data = await res.json(); |
| allUsers = data.users || []; |
| renderTable(currentFilter); |
| } |
| |
| function filterUsers(status) { |
| currentFilter = status; |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); |
| event.target.classList.add('active'); |
| renderTable(status); |
| } |
| |
| function renderTable(filter) { |
| const tbody = document.getElementById('usersTable'); |
| const users = filter === 'All' ? allUsers : allUsers.filter(u => u.status === filter); |
| |
| if (users.length === 0) { |
| tbody.innerHTML = `<tr><td colspan="6" class="empty">No ${filter.toLowerCase()} users found.</td></tr>`; |
| return; |
| } |
| |
| tbody.innerHTML = users.map(u => ` |
| <tr> |
| <td><strong>${u.name}</strong></td> |
| <td>${u.email}</td> |
| <td>${u.role}</td> |
| <td>${u.created_at}</td> |
| <td><span class="badge ${u.status.toLowerCase()}">${u.status}</span></td> |
| <td> |
| ${u.status !== 'Approved' ? `<button class="btn-approve" onclick="updateUser('${u.email}','Approved')">✓ Approve</button>` : ''} |
| ${u.status !== 'Rejected' ? `<button class="btn-reject" onclick="updateUser('${u.email}','Rejected')">✕ Reject</button>` : ''} |
| </td> |
| </tr> |
| `).join(''); |
| } |
| |
| async function updateUser(email, action) { |
| const res = await fetch('/api/admin/approve', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ email, action }) |
| }); |
| const data = await res.json(); |
| if (data.success) { |
| showToast(`User ${action.toLowerCase()} successfully`); |
| loadUsers(); |
| } |
| } |
| |
| function showToast(msg) { |
| const t = document.getElementById('toast'); |
| t.textContent = msg; |
| t.style.display = 'block'; |
| setTimeout(() => t.style.display = 'none', 2500); |
| } |
| |
| loadUsers(); |
| loadKnowledge(); |
| |
| async function addKnowledge() { |
| const title = document.getElementById('kTitle').value.trim(); |
| const url = document.getElementById('kUrl').value.trim(); |
| const content = document.getElementById('kContent').value.trim(); |
| |
| if (!title) return showToast('Please enter a title'); |
| if (!url && !content) return showToast('Please enter a URL or content'); |
| |
| showToast('Adding entry...'); |
| const res = await fetch('/api/knowledge/add', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ title, url, content }) |
| }); |
| const data = await res.json(); |
| showToast(data.message); |
| if (data.success) { |
| document.getElementById('kTitle').value = ''; |
| document.getElementById('kUrl').value = ''; |
| document.getElementById('kContent').value = ''; |
| loadKnowledge(); |
| } |
| } |
| |
| async function loadKnowledge() { |
| const res = await fetch('/api/knowledge'); |
| const data = await res.json(); |
| const tbody = document.getElementById('kTableBody'); |
| if (!data.entries || data.entries.length === 0) { |
| tbody.innerHTML = '<tr><td colspan="4" style="padding:3rem; text-align:center; color:var(--hint); font-size:0.875rem;">No knowledge entries yet.</td></tr>'; |
| return; |
| } |
| tbody.innerHTML = data.entries.map(e => ` |
| <tr> |
| <td style="padding:14px 16px; font-size:0.875rem; font-weight:600; color:var(--text); border-bottom:1px solid var(--border);">${e.title}</td> |
| <td style="padding:14px 16px; font-size:0.8125rem; color:var(--muted); border-bottom:1px solid var(--border); max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${e.content.slice(0, 100)}...</td> |
| <td style="padding:14px 16px; font-size:0.8125rem; color:var(--hint); border-bottom:1px solid var(--border);">${e.created_at}</td> |
| <td style="padding:14px 16px; border-bottom:1px solid var(--border);"> |
| <button onclick="deleteKnowledge('${e.id}')" style="padding:5px 12px; border-radius:6px; font-size:0.8125rem; font-weight:500; cursor:pointer; background:#FEE2E2; color:#991B1B; border:1px solid #FECACA; font-family:'DM Sans',sans-serif;">✕ Delete</button> |
| </td> |
| </tr> |
| `).join(''); |
| } |
| |
| async function uploadPdf() { |
| const title = document.getElementById('pdfTitle').value.trim(); |
| const file = document.getElementById('pdfFile').files[0]; |
| |
| if (!title) return showToast('Please enter a title for the PDF'); |
| if (!file) return showToast('Please select a PDF file'); |
| |
| showToast('Uploading PDF...'); |
| const formData = new FormData(); |
| formData.append('pdf', file); |
| formData.append('title', title); |
| |
| const res = await fetch('/api/knowledge/upload-pdf', { method: 'POST', body: formData }); |
| const data = await res.json(); |
| showToast(data.message); |
| if (data.success) { |
| document.getElementById('pdfTitle').value = ''; |
| document.getElementById('pdfFile').value = ''; |
| loadKnowledge(); |
| } |
| } |
| |
| async function deleteKnowledge(id) { |
| if (!confirm('Delete this knowledge entry?')) return; |
| const res = await fetch('/api/knowledge/delete', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ id }) |
| }); |
| const data = await res.json(); |
| showToast(data.message); |
| if (data.success) loadKnowledge(); |
| } |
| </script> |
| </body> |
| </html> |