diff --git a/chat/public/admin-accounts.html b/chat/public/admin-accounts.html index b83d873..dd397dc 100644 --- a/chat/public/admin-accounts.html +++ b/chat/public/admin-accounts.html @@ -35,6 +35,7 @@ Resources External Testing Contact Messages + Feature Requests Login diff --git a/chat/public/admin-affiliates.html b/chat/public/admin-affiliates.html index c9381b3..9209830 100644 --- a/chat/public/admin-affiliates.html +++ b/chat/public/admin-affiliates.html @@ -35,6 +35,7 @@ Resources External Testing Contact Messages + Feature Requests Login diff --git a/chat/public/admin-blogs.html b/chat/public/admin-blogs.html index 261d652..1f94f59 100644 --- a/chat/public/admin-blogs.html +++ b/chat/public/admin-blogs.html @@ -247,6 +247,7 @@ Resources External Testing Contact Messages + Feature Requests Blog Management Login diff --git a/chat/public/admin-contact-messages.html b/chat/public/admin-contact-messages.html index 887584e..435cfbd 100644 --- a/chat/public/admin-contact-messages.html +++ b/chat/public/admin-contact-messages.html @@ -140,6 +140,7 @@ Resources External Testing Contact Messages + Feature Requests Login diff --git a/chat/public/admin-external-testing.html b/chat/public/admin-external-testing.html index 9212651..2c2d6f8 100644 --- a/chat/public/admin-external-testing.html +++ b/chat/public/admin-external-testing.html @@ -33,6 +33,7 @@ Resources External Testing Contact Messages + Feature Requests Login diff --git a/chat/public/admin-feature-requests.html b/chat/public/admin-feature-requests.html new file mode 100644 index 0000000..1913d2e --- /dev/null +++ b/chat/public/admin-feature-requests.html @@ -0,0 +1,537 @@ + + + + + + Feature Requests - Admin Panel + + + + + + +
+ +
+
+
+ +
+
Admin
+
Feature Requests
+
Manage and respond to feature requests
+
+
+ + +
+
+ +
+
+
+

All Feature Requests

+
0
+
+ +
+ + +
+
+
+ + + +

No feature requests yet

+
+
+
+
+
+
+
+ + + + diff --git a/chat/public/admin-plan.html b/chat/public/admin-plan.html index 9d9cb32..b9ef621 100644 --- a/chat/public/admin-plan.html +++ b/chat/public/admin-plan.html @@ -33,6 +33,7 @@ Resources External Testing Contact Messages + Feature Requests Login diff --git a/chat/public/admin-plans.html b/chat/public/admin-plans.html index da1284c..e3b5090 100644 --- a/chat/public/admin-plans.html +++ b/chat/public/admin-plans.html @@ -33,6 +33,7 @@ Resources External Testing Contact Messages + Feature Requests Login diff --git a/chat/public/admin-resources.html b/chat/public/admin-resources.html index a02ba4e..23b6c1e 100644 --- a/chat/public/admin-resources.html +++ b/chat/public/admin-resources.html @@ -226,6 +226,7 @@ Resources External Testing Contact Messages + Feature Requests Login diff --git a/chat/public/admin-tracking.html b/chat/public/admin-tracking.html index 6e18ccc..b7ead99 100644 --- a/chat/public/admin-tracking.html +++ b/chat/public/admin-tracking.html @@ -186,6 +186,7 @@ Resources External Testing Contact Messages + Feature Requests Login diff --git a/chat/public/admin-withdrawals.html b/chat/public/admin-withdrawals.html index eaf1d8a..1c087c1 100644 --- a/chat/public/admin-withdrawals.html +++ b/chat/public/admin-withdrawals.html @@ -35,6 +35,7 @@ Resources External Testing Contact Messages + Feature Requests Login diff --git a/chat/public/admin.html b/chat/public/admin.html index 2ae575f..d621657 100644 --- a/chat/public/admin.html +++ b/chat/public/admin.html @@ -45,6 +45,7 @@ Resources External Testing Contact Messages + Feature Requests Blog Management Login diff --git a/chat/public/feature-requests.html b/chat/public/feature-requests.html index e73536a..6312e58 100644 --- a/chat/public/feature-requests.html +++ b/chat/public/feature-requests.html @@ -271,6 +271,66 @@ gap: 16px; font-size: 12px; color: #adb5bd; + align-items: center; + } + + .fr-status { + display: inline-block; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + } + + .fr-status.pending { + background: rgba(108, 117, 125, 0.1); + color: #6c757d; + } + + .fr-status.planned { + background: rgba(0, 123, 255, 0.1); + color: #007bff; + } + + .fr-status.in-progress { + background: rgba(255, 193, 7, 0.1); + color: #856404; + } + + .fr-status.completed { + background: rgba(40, 167, 69, 0.1); + color: #28a745; + } + + .fr-status.declined { + background: rgba(220, 53, 69, 0.1); + color: #dc3545; + } + + .fr-admin-reply { + background: linear-gradient(135deg, rgba(0, 128, 96, 0.05), rgba(0, 76, 63, 0.05)); + border: 1px solid rgba(0, 128, 96, 0.2); + border-radius: 10px; + padding: 14px 16px; + margin-top: 14px; + } + + .fr-admin-reply-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + color: var(--shopify-green); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; + } + + .fr-admin-reply-text { + font-size: 14px; + line-height: 1.6; + color: #1a1a1a; } .fr-empty { @@ -697,6 +757,17 @@ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } + function getStatusLabel(status) { + const labels = { + 'pending': 'Pending', + 'planned': 'Planned', + 'in-progress': 'In Progress', + 'completed': 'Completed', + 'declined': 'Declined' + }; + return labels[status] || status; + } + function renderFeatureRequests() { if (state.featureRequests.length === 0) { frList.innerHTML = ` @@ -725,10 +796,21 @@

${escapeHtml(fr.title)}

${escapeHtml(fr.description)}

- Submitted by ${escapeHtml(fr.authorEmail || 'Anonymous')} + ${getStatusLabel(fr.status || 'pending')} ${formatDate(fr.createdAt)}
+ ${fr.adminReply ? ` +
+
+ + + + Admin Response +
+
${escapeHtml(fr.adminReply)}
+
+ ` : ''} diff --git a/chat/server.js b/chat/server.js index 2279b49..e0a5b39 100644 --- a/chat/server.js +++ b/chat/server.js @@ -11217,7 +11217,6 @@ async function handleAdminWithdrawalUpdate(req, res) { async function handleFeatureRequestsList(req, res) { const session = getUserSession(req); const userId = session?.userId || ''; - const userEmail = userId ? (findUserById(userId)?.email || '') : ''; const sorted = [...featureRequestsDb].sort((a, b) => { if (b.votes !== a.votes) return b.votes - a.votes; @@ -11230,7 +11229,8 @@ async function handleFeatureRequestsList(req, res) { description: fr.description, votes: fr.votes, createdAt: fr.createdAt, - authorEmail: fr.authorEmail, + status: fr.status || 'pending', + adminReply: fr.adminReply || '', hasVoted: userId ? fr.upvoters.includes(userId) : false, })); @@ -11264,7 +11264,10 @@ async function handleFeatureRequestCreate(req, res) { upvoters: [session.userId], authorEmail: user.email || '', authorId: session.userId, + status: 'pending', + adminReply: '', createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), }; featureRequestsDb.push(featureRequest); @@ -11279,7 +11282,8 @@ async function handleFeatureRequestCreate(req, res) { description: featureRequest.description, votes: featureRequest.votes, createdAt: featureRequest.createdAt, - authorEmail: featureRequest.authorEmail, + status: featureRequest.status, + adminReply: featureRequest.adminReply, hasVoted: true, } }); @@ -11315,6 +11319,109 @@ async function handleFeatureRequestUpvote(req, res, id) { }); } +async function handleAdminFeatureRequestsList(req, res) { + const session = getAdminSession(req); + if (!session) { + return sendJson(res, 403, { error: 'Admin access required' }); + } + + const sorted = [...featureRequestsDb].sort((a, b) => { + if (b.votes !== a.votes) return b.votes - a.votes; + return new Date(b.createdAt) - new Date(a.createdAt); + }); + + const result = sorted.map(fr => ({ + id: fr.id, + title: fr.title, + description: fr.description, + votes: fr.votes, + createdAt: fr.createdAt, + updatedAt: fr.updatedAt, + authorEmail: fr.authorEmail, + authorId: fr.authorId, + status: fr.status || 'pending', + adminReply: fr.adminReply || '', + upvoters: fr.upvoters || [], + })); + + sendJson(res, 200, { featureRequests: result }); +} + +async function handleFeatureRequestReply(req, res, id) { + const session = getAdminSession(req); + if (!session) { + return sendJson(res, 403, { error: 'Admin access required' }); + } + + const featureRequest = featureRequestsDb.find(fr => fr.id === id); + if (!featureRequest) { + return sendJson(res, 404, { error: 'Feature request not found' }); + } + + try { + const body = await parseJsonBody(req); + const reply = (body.reply || '').toString().trim(); + + featureRequest.adminReply = reply; + featureRequest.updatedAt = new Date().toISOString(); + await persistFeatureRequestsDb(); + + log('Feature request reply added', { id, adminId: session.userId }); + sendJson(res, 200, { ok: true, adminReply: reply }); + } catch (error) { + sendJson(res, 400, { error: error.message || 'Unable to add reply' }); + } +} + +async function handleFeatureRequestUpdateStatus(req, res, id) { + const session = getAdminSession(req); + if (!session) { + return sendJson(res, 403, { error: 'Admin access required' }); + } + + const featureRequest = featureRequestsDb.find(fr => fr.id === id); + if (!featureRequest) { + return sendJson(res, 404, { error: 'Feature request not found' }); + } + + try { + const body = await parseJsonBody(req); + const status = (body.status || '').toString().trim(); + const validStatuses = ['pending', 'planned', 'in-progress', 'completed', 'declined']; + + if (!validStatuses.includes(status)) { + return sendJson(res, 400, { error: 'Invalid status. Must be one of: ' + validStatuses.join(', ') }); + } + + featureRequest.status = status; + featureRequest.updatedAt = new Date().toISOString(); + await persistFeatureRequestsDb(); + + log('Feature request status updated', { id, status, adminId: session.userId }); + sendJson(res, 200, { ok: true, status }); + } catch (error) { + sendJson(res, 400, { error: error.message || 'Unable to update status' }); + } +} + +async function handleFeatureRequestDelete(req, res, id) { + const session = getAdminSession(req); + if (!session) { + return sendJson(res, 403, { error: 'Admin access required' }); + } + + const index = featureRequestsDb.findIndex(fr => fr.id === id); + if (index === -1) { + return sendJson(res, 404, { error: 'Feature request not found' }); + } + + featureRequestsDb.splice(index, 1); + await persistFeatureRequestsDb(); + + log('Feature request deleted', { id, adminId: session.userId }); + sendJson(res, 200, { ok: true }); +} + async function handleContactMessagesList(req, res) { const session = getAdminSession(req); @@ -17369,6 +17476,14 @@ async function routeInternal(req, res, url, pathname) { if (req.method === 'POST' && pathname === '/api/feature-requests') return handleFeatureRequestCreate(req, res); const featureUpvoteMatch = pathname.match(/^\/api\/feature-requests\/([a-f0-9\-]+)\/upvote$/i); if (req.method === 'POST' && featureUpvoteMatch) return handleFeatureRequestUpvote(req, res, featureUpvoteMatch[1]); + // Admin feature request endpoints + if (req.method === 'GET' && pathname === '/api/admin/feature-requests') return handleAdminFeatureRequestsList(req, res); + const featureRequestReplyMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)\/reply$/i); + if (req.method === 'POST' && featureRequestReplyMatch) return handleFeatureRequestReply(req, res, featureRequestReplyMatch[1]); + const featureRequestStatusMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)\/status$/i); + if (req.method === 'POST' && featureRequestStatusMatch) return handleFeatureRequestUpdateStatus(req, res, featureRequestStatusMatch[1]); + const featureRequestDeleteMatch = pathname.match(/^\/api\/admin\/feature-requests\/([a-f0-9\-]+)$/i); + if (req.method === 'DELETE' && featureRequestDeleteMatch) return handleFeatureRequestDelete(req, res, featureRequestDeleteMatch[1]); if (req.method === 'POST' && pathname === '/api/contact') return handleContactMessageCreate(req, res); const contactMessagesMatch = pathname.match(/^\/api\/contact\/messages$/i); if (req.method === 'GET' && contactMessagesMatch) return handleContactMessagesList(req, res);