# PRD: SMS Template Module — Bug Fixes

**Date:** 2026-04-29
**Module:** SMS Template (`manages.sms-templates`)
**Failing Test Cases:** SMS-TC-002, SMS-TC-019, SMS-TC-020, SMS-TC-029, SMS-TC-030, SMS-TC-031, SMS-TC-032, SMS-TC-034

---

## Overview

Eight QA test cases fail in the SMS Template module. They map to six logical fix groups covering missing UI elements, absent validation, no real-time counters, broken content preview, and a broken permission button.

---

## Fix 1 — Back to Administration Button (SMS-TC-002)

### Failing Test
| ID | Priority | Scenario |
|----|----------|----------|
| SMS-TC-002 | Medium | Click "Back to Administration" → user is redirected to manage page |

### Root Cause
`resources/views/admin/manages/sms-templates/index.blade.php` has no back button. The `<div class="back-section mb-2">` block that exists in the email template module is absent here.

### Affected File
- `resources/views/admin/manages/sms-templates/index.blade.php`

### Fix
Add the back button immediately before the opening `<div class="project-box">` (line 6):

```blade
<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 Administration
    </button>
</div>
```

---

## Fix 2 — Duplicate Template Title Rejection (SMS-TC-019)

### Failing Test
| ID | Priority | Scenario |
|----|----------|----------|
| SMS-TC-019 | High | Create a template with an existing title → system rejects the duplicate |

### Root Cause
`__formPost()` in `SmsTemplateController` validates `template_title` only as `required|max:50`. There is no duplicate check. Two SMS templates with identical titles can be created freely.

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

### Fix
In `__formPost()`, after `$this->validate(...)` passes, add a case-insensitive duplicate check that excludes the current record when editing:

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

    $duplicate = \App\Models\Masters\SiteTemplate::whereRaw('LOWER(template_title) = LOWER(?)', [$request->template_title])
        ->where('company_id', \Auth::user()->company_id)
        ->where('template_type', 2)
        ->whereNull('deleted_at')
        ->when($id, fn($q) => $q->where('id', '!=', $id))
        ->exists();

    if ($duplicate) {
        return redirect()->back()
            ->withInput()
            ->withErrors(['template_title' => 'A template with this title already exists.']);
    }

    try {
        $input    = $request->all();
        $response = $this->_model->store($input, $id, $request);
        if (in_array($response['status'], [200, 201])) {
            return redirect()
                ->route($this->_routePrefix . '.index', ['gr' => $response['data']['template_category']])
                ->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());
    }
}
```

**Key details:**
- `template_type = 2` scopes the duplicate check to SMS templates only (not email/PDF)
- `whereNull('deleted_at')` ensures soft-deleted templates don't block new ones with the same name
- `when($id, ...)` skips self-comparison during an update
- Validation is moved outside the try/catch so `ValidationException` propagates normally

---

## Fix 3 — Max Title Length Client-side Enforcement (SMS-TC-020)

### Failing Test
| ID | Priority | Scenario |
|----|----------|----------|
| SMS-TC-020 | Medium | Enter a very long title → system restricts or shows validation message |

### Root Cause
The `template_title` field definition in `__formUiGeneration()` has no `maxlength` HTML attribute. Server-side validation enforces `max:50`, but there is no client-side restriction. The user can type an unlimited number of characters with no visual feedback until form submission fails.

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

### Fix
Add `'maxlength' => 50` to the `template_title` field attributes in `__formUiGeneration()`:

```php
'template_title' => [
    'type'       => 'text',
    'label'      => 'SMS Template Title',
    'help'       => 'Maximum 50 characters',
    'attributes' => ['required' => true, 'maxlength' => 50],
    'value'      => isset($data->template_title) ? $data->template_title : '',
],
```

---

## Fix 4 — SMS Character and Segment Counter (SMS-TC-029, SMS-TC-030, SMS-TC-031)

### Failing Tests
| ID | Priority | Scenario |
|----|----------|----------|
| SMS-TC-029 | High | Type message → Chars count updates in real time |
| SMS-TC-030 | High | Enter long SMS text → Message(s) count increases correctly |
| SMS-TC-031 | Medium | Add `{%placeholder%}` → character/message counts exclude it |

### Root Cause
`sms-template.js` contains only special-field insertion logic. There is no character or segment counter. The form has no counter UI element. The textarea `template_content_sms` provides no feedback on SMS length.

**SMS segment rules:**
- 1 segment = up to 160 characters (GSM 7-bit encoding assumed)
- Multi-segment: each segment holds 153 characters (7 chars used for UDH concatenation header)

### Affected Files
- `app/Http/Controllers/Manages/SmsTemplateController.php`
- `public/assets/js/sms-template.js`

### Fix A — Controller: Add counter display HTML field

In `__formUiGeneration()`, add an `html` field after `template_content_sms`:

```php
'sms_counter' => [
    'type'  => 'html',
    'label' => '',
    'value' => '<div id="sms-counter" class="text-muted small mt-1 mb-2">
        Chars: <strong id="sms-char-count">0</strong> &nbsp;|&nbsp;
        Message(s): <strong id="sms-msg-count">1</strong>
        <span class="ms-2" style="color:#f0ad4e;">
            <i class="mdi mdi-information-outline"></i>
            Special field placeholders (e.g. {%first_name%}) are excluded from the count.
        </span>
    </div>',
],
```

### Fix B — JS: Add real-time counter logic to `sms-template.js`

Append to `public/assets/js/sms-template.js`:

```javascript
// SMS character and segment counter
$(document).ready(function () {
    var $textarea = $('[name="template_content_sms"]');

    function updateSmsCounter() {
        var text = $textarea.val();

        // Exclude special field placeholders {%any_field%} from the count
        var cleaned = text.replace(/\{%[^%]+%\}/g, '');

        var chars = cleaned.length;
        var messages = chars <= 160 ? 1 : Math.ceil(chars / 153);

        $('#sms-char-count').text(chars);
        $('#sms-msg-count').text(messages);
    }

    // Update on every keystroke
    $(document).on('input', '[name="template_content_sms"]', updateSmsCounter);

    // Run once on load to handle pre-filled edit values
    updateSmsCounter();
});
```

**After inserting a special field, the counter must also refresh.** Update the existing `add-special-field` click handler to call `updateSmsCounter()` (or trigger the `input` event on the textarea) after inserting the tag:

```javascript
$(document).on('click', '.add-special-field', function (e) {
    e.preventDefault();

    const selectedField = $('[name="special_fields"]').val();
    const $textarea = $('[name="template_content_sms"]');

    if (!selectedField) return;

    const tag = `{%${selectedField}%}`;
    const textareaEl = $textarea[0];
    const startPos = textareaEl.selectionStart;
    const endPos = textareaEl.selectionEnd;
    const textBefore = $textarea.val().substring(0, startPos);
    const textAfter = $textarea.val().substring(endPos);

    $textarea.val(textBefore + tag + textAfter);

    const cursorPos = startPos + tag.length;
    textareaEl.setSelectionRange(cursorPos, cursorPos);
    textareaEl.focus();

    // Refresh counter after inserting a placeholder
    $textarea.trigger('input');
});
```

Replace the existing `.add-special-field` handler in `sms-template.js` entirely with the above.

---

## Fix 5 — Preview Content Rendering (SMS-TC-032)

### Failing Test
| ID | Priority | Scenario |
|----|----------|----------|
| SMS-TC-032 | High | Click Preview → popup displays selected SMS template content correctly |

### Root Cause
Two issues:

1. **Missing `nl2br`**: The preview `showForm` in `__formUiGeneration()` renders `$data->template_content` as raw HTML concatenation. SMS content is plain text — newlines are not converted to `<br>` tags, so multi-line SMS messages appear as a single squashed line.

2. **Empty `show.blade.php`**: The `show()` controller method routes to `resources/views/admin/manages/sms-templates/show.blade.php`, which is an empty file. If a user navigates to the show route directly (non-AJAX), they see a blank page.

### Affected Files
- `app/Http/Controllers/Manages/SmsTemplateController.php`
- `resources/views/admin/manages/sms-templates/show.blade.php`

### Fix A — Controller: Use `nl2br` + `e()` for preview content

In the `showForm` block inside `__formUiGeneration()`, replace the raw content output:

```php
if ($id && $type && $type == 'show') {
    $showForm = [
        'form_required' => false,
        'fields' => [
            'view_content' => [
                'type'  => 'html',
                'label' => '',
                'value' => '<div style="display:inline-block;max-width:100%;overflow:auto;white-space:pre-wrap;">'
                    . (!empty($data->template_content)
                        ? nl2br(e($data->template_content))
                        : '<em>No content available</em>')
                    . '</div>',
            ],
            'buttons' => [
                'type'  => 'html',
                'value' => '<a href="javascript:void(0)" class="' . \Config::get('view.buttons.secondary') . '" data-bs-dismiss="modal">Cancel</a>',
            ],
        ],
    ];

    $moduleTitle = 'Preview — ' . e($data->template_title);
    $this->_data['form'] = $showForm;
}
```

**Key changes:**
- `nl2br(e($data->template_content))`: escapes HTML entities first, then converts `\n` to `<br>` so line breaks are visible
- `white-space:pre-wrap` on the container also ensures spacing is respected
- `e($data->template_title)` escapes special characters in the modal title

### Fix B — `show.blade.php`: Add non-AJAX fallback

Replace the empty file with a minimal fallback that renders the template's content for direct page access:

```blade
@extends('admin.layouts.layout')
@section('content')
    <div class="col-12">
        <div class="project-box">
            <h5>{{ $data->first()->template_title ?? 'SMS Template' }}</h5>
            <div style="white-space:pre-wrap;">
                {!! nl2br(e($data->first()->template_content ?? '')) !!}
            </div>
        </div>
    </div>
@endsection
```

---

## Fix 6 — Permission Button Display (SMS-TC-034)

### Failing Test
| ID | Priority | Scenario |
|----|----------|----------|
| SMS-TC-034 | Medium | Click Permissions button → opens permission popup for role/user configuration |

### Root Cause
In `index()`, `permissionForm` permission is fetched via:
```php
$managePermissions = Permission::checkModulePermissions(['permissionForm'], 'SmsTemplateController');
$this->_data['permission'] = array_merge($this->_data['permission'], $manageRole, $managePermissions);
```

If the `permissionForm` permission record is not registered in the database for `SmsTemplateController`, `checkModulePermissions` may return `['permissionForm' => false]` or an empty array. After the merge, `$permission['permissionForm']` is falsy, so the Permissions button is never rendered in the view. This matches the same root cause as the `FileForm` fix applied to the email template module.

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

### Fix
After the permission merge in `index()`, add the same fallback pattern used in the email template:

```php
$this->_data['permission'] = array_merge(
    $this->_data['permission'],
    $manageRole,
    $managePermissions
);

// Fallback: if permissionForm is not explicitly set, inherit from edit permission
$this->_data['permission']['permissionForm'] =
    $this->_data['permission']['permissionForm']
    ?? $this->_data['permission']['edit']
    ?? false;
```

This ensures users with `edit` access can still access the permission form even if `permissionForm` is not separately registered in the permissions table.

---

## Summary

| Fix | Test Cases | Files Changed |
|-----|------------|---------------|
| Fix 1: Back button | SMS-TC-002 | `sms-templates/index.blade.php` |
| Fix 2: Duplicate title check | SMS-TC-019 | `SmsTemplateController.php` |
| Fix 3: maxlength attribute | SMS-TC-020 | `SmsTemplateController.php` |
| Fix 4: Character/segment counter | SMS-TC-029, SMS-TC-030, SMS-TC-031 | `SmsTemplateController.php`, `sms-template.js` |
| Fix 5: Preview nl2br + show.blade | SMS-TC-032 | `SmsTemplateController.php`, `show.blade.php` |
| Fix 6: Permission button fallback | SMS-TC-034 | `SmsTemplateController.php` |

**Total files:** 4
