diff --git a/src/routes/admin/kb.tsx b/src/routes/admin/kb.tsx index da02e33..7425d44 100644 --- a/src/routes/admin/kb.tsx +++ b/src/routes/admin/kb.tsx @@ -1,37 +1,535 @@ import { createResource, createSignal, Show, For, createEffect } from 'solid-js'; import { BookOpen, Plus, Search, Edit2, Trash2, CheckCircle, - Clock, User, ChevronRight, FileText, LayoutGrid, List, - Save, X, Globe, Lock, AlertCircle + Clock, Globe, AlertCircle } from 'lucide-solid'; -import { saveRuntimeConfig, getRuntimeConfig } from '~/lib/runtime/storage'; -import { RuntimeKBConfig, RuntimeKBArticle } from '~/lib/runtime/types'; -const CATEGORIES = [ - 'General', 'Account & Profiles', 'Lead System', 'Credits & Payments', - 'Verifications', 'Security', 'Policies', 'Troubleshooting' -]; +const API = '/api/gateway'; -async function loadKBArticles(): Promise { - const config = await getRuntimeConfig('knowledge_base', 'GLOBAL_KB'); - return config?.payload || { articles: [] }; +interface Category { + id: string; + name: string; + slug: string; + description?: string; + display_order?: number; + is_active?: boolean; + article_count?: number; +} + +interface Article { + id: string; + title: string; + slug: string; + summary?: string; + body: string; + category_id: string; + category?: string; + status: 'PUBLISHED' | 'DRAFT'; + target_roles?: string[]; + tags?: string[]; + views: number; + created_at: string; + updated_at: string; +} + +async function loadArticles(): Promise { + try { + const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; + const res = await fetch(`${API}/api/admin/kb/articles`, { + headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) }, + credentials: 'include', + }); + if (!res.ok) throw new Error('Failed to load articles'); + const data = await res.json(); + return Array.isArray(data) ? data : (data.articles || []); + } catch (e) { + console.error('KB load failed:', e); + return []; + } +} + +async function loadCategories(): Promise { + 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 []; + } +} + +async function createArticle(article: Partial
): Promise
{ + try { + const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; + const res = await fetch(`${API}/api/admin/kb/articles`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) + }, + credentials: 'include', + body: JSON.stringify(article), + }); + if (!res.ok) throw new Error('Failed to create article'); + return await res.json(); + } catch (e) { + console.error('KB create failed:', e); + return null; + } +} + +async function updateArticle(id: string, article: Partial
): Promise
{ + try { + const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; + const res = await fetch(`${API}/api/admin/kb/articles/${id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) + }, + credentials: 'include', + body: JSON.stringify(article), + }); + if (!res.ok) throw new Error('Failed to update article'); + return await res.json(); + } catch (e) { + console.error('KB update failed:', e); + return null; + } +} + +async function deleteArticle(id: string): Promise { + try { + const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; + const res = await fetch(`${API}/api/admin/kb/articles/${id}`, { + method: 'DELETE', + headers: { ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) }, + credentials: 'include', + }); + return res.ok; + } catch (e) { + console.error('KB delete failed:', e); + return false; + } } export default function KnowledgeBasePage() { - const [kbData, { refetch }] = createResource(loadKBArticles); + 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 (API format) + const [articleId, setArticleId] = createSignal(null); + const [title, setTitle] = createSignal(''); + const [slug, setSlug] = createSignal(''); + const [content, setContent] = createSignal(''); + const [categoryId, setCategoryId] = createSignal(''); + const [status, setStatus] = createSignal<'PUBLISHED' | 'DRAFT'>('DRAFT'); + const [summary, setSummary] = createSignal(''); + const [saving, setSaving] = createSignal(false); + const [error, setError] = createSignal(''); + + // Map category name to ID from categories list + const getCategoryIdByName = (name: string): string | undefined => { + const cats = categories(); + const cat = cats.find(c => c.name === name); + return cat?.id; + }; + + // Map category ID to name + const getCategoryNameById = (id: string): string => { + const cats = categories(); + const cat = cats.find(c => c.id === id); + return cat?.name || 'Unknown'; + }; + + const filteredArticles = () => { + const list = articles() || []; + return list.filter(a => { + const matchesSearch = a.title.toLowerCase().includes(searchQuery().toLowerCase()) || + (a.body && a.body.toLowerCase().includes(searchQuery().toLowerCase())); + const matchesCategory = selectedCategory() === 'All' || getCategoryNameById(a.category_id) === selectedCategory(); + return matchesSearch && matchesCategory; + }); + }; + + const openCreate = () => { + setArticleId(null); + setTitle(''); + setSlug(''); + setContent(''); + setCategoryId(''); + setStatus('DRAFT'); + setSummary(''); + setError(''); + setView('editor'); + }; + + const openEdit = (article: Article) => { + setArticleId(article.id); + setTitle(article.title); + setSlug(article.slug); + setContent(article.body); + setCategoryId(article.category_id); + setStatus(article.status); + setSummary(article.summary || ''); + setError(''); + setView('editor'); + }; + + const handleSave = async () => { + if (!title() || !content()) { + setError('Title and content are required'); + return; + } + + setSaving(true); + setError(''); + try { + const payload: Partial
= { + title: title(), + slug: slug() || undefined, + content: content(), + category_id: categoryId() || undefined, + status: status(), + summary: summary() || undefined, + }; + + let result: Article | null = null; + if (articleId()) { + // Update existing + result = await updateArticle(articleId(), payload); + } else { + // Create new + result = await createArticle(payload); + } + + if (!result) { + throw new Error(articleId() ? 'Failed to update article' : 'Failed to create article'); + } + + await refetch(); + setView('list'); + } catch (err: any) { + setError(err.message || 'Failed to save article'); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm('Are you sure you want to delete this article?')) return; + + try { + const ok = await deleteArticle(id); + if (ok) { + await refetch(); + } else { + alert('Failed to delete article'); + } + } catch (err) { + console.error('Failed to delete article:', err); + alert('Failed to delete article'); + } + }; + + const seedArticles = async () => { + if (!confirm('This will seed 40 platform articles. Continue?')) return; + + setSaving(true); + try { + const cats = categories(); + if (cats.length === 0) { + alert('Please create categories first before seeding.'); + setSaving(false); + return; + } + + 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'] }, + ]; + + let created = 0; + for (const t of articleTemplates) { + const catId = getCategoryIdByName(t.category); + if (!catId) continue; + + for (const topic of t.topics) { + const slug = topic.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); + const payload: Partial
= { + title: topic, + slug: `kb-seed-${slug}`, + content: `This is a comprehensive guide about ${topic} on the Nxtgauge platform. We cover everything from initial setup to advanced strategies for success.\n\n### Key Highlights:\n1. Understanding the fundamentals of ${topic}.\n2. Step-by-step implementation guide.\n3. Troubleshooting common roadblocks.\n4. Professional tips for maximizing your business results.\n\nNxtgauge is dedicated to providing the best tools for your professional growth. Check back frequently for updates on this guide.`, + category_id: catId, + status: 'PUBLISHED', + }; + const result = await createArticle(payload); + if (result) created++; + } + } + + alert(`${created} articles seeded successfully!`); + await refetch(); + } catch (err) { + console.error('Seeding failed:', err); + alert('Seeding failed. See console for details.'); + } finally { + setSaving(false); + } + }; + + return ( +
+
+
+

+ + Knowledge Base Management +

+

Manage help articles via persistent API storage.

+
+
+ + +
+
+ + +
+
+

{articleId() ? 'Edit' : 'Create'} Article

+ +
+
+
+ + setTitle(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" + /> +
+
+
+ + setSlug(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="url-friendly-slug" + /> +
+
+ + +
+
+
+ + +
+
+ + setSummary(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="Brief summary for listings" + /> +
+
+ +