diff --git a/src/platform/package.json b/src/platform/package.json
index 7993750445..bb6ef54352 100644
--- a/src/platform/package.json
+++ b/src/platform/package.json
@@ -3,8 +3,10 @@
"version": "3.7.0",
"private": true,
"scripts": {
+ "clean:next": "node -e \"const fs=require('node:fs'); try { fs.rmSync('.next',{recursive:true,force:true,maxRetries:10,retryDelay:200}); } catch (error) { if (error && typeof error === 'object' && 'code' in error && (error.code === 'EPERM' || error.code === 'EBUSY')) { console.warn('[clean:next] Skipping .next cleanup because the directory is locked:', error.code); } else { throw error; } }\"",
"dev": "next dev",
"build": "next build",
+ "prebuild": "yarn clean:next",
"postbuild": "node -e \"const fs=require('node:fs'); const path=require('node:path'); const projectRoot=process.cwd(); const standaloneRoot=path.join(projectRoot,'.next','standalone'); const publicSource=path.join(projectRoot,'public'); const publicTarget=path.join(standaloneRoot,'public'); const staticSource=path.join(projectRoot,'.next','static'); const staticTarget=path.join(standaloneRoot,'.next','static'); const copyDirectory=(source,target)=>{ if(!fs.existsSync(source)) return false; fs.mkdirSync(path.dirname(target),{recursive:true}); fs.rmSync(target,{recursive:true,force:true}); fs.cpSync(source,target,{recursive:true}); return true; }; if(!fs.existsSync(standaloneRoot)){ throw new Error(`Standalone output not found at ${standaloneRoot}. Run 'yarn build' first.`); } copyDirectory(publicSource, publicTarget); copyDirectory(staticSource, staticTarget);\"",
"start": "node -e \"const { loadEnvConfig } = require('@next/env'); loadEnvConfig(process.cwd()); process.env.HOSTNAME='localhost'; require('./.next/standalone/server.js')\"",
"lint": "next lint",
diff --git a/src/platform/src/app/(dashboard)/system/feedback/[feedbackId]/page.tsx b/src/platform/src/app/(dashboard)/system/feedback/[feedbackId]/page.tsx
index cc66cf8556..c6b675c812 100644
--- a/src/platform/src/app/(dashboard)/system/feedback/[feedbackId]/page.tsx
+++ b/src/platform/src/app/(dashboard)/system/feedback/[feedbackId]/page.tsx
@@ -1,14 +1,17 @@
'use client';
import React, { useEffect, useMemo, useState } from 'react';
+import Image from 'next/image';
import { useParams, useRouter } from 'next/navigation';
import useSWR, { useSWRConfig } from 'swr';
+import { FaRegStar, FaStar } from 'react-icons/fa';
+import { FiExternalLink } from 'react-icons/fi';
import { PermissionGuard } from '@/shared/components';
import {
Button,
Card,
- PageHeading,
LoadingState,
+ PageHeading,
} from '@/shared/components/ui';
import { AqArrowLeft } from '@airqo/icons-react';
import { feedbackService } from '@/modules/feedback';
@@ -60,6 +63,41 @@ const formatDateTime = (value: string) =>
minute: '2-digit',
});
+const formatKeyLabel = (key: string) =>
+ key
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
+ .replace(/_/g, ' ')
+ .replace(/\b\w/g, character => character.toUpperCase());
+
+const DetailPanel: React.FC<{
+ label: string;
+ children: React.ReactNode;
+ valueClassName?: string;
+}> = ({ label, children, valueClassName = '' }) => (
+
+
+ {label}
+
+
+ {children}
+
+
+);
+
+const SectionHeader: React.FC<{
+ title: string;
+ description: string;
+ action?: React.ReactNode;
+}> = ({ title, description, action }) => (
+
+
+
{title}
+
{description}
+
+ {action ?
{action}
: null}
+
+);
+
const FeedbackDetailsContent: React.FC<{ feedbackId: string }> = ({
feedbackId,
}) => {
@@ -91,14 +129,65 @@ const FeedbackDetailsContent: React.FC<{ feedbackId: string }> = ({
return [];
}
- return [
- ['Page', feedback.metadata.page || '--'],
- ['Browser', feedback.metadata.browser || '--'],
- ['App version', feedback.metadata.appVersion || '--'],
- ['Screen resolution', feedback.metadata.screenResolution || '--'],
- ] as Array<[string, string]>;
+ return Object.entries(feedback.metadata)
+ .filter(([, value]) => {
+ if (value === null || value === undefined) {
+ return false;
+ }
+
+ const normalizedValue = String(value).trim();
+ return normalizedValue.length > 0;
+ })
+ .map(([key, value]) => ({
+ key,
+ label: formatKeyLabel(key),
+ value: String(value),
+ }))
+ .sort((leftEntry, rightEntry) =>
+ leftEntry.label.localeCompare(rightEntry.label)
+ );
}, [feedback?.metadata]);
+ const overviewItems = useMemo(
+ () => [
+ {
+ label: 'App',
+ value: feedback?.app || '--',
+ },
+ {
+ label: 'Platform',
+ value: feedback?.platform || '--',
+ },
+ {
+ label: 'Tenant',
+ value: feedback?.tenant || '--',
+ },
+ {
+ label: 'Submission ID',
+ value: feedback?._id || '--',
+ },
+ ],
+ [feedback?._id, feedback?.app, feedback?.platform, feedback?.tenant]
+ );
+
+ const allowedStatusOptions = useMemo(() => {
+ if (!feedback) {
+ return [];
+ }
+
+ const allowedStatuses = new Set([
+ feedback.status,
+ ...(ALLOWED_TRANSITIONS[feedback.status] || []),
+ ]);
+
+ return STATUS_OPTIONS.filter(status => allowedStatuses.has(status));
+ }, [feedback]);
+
+ const rating = Math.max(0, Math.min(5, Number(feedback?.rating || 0)));
+ const headingSubtitle = `Submitted by ${feedback?.email || '--'} on ${formatDateTime(
+ feedback?.createdAt || new Date().toISOString()
+ )}`;
+
const handleBack = () => {
router.push('/system/feedback');
};
@@ -168,153 +257,229 @@ const FeedbackDetailsContent: React.FC<{ feedbackId: string }> = ({
-
-
-
-
-
-
-
Current status
-
+ {STATUS_LABELS[feedback.status] || feedback.status}
+
+ }
+ >
+
+
+ {CATEGORY_LABELS[feedback.category] || feedback.category}
+
+
+ {feedback.platform}
+
+ {feedback.app ? (
+
+ {feedback.app}
+
+ ) : null}
+
+
+ {Array.from({ length: 5 }, (_, index) =>
+ index < rating ? (
+
+ ) : (
+
+ )
+ )}
+
+ {rating}/5
+
+
+
+
+
+
+
+
+
+
+
+ {feedback.email}
+
+
+ {formatDateTime(feedback.createdAt)}
+
+
+ {formatDateTime(feedback.updatedAt)}
+
+
+
+
+ {Array.from({ length: 5 }, (_, index) =>
+ index < rating ? (
+
+ ) : (
+
+ )
+ )}
+
+
+ {rating}/5
+
+
+
+ {overviewItems.map(item => (
+
- {STATUS_LABELS[feedback.status] || feedback.status}
-
-
+ {item.value}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {STATUS_LABELS[feedback.status] || feedback.status}
+
+
+
+
+ {allowedStatusOptions.map(status => (
+
+ ))}
+
+
+
+
-
- {STATUS_OPTIONS.filter(status => {
- const allowed = new Set
([
- feedback.status,
- ...(ALLOWED_TRANSITIONS[feedback.status] || []),
- ]);
- return allowed.has(status);
- }).map(status => (
-