Skip to content

Implement certificate generation and approval workflow#255

Open
KotapatiSaiMounika wants to merge 2 commits into
OpenLake:mainfrom
KotapatiSaiMounika:feature/certificate-management
Open

Implement certificate generation and approval workflow#255
KotapatiSaiMounika wants to merge 2 commits into
OpenLake:mainfrom
KotapatiSaiMounika:feature/certificate-management

Conversation

@KotapatiSaiMounika

@KotapatiSaiMounika KotapatiSaiMounika commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR introduces a complete certificate management workflow for the Student Database COSA project.

Features

  • Certificate template management
  • Certificate batch creation and validation
  • Batch approval workflow
  • PDF certificate generation using Puppeteer and Handlebars
  • Student certificate dashboard
  • Certificate APIs
  • Duplicate certificate prevention
  • Backend service layer for certificate management
  • Frontend pages and components for templates, batches, requests, and certificates

Technical Changes

  • Added certificate and template models
  • Added certificate, template, and batch controllers
  • Added new backend routes
  • Added certificate generation service
  • Added PDF rendering utility
  • Added validation utilities
  • Added frontend pages, components, and services

Known Limitation

Cloudinary upload/download integration is temporarily stubbed (certificateUrl = "#") due to a local networking issue affecting Cloudinary uploads during development. The certificate workflow is implemented, and Cloudinary integration will be completed in a follow-up PR.

Summary by CodeRabbit

  • New Features
    • Added certificate management, including batch creation, editing, duplication, archiving, deletion, approval, rejection, and certificate viewing.
    • Added certificate PDF generation and certificate listing with search and status filters.
    • Added template creation, editing, viewing, deletion, filtering, and grid/list layouts.
    • Added participant selection with search, bulk selection, and editable batch membership.
    • Added dedicated Requests workflows for reviewing, editing, approving, and rejecting batches.
    • Added navigation and dashboard access for Certificates, Batches, Templates, and Requests.
  • Bug Fixes
    • Improved validation and approval controls to prevent invalid submissions and unauthorized actions.

@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

@KotapatiSaiMounika is attempting to deploy a commit to the openlake's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@KotapatiSaiMounika, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 017e487d-b5e9-434a-b64c-951e83149efa

📥 Commits

Reviewing files that changed from the base of the PR and between 5c181ca and b4f3b18.

📒 Files selected for processing (1)
  • backend/controllers/templateController.js

Walkthrough

Adds certificate batch creation and approval workflows, template CRUD, PDF certificate generation, Cloudinary support, and frontend interfaces for batches, requests, templates, and certificates.

Changes

Certificate platform

Layer / File(s) Summary
Certificate data and generation foundation
backend/models/*, backend/services/*, backend/utils/*, backend/template-designs/*, backend/package.json
Adds certificate/template schemas, validation, organization and user services, PDF rendering, certificate generation, upload handling, email helpers, and required packages.
Batch approval API
backend/controllers/certificateBatchController.js, backend/routes/certificateBatch.js, backend/index.js
Adds authenticated batch creation, editing, duplication, lifecycle updates, participant updates, listing, approval, rejection, and route mounting.
Template management
backend/controllers/templateController.js, backend/routes/template.js, frontend/src/Components/Templates/*, frontend/src/services/templates.js, frontend/src/pages/templatesPage.jsx
Adds template CRUD endpoints and frontend filtering, viewing, editing, deletion, thumbnails, and page integration.
Coordinator batch interface
frontend/src/Components/Batches/*, frontend/src/services/batch.js, frontend/src/services/events.js, frontend/src/pages/batchesPage.jsx
Adds batch listing, filtering, modal editing, participant selection, lifecycle actions, and reusable batch UI components.
Approver request interface
frontend/src/Components/Requests/*, frontend/src/context/RequestContext.js, frontend/src/pages/requestsPage.jsx
Adds request statistics, filtering, participant editing, request details, approval actions, and request context wiring.
Certificate retrieval and application wiring
backend/controllers/certificateController.js, backend/routes/certificate.js, frontend/src/Components/Certificates/*, frontend/src/services/certificate.js, frontend/src/pages/certificatesPage.jsx, frontend/src/config/*, frontend/src/routes/StudentRoutes.js
Adds authenticated certificate retrieval, certificate filtering and actions, protected student routing, dashboard mappings, and role-specific navigation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Coordinator
  participant BatchAPI
  participant Approver
  participant CertificateService
  participant Student
  Coordinator->>BatchAPI: Create and submit batch
  BatchAPI-->>Approver: Expose submitted request
  Approver->>BatchAPI: Approve or reject batch
  BatchAPI->>CertificateService: Generate certificates after final approval
  CertificateService-->>Student: Store certificate record
  Student->>BatchAPI: Fetch certificates
  BatchAPI-->>Student: Return certificate list
Loading

Possibly related issues

Suggested reviewers: harshitap1305

Poem

A rabbit hops through batches bright,
Approvers guide them left and right.
Templates bloom, certificates gleam,
PDFs drift softly like a dream.
“Hop approved!” the bunny cries,
While Cloudinary fills the skies.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is on-topic but omits most required template sections, including related issue, testing, checklist, and deployment notes. Add the missing template sections: related issue, changes introduced, why this change, screenshots/video, testing, documentation updates, checklist, deployment notes, and additional notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: certificate generation and approval workflow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Note

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

🟠 Major comments (27)
frontend/src/Components/Templates/Template.jsx-327-329 (1)

327-329: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

t.title.includes("Copy") is a fragile and incorrect authorization proxy.

Both the grid view (line 329) and list view (line 495) use t.title.includes("Copy") to decide whether to show Edit/Delete buttons. This grants edit/delete UI to any user for any template whose title happens to contain "Copy", regardless of ownership or role. Conversely, a duplicated template renamed to "Duplicate" would not show the buttons.

If the intent is to let creators edit their own templates, check ownership instead:

🔧 Proposed fix
-                      {(role === "PRESIDENT" ||
-                      role.startsWith("GENSEC") ||
-                      t.title.includes("Copy")) ? (
+                      {(role === "PRESIDENT" ||
+                      role.startsWith("GENSEC") ||
+                      t.createdBy?._id === currentUser?._id) ? (

Apply the same change at line 495. This requires passing the current user ID from useAdminContext.

Also applies to: 493-495

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Templates/Template.jsx` around lines 327 - 329,
Replace the title-based authorization condition in both the grid and list view
action controls with an ownership check using the current user ID from
useAdminContext, while preserving PRESIDENT and GENSEC role access. Update the
relevant Template.jsx logic near the existing role checks so edit/delete buttons
are shown for authorized roles or templates owned by the current user, and apply
the same change in both locations.
frontend/src/Components/Templates/Template.jsx-93-102 (1)

93-102: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

filters useMemo crashes when createdBy is null.

Line 98 accesses t.createdBy.personal_info.name without optional chaining. If any template has a null createdBy (e.g., the creator's account was deleted), this throws TypeError: Cannot read properties of null (reading 'personal_info') and crashes the entire component. The filtered function at line 81 guards against this, but filters does not.

🛡️ Proposed fix
   const filters = useMemo(
     () => ({
       categories: [...new Set(templates?.map((t) => t.category))],
       statuses: [...new Set(templates?.map((t) => t.status))],
       creators: [
-        ...new Set(templates?.map((t) => t.createdBy.personal_info.name)),
+        ...new Set(
+          templates
+            ?.filter((t) => t?.createdBy?.personal_info?.name)
+            .map((t) => t.createdBy.personal_info.name)
+        ),
       ],
     }),
     [templates],
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Templates/Template.jsx` around lines 93 - 102, Update
the creators mapping in the filters useMemo to safely handle templates with null
or missing createdBy data, matching the defensive behavior already used by
filtered. Preserve valid creator names while preventing the component from
throwing when creator information is absent.
backend/controllers/templateController.js-60-73 (1)

60-73: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

design default and category requirement mismatch with schema.

Two contract inconsistencies between the controller and templateSchema.js:

  1. The controller defaults design to "default.html" (line 70), but the schema default is "Default" and the frontend dropdown (TemplateModal.jsx:24-28) only offers ["Default", "Classic", "Modern"]. A template created via direct API call without design would store a value not present in the frontend dropdown.

  2. The controller requires category (line 60), but the schema does not mark it as required. Templates created through other paths (seeds, other controllers) could lack category and would be silently hidden by the frontend filter at Template.jsx:81.

🔧 Proposed fix
-    design: design || "default.html",
+    design: design || "Default",

Additionally, add required: true to the category field in templateSchema.js to align the schema with the controller's validation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/templateController.js` around lines 60 - 73, Align
template creation and validation contracts: update the design fallback in the
template creation flow to use the schema/frontend value “Default” instead of
“default.html”, and mark the category field as required in templateSchema so all
creation paths enforce the controller’s requirement. Use the existing template
creation logic and category schema definition without changing unrelated
behavior.
frontend/src/services/templates.js-4-91 (1)

4-91: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Inconsistent error handling pattern breaks all component error handling.

Four service functions (fetchTemplates, fetchTemplate, createTemplate, deleteTemplate) catch errors and return err.response?.data?.message (a string or undefined) instead of throwing. This makes every catch block in consuming components dead code:

  • Template.jsx:loadTemplates — silently swallows fetch errors; user sees an empty list with no explanation.
  • Template.jsx:deldeleteTemplate never throws, so toast.error("Failed to archive template.") never fires; loadTemplates() runs unconditionally, making the failed delete appear as a no-op.
  • TemplateModal.jsx:handleSubmitcreateTemplate/updateTemplate never throw, so the catch toast is dead code; the modal closes via handleClose() even on API failure, losing the user's form data without feedback.

Meanwhile archiveTemplate (line 79-90) uses the correct pattern (re-throws after logging) but duplicates deleteTemplate's API call.

Align all functions on throw-after-log:

♻️ Proposed fix (apply to all four functions)
 export async function fetchTemplates() {
   try {
     const res = await api.get("/api/templates");
     return {
       data: res.data.message,
       status: res.status,
     };
   } catch (err) {
     console.error("fetchTemplates error:", err);
-    return err.response?.data?.message;
+    throw err;
   }
 }

Apply the same throw err replacement to fetchTemplate, createTemplate, and deleteTemplate. Remove archiveTemplate as it becomes identical to deleteTemplate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/services/templates.js` around lines 4 - 91, Update
fetchTemplates, fetchTemplate, createTemplate, and deleteTemplate to rethrow the
caught error after logging instead of returning err.response?.data?.message, so
consuming component catch handlers execute. Remove archiveTemplate because
deleteTemplate now provides the same throw-after-log behavior, while preserving
each function’s existing API call and successful response shape.
backend/controllers/certificateController.js-38-38 (1)

38-38: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Date formatted as en-GB string breaks frontend parsing.

toLocaleDateString("en-GB") produces "DD/MM/YYYY" (e.g., "15/01/2024"). The frontend's formatDate then calls new Date(dateString) on this value, which is implementation-dependent and returns Invalid Date in most browsers. Send the raw ISO date or timestamp and let the frontend handle all formatting.

🐛 Proposed fix
-        date: new Date(cert.createdAt).toLocaleDateString("en-GB"),
+        date: cert.createdAt,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/certificateController.js` at line 38, Update the
certificate response date field to return the raw cert.createdAt value (or an
ISO-compatible timestamp) instead of formatting it with
toLocaleDateString("en-GB"), so the frontend formatDate can parse and format it
consistently.
backend/controllers/certificateBatchController.js-143-212 (1)

143-212: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

editBatch lacks ownership authorization — any coordinator can edit any batch.

deleteBatch and archiveBatch both filter by initiatedBy: id, but editBatch fetches by batchId only and never checks that batch.initiatedBy matches the caller. A coordinator could edit batches they didn't create. Additionally, there's no state guard preventing edits to batches that are already Submitted or Active.

🔒 Proposed fix
     const batch = await CertificateBatch.findById(batchId);
 
     if (!batch) {
       return res.status(404).json({ message: "Batch not found" });
     }
 
+    if (batch.initiatedBy.toString() !== id) {
+      return res.status(403).json({ message: "You are not authorized to edit this batch" });
+    }
+
+    if (batch.lifecycleStatus === "Submitted" || batch.lifecycleStatus === "Active") {
+      return res.status(400).json({ message: "Cannot edit a batch that is already submitted or active" });
+    }
+
     Object.assign(batch, validation.data);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/certificateBatchController.js` around lines 143 - 212,
Update editBatch to fetch or validate the CertificateBatch using both batchId
and initiatedBy: id, returning the existing not-found response when the caller
does not own the batch. Add a state guard before Object.assign so batches with
lifecycleStatus "Submitted" or "Active" cannot be edited, while preserving edits
for allowed states.
backend/controllers/certificateBatchController.js-104-107 (1)

104-107: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

missing.map() result is discarded — error details sent to client are raw IDs, not structured objects.

Line 105 calls missing.map(...) but doesn't assign the result. The details field at line 106 receives the original missing array of raw string IDs, not the intended [{ uid, ok: false, reason: "User not found" }] objects.

🐛 Proposed fix
     if(missing.length > 0){
-      missing.map((uid) => ({ uid, ok: false, reason: "User not found" }));
+      const details = missing.map((uid) => ({ uid, ok: false, reason: "User not found" }));
       return res.status(400).json({ message: "Invalid user data sent", details: missing });
+      return res.status(400).json({ message: "Invalid user data sent", details });
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/certificateBatchController.js` around lines 104 - 107,
Update the missing-user handling in the certificate batch controller so the
mapped objects from missing are retained and sent in the response details.
Replace the discarded missing.map(...) call with an assignment or directly use
its result, preserving each uid with ok: false and reason: "User not found".
backend/controllers/certificateBatchController.js-428-456 (1)

428-456: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

approverEditBatch has no try-catch — unhandled errors will crash the request.

Every other controller function wraps its body in try-catch, but this one doesn't. If CertificateBatch.findById or batch.save() throws, Express will return an unhandled error.

Additionally, there's no check that the caller is actually an assigned approver for this specific batch — any GENSEC or President can modify participants on any batch.

🛡️ Proposed fix
 async function approverEditBatch(req, res) {
+  try {
   const { id } = req.user;
 
   const user = await User.findById(id);
   if (!user) {
     return res.status(404).json({ message: "User not found" });
   }
 
   if (user.role !== "PRESIDENT" && !user.role.startsWith("GENSEC")) {
     return res.status(403).json({ message: "Access denied" });
   }
 
   let { users } = req.body;
   const validation = validateBatchUsersIds.safeParse(users);
   if (!validation.success) {
     let errors = validation.error.issues.map((issue) => issue.message);
     return res.status(400).json({ message: errors });
   }
 
   const { _id } = req.body;
   const batch = await CertificateBatch.findById(_id);
   if (!batch) {
     return res.status(404).json({ message: "Batch not found" });
   }
+  if (!batch.approverIds.some((a) => a.toString() === id.toString())) {
+    return res.status(403).json({ message: "You are not an assigned approver for this batch" });
+  }
   batch.users = users;
   await batch.save();
 
   res.status(200).json({ message: "Batch updated successfully" });
+  } catch (err) {
+    if (err instanceof HttpError) {
+      const payload = { message: err.message };
+      if (err.details) payload.details = err.details;
+      return res.status(err.statusCode).json(payload);
+    }
+    return res.status(500).json({ message: err.message || "Internal server error" });
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/certificateBatchController.js` around lines 428 - 456,
Update approverEditBatch to wrap its database lookups, authorization, mutation,
and response flow in the controller’s established try-catch pattern, forwarding
failures through the existing error-handling mechanism. Before assigning
batch.users, verify that the authenticated user is an assigned approver for the
specific batch, while preserving the existing role and not-found checks; reject
unauthorized callers without modifying or saving the batch.
backend/routes/certificateBatch.js-75-87 (1)

75-87: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use a state-changing method for approve/reject
/api/batches/:batchId/approve and /api/batches/:batchId/reject mutate batch state, and the frontend already calls them with GET. Switch both the routes and frontend/src/services/batch.js to POST or PATCH so they aren’t accidentally triggered by prefetching or caching behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/routes/certificateBatch.js` around lines 75 - 87, Change the
approveBatch and rejectBatch routes from GET to POST or PATCH, and update the
corresponding frontend service calls in batch.js to use the same state-changing
method. Preserve the existing paths, authentication, authorization, and handlers
while ensuring both operations are no longer invoked through GET requests.
backend/controllers/certificateBatchController.js-542-559 (1)

542-559: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Avoid marking the batch Active before certificate generation finishes. generateCertificates() can throw after findOneAndUpdate() has already persisted lifecycleStatus: "Active", and the service also allows partial certificate creation before bubbling an error. That leaves the batch in a completed state even though generation failed.

Keep the batch in the pre-active state until generation succeeds, or add a compensating/retry path for failed generations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/certificateBatchController.js` around lines 542 - 559,
Update the level-1 approval flow around findOneAndUpdate and
generateCertificates so lifecycleStatus is not persisted as "Active" until
certificate generation completes successfully. Ensure generation failures leave
the batch retryable or execute a compensating update that restores the
pre-active state, while preserving the existing conflict response and
exactly-once approval behavior.
backend/controllers/certificateBatchController.js-293-324 (1)

293-324: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Restrict deleteBatch to Draft batches. Active batches already have Certificate records linked by batchId, so deleting them leaves orphaned certificates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/certificateBatchController.js` around lines 293 - 324,
Update deleteBatch to restrict the findOneAndDelete query to batches whose
status is Draft, while preserving the existing batchId validation and
initiatedBy ownership checks. Active or any non-Draft batch must follow the
existing not-found/unauthorized response and remain undeleted.
frontend/src/services/batch.js-23-32 (1)

23-32: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

createBatch has inconsistent return types between success and error paths.

On success, it returns {data, status} (an object). On error, it returns err.response?.data.message (a string or undefined). Callers cannot reliably distinguish success from error without checking the shape of the return value. Align with the other functions in this module or throw on error.

🔧 Proposed fix for consistent return type
 export async function createBatch(data) {
   try {
     const response = await api.post("/api/batches/create-batch", data);
-    console.log("Response", response);
-    return {data: response.data.message, status: response.status};
+    return response.data.message;
   } catch (err) {
     console.error("createBatch error:", err);
     return err.response?.data.message;
   }
 }

Alternatively, if the status is needed by callers, return a consistent object shape on both paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/services/batch.js` around lines 23 - 32, Update createBatch so
its success and error paths use a consistent return contract: either align both
with the surrounding batch-service functions or return the same object shape
containing data and status for both outcomes. Do not return the raw error
message string or undefined; preserve error details through the chosen
consistent contract.
frontend/src/services/batch.js-88-106 (1)

88-106: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use POST/PATCH for batch approval actions. approveBatch and rejectBatch mutate server state but are exposed as GET endpoints. With cookie-based auth (withCredentials: true) and no CSRF guard, these requests should be non-GET on both the client and backend/routes/certificateBatch.js.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/services/batch.js` around lines 88 - 106, Update approveBatch
and rejectBatch to send POST or PATCH requests instead of GET while preserving
their existing endpoints, response handling, and error handling. Update the
corresponding approval and rejection routes in certificateBatch.js to use the
same non-GET method so client and server remain aligned.
frontend/src/Components/Batches/StudentPanel.jsx-154-154 (1)

154-154: 🎯 Functional Correctness | 🟠 Major | ⚖️ Poor tradeoff

Tailwind v4 important modifier syntax is incorrect
!rounded-lg / !rounded-xl should be rounded-lg! / rounded-xl! in frontend/src/Components/Batches/StudentPanel.jsx; the same fix applies to the matching occurrences at lines 154, 206, and 213.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Batches/StudentPanel.jsx` at line 154, Update the
Tailwind important modifier syntax in StudentPanel.jsx by changing each matching
!rounded-lg and !rounded-xl utility to rounded-lg! and rounded-xl!, including
the occurrences near the student panel controls at the referenced locations.
frontend/src/Components/Batches/batchCard.jsx-201-237 (1)

201-237: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Inconsistent action buttons between grid and list views.

The available actions for the same lifecycleStatus differ significantly between BatchCard (grid) and BatchList (list):

Status Grid (BatchCard) List (BatchList)
Draft View, Edit, Duplicate, Delete Edit, Delete
Active View, Duplicate, Archive View, Edit, Duplicate, Delete
Submitted View, Duplicate, Archive View, Archive
Archived View, Duplicate, Delete View, Duplicate, Delete

For example, "Active" batches can be edited and deleted in list view but not in grid view, and "Draft" batches lose View and Duplicate actions in list view. Users switching views will experience unexpected changes in available actions.

Also applies to: 387-454

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Batches/batchCard.jsx` around lines 201 - 237, The
action sets in BatchCard and BatchList must be consistent for every
lifecycleStatus. Update the status-specific action rendering in BatchCard and
the corresponding BatchList branches so Draft, Active, Submitted, and Archived
expose the same buttons and handlers in both views, preserving the existing
status-to-action mapping and callback parameters.
frontend/src/Components/Batches/batchCard.jsx-114-122 (1)

114-122: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add null guards on nested property access.

Several deeply nested property chains (batch.eventId.title, batch.eventId.organizing_unit_id.name, batch.users.length, b.initiatedBy.personal_info.name) have no optional chaining, while other lines in the same component do (e.g., line 186 uses batch?.initiatedBy?.personal_info?.name). If the backend returns a batch with a missing eventId, users, or initiatedBy, these will throw and crash the entire list.

Also applies to: 153-153, 159-159, 318-318, 330-330, 360-360

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Batches/batchCard.jsx` around lines 114 - 122, Add
optional chaining to the nested batch property accesses identified in the
review, including eventId.title, eventId.organizing_unit_id.name, users.length,
and initiatedBy.personal_info.name, while preserving existing fallback values
such as empty strings. Align these accesses with the guarded pattern already
used in batchCard.jsx, including all additionally referenced locations.
frontend/src/services/events.js-1-13 (1)

1-13: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Error return value is truthy — callers cannot detect failures.

fetchEvents returns err.response?.data.message (a string) on error, but callers like batches.jsx check if (!events) to detect failure. A non-empty error string is truthy, so errors are indistinguishable from valid data. Return null on error so callers can reliably detect failures.

🐛 Proposed fix
   } catch (err) {
     console.error("fetchEvents error:", err);
-    return err.response?.data.message;
+    return null;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/services/events.js` around lines 1 - 13, Update the catch block
in fetchEvents to return null instead of err.response?.data.message, preserving
the existing error logging so callers checking for a falsy result can reliably
detect request failures.
frontend/src/Components/Batches/batches.jsx-512-514 (1)

512-514: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

React key uses b.id but batch objects use _id.

key={b.id} is used in the grid view map, but batch objects from MongoDB use _id as their identifier (as seen in BatchCard which checks batch._id || batch.id || batch.batchId). If b.id is undefined, React will use undefined for all keys, causing incorrect reconciliation. Use b._id instead.

🐛 Proposed fix
-            <BatchCard
-              key={b.id}
+            <BatchCard
+              key={b._id}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Batches/batches.jsx` around lines 512 - 514, Update
the key in the grid view’s filtered.map rendering to use each batch’s MongoDB
identifier, b._id, instead of b.id, while leaving the surrounding BatchCard
rendering unchanged.
frontend/src/Components/Batches/batches.jsx-322-342 (1)

322-342: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

updated.length !== 0 prevents UI update when last batch is removed.

In delBatch, archiveBatch, and dupBatch, the condition if (updated && updated.length !== 0) setBatches(updated) means that if the API returns an empty array (e.g., after deleting the last batch), setBatches is never called and the stale batch remains visible. The check should be if (updated) or if (Array.isArray(updated)).

🐛 Proposed fix
-    if (updated && updated.length !== 0) setBatches(updated);
+    if (updated) setBatches(updated);

Apply this to lines 326, 333, and 341.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Batches/batches.jsx` around lines 322 - 342, Update
the post-fetch state updates in delBatch, archiveBatch, and dupBatch to call
setBatches when updated is a valid result even if it is an empty array. Replace
the updated.length !== 0 guard with an existence or array-validity check so
removing the last batch clears the UI.
frontend/src/Components/Batches/batches.jsx-308-310 (1)

308-310: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make editBatch return the same { status, data } shape as createBatch

editBatch returns only res.data.message, but the save flow expects response.status and response.data. On edits, that leaves both undefined and the modal/toast path never completes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Batches/batches.jsx` around lines 308 - 310, Update
editBatch to return the same { status, data } response shape as createBatch,
preserving the HTTP status alongside the response payload instead of returning
only res.data.message. This allows the save flow’s response.status check,
closeModal(), and fire(response.data) path to work for edits.
frontend/src/Components/Batches/batches.jsx-67-94 (1)

67-94: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Error handling cannot distinguish success from failure for fetchEvents.

fetchEvents returns err.response?.data.message (a truthy string) on error. The check if (!batches || !events || !templates) on line 76 evaluates to false for a non-empty string, so errors pass through as if they were valid data. The events array would then be set to a string, causing downstream .map() calls to fail. Consider returning null on error instead.

🐛 Proposed fix for events.js
   } catch (err) {
     console.error("fetchEvents error:", err);
-    return err.response?.data.message;
+    return null;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Batches/batches.jsx` around lines 67 - 94, Update
fetchEvents so its error path returns null instead of
err.response?.data.message, allowing the existing validation in the batches
useEffect to distinguish failures from successful event arrays. Preserve the
existing error notification behavior while ensuring setEvents only receives
valid event data.
backend/services/email.service.js-56-63 (1)

56-63: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

User-controlled data is interpolated into HTML emails without escaping.

Values like batchObj.title, batchObj.event.name, batchObj.event.description, approver name/email, and batchObj.createdBy are inserted directly into HTML templates. If any contain HTML characters, they can break email layout or inject misleading content/links. Add an escape helper and apply it to all interpolated values.

🔒 Proposed fix: add an HTML-escape helper and use it
+function escapeHtml(str) {
+    return String(str)
+        .replace(/&/g, "&amp;")
+        .replace(/</g, "&lt;")
+        .replace(/>/g, "&gt;")
+        .replace(/"/g, "&quot;")
+        .replace(/'/g, "&`#039`;");
+}
+
 async function newBatchSendEmail(toEmail, ccEmails=[], batchLink, batchObj){
-    const approverList = (batchObj.approverList || []).map((a, index) => `
+    const approverList = (batchObj.approverList || []).map((a, index) => `
         <div>
             <strong>Approver ${index + 1}:</strong><br/>
-            Name: ${a.name}<br/>
-            Email: ${a.email}
+            Name: ${escapeHtml(a.name)}<br/>
+            Email: ${escapeHtml(a.email)}
         </div>
         <br />
     `).join("");

Apply escapeHtml() to all ${...} interpolations of user-controlled data across all three functions.

Also applies to: 82-86, 136-143, 154-155, 161-164, 205-206, 211-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/email.service.js` around lines 56 - 63, In the
email-building functions, add a reusable escapeHtml helper and apply it to every
user-controlled interpolation, including batchObj.title, batchObj.event.name,
batchObj.event.description, batchObj.createdBy, and approver name/email values
in approverList. Keep static HTML markup and non-user-controlled formatting
unchanged while ensuring all displayed data is escaped before insertion.
backend/services/email.service.js-134-263 (1)

134-263: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

batchStatusSendEmail has multiple robustness defects.

  1. Line 136: batchObj.pendingApprovers.map() — no null check. Unlike newBatchSendEmail which uses (batchObj.approverList || []), this will throw if pendingApprovers is undefined (e.g., on rejection).
  2. Line 134: ccEmails has no default parameter. ccEmails.join(",") on line 250 throws if undefined.
  3. Lines 251-252: Emailformat[action].subject throws a TypeError for an invalid action before the validation check on line 256 is reached — the validation is dead code.
  4. Line 261: throw new Error(err) wraps an Error object, losing the stack trace and original message. Use throw err instead.
🐛 Proposed fix
-async function batchStatusSendEmail(toEmail, ccEmails, batchLink, batchObj, action){
+async function batchStatusSendEmail(toEmail, ccEmails = [], batchLink, batchObj, action){
+
+    if(!["approve", "reject"].includes(action)){
+        throw new Error("Invalid action");
+    }
 
-    const approverList = batchObj.pendingApprovers.map((a, index) => `
+    const approverList = (batchObj.pendingApprovers || []).map((a, index) => `
         <div>
             <strong>Approver ${index + 1}:</strong><br/>
             Name: ${a.name}<br/>
             Email: ${a.email}
         </div>
         <br />    
     `).join("");

And move the validation before options construction, then fix the catch:

     try{
-        if(!["approve", "reject"].includes(action)){
-            throw new Error("Invalid action");
-        }
         await transport.sendMail(options);
     }catch(err){
-        throw new Error(err);
+        throw err;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/email.service.js` around lines 134 - 263, Update
batchStatusSendEmail to default ccEmails and pendingApprovers to empty arrays
before calling join or map, validate action against the supported values before
constructing options or accessing Emailformat[action], and rethrow the caught
error directly with throw err to preserve its original message and stack.
backend/services/organization.service.js-50-52 (1)

50-52: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Escape regex metacharacters in email-based org lookup.

userEmail is interpolated directly into a RegExp without escaping. Email addresses commonly contain . (matches any character) and + (regex quantifier), so john.doe@example.com would also match john_doe@example.com in the database. Since getCoordinatorOrganization determines which organization a coordinator is authorized for, an incorrect match is an authorization bypass risk.

🔒 Proposed fix — escape regex metacharacters
 async function getCoordinatorOrganization(user) {
   const userEmail = String(
     user.username || user.personal_info?.email || ""
   )
     .trim()
     .toLowerCase();

+  const escapedEmail = userEmail.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
   const organization = await OrganizationalUnit.findOne({
-    "contact_info.email": new RegExp(`^${userEmail}$`, "i"),
+    "contact_info.email": new RegExp(`^${escapedEmail}$`, "i"),
   });

Alternatively, use MongoDB collation for case-insensitive exact match without regex:

   const organization = await OrganizationalUnit.findOne({
-    "contact_info.email": new RegExp(`^${userEmail}$`, "i"),
+    "contact_info.email": userEmail,
+  }).collation({ locale: "en", strength: 2 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/organization.service.js` around lines 50 - 52, Escape all
regular-expression metacharacters in userEmail before constructing the RegExp
used by getCoordinatorOrganization’s OrganizationalUnit.findOne lookup, while
preserving the existing case-insensitive exact-match behavior. Ensure the
escaped value cannot broaden the query beyond the literal email address.

Source: Linters/SAST tools

frontend/src/Components/Requests/Requests.jsx-108-121 (1)

108-121: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Error responses displayed as success toasts and can crash the component

approveBatch and rejectBatch return error message strings on failure (from err.response?.data.message). response && toast.success(response) shows these as green success toasts. Additionally, fetchBatches can return a string on error, and setRequests(string) will crash the next render when filteredRequests() calls .filter() on a non-array.

🛡️ Proposed fix: guard setRequests and add error toast
   const approve = async function (batch) {
     const response = await approveBatch(batch);
-    console.log("Approve Batch ", response);
-    response && toast.success(response);
+    if (response) toast.success(response);
+    else toast.error("Approval failed");
     const updated = await fetchBatches(isUserLoggedIn?._id);
-    updated && setRequests(updated);
+    if (Array.isArray(updated)) setRequests(updated);
+    else toast.error(updated || "Failed to refresh requests");
   };

   const reject = async function (batch) {
     const response = await rejectBatch(batch);
-    response && toast.success(response);
+    if (response) toast.success(response);
+    else toast.error("Rejection failed");
     const updated = await fetchBatches(isUserLoggedIn?._id);
-    updated && setRequests(updated);
+    if (Array.isArray(updated)) setRequests(updated);
+    else toast.error(updated || "Failed to refresh requests");
   };

Note: the root cause is that the service layer (batch.js) catches errors and returns strings instead of throwing. A follow-up should refactor the services to throw on error so callers can distinguish success from failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Requests/Requests.jsx` around lines 108 - 121, Update
the approve and reject handlers to treat service-returned error strings as
failures: show them with toast.error and only show toast.success for successful
responses. Guard the fetchBatches result before calling setRequests, updating
state only when the result is an array so filteredRequests remains safe.
frontend/src/Components/Requests/editModal.jsx-88-102 (1)

88-102: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Error responses shown as success; modal closes unconditionally on failure

approverEditBatch returns error strings on failure, displayed as success toasts via response && toast.success(response). fetchBatches can return a string on error, crashing setRequests. onClose() is called regardless of success or failure, closing the modal even when the save fails — the user assumes the update succeeded.

🛡️ Proposed fix: guard setRequests and prevent close on failure
   const onSave = async () => {
     if (selected.size === 0) {
       toast.error("Please select at least one user");
       return;
     }

     const response = await approverEditBatch({
       ...form,
       users: Array.from(selected),
     });
-    response && toast.success(response);
+    if (!response) {
+      toast.error("Failed to update batch");
+      return;
+    }
+    toast.success(response);
     const updated = await fetchBatches(isUserLoggedIn?._id);
-    updated && setRequests(updated);
-    onClose();
+    if (Array.isArray(updated)) setRequests(updated);
+    onClose();
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Requests/editModal.jsx` around lines 88 - 102, Update
onSave to distinguish successful results from error strings returned by
approverEditBatch and fetchBatches: show failures with an error toast, only call
setRequests when fetchBatches returns valid batch data, and call onClose only
after the edit and refresh both succeed. Preserve the existing validation for an
empty selected set.
frontend/src/Components/Requests/viewModal.jsx-35-43 (1)

35-43: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Replace the runtime Tailwind color classes with static styling In frontend/src/Components/Requests/viewModal.jsx:35-43 and :87-99, text-[${C.text}] / text-[${C.warmGray}] are interpolated at runtime, so Tailwind won’t generate those utilities. Use inline style={{ color: ... }} or static class names instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Requests/viewModal.jsx` around lines 35 - 43, Replace
the runtime Tailwind color classes in the value display and the corresponding
section around the second referenced block with static styling. Remove
text-[${C.text}] and text-[${C.warmGray}] usage, applying the respective C.text
and C.warmGray values through inline color styles or existing static color
classes while preserving the current typography classes.
🟡 Minor comments (13)
frontend/src/Components/Certificates/CertificatesList.jsx-127-127 (1)

127-127: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

!rounded-2xl uses Tailwind v3 important syntax; v4 requires rounded-2xl!.

Per Tailwind CSS v4 breaking changes, the ! important modifier moved from prefix to suffix. The project uses Tailwind v4.0.3.

💚 Proposed fix
-            className={`px-4 h-[40px] !rounded-2xl font-semibold transition-all duration-200 ${
+            className={`px-4 h-[40px] rounded-2xl! font-semibold transition-all duration-200 ${
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Certificates/CertificatesList.jsx` at line 127,
Update the button className in CertificatesList so the Tailwind v4 important
modifier uses the suffix form for rounded-2xl, preserving the existing styling
and other classes.
frontend/src/Components/Certificates/CertificatesList.jsx-29-34 (1)

29-34: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Non-array response silently shows empty list with no error feedback.

If fetchCertificates returns a non-array (e.g., undefined on network error, or an error message string), the code falls through without setting error or certificates. The user sees an empty list with no indication something went wrong.

💚 Proposed fix
        const response = await fetchCertificates();
        if(Array.isArray(response)){
          toast.success("Certificates fetched successfully");
          setCertificates(response);
          return ;
        }
+       setError("Failed to fetch certificates");
      } catch (err) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Certificates/CertificatesList.jsx` around lines 29 -
34, Update the fetchCertificates handling in the Certificates list component so
every non-array response is treated as a failure: set the component’s error
state and ensure certificates is reset to an empty list, while preserving the
existing success path for array responses. Use the component’s existing error
state and user-facing feedback mechanism rather than silently falling through.
backend/controllers/certificateBatchController.js-271-273 (1)

271-273: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Duplicate title logic produces inconsistent and incorrect results.

Splitting on "(Copy)" and using array.length - 1 as the count doesn't track actual copy numbers. For a title "My Title", the first duplicate becomes "My Title Copy(0)". Duplicating that gives "My Title Copy(0) Copy(0)" instead of "My Title Copy(1)". The separator "(Copy)" also doesn't match the output format "Copy(n)".

💚 Proposed fix
-    const array = batch.title.split("(Copy)");
-    const count = array.length - 1;
-    const title = `${array[0]} Copy(${count})`;
+    const baseTitle = batch.title.replace(/\s*Copy\(\d+\)\s*$/, "").trim();
+    const existingCopies = await CertificateBatch.countDocuments({
+      title: { $regex: `^${baseTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*Copy\\(\\d+\\)$` },
+      initiatedBy: id,
+    });
+    const title = `${baseTitle} Copy(${existingCopies + 1})`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/certificateBatchController.js` around lines 271 - 273,
Update the duplicate-title generation near the batch title handling to parse an
existing trailing “Copy(n)” suffix and increment its numeric value, defaulting
the first duplicate to Copy(1). Preserve the original base title when no suffix
exists, and ensure repeated duplication produces a single correctly incremented
Copy(n) suffix rather than splitting on “(Copy)”.
frontend/src/Components/Batches/studentPanel.jsx-46-46 (1)

46-46: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove debug console.log statement.

Line 46 has console.log("studentIds:", studentIds) left from debugging. This should be removed before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Batches/studentPanel.jsx` at line 46, Remove the
debug console.log statement that prints studentIds from the student panel
component, leaving the surrounding logic unchanged.
frontend/src/services/batch.js-26-26 (1)

26-26: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove console.log debug statements.

Lines 26 and 91 contain console.log calls that should be removed before merge.

Also applies to: 91-91

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/services/batch.js` at line 26, Remove the debug console.log
calls at both locations in the batch service, including the “Response” log and
the statement near the second referenced location, while leaving the surrounding
request handling unchanged.
frontend/src/Components/Batches/modalDialog.jsx-131-138 (1)

131-138: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against undefined signatoryDetails to prevent runtime crash.

If form.signatoryDetails is undefined when the component mounts, form.signatoryDetails.length on line 132 will throw a TypeError. Additionally, line 276 (form.signatoryDetails.map) would crash during render before the effect even runs. Use optional chaining for safety.

🛡️ Proposed fix
   useEffect(() => {
-    if (form.signatoryDetails.length === 0) {
+    if (!form.signatoryDetails || form.signatoryDetails.length === 0) {
       setForm((prev) => ({
         ...prev,
         signatoryDetails: [{ name: "", role: "" }],
       }));
     }
   }, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Batches/modalDialog.jsx` around lines 131 - 138,
Update the signatoryDetails handling in the component’s useEffect and render
mapping to safely support an undefined value: use optional chaining when
checking its length and when invoking map. Preserve initialization to the
default signatory entry when the value is absent or empty.
frontend/src/Components/Batches/ui.jsx-127-128 (1)

127-128: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

CertThumb uses Math.random() on every render, causing color flicker.

The thumbnail color changes randomly on every parent re-render. Use a deterministic index based on the batch ID instead.

🔧 Proposed deterministic color selection
 export function CertThumb({ batch }) {
-  const colorIndex = Math.floor(Math.random() * BATCH_COLORS.length);
-  const c = BATCH_COLORS[colorIndex % BATCH_COLORS.length];
+  const idSum = (batch?._id || batch?.title || "").split("").reduce((a, ch) => a + ch.charCodeAt(0), 0);
+  const c = BATCH_COLORS[idSum % BATCH_COLORS.length];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Batches/ui.jsx` around lines 127 - 128, Update the
color selection in CertThumb to derive a stable index deterministically from the
batch ID instead of calling Math.random() during rendering. Preserve the
existing BATCH_COLORS selection and ensure the same batch ID consistently
produces the same thumbnail color across re-renders.
backend/services/certificates.service.js-42-49 (1)

42-49: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Contradictory certificate type and title.

certificateType is "Certificate of Participation" while certificateTitle is "Certificate of Achievement". These render in different sections of the same PDF — the small label says "Participation" and the large heading says "Achievement". Pick one category or make them configurable per batch/template.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/certificates.service.js` around lines 42 - 49, Align
certificateType and certificateTitle in the certificate data constructed by the
certificate-generation method so both represent the same category, or derive
them from the same batch/template configuration. Preserve the existing
recipient, description, and certificate identifier fields.
backend/models/certificateSchema.js-43-54 (1)

43-54: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

signature default evaluates to undefined at schema definition time.

default: this.name is evaluated when the object literal is created, not when a subdocument is instantiated. At that point this is the module scope, so this.name is undefined. Use a function so Mongoose binds this to the subdocument being created.

🛠️ Proposed fix
           name: { type: String, required: true },
-          signature: { type: String, default: this.name },
+          signature: { type: String, default: function() { return this.name; } },
           role: { type: String, required: true },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/models/certificateSchema.js` around lines 43 - 54, Update the
signature field definition within signatoryDetails so its default is provided by
a function evaluated for each subdocument, with this bound to that subdocument
and returning its name. Keep the existing required and lifecycleStatus behavior
unchanged.
backend/utils/cloudinary.js-20-24 (1)

20-24: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Local PDF is deleted even when upload fails, preventing retry.

The finally block deletes the local file regardless of upload success. If the upload fails, the PDF is lost and must be regenerated. Consider deleting only on success, or moving cleanup to the caller.

🛠️ Proposed fix: delete only on success
     try {
         const result = await cloudinary.uploader.upload(pdfPath, {
             resource_type: "raw",
             folder: "certificates",
             timeout: 120000,
         });
 
+        if (fs.existsSync(pdfPath)) {
+            fs.unlinkSync(pdfPath);
+        }
         return result.secure_url;
     } catch (err) {
         console.error("Cloudinary upload failed:");
         console.dir(err, { depth: null });
         throw err;
-    } finally {
-        if (fs.existsSync(pdfPath)) {
-            fs.unlinkSync(pdfPath);
-        }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/utils/cloudinary.js` around lines 20 - 24, Update the cleanup flow
surrounding the Cloudinary upload so the local PDF is deleted only after a
successful upload; preserve the file when the upload throws or otherwise fails,
allowing retry. Adjust the existing finally-block logic and any success tracking
in the upload function rather than changing unrelated behavior.
backend/utils/batchValidate.js-6-18 (1)

6-18: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix misleading validation message and typos in error strings.

Line 12 enforces min(3) but the error message says "at least 5 characters" — users entering a 3–4 character name will see a confusing message. Additionally, "atleast" should be "at least" on lines 6, 12, and 18. These messages are surfaced directly to API callers via validation.error.issues.

✏️ Proposed fix
   title: zod.string().min(5, "Title should be at least 5 characters"),
   eventId: zodObjectId,
   templateId: zodObjectId,
   signatoryDetails: zod
     .array(
       zod.object({
-        name: zod.string().min(3, "Name must be atleast 5 characters"),
+        name: zod.string().min(3, "Name must be at least 3 characters"),
         signature: zod.string().optional(),
         role: zod.string().min(1, "Invalid position"),
       }),
     )
     .nonempty("At least one signatory is required"),
-  users: zod.array(zodObjectId).min(1, "Atleast 1 user must be associated."),
+  users: zod.array(zodObjectId).min(1, "At least 1 user must be associated."),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/utils/batchValidate.js` around lines 6 - 18, Update the validation
messages in the visible schema: make the signatory name message match its min(3)
constraint, and replace every “atleast” occurrence in the title, name, and users
messages with “at least,” preserving the existing validation rules.
frontend/src/Components/Requests/Card.jsx-93-93 (1)

93-93: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix invalid CSS value solidrgb in border-bottom style

"2px solidrgb(198, 194, 150)" is invalid CSS — solidrgb should be solid rgb. The browser ignores the entire declaration, so non-last rows have no visible bottom border.

🐛 Proposed fix
-                    borderBottom: last ? "none" : "2px solidrgb(198, 194, 150)",
+                    borderBottom: last ? "none" : "2px solid rgb(198, 194, 150)",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Requests/Card.jsx` at line 93, Correct the
borderBottom value in the Card component so the non-last-row style uses valid
CSS by separating the “solid” keyword from the rgb color function. Preserve the
existing “none” value for the last row.
frontend/src/Components/Requests/Requests.jsx-61-98 (1)

61-98: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Add null guards in filteredRequests and stats calculation

filteredRequests accesses req.eventId.title, req.eventId.organizing_unit_id.name, and req.initiatedBy.personal_info.name without optional chaining. The stats useEffect accesses req.users.length without a null check. If any nested object is missing from the API response, the component crashes with a TypeError.

🛡️ Proposed fix with optional chaining
-      const event = req.eventId.title.toLowerCase();
-      const organization = req.eventId.organizing_unit_id.name.toLowerCase();
-      const requestedBy = req.initiatedBy.personal_info.name.toLowerCase();
+      const event = req.eventId?.title?.toLowerCase() ?? "";
+      const organization = req.eventId?.organizing_unit_id?.name?.toLowerCase() ?? "";
+      const requestedBy = req.initiatedBy?.personal_info?.name?.toLowerCase() ?? "";
-        total += req.users.length || 0;
+        total += req.users?.length || 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/Components/Requests/Requests.jsx` around lines 61 - 98, Update
filteredRequests to safely access nested request fields through optional
chaining or equivalent fallbacks before lowercasing, and guard req.users when
calculating approved request totals in the stats useEffect. Preserve the
existing search matching and status-count behavior when the API provides
complete data.

Comment thread backend/controllers/templateController.js Outdated
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