Skip to content

Feature/navbar add tickets - #5

Merged
poner2547 merged 3 commits into
developfrom
feature/navbar-addTickets
Apr 10, 2026
Merged

poner2547 merged 3 commits into
developfrom
feature/navbar-addTickets

Conversation

@poner2547

@poner2547 poner2547 commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator

addTicket Cloudinary Api-add

Summary by CodeRabbit

  • New Features

    • Added ticket detail page displaying ticket information, status timeline, and associated images
    • Added dashboard carousel showing completed tickets with left/right navigation controls
    • Added star rating display for tickets
    • Added image upload capability for ticket submissions and admin updates
  • Style

    • Updated application color scheme to green-based palette

@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request adds a comprehensive ticket detail and management system. It introduces frontend components for displaying finished problems with ratings and reviews, a dedicated ticket detail page with timeline visualization, backend controllers for CRUD operations on tickets and management entities, and integration with Cloudinary for image uploads. Supporting utilities for date formatting and timeline construction are also included.

Changes

Cohort / File(s) Summary
Client Configuration
client/index.html, client/package.json
Updated favicon href to empty string and added react-icons dependency for icon rendering.
Client Styling
client/src/App.css, client/src/index.css
Normalized line endings and replaced global theme variables with a new green-based CSS custom property palette; removed legacy typography and layout rules.
Client Routing & App Setup
client/src/App.jsx
Added new route for /Ticketproblem path rendering DetailTicket component; adjusted import semicolon.
Client Components
client/src/components/CardFinishProblem.*, client/src/components/StarRating.jsx
New components for displaying finished ticket cards with star ratings and image galleries; star rating accepts fractional values with full/half/empty star rendering.
Client Pages
client/src/pages/Dashboard.*, client/src/pages/DetailTicket.*
Dashboard now renders horizontally scrollable carousel of finished problems with navigation controls; DetailTicket displays full ticket information, image galleries (before/after), comments, and vertical status timeline.
Client Hooks & Utilities
client/src/hooks/useTickets.js, client/src/utils/formatDate.js, client/src/utils/timeline.js
New hook for fetching and managing tickets; utility functions for Thai-locale date formatting and constructing ticket timeline data from status changes.
Server Configuration
server/config/cloudinaryControllers.js, server/config/prismaControllers.js
Cloudinary client initialization with Multer memory-based file upload middleware; Prisma client configured with PostgreSQL adapter via environment variables.
Server Controllers & Routes
server/controllers/managementControllers.js, server/controllers/ticketControllers.js, server/routes/managementRoutes.js, server/routes/ticketRoutes.js
Six management endpoints for creating categories, locations, floors, rooms, and equipment; three ticket endpoints for creating tickets (with image upload), retrieving all tickets with nested relations, and updating ticket status/admin notes with optional image upload (marking as "after" images).
Server Integration
server/src/index.js, server/package.json, server/utils/formatDate.js
Added imports and wiring for new ticket and management routes under /api/tickets and /api/manage; added Cloudinary and Multer dependencies; utility for Thai-locale date/datetime formatting on the server side.

Sequence Diagram

sequenceDiagram
    actor User
    participant Client as Client (React)
    participant API as Backend API
    participant DB as Database
    participant Cloud as Cloudinary

    rect rgba(100, 200, 100, 0.5)
        note over User,Cloud: Ticket Listing & Navigation
        User->>Client: Visit Dashboard
        Client->>API: GET /api/tickets/get
        API->>DB: SELECT * FROM tickets (with relations)
        DB-->>API: tickets data
        API-->>Client: tickets array
        Client->>Client: Render CardFinishProblem carousel
        User->>Client: Click card or navigate to /Ticketproblem?ticketId=X
    end

    rect rgba(100, 150, 200, 0.5)
        note over User,Cloud: Ticket Detail Display
        Client->>Client: Extract ticketId from URL params
        Client->>Client: Locate matching ticket from cached data
        Client->>Client: Compute timeline (resolved/pending/in-progress)
        Client->>Client: Render DetailTicket with images & timeline
        User->>Client: View before/after images & status history
    end

    rect rgba(200, 150, 100, 0.5)
        note over User,Cloud: Image Upload (via Admin)
        User->>Client: Submit ticket with before/after image
        Client->>API: POST /api/tickets/add (multipart, image buffer)
        API->>Cloud: Upload image buffer
        Cloud-->>API: Image URL
        API->>DB: INSERT ticket + ticketImage record
        DB-->>API: Created ticket
        API-->>Client: Ticket confirmation
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Feature/login google o auth2.0 #4: Overlaps on client-side UI files (client/index.html, client/src/components/Navbar.jsx) indicating related frontend component modifications.
  • Develop #1: Extends server/src/index.js entrypoint and modifies overlapping client App files, suggesting coordinated changes to the application's core routing and entry points.

Poem

🐰 Tickets hop through timelines so fine,
Cards finish with stars all in line,
Images before and after shown,
With Cloudinary's magic uptown,
The detail page makes all problems shine!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Title check ❓ Inconclusive The title 'Feature/navbar add tickets' is vague and only partially reflects the changeset. While the PR does add ticket-related features, it encompasses far more than navbar changes—including new dashboard components, detail pages, utility functions, and server-side API routes with Cloudinary integration. Revise the title to be more descriptive and specific, such as 'Add ticket detail page with dashboard carousel, card components, and Cloudinary image upload' to better reflect the scope of changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/navbar-addTickets

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Note

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

🟠 Major comments (16)
server/utils/formatDate.js-1-23 (1)

1-23: ⚠️ Potential issue | 🟠 Major

Handle invalid date strings to prevent "Invalid Date" display.

Both formatDate and formatDateTime return "-" for falsy inputs but do not validate whether the Date object is valid after construction. If dateString is truthy but not a valid date (e.g., "invalid-string", "2023-13-45"), new Date(dateString) creates an Invalid Date object, and the subsequent .toLocaleDateString() or .toLocaleString() calls will return "Invalid Date" instead of "-".

This breaks the intended fallback behavior and displays a confusing error message to users.

🛡️ Proposed fix to validate Date objects
 export const formatDate = (dateString) => {
   if (!dateString) return "-";
   
   const date = new Date(dateString);
+  if (isNaN(date.getTime())) return "-";
   return date.toLocaleDateString('th-TH', {
     year: 'numeric',
     month: 'short',
     day: 'numeric',
   });
 };

 export const formatDateTime = (dateString) => {
   if (!dateString) return "-";
   
   const date = new Date(dateString);
+  if (isNaN(date.getTime())) return "-";
   return date.toLocaleString('th-TH', {
     year: 'numeric',
     month: 'short',
     day: 'numeric',
     hour: '2-digit',
     minute: '2-digit',
   });
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/formatDate.js` around lines 1 - 23, Both formatDate and
formatDateTime create Date objects but don't check validity, so truthy invalid
strings yield "Invalid Date"; update both functions (formatDate and
formatDateTime) to construct the Date and then validate it (e.g., check
isNaN(date.getTime()) or isNaN(date.valueOf())) and return "-" when the date is
invalid before calling toLocaleDateString/toLocaleString; keep the existing
locale/options when valid.
client/src/utils/timeline.js-26-30 (1)

26-30: ⚠️ Potential issue | 🟠 Major

Resolve the finished timestamp from the schema field, not finishedAt.

The Prisma model uses timestampFinished, not finishedAt, and the current fallback only fixes datetime still becomes "-" when finishedAt is missing. Resolved tickets will render the last step incorrectly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/utils/timeline.js` around lines 26 - 30, The code uses
ticket?.finishedAt for the resolved step but the Prisma field is
timestampFinished; update the timeline.push block (the branch checking
ticket?.ticketStatus === "resolved") to use ticket?.timestampFinished as the
primary resolved timestamp (falling back to ticket?.updatedAt) for both the date
(formatDate) and time calculations instead of finishedAt, and ensure the time
string is derived from new Date(ticket.timestampFinished) when present so time
isn't "-" when finishedAt is absent.
client/src/utils/timeline.js-15-19 (1)

15-19: ⚠️ Potential issue | 🟠 Major

Use the real in-progress timestamp here.

The schema already has timestampInprogress; using updatedAt makes this step drift whenever the ticket is edited again or resolved. This should read the dedicated field, and the ticket query needs to return it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/utils/timeline.js` around lines 15 - 19, The timeline entry that
sets status "กำลังดำเนินการ" is using ticket?.updatedAt for date/time (in
timeline.push in client/src/utils/timeline.js); change those to use the
dedicated field ticket?.timestampInprogress (e.g.,
formatDate(ticket?.timestampInprogress) and new
Date(ticket.timestampInprogress).toLocaleTimeString(...)) and update the ticket
query that populates tickets to include timestampInprogress so the field is
returned to the client.
server/config/cloudinaryControllers.js-12-14 (1)

12-14: ⚠️ Potential issue | 🟠 Major

Add file size limits and type validation to prevent abuse.

The multer configuration accepts any file without size or type restrictions. This could lead to denial-of-service via large uploads or security issues from non-image files.

🛡️ Proposed fix
 const storage = multer.memoryStorage();
-const upload = multer({ storage });
+const upload = multer({ 
+  storage,
+  limits: {
+    fileSize: 5 * 1024 * 1024, // 5MB limit
+  },
+  fileFilter: (req, file, cb) => {
+    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
+    if (allowedTypes.includes(file.mimetype)) {
+      cb(null, true);
+    } else {
+      cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed.'));
+    }
+  }
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/config/cloudinaryControllers.js` around lines 12 - 14, The multer
setup uses memoryStorage() and creates upload via multer({ storage }) with no
size or type checks; update the multer configuration to include limits (e.g.,
limits: { fileSize: <reasonable bytes> }) and a fileFilter that validates
allowed MIME types (e.g., image/jpeg, image/png, image/webp, image/gif) and
rejects others; locate the storage constant and the upload assignment (storage,
upload, multer.memoryStorage, multer({ storage })) and modify multer({ ... }) to
add the limits and fileFilter functions so only permitted image types under the
size cap are accepted.
server/src/index.js-24-24 (1)

24-24: ⚠️ Potential issue | 🟠 Major

Redundant router mounting creates duplicate paths.

ticketRoutes is already mounted at /api/tickets (line 13). Mounting it again at /api/updateTicket creates confusing duplicate endpoints. For example, the PATCH endpoint would be accessible at both /api/tickets/admin/updateticket/:id and /api/updateTicket/admin/updateticket/:id.

🐛 Proposed fix - remove redundant mounting
-// api update
-app.use('/api/updateTicket',ticketRoutes)

Use the existing /api/tickets/admin/updateticket/:id endpoint instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.js` at line 24, The app currently mounts the same router
twice causing duplicate endpoints: remove the redundant mounting call that uses
app.use('/api/updateTicket', ticketRoutes) so only the original
app.use('/api/tickets', ticketRoutes) remains; locate the duplicate mount
referencing ticketRoutes and delete the app.use('/api/updateTicket',
ticketRoutes) line so routes are only served under /api/tickets (e.g., use
/api/tickets/admin/updateticket/:id).
client/src/hooks/useTickets.js-9-9 (1)

9-9: ⚠️ Potential issue | 🟠 Major

Hardcoded localhost URL will break in production.

The URL http://localhost:3000 won't work when deployed. Use environment variables or relative URLs.

🐛 Proposed fix
-            const response = await axios.get('http://localhost:3000/api/tickets/get');
+            const response = await axios.get('/api/tickets/get');

Or use an environment variable:

const API_URL = import.meta.env.VITE_API_URL || '';
// ...
const response = await axios.get(`${API_URL}/api/tickets/get`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/hooks/useTickets.js` at line 9, The code in useTickets.js
hardcodes the API base URL in the axios call (const response = await
axios.get('http://localhost:3000/api/tickets/get')), which will fail in
production; update the hook to use a configurable base URL (e.g., read from
import.meta.env.VITE_API_URL or process.env) or a relative URL, and then build
the request using that variable (so the axios.get call in the useTickets hook
uses `${API_URL}/api/tickets/get` or `/api/tickets/get`), ensuring a sensible
default fallback and no plaintext localhost string remains.
client/src/components/Navbar.jsx-27-33 (1)

27-33: ⚠️ Potential issue | 🟠 Major

NavLink targets are out of sync with registered routes.

Lines 28–33 point to paths not defined in client/src/App.jsx (/report, /my-problems, /static, /login), so these links lead to broken navigation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/Navbar.jsx` around lines 27 - 33, The NavLink targets
in Navbar.jsx (the <NavLink> instances including the Login button using
onClick={closeMenu}) point to routes that don't exist in App.jsx; update the to=
values to match the registered route paths in App.jsx (or add corresponding
Routes in App.jsx) so navigation isn't broken—inspect Navbar.jsx's NavLink items
("/report", "/my-problems", "/static", "/login") and either change them to the
actual routes defined in App.jsx or add matching Route entries there so NavLink
and App.jsx routes are consistent.
client/src/pages/DetailTicket.jsx-21-22 (1)

21-22: ⚠️ Potential issue | 🟠 Major

Guard nested relation access to avoid runtime crashes.

Lines 21 and 35–39 dereference nested objects without null checks (ticket.category, ticket.location, ticket.floor, ticket.room). If any relation is missing, the page will throw.

🛡️ Suggested null-safe rendering
-<span className="ticket-type">{ticket.category.ticketCtgName}</span>
+<span className="ticket-type">{ticket.category?.ticketCtgName ?? '-'}</span>

-<p>สถานที่ : {ticket.location.locationName}</p>
+<p>สถานที่ : {ticket.location?.locationName ?? '-'}</p>

-<p>ชั้น : {ticket.floor.floorLevel}</p>
-<span>ห้อง : {ticket.room.roomName}</span>
+<p>ชั้น : {ticket.floor?.floorLevel ?? '-'}</p>
+<span>ห้อง : {ticket.room?.roomName ?? '-'}</span>

Also applies to: 35-39

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/DetailTicket.jsx` around lines 21 - 22, The JSX in
DetailTicket.jsx directly dereferences nested relations
(ticket.category.ticketCtgName and ticket.ticketStatus, and location/floor/room
fields) which can crash if any relation is null; update the rendering to use
null-safe checks (e.g., optional chaining like ticket?.category?.ticketCtgName
or short-circuit conditionals) and provide sensible fallbacks (empty string or
"N/A") for ticket.category, ticket.location, ticket.floor, and ticket.room so
the component renders safely when relations are missing.
client/src/pages/DetailTicket.jsx-59-60 (1)

59-60: ⚠️ Potential issue | 🟠 Major

Harden external image tab opening against tabnabbing.

Use noopener,noreferrer when calling window.open with _blank.

🔐 Suggested fix
-onClick={() => window.open(img.imageUrl, '_blank')}
+onClick={() => window.open(img.imageUrl, '_blank', 'noopener,noreferrer')}

Also applies to: 80-81

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/DetailTicket.jsx` around lines 59 - 60, The onClick handlers
that call window.open(img.imageUrl, '_blank') (used in the image thumbnail
rendering and the similar handler later) are vulnerable to tabnabbing; change
these handlers to pass window.open(img.imageUrl, '_blank',
'noopener,noreferrer') and/or use a safer pattern (create an anchor with
target="_blank" rel="noopener noreferrer") so the new tab cannot access
window.opener. Update the onClick callbacks where window.open is used (the
handlers referencing img.imageUrl) to include the 'noopener,noreferrer' feature
string or replace with a rel-protected anchor.
client/src/components/Navbar.jsx-19-23 (1)

19-23: ⚠️ Potential issue | 🟠 Major

Convert the hamburger div to a semantic button element for accessibility.

The menu toggle is implemented as a non-semantic div with an onClick handler, which lacks keyboard navigation and screen reader support. Users cannot tab to focus the control or activate it via keyboard, and assistive technologies cannot identify it as an interactive control.

♿ Suggested accessible toggle
-      <div className={`hamburger ${isOpen ? 'active' : ''}`} onClick={toggleMenu}>
+      <button
+        type="button"
+        className={`hamburger ${isOpen ? 'active' : ''}`}
+        onClick={toggleMenu}
+        aria-expanded={isOpen}
+        aria-label="Toggle navigation menu"
+      >
         <span className="bar"></span>
         <span className="bar"></span>
         <span className="bar"></span>
-      </div>
+      </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/Navbar.jsx` around lines 19 - 23, Replace the
non-semantic <div className={`hamburger ${isOpen ? 'active' : ''}`}
onClick={toggleMenu}> in Navbar.jsx with a proper <button> element (keep
className logic) so it is focusable and keyboard-operable; add type="button",
aria-expanded={isOpen} and a clear aria-label (e.g., "Toggle menu") to expose
state to screen readers, and remove any role/onKey handlers that attempted to
emulate button behavior since a native button handles Enter/Space.
client/src/components/CardFinishProblem.jsx-13-13 (1)

13-13: ⚠️ Potential issue | 🟠 Major

Make the clickable card keyboard-accessible.

div.container-card is an interactive control that cannot be activated via keyboard. Add keyboard support to meet WCAG accessibility standards.

♿ Suggested fix
-    <div className='container-card' onClick={handleClick}>
+    <div
+      className='container-card'
+      onClick={handleClick}
+      role="button"
+      tabIndex={0}
+      onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && handleClick()}
+    >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/CardFinishProblem.jsx` at line 13, The clickable div
(div.container-card) is not keyboard-accessible; update the
CardFinishProblem.jsx component to either replace the div with a semantic
<button> or, if keeping the div, add accessibility attributes and keyboard
handlers: add role="button" and tabIndex={0} to the container-card, implement an
onKeyDown handler (or onKeyPress) that calls the existing handleClick when Enter
or Space is pressed (preventDefault for Space), and ensure an appropriate
aria-label or accessible text is present; reference the container div and the
handleClick function when making these changes.
server/routes/ticketRoutes.js-9-9 (1)

9-9: ⚠️ Potential issue | 🟠 Major

Add authentication and role authorization middleware to the admin update endpoint.

The /admin/updateticket/:id route at line 9 lacks any authentication or authorization checks. No middleware protects this endpoint at the route level, no upstream protection exists in the app setup (index.js), and the controller accepts adminId directly from the request body without validating the caller's actual role. This allows any unauthenticated user to modify ticket state.

Create and apply authentication and admin role-checking middleware to this route:

Suggested route hardening
-router.patch('/admin/updateticket/:id', upload.single('image'), updateTicketByadmin);
+router.patch(
+  '/admin/updateticket/:id',
+  authenticateUser,
+  requireAdmin,
+  upload.single('image'),
+  updateTicketByadmin
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/ticketRoutes.js` at line 9, The admin update route currently
exposes router.patch('/admin/updateticket/:id', upload.single('image'),
updateTicketByadmin) with no auth checks; add middleware to authenticate the
request and verify the caller has admin role (e.g., insert authenticateJwt and
authorizeAdmin middleware before upload.single and updateTicketByadmin) and
update the controller logic to derive admin identity from the authenticated
token (req.user/id) instead of trusting adminId from req.body; ensure middleware
names (authenticateJwt, authorizeAdmin) or your project's equivalents are used
and applied at this route so only authenticated admins can call
updateTicketByadmin.
server/controllers/ticketControllers.js-26-27 (1)

26-27: ⚠️ Potential issue | 🟠 Major

Do not accept audit timestamps from client payload.

Lines 26-27 and 68-69 allow client-controlled createdAt/updatedAt, which weakens data integrity and auditability.

🔧 Proposed fix
         const {
             userId,
             ticketCtgId,
             locationId,
             floorId,
             roomId,
             equipmentId,
             title,
             description,
             ticketStatus,
             parentTicketId,
             adminId,
             adminNote,
             rating,
             comment,
-            createdAt,
-            updatedAt,
         } = req.body;
...
                 data: {
                     userId: Number(userId),
                     ticketCtgId: Number(ticketCtgId),
                     locationId: Number(locationId),
                     floorId: Number(floorId),
                     roomId: Number(roomId),
                     equipmentId: equipmentId ? Number(equipmentId) : null,
                     title,
                     description,
                     ticketStatus,
                     parentTicketId: parentTicketId ? Number(parentTicketId) : null,
                     adminId,
                     adminNote,
                     rating: rating ? Number(rating) : null,
                     comment,
-                    createdAt,
-                    updatedAt,
                 }

Also applies to: 68-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/controllers/ticketControllers.js` around lines 26 - 27, Remove
client-controlled audit fields by dropping createdAt and updatedAt from any
destructuring of req.body and by ensuring neither field is passed into your
persistence calls (e.g., in the controller functions that read req.body and call
your model create/update). Instead rely on the database/ORM to populate
timestamps (or set updatedAt server-side), and explicitly omit these properties
when building the payload sent to methods like Model.create or Model.update so
clients cannot override audit timestamps.
server/controllers/ticketControllers.js-54-67 (1)

54-67: ⚠️ Potential issue | 🟠 Major

Validate numeric inputs before Prisma calls and return 400 on invalid payloads.

Lines 54-67 cast request values directly. Invalid IDs/ratings currently bubble as 500 instead of a client error.

🔧 Proposed fix
+ const toInt = (value, field, { optional = false } = {}) => {
+   if ((value === undefined || value === null || value === '') && optional) return null;
+   const n = Number(value);
+   if (!Number.isInteger(n)) throw new Error(`Invalid ${field}`);
+   return n;
+ };

-                    userId: Number(userId),
-                    ticketCtgId: Number(ticketCtgId),
-                    locationId: Number(locationId),
-                    floorId: Number(floorId),
-                    roomId: Number(roomId),
-                    equipmentId: equipmentId ? Number(equipmentId) : null,
+                    userId: toInt(userId, 'userId'),
+                    ticketCtgId: toInt(ticketCtgId, 'ticketCtgId'),
+                    locationId: toInt(locationId, 'locationId'),
+                    floorId: toInt(floorId, 'floorId'),
+                    roomId: toInt(roomId, 'roomId'),
+                    equipmentId: toInt(equipmentId, 'equipmentId', { optional: true }),
...
-                    parentTicketId: parentTicketId ? Number(parentTicketId) : null,
+                    parentTicketId: toInt(parentTicketId, 'parentTicketId', { optional: true }),
...
-                    rating: rating ? Number(rating) : null,
+                    rating: toInt(rating, 'rating', { optional: true }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/controllers/ticketControllers.js` around lines 54 - 67, Validate all
numeric request fields before calling Prisma in the ticket creation/update flow:
check that userId, ticketCtgId, locationId, floorId, roomId, equipmentId (if
provided), parentTicketId (if provided), and rating (if provided) parse to valid
numbers (use parseInt/Number and Number.isFinite/Number.isInteger or isNaN
checks) and return res.status(400).json(...) on any invalid payload instead of
proceeding to the Prisma call; update the code around the object construction
(the block that assigns userId, ticketCtgId, locationId, floorId, roomId,
equipmentId, parentTicketId, rating, etc. in the ticket controller—e.g.,
createTicket or the function that builds this payload) to perform these
validations and only cast values for Prisma after validation.
server/controllers/ticketControllers.js-148-154 (1)

148-154: ⚠️ Potential issue | 🟠 Major

Add validation for invalid id (400) and handle missing ticket (404) instead of generic 500.

Line 177 blindly uses Number(id) without validation, and the catch block returns 500 for all errors. Invalid IDs should return 400 and missing tickets should return 404 for predictable API behavior.

🔧 Proposed fix
+import { Prisma } from '@prisma/client';
...
 export const updateTicketByadmin = async (req,res) =>{
     try {
         const { id } = req.params;
+        const ticketId = Number(id);
+        if (!Number.isInteger(ticketId)) {
+            return res.status(400).json({ error: 'Invalid ticket id' });
+        }
...
             const ticket = await tx.ticket.update({
-                where: { ticketId: Number(id) },
+                where: { ticketId },
                 data: {
                     ticketStatus,
                     adminNote,
                     adminId: adminId ? Number(adminId) : undefined,
                     updatedAt: new Date(),
                     timestampInprogress ,
                 }
             });
...
     } catch (error) {
+        if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
+            return res.status(404).json({ error: 'Ticket not found' });
+        }
         console.error('Admin Update Error:', error);
         res.status(500).json({ error: 'Failed to update by admin' });
     }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/controllers/ticketControllers.js` around lines 148 - 154, The
controller currently converts req.params.id with Number(id) and treats every
failure as a 500; update the handler to validate req.params.id first (e.g.,
check it's present and a finite integer using Number.isInteger(Number(id)) or
/^\d+$/.test(id)) and return res.status(400).json({ message: 'Invalid ticket id'
}) for invalid IDs; after converting to a numeric id, attempt to fetch/update
the ticket (the code path using Number(id)) and if no ticket is found return
res.status(404).json({ message: 'Ticket not found' }); keep the catch block but
ensure it only returns res.status(500) for unexpected errors and does not mask
validation/missing-ticket responses.
server/controllers/ticketControllers.js-33-49 (1)

33-49: ⚠️ Potential issue | 🟠 Major

Add Cloudinary cleanup on DB transaction failure to prevent orphaned uploads.

Both addTicket and updateTicketByAdmin upload images to Cloudinary before DB writes. If the transaction fails, the uploaded image remains orphaned with no linked record. The existing try-catch blocks (lines 85-88 and 201-204) don't clean up these orphaned files.

Capture the public_id from the upload response and destroy it if the transaction fails:

🔧 Suggested pattern for both locations
  let uploadedImageUrl = null;
+ let uploadedPublicId = null;

  if (req.file) {
    const result = await new Promise((resolve, rejects) => {
      const stream = cloudinary.uploader.upload_stream(
        { folder: "TTS-img", resource_type: "auto" },
        (error, result) => {
          if (error) rejects(error);
          else resolve(result);
        }
      );
      stream.end(req.file.buffer);
    });
    uploadedImageUrl = result.secure_url;
+   uploadedPublicId = result.public_id;
  }

- const result = await prisma.$transaction(async (tx) => {
+ try {
+   const result = await prisma.$transaction(async (tx) => {
      // db writes
-   });
+   });
+ } catch (err) {
+   if (uploadedPublicId) await cloudinary.uploader.destroy(uploadedPublicId);
+   throw err;
+ }

Also note: Line 166 has rejects(error) but the Promise callback parameter is reject.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/controllers/ticketControllers.js` around lines 33 - 49, The image
upload Promise in addTicket and updateTicketByAdmin currently uploads to
Cloudinary before the DB transaction and doesn't remove the file if the DB write
fails, and the Promise uses the wrong reject parameter name; fix by capturing
the upload response's public_id (e.g., store it in uploadedImagePublicId
alongside uploadedImageUrl/uploadedImageType) and, inside the existing
catch/transaction-failure paths for addTicket and updateTicketByAdmin, call
cloudinary.uploader.destroy(uploadedImagePublicId) to delete the orphaned image;
also correct the Promise executor parameter from "rejects" to "reject" in the
upload code (the function that creates the stream and calls
stream.end(req.file.buffer)).
🟡 Minor comments (8)
client/src/pages/Dashboard.css-26-31 (1)

26-31: ⚠️ Potential issue | 🟡 Minor

Fix the CSS variable typo on the scroll buttons.

var(---text-color-main) is invalid, so the text/icon color falls back unexpectedly. This should be var(--text-color-main).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/Dashboard.css` around lines 26 - 31, The .scroll-btn rule
uses an invalid CSS variable name var(---text-color-main); update the property
in the .scroll-btn selector to use the correct CSS custom property
var(--text-color-main) so the button text/icons inherit the intended color from
--text-color-main.
client/index.html-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor

Restore a valid favicon path.

Line 5 sets href="", which is invalid HTML and leaves the app without a usable favicon. Point this back to an asset path or remove the tag until the icon exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/index.html` at line 5, The link tag for the favicon currently has an
empty href, leaving the app without a valid favicon; update the rel="icon" link
by setting href to a valid asset path (e.g., the actual favicon file under your
static assets) or remove the link tag entirely until the icon exists; locate the
link element with rel="icon" and fix the href attribute accordingly so the
browser can load the favicon.
client/src/index.css-4-5 (1)

4-5: ⚠️ Potential issue | 🟡 Minor

Unquote the single-word font families.

Stylelint is already flagging Line 4-5, so Roboto, Oxygen, Ubuntu, and Cantarell need to be unquoted for the lint step to pass.

Suggested fix
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
-    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
+    Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/index.css` around lines 4 - 5, The font-family declaration in
client/src/index.css contains unnecessary quotes around single-word font
families causing stylelint failures; edit the font-family list in the
font-family declaration (the rule containing "-apple-system, BlinkMacSystemFont,
'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid
Sans', 'Helvetica Neue',") by removing the single quotes around Roboto, Oxygen,
Ubuntu, and Cantarell (leave multi-word names like 'Segoe UI', 'Fira Sans',
'Droid Sans', 'Helvetica Neue' quoted) so the lint step passes.
client/src/pages/Dashboard.jsx-14-16 (1)

14-16: ⚠️ Potential issue | 🟡 Minor

Add null check before accessing scrollRef.current.

If scrollRef.current is null (e.g., before the DOM mounts or during fast interactions), calling scrollBy will throw a TypeError.

🛡️ Proposed fix
-        <button className="scroll-btn left" onClick={() => 
-            scrollRef.current.scrollBy({ left: -370, behavior: 'smooth' })}>
+        <button className="scroll-btn left" onClick={() => 
+            scrollRef.current?.scrollBy({ left: -370, behavior: 'smooth' })}>

Apply the same fix for the right scroll button.

Also applies to: 25-27

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/Dashboard.jsx` around lines 14 - 16, The left and right
scroll button handlers call scrollRef.current.scrollBy(...) without guarding
against scrollRef.current being null; update the onClick handlers for the left
button (using FaChevronLeft) and the right button to first check that scrollRef
and scrollRef.current exist (e.g., if (scrollRef?.current) or if (scrollRef &&
scrollRef.current)) before calling scrollRef.current.scrollBy({ left: ...,
behavior: 'smooth' }) to avoid TypeError when the DOM ref is not yet mounted.
client/src/components/StarRating.jsx-4-22 (1)

4-22: ⚠️ Potential issue | 🟡 Minor

Handle undefined or invalid rating prop.

If rating is undefined, null, or not a number, the comparisons on lines 10 and 14 may behave unexpectedly (e.g., all empty stars or incorrect display). Consider adding a default value.

🛡️ Proposed fix
-export const StarRating = ({rating}) => {
+export const StarRating = ({rating = 0}) => {
+    const safeRating = typeof rating === 'number' ? rating : 0;
     const stars = Array.from({ length: 5 }, (_, index) => {
     
     const starValue = index + 1;

-    if (rating >= starValue) {
+    if (safeRating >= starValue) {
       return <FaStar key={index} color="#ffc107" size={15} />;
       
-    } else if (rating >= starValue - 0.5) {
+    } else if (safeRating >= starValue - 0.5) {
       return <FaStarHalfAlt key={index} color="#ffc107" size={15} />;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/StarRating.jsx` around lines 4 - 22, The StarRating
component currently compares rating with numbers without guarding against
undefined/non-number values; ensure rating is validated and normalized at the
top of the component (e.g., in StarRating) by defaulting to 0 when rating is
undefined/null, coercing to a Number, and clamping to the valid range 0–5 before
building the stars array (use the normalizedRating variable when comparing
against starValue inside the stars mapping).
client/src/pages/DetailTicket.jsx-102-119 (1)

102-119: ⚠️ Potential issue | 🟡 Minor

Don’t render timeline when ticket is not found.

timelineData is always mapped, so invalid ticketId still shows a pseudo timeline. Gate this block with ticket (or show a clear “not found” state).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/DetailTicket.jsx` around lines 102 - 119, The timeline is
rendered unconditionally by mapping timelineData in the DetailTicket component,
which shows a fake timeline when an invalid ticketId is used; wrap the timeline
JSX (the timeline-container mapping over timelineData) with a guard that checks
for a valid ticket (e.g., if (ticket) or ticket != null) or render an explicit
“Ticket not found” / empty state instead; update the render logic around the
timelineData map so it only runs when ticket exists and remove/replace the
timeline when ticket is falsy.
client/src/components/Navbar.css-40-70 (1)

40-70: ⚠️ Potential issue | 🟡 Minor

Add visible keyboard focus styles for navbar controls.

Links/button/hamburger have hover styling but no explicit focus-visible treatment, making keyboard focus hard to track.

⌨️ Suggested CSS additions
+.nav-links li a:focus-visible,
+.btnLogin:focus-visible,
+.hamburger:focus-visible {
+  outline: 2px solid var(--text-color-light);
+  outline-offset: 2px;
+}

Also applies to: 73-87

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/Navbar.css` around lines 40 - 70, Add explicit keyboard
focus-visible styles for interactive navbar controls: create :focus-visible
rules for .nav-links li a and .btnLogin (and the hamburger/menu button class
used in the navbar) that mirror the hover visuals but include a clear,
high-contrast focus indicator (e.g., outline or box-shadow using a CSS variable
like --focus-color), keep existing border-radius/padding to avoid layout shift,
and ensure transition persists; also add a fallback :focus if you need broader
browser support. This will make keyboard focus as visible as the hover state for
.nav-links li a, .btnLogin, and the hamburger/menu button.
client/src/components/Navbar.jsx-27-32 (1)

27-32: ⚠️ Potential issue | 🟡 Minor

Close mobile menu on all nav-item clicks, not just Login.

Currently only the Login link calls closeMenu. On mobile, selecting other links keeps the drawer open.

📱 Suggested fix
-          <li><NavLink to="/">Home</NavLink></li>
-          <li><NavLink to="/report">Report</NavLink></li>
-          <li><NavLink to="/my-problems">My Problems</NavLink></li>
-          <li><NavLink to="/static">Static</NavLink></li>
+          <li><NavLink to="/" onClick={closeMenu}>Home</NavLink></li>
+          <li><NavLink to="/report" onClick={closeMenu}>Report</NavLink></li>
+          <li><NavLink to="/my-problems" onClick={closeMenu}>My Problems</NavLink></li>
+          <li><NavLink to="/static" onClick={closeMenu}>Static</NavLink></li>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/Navbar.jsx` around lines 27 - 32, Add the closeMenu
call to every navigation item so the mobile drawer closes when any NavLink is
clicked: update each NavLink (the ones rendering "Home", "Report", "My
Problems", "Static" in Navbar.jsx) to include onClick={closeMenu} (or
alternatively attach a single onClick handler to the parent <ul> that calls
closeMenu when a nav item is activated); ensure the existing Login NavLink
remains unchanged and reference the closeMenu function and NavLink components
when making the changes.
🧹 Nitpick comments (12)
server/utils/formatDate.js (1)

1-23: Consider extracting shared logic to reduce duplication.

Both functions follow nearly identical patterns: guard clause, Date construction, validation (once added), and locale formatting. You could refactor to a shared helper that accepts formatting options.

♻️ Proposed refactor to DRY
+const formatDateWithLocale = (dateString, options) => {
+  if (!dateString) return "-";
+  
+  const date = new Date(dateString);
+  if (isNaN(date.getTime())) return "-";
+  
+  return options.hour
+    ? date.toLocaleString('th-TH', options)
+    : date.toLocaleDateString('th-TH', options);
+};
+
 export const formatDate = (dateString) => {
-  if (!dateString) return "-";
-  
-  const date = new Date(dateString);
-  return date.toLocaleDateString('th-TH', {
+  return formatDateWithLocale(dateString, {
     year: 'numeric',
     month: 'short',
     day: 'numeric',
   });
 };

 export const formatDateTime = (dateString) => {
-  if (!dateString) return "-";
-  
-  const date = new Date(dateString);
-  return date.toLocaleString('th-TH', {
+  return formatDateWithLocale(dateString, {
     year: 'numeric',
     month: 'short',
     day: 'numeric',
     hour: '2-digit',
     minute: '2-digit',
   });
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/formatDate.js` around lines 1 - 23, Both formatDate and
formatDateTime duplicate the same guard, Date construction and locale
formatting; refactor by extracting a shared helper (e.g., formatWithOptions or
parseAndFormat) that takes dateString, locale/options and fallback value,
performs the null guard and Date validation (isNaN(date.getTime())), and returns
the formatted string; then have formatDate and formatDateTime call that helper
with their respective toLocaleDateString/toLocaleString option objects and the
same 'th-TH' locale to remove duplication and centralize validation.
client/src/App.jsx (2)

19-19: Consider using lowercase kebab-case for route paths.

/Ticketproblem uses unconventional casing. Standard practice is lowercase with hyphens (e.g., /ticket-problem or /tickets/:id). This also aligns better with REST conventions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/App.jsx` at line 19, The route path in the Route element using
DetailTicket currently uses PascalCase ("/Ticketproblem"); change it to a
lowercase kebab-case or REST-style path (e.g., "/ticket-problem" or
"/tickets/:id") by updating the Route path string where <Route
path="/Ticketproblem" element={<DetailTicket />} /> is declared and adjust any
corresponding Link/Navigate/usRoute usages and tests to match the new path.

1-6: Remove unused imports.

useState, useEffect, axios, and CardFinishProblem are imported but not used in this file.

🧹 Proposed fix
-import { useState, useEffect } from 'react';
-import axios from 'axios';
 import './App.css'
 import { BrowserRouter as Router, Routes, Route} from 'react-router-dom' 
 import { Navbar } from './components/navbar';
-import { CardFinishProblem } from './components/CardFinishProblem';
 import Dashboard from './pages/Dashboard';
 import DetailTicket from './pages/DetailTicket';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/App.jsx` around lines 1 - 6, App.jsx currently imports unused
symbols (useState, useEffect, axios, CardFinishProblem); remove those unused
imports from the import list so only actually used exports remain (e.g., keep
React Router and Navbar imports if used). Specifically, edit the top-of-file
import statements to delete references to useState, useEffect, axios, and
CardFinishProblem to eliminate dead imports and related lint warnings.
client/src/hooks/useTickets.js (2)

10-10: Remove debug console.log before merging.

The console.log with Thai text appears to be debug code and should be removed for production.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/hooks/useTickets.js` at line 10, Remove the debug console.log in
client/src/hooks/useTickets.js (the line logging "ข้อมูลจาก Backend:" inside the
hook that handles the response) before merging; either delete that console.log
or replace it with a proper production logging call (e.g., use your app's logger
or conditional debug flag) inside the function that processes response.data (the
response handler in useTickets hook).

4-21: Consider exposing loading and error states.

The hook currently only returns tickets and refetch. Consumers like Dashboard have no way to show loading indicators or error messages, which degrades UX.

♻️ Proposed enhancement
 export const useTickets = () => {
     const [tickets, setTickets] = useState([]);
+    const [loading, setLoading] = useState(true);
+    const [error, setError] = useState(null);

     const fetchTickets = useCallback(async () => {
+        setLoading(true);
+        setError(null);
         try {
-            const response = await axios.get('http://localhost:3000/api/tickets/get');
-            console.log("ข้อมูลจาก Backend:", response.data);
+            const response = await axios.get('/api/tickets/get');
             setTickets(response.data);
         } catch (error) {
             console.error('Error fetching tickets:', error);
+            setError(error);
+        } finally {
+            setLoading(false);
         }
     }, []);
     // ...
-    return { tickets, refetch: fetchTickets };
+    return { tickets, loading, error, refetch: fetchTickets };
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/hooks/useTickets.js` around lines 4 - 21, The hook useTickets only
returns tickets and refetch, so add loading and error state to improve UX:
introduce useState hooks for loading (e.g., loading) and error (e.g.,
fetchError), set loading true at start of fetchTickets and false in both success
and finally, set fetchError inside the catch (and clear it on successful fetch
or before retry), and include these new values in the returned object ({
tickets, loading, error: fetchError, refetch: fetchTickets }) so consumers like
Dashboard can show spinners and error messages; update references to setTickets,
fetchTickets and useEffect accordingly.
client/src/pages/Dashboard.jsx (2)

19-23: Use a stable key instead of array index.

Using index as the key can cause rendering bugs if tickets are reordered, filtered, or removed. Use ticket.ticketId which is unique per ticket.

♻️ Proposed fix
         {tickets
-        // ?.filter((ticket) =>  ticket.ticketStatus === "resolved")
         .map((ticket, index) => (
-          <CardFinishProblem key={index} data={ticket} />
+          <CardFinishProblem key={ticket.ticketId} data={ticket} />
         ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/Dashboard.jsx` around lines 19 - 23, The map in
Dashboard.jsx uses index as the React key which can break rendering; update the
mapping that renders <CardFinishProblem> to use a stable unique identifier (e.g.
key={ticket.ticketId}) instead of key={index}; locate the .map over tickets and
replace the index key with ticket.ticketId (or a reliable fallback like
ticket.id) and ensure ticket.ticketId is present on the ticket objects before
using it.

1-2: Remove unused imports.

useState, useEffect, and axios are imported but not used in this component. The useTickets hook handles data fetching internally.

🧹 Proposed fix
-import { useState, useEffect, useRef } from 'react'
-import axios from 'axios';
+import { useRef } from 'react'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/Dashboard.jsx` around lines 1 - 2, Remove the unused imports
from the top of the component: delete useState, useEffect and the axios import
from the import statement (keep useRef only if the component actually uses it);
update the import line accordingly and ensure there are no remaining references
to useState/useEffect/axios elsewhere in the file (data fetching should remain
handled by useTickets).
server/config/cloudinaryControllers.js (1)

6-10: Validate that Cloudinary environment variables are defined.

If any of the Cloudinary environment variables are missing, uploads will silently fail or throw cryptic errors at runtime.

🛡️ Proposed fix
+const requiredEnvVars = ['CLOUDINARY_CLOUD_NAME', 'CLOUDINARY_API_KEY', 'CLOUDINARY_API_SECRET'];
+for (const envVar of requiredEnvVars) {
+  if (!process.env[envVar]) {
+    throw new Error(`Missing required environment variable: ${envVar}`);
+  }
+}
+
 cloudinary.config({ 
   cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 
   api_key: process.env.CLOUDINARY_API_KEY, 
   api_secret: process.env.CLOUDINARY_API_SECRET 
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/config/cloudinaryControllers.js` around lines 6 - 10, The Cloudinary
config block does not validate presence of required env vars
(CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET); add a
startup check before calling cloudinary.config that verifies each of these
process.env variables is defined and if any are missing, log a clear error and
throw or exit (so the app fails fast) rather than allowing cloudinary.config to
proceed silently; update the module-level initialization where
cloudinary.config(...) is called to perform this validation (or extract it into
a small helper like validateCloudinaryEnv) and include the missing-variable
names in the error message.
server/config/prismaControllers.js (1)

5-13: Consider adding pool configuration and graceful shutdown handling.

The pool is created without explicit configuration options (e.g., max, idleTimeoutMillis, connectionTimeoutMillis), which may lead to resource issues under load. Additionally, the pool is never closed on process termination, which can leave connections dangling.

♻️ Suggested improvements
 const pool = new Pool({ 
-  connectionString: process.env.DATABASE_URL 
+  connectionString: process.env.DATABASE_URL,
+  max: 10,
+  idleTimeoutMillis: 30000,
+  connectionTimeoutMillis: 5000,
 });
+
+// Graceful shutdown
+process.on('SIGTERM', async () => {
+  await prisma.$disconnect();
+  await pool.end();
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/config/prismaControllers.js` around lines 5 - 13, The Pool is created
without explicit connection limits and is never closed; update the Pool
construction (the pool variable created with new Pool({...})) to include
recommended options like max, idleTimeoutMillis, and connectionTimeoutMillis
(tune values as appropriate), and add graceful shutdown handlers that call
pool.end() before exiting—register on process events such as SIGINT, SIGTERM,
beforeExit/exit and uncaughtException to close the pool and then cleanly shut
down the PrismaClient (prisma) and any adapters (PrismaPg adapter) to avoid
dangling connections.
client/src/pages/DetailTicket.css (1)

33-37: Add single-column fallback for detail/image grids on small screens.

At mobile widths, .detail-ticket and .ticket-img remain 2-column, which can compress content too much.

📐 Suggested responsive update
 `@media` (max-width: 820px) {
  .type-status-ticket,.starRating {
     display: flex;
     flex-direction: column;
  }
+ .detail-ticket,
+ .ticket-img {
+    grid-template-columns: 1fr;
+ }
  .starRating p{
     margin-left: 0;
     margin-top: 10px;
  }
  .ticket-status{
     margin-left: 0;
  }
 
 }

Also applies to: 53-57, 195-208

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/DetailTicket.css` around lines 33 - 37, Update the
responsive CSS for the grid classes so they collapse to a single column on small
screens: add a media query (e.g., max-width: 600px) that sets .detail-ticket and
.ticket-img grid-template-columns to 1fr (and adjust any nested grid like
.ticket-info/.ticket-img to stack), ensure images and content inside those
selectors use width: 100% / auto layout and appropriate gaps/margins so nothing
is compressed; apply the same single-column fallback logic to the other grid
rules referenced for .detail-ticket and .ticket-img variants.
server/controllers/managementControllers.js (1)

17-20: Map known Prisma errors to 4xx/409 instead of returning 500 for all failures.

Constraint/validation failures should not be exposed as generic server errors. Handle Prisma known request errors (P2002, P2003, etc.) with client-appropriate status codes.

Also applies to: 37-40, 61-63, 84-86, 105-107, 134-136

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/controllers/managementControllers.js` around lines 17 - 20, Update the
catch blocks in server/controllers/managementControllers.js (e.g., the
createTicketCategory handler and the other handlers mentioned) to detect Prisma
known request errors and map them to appropriate 4xx responses instead of always
returning 500: import Prisma from '@prisma/client' (or use
prisma.PrismaClientKnownRequestError), then in each catch check if error is an
instance of Prisma.PrismaClientKnownRequestError and switch on error.code (e.g.,
P2002 -> return res.status(409).json({ error: 'Conflict: duplicate entry' }),
P2003 -> return res.status(400).json({ error: 'Bad Request: foreign key
constraint' }), validation-like codes -> 400), otherwise fall back to logging
and res.status(500).json({ error: 'Failed to ...' }); apply this change to the
same catch blocks referenced (the createTicketCategory catch and the other
handlers at the indicated locations).
server/controllers/ticketControllers.js (1)

93-139: Add pagination to the ticket list endpoint.

findMany is currently unbounded; this can become a hot path and memory-heavy as ticket volume grows.

🔧 Suggested change
 export const getAllTickets = async (req, res) => {
     try {
+        const take = Math.min(Number(req.query.take) || 50, 200);
+        const skip = Number(req.query.skip) || 0;
         const tickets = await prisma.ticket.findMany({
+            take,
+            skip,
             select: {
               ...
             }
         });
         res.status(200).json(tickets);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/controllers/ticketControllers.js` around lines 93 - 139, The ticket
listing currently uses prisma.ticket.findMany without limits; modify the
controller that calls prisma.ticket.findMany to accept pagination params (e.g.,
page and limit from req.query), compute skip = (page-1)*limit and take = limit,
and pass skip/take to prisma.ticket.findMany; also validate/normalize
page/limit, default them (e.g., limit=20), and include pagination metadata
(total count via prisma.ticket.count and current page/limit) in the JSON
response alongside the selected ticket fields (the existing select block and
images selection remain unchanged).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0f70ea24-e4e8-4062-86ff-e59fe41c861b

📥 Commits

Reviewing files that changed from the base of the PR and between f8276c7 and 91f05e1.

⛔ Files ignored due to path filters (9)
  • client/package-lock.json is excluded by !**/package-lock.json
  • client/public/description.png is excluded by !**/*.png
  • client/public/favicon.svg is excluded by !**/*.svg
  • client/public/icons.svg is excluded by !**/*.svg
  • client/public/rmutk.png is excluded by !**/*.png
  • client/src/assets/hero.png is excluded by !**/*.png
  • client/src/assets/react.svg is excluded by !**/*.svg
  • client/src/assets/vite.svg is excluded by !**/*.svg
  • server/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (27)
  • client/index.html
  • client/package.json
  • client/src/App.css
  • client/src/App.jsx
  • client/src/components/CardFinishProblem.css
  • client/src/components/CardFinishProblem.jsx
  • client/src/components/Navbar.css
  • client/src/components/Navbar.jsx
  • client/src/components/StarRating.jsx
  • client/src/hooks/useTickets.js
  • client/src/index.css
  • client/src/pages/Dashboard.css
  • client/src/pages/Dashboard.jsx
  • client/src/pages/DetailTicket.css
  • client/src/pages/DetailTicket.jsx
  • client/src/utils/formatDate.js
  • client/src/utils/timeline.js
  • server/config/cloudinaryControllers.js
  • server/config/prismaControllers.js
  • server/controllers/managementControllers.js
  • server/controllers/ticketControllers.js
  • server/package.json
  • server/prisma/schema.prisma
  • server/routes/managementRoutes.js
  • server/routes/ticketRoutes.js
  • server/src/index.js
  • server/utils/formatDate.js
💤 Files with no reviewable changes (1)
  • client/src/App.css

Comment on lines +50 to +53
const result = await prisma.$transaction(async (tx) => {

const ticket = await prisma.ticket.create({
data: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify transaction callbacks do not use prisma.* writes directly
rg -nP '\$transaction\s*\(\s*async\s*\(\s*tx\s*\)\s*=>[\s\S]{0,500}\bprisma\.[a-zA-Z_]+' server/controllers/ticketControllers.js
# Expected: no matches in transaction callback bodies.

Repository: CNPJB/trouble-tracking-system-project

Length of output: 63


🏁 Script executed:

cat -n server/controllers/ticketControllers.js | sed -n '45,85p'

Repository: CNPJB/trouble-tracking-system-project

Length of output: 1894


🏁 Script executed:

rg -n '\$transaction' server/controllers/ticketControllers.js

Repository: CNPJB/trouble-tracking-system-project

Length of output: 220


🏁 Script executed:

cat -n server/controllers/ticketControllers.js | sed -n '170,210p'

Repository: CNPJB/trouble-tracking-system-project

Length of output: 1529


Use the transaction client for all writes inside $transaction.

Line 52 uses prisma.ticket.create(...) instead of tx.ticket.create(...), breaking the transaction atomicity. If the image creation at line 73 fails, the ticket persists in the database without its related image record, leaving inconsistent data.

🔧 Proposed fix
- const ticket = await prisma.ticket.create({
+ const ticket = await tx.ticket.create({
    data: {
      userId: Number(userId),
      ticketCtgId: Number(ticketCtgId),
      locationId: Number(locationId),
      floorId: Number(floorId),
      roomId: Number(roomId),
      equipmentId: equipmentId ? Number(equipmentId) : null,
      title,
      description,
      ticketStatus,
      parentTicketId: parentTicketId ? Number(parentTicketId) : null,
      adminId,
      adminNote,
      rating: rating ? Number(rating) : null,
      comment,
      createdAt,
      updatedAt,
    }
  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/controllers/ticketControllers.js` around lines 50 - 53, The ticket
creation inside the prisma.$transaction is using prisma.ticket.create instead of
the transaction client, which breaks atomicity; replace
prisma.ticket.create(...) with tx.ticket.create(...) (and ensure any other write
calls inside the transaction—e.g., the image creation call that currently uses
prisma.image.create or similar—use tx.image.create or the corresponding tx.*
methods) so all DB writes inside the async (tx) => { ... } block go through the
tx client and are rolled back together on failure.

Comment on lines +159 to +167
const result = await new Promise((resolve, reject) => {
const stream = cloudinary.uploader.upload_stream(
{
folder: "TTS-img",
resource_type: "auto"
},
(error, result) => {
if (error) rejects(error);
else resolve(result);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify reject/rejects mismatches in Promise executors
rg -nP 'new Promise\s*\(\s*\(\s*resolve\s*,\s*reject\s*\)\s*=>[\s\S]{0,250}\brejects\s*\(' server/controllers/ticketControllers.js
# Expected: no matches.

Repository: CNPJB/trouble-tracking-system-project

Length of output: 63


🏁 Script executed:

cat -n server/controllers/ticketControllers.js | sed -n '159,167p'

Repository: CNPJB/trouble-tracking-system-project

Length of output: 549


Fix rejection variable typo in Cloudinary upload Promise.

Line 166 calls rejects(error) but the Promise executor parameter is reject. On upload failure this throws a ReferenceError and breaks error handling.

🔧 Proposed fix
-                    if (error) rejects(error);
+                    if (error) reject(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
const result = await new Promise((resolve, reject) => {
const stream = cloudinary.uploader.upload_stream(
{
folder: "TTS-img",
resource_type: "auto"
},
(error, result) => {
if (error) rejects(error);
else resolve(result);
const result = await new Promise((resolve, reject) => {
const stream = cloudinary.uploader.upload_stream(
{
folder: "TTS-img",
resource_type: "auto"
},
(error, result) => {
if (error) reject(error);
else resolve(result);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/controllers/ticketControllers.js` around lines 159 - 167, The Promise
wrapper around cloudinary.uploader.upload_stream in the ticket controller
incorrectly calls rejects(error) causing a ReferenceError; update the Promise
executor callback to call the provided reject parameter (i.e., use
reject(error)) when the upload_stream callback receives an error so the Promise
rejects properly and error handling works for the function using the result
variable.

Comment thread server/prisma/schema.prisma Outdated
locationName String @map("location_name") @db.VarChar(100)
locationStatus LocationStatus @default(active) @map("location_status")
createdAt DateTime? @map("created_at") @db.Timestamp()
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Changed schema defaults:"
rg -n '@default\(now\(\)\) `@map`\("created_at"\)' server/prisma/schema.prisma

echo
if [ -d server/prisma/migrations ]; then
  echo "Migration SQL touching created_at/defaults:"
  fd -e sql . server/prisma/migrations -x rg -n 'created_at|DEFAULT\s+NOW\(\)|ALTER\s+TABLE' {}
else
  echo "server/prisma/migrations not found"
fi

Repository: CNPJB/trouble-tracking-system-project

Length of output: 3653


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the table name for each schema line with `@default`(now())
echo "=== Mapping schema lines to table names ==="
for line in 81 97 114 130 165 178 204; do
  table=$(sed -n "1,${line}p" server/prisma/schema.prisma | tac | grep -m1 "^model " | awk '{print $2}')
  echo "Line $line: Table $table"
done

echo
echo "=== Checking which tables have DEFAULT CURRENT_TIMESTAMP in migration ==="
# List all CREATE TABLE statements with their defaults
fd -e sql . server/prisma/migrations -x rg -B2 'DEFAULT CURRENT_TIMESTAMP|^[[:space:]]*CREATE TABLE' {} +

echo
echo "=== Summary: Tables without DEFAULT in migration but with `@default`(now()) in schema ==="
# Get the full migration file to analyze
cat server/prisma/migrations/*/migration.sql | grep -A10 "CREATE TABLE" | grep -B2 "created_at" | head -50

Repository: CNPJB/trouble-tracking-system-project

Length of output: 4055


🏁 Script executed:

cat server/prisma/migrations/20260404071617_init_tts_project/migration.sql | sed -n '/CREATE TABLE "locations"/,/^-- CreateTable/p' | head -20
echo "---"
cat server/prisma/migrations/20260404071617_init_tts_project/migration.sql | sed -n '/CREATE TABLE "floors"/,/^-- CreateTable/p' | head -20
echo "---"
cat server/prisma/migrations/20260404071617_init_tts_project/migration.sql | sed -n '/CREATE TABLE "rooms"/,/^-- CreateTable/p' | head -20
echo "---"
cat server/prisma/migrations/20260404071617_init_tts_project/migration.sql | sed -n '/CREATE TABLE "equipment_categories"/,/^-- CreateTable/p' | head -20
echo "---"
cat server/prisma/migrations/20260404071617_init_tts_project/migration.sql | sed -n '/CREATE TABLE "ticket_categories"/,/^-- CreateTable/p' | head -20

Repository: CNPJB/trouble-tracking-system-project

Length of output: 1728


The migration is incomplete—5 of 7 affected tables are missing database-level defaults.

The schema adds @default(now()) to created_at columns in locations, floors, rooms, equipment_categories, and ticket_categories, but the migration only includes DEFAULT CURRENT_TIMESTAMP for ticket_images and tickets. Prisma schema defaults are client-side only; without database-level defaults, direct SQL inserts or other applications will not auto-populate these timestamps.

Add ALTER TABLE statements to the migration for these tables:

ALTER TABLE "locations" ADD DEFAULT CURRENT_TIMESTAMP TO "created_at";
ALTER TABLE "floors" ADD DEFAULT CURRENT_TIMESTAMP TO "created_at";
ALTER TABLE "rooms" ADD DEFAULT CURRENT_TIMESTAMP TO "created_at";
ALTER TABLE "equipment_categories" ADD DEFAULT CURRENT_TIMESTAMP TO "created_at";
ALTER TABLE "ticket_categories" ADD DEFAULT CURRENT_TIMESTAMP TO "created_at";

Affected schema lines: 81, 97, 114, 130, 178

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/prisma/schema.prisma` at line 81, The migration is missing
database-level defaults for the created_at columns even though schema.prisma
uses `@default`(now()); add ALTER TABLE ... ADD DEFAULT CURRENT_TIMESTAMP for the
tables locations, floors, rooms, equipment_categories, and ticket_categories so
the DB will populate created_at on direct SQL inserts; target the created_at
columns (createdAt in Prisma) for each table to match the schema change and
ensure the migration includes these five ALTER TABLE statements alongside the
existing changes for ticket_images and tickets.

Comment thread server/src/index.js
Comment thread server/src/index.js


// api get
app.get('/api/gettickets', ticketRoutes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect usage of Express router with app.get().

Passing an Express router directly to app.get() is incorrect. Routers should be mounted with app.use(). This line effectively does nothing useful since ticketRoutes is already mounted at /api/tickets (line 13), which already handles GET /api/tickets/get.

🐛 Proposed fix - remove redundant line
-// api get
-app.get('/api/gettickets', ticketRoutes);

The endpoint GET /api/tickets/get is already available via line 13. If you need /api/gettickets specifically, create a dedicated route handler or use a redirect.

📝 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
app.get('/api/gettickets', ticketRoutes);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.js` at line 18, The call app.get('/api/gettickets',
ticketRoutes) is incorrect because Express routers are mounted with app.use;
remove this redundant line (app.get('/api/gettickets', ticketRoutes)) since
ticketRoutes is already mounted via app.use('/api/tickets', ticketRoutes) and
provides GET /api/tickets/get; if you specifically need /api/gettickets instead,
replace the line with a dedicated handler (e.g., app.get('/api/gettickets',
(req,res) => ...) or add a redirect to /api/tickets/get) rather than passing the
router to app.get.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/index.html`:
- Line 5: The link tag in client/index.html has an empty favicon href (the <link
rel="icon"> element) which is invalid; either remove that <link rel="icon"> line
entirely or replace the empty href with a valid asset path (e.g., add a favicon
file to the project and set href to its path such as /favicon.svg) so the
browser can load the favicon correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a0baf8dd-dab2-4d45-9a13-70d35275dad8

📥 Commits

Reviewing files that changed from the base of the PR and between 91f05e1 and 86a69a1.

⛔ Files ignored due to path filters (2)
  • client/package-lock.json is excluded by !**/package-lock.json
  • server/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • client/index.html
  • client/package.json
  • client/src/App.css
  • client/src/App.jsx
  • client/src/components/Navbar.jsx
  • client/src/pages/Dashboard.jsx
  • server/package.json
  • server/src/index.js
✅ Files skipped from review due to trivial changes (4)
  • client/package.json
  • client/src/components/Navbar.jsx
  • client/src/App.css
  • server/package.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • client/src/pages/Dashboard.jsx
  • server/src/index.js
  • client/src/App.jsx

Comment thread client/index.html
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check if client/index.html exists and read its content
cat -n client/index.html

Repository: CNPJB/trouble-tracking-system-project

Length of output: 534


🏁 Script executed:

# Search for favicon-related assets in the repository
find . -type f \( -name "*favicon*" -o -name "*.svg" -o -name "*.ico" \) | head -20

Repository: CNPJB/trouble-tracking-system-project

Length of output: 63


🏁 Script executed:

# Search for any href patterns related to favicon in the codebase
rg "favicon|rel=\"icon\"" --type html --type js --type ts

Repository: CNPJB/trouble-tracking-system-project

Length of output: 151


Remove the empty favicon href or provide an actual asset path.

At Line 5, href="" is invalid and breaks favicon loading. The suggested path /favicon.svg does not exist in the repository. Either create a favicon asset and reference it, or remove this line entirely if a favicon is not needed.

Example fix (if favicon asset will be added)
-    <link rel="icon" type="image/svg+xml" href="" />
+    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
🧰 Tools
🪛 HTMLHint (1.9.2)

[error] 5-5: The attribute [ href ] of the tag [ link ] must have a value.

(src-not-empty)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/index.html` at line 5, The link tag in client/index.html has an empty
favicon href (the <link rel="icon"> element) which is invalid; either remove
that <link rel="icon"> line entirely or replace the empty href with a valid
asset path (e.g., add a favicon file to the project and set href to its path
such as /favicon.svg) so the browser can load the favicon correctly.

@poner2547
poner2547 merged commit ecf263d into develop Apr 10, 2026
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Jun 1, 2026
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