# PRD: User Accounts Module — Fix 4 Failed QA Test Cases

## Context

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

- `resources/views/admin/manages/users/index.blade.php` — checkbox uses same URL param as status dropdown (conflict); "Activity History" button has no route/link
- `app/Http/Controllers/Manages/UserController.php` — no server-side validation in `__formPost()`; missing `activityHistory()` controller method
- `public/assets/js/user.js` — no click handler for column sorting; missing route/link for activity history button
- `routes/web.php` — missing `activity-history` route

---

## Fix 1 — UA_005: "Show Deactivated Users" toggle does not work correctly

### Root Cause

The `#showDeactivatedUsers` checkbox and the `.user-status-filter` dropdown both read and write the **same** `status` URL query parameter. When any status is already selected in the dropdown, toggling the checkbox silently overwrites that selection (or clears it on uncheck). This produces unpredictable results and makes the toggle non-functional in practice.

Additionally, both controls are bidirectionally unaware of each other — selecting "Blocked" in the dropdown checks the checkbox, while unchecking the checkbox deletes the `status` param even when it was set by the dropdown.

### Expected Behaviour

Checking "Show Deactivated Users" shows users with status `Blocked` (status=4) regardless of what the dropdown currently shows. Unchecking it reverts to showing all users (or restores the prior dropdown selection). The two controls operate independently.

### Changes

**`resources/views/admin/manages/users/index.blade.php`** — change the checkbox checked condition to use a dedicated `deactivated` param:

```blade
{{-- REPLACE: --}}
<input type="checkbox" class="custom-control-input input-mini" id="showDeactivatedUsers"
    {{ (request()->input('status') ?? null) == 4 ? 'checked' : '' }}>

{{-- WITH: --}}
<input type="checkbox" class="custom-control-input input-mini" id="showDeactivatedUsers"
    {{ request()->boolean('deactivated') ? 'checked' : '' }}>
```

**`public/assets/js/user.js`** — update the checkbox handler to use a dedicated `deactivated` param instead of `status`:

```javascript
// REPLACE:
$("#showDeactivatedUsers").change(function () {
    var showDeactivated = $(this).is(":checked");
    let url = new URL(window.location.href);
    if (showDeactivated) {
        url.searchParams.set('status', '4');
    } else {
        url.searchParams.delete('status');
    }
    window.location.href = url.toString();
});

// WITH:
$("#showDeactivatedUsers").change(function () {
    let url = new URL(window.location.href);
    if ($(this).is(":checked")) {
        url.searchParams.set('deactivated', '1');
    } else {
        url.searchParams.delete('deactivated');
    }
    window.location.href = url.toString();
});
```

**`app/Http/Controllers/Manages/UserController.php`** — pass the `deactivated` param to the model's search params in `index()`:

```php
// ADD after line: $srch_params['status'] = $request->get('status', '');
if ($request->boolean('deactivated')) {
    $srch_params['status'] = 4;
}
```

---

## Fix 2 — UA_006: Clicking column headers does not sort

### Root Cause

The dynamic column headers rendered via `admin.components.form-header-fields` already have `class="sortable-col"`, `data-sort-col="<column>"`, and a `⇅` sort icon. The backend `getListing()` already processes `orderBy` via `Helper::manageOrderBy()`, and the controller passes `$this->_data['orderBy'] = $this->_model->orderBy` to the view.

However, there is **no click handler** in `user.js` for `.sortable-col` elements. Clicking a column header does nothing.

### Expected Behaviour

Clicking any column header sorts the table by that column ascending; clicking again toggles to descending. A sort arrow (`↑`/`↓`) replaces `⇅` on the active column.

### Changes

**`public/assets/js/user.js`** — add a sort click handler inside the existing `$(document).ready()` block:

```javascript
$(document).on('click', '#datatable thead .sortable-col', function () {
    var col = $(this).data('sort-col');
    var currentOrder = new URLSearchParams(window.location.search).get('orderBy') || '';
    var parts = currentOrder.split('__');
    var dir = (parts[0] === col && parts[1] === 'asc') ? 'desc' : 'asc';
    var url = new URL(window.location.href);
    url.searchParams.set('orderBy', col + '__' + dir);
    window.location.href = url.toString();
});
```

---

## Fix 3 — UA_008: "User Activity History" button does nothing

### Root Cause

The button in `index.blade.php` is a plain `<button>` with no `href`, no click handler, and no `data-*` attribute. There is also no route, controller method, or blade view for User Activity History. The `UserActivityLog` model (`app/Models/Users/UserActivityLog.php`) exists but only defines `$fillable` — it has no query logic.

### Expected Behaviour

Clicking "User Activity History" opens a page listing all user activity log entries (user, activity type, description, timestamp) filterable by user and company.

### Changes

**`routes/web.php`** — add the activity history route inside the existing `manages/users` route group (after the `export` route):

```php
Route::get('activity-history', 'App\Http\Controllers\Manages\UserController@activityHistory')
    ->name('manages.users.activity-history');
```

**`app/Http/Controllers/Manages/UserController.php`** — add the `activityHistory()` method:

```php
public function activityHistory(Request $request)
{
    try {
        $this->initIndex();
        $this->_data['module']      = 'User Activity History';
        $this->_data['subTitle']    = '';
        $this->_data['routePrefix'] = $this->_routePrefix;
        $this->_data['breadcrumb']  = [
            route($this->_routePrefix . '.index') => 'User Accounts',
            '#' => 'Activity History',
        ];

        $query = \App\Models\Users\UserActivityLog::with('user')
            ->where('company_id', \Auth::user()->company_id)
            ->when($request->get('user_id'), fn($q, $uid) => $q->where('user_id', $uid))
            ->when($request->get('search_text'), fn($q, $s) => $q->where('activity_description', 'LIKE', "%{$s}%"))
            ->orderBy('id', 'DESC');

        $this->_data['data'] = $query->paginate($this->_offset);

        return view('admin.' . $this->_routePrefix . '.activity-history', $this->_data)
            ->with('i', ($request->input('page', 1) - 1) * $this->_offset);
    } catch (\Exception $e) {
        \App\Models\ErrorLog::Log($e);
        return \App\Helpers\Helper::rj($e->getMessage(), 500);
    }
}
```

**`app/Models/Users/UserActivityLog.php`** — add the `user` relationship:

```php
public function user()
{
    return $this->belongsTo(\App\Models\User::class, 'user_id');
}
```

**`resources/views/admin/manages/users/activity-history.blade.php`** — create the view:

```blade
@php(
$headerOption = \App\Http\Controllers\Controller::getHeaderOptions(
    $title ?? '',
    $subTitle ?? '',
    isset($filters) ? $filters : [],
    isset($advancedFilters) ? $advancedFilters : []
)
)
@extends('admin.layouts.layout', $headerOption)
@section('content')
<div class="col-12">
    <div class="back-section mb-2">
        <button type="button" class="btn back-btn waves-effect" onclick="window.history.back()">
            <i class="{{ \Config::get('settings.icon_back') }}"></i> Back to User Accounts
        </button>
    </div>
    <div class="project-box">
        <x-heading-sec :module="$module" :options="[]" />
        <div class="table-responsive project-table">
            <table id="datatable" class="{{ \Config::get('view.table.table_class') }}">
                <thead class="{{ \Config::get('view.table.table_head_class') }}">
                    <tr>
                        <th>#</th>
                        <th>User</th>
                        <th>Activity Type</th>
                        <th>Description</th>
                        <th>Date</th>
                    </tr>
                </thead>
                <tbody>
                    @if(isset($data) && count($data) > 0)
                        @foreach($data as $i => $log)
                        <tr>
                            <td>{{ $i + 1 }}</td>
                            <td>{{ $log->user->full_name ?? '—' }}</td>
                            <td>{{ $log->activity_type ?? '—' }}</td>
                            <td>{{ $log->activity_description ?? '—' }}</td>
                            <td>{!! \App\Helpers\Helper::showdate($log->created_at) !!}</td>
                        </tr>
                        @endforeach
                    @else
                        <tr>
                            <td colspan="5" class="text-center">No Activity Found</td>
                        </tr>
                    @endif
                </tbody>
            </table>
        </div>
        @include('admin.components.pagination')
    </div>
</div>
@endsection
@push('page_script')
<script src="{{ asset('assets/js/common.js') }}"></script>
@endpush
```

**`resources/views/admin/manages/users/index.blade.php`** — make the button a link:

```blade
{{-- REPLACE: --}}
<button type="button" class="{{ \Config::get('view.buttons.primary') }}"><i class="mdi mdi-clock-outline"></i> User Activity History</button>

{{-- WITH: --}}
<a href="{{ route('manages.users.activity-history') }}" class="{{ \Config::get('view.buttons.primary') }}"><i class="mdi mdi-clock-outline"></i> User Activity History</a>
```

---

## Fix 4 — UA_011: Submitting an empty form shows no validation errors

### Root Cause

`UserController::__formPost()` passes `$request` directly to `$this->_model->store()` without any prior `validate()` call. Inside `User::store()`, only `email` and `username` are validated via `$request->validate()`. This validation call is inside a `try/catch` block — when it throws `ValidationException`, the catch returns `redirect()->back()->with('error', $e->getMessage())`, which shows a generic flash message instead of per-field errors. All other required fields (first_name, last_name, role_id, phone, passwords for create) have no server-side check at all.

Additionally, `user.js` sets `$("form").attr("novalidate", "novalidate")` which disables HTML5 client-side validation entirely.

### Expected Behaviour

Submitting an empty (or partial) form surfaces field-level errors under each required input: first name, last name, username, email, user class (role), mobile number, and passwords on create.

### Changes

**`app/Http/Controllers/Manages/UserController.php`** — add comprehensive `validate()` call in `__formPost()`, **outside the try/catch** so `ValidationException` reaches Laravel's exception handler and returns proper field errors:

```php
protected function __formPost(Request $request, $id = 0)
{
    $rules = [
        'first_name' => 'required|max:100',
        'last_name'  => 'required|max:100',
        'username'   => 'required|max:255|unique:users,username' . ($id ? ",{$id}" : ''),
        'email'      => 'required|email|unique:users,email'      . ($id ? ",{$id}" : ''),
        'role_id'    => 'required',
        'phone'      => 'required',
    ];

    if (!$id) {
        $rules['create_password']  = 'required|min:8';
        $rules['confirm_password'] = 'required|same:create_password';
    }

    $this->validate($request, $rules);

    try {
        $isOwnAcc = Auth::user()->id != $id ? false : true;

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

        if (in_array($response['status'], [200, 201])) {
            if ($id) {
                return redirect()
                    ->route($this->_routePrefix . '.edit', $id)
                    ->with('success', $response['message']);
            }
            return redirect()
                ->route($this->_routePrefix . '.index')
                ->with('success', $response['message']);
        }
        return redirect()
            ->route($this->_routePrefix . '.index')
            ->with('error', $response['message']);

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

> The `email` and `username` unique rules are now declared in the controller, making the duplicate `$request->validate()` call inside `User::store()` redundant. It can remain as a safety net or be removed in a follow-up.

---

## Files Changed Summary

| File | Fixes |
|------|-------|
| `resources/views/admin/manages/users/index.blade.php` | UA_005, UA_008 |
| `resources/views/admin/manages/users/activity-history.blade.php` | UA_008 (new file) |
| `app/Http/Controllers/Manages/UserController.php` | UA_005, UA_008, UA_011 |
| `app/Models/Users/UserActivityLog.php` | UA_008 |
| `public/assets/js/user.js` | UA_005, UA_006 |
| `routes/web.php` | UA_008 |

No migrations required.

---

## Test Verification Checklist

| Test Case | Scenario | Pass Condition |
|-----------|----------|----------------|
| UA_005 | Toggle "Show Deactivated Users" checkbox | Only Blocked (status=4) users appear; dropdown selection is unaffected |
| UA_006 | Click any column header | Table re-sorts by that column; arrow toggles asc↔desc on second click |
| UA_008 | Click "User Activity History" button | Navigates to `/admin/manages/users/activity-history` listing page |
| UA_011 | Submit empty create form | Per-field errors appear under: First Name, Last Name, Username, Email, User Class, Phone, Password |
