# PRD: User Class Module — Fix 6 Failed QA Test Cases

## Context

6 QA test cases in the User Class module are failing. Root causes span three files:

- `resources/views/admin/manages/user-classes/index.blade.php` — missing sort links on column header
- `app/Http/Controllers/Manages/UserClassController.php` — validation catch block swallows `ValidationException`, no duplicate-name check, emoji not rejected with friendly message, `copy_from_option` field type is wrong
- `public/assets/js/user-class.js` — copy-permissions toggle logic keyed on radio buttons instead of a checkbox

---

## Fix 1 — UC_021: Column sorting does nothing

### Root Cause

The `<th>User Class</th>` header in `index.blade.php` is plain text. No `Helper::sort()` call exists. The controller already passes `$this->_data['orderBy'] = $this->_model->orderBy` to the view, and `getListing()` already processes `orderBy` via `Helper::manageOrderBy()` — the backend is fully ready. Only the view header is missing the sort link.

### Expected Behaviour

Clicking the "User Class" column header toggles ascending/descending sort and reloads the page with sorted records.

### Changes

**`resources/views/admin/manages/user-classes/index.blade.php`** — replace the plain `<th>` tag

```blade
{{-- REPLACE: --}}
<th>User Class</th>

{{-- WITH: --}}
<th>User Class {!! \App\Helpers\Helper::sort($routePrefix . '.index', 'title', $orderBy) !!}</th>
```

---

## Fix 2 — UC_035: No duplicate name validation

### Root Cause

`__formPost()` in `UserClassController` only validates `required|max:255`. There is no uniqueness check on `title`. The `Role::store()` model method also has no duplicate guard. A user can create two user classes with the identical name.

### Expected Behaviour

Submitting a name that already exists (e.g. "Administrator") shows a field-level error: _"The user class name has already been taken."_

### Changes

**`app/Http/Controllers/Manages/UserClassController.php`** — add duplicate check inside `__formPost()` after validation, before calling `store()`.

```php
// After $this->validate(...), before $this->_model->store(...)
$duplicate = \App\Models\Masters\Role::whereRaw('LOWER(title) = LOWER(?)', [$request->title])
    ->whereNull('deleted_at')
    ->when($id, fn($q) => $q->where('id', '!=', $id))
    ->exists();

if ($duplicate) {
    return redirect()->back()
        ->withInput()
        ->withErrors(['title' => 'The user class name has already been taken.']);
}
```

---

## Fix 3 — UC_036 / UC_037: Max-length validation errors not shown

### Root Cause

`__formPost()` wraps `$this->validate()` inside a `try/catch (\Exception $e)` block. `Illuminate\Validation\ValidationException` extends `\Exception`, so the catch block intercepts it before Laravel's exception handler can convert it into a proper 422 JSON response (for AJAX forms) or a redirect-with-errors (for standard requests). Instead, the catch block returns `redirect()->back()->with('error', $e->getMessage())`, which:

- For AJAX (`ajax-form` class): the AJAX handler receives a redirect it cannot follow, so no field error is ever displayed — a 256-char title appears to be silently accepted (UC_037).
- For non-AJAX: the flash message says only _"The given data was invalid."_ with no field-level indicator — a 255-char title may appear to fail with a generic error rather than being accepted (UC_036 if a DB error occurs downstream) or with the wrong error type.

### Expected Behaviour

- A 255-character title is accepted without error.
- A 256-character title surfaces a field-level error: _"The title may not be greater than 255 characters."_

### Changes

**`app/Http/Controllers/Manages/UserClassController.php`** — move `validate()` outside the try/catch so `ValidationException` propagates to Laravel's handler:

```php
protected function __formPost(Request $request, $id = 0) {
    // Validate OUTSIDE try/catch so ValidationException reaches Laravel's handler
    $this->validate($request, [
        'title' => 'required|max:255|regex:/^(?!.*[\x{10000}-\x{10FFFF}]).*$/u',
    ]);

    // ... duplicate check (Fix 2) ...

    try {
        $input = $request->all();
        $response = $this->_model->store($input, $id, $request);

        if (in_array($response['status'], [200, 201])) {
            return redirect()->back()->with('success', $response['message']);
        }
        return redirect()->back()->with('error', $response['message']);

    } catch (\Exception $e) {
        \App\Models\ErrorLog::Log($e);
        return redirect()->back()->with('error', $e->getMessage());
    }
}
```

> The `regex` rule above also covers Fix 4 (UC_045) — see next section.

Also add `maxlength` to the title input as a belt-and-suspenders client-side guard:

**`app/Http/Controllers/Manages/UserClassController.php`** — in `__formUiGeneration()`, update the `title` field attributes:

```php
'title' => [
    'type' => 'text',
    'label' => 'New User Class Name',
    'attributes' => [
        'required'     => true,
        'autocomplete' => 'off',
        'maxlength'    => 255,   // ADD THIS
    ],
],
```

---

## Fix 4 — UC_045: Emoji input shows wrong/confusing alert message

### Root Cause

When an emoji is entered in the title, `Role::store()` attempts a DB INSERT. MySQL columns with `utf8` charset (3-byte) reject 4-byte emoji characters and throw `SQLSTATE[HY000]: General error: 1366 Incorrect string value`. The catch block in `__formPost()` catches this and surfaces the raw SQL error message as a flash error — confusing and not user-friendly.

### Expected Behaviour

Emoji or 4-byte special characters in the title show a clear, friendly validation error: _"Emojis and special characters beyond standard Unicode are not allowed."_

### Changes

This is resolved by the regex rule added in Fix 3. The rule `/^(?!.*[\x{10000}-\x{10FFFF}]).*$/u` rejects any character with code point ≥ U+10000 (all emoji, mathematical alphanumerics, supplementary ideographs, etc.) at the validation layer — before any DB call is made.

Add a custom validation message to make the error copy user-friendly:

**`app/Http/Controllers/Manages/UserClassController.php`** — add a `$messages` array as the third argument to `validate()`:

```php
$this->validate($request, [
    'title' => 'required|max:255|regex:/^(?!.*[\x{10000}-\x{10FFFF}]).*$/u',
], [
    'title.regex' => 'Emojis and special characters are not allowed in the class name.',
]);
```

---

## Fix 5 — UC_047: Copy Permissions toggle does not disable User Class dropdown

### Root Cause

The `copy_from_option` field in `__formUiGeneration()` is rendered as a **radio button group** with only one visible option (`1 => 'Copy Permissions From'`; option `2` is commented out). A radio button with a single option cannot be "unchecked" — once selected it stays selected forever. The `user_class` dropdown is therefore always enabled, making it impossible to verify the "disable when unchecked" behaviour the test requires.

`user-class.js` binds to `$('input[name="copy_from_option"]').on('change', ...)`, which only fires when a different radio is selected — it never fires for a lone radio.

### Expected Behaviour

- The "Copy Permissions From" field renders as a **checkbox**, checked by default.
- When checked: the User Class dropdown is enabled and required.
- When unchecked: the User Class dropdown is disabled (greyed out) and its value is ignored on submit.

### Changes

**`app/Http/Controllers/Manages/UserClassController.php`** — change `copy_from_option` type from `radio` to `checkbox` in `__formUiGeneration()`:

```php
'copy_from_option' => [
    'type'    => 'checkbox',
    'label'   => 'Copy Permissions From',
    'value'   => 1,
    'options' => [1 => 'Copy Permissions From'],
    'width'   => 'col-lg-12 col-md-12 col-xs-12 col-sm-12',
],
```

Also remove the stale readonly logic that read from the now-gone radio value:

```php
// REMOVE these lines (lines 214-220 in the original):
if ($request->get('copy_from_option') == 1) {
    $form['fields']['copy_from_group']['fields']['user_class']['attributes']['readonly'] = true;
    ...
} else {
    ...
}
```

**`public/assets/js/user-class.js`** — replace the radio-change handler with a checkbox-change handler:

```javascript
$(document).ready(function() {
    $("form").attr("novalidate", "novalidate");

    $(".user-class-form").on("submit", function(e) {
        return handleFormSubmission(e);
    });

    // Initialise state on load
    syncCopyFromCheckbox();

    // Toggle on change
    $('input[name="copy_from_option"]').on('change', syncCopyFromCheckbox);

    function syncCopyFromCheckbox() {
        const checked = $('input[name="copy_from_option"]').is(':checked');
        if (checked) {
            $('#user_class').prop('disabled', false).closest('.form-group').removeClass('opacity-50');
        } else {
            $('#user_class').prop('disabled', true).closest('.form-group').addClass('opacity-50');
        }
    }
});
```

> **Note:** When a `<select>` is `disabled`, browsers do not include its value in the form POST. This means on submit, if the checkbox is unchecked, `user_class` will simply be absent from `$request->all()` — which is the "ignored" behaviour the test expects. No server-side changes are needed to handle this.

---

## Files Changed Summary

| File | Fixes |
|------|-------|
| `resources/views/admin/manages/user-classes/index.blade.php` | UC_021 |
| `app/Http/Controllers/Manages/UserClassController.php` | UC_021 (orderBy already passed), UC_035, UC_036, UC_037, UC_045, UC_047 |
| `public/assets/js/user-class.js` | UC_047 |

No migrations required.

---

## Test Verification Checklist

| Test Case | Scenario | Pass Condition |
|-----------|----------|----------------|
| UC_021 | Click "User Class" column header | Records re-order; arrow indicator toggles |
| UC_035 | Submit "Administrator" (existing name) | Field error: _"The user class name has already been taken."_ |
| UC_036 | Submit 255-char name | Record created successfully |
| UC_037 | Submit 256-char name | Field error: _"The title may not be greater than 255 characters."_ |
| UC_045 | Submit name containing emoji | Field error: _"Emojis and special characters are not allowed in the class name."_ |
| UC_047 | Uncheck "Copy Permissions From" | User Class dropdown becomes disabled/greyed out |
