diff --git a/src/lib/runtime/storage.ts b/src/lib/runtime/storage.ts index 73480e1..16a4b45 100644 --- a/src/lib/runtime/storage.ts +++ b/src/lib/runtime/storage.ts @@ -3,7 +3,7 @@ import type { RuntimeDashboardLayout } from './types'; // All API calls go through the server route gateway proxy const API_GATEWAY = '/api/gateway'; -export type RuntimeRecordType = 'role' | 'dashboard' | 'onboarding'; +export type RuntimeRecordType = 'role' | 'dashboard' | 'onboarding' | 'knowledge_base'; export type RuntimeRecordStatus = 'draft' | 'published'; export type RuntimeStoredRecord = { diff --git a/src/lib/runtime/types.ts b/src/lib/runtime/types.ts index c202fd2..67e512c 100644 --- a/src/lib/runtime/types.ts +++ b/src/lib/runtime/types.ts @@ -65,3 +65,18 @@ export type RuntimeOnboardingConfig = { version: number; steps: RuntimeOnboardingStep[]; }; + +export type RuntimeKBArticle = { + id: string; + title: string; + category: string; + content: string; + author: string; + isPublished: boolean; + viewCount: number; + lastUpdated: string; +}; + +export type RuntimeKBConfig = { + articles: RuntimeKBArticle[]; +}; diff --git a/src/routes/admin/approval.tsx b/src/routes/admin/approval.tsx index f404813..f630d32 100644 --- a/src/routes/admin/approval.tsx +++ b/src/routes/admin/approval.tsx @@ -265,7 +265,7 @@ export default function ApprovalManagementPage() { const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; - const res = await fetch(`${API}/api/admin/approvals?page=1&limit=100`, { + const res = await fetch(`${API}/api/admin/verifications?page=1&limit=100`, { headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), @@ -274,108 +274,33 @@ export default function ApprovalManagementPage() { }); if (!res.ok) throw new Error(`Request failed (${res.status})`); const payload = await res.json().catch(() => ({} as any)); + const items = Array.isArray(payload?.items) ? payload.items : []; - const jobs = Array.isArray(payload?.jobs) ? payload.jobs : []; - const requirements = Array.isArray(payload?.requirements) ? payload.requirements : []; - const profiles = Array.isArray(payload?.profiles) ? payload.profiles : []; - const portfolios = Array.isArray(payload?.portfolios) ? payload.portfolios : []; - - const mappedJobs: ApprovalRecord[] = jobs.map((job: any) => ({ - id: `AP-J-${String(job.id || Math.random()).slice(0, 8)}`, - name: `Job Approval - ${String(job.title || 'Untitled Job')}`, - applicantName: String(job.company_name || job.created_by_name || job.title || 'Company Applicant'), - approvalType: 'JOB', - userType: 'COMPANY', - roleTags: extractRoleTags(job), - primaryService: String(job.category || job.department || job.role || 'Job Posting'), - area: String(job.location || job.city || job.work_mode || 'Chennai'), - submittedDate: String(job.created_at || ''), - verificationStatus: 'VERIFIED', - assignedApprover: 'Unassigned', - priority: 'HIGH', - status: 'PENDING', - updatedAt: String(job.updated_at || job.created_at || ''), - sourceKey: `jobs:${String(job.id || '')}`, - submittedFields: extractSubmittedFields(job), - documents: extractDocuments(job), - payload: job, - })); - - const mappedReqs: ApprovalRecord[] = requirements.map((req: any) => ({ - id: `AP-R-${String(req.id || Math.random()).slice(0, 8)}`, - name: `Requirement Approval - ${String(req.title || 'Untitled Requirement')}`, - applicantName: String(req.created_by_name || req.customer_name || req.title || 'Service Seeker Applicant'), - approvalType: 'REQUIREMENT', - userType: 'CUSTOMER', - roleTags: extractRoleTags(req), - primaryService: String(req.category || req.profession || req.service_type || 'Requirement'), - area: String(req.location || req.city || req.area || 'Chennai'), - submittedDate: String(req.created_at || ''), - verificationStatus: 'VERIFIED', - assignedApprover: 'Unassigned', - priority: 'MEDIUM', - status: 'PENDING', - updatedAt: String(req.updated_at || req.created_at || ''), - sourceKey: `requirements:${String(req.id || '')}`, - submittedFields: extractSubmittedFields(req), - documents: extractDocuments(req), - payload: req, - })); - - const mappedProfiles: ApprovalRecord[] = profiles.map((profile: any) => ({ - id: `AP-P-${String(profile.id || profile.user_id || Math.random()).slice(0, 8)}`, - name: `Profile Approval - ${String(profile.full_name || profile.name || 'Applicant')}`, - applicantName: String(profile.full_name || profile.name || profile.user_name || 'Applicant'), - approvalType: profile.role_key === 'COMPANY' ? 'BUSINESS' : 'PROFILE', - userType: normalizeUserType(profile.role_key || profile.roleKey || profile.user_type), - roleTags: extractRoleTags(profile), - primaryService: String(profile.role_key || profile.role || profile.category || 'Profile'), - area: String(profile.area || profile.place || profile.city || 'Chennai'), - submittedDate: String(profile.submitted_at || profile.created_at || ''), - verificationStatus: 'VERIFIED', - assignedApprover: 'Unassigned', - priority: 'MEDIUM', - status: 'PENDING', - updatedAt: String(profile.updated_at || profile.created_at || ''), - sourceKey: `profiles:${String(profile.id || profile.user_id || '')}`, - submittedFields: extractSubmittedFields(profile), - documents: extractDocuments(profile), - payload: profile, - })); - - const mappedPortfolios: ApprovalRecord[] = portfolios.map((portfolio: any) => ({ - id: `AP-F-${String(portfolio.id || portfolio.user_id || Math.random()).slice(0, 8)}`, - name: `Portfolio Approval - ${String(portfolio.full_name || portfolio.name || 'Professional')}`, - applicantName: String(portfolio.full_name || portfolio.name || 'Professional'), - approvalType: 'PORTFOLIO', - userType: 'PROFESSIONAL', - roleTags: extractRoleTags(portfolio), - primaryService: String(portfolio.role_key || portfolio.role || 'Professional Portfolio'), - area: String(portfolio.area || portfolio.place || portfolio.city || 'Chennai'), - submittedDate: String(portfolio.submitted_at || portfolio.created_at || ''), - verificationStatus: 'VERIFIED', - assignedApprover: 'Unassigned', - priority: 'HIGH', - status: 'PENDING', - updatedAt: String(portfolio.updated_at || portfolio.created_at || ''), - sourceKey: `portfolios:${String(portfolio.id || portfolio.user_id || '')}`, - submittedFields: extractSubmittedFields(portfolio), - documents: extractDocuments(portfolio), - payload: portfolio, - })); - - const queueRaw = typeof window !== 'undefined' ? window.localStorage.getItem(APPROVAL_QUEUE_STORAGE_KEY) : null; - const queueParsed = queueRaw ? JSON.parse(queueRaw) : []; - const queueItems = Array.isArray(queueParsed) ? queueParsed as ApprovalQueueItem[] : []; - const mappedQueue = mapQueueItemsToApprovals(queueItems); - - const merged = [...mappedQueue, ...mappedJobs, ...mappedReqs, ...mappedProfiles, ...mappedPortfolios]; - const deduped = merged.filter((row, index, arr) => { - const key = row.sourceKey || row.id; - return arr.findIndex((candidate) => (candidate.sourceKey || candidate.id) === key) === index; + const mappedItems: ApprovalRecord[] = items.map((v: any) => { + const p = v.payload || {}; + return { + id: v.id, + name: `${toTitle(v.type)} - ${v.user_name || 'Applicant'}`, + applicantName: v.user_name || 'Applicant', + approvalType: verificationToApprovalType(v.type === 'job_approval' ? 'Job Approval' : (v.type === 'requirement_approval' ? 'Service Seeker Requirement' : 'Profile Approval')), + userType: normalizeUserType(v.role_key), + roleTags: [toTitle(v.role_key)], + primaryService: toTitle(v.role_key || 'User'), + area: p.city || p.area || 'Unknown', + submittedDate: v.created_at, + verificationStatus: v.status === 'APPROVED' ? 'VERIFIED' : 'PENDING', + assignedApprover: 'Unassigned', + priority: 'MEDIUM', + status: v.status === 'APPROVED' ? 'APPROVED' : (v.status === 'REJECTED' ? 'REJECTED' : 'PENDING'), + updatedAt: v.updated_at, + sourceKey: `v:${v.id}`, + submittedFields: extractSubmittedFields(p), + documents: extractDocuments(p), + payload: v, + }; }); - - setRows(deduped); + + setRows(mappedItems); } catch (e: any) { setRows([]); setError(e?.message || 'Could not reach approvals API.'); @@ -476,9 +401,7 @@ export default function ApprovalManagementPage() { const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; - const endpoint = type === 'JOB' - ? `${API}/api/admin/approvals/jobs/${String(row.id).replace(/^AP-J-/, '')}/${action}` - : `${API}/api/admin/approvals/requirements/${String(row.id).replace(/^AP-R-/, '')}/${action}`; + const endpoint = `${API}/api/admin/verifications/${row.id}/${action}`; const res = await fetch(endpoint, { method: 'POST', headers: { diff --git a/src/routes/admin/external-roles.tsx b/src/routes/admin/external-roles.tsx index 09c92ba..1165475 100644 --- a/src/routes/admin/external-roles.tsx +++ b/src/routes/admin/external-roles.tsx @@ -61,11 +61,8 @@ function normalizeExternalRole(item: any, index: number): ExternalRoleRecord { const USER_TYPE_OPTIONS = ['COMPANY', 'CANDIDATE', 'PHOTOGRAPHER', 'MAKEUP_ARTIST', 'TUTOR', 'DEVELOPER', 'VIDEO_EDITOR', 'UGC_CONTENT_CREATOR', 'FITNESS_TRAINER', 'CATERER', 'GRAPHIC_DESIGNER', 'SOCIAL_MEDIA_MANAGER', 'CUSTOMER']; -const ONBOARDING_SCHEMAS = [ - 'company_onboarding_v1', 'job_seeker_onboarding_v1', 'customer_onboarding_v1', 'photographer_onboarding_v1', - 'makeup_artist_onboarding_v1', 'tutor_onboarding_v1', 'developer_onboarding_v1', 'video_editor_onboarding_v1', - 'ugc_content_creator_onboarding_v1' -]; +// Onboarding schemas removed in favor of Dashboard-first profile completion. +const ONBOARDING_SCHEMAS: string[] = []; const MODULES_BY_VERTICAL = { jobs: [ @@ -103,17 +100,9 @@ function StatusBadge(props: { status: string }) { ); } -const FALLBACK_LOGS = [ - { id: 'l1', user: 'Admin Ashwin', action: 'Updated Permissions', target: 'Verified Company', date: '2026-03-27 10:30' }, - { id: 'l2', user: 'Admin Ashwin', action: 'Changed Status', target: 'Professional Photographer', date: '2026-03-26 14:15' }, - { id: 'l3', user: 'System', action: 'Auto-sync Schema', target: 'Verified Company', date: '2026-03-25 09:00' }, -]; - -const FALLBACK_ASSIGNED_USERS = [ - { id: 'u1', name: 'John Doe', email: 'john@example.com', joined: '2026-01-20' }, - { id: 'u2', name: 'Jane Smith', email: 'jane@studios.com', joined: '2026-02-05' }, - { id: 'u3', name: 'Alice Wong', email: 'alice@photo.me', joined: '2026-03-12' }, -]; +// TODO: Wire to real backend log/user endpoints +const FALLBACK_LOGS: any[] = []; +const FALLBACK_ASSIGNED_USERS: any[] = []; function FormInput(props: { label: string; required?: boolean; value: string; onInput: (v: string) => void; placeholder?: string; type?: string }) { return ( @@ -799,13 +788,6 @@ export default function ExternalRoleManagementPage() { - - diff --git a/src/routes/admin/kb.tsx b/src/routes/admin/kb.tsx index 11f5ef7..da02e33 100644 --- a/src/routes/admin/kb.tsx +++ b/src/routes/admin/kb.tsx @@ -1,684 +1,335 @@ -import { createResource, createSignal, createMemo, Show, For } from 'solid-js'; -import { A } from '@solidjs/router'; +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 +} from 'lucide-solid'; +import { saveRuntimeConfig, getRuntimeConfig } from '~/lib/runtime/storage'; +import { RuntimeKBConfig, RuntimeKBArticle } from '~/lib/runtime/types'; -const API = '/api/gateway'; - -type KbCategory = { - id: string; - name: string; - slug: string; - description?: string; - article_count?: number; - updated_at?: string; -}; - -type KbArticle = { - id: string; - title: string; - slug?: string; - category_id?: string; - category?: string; - status: string; - updated_at?: string; -}; - -type SeedArticle = { - title: string; - slug: string; - categorySlug: string; - content: string; -}; - -const KB_SEED_CATEGORIES: Array<{ name: string; slug: string; description: string }> = [ - { name: 'Getting Started', slug: 'getting-started', description: 'Account setup, role selection, and first actions.' }, - { name: 'Account & Login', slug: 'account-login', description: 'Login, password, account access, and security.' }, - { name: 'Profile & Verification', slug: 'profile-verification', description: 'Profile setup and verification workflow help.' }, - { name: 'Platform Basics', slug: 'platform-basics', description: 'Simple guides to understand core platform workflows.' }, - { name: 'Jobs & Applications', slug: 'jobs-applications', description: 'Posting jobs, applying, and status tracking.' }, - { name: 'Leads & Requests', slug: 'leads-requests', description: 'Handling incoming leads and request responses.' }, - { name: 'TraceCoins & Billing', slug: 'tracecoins-billing', description: 'Credits, payments, invoices, and refunds.' }, - { name: 'Privacy & Safety', slug: 'privacy-safety', description: 'Policy, reporting, trust and account safety.' }, - { name: 'Troubleshooting', slug: 'troubleshooting', description: 'Common issues and quick resolution guides.' }, +const CATEGORIES = [ + 'General', 'Account & Profiles', 'Lead System', 'Credits & Payments', + 'Verifications', 'Security', 'Policies', 'Troubleshooting' ]; -const KB_SEED_ARTICLES: SeedArticle[] = [ - { title: 'Create your Nxtgauge account in 3 steps', slug: 'create-account-3-steps', categorySlug: 'getting-started', content: 'Step 1: Sign up with your email or mobile number.\n\nStep 2: Verify OTP and complete basic account details.\n\nStep 3: Go to role setup and add clear profile information.\n\nTip: Keep your name, location, and contact details accurate so approval and communication are smooth.' }, - { title: 'Choose your role after signup', slug: 'choose-role-after-signup', categorySlug: 'getting-started', content: 'Nxtgauge uses one account with role-based journeys. You can choose your role after signup.\n\nIf you are a company, use the company flow for job posting.\nIf you are a professional, use the service profile flow.\nIf you are a job seeker, complete candidate details.\n\nYou can update your role path later when needed.' }, - { title: 'Understand account statuses (Draft, Review, Approved)', slug: 'understand-account-statuses', categorySlug: 'getting-started', content: 'Draft: Your profile is incomplete and not ready for full visibility.\n\nReview: Your submission is under verification checks.\n\nApproved: Your profile or listing passed checks and is visible in discovery.\n\nIf your status stays in review for too long, check if any required data is missing.' }, - { title: 'How the Nxtgauge platform works', slug: 'how-nxtgauge-platform-works', categorySlug: 'platform-basics', content: 'Nxtgauge is built on a trust-first workflow.\n\n1. User submits profile, requirement, or job.\n2. Platform runs verification checks.\n3. Approved submissions become discoverable.\n4. Users track status and responses in one flow.\n\nThis helps reduce spam and improves first-contact quality.' }, - { title: 'One account, multiple journeys explained', slug: 'one-account-multiple-journeys', categorySlug: 'platform-basics', content: 'You do not need separate accounts for each use case.\n\nWith one account, you can move across role journeys based on your need.\nExamples:\n- Company posting jobs\n- Professional offering services\n- Job seeker applying to roles\n\nKeep each role profile complete for better matching.' }, - { title: 'How review and approval improve trust', slug: 'review-approval-improve-trust', categorySlug: 'platform-basics', content: 'Review and approval help ensure better quality listings and profiles.\n\nWhy it matters:\n- Less fake activity\n- Better discovery relevance\n- Higher confidence before first contact\n\nMost checks complete in 24-48 hours for standard submissions.' }, - { title: 'How to track progress without confusion', slug: 'track-progress-without-confusion', categorySlug: 'platform-basics', content: 'Use status labels and timeline views in your dashboard.\n\nBest practice:\n- Check updates daily\n- Respond quickly to requests\n- Keep your profile current\n\nWhen status changes, follow the next action shown in your panel.' }, - { title: 'Common platform terms in simple words', slug: 'platform-terms-simple-words', categorySlug: 'platform-basics', content: 'Draft: Not complete yet.\nReview: Being checked by the platform.\nApproved: Passed checks and visible.\nRequirement: Customer request.\nLead: Potential opportunity.\n\nKnowing these terms helps you move faster through workflows.' }, - { title: 'How to get better platform visibility', slug: 'get-better-platform-visibility', categorySlug: 'platform-basics', content: 'To improve visibility:\n- Complete profile fields fully\n- Add clear scope and category fit\n- Keep media and proof updated\n- Respond fast to valid requests\n\nProfiles with clear information usually get better conversion quality.' }, - { title: 'How to reset your password', slug: 'reset-password', categorySlug: 'account-login', content: 'Use Forgot Password on login, verify via OTP, and set a new secure password to restore account access.' }, - { title: 'Why your login OTP might fail', slug: 'otp-failure-reasons', categorySlug: 'account-login', content: 'OTP may fail due to expiry, wrong input, network delay, or blocked SMS routes. Request a fresh OTP and retry.' }, - { title: 'Update email or phone on your account', slug: 'update-email-phone', categorySlug: 'account-login', content: 'Go to account settings and submit an update request. Certain changes may require re-verification for security.' }, - { title: 'How to complete a high-conversion profile', slug: 'high-conversion-profile', categorySlug: 'profile-verification', content: 'Add role clarity, service scope, location, portfolio proof, pricing structure, and response timelines for better conversion.' }, - { title: 'Verification checklist before submission', slug: 'verification-checklist-before-submit', categorySlug: 'profile-verification', content: 'Check identity fields, profile completeness, media proofs, and category fit before submitting for review.' }, - { title: 'Expected verification timeline (24-48 hours)', slug: 'verification-timeline-24-48-hours', categorySlug: 'profile-verification', content: 'Most profile reviews are completed within 24-48 hours. Complex or incomplete submissions may take longer.' }, - { title: 'Why verification was sent back for correction', slug: 'verification-sent-back-correction', categorySlug: 'profile-verification', content: 'Correction requests happen when details are incomplete, unclear, mismatched, or missing required proof.' }, - { title: 'How companies can post jobs correctly', slug: 'companies-post-jobs-correctly', categorySlug: 'jobs-applications', content: 'Use clear job titles, responsibilities, compensation, location, and hiring timeline for better candidate fit.' }, - { title: 'How job seekers can track application status', slug: 'track-application-status', categorySlug: 'jobs-applications', content: 'Open your applications panel to check submitted, viewed, shortlisted, or closed statuses in one place.' }, - { title: 'How to improve job application quality', slug: 'improve-application-quality', categorySlug: 'jobs-applications', content: 'Tailor profile details to each role, keep availability updated, and include relevant proof of work where possible.' }, - { title: 'Handling duplicate or irrelevant leads', slug: 'handle-duplicate-irrelevant-leads', categorySlug: 'leads-requests', content: 'Use request filters, quick decline actions, and profile targeting updates to reduce low-fit lead volume.' }, - { title: 'Best response window for higher lead conversion', slug: 'best-response-window-lead-conversion', categorySlug: 'leads-requests', content: 'Respond early with clear scope and timeline. Fast, structured replies generally improve first-contact conversion.' }, - { title: 'How to organize incoming customer requests', slug: 'organize-customer-requests', categorySlug: 'leads-requests', content: 'Tag requests by urgency, scope, and budget range to prioritize responses and avoid missed opportunities.' }, - { title: 'What are TraceCoins and where they are used', slug: 'what-are-tracecoins', categorySlug: 'tracecoins-billing', content: 'TraceCoins are platform credits used for selected actions such as premium interactions and visibility utilities.' }, - { title: 'How to view invoices and payment history', slug: 'view-invoices-payment-history', categorySlug: 'tracecoins-billing', content: 'Open billing settings to view purchase records, invoice references, and transaction status history.' }, - { title: 'Refund and failed payment guidance', slug: 'refund-failed-payment-guidance', categorySlug: 'tracecoins-billing', content: 'If payment fails or is disputed, check transaction status first, then raise a support request with payment reference.' }, - { title: 'How to report suspicious activity', slug: 'report-suspicious-activity', categorySlug: 'privacy-safety', content: 'Use report actions on profiles or listings, include evidence, and avoid direct engagement with suspicious accounts.' }, - { title: 'Managing privacy settings for your account', slug: 'manage-privacy-settings', categorySlug: 'privacy-safety', content: 'Control profile visibility, notification preferences, and contact access from account privacy controls.' }, - { title: 'Account safety best practices', slug: 'account-safety-best-practices', categorySlug: 'privacy-safety', content: 'Use strong passwords, rotate credentials, enable secure recovery methods, and avoid sharing verification codes.' }, - { title: 'Fix profile image or upload issues', slug: 'fix-profile-image-upload-issues', categorySlug: 'troubleshooting', content: 'Try compressed files, supported formats, and stable network. Re-upload after clearing cached session state if needed.' }, - { title: 'What to do if dashboard data does not refresh', slug: 'dashboard-data-not-refreshing', categorySlug: 'troubleshooting', content: 'Refresh the session, confirm role context, and retry after a short delay. Contact support if stale state persists.' }, - { title: 'Why your listing may not appear in search', slug: 'listing-not-appearing-in-search', categorySlug: 'platform-basics', content: 'Listings may not appear when status is Draft/Review, category mapping is weak, or profile details are incomplete.\n\nCheck status first, then update role/category details and retry.' }, - { title: 'Platform checklist before submitting anything', slug: 'platform-checklist-before-submitting', categorySlug: 'platform-basics', content: 'Before submit:\n- Verify title and category\n- Add clear scope and location\n- Add supporting details\n- Check contact correctness\n\nThis reduces correction loops and helps faster approval.' }, - { title: 'How to use Help Center effectively', slug: 'use-help-center-effectively', categorySlug: 'platform-basics', content: 'Search by simple keywords like login, verification, leads, billing, status, and approval.\n\nOpen related articles to understand connected steps.\n\nIf still blocked, share your exact issue with screenshots when contacting support.' }, -]; - -async function loadCategories(): Promise { - try { - const res = await fetch(`${API}/api/admin/kb/categories`); - if (!res.ok) throw new Error('Failed to load'); - const data = await res.json(); - return Array.isArray(data) ? data : (data.categories || []); - } catch { - return []; - } +async function loadKBArticles(): Promise { + const config = await getRuntimeConfig('knowledge_base', 'GLOBAL_KB'); + return config?.payload || { articles: [] }; } -async function loadArticles(): Promise { - try { - const res = await fetch(`${API}/api/admin/kb/articles`); - if (!res.ok) throw new Error('Failed to load'); - const data = await res.json(); - return Array.isArray(data) ? data : (data.articles || []); - } catch { - return []; - } -} +export default function KnowledgeBasePage() { + const [kbData, { refetch }] = createResource(loadKBArticles); + const [view, setView] = createSignal<'list' | 'editor'>('list'); + const [searchQuery, setSearchQuery] = createSignal(''); + const [selectedCategory, setSelectedCategory] = createSignal('All'); + + // Editor state + const [editingArticle, setEditingArticle] = createSignal | null>(null); + const [saving, setSaving] = createSignal(false); -type KbPageProps = { - initialTab?: 'categories' | 'articles' | 'create-article'; -}; + const filteredArticles = () => { + const articles = kbData()?.articles || []; + return articles.filter(a => { + const matchesSearch = a.title.toLowerCase().includes(searchQuery().toLowerCase()) || + a.content.toLowerCase().includes(searchQuery().toLowerCase()); + const matchesCategory = selectedCategory() === 'All' || a.category === selectedCategory(); + return matchesSearch && matchesCategory; + }); + }; -export default function KbPage(props: KbPageProps = {}) { - const [tab, setTab] = createSignal<'categories' | 'articles' | 'create-article'>(props.initialTab || 'categories'); + const handleEdit = (article: RuntimeKBArticle) => { + setEditingArticle({ ...article }); + setView('editor'); + }; - // Categories resource - const [categories, { refetch: refetchCategories }] = createResource(loadCategories); - // Articles resource - const [articles, { refetch: refetchArticles }] = createResource(loadArticles); + const handleCreate = () => { + setEditingArticle({ + id: `kb-${Date.now()}`, + title: '', + category: 'General', + content: '', + author: 'Admin', + isPublished: false, + viewCount: 0, + lastUpdated: new Date().toISOString() + }); + setView('editor'); + }; - // --- Categories tab state --- - const [showCatForm, setShowCatForm] = createSignal(false); - const [catName, setCatName] = createSignal(''); - const [catSlug, setCatSlug] = createSignal(''); - const [catDesc, setCatDesc] = createSignal(''); - const [catSaving, setCatSaving] = createSignal(false); - const [catError, setCatError] = createSignal(''); - const [editingCatId, setEditingCatId] = createSignal(''); - const [editCatName, setEditCatName] = createSignal(''); - const [editCatSlug, setEditCatSlug] = createSignal(''); - const [editCatSaving, setEditCatSaving] = createSignal(false); - const [editCatError, setEditCatError] = createSignal(''); - const [deletingCatId, setDeletingCatId] = createSignal(''); - const [actionError, setActionError] = createSignal(''); + const handleSave = async () => { + const article = editingArticle(); + if (!article || !article.title || !article.content) return; - // --- Articles tab state --- - const [articleSearch, setArticleSearch] = createSignal(''); - const [deletingArticleId, setDeletingArticleId] = createSignal(''); - const [articleActionError, setArticleActionError] = createSignal(''); - const [seedingArticles, setSeedingArticles] = createSignal(false); - const [seedMessage, setSeedMessage] = createSignal(''); - - // --- Create Article tab state --- - const [artTitle, setArtTitle] = createSignal(''); - const [artSlug, setArtSlug] = createSignal(''); - const [artCategoryId, setArtCategoryId] = createSignal(''); - const [artContent, setArtContent] = createSignal(''); - const [artStatus, setArtStatus] = createSignal('DRAFT'); - const [artSaving, setArtSaving] = createSignal(false); - const [artError, setArtError] = createSignal(''); - - // Filtered articles - const filteredArticles = createMemo(() => { - const all = articles() ?? []; - const q = articleSearch().toLowerCase(); - if (!q) return all; - return all.filter((a) => a.title?.toLowerCase().includes(q)); - }); - - // Categories actions - const handleAddCategory = async (e: Event) => { - e.preventDefault(); + setSaving(true); try { - setCatSaving(true); - setCatError(''); - const res = await fetch(`${API}/api/admin/kb/categories`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: catName(), slug: catSlug(), description: catDesc() }), - }); - if (!res.ok) throw new Error('Failed to create category'); - setCatName(''); - setCatSlug(''); - setCatDesc(''); - setShowCatForm(false); - refetchCategories(); - } catch (err: any) { - setCatError(err.message || 'Failed to create'); - } finally { - setCatSaving(false); - } - }; + const currentArticles = kbData()?.articles || []; + const index = currentArticles.findIndex(a => a.id === article.id); + + const updatedArticle = { + ...article, + lastUpdated: new Date().toISOString() + } as RuntimeKBArticle; - const startEditCat = (cat: KbCategory) => { - setEditingCatId(cat.id); - setEditCatName(cat.name); - setEditCatSlug(cat.slug); - setEditCatError(''); - }; - - const cancelEditCat = () => { - setEditingCatId(''); - setEditCatError(''); - }; - - const saveEditCat = async (id: string) => { - try { - setEditCatSaving(true); - setEditCatError(''); - const res = await fetch(`${API}/api/admin/kb/categories/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: editCatName(), slug: editCatSlug() }), - }); - if (!res.ok) throw new Error('Failed to save'); - setEditingCatId(''); - refetchCategories(); - } catch (err: any) { - setEditCatError(err.message || 'Failed to save'); - } finally { - setEditCatSaving(false); - } - }; - - const deleteCategory = async (id: string, name: string) => { - if (!confirm(`Delete category "${name}"?`)) return; - try { - setDeletingCatId(id); - setActionError(''); - const res = await fetch(`${API}/api/admin/kb/categories/${id}`, { method: 'DELETE' }); - if (!res.ok) throw new Error('Failed to delete'); - refetchCategories(); - } catch (err: any) { - setActionError(err.message || 'Failed to delete'); - } finally { - setDeletingCatId(''); - } - }; - - // Articles actions - const deleteArticle = async (id: string, title: string) => { - if (!confirm(`Delete article "${title}"?`)) return; - try { - setDeletingArticleId(id); - setArticleActionError(''); - const res = await fetch(`${API}/api/admin/kb/articles/${id}`, { method: 'DELETE' }); - if (!res.ok) throw new Error('Failed to delete'); - refetchArticles(); - } catch (err: any) { - setArticleActionError(err.message || 'Failed to delete'); - } finally { - setDeletingArticleId(''); - } - }; - - const seedKnowledgeBaseArticles = async () => { - try { - setSeedingArticles(true); - setArticleActionError(''); - setSeedMessage(''); - - const categoriesRes = await fetch(`${API}/api/admin/kb/categories`); - if (!categoriesRes.ok) throw new Error('Failed to load categories for seeding'); - const categoriesJson = await categoriesRes.json(); - const existingCategories: KbCategory[] = Array.isArray(categoriesJson) ? categoriesJson : (categoriesJson.categories || []); - const categoryBySlug = new Map(existingCategories.map((c) => [c.slug, c])); - - for (const cat of KB_SEED_CATEGORIES) { - if (categoryBySlug.has(cat.slug)) continue; - const createRes = await fetch(`${API}/api/admin/kb/categories`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(cat), - }); - if (!createRes.ok) throw new Error(`Failed to create category: ${cat.name}`); + let newArticles = [...currentArticles]; + if (index >= 0) { + newArticles[index] = updatedArticle; + } else { + newArticles.unshift(updatedArticle); } - const freshCategoriesRes = await fetch(`${API}/api/admin/kb/categories`); - if (!freshCategoriesRes.ok) throw new Error('Failed to refresh categories after seeding'); - const freshCategoriesJson = await freshCategoriesRes.json(); - const freshCategories: KbCategory[] = Array.isArray(freshCategoriesJson) ? freshCategoriesJson : (freshCategoriesJson.categories || []); - const freshCategoryBySlug = new Map(freshCategories.map((c) => [c.slug, c])); - - const articlesRes = await fetch(`${API}/api/admin/kb/articles`); - if (!articlesRes.ok) throw new Error('Failed to load articles for duplicate checks'); - const articlesJson = await articlesRes.json(); - const existingArticles: KbArticle[] = Array.isArray(articlesJson) ? articlesJson : (articlesJson.articles || []); - const existingSlugs = new Set(existingArticles.map((a) => (a.slug || '').trim()).filter(Boolean)); - - let created = 0; - let skipped = 0; - - for (const article of KB_SEED_ARTICLES) { - if (existingSlugs.has(article.slug)) { - skipped += 1; - continue; - } - const category = freshCategoryBySlug.get(article.categorySlug); - if (!category) throw new Error(`Missing category for seed article: ${article.title}`); - - const createArticleRes = await fetch(`${API}/api/admin/kb/articles`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - title: article.title, - slug: article.slug, - content: article.content, - status: 'PUBLISHED', - category_id: category.id, - }), - }); - if (!createArticleRes.ok) throw new Error(`Failed to create article: ${article.title}`); - existingSlugs.add(article.slug); - created += 1; - } - - refetchCategories(); - refetchArticles(); - setSeedMessage(`KB seeding complete: ${created} created, ${skipped} skipped (already existed).`); - } catch (err: any) { - setArticleActionError(err?.message || 'Failed to seed KB articles'); + 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 { - setSeedingArticles(false); + setSaving(false); } }; - // Create article - const handleCreateArticle = async (e: Event) => { - e.preventDefault(); + const handleDelete = async (id: string) => { + if (!confirm('Are you sure you want to delete this article?')) return; + try { - setArtSaving(true); - setArtError(''); - const body: Record = { - title: artTitle(), - slug: artSlug(), - content: artContent(), - status: artStatus(), - }; - if (artCategoryId()) body.category_id = artCategoryId(); - const res = await fetch(`${API}/api/admin/kb/articles`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), + 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() + }); + }); }); - if (!res.ok) throw new Error('Failed to create article'); - setArtTitle(''); - setArtSlug(''); - setArtCategoryId(''); - setArtContent(''); - setArtStatus('DRAFT'); - setTab('articles'); - refetchArticles(); - } catch (err: any) { - setArtError(err.message || 'Failed to create'); + + await saveRuntimeConfig('knowledge_base', 'GLOBAL_KB', { articles }, 'published'); + await refetch(); + alert('40 articles seeded successfully!'); + } catch (err) { + console.error('Seeding failed:', err); } finally { - setArtSaving(false); + setSaving(false); } }; return ( -
-
-

Knowledge Base

-

Manage help articles and categories

+
+
+
+

+ + Knowledge Base Management +

+

Manage help articles and runtime documentation.

- - {/* Tabs */} -
- - -
+
-
- - {/* Categories Tab */} - -
-
- + +
+
+

{editingArticle()?.id ? 'Edit' : 'Create'} Article

+
- - -
{actionError()}
-
- - -
-

New Category

- -
{catError()}
-
-
-
- - setCatName(e.currentTarget.value)} - required - placeholder="e.g. Getting Started" - style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px" - /> -
-
- - setCatSlug(e.currentTarget.value)} - required - placeholder="e.g. getting-started" - style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px" - /> -
-
- -