diff --git a/src/components/AdminShell.tsx b/src/components/AdminShell.tsx index d3153f8..47ee8b5 100644 --- a/src/components/AdminShell.tsx +++ b/src/components/AdminShell.tsx @@ -316,7 +316,7 @@ export default function AdminShell(props: { children: JSX.Element }) { const [isSuperAdmin, setIsSuperAdmin] = createSignal(false); const [sidebarOpen, setSidebarOpen] = createSignal(false); const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false); - const [notifCount] = createSignal(0); + const [unreadCount, setUnreadCount] = createSignal(0); const [theme, setTheme] = createSignal<'light' | 'dark'>('light'); const [routeTransitioning, setRouteTransitioning] = createSignal(false); @@ -407,6 +407,35 @@ export default function AdminShell(props: { children: JSX.Element }) { window.addEventListener('resize', refreshTabIndicator); onCleanup(() => window.removeEventListener('resize', refreshTabIndicator)); + // Fetch unread notification count and poll every 30 seconds + const fetchUnreadCount = async () => { + try { + const accessToken = typeof sessionStorage !== 'undefined' + ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' + : ''; + if (!accessToken) return; + const res = await fetch('/api/gateway/api/me/notifications/unread-count', { + method: 'GET', + headers: { + Accept: 'application/json', + 'x-portal-target': 'admin', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + credentials: 'include', + }); + if (res.ok) { + const data = await res.json(); + setUnreadCount(data.unread_count || 0); + } + } catch (e) { + console.error('Failed to fetch unread count:', e); + } + }; + + fetchUnreadCount(); + const interval = setInterval(fetchUnreadCount, 30000); + onCleanup(() => clearInterval(interval)); + const isPreview = searchParams._preview === '1' || (typeof sessionStorage !== 'undefined' && sessionStorage.getItem('nxtgauge_admin_preview') === '1'); @@ -563,12 +592,12 @@ export default function AdminShell(props: { children: JSX.Element }) { - - - 0}> - - - + + + 0}> + + + diff --git a/src/routes/admin/kb.tsx b/src/routes/admin/kb.tsx index bb13b3d..9187eda 100644 --- a/src/routes/admin/kb.tsx +++ b/src/routes/admin/kb.tsx @@ -112,15 +112,71 @@ async function deleteArticle(id: string): Promise { credentials: 'include', }); return res.ok; + } catch (e) { + console.error('KB delete failed:', e); + return false; + } + } + +// Category Management API +async function createCategory(category: Partial): Promise { + try { + const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; + const res = await fetch(`${API}/api/admin/kb/categories`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) + }, + credentials: 'include', + body: JSON.stringify(category), + }); + if (!res.ok) throw new Error('Failed to create category'); + return await res.json(); } catch (e) { - console.error('KB delete failed:', e); + console.error('KB create category failed:', e); + return null; + } +} + +async function updateCategory(id: string, category: Partial): Promise { + try { + const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; + const res = await fetch(`${API}/api/admin/kb/categories/${id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) + }, + credentials: 'include', + body: JSON.stringify(category), + }); + if (!res.ok) throw new Error('Failed to update category'); + return await res.json(); + } catch (e) { + console.error('KB update category failed:', e); + return null; + } +} + +async function deleteCategory(id: string): Promise { + try { + const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; + const res = await fetch(`${API}/api/admin/kb/categories/${id}`, { + method: 'DELETE', + headers: { ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) }, + credentials: 'include', + }); + return res.ok; + } catch (e) { + console.error('KB delete category failed:', e); return false; } } export default function KnowledgeBasePage() { const [articles, { refetch }] = createResource(loadArticles); - const [categories] = createResource(loadCategories); + const [categories, { refetch: refetchCategories }] = createResource(loadCategories); const [view, setView] = createSignal<'list' | 'editor'>('list'); const [searchQuery, setSearchQuery] = createSignal(''); const [selectedCategory, setSelectedCategory] = createSignal('All'); @@ -135,7 +191,18 @@ export default function KnowledgeBasePage() { const [summary, setSummary] = createSignal(''); const [saving, setSaving] = createSignal(false); const [error, setError] = createSignal(''); - + + // Category management state + const [catMode, setCatMode] = createSignal<'list' | 'create' | 'edit'>('list'); + const [editingCategory, setEditingCategory] = createSignal(null); + const [catName, setCatName] = createSignal(''); + const [catSlug, setCatSlug] = createSignal(''); + const [catDescription, setCatDescription] = createSignal(''); + const [catDisplayOrder, setCatDisplayOrder] = createSignal(0); + const [catIsActive, setCatIsActive] = createSignal(true); + const [catSaving, setCatSaving] = createSignal(false); + const [catError, setCatError] = createSignal(''); + // Map category name to ID from categories list const getCategoryIdByName = (name: string): string | undefined => { const cats = categories(); @@ -240,6 +307,82 @@ export default function KnowledgeBasePage() { } }; + // Category Management Handlers + const openCreateCategory = () => { + setEditingCategory(null); + setCatName(''); + setCatSlug(''); + setCatDescription(''); + setCatDisplayOrder(0); + setCatIsActive(true); + setCatError(''); + setCatMode('create'); + }; + + const openEditCategory = (cat: Category) => { + setEditingCategory(cat); + setCatName(cat.name); + setCatSlug(cat.slug || ''); + setCatDescription(cat.description || ''); + setCatDisplayOrder(cat.display_order || 0); + setCatIsActive(cat.is_active ?? true); + setCatError(''); + setCatMode('edit'); + }; + + const handleSaveCategory = async () => { + if (!catName().trim()) { + setCatError('Category name is required'); + return; + } + setCatSaving(true); + setCatError(''); + try { + const payload: Partial = { + name: catName(), + slug: catSlug() || undefined, + description: catDescription() || undefined, + display_order: catDisplayOrder(), + is_active: catIsActive(), + }; + let result: Category | null = null; + if (catMode() === 'edit' && editingCategory()) { + result = await updateCategory(editingCategory().id, payload); + } else { + result = await createCategory(payload); + } + if (!result) throw new Error(catMode() === 'edit' ? 'Failed to update category' : 'Failed to create category'); + await refetchCategories(); + setCatMode('list'); + setEditingCategory(null); + } catch (err: any) { + setCatError(err.message || 'Failed to save category'); + } finally { + setCatSaving(false); + } + }; + + const handleCancelCategory = () => { + setCatMode('list'); + setEditingCategory(null); + setCatError(''); + }; + + const handleDeleteCategory = async (id: string) => { + if (!confirm('Delete this category? Articles in this category will become unassociated.')) return; + try { + const ok = await deleteCategory(id); + if (ok) { + await refetchCategories(); + } else { + alert('Failed to delete category'); + } + } catch (err) { + console.error('Failed to delete category:', err); + alert('Failed to delete category'); + } + }; + const seedArticles = async () => { if (!confirm('This will seed 40 platform articles. Continue?')) return; @@ -498,335 +641,4 @@ export default function KnowledgeBasePage() { ); } -async function loadCategories(): Promise<{id: string; name: string; slug: string}[]> { - try { - const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; - const res = await fetch(`${API}/api/admin/kb/categories`, { - headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) }, - credentials: 'include', - }); - if (!res.ok) throw new Error('Failed to load categories'); - const data = await res.json(); - return Array.isArray(data) ? data : (data.categories || []); - } catch { - return []; - } -} -export default function KnowledgeBasePage() { - const [articles, { refetch }] = createResource(loadArticles); - const [categories] = createResource(loadCategories); - const [view, setView] = createSignal<'list' | 'editor'>('list'); - const [searchQuery, setSearchQuery] = createSignal(''); - const [selectedCategory, setSelectedCategory] = createSignal('All'); - - // Editor state - const [editingArticle, setEditingArticle] = createSignal(null); - const [saving, setSaving] = createSignal(false); - - const filteredArticles = () => { - const list = articles() || []; - return list.filter(a => { - const matchesSearch = a.title.toLowerCase().includes(searchQuery().toLowerCase()) || - (a.summary && a.summary.toLowerCase().includes(searchQuery().toLowerCase())); - const matchesCategory = selectedCategory() === 'All' || a.category === selectedCategory(); - return matchesSearch && matchesCategory; - }); - }; - - const handleEdit = (article: RuntimeKBArticle) => { - setEditingArticle({ ...article }); - setView('editor'); - }; - - const handleCreate = () => { - setEditingArticle({ - id: `kb-${Date.now()}`, - title: '', - category: 'General', - content: '', - author: 'Admin', - isPublished: false, - viewCount: 0, - lastUpdated: new Date().toISOString() - }); - setView('editor'); - }; - - const handleSave = async () => { - const article = editingArticle(); - if (!article || !article.title || !article.content) return; - - setSaving(true); - try { - const currentArticles = kbData()?.articles || []; - const index = currentArticles.findIndex(a => a.id === article.id); - - const updatedArticle = { - ...article, - lastUpdated: new Date().toISOString() - } as RuntimeKBArticle; - - let newArticles = [...currentArticles]; - if (index >= 0) { - newArticles[index] = updatedArticle; - } else { - newArticles.unshift(updatedArticle); - } - - await saveRuntimeConfig('knowledge_base', 'GLOBAL_KB', { articles: newArticles }, 'published'); - await refetch(); - setView('list'); - setEditingArticle(null); - } catch (err) { - console.error('Failed to save article:', err); - } finally { - setSaving(false); - } - }; - - const handleDelete = async (id: string) => { - if (!confirm('Are you sure you want to delete this article?')) return; - - try { - const currentArticles = kbData()?.articles || []; - const newArticles = currentArticles.filter(a => a.id !== id); - await saveRuntimeConfig('knowledge_base', 'GLOBAL_KB', { articles: newArticles }, 'published'); - await refetch(); - } catch (err) { - console.error('Failed to delete article:', err); - } - }; - - const seedArticles = async () => { - if (!confirm('This will seed 40 platform articles. Continue?')) return; - - setSaving(true); - try { - const articles: RuntimeKBArticle[] = []; - - // Generation logic for 40 articles - const articleTemplates = [ - { category: 'Verifications', topics: ['Business Approval', 'Identity Verification', 'Media Verification', 'Verification Timeline', 'Common Rejection Reasons'] }, - { category: 'Lead System', topics: ['How to Find Leads', 'Response Quality', 'Budget Matching', 'Unlocking Contacts', 'Lead Expiry'] }, - { category: 'Credits & Payments', topics: ['What are Tracecoins', 'Purchasing Plans', 'Refund Policy', 'Tax Invoices', 'Payment Failures'] }, - { category: 'Account & Profiles', topics: ['Portfolio Best Practices', 'Service Switching', 'Subscription Settings', 'Privacy Control', 'Profile URL Customization'] }, - { category: 'Security', topics: ['Password Recovery', '2FA Setup', 'Scam Prevention', 'Data Privacy GDPR', 'Account Recovery'] }, - { category: 'Troubleshooting', topics: ['App Performance', 'Notification Settings', 'Browser Compatibility', 'Image Upload Issues', 'Logout Problems'] }, - { category: 'Policies', topics: ['Terms of Service', 'Privacy Policy', 'Cancellation Policy', 'Refund Terms', 'Platform Guidelines'] }, - { category: 'General', topics: ['Welcome Guide', 'Mastering the Dashboard', 'Referral Program', 'Support Channels', 'App Updates'] }, - ]; - - articleTemplates.forEach(t => { - t.topics.forEach((topic, i) => { - articles.push({ - id: `kb-seed-${t.category.toLowerCase().replace(/\s/g, '-')}-${i}`, - title: topic, - category: t.category, - content: `This is a comprehensive guide about ${topic} on the Nxtgauge platform. We cover everything from initial setup to advanced strategies for success. - -### Key Highlights: -1. Understanding the fundamentals of ${topic}. -2. Step-by-step implementation guide. -3. Troubleshooting common roadblocks. -4. Professional tips for maximizing your business results. - -Nxtgauge is dedicated to providing the best tools for your professional growth. Check back frequently for updates on this guide.`, - author: 'System Architect', - isPublished: true, - viewCount: Math.floor(Math.random() * 500) + 100, - lastUpdated: new Date().toISOString() - }); - }); - }); - - await saveRuntimeConfig('knowledge_base', 'GLOBAL_KB', { articles }, 'published'); - await refetch(); - alert('40 articles seeded successfully!'); - } catch (err) { - console.error('Seeding failed:', err); - } finally { - setSaving(false); - } - }; - - return ( - - - - - - Knowledge Base Management - - Manage help articles and runtime documentation. - - - - Seed 40 Articles - - - New Article - - - - - - - - {editingArticle()?.id ? 'Edit' : 'Create'} Article - setView('list')} class="text-gray-400 hover:text-gray-600"> - - - - Title - setEditingArticle(p => ({ ...p!, title: e.currentTarget.value }))} - class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#FF5E13] focus:border-[#FF5E13] outline-none" - placeholder="Article Title" - /> - - - - Category - setEditingArticle(p => ({ ...p!, category: e.currentTarget.value }))} - class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#FF5E13] outline-none" - > - {cat => {cat}} - - - - Status - - setEditingArticle(p => ({ ...p!, isPublished: e.currentTarget.checked }))} - /> - Published - - - - - Content (Markdown Supported) - setEditingArticle(p => ({ ...p!, content: e.currentTarget.value }))} - class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#FF5E13] outline-none font-mono text-sm leading-relaxed" - placeholder="Article content..." - /> - - - - setView('list')} - class="px-4 py-2 text-sm font-bold text-gray-600 hover:text-gray-800 transition-all" - > - Cancel - - - - {saving() ? 'Saving...' : 'Save Article'} - - - - - }> - - - setSelectedCategory('All')} - class={`px-3 py-1.5 rounded-lg text-sm font-bold transition-all ${selectedCategory() === 'All' ? 'bg-[#FF5E13] text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`} - > - All - - {(cat) => ( - setSelectedCategory(cat)} - class={`px-3 py-1.5 rounded-lg text-sm font-bold transition-all ${selectedCategory() === cat ? 'bg-[#FF5E13] text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`} - > - {cat} - - )} - - - - setSearchQuery(e.currentTarget.value)} - class="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-[#FF5E13] focus:border-[#FF5E13] outline-none" - /> - - - - - - - - Loading Knowledge Base... - - - - - - - No articles found. Try seeding data or adjusting filters. - - - - - {(article) => ( - - - - - {article.category} - - DRAFT}> - - PUBLISHED - - - - - {article.title} - - - {article.content} - - - - - {new Date(article.lastUpdated).toLocaleDateString()} - {article.author} - - - handleEdit(article)} class="text-gray-400 hover:text-[#FF5E13] transition-colors"> - handleDelete(article.id)} class="text-gray-400 hover:text-red-500 transition-colors"> - - - - )} - - - - - ); -}
Manage help articles and runtime documentation.
Loading Knowledge Base...
No articles found. Try seeding data or adjusting filters.
- {article.content} -