worked on short code and sender ID modules

This commit is contained in:
Kwesi Banson Jnr
2026-08-19 11:55:44 +00:00
parent 2cda902a37
commit ab41e10445
10 changed files with 1397 additions and 466 deletions

View File

@@ -3,14 +3,141 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models;
class SenderidsController extends Controller
{
public function index()
public function index(Request $request)
{
// Fetch KPI Metrics (Keep your existing counts)
// $totalCount = \App\Models\SenderId::count();
// $approvedCount = \App\Models\SenderId::where('status', 'APPROVED')->count();
// $pendingCount = \App\Models\SenderId::where('status', 'PENDING')->count();
// $rejectedCount = \App\Models\SenderId::where('status', 'REJECTED')->count();
$totalCount = \App\Models\SenderId::count();
$approvedCount = \App\Models\SenderId::where('status', 'LIKE', 'Approved%')->count();
$pendingCount = \App\Models\SenderId::where('status', 'LIKE', 'Applied%')->count();
$rejectedCount = \App\Models\SenderId::where('status', 'Rejected')->count();
// Fetch dynamic lists for the modal dropdowns
$clients = Models\Client::orderBy('name', 'asc')->get();
$networkOperators = Models\NetworkOperator::orderBy('name', 'asc')->get();
// Start the query
$query = \App\Models\SenderId::query();
// 1. Search Filter (Sender ID, MNO, or Supplier)
if ($request->filled('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('senderid', 'like', "%{$search}%")
->orWhere('mno_name', 'like', "%{$search}%")
->orWhere('supplier_name', 'like', "%{$search}%");
});
}
// 2. Direct MNO Filter
if ($request->filled('direct_mno')) {
$query->where('direct_mno', $request->direct_mno);
}
// 3. Status Filter
if ($request->filled('status')) {
$query->where('status', 'LIKE', '%' . $request->status . '%');
}
// Execute query with pagination
$senderIds = $query->orderBy('created_at', 'desc')->paginate(10);
// Crucial: Append the current request query strings to pagination links
$senderIds->appends($request->query());
$data = [
'page_title' => 'Dashboard'
'page_title' => 'Sender IDs',
'totalCount' => $totalCount,
'approvedCount' => $approvedCount,
'pendingCount' => $pendingCount,
'rejectedCount' => $rejectedCount,
'senderIds' => $senderIds,
'clients' => $clients, // <-- Pass Clients
'networkOperators' => $networkOperators, // <-- Pass MNOs
];
return view('senderids.index', $data);
}
public function store(Request $request)
{
$validated = $request->validate([
'senderid' => 'required|string|max:20',
'direct_mno' => 'required|in:YES,NO',
// Conditional Validation Rules
'mno_name' => 'required_if:direct_mno,YES|nullable|string|max:192',
'supplier_name' => 'required_if:direct_mno,NO|nullable|string|max:192',
'bind_name' => 'nullable|string|max:50',
'type' => 'nullable|string|max:25',
'status' => 'nullable|string|max:154',
'remarks' => 'nullable|string|max:250',
], [
'mno_name.required_if' => 'The MNO Name is required when routing directly to an MNO.',
'supplier_name.required_if' => 'The Supplier Name is required when not routing directly to an MNO.',
]);
// Attach creator ID and save
$validated['created_by'] = auth()->id();
// Create the Sender ID
$senderId = \App\Models\SenderId::create($validated);
return response()->json(['success' => true, 'message' => 'Sender ID added successfully!']);
}
public function update(Request $request, $id)
{
$senderId = Models\SenderId::findOrFail($id);
$validated = $request->validate([
'senderid' => 'required|string|max:20',
'direct_mno' => 'required|in:YES,NO',
// Conditional validation rules for update as well
'mno_name' => 'required_if:direct_mno,YES|nullable|string|max:192',
'supplier_name' => 'required_if:direct_mno,NO|nullable|string|max:192',
'bind_name' => 'nullable|string|max:50',
'type' => 'nullable|string|max:25',
'status' => 'nullable|string|max:154',
'remarks' => 'nullable|string|max:250',
], [
'mno_name.required_if' => 'The MNO Name is required when routing directly to an MNO.',
'supplier_name.required_if' => 'The Supplier Name is required when not routing directly to an MNO.',
]);
// Clear out the opposite field depending on the Direct MNO choice to keep data clean
if ($validated['direct_mno'] === 'YES') {
$validated['supplier_name'] = null;
} else {
$validated['mno_name'] = null;
}
$validated['last_modified_by'] = auth()->id();
$senderId->update($validated);
return response()->json([
'success' => true,
'message' => 'Sender ID updated successfully!'
]);
}
public function destroy($id)
{
$senderId = SenderId::findOrFail($id);
$senderId->delete();
return redirect()->route('sender-ids.index')->with('success', 'Sender ID deleted successfully.');
}
}

View File

@@ -2,15 +2,119 @@
namespace App\Http\Controllers;
use App\Models\ShortCode;
use App\Models\Client;
use App\Models\NetworkOperator;
use App\Models\Country;
use Illuminate\Http\Request;
class ShortCodesController extends Controller
{
public function index()
public function index(Request $request)
{
// KPI Metrics
$totalCount = ShortCode::count();
$smsCount = ShortCode::where('code_type', 'sms')->count();
$ussdCount = ShortCode::where('code_type', 'ussd')->count();
$voiceCount = ShortCode::where('code_type', 'voice')->count();
$countries = Country::orderBy('en_short_name', 'asc')->get();
// dd($countries);
// Dropdown options
$clients = Client::orderBy('name', 'asc')->get();
$networkOperators = NetworkOperator::orderBy('name', 'asc')->get();
$query = ShortCode::query();
// Search Filter
if ($request->filled('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('shortcode', 'like', "%{$search}%")
->orWhere('name', 'like', "%{$search}%")
->orWhere('toll_free', 'like', "%{$search}%")
->orWhere('country', 'like', "%{$search}%")
->orWhere('network', 'like', "%{$search}%")
->orWhere('code_type', 'like', "%{$search}%");
});
}
// Status Filter
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Execute query with pagination (Fixed variable name from $senderIds to $shortcodes)
$shortcodes = $query->orderBy('created_at', 'desc')->paginate(10);
$shortcodes->appends($request->query());
$data = [
'page_title' => 'Dashboard'
'page_title' => 'Short Codes Management',
'totalCount' => $totalCount,
'smsCount' => $smsCount,
'ussdCount' => $ussdCount,
'voiceCount' => $voiceCount,
'shortcodes' => $shortcodes,
'clients' => $clients,
'countries' => $countries,
'networkOperators' => $networkOperators,
];
return view('shortcodes.index', $data);
}
public function store(Request $request)
{
$validated = $request->validate([
'client_id' => 'nullable|integer',
'name' => 'nullable|string|max:192',
'shortcode' => 'required|string|max:20',
'code_type' => 'nullable|string|max:15',
'toll_free' => 'nullable|string|max:10',
'monthly_fee' => 'nullable|numeric',
'country' => 'nullable|string|max:45',
'network' => 'nullable|string|max:45',
'status' => 'nullable|string|max:15',
'launch_date' => 'nullable|date',
'expiry_date' => 'nullable|date',
'remarks' => 'nullable|string|max:250',
]);
ShortCode::create($validated);
return response()->json(['success' => true, 'message' => 'Short code created successfully!']);
}
public function update(Request $request, $id)
{
$shortcode = ShortCode::findOrFail($id);
$validated = $request->validate([
'client_id' => 'nullable|integer',
'name' => 'nullable|string|max:192',
'shortcode' => 'required|string|max:20',
'code_type' => 'nullable|string|max:15',
'toll_free' => 'nullable|string|max:10',
'monthly_fee' => 'nullable|numeric',
'country' => 'nullable|string|max:45',
'network' => 'nullable|string|max:45',
'status' => 'nullable|string|max:15',
'launch_date' => 'nullable|date',
'expiry_date' => 'nullable|date',
'remarks' => 'nullable|string|max:250',
]);
$validated['last_updated_by'] = auth()->id();
$shortcode->update($validated);
return response()->json(['success' => true, 'message' => 'Short code updated successfully!']);
}
public function destroy($id)
{
$shortcode = ShortCode::findOrFail($id);
$shortcode->delete();
return redirect()->route('shortcodes.index')->with('success', 'Short code deleted successfully.');
}
}

View File

@@ -7,5 +7,5 @@ use Illuminate\Database\Eloquent\Model;
class Country extends Model
{
protected $guarded = array('id');
public $table = "countries";
public $table = "countries_new";
}

View File

@@ -7,15 +7,15 @@ use Illuminate\Database\Eloquent\Model;
class ShortCode extends Model
{
protected $guarded = array('id');
public $table = "short_codes";
public $table = "client_short_codes";
public function client_info(){
return $this->hasOne('App\Models\Client', 'id', 'client_id');
}
public function update_info(){
return $this->hasOne('App\Models\SystemUser', 'id', 'last_updated_by');
return $this->hasOne('App\Models\StaffMember', 'id', 'last_updated_by');
}
public function account_mgr_info(){
return $this->hasOne('App\Models\SystemUser', 'id', 'account_manager_id');
return $this->hasOne('App\Models\StaffMember', 'id', 'account_manager_id');
}
}

View File

@@ -0,0 +1,197 @@
$(document).ready(function() {
const senderModal = new bootstrap.Modal(document.getElementById('senderModal'));
const $form = $('#senderForm');
$('#edit_mno_name').select2({
placeholder: "Select Network Operator",
allowClear: true,
width: '100%',
dropdownParent: $('#senderModal') // Crucial for Bootstrap Modals
});
$('#edit_supplier_name').select2({
placeholder: "Select Supplier",
allowClear: true,
width: '100%',
dropdownParent: $('#senderModal') // Crucial for Bootstrap Modals
});
$('#edit_senderid').on('input', function() {
// $(this).val($(this).val().toUpperCase().replace(/\s/g, ''));
//$(this).val($(this).val().replace(/\s/g, ''));
const max = 11;
const val = $(this).val();
if (val.length > max) {
$(this).val(val.slice(0, max));
}
});
// Function to handle the form layout based on Direct MNO selection
function toggleSenderIdRoutingFields(selection, isEdit = false) {
let prefix = isEdit ? 'edit_' : '';
let $mnoWrapper = $('#wrapper_mno_name' + (isEdit ? '_edit' : ''));
let $supplierWrapper = $('#wrapper_supplier_name' + (isEdit ? '_edit' : ''));
let $mnoInput = $('#input_mno_name' + (isEdit ? '_edit' : ''));
let $supplierInput = $('#input_supplier_name' + (isEdit ? '_edit' : ''));
if (selection === 'YES') {
// Show MNO, Hide Supplier
$mnoWrapper.slideDown();
$mnoInput.prop('required', true);
$supplierWrapper.slideUp();
$supplierInput.prop('required', false).val(''); // Clear hidden field
} else {
// Show Supplier, Hide MNO
$supplierWrapper.slideDown();
$supplierInput.prop('required', true);
$mnoWrapper.slideUp();
$mnoInput.prop('required', false).val(''); // Clear hidden field
}
}
// Trigger on change for Create Form
$('#create_direct_mno').on('change', function() {
toggleSenderIdRoutingFields($(this).val(), false);
});
// If you have an Edit form, trigger it there as well
// $('#edit_direct_mno').on('change', function() {
// toggleSenderIdRoutingFields($(this).val(), true);
// });
// Run on initial load to ensure correct state
toggleSenderIdRoutingFields($('#create_direct_mno').val(), false);
// Bind change event to the select dropdown
$('#edit_direct_mno').on('change', function() {
toggleSenderIdRoutingFields($(this).val());
});
// --- OPEN MODAL FOR CREATE ---
$('#btnOpenSenderModal').on('click', function() {
$form[0].reset();
$('#senderRecordId').val('');
$('#senderModalTitle').text('Add Sender ID');
$('#btnSubmitSender').html('<i class="bi bi-send me-2"></i>Save Sender ID');
// Reset toggle state to YES
$('#edit_direct_mno').val('YES').trigger('change');
senderModal.show();
});
// --- OPEN MODAL FOR EDIT ---
$('.btn-edit-sender').on('click', function() {
const data = $(this).data();
$form[0].reset();
$('#senderRecordId').val(data.id);
$('#edit_senderid').val(data.senderid);
$('#edit_direct_mno').val(data.direct_mno);
// Set dependent fields before toggling
$('#edit_mno_name').val(data.mno_name);
$('#edit_supplier_name').val(data.supplier_name);
$('#edit_type').val(data.type);
$('#edit_bind_name').val(data.bind_name);
$('#edit_status').val(data.status);
$('#edit_remarks').val(data.remarks);
$('#senderModalTitle').text('Edit Sender ID: ' + data.senderid);
$('#btnSubmitSender').html('<i class="bi bi-save me-2"></i>Update Sender ID');
// Trigger the toggle layout based on the loaded data
toggleSenderIdRoutingFields(data.direct_mno);
senderModal.show();
});
// --- HANDLE FORM SUBMISSION (Actual AJAX) ---
$form.on('submit', function(e) {
e.preventDefault();
const $btn = $('#btnSubmitSender');
const originalText = $btn.html();
const $alertBox = $('#senderIdModalAlert');
// Show loading state and clear old errors
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span> Processing...');
$btn.prop('disabled', true);
$alertBox.html('');
$form.find('.is-invalid').removeClass('is-invalid');
$form.find('.invalid-feedback').remove();
// Determine if we are creating or updating based on the hidden ID field
const recordId = $('#senderRecordId').val();
const submitUrl = base_url + "/" + recordId ? "senderids/" + recordId : "senderids";
// Add a hidden _method field for PUT requests if editing
let formData = $form.serialize();
if (recordId) {
formData += '&_method=PUT';
}
$.ajax({
url: submitUrl,
type: 'POST', // We use POST but pass _method=PUT in the data payload for Laravel
data: formData,
success: function(response) {
if (response.success) {
senderModal.hide();
Swal.fire({
icon: 'success',
title: 'Success!',
text: response.message || 'Sender ID record saved.',
confirmButtonColor: '#5c4df0'
}).then(() => {
// Reload the page to refresh the table and KPI counts
location.reload();
});
}
},
error: function(xhr) {
// Handle Laravel Validation Errors
if (xhr.status === 422) {
let errors = xhr.responseJSON.errors;
$alertBox.html(`
<div class="alert alert-danger d-flex align-items-center py-2 px-3 small" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<div>Please check the highlighted fields below.</div>
</div>
`);
// Highlight specific invalid fields
$.each(errors, function(key, value) {
let $input = $form.find('[name="' + key + '"]');
if ($input.length) {
$input.addClass('is-invalid');
$input.parent().append('<div class="invalid-feedback d-block fw-medium small">' + value[0] + '</div>');
}
});
} else {
// Handle standard 500 server errors
$alertBox.html(`
<div class="alert alert-danger d-flex align-items-center py-2 px-3 small" role="alert">
<i class="bi bi-x-circle-fill me-2"></i>
<div>An unexpected server error occurred. Please try again.</div>
</div>
`);
}
},
complete: function() {
// Restore button state
$btn.html(originalText).prop('disabled', false);
}
});
});
});

View File

@@ -3,7 +3,22 @@
@section('title', 'Click ERP - SMS Sender IDs')
@push('styles')
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<style>
/* Keep your existing styles here... */
/* Select2 Bootstrap 5 / Custom tweaks */
.select2-container .select2-selection--single {
height: 38px;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
display: flex;
align-items: center;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
}
.network-badge { font-size: 0.65rem; padding: 0.2rem 0.4rem; border-radius: 4px; border: 1px solid #e2e8f0; font-weight: 600; display: inline-flex; align-items: center; gap: 3px; }
.net-approved { background-color: rgba(16, 185, 129, 0.1); color: #10b981; border-color: rgba(16, 185, 129, 0.2); }
.net-pending { background-color: rgba(245, 158, 11, 0.1); color: #f59e0b; border-color: rgba(245, 158, 11, 0.2); }
@@ -29,34 +44,35 @@
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Manage alphanumeric masks, shortcodes, and MNO registration statuses.</p>
</div>
<button class="btn text-white fw-bold d-flex align-items-center" style="background-color: #5c4df0; border-radius: 8px;" id="btnOpenSenderModal">
<i class="bi bi-plus-lg me-2"></i> Request Sender ID
<i class="bi bi-plus-lg me-2"></i> Add Sender ID
</button>
</div>
<!-- KPIs -->
<!-- KPIs -->
<div class="row g-4 mb-4">
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #5c4df0 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">TOTAL SENDER IDs</div>
<h3 class="fw-bold mb-0">48</h3>
<h3 class="fw-bold mb-0">{{ $totalCount }}</h3>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #10b981 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">FULLY APPROVED</div>
<h3 class="fw-bold mb-0">36</h3>
<h3 class="fw-bold mb-0">{{ $approvedCount }}</h3>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #f59e0b !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">PENDING MNO REVIEW</div>
<h3 class="fw-bold mb-0">9</h3>
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">APPLIED</div>
<h3 class="fw-bold mb-0">{{ $pendingCount }}</h3>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #ef4444 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">REJECTED / BLOCKED</div>
<h3 class="fw-bold mb-0">3</h3>
<h3 class="fw-bold mb-0">{{ $rejectedCount }}</h3>
</div>
</div>
</div>
@@ -64,120 +80,119 @@
<!-- Data Table -->
<div class="content-card">
<div class="p-3 border-bottom bg-light bg-opacity-50">
<div class="row g-3">
<div class="col-md-5">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-secondary"><i class="bi bi-search"></i></span>
<input type="text" class="form-control bg-white border-start-0 ps-0" placeholder="Search by sender name or client...">
<form action="{{ route('senderids.index') }}" method="GET">
<div class="row g-3">
<div class="col-md-5">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-secondary"><i class="bi bi-search"></i></span>
<input type="text" name="search" value="{{ request('search') }}" class="form-control bg-white border-start-0 ps-0" placeholder="Search by sender ID, MNO, or supplier...">
</div>
</div>
<div class="col-md-3">
<select name="direct_mno" class="form-select bg-white" onchange="this.form.submit()">
<option value="">Direct MNO: All</option>
<option value="YES" {{ request('direct_mno') == 'YES' ? 'selected' : '' }}>YES</option>
<option value="NO" {{ request('direct_mno') == 'NO' ? 'selected' : '' }}>NO</option>
</select>
</div>
<div class="col-md-3">
<select name="status" class="form-select bg-white" onchange="this.form.submit()">
<option value="">Status: All</option>
<option value="Approved" {{ request('status') == 'Approved' ? 'selected' : '' }}>Approved</option>
<option value="Applied" {{ request('status') == 'Applied' ? 'selected' : '' }}>Applied</option>
<option value="Rejected" {{ request('status') == 'Rejected' ? 'selected' : '' }}>Rejected</option>
</select>
</div>
<div class="col-md-1">
<button type="submit" class="btn text-white w-100" style="background-color: #5c4df0;">
<i class="bi bi-funnel"></i>
</button>
</div>
</div>
<div class="col-md-3">
<select class="form-select bg-white">
<option value="">All Clients</option>
<option value="client_1">Click Tech Corp</option>
<option value="client_2">Alpha Telecom</option>
<option value="internal">Internal System</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select bg-white">
<option value="">Status: All</option>
<option value="approved">Fully Approved</option>
<option value="pending">Pending Review</option>
<option value="rejected">Rejected</option>
</select>
</div>
<div class="col-md-1">
<button class="btn btn-secondary w-100"><i class="bi bi-funnel"></i></button>
</div>
</div>
</form>
</div>
<div class="table-responsive">
<table class="table table-custom table-hover mb-0">
<thead>
<tr>
<th>Sender Name</th>
<th>Client / Owner</th>
<th>Type</th>
<th>Network Status</th>
<th>Global Status</th>
<th>Sender ID</th>
<th>Direct MNO</th>
<th>MNO/Supplier</th>
<th>Type & Bind</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<!-- Row 1 -->
<tr>
<td>
<div class="sender-id-text fw-bold">ABSA-OTP</div>
<div class="text-secondary mt-1" style="font-size: 0.75rem;">Created: May 12, 2026</div>
</td>
<td><span class="badge bg-dark bg-opacity-10 text-dark border">Internal System</span></td>
<td><span class="badge bg-secondary bg-opacity-10 text-secondary border">Alphanumeric</span></td>
<td>
<div class="d-flex gap-1 flex-wrap">
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> MTN</span>
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> Telecel</span>
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> AT</span>
</div>
</td>
<td><span class="badge bg-success bg-opacity-10 text-success px-2 py-1"><i class="bi bi-check-all me-1"></i>Active</span></td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-sender"
data-id="1" data-sender="NEXUS-OTP" data-client="internal" data-type="alpha" data-purpose="Sending internal authentication codes." data-status="approved">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light text-danger"><i class="bi bi-trash"></i></button>
</td>
</tr>
<!-- Row 2 -->
<tr>
<td>
<div class="sender-id-text fw-bold">CLICKML</div>
<div class="text-secondary mt-1" style="font-size: 0.75rem;">Created: Jun 02, 2026</div>
</td>
<td><span class="badge bg-info bg-opacity-10 text-info border border-info-subtle">Client: Abusua Solutions</span></td>
<td><span class="badge bg-secondary bg-opacity-10 text-secondary border">Alphanumeric</span></td>
<td>
<div class="d-flex gap-1 flex-wrap">
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> MTN</span>
<span class="network-badge net-pending"><i class="bi bi-clock-fill"></i> Telecel</span>
<span class="network-badge net-pending"><i class="bi bi-clock-fill"></i> AT</span>
</div>
</td>
<td><span class="badge bg-warning bg-opacity-10 text-warning px-2 py-1"><i class="bi bi-hourglass-split me-1"></i>Pending</span></td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-sender"
data-id="2" data-sender="CLICKTECH" data-client="client_1" data-type="alpha" data-purpose="Marketing updates and promotions for hosting services." data-status="pending">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light text-danger"><i class="bi bi-trash"></i></button>
</td>
</tr>
<!-- Row 3 -->
<tr>
<td>
<div class="sender-id-text fw-bold">1334</div>
<div class="text-secondary mt-1" style="font-size: 0.75rem;">Created: Jun 10, 2026</div>
</td>
<td><span class="badge bg-info bg-opacity-10 text-info border border-info-subtle">Client: Alpha Telecom</span></td>
<td><span class="badge bg-primary bg-opacity-10 text-primary border border-primary-subtle">Shortcode</span></td>
<td>
<div class="d-flex gap-1 flex-wrap">
<span class="network-badge net-rejected"><i class="bi bi-x-circle-fill"></i> MTN</span>
</div>
</td>
<td><span class="badge bg-danger bg-opacity-10 text-danger px-2 py-1"><i class="bi bi-x-octagon me-1"></i>Rejected</span></td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-sender"
data-id="3" data-sender="1334" data-client="client_2" data-type="shortcode" data-purpose="Interactive USSD/SMS fallback." data-status="rejected">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light text-danger"><i class="bi bi-trash"></i></button>
</td>
</tr>
@forelse($senderIds as $sender)
<tr>
<td>
<div class="sender-id-text fw-bold">{{ $sender->senderid }}</div>
<div class="text-secondary mt-1" style="font-size: 0.75rem;">Created: {{ $sender->created_at ? $sender->created_at->format('M d, Y') : 'N/A' }}</div>
</td>
<td>
@if($sender->direct_mno == 'YES')
<span class="badge bg-primary bg-opacity-10 text-primary border">YES</span>
@else
<span class="badge bg-secondary bg-opacity-10 text-secondary border">NO</span>
@endif
</td>
<td>
@if($sender->direct_mno == 'YES')
<div class="fw-medium">{{ $sender->mno_name ?? 'N/A' }}</div>
<div class="text-muted small">MNO Name</div>
@else
<div class="fw-medium">{{ $sender->supplier_name ?? 'N/A' }}</div>
<div class="text-muted small">Supplier</div>
@endif
</td>
<td>
<div class="fw-medium">{{ $sender->type ?? 'N/A' }}</div>
<div class="text-muted small">Bind: {{ $sender->bind_name ?? 'default' }}</div>
</td>
<td>
@php
$statusClass = 'bg-secondary text-secondary';
$icon = 'bi-dash-circle';
if($sender->status == 'APPROVED') { $statusClass = 'bg-success text-success'; $icon = 'bi-check-all'; }
elseif($sender->status == 'PENDING') { $statusClass = 'bg-warning text-warning'; $icon = 'bi-hourglass-split'; }
elseif($sender->status == 'REJECTED') { $statusClass = 'bg-danger text-danger'; $icon = 'bi-x-octagon'; }
@endphp
<span class="badge {{ $statusClass }} bg-opacity-10 px-2 py-1"><i class="bi {{ $icon }} me-1"></i>{{ $sender->status }}</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-sender"
data-id="{{ $sender->id }}"
data-senderid="{{ $sender->senderid }}"
data-direct_mno="{{ $sender->direct_mno }}"
data-mno_name="{{ $sender->mno_name }}"
data-supplier_name="{{ $sender->supplier_name }}"
data-type="{{ $sender->type }}"
data-bind_name="{{ $sender->bind_name }}"
data-status="{{ $sender->status }}"
data-remarks="{{ $sender->remarks }}">
<i class="bi bi-pencil"></i>
</button>
<form action="{{ route('senderids.destroy', $sender->id) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-light text-danger" onclick="return confirm('Are you sure you want to delete this Sender ID?')">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center py-4 text-secondary">No Sender IDs found.</td>
</tr>
@endforelse
</tbody>
</table>
<div class="d-flex justify-content-end mt-3">
{{ $senderIds->links('pagination::bootstrap-5') }}
</div>
</div>
</div>
@endsection
@@ -188,66 +203,95 @@
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold" id="senderModalTitle">Request Sender ID</h5>
<h5 class="modal-title fw-bold" id="senderModalTitle">Add Sender ID</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="senderForm">
<form id="senderForm" method="POST" action="{{ route('senderids.store') ?? '#' }}">
@csrf
<div class="modal-body p-4">
<input type="hidden" id="senderRecordId" name="id">
<div id="senderIdModalAlert"></div>
<div class="row g-3 mb-4">
<div class="row g-3">
<!-- Sender ID -->
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Sender Name / Shortcode *</label>
<input type="text" class="form-control fw-bold" id="senderName" name="sender_name" required maxlength="11" placeholder="e.g. NEXUS">
<div class="form-text">Max 11 chars (Alphanumeric) or 15 digits (Numeric). No spaces.</div>
<label class="form-label text-secondary fw-semibold small">Sender ID *</label>
<input type="text" name="senderid" id="edit_senderid" class="form-control" required placeholder="e.g. CLICK_SMS" maxlength="20">
</div>
<!-- Direct MNO Toggle -->
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Type</label>
<select class="form-select" id="senderType" name="type">
<option value="alpha">Alphanumeric</option>
<option value="shortcode">Numeric Shortcode</option>
<label class="form-label text-secondary fw-semibold small">Direct MNO? *</label>
<select name="direct_mno" id="edit_direct_mno" class="form-select" required>
<option value="YES" selected>YES</option>
<option value="NO">NO</option>
</select>
</div>
</div>
<div class="mb-4">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Client / Owner *</label>
<select class="form-select" id="senderClient" name="client_id" required>
<option value="" selected disabled>Select Client...</option>
<option value="internal">Internal System (Nexus ERP)</option>
<option value="client_1">Click Tech Corp</option>
<option value="client_2">Alpha Telecom</option>
<!-- MNO Name (Visible by default) -->
<div class="col-md-12" id="wrapper_mno_name">
<label class="form-label text-secondary fw-semibold small">MNO Name *</label>
<select name="mno_name" id="edit_mno_name" class="form-select">
<option value="">Select Network Operator</option>
@foreach($networkOperators as $mno)
<option value="{{ $mno->name }}">{{ $mno->name }} ({{ $mno->country }})</option>
@endforeach
</select>
</div>
<!-- Supplier Name (Hidden by default) -->
<div class="col-md-12" id="wrapper_supplier_name" style="display: none;">
<label class="form-label text-secondary fw-semibold small">Supplier / Aggregator Name *</label>
<select name="supplier_name" id="edit_supplier_name" class="form-select">
<option value="">Select Supplier</option>
@foreach($clients as $client)
<option value="{{ $client->name }}">{{ $client->name }}</option>
@endforeach
</select>
</div>
<!-- Type & Bind Name -->
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold small">Type</label>
<select name="type" id="edit_type" class="form-select">
<option value="Alphanumeric">Alphanumeric</option>
<option value="Numeric">Numeric</option>
<option value="Shortcode">Shortcode</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold small">Bind Name</label>
<input type="text" name="bind_name" id="edit_bind_name" class="form-control" value="default" maxlength="50">
</div>
<!-- Status -->
<div class="col-md-12">
<label class="form-label text-secondary fw-semibold small">Status</label>
<select name="status" class="form-select">
<option value="" selected>-- Select --</option>
<option value="Applied to MNO">Applied to MNO</option>
<option value="Applied to Aggregator">Applied to Aggregator</option>
<option value="Approved on MNO">Approved on MNO</option>
<option value="Approved on Aggregator">Approved on Aggregator</option>
<option value="Active">Active</option>
<option value="Pending">Pending</option>
<option value="Inactive">Inactive</option>
<option value="Rejected">REJECTED</option>
</select>
</div>
<div class="mb-4">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Purpose & Sample Message *</label>
<textarea class="form-control" id="senderPurpose" name="purpose" rows="3" required placeholder="Required by MNOs for whitelisting. Provide a sample of the SMS content that will be sent."></textarea>
<div class="form-text">Example: "Your verification code is 12345. Valid for 5 minutes."</div>
</div>
<h6 class="fw-bold mb-3 text-secondary text-uppercase" style="font-size: 0.75rem;">MNO Submission Targets</h6>
<div class="p-3 bg-light rounded border border-secondary-subtle">
<div class="form-check form-check-inline me-4">
<input class="form-check-input" type="checkbox" id="mnoMtn" name="networks[]" value="mtn" checked>
<label class="form-check-label fw-semibold text-dark" for="mnoMtn">MTN Ghana</label>
</div>
<div class="form-check form-check-inline me-4">
<input class="form-check-input" type="checkbox" id="mnoTelecel" name="networks[]" value="telecel" checked>
<label class="form-check-label fw-semibold text-dark" for="mnoTelecel">Telecel</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="mnoAt" name="networks[]" value="at" checked>
<label class="form-check-label fw-semibold text-dark" for="mnoAt">AT</label>
<!-- Remarks -->
<div class="col-12">
<label class="form-label text-secondary fw-semibold small">Remarks</label>
<textarea name="remarks" id="edit_remarks" class="form-control" rows="2" placeholder="Optional notes..."></textarea>
</div>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn text-white fw-bold px-4" style="background-color: #5c4df0;" id="btnSubmitSender">
Submit Registration
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn text-white fw-bold btn-sm px-4" style="background-color: #5c4df0;" id="btnSubmitSender">
Save Sender ID
</button>
</div>
</form>
@@ -257,63 +301,9 @@
@endpush
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="{{ asset('public/assets/js/sender-ids.js') }}"></script>
<script>
$(document).ready(function() {
const senderModal = new bootstrap.Modal(document.getElementById('senderModal'));
const $form = $('#senderForm');
// Force Uppercase and trim spaces on Sender ID input
$('#senderName').on('input', function() {
$(this).val($(this).val().toUpperCase().replace(/\s/g, ''));
});
// --- OPEN MODAL FOR CREATE ---
$('#btnOpenSenderModal').on('click', function() {
$form[0].reset();
$('#senderRecordId').val('');
$('#senderModalTitle').text('Request Sender ID');
$('#btnSubmitSender').html('<i class="bi bi-send me-2"></i>Submit Registration');
senderModal.show();
});
// --- OPEN MODAL FOR EDIT ---
$('.btn-edit-sender').on('click', function() {
const data = $(this).data();
$form[0].reset();
$('#senderRecordId').val(data.id);
$('#senderName').val(data.sender);
$('#senderType').val(data.type);
$('#senderClient').val(data.client);
$('#senderPurpose').val(data.purpose);
$('#senderModalTitle').text('Edit Sender ID: ' + data.sender);
$('#btnSubmitSender').html('<i class="bi bi-save me-2"></i>Update Request');
senderModal.show();
});
// --- HANDLE FORM SUBMISSION ---
$form.on('submit', function(e) {
e.preventDefault();
const $btn = $('#btnSubmitSender');
const originalText = $btn.html();
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span> Processing...');
$btn.prop('disabled', true);
setTimeout(() => {
const formData = $(this).serializeArray();
console.log('Sender ID Request Payload:', formData);
$btn.html(originalText).prop('disabled', false);
senderModal.hide();
alert('Sender ID request saved and queued for MNO submission.');
}, 800);
});
});
</script>
@endpush

View File

@@ -0,0 +1,74 @@
<form id="createSenderIdForm" method="POST" action="{{ route('senderids.store') }}">
@csrf
<div class="modal-body p-4">
<div id="senderIdModalAlert"></div>
<div class="row g-3">
<!-- Sender ID -->
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold small">Sender ID *</label>
<input type="text" name="senderid" class="form-control" required placeholder="e.g. CLICK_SMS">
</div>
<!-- Direct MNO Toggle -->
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold small">Direct MNO? *</label>
<select name="direct_mno" id="create_direct_mno" class="form-select" required>
<option value="YES" selected>YES</option>
<option value="NO">NO</option>
</select>
</div>
<!-- MNO Name (Visible by default) -->
<div class="col-md-12" id="wrapper_mno_name">
<label class="form-label text-secondary fw-semibold small">MNO Name *</label>
<input type="text" name="mno_name" id="input_mno_name" class="form-control" placeholder="Enter specific Network Operator">
</div>
<!-- Supplier Name (Hidden by default) -->
<div class="col-md-12" id="wrapper_supplier_name" style="display: none;">
<label class="form-label text-secondary fw-semibold small">Supplier / Aggregator Name *</label>
<input type="text" name="supplier_name" id="input_supplier_name" class="form-control" placeholder="Enter supplier name">
</div>
<!-- Type & Bind Name -->
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold small">Type</label>
<select name="type" class="form-select">
<option value="Alphanumeric">Alphanumeric</option>
<option value="Numeric">Numeric</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold small">Bind Name</label>
<input type="text" name="bind_name" class="form-control" value="default">
</div>
<!-- Status -->
<div class="col-md-12">
<label class="form-label text-secondary fw-semibold small">Status</label>
<select name="status" class="form-select">
<option value="" selected>-- Select --</option>
<option value="Applied to MNO">Applied to MNO</option>
<option value="Applied to Aggregator">Applied to Aggregator</option>
<option value="Approved on MNO">Approved on MNO</option>
<option value="Approved on Aggregator">Approved on Aggregator</option>
<option value="Active">Active</option>
<option value="Pending">Pending</option>
<option value="Inactive">Inactive</option>
<option value="Rejected">REJECTED</option>
</select>
</div>
<!-- Remarks -->
<div class="col-12">
<label class="form-label text-secondary fw-semibold small">Remarks</label>
<textarea name="remarks" class="form-control" rows="2" placeholder="Optional notes..."></textarea>
</div>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" id="btnSubmitSenderId" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">Save Sender ID</button>
</div>
</form>

View File

@@ -0,0 +1,359 @@
@extends('layouts.masterbeta')
@section('title', 'Nexus ERP - Short Codes')
@push('styles')
<style>
.network-badge { font-size: 0.65rem; padding: 0.2rem 0.4rem; border-radius: 4px; border: 1px solid #e2e8f0; font-weight: 600; display: inline-flex; align-items: center; gap: 3px; }
.net-approved { background-color: rgba(16, 185, 129, 0.1); color: #10b981; border-color: rgba(16, 185, 129, 0.2); }
.net-pending { background-color: rgba(245, 158, 11, 0.1); color: #f59e0b; border-color: rgba(245, 158, 11, 0.2); }
.code-text { font-family: 'Courier New', Courier, monospace; font-size: 1.2rem; font-weight: 700; color: #111425; letter-spacing: 1px; }
.cat-badge { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 6px; margin-right: 10px; font-size: 1rem; }
.cat-sms { background-color: rgba(59, 130, 246, 0.1); color: #3b82f6; }
.cat-ussd { background-color: rgba(139, 92, 246, 0.1); color: #8b5cf6; }
.cat-voice { background-color: rgba(16, 185, 129, 0.1); color: #10b981; }
</style>
@endpush
@section('breadcrumbs')
<a href="{{ url('/') }}" class="text-secondary text-decoration-none"><i class="bi bi-house me-2"></i></a>
<i class="bi bi-chevron-right text-secondary me-2" style="font-size: 0.8rem;"></i>
<a href="#" class="text-secondary text-decoration-none me-2" style="font-size: 0.95rem;">Operations</a>
<i class="bi bi-chevron-right text-secondary me-2" style="font-size: 0.8rem;"></i>
<span class="fw-bold" style="font-size: 0.95rem;">Short Codes</span>
@endsection
@section('content')
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="fw-bold mb-1">Short Code Management</h4>
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Provision and manage dedicated/shared codes for SMS, USSD, and Voice routing.</p>
</div>
<button class="btn text-white fw-bold d-flex align-items-center" style="background-color: #5c4df0; border-radius: 8px;" id="btnOpenCodeModal">
<i class="bi bi-plus-lg me-2"></i> Provision Short Code
</button>
</div>
<!-- KPIs -->
<div class="row g-4 mb-4">
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #4b5563 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">TOTAL CODES</div>
<h3 class="fw-bold mb-0">{{ $totalCount }}</h3>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #3b82f6 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">SMS CODES</div>
<h3 class="fw-bold mb-0">{{ $smsCount }}</h3>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #8b5cf6 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">USSD CODES</div>
<h3 class="fw-bold mb-0">{{ $ussdCount }}</h3>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #10b981 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">VOICE CODES</div>
<h3 class="fw-bold mb-0">{{ $voiceCount }}</h3>
</div>
</div>
</div>
<!-- Data Table -->
<div class="content-card">
<div class="p-3 border-bottom bg-light bg-opacity-50">
<div class="row g-3">
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-secondary"><i class="bi bi-search"></i></span>
<input type="text" class="form-control bg-white border-start-0 ps-0" placeholder="Search by code or client...">
</div>
</div>
<div class="col-md-3">
<select class="form-select bg-white">
<option value="">All Types</option>
<option value="sms">SMS Only</option>
<option value="ussd">USSD</option>
<option value="voice">Voice / IVR</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select bg-white">
<option value="">Billing: All</option>
<option value="standard">Standard Rated</option>
<option value="reverse">Reverse Billed (Toll-Free)</option>
<option value="premium">Premium Rated</option>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-secondary w-100">Filter</button>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-custom table-hover mb-0">
<thead>
<tr>
<th>Short Code</th>
<th>Category</th>
<th>Client / Assignment</th>
<th>Billing Type</th>
<th>Network Provisioning</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<!-- Row 1: SMS -->
<tr>
<td>
<div class="d-flex align-items-center">
<div class="cat-badge cat-sms"><i class="bi bi-chat-text-fill"></i></div>
<div>
<div class="code-text">1334</div>
<div class="text-secondary mt-1" style="font-size: 0.75rem;">Dedicated</div>
</div>
</div>
</td>
<td><span class="fw-semibold text-dark">SMS</span></td>
<td>
<div class="fw-semibold" style="font-size: 0.85rem;">Click Tech Corp</div>
<div class="text-secondary text-truncate" style="font-size: 0.75rem; max-width: 150px;" title="https://api.clicktech.com/webhook/sms">https://api.clicktech.com/...</div>
</td>
<td><span class="badge bg-secondary bg-opacity-10 text-secondary border">Standard</span></td>
<td>
<div class="d-flex gap-1 flex-wrap">
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> MTN</span>
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> Telecel</span>
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> AT</span>
</div>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-code"
data-id="1" data-code="1334" data-category="sms" data-type="dedicated" data-client="client_1" data-billing="standard" data-webhook="https://api.clicktech.com/webhook/sms">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
<!-- Row 2: USSD -->
<tr>
<td>
<div class="d-flex align-items-center">
<div class="cat-badge cat-ussd"><i class="bi bi-phone-fill"></i></div>
<div>
<div class="code-text">*711*50#</div>
<div class="text-secondary mt-1" style="font-size: 0.75rem;">Extension (Shared)</div>
</div>
</div>
</td>
<td><span class="fw-semibold text-dark">USSD</span></td>
<td>
<div class="fw-semibold" style="font-size: 0.85rem;">Alpha Telecom</div>
<div class="text-secondary text-truncate" style="font-size: 0.75rem; max-width: 150px;" title="https://ussd.alphatel.com/callback">https://ussd.alphatel.com/...</div>
</td>
<td><span class="badge bg-danger bg-opacity-10 text-danger border border-danger-subtle">Reverse Billed</span></td>
<td>
<div class="d-flex gap-1 flex-wrap">
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> MTN</span>
<span class="network-badge net-pending"><i class="bi bi-clock-fill"></i> Telecel</span>
</div>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-code"
data-id="2" data-code="*711*50#" data-category="ussd" data-type="shared" data-client="client_2" data-billing="reverse" data-webhook="https://ussd.alphatel.com/callback">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
<!-- Row 3: Voice -->
<tr>
<td>
<div class="d-flex align-items-center">
<div class="cat-badge cat-voice"><i class="bi bi-mic-fill"></i></div>
<div>
<div class="code-text">3001</div>
<div class="text-secondary mt-1" style="font-size: 0.75rem;">Dedicated IVR</div>
</div>
</div>
</td>
<td><span class="fw-semibold text-dark">Voice</span></td>
<td>
<div class="fw-semibold" style="font-size: 0.85rem;">Internal Customer Service</div>
<div class="text-secondary text-truncate" style="font-size: 0.75rem; max-width: 150px;">SIP: trunk.nexus.local</div>
</td>
<td><span class="badge bg-secondary bg-opacity-10 text-secondary border">Standard</span></td>
<td>
<div class="d-flex gap-1 flex-wrap">
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> MTN</span>
</div>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-code"
data-id="3" data-code="3001" data-category="voice" data-type="dedicated" data-client="internal" data-billing="standard" data-webhook="SIP: trunk.nexus.local">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
@endsection
@push('modals')
<!-- SHORT CODE MODAL -->
<div class="modal fade" id="codeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold" id="codeModalTitle">Provision Short Code</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="codeForm">
<div class="modal-body p-4">
<input type="hidden" id="recordId" name="id">
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Short Code Number *</label>
<input type="text" class="form-control fw-bold" id="codeNumber" name="code_number" required placeholder="e.g. 1334 or *711#">
</div>
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Service Category *</label>
<select class="form-select" id="codeCategory" name="category" required>
<option value="" selected disabled>Select Category...</option>
<option value="sms">SMS</option>
<option value="ussd">USSD</option>
<option value="voice">Voice / IVR</option>
</select>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Setup Type</label>
<select class="form-select" id="codeType" name="setup_type">
<option value="dedicated">Dedicated</option>
<option value="shared">Shared (Keyword/Extension)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Billing Model</label>
<select class="form-select" id="codeBilling" name="billing_type">
<option value="standard">Standard Rated</option>
<option value="reverse">Reverse Billed (Toll-Free)</option>
<option value="premium">Premium Rated</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Assigned Client *</label>
<select class="form-select" id="codeClient" name="client_id" required>
<option value="internal">Internal System</option>
<option value="client_1">Click Tech Corp</option>
<option value="client_2">Alpha Telecom</option>
</select>
</div>
</div>
<div class="mb-4">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Callback URL / Webhook Endpoint</label>
<input type="text" class="form-control" id="codeWebhook" name="webhook_url" placeholder="https://api.client.com/receive">
<div class="form-text">Where incoming payloads (MO messages, USSD sessions, SIP invites) should be routed.</div>
</div>
<h6 class="fw-bold mb-3 text-secondary text-uppercase" style="font-size: 0.75rem;">MNO Routing Map</h6>
<div class="p-3 bg-light rounded border border-secondary-subtle">
<div class="form-check form-check-inline me-4">
<input class="form-check-input" type="checkbox" id="routeMtn" name="routing[]" value="mtn" checked>
<label class="form-check-label fw-semibold text-dark" for="routeMtn">MTN Ghana</label>
</div>
<div class="form-check form-check-inline me-4">
<input class="form-check-input" type="checkbox" id="routeTelecel" name="routing[]" value="telecel" checked>
<label class="form-check-label fw-semibold text-dark" for="routeTelecel">Telecel</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="routeAt" name="routing[]" value="at" checked>
<label class="form-check-label fw-semibold text-dark" for="routeAt">AT</label>
</div>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn text-white fw-bold px-4" style="background-color: #5c4df0;" id="btnSubmitCode">
Save Configuration
</button>
</div>
</form>
</div>
</div>
</div>
@endpush
@push('scripts')
<script>
$(document).ready(function() {
const codeModal = new bootstrap.Modal(document.getElementById('codeModal'));
const $form = $('#codeForm');
// --- OPEN MODAL FOR CREATE ---
$('#btnOpenCodeModal').on('click', function() {
$form[0].reset();
$('#recordId').val('');
$('#codeModalTitle').text('Provision New Short Code');
$('#btnSubmitCode').html('<i class="bi bi-save me-2"></i>Save Configuration');
codeModal.show();
});
// --- OPEN MODAL FOR EDIT ---
$('.btn-edit-code').on('click', function() {
const data = $(this).data();
$form[0].reset();
$('#recordId').val(data.id);
$('#codeNumber').val(data.code);
$('#codeCategory').val(data.category);
$('#codeType').val(data.type);
$('#codeClient').val(data.client);
$('#codeBilling').val(data.billing);
$('#codeWebhook').val(data.webhook);
$('#codeModalTitle').text('Edit Route: ' + data.code);
$('#btnSubmitCode').html('<i class="bi bi-check-circle me-2"></i>Update Route');
codeModal.show();
});
// --- HANDLE FORM SUBMISSION ---
$form.on('submit', function(e) {
e.preventDefault();
const $btn = $('#btnSubmitCode');
const originalText = $btn.html();
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span> Provisioning...');
$btn.prop('disabled', true);
setTimeout(() => {
const formData = $(this).serializeArray();
console.log('Short Code Payload:', formData);
$btn.html(originalText).prop('disabled', false);
codeModal.hide();
alert('Short code configuration applied to gateway successfully.');
}, 800);
});
});
</script>
@endpush

View File

@@ -1,19 +1,22 @@
@extends('layouts.masterbeta')
@section('title', 'Nexus ERP - Short Codes')
@section('title', 'Click ERP - Short Codes')
@push('styles')
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<style>
.network-badge { font-size: 0.65rem; padding: 0.2rem 0.4rem; border-radius: 4px; border: 1px solid #e2e8f0; font-weight: 600; display: inline-flex; align-items: center; gap: 3px; }
.net-approved { background-color: rgba(16, 185, 129, 0.1); color: #10b981; border-color: rgba(16, 185, 129, 0.2); }
.net-pending { background-color: rgba(245, 158, 11, 0.1); color: #f59e0b; border-color: rgba(245, 158, 11, 0.2); }
.code-text { font-family: 'Courier New', Courier, monospace; font-size: 1.2rem; font-weight: 700; color: #111425; letter-spacing: 1px; }
.cat-badge { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 6px; margin-right: 10px; font-size: 1rem; }
.cat-sms { background-color: rgba(59, 130, 246, 0.1); color: #3b82f6; }
.cat-ussd { background-color: rgba(139, 92, 246, 0.1); color: #8b5cf6; }
.cat-voice { background-color: rgba(16, 185, 129, 0.1); color: #10b981; }
.shortcode-text { font-family: monospace; font-size: 1.1rem; letter-spacing: 1px; color: #111425; }
.select2-container .select2-selection--single {
height: 38px;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
display: flex;
align-items: center;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 36px;
}
</style>
@endpush
@@ -29,269 +32,255 @@
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="fw-bold mb-1">Short Code Management</h4>
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Provision and manage dedicated/shared codes for SMS, USSD, and Voice routing.</p>
<h4 class="fw-bold mb-1">Short Codes Management</h4>
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Manage SMS, USSD, and Voice short codes, toll-free lines, and validity periods.</p>
</div>
<button class="btn text-white fw-bold d-flex align-items-center" style="background-color: #5c4df0; border-radius: 8px;" id="btnOpenCodeModal">
<i class="bi bi-plus-lg me-2"></i> Provision Short Code
<button class="btn text-white fw-bold d-flex align-items-center" style="background-color: #5c4df0; border-radius: 8px;" id="btnOpenShortCodeModal">
<i class="bi bi-plus-lg me-2"></i> Add Short Code
</button>
</div>
<!-- KPIs -->
<div class="row g-4 mb-4">
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #4b5563 !important;">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #5c4df0 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">TOTAL CODES</div>
<h3 class="fw-bold mb-0">34</h3>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #3b82f6 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">ACTIVE SMS CODES</div>
<h3 class="fw-bold mb-0">18</h3>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #8b5cf6 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">ACTIVE USSD CODES</div>
<h3 class="fw-bold mb-0">12</h3>
<h3 class="fw-bold mb-0">{{ $totalCount }}</h3>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #10b981 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">ACTIVE VOICE CODES</div>
<h3 class="fw-bold mb-0">4</h3>
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">SMS CODES</div>
<h3 class="fw-bold mb-0">{{ $smsCount }}</h3>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #f59e0b !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">USSD CODES</div>
<h3 class="fw-bold mb-0">{{ $ussdCount }}</h3>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="content-card p-3 h-100 border-start border-4 border-0" style="border-left-color: #3b82f6 !important;">
<div class="text-secondary fw-bold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">VOICE CODES</div>
<h3 class="fw-bold mb-0">{{ $voiceCount }}</h3>
</div>
</div>
</div>
<!-- Data Table -->
<!-- Data Table & Filter Card -->
<div class="content-card">
<div class="p-3 border-bottom bg-light bg-opacity-50">
<div class="row g-3">
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-secondary"><i class="bi bi-search"></i></span>
<input type="text" class="form-control bg-white border-start-0 ps-0" placeholder="Search by code or client...">
<form action="{{ route('shortcodes.index') }}" method="GET">
<div class="row g-3">
<div class="col-md-7">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-secondary"><i class="bi bi-search"></i></span>
<input type="text" name="search" value="{{ request('search') }}" class="form-control bg-white border-start-0 ps-0" placeholder="Search by shortcode, name, country, or network...">
</div>
</div>
<div class="col-md-4">
<select name="status" class="form-select bg-white" onchange="this.form.submit()">
<option value="">Status: All</option>
<option value="Active" {{ request('status') == 'Active' ? 'selected' : '' }}>Active</option>
<option value="Pending" {{ request('status') == 'Pending' ? 'selected' : '' }}>Pending</option>
<option value="Expired" {{ request('status') == 'Expired' ? 'selected' : '' }}>Expired</option>
</select>
</div>
<div class="col-md-1">
<button type="submit" class="btn text-white w-100" style="background-color: #5c4df0;">
<i class="bi bi-funnel"></i>
</button>
</div>
</div>
<div class="col-md-3">
<select class="form-select bg-white">
<option value="">All Categories</option>
<option value="sms">SMS Only</option>
<option value="ussd">USSD</option>
<option value="voice">Voice / IVR</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select bg-white">
<option value="">Billing: All</option>
<option value="standard">Standard Rated</option>
<option value="reverse">Reverse Billed (Toll-Free)</option>
<option value="premium">Premium Rated</option>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-secondary w-100">Filter</button>
</div>
</div>
</form>
</div>
<div class="table-responsive">
<table class="table table-custom table-hover mb-0">
<table class="table table-custom table-hover mb-0 align-middle">
<thead>
<tr>
<th>Short Code</th>
<th>Category</th>
<th>Client / Assignment</th>
<th>Billing Type</th>
<th>Network Provisioning</th>
<th>Name / Description</th>
<th>Type & Toll-Free</th>
<th>Pricing / Fees</th>
<th>Region / Network</th>
<th>Status & Dates</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<!-- Row 1: SMS -->
<tr>
<td>
<div class="d-flex align-items-center">
<div class="cat-badge cat-sms"><i class="bi bi-chat-text-fill"></i></div>
<div>
<div class="code-text">1334</div>
<div class="text-secondary mt-1" style="font-size: 0.75rem;">Dedicated</div>
</div>
</div>
</td>
<td><span class="fw-semibold text-dark">SMS</span></td>
<td>
<div class="fw-semibold" style="font-size: 0.85rem;">Click Tech Corp</div>
<div class="text-secondary text-truncate" style="font-size: 0.75rem; max-width: 150px;" title="https://api.clicktech.com/webhook/sms">https://api.clicktech.com/...</div>
</td>
<td><span class="badge bg-secondary bg-opacity-10 text-secondary border">Standard</span></td>
<td>
<div class="d-flex gap-1 flex-wrap">
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> MTN</span>
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> Telecel</span>
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> AT</span>
</div>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-code"
data-id="1" data-code="1334" data-category="sms" data-type="dedicated" data-client="client_1" data-billing="standard" data-webhook="https://api.clicktech.com/webhook/sms">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
<!-- Row 2: USSD -->
<tr>
<td>
<div class="d-flex align-items-center">
<div class="cat-badge cat-ussd"><i class="bi bi-phone-fill"></i></div>
<div>
<div class="code-text">*711*50#</div>
<div class="text-secondary mt-1" style="font-size: 0.75rem;">Extension (Shared)</div>
</div>
</div>
</td>
<td><span class="fw-semibold text-dark">USSD</span></td>
<td>
<div class="fw-semibold" style="font-size: 0.85rem;">Alpha Telecom</div>
<div class="text-secondary text-truncate" style="font-size: 0.75rem; max-width: 150px;" title="https://ussd.alphatel.com/callback">https://ussd.alphatel.com/...</div>
</td>
<td><span class="badge bg-danger bg-opacity-10 text-danger border border-danger-subtle">Reverse Billed</span></td>
<td>
<div class="d-flex gap-1 flex-wrap">
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> MTN</span>
<span class="network-badge net-pending"><i class="bi bi-clock-fill"></i> Telecel</span>
</div>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-code"
data-id="2" data-code="*711*50#" data-category="ussd" data-type="shared" data-client="client_2" data-billing="reverse" data-webhook="https://ussd.alphatel.com/callback">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
<!-- Row 3: Voice -->
<tr>
<td>
<div class="d-flex align-items-center">
<div class="cat-badge cat-voice"><i class="bi bi-mic-fill"></i></div>
<div>
<div class="code-text">3001</div>
<div class="text-secondary mt-1" style="font-size: 0.75rem;">Dedicated IVR</div>
</div>
</div>
</td>
<td><span class="fw-semibold text-dark">Voice</span></td>
<td>
<div class="fw-semibold" style="font-size: 0.85rem;">Internal Customer Service</div>
<div class="text-secondary text-truncate" style="font-size: 0.75rem; max-width: 150px;">SIP: trunk.nexus.local</div>
</td>
<td><span class="badge bg-secondary bg-opacity-10 text-secondary border">Standard</span></td>
<td>
<div class="d-flex gap-1 flex-wrap">
<span class="network-badge net-approved"><i class="bi bi-check-circle-fill"></i> MTN</span>
</div>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-code"
data-id="3" data-code="3001" data-category="voice" data-type="dedicated" data-client="internal" data-billing="standard" data-webhook="SIP: trunk.nexus.local">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
@forelse($shortcodes as $code)
<tr>
<td>
<div class="shortcode-text fw-bold">{{ $code->shortcode }}</div>
</td>
<td>
<div class="fw-medium text-dark">{{ $code->name ?? 'N/A' }}</div>
<div class="text-secondary small">Client ID: {{ $code->client_id ?? 'None' }}</div>
</td>
<td>
<span class="badge bg-secondary bg-opacity-10 text-dark border text-uppercase">{{ $code->code_type ?? 'N/A' }}</span>
@if($code->toll_free == 'YES')
<span class="badge bg-success bg-opacity-10 text-success border border-success-subtle ms-1">Toll-Free</span>
@endif
</td>
<td>
<div class="fw-bold text-dark">${{ number_format($code->monthly_fee, 2) }}</div>
<div class="text-secondary" style="font-size: 0.75rem;">Monthly Fee</div>
</td>
<td>
<div class="fw-medium">{{ $code->country ?? 'N/A' }}</div>
<div class="text-secondary small">{{ $code->network ?? 'All Networks' }}</div>
</td>
<td>
<span class="badge {{ $code->status == 'Active' ? 'bg-success' : 'bg-warning' }} bg-opacity-10 text-success px-2 py-1">
{{ $code->status ?? 'Active' }}
</span>
<div class="text-secondary mt-1" style="font-size: 0.7rem;">Exp: {{ $code->expiry_date ? date('M d, Y', strtotime($code->expiry_date)) : 'N/A' }}</div>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-shortcode"
data-id="{{ $code->id }}"
data-client_id="{{ $code->client_id }}"
data-name="{{ $code->name }}"
data-shortcode="{{ $code->shortcode }}"
data-code_type="{{ $code->code_type }}"
data-toll_free="{{ $code->toll_free }}"
data-monthly_fee="{{ $code->monthly_fee }}"
data-country="{{ $code->country }}"
data-network="{{ $code->network }}"
data-status="{{ $code->status }}"
data-launch_date="{{ $code->launch_date ? date('Y-m-d', strtotime($code->launch_date)) : '' }}"
data-expiry_date="{{ $code->expiry_date ? date('Y-m-d', strtotime($code->expiry_date)) : '' }}"
data-remarks="{{ $code->remarks }}">
<i class="bi bi-pencil"></i>
</button>
<form action="{{ route('shortcodes.destroy', $code->id) }}" method="POST" class="d-inline">
@csrf
@method('DELETE')
<button type="button" class="btn btn-sm btn-light text-danger btn-delete-record">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center py-4 text-secondary">No short codes found matching your criteria.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="p-3 border-top">
{{ $shortcodes->links('pagination::bootstrap-5') }}
</div>
</div>
@endsection
@push('modals')
<!-- SHORT CODE MODAL -->
<div class="modal fade" id="codeModal" tabindex="-1" aria-hidden="true">
<!-- ADD/EDIT SHORT CODE MODAL -->
<div class="modal fade" id="shortCodeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold" id="codeModalTitle">Provision Short Code</h5>
<h5 class="modal-title fw-bold" id="shortCodeModalTitle">Add Short Code</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="codeForm">
<form id="shortCodeForm" method="POST">
@csrf
<div class="modal-body p-4">
<input type="hidden" id="recordId" name="id">
<input type="hidden" id="shortCodeRecordId" name="id">
<div id="shortCodeModalAlert"></div>
<div class="row g-3 mb-4">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Short Code Number *</label>
<input type="text" class="form-control fw-bold" id="codeNumber" name="code_number" required placeholder="e.g. 1334 or *711#">
<label class="form-label text-secondary fw-semibold small">Short Code *</label>
<input type="text" name="shortcode" id="modal_shortcode" class="form-control" required placeholder="e.g. 1334">
</div>
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Service Category *</label>
<select class="form-select" id="codeCategory" name="category" required>
<option value="" selected disabled>Select Category...</option>
<label class="form-label text-secondary fw-semibold small">Service Name / Label</label>
<input type="text" name="name" id="modal_name" class="form-control" placeholder="e.g. Banking Alert Line">
</div>
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold small">Client Assignment</label>
<select name="client_id" id="modal_client_id" class="form-select select2-field">
<option value="">Internal / Unassigned</option>
@foreach($clients as $client)
<option value="{{ $client->id }}">{{ $client->name }}</option>
@endforeach
</select>
</div>
<div class="col-md-6">
<label class="form-label text-secondary fw-semibold small">Network Operator Link</label>
<select name="network" id="modal_network" class="form-select select2-field">
<option value="">All Networks / General</option>
@foreach($networkOperators as $mno)
<option value="{{ $mno->name }}">{{ $mno->name }} ({{ $mno->country }})</option>
@endforeach
</select>
</div>
<div class="col-md-4">
<label class="form-label text-secondary fw-semibold small">Code Type</label>
<select name="code_type" id="modal_code_type" class="form-select">
<option value="sms">SMS</option>
<option value="ussd">USSD</option>
<option value="voice">Voice / IVR</option>
</select>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Setup Type</label>
<select class="form-select" id="codeType" name="setup_type">
<option value="dedicated">Dedicated</option>
<option value="shared">Shared (Keyword/Extension)</option>
<option value="voice">Voice</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Billing Model</label>
<select class="form-select" id="codeBilling" name="billing_type">
<option value="standard">Standard Rated</option>
<option value="reverse">Reverse Billed (Toll-Free)</option>
<option value="premium">Premium Rated</option>
<label class="form-label text-secondary fw-semibold small">Toll Free?</label>
<select name="toll_free" id="modal_toll_free" class="form-select">
<option value="NO">NO</option>
<option value="YES">YES</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Assigned Client *</label>
<select class="form-select" id="codeClient" name="client_id" required>
<option value="internal">Internal System</option>
<option value="client_1">Click Tech Corp</option>
<option value="client_2">Alpha Telecom</option>
<label class="form-label text-secondary fw-semibold small">Monthly Fee ($)</label>
<input type="number" step="0.01" name="monthly_fee" id="modal_monthly_fee" class="form-control" value="0.00">
</div>
<div class="col-md-4">
<label class="form-label text-secondary fw-semibold small">Country</label>
<select name="country" id="modal_country" class="form-select select2-field">
<option value="">Select Country</option>
@foreach($countries as $country)
<option value="{{ $country->en_short_name }}">{{ $country->en_short_name }}</option>
@endforeach
</select>
</div>
</div>
<div class="mb-4">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Callback URL / Webhook Endpoint</label>
<input type="text" class="form-control" id="codeWebhook" name="webhook_url" placeholder="https://api.client.com/receive">
<div class="form-text">Where incoming payloads (MO messages, USSD sessions, SIP invites) should be routed.</div>
</div>
<h6 class="fw-bold mb-3 text-secondary text-uppercase" style="font-size: 0.75rem;">MNO Routing Map</h6>
<div class="p-3 bg-light rounded border border-secondary-subtle">
<div class="form-check form-check-inline me-4">
<input class="form-check-input" type="checkbox" id="routeMtn" name="routing[]" value="mtn" checked>
<label class="form-check-label fw-semibold text-dark" for="routeMtn">MTN Ghana</label>
<div class="col-md-4">
<label class="form-label text-secondary fw-semibold small">Launch Date</label>
<input type="date" name="launch_date" id="modal_launch_date" class="form-control">
</div>
<div class="form-check form-check-inline me-4">
<input class="form-check-input" type="checkbox" id="routeTelecel" name="routing[]" value="telecel" checked>
<label class="form-check-label fw-semibold text-dark" for="routeTelecel">Telecel</label>
<div class="col-md-4">
<label class="form-label text-secondary fw-semibold small">Expiry Date</label>
<input type="date" name="expiry_date" id="modal_expiry_date" class="form-control">
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="routeAt" name="routing[]" value="at" checked>
<label class="form-check-label fw-semibold text-dark" for="routeAt">AT</label>
<div class="col-md-12">
<label class="form-label text-secondary fw-semibold small">Status</label>
<select name="status" id="modal_status" class="form-select">
<option value="Active">Active</option>
<option value="Pending">Pending</option>
<option value="Expired">Expired</option>
</select>
</div>
<div class="col-12">
<label class="form-label text-secondary fw-semibold small">Remarks / Notes</label>
<textarea name="remarks" id="modal_remarks" class="form-control" rows="2" placeholder="Optional context..."></textarea>
</div>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn text-white fw-bold px-4" style="background-color: #5c4df0;" id="btnSubmitCode">
Save Configuration
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" id="btnSubmitShortCode" class="btn text-white btn-sm fw-bold px-4" style="background-color: #5c4df0;">Save Short Code</button>
</div>
</form>
</div>
@@ -300,59 +289,144 @@
@endpush
@push('scripts')
<!-- Select2 JS -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
const codeModal = new bootstrap.Modal(document.getElementById('codeModal'));
const $form = $('#codeForm');
// --- HANDLE DELETE CONFIRMATION ---
$('.btn-delete-record').on('click', function() {
const $form = $(this).closest('form');
// --- OPEN MODAL FOR CREATE ---
$('#btnOpenCodeModal').on('click', function() {
$form[0].reset();
$('#recordId').val('');
Swal.fire({
title: 'Are you sure?',
text: "You are about to delete this short code. This action cannot be undone!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444', // Red confirm button
cancelButtonColor: '#6c757d', // Gray cancel button
confirmButtonText: '<i class="bi bi-trash me-1"></i> Yes, delete it!'
}).then((result) => {
if (result.isConfirmed) {
$form.submit();
}
});
});
const shortCodeModal = new bootstrap.Modal(document.getElementById('shortCodeModal'));
const $form = $('#shortCodeForm');
$('#codeModalTitle').text('Provision New Short Code');
$('#btnSubmitCode').html('<i class="bi bi-save me-2"></i>Save Configuration');
codeModal.show();
// Initialize Select2 inside the modal
$('.select2-field').select2({
placeholder: "Select an option",
allowClear: true,
width: '100%',
dropdownParent: $('#shortCodeModal')
});
// --- OPEN MODAL FOR EDIT ---
$('.btn-edit-code').on('click', function() {
// Open Modal for Create
$('#btnOpenShortCodeModal').on('click', function() {
$form[0].reset();
$('#shortCodeRecordId').val('');
$('.select2-field').val(null).trigger('change');
$('#shortCodeModalTitle').text('Add Short Code');
$('#btnSubmitShortCode').html('Save Short Code');
shortCodeModal.show();
});
// Open Modal for Edit
$('.btn-edit-shortcode').on('click', function() {
const data = $(this).data();
$form[0].reset();
$('#recordId').val(data.id);
$('#shortCodeRecordId').val(data.id);
$('#modal_shortcode').val(data.shortcode);
$('#modal_name').val(data.name);
$('#modal_client_id').val(data.client_id).trigger('change');
$('#modal_network').val(data.network).trigger('change');
$('#modal_code_type').val(data.code_type);
$('#modal_toll_free').val(data.toll_free);
$('#modal_monthly_fee').val(data.monthly_fee);
$('#modal_country').val(data.country).trigger('change');
$('#modal_launch_date').val(data.launch_date);
$('#modal_expiry_date').val(data.expiry_date);
$('#modal_status').val(data.status);
$('#modal_remarks').val(data.remarks);
$('#codeNumber').val(data.code);
$('#codeCategory').val(data.category);
$('#codeType').val(data.type);
$('#codeClient').val(data.client);
$('#codeBilling').val(data.billing);
$('#codeWebhook').val(data.webhook);
$('#shortCodeModalTitle').text('Edit Short Code: ' + data.shortcode);
$('#btnSubmitShortCode').html('Update Short Code');
$('#codeModalTitle').text('Edit Route: ' + data.code);
$('#btnSubmitCode').html('<i class="bi bi-check-circle me-2"></i>Update Route');
codeModal.show();
shortCodeModal.show();
});
// --- HANDLE FORM SUBMISSION ---
// Handle AJAX Form Submission
$form.on('submit', function(e) {
e.preventDefault();
const $btn = $('#btnSubmitCode');
const $btn = $('#btnSubmitShortCode');
const originalText = $btn.html();
const $alertBox = $('#shortCodeModalAlert');
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span> Provisioning...');
$btn.html('<span class="spinner-border spinner-border-sm me-2"></span> Processing...');
$btn.prop('disabled', true);
$alertBox.html('');
$form.find('.is-invalid').removeClass('is-invalid');
$form.find('.invalid-feedback').remove();
setTimeout(() => {
const formData = $(this).serializeArray();
console.log('Short Code Payload:', formData);
const recordId = $('#shortCodeRecordId').val();
const submitUrl = recordId ? "{{ url('shortcodes') }}/" + recordId : "{{ url('shortcodes') }}";
$btn.html(originalText).prop('disabled', false);
codeModal.hide();
alert('Short code configuration applied to gateway successfully.');
}, 800);
let formData = $form.serialize();
if (recordId) {
formData += '&_method=PUT';
}
$.ajax({
url: submitUrl,
type: 'POST',
data: formData,
success: function(response) {
if (response.success) {
shortCodeModal.hide();
Swal.fire({
icon: 'success',
title: 'Success!',
text: response.message,
confirmButtonColor: '#5c4df0'
}).then(() => {
location.reload();
});
}
},
error: function(xhr) {
if (xhr.status === 422) {
let errors = xhr.responseJSON.errors;
$alertBox.html(`
<div class="alert alert-danger d-flex align-items-center py-2 px-3 small" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<div>Please check the highlighted fields below.</div>
</div>
`);
$.each(errors, function(key, value) {
let $input = $form.find('[name="' + key + '"]');
if ($input.length) {
$input.addClass('is-invalid');
$input.parent().append('<div class="invalid-feedback d-block fw-medium small">' + value[0] + '</div>');
}
});
} else {
$alertBox.html(`
<div class="alert alert-danger d-flex align-items-center py-2 px-3 small" role="alert">
<i class="bi bi-x-circle-fill me-2"></i>
<div>An unexpected server error occurred.</div>
</div>
`);
}
},
complete: function() {
$btn.html(originalText).prop('disabled', false);
}
});
});
});
</script>

View File

@@ -1,6 +1,7 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\SenderIdController;
Route::get('test', function () {
return view('clients.index');
@@ -40,10 +41,15 @@ Route::middleware(['auth'])->group(function () {
Route::get('/documents', [App\Http\Controllers\DocumentsVaultController::class, 'index'])->name('documents.index');
Route::get('/senderids', [App\Http\Controllers\SenderidsController::class, 'index']);
Route::get('/shortcodes', [App\Http\Controllers\ShortCodesController::class, 'index']);
// Route::get('/senderids', [App\Http\Controllers\SenderidsController::class, 'index']);
Route::resource('senderids', App\Http\Controllers\SenderIdsController::class)->except(['create', 'show', 'edit']);
// Route::get('/shortcodes', [App\Http\Controllers\ShortCodesController::class, 'index']);
// use App\Http\Controllers\ShortCodesController;
Route::resource('shortcodes', App\Http\Controllers\ShortCodesController::class)->except(['create', 'show', 'edit']);
Route::get('/api/services', [App\Http\Controllers\ServicesController::class, 'getServicesJson']);
Route::get('/api/countries', [App\Http\Controllers\HelperController::class, 'getCountriesJson']);