Implement Admin Gym Class management feature#42
Conversation
- Add `AdminDashboard.tsx` for viewing, filtering, creating, editing, and canceling gym classes. - Develop `AdminGymClassService`, `AdminGymClassController`, and related domain logic for backend functionality. - Add `AdminGymClassResponse` for structured API responses. - Implement API routes: list, create, update, and cancel gym classes. - Add tests for `AdminGymClassService`. - Integrate styles and filter logic for class management UI.
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughAdds an admin dashboard feature enabling admin users to view, create, edit, and cancel gym classes through a paginated UI with status filtering. Includes backend service layer, API endpoints, admin user seeding via configuration, and comprehensive test coverage. Changes
Sequence Diagram(s)sequenceDiagram
participant Admin as Admin User
participant UI as Frontend UI
participant API as API Controller
participant Service as Admin Service
participant Repo as Repository
participant Port as Provider Port
Admin->>UI: View Admin Dashboard
UI->>API: GET /api/admin/classes?status=SCHEDULED
API->>Service: getAllClasses(status, pageable)
Service->>Repo: findByStatus(SCHEDULED, pageable)
Repo-->>Service: Page<GymClass>
Service->>Port: getAllInstructors()
Port-->>Service: List<InstructorDetails>
Service-->>API: Page<AdminGymClassResponse>
API-->>UI: Classes with enriched data
UI-->>Admin: Display paginated class list
Admin->>UI: Click Edit Class
UI-->>Admin: Show ClassFormModal with class data
Admin->>UI: Update capacity & end time, submit
UI->>API: PUT /api/admin/classes/{id}
API->>Service: updateClass(id, UpdateGymClassRequest)
Service->>Repo: findById(id)
Repo-->>Service: GymClass
Service->>Service: Validate times & refs
Service->>Repo: save(updated GymClass)
Repo-->>Service: Updated GymClass
Service-->>API: AdminGymClassResponse
API-->>UI: 200 OK
UI->>UI: Close modal, refresh list
UI-->>Admin: Updated class visible
sequenceDiagram
participant Admin as Admin User
participant UI as Frontend UI
participant API as API Controller
participant Service as Admin Service
participant Repo as Repository
Admin->>UI: Click Cancel Class
UI->>UI: Show confirmation dialog
Admin->>UI: Confirm cancellation
UI->>API: DELETE /api/admin/classes/{id}
API->>Service: cancelClass(id)
Service->>Repo: findById(id)
Repo-->>Service: GymClass
Service->>Service: Verify not already CANCELLED
Service->>Repo: save(GymClass with CANCELLED)
Repo-->>Service: Success
Service-->>API: void
API-->>UI: 204 No Content
UI->>UI: Remove from list or refresh
UI-->>Admin: Class marked cancelled
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 58 minutes and 28 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (7)
frontend/src/App.tsx (1)
95-108: Optional: factor out the repeated nav-button styling.The My Bookings / Settings / Admin buttons share the exact same
className,styleshape, and active-state logic; only the label and target path differ. Extracting a smallNavButton(or using a CSS class toggled onpathnamematch) would remove the repetition and make future nav additions cheap.♻️ Example extraction
const NavButton = ({ to, label }: { to: string; label: string }) => ( <button type="button" className="nav-link" onClick={() => navigate(to)} style={{ fontWeight: pathname === to ? 'bold' : 'normal', color: pathname === to ? '#ff1493' : '#333', marginRight: '15px', }} > {label} </button> );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/App.tsx` around lines 95 - 108, Extract the repeated nav-button markup inside App (the Admin / My Bookings / Settings buttons) into a small reusable NavButton component (or create a CSS class toggled by active state); NavButton should accept props like to and label, use the existing navigate and pathname logic to set the onClick and active styling (fontWeight and color, plus marginRight), and replace each inline button with <NavButton to="..." label="..."> so the className, inline style shape, and active-state determination are centralized.src/main/java/com/example/boka/gymclass/application/AdminGymClassResponse.java (1)
11-12: Consider exposingstartTime/endTimeasLocalDateTimerather thanString.Mapping via
LocalDateTime.toString()bypasses Jackson'sJavaTimeModule/@JsonFormatso the wire format can drift from other endpoints, and clients lose the benefit of a typed, consistently serialized ISO-8601 value. Keeping the type asLocalDateTime(orOffsetDateTime) lets serialization be centrally configured and keeps the admin API consistent with the rest of the codebase.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/example/boka/gymclass/application/AdminGymClassResponse.java` around lines 11 - 12, Change the AdminGymClassResponse fields startTime and endTime from String to java.time.LocalDateTime (or OffsetDateTime if you need timezone) and update their declarations, constructor parameters, getters/setters (or records/ builder) in AdminGymClassResponse so types match; add the appropriate import (java.time.LocalDateTime) and, if necessary, annotate with `@JsonFormat` or ensure the module registration is used consistently with other DTOs so Jackson's JavaTimeModule handles ISO‑8601 serialization/deserialization.src/test/java/com/example/boka/gymclass/application/AdminGymClassServiceTest.java (1)
153-175: Redundant stubbing.
instructorProviderPort.findInstructorById(INSTRUCTOR_ID)is stubbed on both line 157 and line 162 with identical return values — the second call just overrides the first to the same value. Same pattern is duplicated inupdateClass_Successful_UpdatesAllFields(lines 231 and 235). Drop the duplicate line in each to keep the test intent crisp. Also, withMockitoExtensionusing strict stubbing by default, duplicate stubs can occasionally surface as "unnecessary stubbing" warnings.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/java/com/example/boka/gymclass/application/AdminGymClassServiceTest.java` around lines 153 - 175, Remove the redundant Mockito stubbing of instructorProviderPort.findInstructorById(INSTRUCTOR_ID) in the test methods; keep a single when(instructorProviderPort.findInstructorById(INSTRUCTOR_ID)).thenReturn(Optional.of(instructor())) per test (e.g., in createClass_Successful and updateClass_Successful_UpdatesAllFields) so the duplicate call that re-stubs the same return value is deleted and only one stub remains before invoking adminGymClassService.createClass/updateClass.src/main/java/com/example/boka/gymclass/infrastructure/AdminGymClassController.java (1)
66-69: Unpaginated/api/admin/instructors.The endpoint returns every instructor in one shot. Fine for small fleets, but if the instructor population can grow, consider pagination or at least a hard cap — the admin UI is the only known consumer today and it just populates a
<select>, so this is informational rather than blocking.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/example/boka/gymclass/infrastructure/AdminGymClassController.java` around lines 66 - 69, The getInstructors method in AdminGymClassController currently returns all instructors via instructorProviderPort.getAllInstructors() which can scale poorly; modify getInstructors to accept pagination or limit parameters (e.g., page/size or a maxResults cap) and update InstructorProviderPort to expose a paginated method (or a method like getInstructors(int limit, int offset)) that returns a subset of InstructorProviderPort.InstructorDetails; enforce a sensible hard cap on size if no paging is provided and document the new params so the admin UI can request only what it needs for the <select>.frontend/src/admin/AdminDashboard.tsx (1)
56-66: Checkres.okbefore parsing reference-data responses.Each fetch calls
r.json()unconditionally. A 401/403/500 response (e.g. admin session expired) may return HTML or a non-JSON error body, causing a parse error that's caught as a generic "Failed to load reference data" toast and hides the underlying issue. Prefer validatingres.okper request and surfacing a more specific message.♻️ Proposed change
- Promise.all([ - fetch('/api/admin/class-types').then(r => r.json()), - fetch('/api/admin/instructors').then(r => r.json()), - fetch('/api/gyms?size=100').then(r => r.json()), - ]).then(([cts, insts, gymsData]) => { + const getJson = async (url: string) => { + const r = await fetch(url); + if (!r.ok) throw new Error(`${url} -> ${r.status}`); + return r.json(); + }; + Promise.all([ + getJson('/api/admin/class-types'), + getJson('/api/admin/instructors'), + getJson('/api/gyms?size=100'), + ]).then(([cts, insts, gymsData]) => {Also,
/api/gyms?size=100silently truncates beyond 100 gyms — fine for now, but consider paginating if the fleet can grow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/admin/AdminDashboard.tsx` around lines 56 - 66, In the useEffect that loads reference data, don't call r.json() unconditionally in the Promise.all fetches; instead check each Response.ok (for the three calls used in useEffect) and if not ok throw or return a rejected Promise with the status/text so the catch can surface a clearer error; update the three fetches (the ones that feed setClassTypes, setInstructors, and setGyms) to validate res.ok before parsing, and ensure you still handle gymsData.content ?? gymsData when assigning setGyms so existing fallback logic remains.src/main/java/com/example/boka/gymclass/application/AdminGymClassService.java (1)
149-196: Reduce duplication betweenenrich(...)andtoResponse(...).The per-row mapping in
enrich(lines 149-171) andtoResponse(lines 181-195) are identical except for how the instructor is resolved (batch vs single lookup). Extracting the mapping into a privatebuildResponse(GymClass gc, String instructorName, int bookingCount)would remove the duplicated field-by-field construction and make futureAdminGymClassResponseadditions a one-line change.Also minor:
gc.getCapacity()is unboxed for theMath.max(0, capacity - bookingCount)arithmetic in both paths — if a legacy row has a null capacity this NPEs. Defending withgc.getCapacity() != null ? Math.max(0, gc.getCapacity() - bookingCount) : 0(or asserting non-null at the entity level) avoids the crash.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/example/boka/gymclass/application/AdminGymClassService.java` around lines 149 - 196, Extract the duplicated AdminGymClassResponse construction into a private helper buildResponse(GymClass gc, String instructorName, int bookingCount) and have both the page.map(...) lambda in enrich(...) and toResponse(...) call buildResponse(...) instead of duplicating field-by-field construction; in buildResponse compute remaining capacity defensively (e.g. if gc.getCapacity() is null return 0 else Math.max(0, gc.getCapacity() - bookingCount)) so null capacities don't NPE, and keep toResponse only responsible for resolving the instructorName (call instructorProviderPort.findInstructorById(...).orElse(null) and pass the resolved name into buildResponse).src/main/java/com/example/boka/gymclass/application/UpdateGymClassRequest.java (1)
9-16: Consider symmetry betweenstartTimeandendTimevalidation.
startTimeuses@FutureOrPresentbutendTimeuses@Future. Since the service enforcesstartTime < endTimeanyway, the asymmetry is functionally fine, but intent is clearer if both use@FutureOrPresent(the service boundary check catches the ordering). Minor consistency nit — feel free to ignore if the asymmetry was intentional.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/example/boka/gymclass/application/UpdateGymClassRequest.java` around lines 9 - 16, The annotations on UpdateGymClassRequest are asymmetric: startTime is annotated with `@FutureOrPresent` while endTime uses `@Future`; update the endTime annotation to `@FutureOrPresent` so both startTime and endTime use the same validation annotation (change the `@Future` on the endTime parameter to `@FutureOrPresent`) while leaving the service-level ordering check intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/admin/ClassFormModal.tsx`:
- Around line 100-103: The client currently expects a top-level data.message
after a non-OK response in ClassFormModal.tsx (the else branch handling
res.json()), but Spring's validation 400 uses a BindingResult shape so
data.message is often undefined; update the error handling where res is
processed (in the submit/response handler in ClassFormModal.tsx) to extract a
user-friendly message by checking data.errors?.[0]?.defaultMessage,
data.fieldErrors?.[0]?.defaultMessage, or data.message in that order and pass
the resulting string to toast.error (with the existing generic fallback).
Optionally note that the controller (Create/Update endpoints) could instead map
MethodArgumentNotValidException to {"message": "..."} via an `@ExceptionHandler`,
but implement the client-side parsing change to ensure validation messages
surface to users.
- Around line 48-57: The helper functions toDatetimeLocal and addMinutes are
wrong: remove the no-op .replace in toDatetimeLocal and make both functions
produce a timezone-free "YYYY-MM-DDTHH:MM" local datetime string. Fix by parsing
the input into local date components (year, month, day, hours, minutes), use
Date setters/getters (e.g., create Date(year, month-1, day, hour, minute)), for
addMinutes call setMinutes(getMinutes()+minutes) on that local Date, and then
format the result using local getters pad‑to‑2 digits into "YYYY-MM-DDTHH:MM".
Update the implementations of toDatetimeLocal and addMinutes accordingly so they
never call toISOString() and always return a local "datetime-local" string.
In `@src/main/java/com/example/boka/config/DataSeeder.java`:
- Around line 127-147: upsertAdminUser currently re-encodes and writes
adminPassword on every startup; change it so existing users are not overwritten
unless an explicit opt-in flag is set and avoid unnecessary writes: in
upsertAdminUser, when userRepository.findByEmail(adminEmail) returns an existing
user, first check passwordEncoder.matches(adminPassword,
existing.getPasswordHash()) and if it matches do nothing; if it does not match,
only overwrite/save when a new boolean property (e.g. adminPasswordForceSync or
admin.password.force-sync) is true (otherwise log/skips), and when creating a
new User still set the password as before; this ensures creation-once bootstrap
semantics by default and an explicit force-sync path when needed.
- Around line 48-52: Remove the hardcoded fallback for adminPassword and enforce
a safer behavior in DataSeeder: change the `@Value` usage so ADMIN_PASSWORD is not
defaulted to "password123" (make it required or nullable), inject Spring
Environment into DataSeeder, and in upsertAdminUser() check if adminPassword is
null/blank; if the active profile is not "dev" or "test" then throw an
IllegalStateException to fail startup (or alternatively annotate
DataSeeder/upsertAdminUser() with `@Profile`("dev") to restrict seeding to dev
only); if you choose to keep a fallback for local dev, replace the default with
a dev-only guard and emit a clear WARN via the class logger when the default is
being used.
In
`@src/main/java/com/example/boka/gymclass/application/AdminGymClassService.java`:
- Around line 120-131: cancelClass currently sets GymClass.status to
ClassStatus.CANCELLED via gymClassRepository.save(gymClass) but does not notify
or update bookings; add a booking-side notification by extending the
BookingProviderPort with a cancelBookingsForClass(Long classId) (or similarly
named) and invoke bookingProviderPort.cancelBookingsForClass(id) from
cancelClass (inside the same `@Transactional` flow, after setting status and
before/after save as per desired semantics) so that confirmed bookings are
cancelled/marked notified; ensure BookingProviderPort is injected into
AdminGymClassService and handle/propagate any errors consistently with existing
exception behavior.
- Around line 107-113: When updating capacity in updateClass, reject any
requested capacity that is less than the current confirmed booking count to
avoid creating an overbooked state: if req.capacity() != null, compute the
current confirmed booking count from the gymClass (e.g.,
gymClass.getConfirmedBookingCount() or gymClass.getBookings().size()) and if
req.capacity() < confirmedCount throw an IllegalArgumentException (with a clear
message like "Capacity cannot be lower than current confirmed bookings"); keep
the existing start/end time validation intact.
In
`@src/main/java/com/example/boka/gymclass/infrastructure/AdminGymClassController.java`:
- Around line 38-54: The controller currently only catches
IllegalArgumentException/IllegalStateException so validation failures from
`@Valid` on CreateGymClassRequest and UpdateGymClassRequest throw
MethodArgumentNotValidException and return Spring's default shape; add a handler
that converts MethodArgumentNotValidException into the same {"message": "..."}
payload: either add an `@ExceptionHandler`(MethodArgumentNotValidException.class)
method inside AdminGymClassController or create a `@RestControllerAdvice` that
handles MethodArgumentNotValidException, extract and join field error messages
(from ex.getBindingResult().getFieldErrors()) into a single string, and return
ResponseEntity.badRequest().body(Map.of("message", joinedMessage)) so clients
expecting the top-level "message" keep working.
- Around line 28-36: Clamp the incoming page size to a safe maximum in
AdminGymClassController#getClasses to prevent unbounded requests: validate the
request param size (and optionally page) before creating the PageRequest (e.g.,
enforce size = Math.min(Math.max(size, 1), MAX_PAGE_SIZE) where MAX_PAGE_SIZE is
a meaningful constant like 100), then call PageRequest.of(page, size,
Sort.by(...)) and pass that to adminGymClassService.getAllClasses(status,
pageable); alternatively replace the int params with Spring's Pageable using
`@PageableDefault`(size = 15) and configure spring.data.web.pageable.max-page-size
in configuration, but ensure the controller references and uses the bounded
value when constructing the PageRequest.
---
Nitpick comments:
In `@frontend/src/admin/AdminDashboard.tsx`:
- Around line 56-66: In the useEffect that loads reference data, don't call
r.json() unconditionally in the Promise.all fetches; instead check each
Response.ok (for the three calls used in useEffect) and if not ok throw or
return a rejected Promise with the status/text so the catch can surface a
clearer error; update the three fetches (the ones that feed setClassTypes,
setInstructors, and setGyms) to validate res.ok before parsing, and ensure you
still handle gymsData.content ?? gymsData when assigning setGyms so existing
fallback logic remains.
In `@frontend/src/App.tsx`:
- Around line 95-108: Extract the repeated nav-button markup inside App (the
Admin / My Bookings / Settings buttons) into a small reusable NavButton
component (or create a CSS class toggled by active state); NavButton should
accept props like to and label, use the existing navigate and pathname logic to
set the onClick and active styling (fontWeight and color, plus marginRight), and
replace each inline button with <NavButton to="..." label="..."> so the
className, inline style shape, and active-state determination are centralized.
In
`@src/main/java/com/example/boka/gymclass/application/AdminGymClassResponse.java`:
- Around line 11-12: Change the AdminGymClassResponse fields startTime and
endTime from String to java.time.LocalDateTime (or OffsetDateTime if you need
timezone) and update their declarations, constructor parameters, getters/setters
(or records/ builder) in AdminGymClassResponse so types match; add the
appropriate import (java.time.LocalDateTime) and, if necessary, annotate with
`@JsonFormat` or ensure the module registration is used consistently with other
DTOs so Jackson's JavaTimeModule handles ISO‑8601 serialization/deserialization.
In
`@src/main/java/com/example/boka/gymclass/application/AdminGymClassService.java`:
- Around line 149-196: Extract the duplicated AdminGymClassResponse construction
into a private helper buildResponse(GymClass gc, String instructorName, int
bookingCount) and have both the page.map(...) lambda in enrich(...) and
toResponse(...) call buildResponse(...) instead of duplicating field-by-field
construction; in buildResponse compute remaining capacity defensively (e.g. if
gc.getCapacity() is null return 0 else Math.max(0, gc.getCapacity() -
bookingCount)) so null capacities don't NPE, and keep toResponse only
responsible for resolving the instructorName (call
instructorProviderPort.findInstructorById(...).orElse(null) and pass the
resolved name into buildResponse).
In
`@src/main/java/com/example/boka/gymclass/application/UpdateGymClassRequest.java`:
- Around line 9-16: The annotations on UpdateGymClassRequest are asymmetric:
startTime is annotated with `@FutureOrPresent` while endTime uses `@Future`; update
the endTime annotation to `@FutureOrPresent` so both startTime and endTime use the
same validation annotation (change the `@Future` on the endTime parameter to
`@FutureOrPresent`) while leaving the service-level ordering check intact.
In
`@src/main/java/com/example/boka/gymclass/infrastructure/AdminGymClassController.java`:
- Around line 66-69: The getInstructors method in AdminGymClassController
currently returns all instructors via instructorProviderPort.getAllInstructors()
which can scale poorly; modify getInstructors to accept pagination or limit
parameters (e.g., page/size or a maxResults cap) and update
InstructorProviderPort to expose a paginated method (or a method like
getInstructors(int limit, int offset)) that returns a subset of
InstructorProviderPort.InstructorDetails; enforce a sensible hard cap on size if
no paging is provided and document the new params so the admin UI can request
only what it needs for the <select>.
In
`@src/test/java/com/example/boka/gymclass/application/AdminGymClassServiceTest.java`:
- Around line 153-175: Remove the redundant Mockito stubbing of
instructorProviderPort.findInstructorById(INSTRUCTOR_ID) in the test methods;
keep a single
when(instructorProviderPort.findInstructorById(INSTRUCTOR_ID)).thenReturn(Optional.of(instructor()))
per test (e.g., in createClass_Successful and
updateClass_Successful_UpdatesAllFields) so the duplicate call that re-stubs the
same return value is deleted and only one stub remains before invoking
adminGymClassService.createClass/updateClass.
🪄 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: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 29922489-a915-4479-86b0-695d40b09aca
📒 Files selected for processing (16)
frontend/src/App.tsxfrontend/src/admin/AdminDashboard.tsxfrontend/src/admin/ClassFormModal.tsxsrc/main/java/com/example/boka/config/DataSeeder.javasrc/main/java/com/example/boka/config/SpaController.javasrc/main/java/com/example/boka/gymclass/InstructorProviderPort.javasrc/main/java/com/example/boka/gymclass/application/AdminGymClassResponse.javasrc/main/java/com/example/boka/gymclass/application/AdminGymClassService.javasrc/main/java/com/example/boka/gymclass/application/ClassTypeResponse.javasrc/main/java/com/example/boka/gymclass/application/CreateGymClassRequest.javasrc/main/java/com/example/boka/gymclass/application/UpdateGymClassRequest.javasrc/main/java/com/example/boka/gymclass/domain/GymClassRepository.javasrc/main/java/com/example/boka/gymclass/infrastructure/AdminGymClassController.javasrc/main/java/com/example/boka/security/SecurityConfig.javasrc/main/java/com/example/boka/user/infrastructure/InstructorProviderAdapter.javasrc/test/java/com/example/boka/gymclass/application/AdminGymClassServiceTest.java
…curacy - Add `pad()` helper to ensure proper zero-padding for single-digit date-time components. - Update `toDatetimeLocal()` to use `Date` object for reliable ISO-to-local conversion. - Refactor `addMinutes()` to improve date-time parsing logic.
- Add capacity check in `AdminGymClassService` to prevent capacity below confirmed bookings, throwing `IllegalArgumentException` if violated. - Introduce cancellation of class bookings upon gym class cancellation. - Refactor admin password logic in `DataSeeder` to enforce `ADMIN_PASSWORD` for non-dev/test, add force-sync support, and use an insecure fallback for dev/test environments. - Improve error messaging in `ClassFormModal.tsx` toast notifications. - Add new `BookingRepository` query for booking cancellations and implement `BookingProviderPort.cancelBookingsForClass`.
- Introduce `GlobalExceptionHandler` to handle `MethodArgumentNotValidException`. - Return structured validation error messages with bad request status.
AdminDashboard.tsxfor viewing, filtering, creating, editing, and canceling gym classes.AdminGymClassService,AdminGymClassController, and related domain logic for backend functionality.AdminGymClassResponsefor structured API responses.AdminGymClassService.Summary by CodeRabbit