Skip to content

Add user feedback module#337

Open
linusnorton wants to merge 1 commit intomasterfrom
feature/feedback-module-clean
Open

Add user feedback module#337
linusnorton wants to merge 1 commit intomasterfrom
feature/feedback-module-clean

Conversation

@linusnorton
Copy link
Copy Markdown
Collaborator

@linusnorton linusnorton commented Feb 5, 2026

Summary

Adds a new feedback feature module that allows users to submit feedback about the service:

  • User-facing feedback form with rating, category, comments and optional email
  • Admin dashboard for viewing and managing feedback submissions
  • Statistics panel showing total submissions, average rating, resolved/unresolved counts
  • API endpoints for feedback CRUD operations
  • Database schema for storing feedback with tracking metadata

Changes

  • libs/feedback/ - New module with:
    • Prisma schema for Feedback model
    • Service layer for business logic
    • Validation functions for form input
    • Page controllers for user and admin interfaces
    • API route handlers
    • Nunjucks templates following GOV.UK Design System
    • English and Welsh translation files

Test plan

  • Submit feedback as anonymous user
  • Submit feedback as authenticated user
  • Verify validation errors display correctly
  • Test Welsh language toggle
  • Access admin dashboard
  • View feedback statistics
  • Resolve feedback item
  • Delete feedback item

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a user feedback submission form allowing visitors to provide ratings, categorise issues, and leave comments
    • Introduced an admin dashboard to view, search, and manage all feedback submissions
    • Implemented feedback resolution tracking with administrative notes capability
    • Added multi-language support (English and Welsh) across feedback interfaces
    • Included feedback statistics dashboard showing submission metrics and resolution status

Adds a new feedback feature that allows users to submit feedback about the service:

- User-facing feedback form with rating, category, comments and optional email
- Admin dashboard for viewing and managing feedback submissions
- Statistics panel showing total, average rating, resolved/unresolved counts
- API endpoints for feedback operations
- Database schema for storing feedback with metadata

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

This introduces a new @hmcts/feedback package providing complete feedback collection and management functionality, including database schema, API routes, server-rendered form pages with validation, admin dashboard, and internationalization support.

Changes

Cohort / File(s) Summary
Build & Configuration
libs/feedback/package.json, libs/feedback/tsconfig.json
Package metadata, ESM configuration, build scripts for TypeScript and Nunjucks processing, dependencies including express and postgres-prisma, and test/lint configuration.
Database Schema
libs/feedback/prisma/schema.prisma
Feedback model with fields for user input (rating, category, comments, email), metadata (userId, pageUrl, userAgent, ipAddress), resolution tracking (isResolved, resolvedBy, resolvedAt, adminNotes), timestamps, and two indexes for efficient querying.
Core Configuration
libs/feedback/src/config.ts, libs/feedback/src/index.ts
Path exports for pages, routes, and Prisma schemas; re-exports of service and queries modules for public API surface.
Data Access & Service Layer
libs/feedback/src/feedback/queries.ts, libs/feedback/src/feedback/service.ts, libs/feedback/src/feedback/validation.ts, libs/feedback/src/feedback/service.test.ts
Prisma-based CRUD operations (create, retrieve, search, update, delete feedback), service wrapper with validation and business logic, form validation utilities, and test suite with mocked Prisma client.
API Routes
libs/feedback/src/routes/feedback.ts
REST endpoints for retrieving feedback (GET by id or search), resolving/deleting feedback (POST/PATCH), and fetching statistics with appropriate error handling and status codes.
Feedback Form Pages
libs/feedback/src/pages/feedback/index.ts, libs/feedback/src/pages/feedback/index.njk, libs/feedback/src/pages/feedback/success/...
Server-rendered feedback form with rating, category, comments, and email fields; locale-aware form handling with validation error display; success page with redirect protection and localized content.
Localization
libs/feedback/src/pages/feedback/en.ts, libs/feedback/src/pages/feedback/cy.ts
English and Welsh translations for feedback form UI, labels, hints, error messages, and success content.
Admin Dashboard Pages
libs/feedback/src/pages/admin/feedback/index.ts, libs/feedback/src/pages/admin/feedback/index.njk
Admin interface displaying feedback list with status tags, statistics cards (total count, average rating, resolved/unresolved), and actions for viewing and deleting entries; locale support for UI text.

Sequence Diagram(s)

sequenceDiagram
    participant User as User<br/>(Browser)
    participant Server as Express<br/>Server
    participant Validation as Validation<br/>Layer
    participant Service as Feedback<br/>Service
    participant Database as Prisma<br/>(PostgreSQL)

    User->>Server: GET /feedback
    Server->>Server: Select locale translation
    Server->>User: Render feedback form
    
    User->>Server: POST /feedback<br/>(rating, category, comments, etc.)
    Server->>Validation: validateFeedbackForm()
    alt Validation Errors
        Validation-->>Server: Return errors array
        Server->>User: Re-render form with errors
    else Valid
        Validation-->>Server: No errors
        Server->>Service: submitFeedback()
        Service->>Database: createFeedback()
        Database-->>Service: Feedback record created
        Service-->>Server: Success
        Server->>User: Redirect to success page
    end
Loading
sequenceDiagram
    participant Admin as Admin<br/>(Browser)
    participant Server as Express<br/>Server
    participant Service as Feedback<br/>Service
    participant Database as Prisma<br/>(PostgreSQL)

    Admin->>Server: GET /admin/feedback
    Server->>Service: listAllFeedback()
    Service->>Database: Query all feedback
    Database-->>Service: Return feedback list
    Server->>Service: getFeedbackStats()
    Service->>Database: Query counts & averages
    Database-->>Service: Return stats
    Server->>Admin: Render dashboard<br/>(table + statistics)
    
    Admin->>Server: POST /admin/feedback<br/>(action: delete, id)
    Server->>Service: removeFeedback(id)
    Service->>Database: Delete feedback by id
    Database-->>Service: Deletion confirmed
    Server->>Admin: Redirect to dashboard
Loading
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarises the main change—introducing a complete user feedback module with forms, validation, admin dashboard, and API endpoints.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/feedback-module-clean

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 5, 2026

🎭 Playwright E2E Test Results

0 tests   0 ✅  0s ⏱️
0 suites  0 💤
0 files    0 ❌

Results for commit 90b2d8c.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (11)
libs/feedback/src/pages/feedback/success/index.njk-1-19 (1)

1-19: ⚠️ Potential issue | 🟡 Minor

Add govukErrorSummary handling to satisfy template requirements.
This template omits the error summary block. Please include it (conditionally is fine) to meet the template standard.

💡 Suggested update
 {% extends "layouts/base-template.njk" %}
 {% from "govuk/components/panel/macro.njk" import govukPanel %}
+{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
@@
 {% block content %}
 <div class="govuk-grid-row">
   <div class="govuk-grid-column-two-thirds">
+
+    {% if errors and errors.length %}
+      {{ govukErrorSummary({
+        titleText: t.errorSummaryTitle,
+        errorList: errors
+      }) }}
+    {% endif %}
As per coding guidelines: Nunjucks templates must extend `layouts/base-template.njk`, use govuk macros from govuk/components, and include error handling with govukErrorSummary.
libs/feedback/src/feedback/validation.ts-12-13 (1)

12-13: ⚠️ Potential issue | 🟡 Minor

Use strict equality (===) instead of loose equality (==).

In TypeScript with strict mode enabled, == with undefined is non-idiomatic and risks unintended type coercion. Use === to enforce strict equality comparisons.

✅ Suggested fix
-  if (rating == undefined || rating == "") {
+  if (rating === undefined || rating === "") {
libs/feedback/src/feedback/validation.ts-33-45 (1)

33-45: ⚠️ Potential issue | 🟡 Minor

Use isValidEmail consistently and return boolean.
This keeps validation logic in one place and uses TypeScript's primitive boolean type rather than the wrapper object type.

Suggested fix
-  if (email && !email.includes("@")) {
+  if (email && !isValidEmail(email)) {
@@
-export function isValidEmail(email: string): Boolean {
+export function isValidEmail(email: string): boolean {
   return email.includes("@") && email.includes(".");
 }
libs/feedback/src/pages/admin/feedback/index.njk-71-74 (1)

71-74: ⚠️ Potential issue | 🟡 Minor

View link is non-functional; delete lacks confirmation.

The view link href="#" is a placeholder and won't navigate anywhere. Additionally, the delete link directly triggers deletion without user confirmation, risking accidental data loss.

Consider:

  1. Implementing the view route (e.g., /admin/feedback/{{ item.id }})
  2. Adding a confirmation step or using a POST form for delete actions
libs/feedback/src/pages/feedback/index.njk-98-98 (1)

98-98: ⚠️ Potential issue | 🟡 Minor

Cancel link missing href.

The anchor tag has no href attribute, rendering it non-functional. Users cannot cancel and navigate away.

Proposed fix
-        <a class="govuk-link">{{ t.cancelLink }}</a>
+        <a class="govuk-link" href="/">{{ t.cancelLink }}</a>
libs/feedback/src/feedback/service.test.ts-70-80 (1)

70-80: ⚠️ Potential issue | 🟡 Minor

Test missing assertions.

The markAsResolved test calls the function but has no assertions to verify behaviour. At minimum, verify the function resolves or check the mock was called with expected arguments.

Proposed fix
     it("should mark feedback as resolved", async () => {
       // Arrange
-      vi.mocked(prisma.feedback.update).mockResolvedValue({ count: 1 });
+      vi.mocked(resolveFeedback).mockResolvedValue({ id: "123", isResolved: true });

       // Act
-      await markAsResolved("123", "admin-user", "Resolved");
+      const result = await markAsResolved("123", "admin-user", "Resolved");

       // Assert
+      expect(resolveFeedback).toHaveBeenCalledWith("123", "admin-user", "Resolved");
+      expect(result).toBeDefined();
     });
libs/feedback/src/pages/admin/feedback/index.njk-14-16 (1)

14-16: ⚠️ Potential issue | 🟡 Minor

Use govukErrorSummary for error display.

The coding guidelines require error handling with govukErrorSummary. The inline styled paragraph doesn't follow GOV.UK Design System patterns.

Proposed fix
     {% if error %}
-      <p style="color: red;">Error: {{ error }}</p>
+      {{ govukErrorSummary({
+        titleText: "There is a problem",
+        errorList: [{ text: error }]
+      }) }}
     {% endif %}
libs/feedback/src/routes/feedback.ts-17-19 (1)

17-19: ⚠️ Potential issue | 🟡 Minor

Return 404 status for missing resources.

When feedback is not found, the response should use HTTP 404 rather than 200 with an error payload.

Proposed fix
       if (!feedback) {
-        res.json({ error: "Feedback not found" });
+        res.status(404).json({ error: "Feedback not found" });
         return;
       }
libs/feedback/src/routes/feedback.ts-54-54 (1)

54-54: ⚠️ Potential issue | 🟡 Minor

Typo in error message.

"Inavlid" should be "Invalid".

Proposed fix
-    res.status(400).json({ error: "Inavlid action" });
+    res.status(400).json({ error: "Invalid action" });
libs/feedback/src/feedback/service.ts-78-80 (1)

78-80: ⚠️ Potential issue | 🟡 Minor

Validate against an undefined key.

If ADMIN_API_KEY is moved to an environment variable (as recommended), this function will return true when both key and the env var are undefined. Add a guard.

Proposed fix
 export function validateApiKey(key: string): boolean {
+  if (!ADMIN_API_KEY) {
+    return false;
+  }
   return key === ADMIN_API_KEY;
 }
libs/feedback/src/pages/admin/feedback/index.ts-48-48 (1)

48-48: ⚠️ Potential issue | 🟡 Minor

Avoid exposing raw error messages to the view.

error.message may contain sensitive database or system information. Consider using a generic user-facing message instead.

Proposed fix
-      error: error.message,
+      error: "An error occurred while loading feedback data.",
🧹 Nitpick comments (10)
libs/feedback/src/feedback/validation.ts (1)

51-54: Move constants above functions to match module ordering.
This keeps the module consistent with the required structure.

🔧 Suggested layout
 import type { ValidationError } from "@hmcts/web-core";
+
+const ALLOWED_CATEGORIES = ["General", "Bug Report", "Feature Request", "Accessibility", "Other"];
@@
-const ALLOWED_CATEGORIES = ["General", "Bug Report", "Feature Request", "Accessibility", "Other"];
-
 export function isValidCategory(category: string): boolean {
   return ALLOWED_CATEGORIES.includes(category);
 }
As per coding guidelines: Module ordering: constants outside function scope at top, exported functions next, other functions in usage order, interfaces and types at bottom.
libs/feedback/src/feedback/service.test.ts (1)

83-83: Empty test suite.

The FeedbackStats describe block contains no tests. Either add tests or remove the placeholder.

Would you like me to generate tests for FeedbackStats or open an issue to track this?

libs/feedback/src/pages/admin/feedback/index.njk (1)

19-44: Avoid inline styles.

The stat cards use inline style attributes. Prefer CSS classes for maintainability and consistency with the GOV.UK Design System.

libs/feedback/src/pages/feedback/index.njk (1)

36-37: Hardcoded English string.

The placeholder text "Choose a rating" should use a translation key (e.g., t.ratingPlaceholder) for i18n consistency.

libs/feedback/src/pages/feedback/index.ts (3)

7-7: Unused import.

FeedbackCategoryOptions is imported but never used. Remove to reduce noise.


44-44: Deprecated API.

req.connection is deprecated. Use req.socket.remoteAddress instead.

Proposed fix
-    const ipAddress = req.ip || req.connection.remoteAddress || "unknown";
+    const ipAddress = req.ip || req.socket?.remoteAddress || "unknown";

78-83: Unused interface.

FeedbackFormData is defined but never referenced. Remove dead code per YAGNI. Based on learnings: only export functions intended for use outside the module.

libs/feedback/src/routes/feedback.ts (1)

72-72: Use camelCase for function names.

getStats_handler should be getStatsHandler per project naming conventions.

Proposed fix
-export const getStats_handler = async (_req: Request, res: Response) => {
+export const getStatsHandler = async (_req: Request, res: Response) => {

As per coding guidelines: "TypeScript variables should use camelCase."

libs/feedback/src/feedback/queries.ts (1)

37-47: Consider using update instead of updateMany for single-record updates.

Since id is unique, update is more appropriate and returns the updated record, which may be useful for callers.

Proposed fix
 export async function resolveFeedback(id: string, resolvedBy: string, adminNotes: string) {
-  return prisma.feedback.updateMany({
+  return prisma.feedback.update({
     where: { id },
     data: {
       isResolved: true,
       resolvedBy,
       resolvedAt: new Date(),
       adminNotes,
     },
   });
 }
libs/feedback/src/feedback/service.ts (1)

86-94: Use camelCase for function names.

FeedbackCategoryOptions should be feedbackCategoryOptions or, if treated as a constant, FEEDBACK_CATEGORY_OPTIONS.

Proposed fix
-export function FeedbackCategoryOptions() {
+export function feedbackCategoryOptions() {
   return [
     "General",
     "Bug Report",
     "Feature Request",
     "Accessibility",
     "Other",
   ];
 }

As per coding guidelines: "TypeScript variables should use camelCase."

Comment on lines +17 to +18
@@map("feedbacks")
@@index([createdAt], name: "idx_feedback_created")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Table mapping should be singular snake_case.
@@map("feedbacks") is plural, which violates the schema naming rule.

🛠️ Suggested fix
-  @@map("feedbacks")
+  @@map("feedback")
As per coding guidelines: Database tables and fields MUST be singular and snake_case (e.g., `user`, `case`, `created_at`). Use Prisma `@@map` and `@map` for aliases.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@@map("feedbacks")
@@index([createdAt], name: "idx_feedback_created")
@@map("feedback")
@@index([createdAt], name: "idx_feedback_created")

Comment on lines +7 to +9
export const pageRoutes = { path: path.join(__dirname, "pages") };
export const apiRoutes = { path: path.join(__dirname, "routes") };
export const prismaSchemas = path.join(__dirname, "../prisma");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Export assets path alongside routes and Prisma schema.
config.ts should provide the assets path export as part of the module contract.

✅ Suggested fix
 export const pageRoutes = { path: path.join(__dirname, "pages") };
 export const apiRoutes = { path: path.join(__dirname, "routes") };
 export const prismaSchemas = path.join(__dirname, "../prisma");
+export const assets = { path: path.join(__dirname, "assets") };
As per coding guidelines: Module config.ts must export path configurations (pageRoutes, apiRoutes, prismaSchemas, assets) separately to avoid circular dependencies during Prisma client generation.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const pageRoutes = { path: path.join(__dirname, "pages") };
export const apiRoutes = { path: path.join(__dirname, "routes") };
export const prismaSchemas = path.join(__dirname, "../prisma");
export const pageRoutes = { path: path.join(__dirname, "pages") };
export const apiRoutes = { path: path.join(__dirname, "routes") };
export const prismaSchemas = path.join(__dirname, "../prisma");
export const assets = { path: path.join(__dirname, "assets") };

Comment on lines +3 to +8
export async function searchFeedback(searchTerm: string) {
const results = await prisma.$queryRawUnsafe(
`SELECT * FROM feedbacks WHERE comments LIKE '%${searchTerm}%' OR email LIKE '%${searchTerm}%'`
);
return results;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

SQL injection vulnerability.

Using $queryRawUnsafe with string interpolation allows arbitrary SQL injection. An attacker could supply a malicious searchTerm to extract or modify data.

Proposed fix using Prisma's safe query methods
 export async function searchFeedback(searchTerm: string) {
-  const results = await prisma.$queryRawUnsafe(
-    `SELECT * FROM feedbacks WHERE comments LIKE '%${searchTerm}%' OR email LIKE '%${searchTerm}%'`
-  );
-  return results;
+  return prisma.feedback.findMany({
+    where: {
+      OR: [
+        { comments: { contains: searchTerm, mode: "insensitive" } },
+        { email: { contains: searchTerm, mode: "insensitive" } },
+      ],
+    },
+    orderBy: { createdAt: "desc" },
+  });
 }

As per coding guidelines: "Validate all inputs on endpoints and use parameterized database queries via Prisma."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function searchFeedback(searchTerm: string) {
const results = await prisma.$queryRawUnsafe(
`SELECT * FROM feedbacks WHERE comments LIKE '%${searchTerm}%' OR email LIKE '%${searchTerm}%'`
);
return results;
}
export async function searchFeedback(searchTerm: string) {
return prisma.feedback.findMany({
where: {
OR: [
{ comments: { contains: searchTerm, mode: "insensitive" } },
{ email: { contains: searchTerm, mode: "insensitive" } },
],
},
orderBy: { createdAt: "desc" },
});
}

Comment on lines +55 to +63
export async function incrementFeedbackViews(id: string) {
const feedback = await prisma.feedback.findUnique({ where: { id } });
if (feedback) {
return prisma.feedback.update({
where: { id },
data: { views: (feedback as any).views + 1 },
});
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition and unsafe type assertion.

The read-then-write pattern is susceptible to race conditions under concurrent requests. Additionally, (feedback as any).views suggests views may not exist on the Prisma type, indicating a schema mismatch or missing field.

Proposed fix using atomic increment
 export async function incrementFeedbackViews(id: string) {
-  const feedback = await prisma.feedback.findUnique({ where: { id } });
-  if (feedback) {
-    return prisma.feedback.update({
-      where: { id },
-      data: { views: (feedback as any).views + 1 },
-    });
-  }
+  return prisma.feedback.update({
+    where: { id },
+    data: { views: { increment: 1 } },
+  });
 }

Ensure the views field is defined in the Prisma schema; if not, add it or remove this function.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function incrementFeedbackViews(id: string) {
const feedback = await prisma.feedback.findUnique({ where: { id } });
if (feedback) {
return prisma.feedback.update({
where: { id },
data: { views: (feedback as any).views + 1 },
});
}
}
export async function incrementFeedbackViews(id: string) {
return prisma.feedback.update({
where: { id },
data: { views: { increment: 1 } },
});
}

Comment on lines +72 to +88
export async function getFeedbackStats(): Promise<FeedbackStats> {
const all = await prisma.feedback.findMany();
const resolved = await prisma.feedback.findMany({ where: { isResolved: true } });
const unresolved = await prisma.feedback.findMany({ where: { isResolved: false } });

let totalRating = 0;
for (let i = 0; i < all.length; i++) {
totalRating = totalRating + all[i].rating;
}

return {
totalCount: all.length,
averageRating: totalRating / all.length,
resolvedCount: resolved.length,
unresolvedCount: unresolved.length,
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inefficient stats calculation and division-by-zero risk.

Three separate findMany calls fetch all records when Prisma aggregations would be far more efficient. Additionally, totalRating / all.length throws a division-by-zero error when there is no feedback.

Proposed fix using Prisma aggregations
 export async function getFeedbackStats(): Promise<FeedbackStats> {
-  const all = await prisma.feedback.findMany();
-  const resolved = await prisma.feedback.findMany({ where: { isResolved: true } });
-  const unresolved = await prisma.feedback.findMany({ where: { isResolved: false } });
-
-  let totalRating = 0;
-  for (let i = 0; i < all.length; i++) {
-    totalRating = totalRating + all[i].rating;
-  }
-
-  return {
-    totalCount: all.length,
-    averageRating: totalRating / all.length,
-    resolvedCount: resolved.length,
-    unresolvedCount: unresolved.length,
-  };
+  const [aggregates, resolvedCount, unresolvedCount] = await Promise.all([
+    prisma.feedback.aggregate({
+      _count: true,
+      _avg: { rating: true },
+    }),
+    prisma.feedback.count({ where: { isResolved: true } }),
+    prisma.feedback.count({ where: { isResolved: false } }),
+  ]);
+
+  return {
+    totalCount: aggregates._count,
+    averageRating: aggregates._avg.rating ?? 0,
+    resolvedCount,
+    unresolvedCount,
+  };
 }

Comment on lines +55 to +64
const postHandler: RequestHandler = async (req: Request, res: Response) => {
const { action, feedbackId } = req.body;

if (action === "delete") {
const { prisma } = await import("@hmcts/postgres-prisma");
await prisma.feedback.delete({ where: { id: feedbackId } });
}

res.redirect("/admin/feedback");
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Bypasses service layer and lacks input validation.

The POST handler imports Prisma directly rather than using removeFeedback from the service layer (as done in libs/feedback/src/routes/feedback.ts lines 48-51). Additionally, feedbackId is used without validation, which could lead to runtime errors or unintended behaviour if the value is missing or malformed.

Proposed fix
+import { listAllFeedback, getStats, removeFeedback } from "../../../feedback/service.js";
-import { listAllFeedback, getStats } from "../../../feedback/service.js";
 const postHandler: RequestHandler = async (req: Request, res: Response) => {
   const { action, feedbackId } = req.body;

-  if (action === "delete") {
-    const { prisma } = await import("@hmcts/postgres-prisma");
-    await prisma.feedback.delete({ where: { id: feedbackId } });
+  if (action === "delete" && feedbackId) {
+    await removeFeedback(feedbackId);
   }

   res.redirect("/admin/feedback");
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const postHandler: RequestHandler = async (req: Request, res: Response) => {
const { action, feedbackId } = req.body;
if (action === "delete") {
const { prisma } = await import("@hmcts/postgres-prisma");
await prisma.feedback.delete({ where: { id: feedbackId } });
}
res.redirect("/admin/feedback");
};
const postHandler: RequestHandler = async (req: Request, res: Response) => {
const { action, feedbackId } = req.body;
if (action === "delete" && feedbackId) {
await removeFeedback(feedbackId);
}
res.redirect("/admin/feedback");
};

Comment on lines +1 to +40
export const cy = {
pageTitle: "Give feedback",
heading: "Give feedback about this service",
introText: "Help us improve this service by sharing your experience.",

ratingLabel: "How would you rate this service?",
ratingHint: "1 is very poor, 5 is excellent",
rating1: "1 - Very poor",
rating2: "2 - Poor",
rating3: "3 - Average",
rating4: "4 - Good",
rating5: "5 - Excellent",

categoryLabel: "What is your feedback about?",
categoryGeneral: "General feedback",
categoryBug: "Report a problem",
categoryFeature: "Suggest an improvement",
categoryAccessibility: "Accessibility issue",
categoryOther: "Something else",

commentsLabel: "Your feedback",
commentsHint: "Please provide as much detail as possible",

emailLabel: "Email address (optional)",
emailHint: "We'll only use this to respond to your feedback",

submitButton: "Send feedback",
cancelLink: "Cancel",

errorRatingRequired: "Select a rating",
errorCategoryRequired: "Select what your feedback is about",
errorCommentsRequired: "Enter your feedback",
errorEmailInvalid: "Enter a valid email address",
errorSummaryTitle: "There is a problem",

successTitle: "Feedback sent",
successHeading: "Thank you for your feedback",
successMessage: "We have received your feedback and will use it to improve the service.",
successLink: "Return to homepage",
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Welsh translations missing.

This file is named cy.ts (Welsh) but contains English text identical to en.ts. Welsh-speaking users will see English content when selecting the Welsh language option.

All strings require Welsh translation. For example:

  • pageTitle: "Give feedback"pageTitle: "Rhowch adborth"
  • heading: "Give feedback about this service"heading: "Rhowch adborth am y gwasanaeth hwn"

As per coding guidelines, every page must support both English and Welsh by providing en and cy content objects.


res.redirect("/feedback/success?submitted=true");
} catch (error) {
console.log("Feedback submission failed:", error, req.body);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid logging PII.

req.body may contain the user's email address. Logging PII creates compliance/privacy risks (GDPR, CCPA). Log only non-sensitive fields or sanitise before logging.

Proposed fix
-    console.log("Feedback submission failed:", error, req.body);
+    console.error("Feedback submission failed:", error);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log("Feedback submission failed:", error, req.body);
console.error("Feedback submission failed:", error);

Comment on lines +33 to +35
} catch (error) {
res.status(500).json({ error: error.message, stack: error.stack });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not expose stack traces in API responses.

Returning error.stack leaks internal implementation details and could aid attackers. Return only a generic error message.

Proposed fix
   } catch (error) {
-    res.status(500).json({ error: error.message, stack: error.stack });
+    res.status(500).json({ error: "Internal server error" });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
res.status(500).json({ error: error.message, stack: error.stack });
}
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}

Comment on lines +60 to +70
export const PATCH = async (req: Request, res: Response) => {
const { id } = req.params;
const { adminNotes } = req.body;

try {
await markAsResolved(id, req.user.id, adminNotes);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: "Failed to update feedback" });
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Potential null pointer on req.user.id.

req.user may be undefined if authentication middleware is not applied or the user is unauthenticated. This will throw a runtime error.

Proposed fix
 export const PATCH = async (req: Request, res: Response) => {
   const { id } = req.params;
   const { adminNotes } = req.body;

   try {
+    if (!req.user?.id) {
+      res.status(401).json({ error: "Unauthorized" });
+      return;
+    }
     await markAsResolved(id, req.user.id, adminNotes);
     res.json({ success: true });
   } catch (error) {
     res.status(500).json({ error: "Failed to update feedback" });
   }
 };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant