# PRD: Lead Statuses & Triggers Module — Bug Fixes

**Date:** 2026-04-30
**Module:** Lead Statuses & Triggers (`manages.lead-statuses`)
**Failing Test Cases:** LST-003, LST-004, LST-006, LST-007, LST-008, LST-013, LST-015, LST-031, LST-033

---

## Overview

Nine QA test cases fail across five logical fix groups:

| Group | Tests | Area |
|-------|-------|------|
| Fix 1 | LST-003 | Group filter UI — replace nav tabs with labeled dropdown |
| Fix 2 | LST-004, LST-006, LST-007, LST-008 | "View All Categories" popup — does not exist |
| Fix 3 | LST-013 | Duplicate category name validation — missing |
| Fix 4 | LST-015 | Max-length HTML attribute on category/status name — missing |
| Fix 5 | LST-031 | Duplicate status name per category validation — missing |
| Fix 6 | LST-033 | "Prompt to create new user" checkbox — field and column do not exist |

---

## Fix 1 — Group Filter Dropdown (LST-003)

### Failing Test
| ID | Priority | Scenario |
|----|----------|----------|
| LST-003 | High | Open group dropdown beside "Showing Categories and Statuses Available To" → select a group → only that group's categories/statuses display |

### Root Cause
The current listing page renders group filtering as Bootstrap nav **tabs**, not a labeled dropdown. QA tests for:
1. A text label reading **"Showing Categories and Statuses Available To"**
2. A `<select>` dropdown beside it with "Any Group" + individual groups
3. Selecting a group immediately filters the list

Neither the label nor the dropdown element exists — only nav tabs are present, and they have no associated label.

### Affected File
- `resources/views/admin/manages/lead-statuses/index.blade.php`

### Fix
Replace the nav-tab group filter with a labeled dropdown that reloads the page with the `gr` query parameter when the selection changes. The tab filter still works server-side (controller reads `?gr=` to filter by group), so only the UI element changes.

Replace the `<ul class="nav nav-tabs ...">` block with:

```blade
<div class="d-flex align-items-center gap-3 mb-4">
    <label class="mb-0 fw-semibold text-nowrap">Showing Categories and Statuses Available To:</label>
    <select id="group-filter-select" class="form-control form-control-sm" style="max-width:250px;"
            onchange="window.location.href='{{ route($routePrefix . '.index') }}?gr=' + this.value">
        <option value="0" {{ $selectedGroup == 0 ? 'selected' : '' }}>Any Group</option>
        @foreach($masterGroups as $group)
            <option value="{{ $group->id }}" {{ $selectedGroup == $group->id ? 'selected' : '' }}>
                {{ $group->title }}
            </option>
        @endforeach
    </select>
</div>
```

---

## Fix 2 — "View All Categories" Popup (LST-004, LST-006, LST-007, LST-008)

### Failing Tests
| ID | Priority | Scenario |
|----|----------|----------|
| LST-004 | High | Click "View All Categories" → popup opens with columns: Category, Statuses, Groups, Actions |
| LST-006 | Medium | Popup pagination: next/previous arrows navigate without duplicates |
| LST-007 | Medium | Items-per-page dropdown (10 / 25 / 50) refreshes table and updates pagination count |
| LST-008 | Medium | Click X icon → popup closes without page refresh |

### Root Cause
No "View All Categories" button, route, controller method, or popup view exists anywhere in the codebase. The entire feature is absent.

### Affected Files
- `resources/views/admin/manages/lead-statuses/index.blade.php` (add button)
- `app/Http/Controllers/Manages/LeadStatusesTriggersController.php` (add `allCategories()` method)
- `resources/views/admin/manages/lead-statuses/all-categories.blade.php` (new view)
- `routes/web.php` (add route)

### Fix A — Route

Add inside the `lead-statuses` route group (alongside the resource routes):

```php
Route::get('lead-statuses/all-categories',
    'App\Http\Controllers\Manages\LeadStatusesTriggersController@allCategories')
    ->name('manages.lead-statuses.all-categories');
```

> **Route order:** Register this BEFORE `Route::resource('lead-statuses', ...)` so it is not captured by the `{lead_status}` wildcard.

### Fix B — Controller method

Add to `LeadStatusesTriggersController`:

```php
public function allCategories(Request $request)
{
    try {
        $user      = \Auth::user();
        $perPage   = (int) $request->get('per_page', 25);
        $allowedPerPage = [10, 25, 50];
        if (!in_array($perPage, $allowedPerPage)) {
            $perPage = 25;
        }

        $srch_params = [
            'master_lead_status_id' => 0,
            'company_id'            => $user->company_id,
        ];
        if ($request->get('search_text')) {
            $srch_params['search_text'] = $request->get('search_text');
        }

        $categories = $this->_model->getListing($srch_params, $perPage);

        $this->_data['categories']      = $categories;
        $this->_data['perPage']         = $perPage;
        $this->_data['allowedPerPage']  = $allowedPerPage;
        $this->_data['routePrefix']     = $this->_routePrefix;
        $this->_data['permission']      = $this->_data['permission'] ?? [];
        $this->_data['viewPage']        = $request->ajax() ? '-modal' : '';
        $this->_data['module']          = 'All Categories';
        $this->_data['includePage']     = 'admin.' . $this->_routePrefix . '.all-categories';

        return view('admin.components.general' . $this->_data['viewPage'], $this->_data);
    } catch (\Exception $e) {
        \App\Models\ErrorLog::Log($e);
        return Helper::rj($e->getMessage(), 500);
    }
}
```

### Fix C — Add button to index heading (LST-004)

In `index.blade.php`, add a "View All Categories" button to `$headingOptions`:

```blade
@php($headingOptions = [
    '<div class="search-field">...</div>',
    $permission['create'] ? '<a href="' . route($routePrefix.'.create', ['type' => 'category']) . '" ...>Add New Category</a>' : '',
    $permission['create'] ? '<a href="' . route($routePrefix.'.create', ['type' => 'status']) . '" ...>Add New Status</a>' : '',
    '<a href="' . route($routePrefix.'.all-categories') . '"
        class="' . \Config::get('view.buttons.secondary') . ' show-modal-lg"
        data-url="' . route($routePrefix.'.all-categories') . '">
        <i class="bx bx-list-ul"></i> View All Categories
    </a>',
])
```

### Fix D — Popup view `all-categories.blade.php` (LST-004, LST-006, LST-007, LST-008)

Create `resources/views/admin/manages/lead-statuses/all-categories.blade.php`:

```blade
<div class="body">
    {{-- Items-per-page + search bar (LST-007) --}}
    <div class="d-flex justify-content-between align-items-center mb-3">
        <div class="d-flex align-items-center gap-2">
            <label class="mb-0">Items per page:</label>
            <select id="per-page-select" class="form-control form-control-sm" style="width:80px;">
                @foreach($allowedPerPage as $size)
                    <option value="{{ $size }}" {{ $perPage == $size ? 'selected' : '' }}>{{ $size }}</option>
                @endforeach
            </select>
        </div>
    </div>

    {{-- Category table --}}
    <div class="table-responsive">
        <table class="table table-bordered table-hover">
            <thead class="table-light">
                <tr>
                    <th>Category</th>
                    <th>Statuses</th>
                    <th>Groups</th>
                    @if(isset($permission['edit']) && $permission['edit'])
                        <th>Actions</th>
                    @endif
                </tr>
            </thead>
            <tbody>
                @forelse($categories as $cat)
                    <tr>
                        <td>
                            <span style="display:inline-block;width:14px;height:14px;border-radius:50%;
                                         background:{{ $cat->color_code }};border:1px solid #aaa;
                                         vertical-align:middle;margin-right:6px;"></span>
                            {{ $cat->title }}
                        </td>
                        <td>{{ $cat->children->count() }}</td>
                        <td>
                            @if($cat->groupMaps->count())
                                {{ $cat->groupMaps->map(fn($m) => optional($m->group)->title)->filter()->implode(', ') }}
                            @else
                                <span class="text-muted">—</span>
                            @endif
                        </td>
                        @if(isset($permission['edit']) && $permission['edit'])
                            <td>
                                <a href="{{ route($routePrefix.'.edit', ['lead_status' => $cat->id, 'type' => 'category']) }}"
                                   class="action-btn show-modal"
                                   data-url="{{ route($routePrefix.'.edit', ['lead_status' => $cat->id, 'type' => 'category']) }}"
                                   title="Edit">
                                    <i class="bx bx-pencil"></i>
                                </a>
                            </td>
                        @endif
                    </tr>
                @empty
                    <tr><td colspan="4" class="text-center text-muted">No categories found.</td></tr>
                @endforelse
            </tbody>
        </table>
    </div>

    {{-- Pagination (LST-006) --}}
    <div class="d-flex justify-content-end mt-2">
        {{ $categories->appends(['per_page' => $perPage])->links() }}
    </div>
</div>

<script>
// LST-007: items-per-page dropdown reloads the popup content
document.getElementById('per-page-select').addEventListener('change', function () {
    const url = new URL(window.location.href);
    // Reload the modal content via AJAX with the new per_page value
    const currentSrc = document.querySelector('[data-url*="all-categories"]')?.getAttribute('data-url')
                       || '{{ route($routePrefix . ".all-categories") }}';
    const newUrl = new URL(currentSrc, window.location.origin);
    newUrl.searchParams.set('per_page', this.value);

    fetch(newUrl.toString(), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(r => r.text())
        .then(html => {
            // Replace modal body content
            const wrapper = document.querySelector('.modal.show .modal-body');
            if (wrapper) wrapper.innerHTML = html;
        });
});
</script>
```

**Note on groups in the table:** The `groupMaps` eager relation on `MasterLeadStatus` returns `MasterLeadStatusGroupMap` records which need a `group()` belongs-to relationship added to `MasterLeadStatusGroupMap`:

```php
// In MasterLeadStatusGroupMap
public function group()
{
    return $this->belongsTo(\App\Models\Masters\MasterGroup::class, 'master_group_id');
}
```

The close X (LST-008) is provided by the standard modal scaffold (`admin.components.general-modal`) — no additional code needed.

---

## Fix 3 — Duplicate Category Name Validation (LST-013)

### Failing Test
| ID | Priority | Scenario |
|----|----------|----------|
| LST-013 | High | Enter existing category name → system prevents duplicate or shows validation message |

### Root Cause
`__formPost()` validates only `'title' => 'required|max:255'`. There is no uniqueness check for category titles within the same company. Two categories with the same name (e.g. "Boarding") can be created freely.

### Affected File
- `app/Http/Controllers/Manages/LeadStatusesTriggersController.php`

### Fix

Rewrite `__formPost()` to add duplicate checks for both categories and statuses, handling each type separately based on the `type` request parameter:

```php
protected function __formPost(Request $request, $id = 0)
{
    $this->validate($request, [
        'title' => 'required|max:255',
    ]);

    $user = \Auth::user();
    $type = $request->get('type', 'category');

    if ($type === 'category') {
        // Duplicate category check: same title, same company, root level (master_lead_status_id = 0)
        $duplicate = \App\Models\Masters\MasterLeadStatus::whereRaw('LOWER(title) = LOWER(?)', [$request->title])
            ->where('company_id', $user->company_id)
            ->where('master_lead_status_id', 0)
            ->when($id, fn($q) => $q->where('id', '!=', $id))
            ->exists();

        if ($duplicate) {
            return redirect()->back()
                ->withInput()
                ->withErrors(['title' => 'A category with this name already exists.']);
        }
    } elseif ($type === 'status' && $request->filled('master_lead_status_id')) {
        // Duplicate status check: same title, same category, same company (Fix 5)
        $duplicate = \App\Models\Masters\MasterLeadStatus::whereRaw('LOWER(title) = LOWER(?)', [$request->title])
            ->where('company_id', $user->company_id)
            ->where('master_lead_status_id', $request->master_lead_status_id)
            ->when($id, fn($q) => $q->where('id', '!=', $id))
            ->exists();

        if ($duplicate) {
            return redirect()->back()
                ->withInput()
                ->withErrors(['title' => 'A status with this name already exists in the selected category.']);
        }
    }

    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 Helper::rj($e->getMessage(), 500);
    }
}
```

**Key details:**
- `validate()` is moved outside try/catch so `ValidationException` propagates correctly
- `LOWER()` makes the duplicate check case-insensitive
- Category check: scoped to `master_lead_status_id = 0` (root records are categories)
- Status check: scoped to the submitted `master_lead_status_id` (parent category)
- `when($id, ...)` skips self-comparison during edits
- Both LST-013 (category) and LST-031 (status) are handled in a single unified method

---

## Fix 4 — Max Length HTML Attribute (LST-015)

### Failing Test
| ID | Priority | Scenario |
|----|----------|----------|
| LST-015 | Medium | Enter 256+ character category name → system restricts length or shows validation message without UI distortion |

### Root Cause
The `title` field in `__formUiGeneration()` is missing the `maxlength` HTML attribute. Server-side `max:255` catches the error only on submission; the user can type an unlimited string that distorts the form layout before hitting the server.

The current field definition (inside the `status_title` group):
```php
'title' => [
    'type'  => 'text',
    'label' => 'Name',
    'value' => $data->title,
    'attributes' => [
        'required'     => true,
        'autocomplete' => 'off',
        'placeholder'  => 'Enter name for the category',
    ],
],
```

### Affected File
- `app/Http/Controllers/Manages/LeadStatusesTriggersController.php`

### Fix
Add `'maxlength' => 255` to the `title` field attributes:

```php
'title' => [
    'type'  => 'text',
    'label' => 'Name',
    'width' => 'col-md-9 col-lg-9',
    'value' => $data->title,
    'attributes' => [
        'required'     => true,
        'autocomplete' => 'off',
        'placeholder'  => 'Enter name for the category',
        'maxlength'    => 255,
    ],
],
```

---

## Fix 5 — Duplicate Status Name Within Same Category (LST-031)

### Failing Test
| ID | Priority | Scenario |
|----|----------|----------|
| LST-031 | High | Add status with name "Initial Dispute Notification" in category "Dispute Responder" → system prevents duplicate within same category |

### Root Cause
`__formPost()` validates only `required|max:255`. There is no check for uniqueness of a status title within its parent category (`master_lead_status_id`). Two statuses with the same name can exist under the same category.

### Affected File
- `app/Http/Controllers/Manages/LeadStatusesTriggersController.php`

### Fix
Covered by Fix 3 — the unified `__formPost()` rewrite includes the status duplicate check in the `elseif ($type === 'status')` branch. No additional changes needed beyond Fix 3.

---

## Fix 6 — "Prompt to Create New User" Checkbox (LST-033)

### Failing Test
| ID | Priority | Scenario |
|----|----------|----------|
| LST-033 | Medium | Check "Prompt to create a new user when selected" → save status → status saves with prompt setting |

### Root Cause
The field does not exist anywhere in the codebase:
- The `master_lead_statuses` table has no `prompt_create_user` column (not in `$fillable`)
- The status form in `__formUiGeneration()` has no such checkbox field
- The model's `store()` method does not handle this flag

### Affected Files
1. **Migration** — `database/migrations/` (new migration to add column)
2. `app/Models/Masters/MasterLeadStatus.php` — add to `$fillable`, add else-clause in `store()`
3. `app/Http/Controllers/Manages/LeadStatusesTriggersController.php` — add checkbox to status form

### Fix A — Migration

Create a new migration:
```bash
php artisan make:migration add_prompt_create_user_to_master_lead_statuses_table
```

```php
public function up(): void
{
    Schema::table('master_lead_statuses', function (Blueprint $table) {
        $table->tinyInteger('prompt_create_user')->default(0)->after('color_code');
    });
}

public function down(): void
{
    Schema::table('master_lead_statuses', function (Blueprint $table) {
        $table->dropColumn('prompt_create_user');
    });
}
```

### Fix B — Model: add to `$fillable` + handle unchecked state

```php
protected $fillable = [
    'company_id',
    'master_lead_status_id',
    'status',
    'title',
    'color_code',
    'prompt_create_user',   // ← added
];
```

In `store()`, add an explicit reset for unchecked checkboxes (HTML omits unchecked checkboxes from POST):

```php
public function store($input = [], $id = 0, $request = null)
{
    try {
        $user = \Auth::user();
        $MasterLeadStatusClassMap = new MasterLeadStatusClassMap();

        // Normalise checkbox — absent from POST when unchecked
        $input['prompt_create_user'] = isset($input['prompt_create_user']) ? 1 : 0;

        if ($id) {
            // ... existing update logic unchanged
        } else {
            // ... existing create logic unchanged
        }
        // ... rest unchanged
    }
}
```

### Fix C — Controller: add checkbox to status form only

In `__formUiGeneration()`, add the checkbox field after `master_lead_status_id` in the status-only conditional block:

```php
...(empty($request->type == 'status') ? [] : [
    'master_lead_status_id' => [
        'type'    => 'select',
        'label'   => 'Category',
        'options' => ['' => 'Select...'] + $masterLeadStatus,
        'value'   => $data->master_lead_status_id,
        'attributes' => ['required' => true],
    ],
    'prompt_create_user' => [
        'type'    => 'checkbox',
        'label'   => 'Prompt to create a new user when selected',
        'options' => [1 => 'Enable prompt to create a new user when this status is selected in a lead workflow'],
        'value'   => isset($data->prompt_create_user) && $data->prompt_create_user ? [1] : [],
        'attributes' => ['name' => 'prompt_create_user'],
    ],
]),
```

---

## Summary

| Fix | Test Cases | Root Cause | Files |
|-----|------------|------------|-------|
| Fix 1: Group filter dropdown | LST-003 | Nav tabs used instead of labeled `<select>` dropdown | `index.blade.php` |
| Fix 2: View All Categories popup | LST-004, 006, 007, 008 | Feature entirely absent | `index.blade.php`, `LeadStatusesTriggersController.php`, `all-categories.blade.php`, `routes/web.php`, `MasterLeadStatusGroupMap.php` |
| Fix 3: Duplicate category validation | LST-013 | No uniqueness check in `__formPost()` | `LeadStatusesTriggersController.php` |
| Fix 4: maxlength attribute | LST-015 | `title` field missing `maxlength=255` | `LeadStatusesTriggersController.php` |
| Fix 5: Duplicate status per category | LST-031 | No per-category uniqueness check | `LeadStatusesTriggersController.php` (via Fix 3) |
| Fix 6: Prompt-to-create-user checkbox | LST-033 | Column, field, and save logic all absent | migration, `MasterLeadStatus.php`, `LeadStatusesTriggersController.php` |

**Total files:** 6 (+ 1 new migration + 1 new view)
