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 ? `
+
+
+
${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);