# PRD: Webforms Module — Listing Page Bug Fixes

**Reference file:** `webforms-failed.csv` (29 failing test cases)
**Date:** 2026-04-30
**Module:** Web Forms (`manages.web-forms.index`)

---

## 1. Overview

29 QA test cases are failing on the Web Forms listing page. The root cause is that the current listing
uses an **accordion-based grouping UI** that has no search, sort, pagination, group filter dropdown,
or proper column definitions. The layout also has a `@php` block that overrides the controller's
`$headerOption`, breaking the breadcrumb.

This PRD converts the listing to a **standard flat-table format** consistent with other CRM modules,
adding full search / filter / sort / pagination and fixing column data and action icons.

---

## 2. Failing Tests Map

| Group | Test IDs | Root Cause |
|-------|----------|------------|
| G1 — Breadcrumb | WF_004 | `index.blade.php` lines 1–9 override the controller's `$headerOption` with empty args; controller does not pass `$breadcrumb` |
| G2 — Count Display | WF_005 | No total count computed or displayed |
| G3 — Group Filter | WF_006–009 | Accordion is the only grouping mechanism; no filter dropdown exists |
| G4 — Search | WF_010–015 | No search input or server-side search logic |
| G5 — Gear Icon | WF_016 | No settings / gear icon exists near the search box |
| G6 — View Sample Form | WF_019–020 | Per-row "View Public Form" button tooltip mismatch; button labeled/titled differently than expected |
| G7 — Column Data | WF_025–029 | Form Sender, Created By, Updated At columns absent; E-Sign Docs not clickable |
| G8 — Action Icons | WF_033–034 | Share icon (`bx-share-alt`) / Edit icon (`bx-edit`) don't match QA expectations |
| G9 — Sorting | WF_043–048 | No sortable column headers; accordion does not support sorting |
| G10 — Pagination | WF_049–050 | All forms loaded at once; no per-page selector or pagination controls |

---

## 3. Target Architecture

Replace the accordion listing with a **flat paginated table** identical in structure to other CRM
listing pages (helpdesk tickets, leads, etc.).

```
Toolbar
├── "Viewing N Web Forms" count badge
├── Group filter <select> (All Groups → specific group)
├── Search <input> + gear icon (opens per-page / settings dropdown)
└── Items-per-page <select> (10 | 25 | 50 | 100)

Table columns
  Form Name | Group | Form Sender | E-Sign Docs | Status | Created By | Created At | Updated At | Actions

Per-row actions
  [View Sample Form] [Share] [Settings ⚙] [Builder] [Duplicate] [Delete]

Pagination bar
  Showing X–Y of Z results  | « Prev  Page N of M  Next »
```

---

## 4. Fix Groups

---

### G1 — Breadcrumb (WF_004)

**Files:**
- `app/Http/Controllers/Manages/WebFormController.php`
- `resources/views/admin/manages/web-forms/index.blade.php`

**Root Cause:**

1. `index.blade.php` lines 1–9 call `getHeaderOptions('')` inside a `@php` block, silently
   overwriting the `$headerOption` set by the controller with an empty-title, no-breadcrumb version.
2. `WebFormController::index()` never creates or passes a `$breadcrumb` variable.

**Fixes:**

_Controller_ — add `$breadcrumb` array before the `return view(...)` call:

```php
$breadcrumb = [
    route('manages.web-forms.index') => 'Web Forms',
];
return view(
    'admin.manages.web-forms.index',
    compact('forms', 'groups', 'totalForms', 'headerOption', 'breadcrumb',
            'search', 'groupId', 'sortBy', 'sortDir', 'perPage')
);
```

_View_ — remove the entire `@php(...) @php` block at lines 1–9 (the `getHeaderOptions` override).
Keep only:

```blade
@extends('admin.layouts.layout', $headerOption)
```

---

### G2 — Count Display (WF_005)

**File:** `WebFormController.php`, `index.blade.php`

**Fix:**

_Controller_ — compute total after building the base query (before applying search/filter):

```php
$totalForms = WebForm::where('company_id', $companyId)->count();
```

_View_ — render count label near the table heading:

```html
<span class="text-muted small ms-2">Viewing {{ $totalForms }} Web Form{{ $totalForms !== 1 ? 's' : '' }}</span>
```

---

### G3 — Group Filter Dropdown (WF_006–009)

**File:** `WebFormController.php`, `index.blade.php`

**Root Cause:** No group filter dropdown; accordion was the only way to see per-group data.

**Controller changes:**

```php
$groupId = (int) $request->input('group_id', 0);

// Pass all groups for the dropdown
$groups = WebFormGroup::where('company_id', $companyId)
    ->orderBy('display_order')->orderBy('id')
    ->get(['id', 'name']);

// Apply filter when a group is selected
->when($groupId, fn($q) => $q->where('web_forms.web_form_group_id', $groupId))
```

**View** — add a `<select>` in the toolbar:

```html
<select name="group_id" class="form-select form-select-sm" onchange="this.form.submit()">
    <option value="0" {{ $groupId == 0 ? 'selected' : '' }}>All Groups</option>
    @foreach($groups as $g)
        <option value="{{ $g->id }}" {{ $groupId == $g->id ? 'selected' : '' }}>{{ $g->name }}</option>
    @endforeach
</select>
```

- When a group is selected only forms in that group are shown (WF_008).
- When no forms match the selected group the empty-state row is shown without error (WF_009).

---

### G4 — Search (WF_010–015)

**File:** `WebFormController.php`, `index.blade.php`

**Controller:**

```php
$search = trim($request->input('search', ''));

->when($search !== '', fn($q) => $q->where('web_forms.name', 'like', '%' . $search . '%'))
```

- `LIKE` is case-insensitive on MySQL default collation (WF_013).
- Empty `$search` restores the full list (WF_015).
- Non-matching keywords return zero rows without error (WF_014).

**View** — add search input bound inside a `<form method="GET">` wrapper:

```html
<input type="text" name="search" class="form-control form-control-sm module-search"
       placeholder="Search by form name…"
       value="{{ $search }}"
       autocomplete="off">
```

---

### G5 — Gear / Settings Icon Near Search (WF_016)

**File:** `index.blade.php`

Add a gear icon button immediately after the search input. Clicking it toggles a small dropdown
that exposes the **Items per page** selector (satisfying WF_016; also used by G10):

```html
<div class="dropdown d-inline-block ms-1">
    <button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle-no-caret"
            data-bs-toggle="dropdown" aria-expanded="false" title="Table settings">
        <i class="bx bx-cog"></i>
    </button>
    <ul class="dropdown-menu dropdown-menu-end p-2" style="min-width:160px;">
        <li><label class="form-label small mb-1">Items per page</label></li>
        @foreach([10, 25, 50, 100] as $pp)
        <li>
            <a class="dropdown-item small {{ $perPage == $pp ? 'active' : '' }}"
               href="{{ request()->fullUrlWithQuery(['per_page' => $pp, 'page' => 1]) }}">
                {{ $pp }}
            </a>
        </li>
        @endforeach
    </ul>
</div>
```

---

### G6 — View Sample Form (WF_019–020)

**File:** `index.blade.php`

The per-row "View Public Form" button exists but uses the title `"View Public Form"`. QA expects
`"View Sample Form"`. Additionally, the button must be visible in every row.

**Fix:** Change the per-row action button's `title` attribute and ensure it always renders:

```html
<a href="{{ route('webform.public.show', $form->public_slug) }}"
   target="_blank"
   class="btn btn-xs btn-outline-success"
   title="View Sample Form">
    <i class="bx bx-show"></i>
</a>
```

---

### G7 — Column Data: Form Sender, E-Sign Docs, Created By, Updated At (WF_025–029)

**File:** `WebFormController.php`, `index.blade.php`

**Root Cause:**
- "Form Sender" and "Created By" columns are absent (only "Created" date is shown).
- E-Sign Docs are non-clickable yellow badges.
- "Updated At" column is absent.

**Controller — join users tables and select computed columns:**

```php
use Illuminate\Support\Facades\DB;

$query = WebForm::query()
    ->where('web_forms.company_id', $companyId)
    ->leftJoin('web_form_groups as wfg', 'web_forms.web_form_group_id', '=', 'wfg.id')
    ->leftJoin('users as sender',  'web_forms.assigned_user_id',    '=', 'sender.id')
    ->leftJoin('users as creator', 'web_forms.created_by_user_id',  '=', 'creator.id')
    ->with(['esignMaps.document'])
    ->select([
        'web_forms.*',
        'wfg.name as group_name',
        DB::raw("TRIM(CONCAT(COALESCE(sender.first_name,''), ' ', COALESCE(sender.last_name,'')))  AS form_sender_name"),
        DB::raw("TRIM(CONCAT(COALESCE(creator.first_name,''), ' ', COALESCE(creator.last_name,''))) AS created_by_name"),
    ]);
```

**View — add / update columns:**

| Column | Display logic |
|--------|--------------|
| **Form Sender** (WF_025) | `{{ $form->form_sender_name ?: 'N/A' }}` (mapped to `assigned_user_id`) |
| **E-Sign Docs** (WF_026) | `<a href="{{ Storage::url($map->document->file_path) }}" target="_blank" class="badge bg-warning text-dark">{{ $map->document->title }}</a>` — when no docs: `<span class="text-muted small">None</span>` |
| **Created By** (WF_027) | `{{ $form->created_by_name ?: 'N/A' }}` |
| **Updated At** (WF_029) | `{{ $form->updated_at ? $form->updated_at->format('M d, Y') : '—' }}` |

---

### G8 — Action Icons (WF_033–034)

**File:** `index.blade.php`

| Test | Current icon | Expected fix |
|------|-------------|--------------|
| WF_033 — open/share | `bx-share-alt` | Keep `bx-share-alt`; separate "open" role is covered by the "View Sample Form" (`bx-show`) button — no icon change needed, just confirm both buttons are present and visible |
| WF_034 — settings icon | `bx-edit` | Change to `bx-cog` and update title to `"Settings"` |

**Fix WF_034:**

```html
{{-- Settings (was Edit Settings) --}}
<a href="{{ route('manages.web-forms.edit', $form->id) }}"
   class="btn btn-xs btn-outline-primary show-modal-lg"
   title="Settings">
    <i class="bx bx-cog"></i>
</a>
```

---

### G9 — Sortable Columns (WF_043–048)

**File:** `WebFormController.php`, `index.blade.php`

**Controller:**

```php
$allowedSorts = ['name', 'group_name', 'form_sender_name', 'created_by_name', 'created_at', 'updated_at'];
$sortBy  = in_array($request->input('sort_by'), $allowedSorts) ? $request->input('sort_by') : 'created_at';
$sortDir = strtolower($request->input('sort_dir', 'desc')) === 'asc' ? 'asc' : 'desc';

$sortColumn = match($sortBy) {
    'group_name'       => 'wfg.name',
    'form_sender_name' => DB::raw("TRIM(CONCAT(COALESCE(sender.first_name,''), ' ', COALESCE(sender.last_name,'')))"),
    'created_by_name'  => DB::raw("TRIM(CONCAT(COALESCE(creator.first_name,''), ' ', COALESCE(creator.last_name,'')))"),
    default            => 'web_forms.' . $sortBy,
};

$query->orderBy($sortColumn, $sortDir);
```

**View** — add a `sortHeader()` Blade macro / inline helper for each column:

```blade
@php
    function sortLink(string $col, string $label, string $currentSort, string $currentDir): string {
        $dir  = ($currentSort === $col && $currentDir === 'asc') ? 'desc' : 'asc';
        $icon = $currentSort === $col
            ? ($currentDir === 'asc' ? '↑' : '↓')
            : '⇅';
        $url  = request()->fullUrlWithQuery(['sort_by' => $col, 'sort_dir' => $dir, 'page' => 1]);
        return '<a href="' . $url . '" class="text-dark text-decoration-none">' . e($label) . ' <small>' . $icon . '</small></a>';
    }
@endphp

<th>{!! sortLink('name',            'Form Name',    $sortBy, $sortDir) !!}</th>
<th>{!! sortLink('group_name',      'Group',        $sortBy, $sortDir) !!}</th>
<th>{!! sortLink('form_sender_name','Form Sender',  $sortBy, $sortDir) !!}</th>
<th>E-Sign Docs</th>
<th>Status</th>
<th>{!! sortLink('created_by_name', 'Created By',   $sortBy, $sortDir) !!}</th>
<th>{!! sortLink('created_at',      'Created At',   $sortBy, $sortDir) !!}</th>
<th>{!! sortLink('updated_at',      'Updated At',   $sortBy, $sortDir) !!}</th>
<th class="text-end">Actions</th>
```

---

### G10 — Items per Page & Pagination (WF_049–050)

**File:** `WebFormController.php`, `index.blade.php`

**Controller:**

```php
$perPage = in_array((int) $request->input('per_page', 25), [10, 25, 50, 100])
    ? (int) $request->input('per_page', 25)
    : 25;

$forms = $query->paginate($perPage)->withQueryString();
```

**View** — replace accordion loop with `@foreach($forms as $form)` in a `<tbody>`.
Add pagination footer:

```blade
{{-- Footer: showing range + Laravel pagination --}}
<div class="d-flex justify-content-between align-items-center mt-2">
    <small class="text-muted">
        Showing {{ $forms->firstItem() ?? 0 }}–{{ $forms->lastItem() ?? 0 }}
        of {{ $forms->total() }} Web Form{{ $forms->total() !== 1 ? 's' : '' }}
    </small>
    {{ $forms->links('pagination::bootstrap-5') }}
</div>
```

The items-per-page selector is exposed via the gear icon dropdown (G5).

---

## 5. Full Index Method Rewrite (WebFormController.php)

Replace the existing `index()` method with:

```php
public function index(Request $request)
{
    try {
        $companyId = (int) Auth::user()->company_id;

        $search  = trim($request->input('search', ''));
        $groupId = (int) $request->input('group_id', 0);
        $perPage = in_array((int) $request->input('per_page', 25), [10, 25, 50, 100])
                   ? (int) $request->input('per_page', 25) : 25;

        $allowedSorts = ['name', 'group_name', 'form_sender_name', 'created_by_name', 'created_at', 'updated_at'];
        $sortBy  = in_array($request->input('sort_by'), $allowedSorts)
                   ? $request->input('sort_by') : 'created_at';
        $sortDir = strtolower($request->input('sort_dir', 'desc')) === 'asc' ? 'asc' : 'desc';

        $sortColumn = match ($sortBy) {
            'group_name'       => 'wfg.name',
            'form_sender_name' => DB::raw("TRIM(CONCAT(COALESCE(sender.first_name,''), ' ', COALESCE(sender.last_name,'')))"),
            'created_by_name'  => DB::raw("TRIM(CONCAT(COALESCE(creator.first_name,''), ' ', COALESCE(creator.last_name,'')))"),
            default            => 'web_forms.' . $sortBy,
        };

        $query = WebForm::query()
            ->where('web_forms.company_id', $companyId)
            ->leftJoin('web_form_groups as wfg', 'web_forms.web_form_group_id', '=', 'wfg.id')
            ->leftJoin('users as sender',  'web_forms.assigned_user_id',   '=', 'sender.id')
            ->leftJoin('users as creator', 'web_forms.created_by_user_id', '=', 'creator.id')
            ->with(['esignMaps.document'])
            ->select([
                'web_forms.*',
                'wfg.name as group_name',
                DB::raw("TRIM(CONCAT(COALESCE(sender.first_name,''), ' ', COALESCE(sender.last_name,'')))  AS form_sender_name"),
                DB::raw("TRIM(CONCAT(COALESCE(creator.first_name,''), ' ', COALESCE(creator.last_name,''))) AS created_by_name"),
            ])
            ->when($search !== '', fn($q) => $q->where('web_forms.name', 'like', '%' . $search . '%'))
            ->when($groupId > 0,   fn($q) => $q->where('web_forms.web_form_group_id', $groupId))
            ->orderBy($sortColumn, $sortDir);

        $forms      = $query->paginate($perPage)->withQueryString();
        $totalForms = WebForm::where('company_id', $companyId)->count();
        $groups     = WebFormGroup::where('company_id', $companyId)
                        ->orderBy('display_order')->orderBy('id')
                        ->get(['id', 'name']);

        $breadcrumb = [
            route('manages.web-forms.index') => 'Web Forms',
        ];

        $headerOption = Controller::getHeaderOptions(
            'Web Forms',
            'Manage Web Forms & Groups',
            [],
            [],
            false,
            [
                '<a href="' . route('manages.web-form-groups.create') . '" class="btn btn-success show-modal"><i class="bx bx-plus"></i> New Form Group</a>',
            ]
        );

        return view(
            'admin.manages.web-forms.index',
            compact('forms', 'groups', 'totalForms', 'headerOption', 'breadcrumb',
                    'search', 'groupId', 'sortBy', 'sortDir', 'perPage')
        );
    } catch (\Exception $e) {
        ErrorLog::Log($e);
        return redirect()->back()->with('error', $e->getMessage());
    }
}
```

---

## 6. Full Index View Rewrite (index.blade.php)

Replace the existing file entirely with a flat-table layout:

```blade
@extends('admin.layouts.layout', $headerOption)
@section('content')

<div class="project-box">

    {{-- Toolbar --}}
    <form method="GET" action="{{ route('manages.web-forms.index') }}" id="wfFilterForm">
        <input type="hidden" name="sort_by"  value="{{ $sortBy }}">
        <input type="hidden" name="sort_dir" value="{{ $sortDir }}">
        <input type="hidden" name="per_page" value="{{ $perPage }}">

        <div class="heading-btn-sec px-3 pt-3 pb-2 resp-flex-column">
            {{-- Count --}}
            <span class="text-muted small">
                Viewing <strong>{{ $totalForms }}</strong> Web Form{{ $totalForms !== 1 ? 's' : '' }}
            </span>

            <div class="heading-btn mob-resp gap-2">
                {{-- Group filter --}}
                <select name="group_id" class="form-select form-select-sm"
                        onchange="document.getElementById('wfFilterForm').submit()" style="min-width:160px;">
                    <option value="0" {{ $groupId == 0 ? 'selected' : '' }}>All Groups</option>
                    @foreach($groups as $g)
                        <option value="{{ $g->id }}" {{ $groupId == $g->id ? 'selected' : '' }}>
                            {{ $g->name }}
                        </option>
                    @endforeach
                </select>

                {{-- Search + Gear --}}
                <div class="search-field d-flex align-items-center gap-1">
                    <input type="text" name="search" class="form-control form-control-sm module-search"
                           placeholder="Search by form name…"
                           value="{{ $search }}" autocomplete="off"
                           id="wfSearchInput">
                    <i class="{{ \Config::get('settings.icon_search') }}" style="pointer-events:none;"></i>

                    {{-- Gear icon (WF_016) --}}
                    <div class="dropdown">
                        <button type="button"
                                class="btn btn-sm btn-outline-secondary"
                                data-bs-toggle="dropdown"
                                aria-expanded="false"
                                title="Table settings">
                            <i class="bx bx-cog"></i>
                        </button>
                        <ul class="dropdown-menu dropdown-menu-end p-2" style="min-width:160px;">
                            <li><label class="form-label small mb-1 fw-semibold">Items per page</label></li>
                            @foreach([10, 25, 50, 100] as $pp)
                            <li>
                                <a class="dropdown-item small {{ $perPage == $pp ? 'active' : '' }}"
                                   href="{{ request()->fullUrlWithQuery(['per_page' => $pp, 'page' => 1]) }}">
                                    {{ $pp }} per page
                                </a>
                            </li>
                            @endforeach
                        </ul>
                    </div>
                </div>

                {{-- Clear search button --}}
                @if($search)
                <a href="{{ request()->fullUrlWithQuery(['search' => '', 'page' => 1]) }}"
                   class="btn btn-sm btn-outline-secondary" title="Clear search">
                    <i class="bx bx-x"></i>
                </a>
                @endif
            </div>
        </div>
    </form>

    {{-- Table --}}
    @php
        function wfSortLink(string $col, string $label, string $curSort, string $curDir): string {
            $nextDir = ($curSort === $col && $curDir === 'asc') ? 'desc' : 'asc';
            $icon    = $curSort === $col ? ($curDir === 'asc' ? '↑' : '↓') : '⇅';
            $url     = request()->fullUrlWithQuery(['sort_by' => $col, 'sort_dir' => $nextDir, 'page' => 1]);
            return '<a href="' . $url . '" class="text-dark text-decoration-none">'
                 . e($label) . ' <small class="text-muted">' . $icon . '</small></a>';
        }
    @endphp

    <div class="table-responsive px-1">
        <table class="table table-hover table-sm mb-0">
            <thead class="table-light">
                <tr>
                    <th>{!! wfSortLink('name',            'Form Name',   $sortBy, $sortDir) !!}</th>
                    <th>{!! wfSortLink('group_name',      'Group',       $sortBy, $sortDir) !!}</th>
                    <th>{!! wfSortLink('form_sender_name','Form Sender', $sortBy, $sortDir) !!}</th>
                    <th>E-Sign Docs</th>
                    <th>Status</th>
                    <th>{!! wfSortLink('created_by_name', 'Created By',  $sortBy, $sortDir) !!}</th>
                    <th>{!! wfSortLink('created_at',      'Created At',  $sortBy, $sortDir) !!}</th>
                    <th>{!! wfSortLink('updated_at',      'Updated At',  $sortBy, $sortDir) !!}</th>
                    <th class="text-end">Actions</th>
                </tr>
            </thead>
            <tbody>
                @forelse($forms as $form)
                <tr>
                    <td><strong>{{ $form->name }}</strong></td>
                    <td class="small text-muted">{{ $form->group_name ?: '—' }}</td>
                    <td class="small">{{ $form->form_sender_name ?: 'N/A' }}</td>
                    <td>
                        @forelse($form->esignMaps as $map)
                            @if($map->document)
                                <a href="{{ \Storage::url($map->document->file_path ?? '') }}"
                                   target="_blank"
                                   class="badge bg-warning text-dark text-decoration-none me-1">
                                    {{ $map->document->title }}
                                </a>
                            @endif
                        @empty
                            <span class="text-muted small">None</span>
                        @endforelse
                    </td>
                    <td>
                        @if($form->status)
                            <span class="badge bg-success">Active</span>
                        @else
                            <span class="badge bg-secondary">Inactive</span>
                        @endif
                    </td>
                    <td class="small">{{ $form->created_by_name ?: 'N/A' }}</td>
                    <td class="small text-muted">{{ $form->created_at ? $form->created_at->format('M d, Y') : '—' }}</td>
                    <td class="small text-muted">{{ $form->updated_at ? $form->updated_at->format('M d, Y') : '—' }}</td>
                    <td class="text-end">
                        <div class="d-flex justify-content-end gap-1">
                            {{-- View Sample Form (WF_019/WF_020/WF_033) --}}
                            <a href="{{ route('webform.public.show', $form->public_slug) }}"
                               target="_blank"
                               class="btn btn-xs btn-outline-success"
                               title="View Sample Form">
                                <i class="bx bx-show"></i>
                            </a>
                            {{-- Share --}}
                            <a href="{{ route('manages.web-forms.share', $form->id) }}"
                               class="btn btn-xs btn-outline-secondary show-modal-lg"
                               title="Share">
                                <i class="bx bx-share-alt"></i>
                            </a>
                            {{-- Settings (WF_034) --}}
                            <a href="{{ route('manages.web-forms.edit', $form->id) }}"
                               class="btn btn-xs btn-outline-primary show-modal-lg"
                               title="Settings">
                                <i class="bx bx-cog"></i>
                            </a>
                            {{-- Form Builder --}}
                            <a href="{{ route('manages.web-forms.builder', $form->id) }}"
                               class="btn btn-xs btn-outline-info"
                               title="Form Builder">
                                <i class="bx bx-layout"></i>
                            </a>
                            {{-- Duplicate --}}
                            <button type="button"
                                    class="btn btn-xs btn-outline-warning btn-duplicate-form"
                                    data-id="{{ $form->id }}"
                                    title="Duplicate">
                                <i class="bx bx-copy"></i>
                            </button>
                            {{-- Delete --}}
                            <button type="button"
                                    class="btn btn-xs btn-outline-danger btn-delete-form"
                                    data-id="{{ $form->id }}"
                                    data-name="{{ $form->name }}"
                                    title="Delete">
                                <i class="bx bx-trash"></i>
                            </button>
                        </div>
                    </td>
                </tr>
                @empty
                <tr>
                    <td colspan="9" class="text-center text-muted py-4">
                        <i class="bx bx-folder-open bx-lg"></i>
                        <p class="mt-2 mb-0">
                            @if($search)
                                No web forms match "{{ $search }}".
                            @elseif($groupId)
                                No web forms in the selected group.
                            @else
                                No web forms found.
                                <a href="{{ route('manages.web-form-groups.create') }}" class="show-modal ms-1">Create a group</a>
                            @endif
                        </p>
                    </td>
                </tr>
                @endforelse
            </tbody>
        </table>
    </div>

    {{-- Pagination footer --}}
    @if($forms->hasPages() || $forms->total() > 0)
    <div class="d-flex justify-content-between align-items-center px-3 py-2 border-top">
        <small class="text-muted">
            Showing {{ $forms->firstItem() ?? 0 }}–{{ $forms->lastItem() ?? 0 }}
            of {{ $forms->total() }} result{{ $forms->total() !== 1 ? 's' : '' }}
        </small>
        {{ $forms->links('pagination::bootstrap-5') }}
    </div>
    @endif

</div>

@endsection

@push('page_script')
<script>
(function($) {
    // Live search: debounce and submit
    var searchTimer;
    $('#wfSearchInput').on('input', function() {
        clearTimeout(searchTimer);
        searchTimer = setTimeout(function() {
            $('#wfFilterForm').submit();
        }, 400);
    });

    // Delete form
    $(document).on('click', '.btn-delete-form', function() {
        var id   = $(this).data('id');
        var name = $(this).data('name');
        if (!confirm('Delete form "' + name + '"? This cannot be undone.')) return;
        $.ajax({
            url:    '/admin/manages/web-forms/' + id,
            method: 'POST',
            data:   { _token: '{{ csrf_token() }}', _method: 'DELETE' },
            success: function(res) {
                if (res && res.status === 200) {
                    if (typeof toastr !== 'undefined') toastr.success(res.message);
                    location.reload();
                } else {
                    alert(res.message || 'Failed to delete form.');
                }
            },
            error: function() { alert('An error occurred.'); }
        });
    });

    // Duplicate form
    $(document).on('click', '.btn-duplicate-form', function() {
        var id  = $(this).data('id');
        var btn = $(this);
        btn.prop('disabled', true);
        $.ajax({
            url:    '/admin/manages/web-forms/' + id + '/duplicate',
            method: 'POST',
            data:   { _token: '{{ csrf_token() }}' },
            success: function(res) {
                btn.prop('disabled', false);
                if (res && res.status === 200) {
                    if (typeof toastr !== 'undefined') toastr.success(res.message);
                    location.reload();
                } else {
                    alert(res.message || 'Failed to duplicate form.');
                }
            },
            error: function() { btn.prop('disabled', false); alert('An error occurred.'); }
        });
    });
})(jQuery);
</script>
@endpush
```

---

## 7. Files to Modify

| File | Type | Changes |
|------|------|---------|
| `app/Http/Controllers/Manages/WebFormController.php` | Controller | Full `index()` method rewrite (G1–G3, G9, G10) |
| `resources/views/admin/manages/web-forms/index.blade.php` | Blade View | Full file rewrite (G1–G10) |

No other files require changes. The model, routes, and other controller methods are unaffected.

---

## 8. Test Coverage Map

| Test ID | Fix Group | Covered By |
|---------|-----------|-----------|
| WF_004 | G1 | Breadcrumb passed from controller; `@php` override removed from view |
| WF_005 | G2 | `$totalForms` count in toolbar |
| WF_006 | G3 | Group filter `<select>` in toolbar |
| WF_007 | G3 | Default value `"All Groups"` (value=0) on group select |
| WF_008 | G3 | `where('web_form_group_id', $groupId)` filter |
| WF_009 | G3 | `@empty` row in table shows no-records message |
| WF_010 | G4 | Search input visible in toolbar |
| WF_011 | G4 | `LIKE '%{$search}%'` on `web_forms.name` |
| WF_012 | G4 | Partial `%keyword%` match |
| WF_013 | G4 | MySQL LIKE is case-insensitive on default collation |
| WF_014 | G4 | Zero rows returned, `@empty` message shown |
| WF_015 | G4 | Empty `$search` = no filter = full list |
| WF_016 | G5 | Gear icon `bx-cog` button next to search |
| WF_019 | G6 | `bx-show` button present in every row |
| WF_020 | G6 | `target="_blank"` opens public form URL |
| WF_025 | G7 | `form_sender_name` column (from `assigned_user_id`) |
| WF_026 | G7 | E-sign docs as `<a>` links |
| WF_027 | G7 | `created_by_name` column |
| WF_029 | G7 | `updated_at` column formatted `M d, Y` |
| WF_033 | G8 | `bx-show` "View Sample Form" + `bx-share-alt` "Share" both present |
| WF_034 | G8 | Edit icon changed to `bx-cog`, title "Settings" |
| WF_043 | G9 | Sort link on `name` column |
| WF_044 | G9 | Sort link on `group_name` column (joined) |
| WF_045 | G9 | Sort link on `form_sender_name` column (joined) |
| WF_046 | G9 | Sort link on `created_by_name` column (joined) |
| WF_047 | G9 | Sort link on `created_at` column |
| WF_048 | G9 | Sort link on `updated_at` column |
| WF_049 | G10 | Per-page dropdown in gear menu; `paginate($perPage)` |
| WF_050 | G10 | `$forms->links()` pagination controls; prev/next work |
