# PRD: Calendar Settings & User Settings — Fix 18 Failed Test Cases

**Date:** 2026-04-28  
**Module:** CRM > Calendar Settings / User Settings  
**Test Cases:** US-002, US-014, US-025, US-030, US-031, US-039, US-043, US-053, US-087, US-101, US-102, US-108, US-117, US-122, US-129, US-131, US-132, US-140

---

## Context

18 QA test cases are failing across the User Settings and Calendar Settings tabs. Root cause analysis reveals five distinct failure patterns:

1. **Ajax-form vs redirect mismatch** — Forms use the `ajax-form` jQuery class (which submits via AJAX and expects a JSON response), but their controllers return `redirect()` responses. AJAX calls follow the redirect to an HTML page; the plugin cannot parse flash messages from HTML, so no error or success feedback is shown to the user. This is the single most widespread root cause.
2. **CSS class mis-targeting on checkboxes** — The `mobileapp_checkbox` CSS class (used by `notification-settings.js`) is declared inside the field's `attributes` array, which the form wrapper may apply to a container div rather than the `<input>` element itself, breaking the All/None JS toggle.
3. **Breadcrumb not rendered** — `$breadcrumb` is set in controller data but the `index()` route path skips it; tab is inactive on that entry point.
4. **UX / display gaps** — Password form has no visual indicator that a password already exists; Terms of Use link falls back to `#`.
5. **Missing CSS for responsive layout and hover states** — Notification table overflows on zoom; All/None hover state lacks sufficient visual feedback.

---

## Files to Be Modified

| File | Test Cases |
|------|-----------|
| `app/Http/Controllers/Calendar/CalendarSettingsController.php` | US-129, US-131, US-132, US-140 |
| `app/Http/Controllers/Manages/UserSettingsController.php` | US-002, US-014, US-025, US-030, US-031, US-039, US-043, US-053, US-087, US-101, US-102, US-108, US-117, US-122 |
| `resources/views/admin/manages/users/notification-settings.blade.php` | US-087, US-101, US-102, US-108, US-117, US-122 |
| `resources/views/admin/manages/users/profile-settings.blade.php` | US-039, US-053 |
| `public/assets/js/notification-settings.js` | US-087, US-101, US-102, US-108, US-117, US-122 |
| `resources/views/admin/settings/settings.blade.php` | US-002 |
| `.env` (environment config) | US-053 |

---

## Fix 1 — US-002: Breadcrumb and active tab not shown on initial entry

### Root Cause
`UserSettingsController::index()` sets `$tab = 'user'` but does NOT set `$this->_data['breadcrumb']` or call `__formUiGeneration()`. The profile settings partial (which requires `$form1`/`$form2`) is rendered empty. When navigating via the `user-settings.index` route, the breadcrumb is missing and the form content does not load.

### Changes

**`app/Http/Controllers/Manages/UserSettingsController.php` — `index()` method**

```php
// BEFORE
public function index(Request $request)
{
    $this->initIndex();
    $this->_data['title'] = '';
    $this->_data['subTitle'] = 'User Settings';
    $this->_data['tab'] = 'user';
    return view('admin.settings.settings', $this->_data);
}

// AFTER — redirect index to the real profile-settings route
public function index(Request $request)
{
    return redirect()->route($this->_routePrefix . '.profile-settings');
}
```

This ensures any hit to `user-settings.index` always renders the full profile settings page with breadcrumb and form content, matching what `profileSettings()` already produces.

---

## Fix 2 — US-014: No validation feedback for name/position over max length

### Root Cause
`profileSettingsStore()` calls `User::storeProfileSettings()` which returns `['status' => 400, 'message' => '...']` on validation failure. The controller then does `redirect()->with('error', ...)`. The form uses `ajax-form` class — AJAX gets a 200 HTML response after following the redirect; the plugin cannot extract the flash error, so no message is displayed.

### Changes

**`app/Http/Controllers/Manages/UserSettingsController.php` — `__profileFormPost()`**

```php
// BEFORE
protected function __profileFormPost(Request $request, $id = 0)
{
    $input = $request->all();
    $response = $this->_model->storeProfileSettings($input, $id, $request);
    if (in_array($response['status'], [200, 201])) {
        return redirect()->route(...)->with('success', $response['message']);
    } else {
        return redirect()->route(...)->with('error', $response['message']);
    }
}

// AFTER — return JSON so ajax-form can display success/error
protected function __profileFormPost(Request $request, $id = 0)
{
    try {
        $input = $request->all();
        $response = $this->_model->storeProfileSettings($input, $id, $request);
        return Helper::resp($response['message'], $response['status']);
    } catch (\Exception $e) {
        \App\Models\ErrorLog::Log($e);
        return Helper::rj($e->getMessage());
    }
}
```

**Note:** The `ajax-form` plugin on the client side must be configured to reload or refresh the page/section after a 200 success response. If the plugin already handles this via a `reload` option or `data-success-redirect`, no additional JS change is required.

---

## Fix 3 — US-025: Invalid phone number format not shown to user

### Root Cause
Same ajax-form vs redirect mismatch as Fix 2. The phone validation error from `storeProfileSettings()` is returned as a flash message that the AJAX plugin cannot display.

Additionally, the `data-inputmask` attribute on the phone field uses `(999) 999-9999` format — this allows only digits in those slots but does not prevent a user with JS disabled (or a bypassed request) from sending arbitrary values. The server already validates `strlen < 10` after stripping non-digits, but the error message never reaches the UI.

### Changes
Same fix as Fix 2 (`__profileFormPost` → return JSON). No additional model change required since the validation logic already exists.

---

## Fix 4 — US-030 & US-031: 2FA enable/disable fails

### Root Cause
The 2FA modals in `profile-settings.blade.php` use `_token` (a global JS variable) in AJAX calls:
```javascript
data: { _token: _token }
```
If `_token` is not defined globally on the page (it should be set in the layout), the AJAX call will fail with a CSRF mismatch (419 error) or a JS `ReferenceError`. Additionally, the profile settings partial is loaded inside a settings container — if this container is loaded inside a modal, the `@push('modals')` section may not execute.

### Changes

**`resources/views/admin/manages/users/profile-settings.blade.php` — CSRF token fallback**

In the `@push('page_script')` block, add a safe CSRF token reference before the IIFE:

```javascript
// BEFORE
var _setupRoute = '{{ route("user-settings.2fa.setup") }}';

// AFTER — use meta tag as fallback if _token is not globally defined
var _token = (typeof _token !== 'undefined') ? _token : (document.querySelector('meta[name="csrf-token"]') || {}).content || '';
var _setupRoute = '{{ route("user-settings.2fa.setup") }}';
```

**Verify** that `<meta name="csrf-token" content="{{ csrf_token() }}">` is present in `admin.layouts.layout`. If not, add it to the `<head>` section.

---

## Fix 5 — US-039: Password form has no indicator that a password is already set

### Root Cause
The password change form shows three empty `<input type="password">` fields. The test expects a visual hint (`***`) that a password is currently set. This is a UX gap — the form provides no context.

### Changes

**`app/Http/Controllers/Manages/UserSettingsController.php` — `__formUiGeneration()`, `$form2` definition**

Add a `text_label` field above `current_password` to indicate a password is already configured:

```php
// Add before 'current_password' field:
'password_status' => [
    'type' => 'text_label',
    'label' => '',
    'value' => '<span class="text-muted small"><i class="bx bx-lock me-1"></i>A password is currently set &nbsp; <span style="letter-spacing:2px">●●●●●●●●●●</span></span>',
    'width' => 'col-md-12',
],
'current_password' => [
    'type' => 'password',
    ...
],
```

---

## Fix 6 — US-043: Incorrect current password not shown as an error

### Root Cause
Same ajax-form vs redirect mismatch. `__changePasswordFormPost()` returns `redirect()->with('error', ...)` for wrong password. The AJAX plugin receives an HTML page and shows nothing.

### Changes

**`app/Http/Controllers/Manages/UserSettingsController.php` — `__changePasswordFormPost()`**

```php
// AFTER — return JSON response
protected function __changePasswordFormPost(Request $request, $id = 0)
{
    try {
        $input = $request->all();
        $response = $this->_model->changePasswordFormPost($input, $id, $request);
        return Helper::resp($response['message'], $response['status']);
    } catch (\Exception $e) {
        \App\Models\ErrorLog::Log($e);
        return Helper::rj($e->getMessage());
    }
}
```

**Note:** The original success case redirected to `admin.logout`. Since this now returns JSON, the ajax-form plugin's success handler must trigger logout. Add a `data-success-url` attribute to the form or handle via the plugin's success callback:

In `__formUiGeneration()`, update `$form2` class or add attribute:
```php
'class' => 'form-horizontal custom-validation ajax-form',
'attributes' => ['data-on-success' => 'logout'],
```

Then in the page script, listen for the form's success event and redirect to the logout URL if `data-on-success === 'logout'`:
```javascript
$(document).on('ajax-form-success', '#user_password_form', function(e, response) {
    if ($(this).data('on-success') === 'logout') {
        window.location.href = '{{ route("admin.logout") }}';
    }
});
```

---

## Fix 7 — US-053: Terms of Use link goes to `#` (not opening)

### Root Cause
`config('app.terms_url', '#')` falls back to `#` when `TERMS_URL` is not set in `.env`. Clicking `#` on the same page scrolls to top rather than opening a terms document.

### Changes

**`.env`** — Add the terms URL:
```
TERMS_URL=https://hubwallet.com/terms
```

**`config/app.php`** — Ensure the key exists (it already does based on exploration):
```php
'terms_url' => env('TERMS_URL', 'https://hubwallet.com/terms'),
```

Update the default fallback from `#` to the actual terms URL so that even without the env variable, the link works.

---

## Fix 8 — US-087, US-101, US-102: Mobile App checkboxes not selectable / All-None broken

### Root Cause
The notification form defines each Mobile App checkbox as:
```php
'attributes' => [
    'name' => "mobile[{$item['id']}]",
    'class' => 'mobileapp_checkbox mt-4'
],
```

The `admin-form-wrapper` component for `type: checkbox` likely applies the `attributes.class` to the outer wrapper `<div>` rather than the `<input type="checkbox">` element. The `notification-settings.js` selector `$('.mobileapp_checkbox')` then matches the wrapper div, not the input, so `.prop('checked', ...)` has no effect.

### Changes

**`public/assets/js/notification-settings.js`** — Update selectors to target actual inputs within the class:

```javascript
// BEFORE
var checkType = (type == 'm') ? 'mobileapp_checkbox' : ((type == 'e') ? 'email_checkbox' : 'popup_checkbox');
if (checkUncheck == 'all') {
    $('.' + checkType).prop('checked', true);
} else {
    $('.' + checkType).prop('checked', false);
}

// AFTER — target the input inside the wrapper
var checkType = (type == 'm') ? 'mobileapp_checkbox' : ((type == 'e') ? 'email_checkbox' : 'popup_checkbox');
var $inputs = $('.' + checkType + ' input[type="checkbox"], input[type="checkbox"].' + checkType);
if (checkUncheck == 'all') {
    $inputs.prop('checked', true);
} else {
    $inputs.prop('checked', false);
}
```

This dual selector (`wrapper input` OR `input.class`) handles both cases: class on wrapper and class on input.

Additionally, add a `data-column` attribute approach by updating the controller to add `data-column="mobile"` to each checkbox `<input>` via the `attributes` array — but only if the form wrapper passes arbitrary data attributes to the input element. If it does:

```php
// In UserSettingsController::__formUiGenerationForNotification()
'attributes' => [
    'name' => "mobile[{$item['id']}]",
    'class' => 'mobileapp_checkbox mt-4',
    'data-column' => 'mobile',   // add this
],
```

And update `notification-settings.js` to use:
```javascript
var colMap = { m: 'mobile', e: 'email', p: 'popup' };
var column = colMap[type];
var $inputs = $('input[type="checkbox"][data-column="' + column + '"]');
```

---

## Fix 9 — US-108: Mobile App None not persisting after save + refresh

### Root Cause
The notification settings form uses `ajax-form` class, but `__notificationTypesFormPost()` returns `redirect()`. The AJAX plugin receives an HTML page, treats the submission as complete without actually following through on its save-confirmation flow, or the page is not refreshed after save. The database write may succeed but the user cannot confirm it, and the page may not reload to reflect the saved state.

### Changes

**`app/Http/Controllers/Manages/UserSettingsController.php` — `__notificationTypesFormPost()`**

```php
// AFTER — return JSON
protected function __notificationTypesFormPost(Request $request, $id = 0)
{
    try {
        $input = $request->all();
        $response = $this->user_notification_model->store($input, $id, $request);
        return Helper::resp($response['message'] ?? 'Saved.', $response['status'] ?? 200);
    } catch (\Exception $e) {
        \App\Models\ErrorLog::Log($e);
        return Helper::rj($e->getMessage());
    }
}
```

Also apply the same change to `notificationPopupReminderStore()`:
```php
public function notificationPopupReminderStore(Request $request)
{
    try {
        $input = $request->all();
        $response = $this->user_profile_model->store($input, 'settings');
        return Helper::resp($response['message'] ?? 'Saved.', $response['status'] ?? 200);
    } catch (\Exception $e) {
        \App\Models\ErrorLog::Log($e);
        return Helper::rj($e->getMessage());
    }
}
```

---

## Fix 10 — US-117: Notification table overflows at zoom / smaller screen

### Root Cause
The notification matrix uses Bootstrap's `col-md-X` classes: `col-md-6` for label + `col-md-2` × 3 for columns = 12 total. At ≥125% zoom or on small screens, the `md` breakpoint is no longer active and columns stack or overflow.

### Changes

**`app/Http/Controllers/Manages/UserSettingsController.php` — `__formUiGenerationForNotification()`, heading fields**

Change column widths to include `col-sm` equivalents and add `overflow-x: auto` wrapper:

```php
// heading fields — change col-md-2 to col-sm-2 col-md-2
"heading_mobile"  => [
    'type' => 'text_label',
    'label' => 'Mobile App: <br> ...',
    'width' => 'col-sm-2 col-md-2 text-center',
],
// same for heading_app and heading_popup

// each notification row columns
"mobile__{$item['id']}"  => [
    ...
    'width' => 'col-sm-2 col-md-2',
    ...
],
// same for email and popup columns
```

**`resources/views/admin/manages/users/notification-settings.blade.php`**

Wrap the form include in an `overflow-x: auto` div:
```blade
<div class="row col-md-12">
    <div class="col-md-8">
        <div class="table-responsive">   {{-- already present -- ensure it wraps the form --}}
            @php($form = $form1)
            @include('admin.components.admin-form-wrapper')
        </div>
    </div>
    ...
```

Verify the `table-responsive` wrapper is present on the form container (it already appears in the view at line 3). If the form renders rows outside this wrapper, move the wrapper to contain the full form output.

---

## Fix 11 — US-122: Mouse hover on All/None not visually obvious as clickable

### Root Cause
The inline `<style>` injected via `include_scripts` in the controller adds `.h_check_uncheck { cursor: pointer; } .h_check_uncheck:hover { opacity: 0.75; }`. This style is injected inside the form body by the form wrapper. While browsers generally accept `<style>` inside `<body>`, it may be overridden by other CSS rules or the `<u>` tag's default styles, making the hover effect insufficient.

### Changes

**`app/Http/Controllers/Manages/UserSettingsController.php` — `__formUiGenerationForNotification()`, `$form1` definition**

Enhance the inline style with explicit hover styling:

```php
'include_scripts' => '<style>
    .h_check_uncheck {
        cursor: pointer;
        color: #3490dc;
        font-weight: 600;
        text-decoration: underline;
        transition: color 0.15s ease, opacity 0.15s ease;
    }
    .h_check_uncheck:hover {
        color: #1d68a7;
        opacity: 0.85;
        text-decoration: none;
    }
</style>
<script src="' . asset('assets/js/notification-settings.js') . '"></script>',
```

Move the `<style>` block to a `@push('page_css')` section in `notification-settings.blade.php` for cleaner rendering:

**`resources/views/admin/manages/users/notification-settings.blade.php`**

```blade
@push('page_css')
<style>
    .h_check_uncheck {
        cursor: pointer;
        color: #3490dc;
        font-weight: 600;
        transition: color 0.15s ease, opacity 0.15s ease;
    }
    .h_check_uncheck:hover {
        color: #1d68a7;
        opacity: 0.85;
        text-decoration: none;
    }
</style>
@endpush
```

And remove the inline `<style>` from the `include_scripts` string.

---

## Fix 12 — US-129: Appointment link with spaces not properly rejected (no UI feedback)

### Root Cause
`UserCalendarSettings::calendarAppointmentLinkFormPost()` correctly validates and trims spaces, returning `['status' => 400, 'message' => '...']`. However, `CalendarSettingsController::__calendarAppointmentLinkFormPost()` wraps this in a `redirect()->with('error', ...)`. The form uses `ajax-form`, so the redirect HTML response is silently ignored — the user sees no error.

### Changes

**`app/Http/Controllers/Calendar/CalendarSettingsController.php` — `__calendarAppointmentLinkFormPost()`**

```php
// AFTER — return JSON
public function __calendarAppointmentLinkFormPost(Request $request)
{
    try {
        $response = $this->_model->calendarAppointmentLinkFormPost($request);
        return Helper::resp($response['message'], $response['status']);
    } catch (\Exception $e) {
        \App\Models\ErrorLog::Log($e);
        return Helper::rj($e->getMessage());
    }
}
```

---

## Fix 13 — US-131: Special characters in appointment link not rejected with UI feedback

### Root Cause
Same as Fix 12 — validation exists but error is not displayed due to redirect response.

### Changes
Same as Fix 12 (same method handles all appointment link validation).

---

## Fix 14 — US-132: Appointment link max length boundary not enforced with UI feedback

### Root Cause
Same as Fix 12 — `strlen > 100` check exists in the model but error is never surfaced.

### Changes
Same as Fix 12. Additionally, add `maxlength="100"` attribute to the appointment link input field in the controller's form definition so the browser also enforces the limit:

**`app/Http/Controllers/Calendar/CalendarSettingsController.php` — `__formUiGeneration()`, `$form1` fields**

```php
'appointment_link' => [
    'type' => 'text',
    'label' => '',
    'width' => 'col-md-8',
    'value' => $userProfileData->appointment_link ?? '',
    'attributes' => [
        'placeholder' => 'Enter appointment link',
        'required' => true,
        'maxlength' => 100,  // add this
    ],
],
```

---

## Fix 15 — US-140: Same start and end time for working hours not rejected with UI feedback

### Root Cause
`UserCalendarSettings::workingHoursFormPost()` validates `$startTime === $endTime` and returns `['status' => 400, 'message' => '...']`. `CalendarSettingsController::__workingHoursFormPost()` wraps this in `redirect()->with('error', ...)`. Same ajax-form vs redirect mismatch.

### Changes

**`app/Http/Controllers/Calendar/CalendarSettingsController.php` — `__workingHoursFormPost()`**

```php
// AFTER — return JSON
public function __workingHoursFormPost(Request $request)
{
    try {
        $response = $this->_model->workingHoursFormPost($request);
        return Helper::resp($response['message'], $response['status']);
    } catch (\Exception $e) {
        \App\Models\ErrorLog::Log($e);
        return Helper::rj($e->getMessage());
    }
}
```

---

## Summary Table

| Test | Root Cause | Fix Location | Key Change |
|------|-----------|-------------|------------|
| US-002 | `index()` skips breadcrumb & form | `UserSettingsController::index()` | Redirect to `profile-settings` route |
| US-014 | ajax-form gets HTML redirect on error | `UserSettingsController::__profileFormPost()` | Return `Helper::resp()` JSON |
| US-025 | ajax-form gets HTML redirect on error | Same as US-014 | Same as US-014 |
| US-030 | `_token` may be undefined in 2FA AJAX | `profile-settings.blade.php` | Add CSRF meta fallback |
| US-031 | Same as US-030 | Same as US-030 | Same as US-030 |
| US-039 | No visual indicator password exists | `UserSettingsController::__formUiGeneration()` | Add `password_status` text_label field |
| US-043 | ajax-form gets HTML redirect on error | `UserSettingsController::__changePasswordFormPost()` | Return JSON + handle logout redirect in JS |
| US-053 | `TERMS_URL` env not set → `#` | `.env` + `config/app.php` | Set `TERMS_URL` and update fallback |
| US-087 | `.mobileapp_checkbox` targets wrapper not input | `notification-settings.js` | Dual selector `wrapper input + input.class` |
| US-101 | Same as US-087 | Same as US-087 | Same as US-087 |
| US-102 | Same as US-087 | Same as US-087 | Same as US-087 |
| US-108 | ajax-form gets HTML redirect; save uncertain | `UserSettingsController::__notificationTypesFormPost()` | Return JSON |
| US-117 | Fixed `col-md` breakpoints overflow at zoom | notification-settings view + controller | Add `col-sm` breakpoints + ensure `table-responsive` wraps form |
| US-122 | Hover CSS injected in wrong context | `notification-settings.blade.php` | Move CSS to `@push('page_css')` + enhance styles |
| US-129 | ajax-form gets HTML redirect on error | `CalendarSettingsController::__calendarAppointmentLinkFormPost()` | Return `Helper::resp()` JSON |
| US-131 | Same as US-129 | Same as US-129 | Same as US-129 |
| US-132 | Same as US-129 + no HTML `maxlength` | Same as US-129 + `$form1` fields | JSON response + add `maxlength=100` attribute |
| US-140 | ajax-form gets HTML redirect on error | `CalendarSettingsController::__workingHoursFormPost()` | Return `Helper::resp()` JSON |

---

## Verification Steps

1. **US-002** — Navigate to `/user-settings` → should auto-redirect to profile settings page; breadcrumb "User Accounts > My Profile" visible; "User Settings" tab is active.
2. **US-014** — Type 101 characters into First Name; save → inline error toast appears ("The first name may not be greater than 100 characters"). Same for Last Name and Job Title.
3. **US-025** — Enter "abc123" in Mobile Number field and submit → error toast "Phone number must be at least 10 digits". Enter "12" → same error.
4. **US-030** — Click "Set Up 2FA" button; modal opens; select Authenticator App; QR code loads; enter a valid TOTP code; click Confirm → success message; page shows "Enabled" badge.
5. **US-031** — With 2FA enabled, click "Disable 2FA"; enter current password; click Disable → 2FA disabled; page shows "Disabled" badge.
6. **US-039** — Open password section → a label "A password is currently set ●●●●●●●●●●" appears above the current password input.
7. **US-043** — Enter an incorrect current password; click Update → error toast "The current password is incorrect."
8. **US-053** — Click "See Terms of Use" link → a new tab opens with the terms URL (not `#`).
9. **US-087** — Open Notifications tab → each row has a visible, clickable checkbox in the Mobile App column.
10. **US-101** — Click "Mobile App > All" → all Mobile App checkboxes become checked.
11. **US-102** — Click "Mobile App > None" → all Mobile App checkboxes become unchecked.
12. **US-108** — Click "Mobile App > None" → Save → manually refresh page → Mobile App column is still fully unchecked.
13. **US-117** — Set browser zoom to 125% and 150% → notification table remains readable; columns do not overlap; a horizontal scrollbar appears if needed.
14. **US-122** — Hover mouse over any "All" or "None" link → cursor changes to pointer; link text color changes (darker blue); opacity slightly decreases indicating it is clickable.
15. **US-129** — Enter "my link " (with trailing space) in appointment link → save → error toast "Appointment link must not contain spaces."
16. **US-131** — Enter "link@#$/test" → save → error toast "Appointment link may only contain letters, numbers, hyphens, and underscores."
17. **US-132** — Enter 101 characters → save → error toast "Appointment link must not exceed 100 characters." Enter exactly 100 characters → save succeeds.
18. **US-140** — Enable Monday, set Start Time = 10:00 and End Time = 10:00 → save → error toast "Start time and end time cannot be the same for Monday."
