updated client payment, file upload and notes

This commit is contained in:
Kwesi Banson Jnr
2026-08-08 20:47:45 +00:00
parent daf7d9b9dc
commit 6cce100e62
17 changed files with 508 additions and 94 deletions

View File

@@ -23,3 +23,7 @@
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L] RewriteRule ^ index.php [L]
</IfModule> </IfModule>
<FilesMatch "^\.env">
Require all denied
</FilesMatch>
RedirectMatch 404 /\.git

View File

@@ -11,6 +11,8 @@ use Illuminate\Support\Facades\Log;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class ClientsController extends Controller class ClientsController extends Controller
{ {
@@ -363,14 +365,15 @@ class ClientsController extends Controller
Log::info('ussd client detected'); Log::info('ussd client detected');
Models\UssdClientPayment::create([ Models\UssdClientPayment::create([
'client_id' => $result->id, 'client_id' => $result->id,
'last_modified_by_id' => session('current_user.id') 'last_modified_by_id' => Auth::user()->id
]); ]);
} }
$user_id = $auth_user->id;
Models\UserActivity::create([ Models\UserActivity::create([
'type' => 'staff', 'type' => 'staff',
'content' => session('current_user.name') . " added a new client ({$result->name}) successfully!", 'content' => Auth::user()->name . " added a new client ({$result->name}) successfully!",
'user_id' => session('current_user.id'), 'user_id' => Auth::user()->id,
'ip_address' => request()->ip(), 'ip_address' => request()->ip(),
'device' => $request->header('User-Agent') 'device' => $request->header('User-Agent')
]); ]);
@@ -402,7 +405,7 @@ class ClientsController extends Controller
$client = Models\Client::find($request->client_id); $client = Models\Client::find($request->client_id);
$document_arr['file_extension'] = $request->document->extension(); $document_arr['file_extension'] = $request->document->extension();
$document_arr['file_reff'] = time() . uniqid(); $document_arr['file_reff'] = time() . uniqid();
$document_arr['last_modified_by'] = session('current_user.id'); $document_arr['last_modified_by'] = Auth::user()->id;
$result = Models\ClientFile::create($document_arr); $result = Models\ClientFile::create($document_arr);
@@ -412,13 +415,64 @@ class ClientsController extends Controller
else{ else{
$data = ['code' => 3, 'msg' => 'Your request could not be handled at this time']; $data = ['code' => 3, 'msg' => 'Your request could not be handled at this time'];
} }
$user_id = session('current_user.id'); $user_id = Auth::user()->id;
$username = session('current_user.name'); $username = Auth::user()->name;
$content = "User ID : " . $user_id . " (" . $username . ") Document successfully uploaded for " . $client->name; $content = "User ID : " . $user_id . " (" . $username . ") Document successfully uploaded for " . $client->name;
$this->logUsersActivity($type = 'staff', $content); #$this->logUsersActivity($type = 'staff', $content);
return response()->json($data, 200); return response()->json($data, 200);
} }
public function storeDocument(Request $request)
{
$request->validate([
'client_id' => 'required|exists:clients,id',
'names' => 'required|array',
'names.*' => 'required|string|max:255',
'files' => 'required|array',
'files.*' => 'required|file|mimes:pdf,doc,docx,jpg,png,xlsx|max:15360',
]);
// return response()->json(['message' => 'check check', 'data' => $request->all()]);
foreach ($request->file('files') as $index => $file) {
$customName = $request->names[$index];
$filename = time() . '_' . uniqid() . '.' . $file->getClientOriginalExtension();
$file->storeAs('', $filename, 'client_files');
Models\ClientFile::create([
'client_id' => $request->client_id,
'created_by' => Auth::user()->id,
'name' => $customName,
'file_path' => $filename,
'file_extension' => $file->getClientOriginalExtension(),
]);
}
return response()->json(['message' => 'Documents uploaded successfully!']);
}
public function clientFiledownload($id)
{
$file = Models\ClientFile::findOrFail($id);
$filePath = 'public/client_files/' . $file->file_path;
$filename = basename($file->file_path);
// dd($filePath);
if (!Storage::disk('client_files')->exists($filename)) {
abort(404, 'Document not found.');
}
// This forces a download and names the file neatly for the user
// $downloadName = $file->name . '.' . $file->file_extension;
// dd($downloadName);
// 4. Construct a download name using the custom user title + the original extension
$downloadName = Str::slug($file->name) . '.' . $file->file_extension;
// 5. Securely stream the file payload back to the browser
return Storage::disk('client_files')->download($filename, $downloadName);
return Storage::download($filePath, $downloadName);
}
public function getClientFile($id){ public function getClientFile($id){
$client_file = Models\ClientFile::with('client_info')->findOrFail($id); $client_file = Models\ClientFile::with('client_info')->findOrFail($id);
$file = public_path('documents/client_files/') . $client_file->file_path; $file = public_path('documents/client_files/') . $client_file->file_path;
@@ -438,7 +492,7 @@ class ClientsController extends Controller
'invoice_date' => 'required', 'invoice_date' => 'required',
'invoice_status' => 'required', 'invoice_status' => 'required',
]); ]);
$auth_user = session('current_user'); $auth_user = Auth::user();
if ($request->short_code !== null) { if ($request->short_code !== null) {
$check = is_numeric($request->short_code); $check = is_numeric($request->short_code);
@@ -447,14 +501,13 @@ class ClientsController extends Controller
return response()->json($data, 200); return response()->json($data, 200);
} }
} }
$finance_arr = [ $finance_arr = [
'invoice_number' => $request->invoice_number, 'invoice_number' => $request->invoice_number,
'invoice_amount' => $request->invoice_amount, 'invoice_amount' => $request->invoice_amount,
'invoice_date' => $request->invoice_date, 'invoice_date' => $request->invoice_date,
'invoice_status' => $request->invoice_status, 'invoice_status' => $request->invoice_status,
'short_code' => ($request->short_code) ? $request->short_code : "", 'short_code' => ($request->short_code) ? $request->short_code : "",
'services' => implode(',', $request->services), 'services' => $request->services, //implode(',', $request->services),
'user_id' => $auth_user['id'], 'user_id' => $auth_user['id'],
'client_id' => $request->client_id 'client_id' => $request->client_id
]; ];
@@ -478,10 +531,11 @@ class ClientsController extends Controller
else{ else{
$data = ['code' => 3, 'msg' => 'Your request could not be handled at this time']; $data = ['code' => 3, 'msg' => 'Your request could not be handled at this time'];
} }
$user_id = session('current_user.id'); $user_id = $auth_user->id;
$username = session('current_user.name'); $username = $auth_user->name;
$content = "User ID : " . $user_id . " (" . $username . ") Added a payment record for : " . $client->name; $content = "User ID : " . $user_id . " (" . $username . ") Added a payment record for : " . $client->name;
$this->logUsersActivity($type = 'staff', $content); #$this->logUsersActivity($type = 'staff', $content);
return response()->json($data, 200); return response()->json($data, 200);
} }
public function financeUpdate(Request $request){ public function financeUpdate(Request $request){
@@ -495,7 +549,7 @@ class ClientsController extends Controller
'invoice_status' => 'required' 'invoice_status' => 'required'
]); ]);
$auth_user = session('current_user'); $auth_user = Auth::user();
$payment = Models\ClientPayment::findOrFail($request->payment_id); $payment = Models\ClientPayment::findOrFail($request->payment_id);
$payment->invoice_number = $request->invoice_number; $payment->invoice_number = $request->invoice_number;
@@ -516,7 +570,7 @@ class ClientsController extends Controller
'invoice_date' => $request->invoice_date, 'invoice_date' => $request->invoice_date,
'invoice_status' => $request->invoice_status, 'invoice_status' => $request->invoice_status,
'short_code' => ($request->short_code) ? $request->short_code : "", 'short_code' => ($request->short_code) ? $request->short_code : "",
'services' => implode(',', $request->services), 'services' => $request->services, //implode(',', $request->services),
'user_id' => $auth_user['id'], 'user_id' => $auth_user['id'],
'client_id' => $request->client_id 'client_id' => $request->client_id
]; ];
@@ -533,10 +587,10 @@ class ClientsController extends Controller
else{ else{
$data = ['code' => 3, 'msg' => 'Your request could not be handled at this time']; $data = ['code' => 3, 'msg' => 'Your request could not be handled at this time'];
} }
$user_id = session('current_user.id'); $user_id = $auth_user->id;
$username = session('current_user.name'); $username = $auth_user->name;
$content = "User ID : " . $user_id . " (" . $username . ") updated an existing payment record for " . $client->name; $content = "User ID : " . $user_id . " (" . $username . ") updated an existing payment record for " . $client->name;
$this->logUsersActivity($type = 'staff', $content); #$this->logUsersActivity($type = 'staff', $content);
@@ -546,7 +600,7 @@ class ClientsController extends Controller
$request->validate([ $request->validate([
'client_id' => 'required', 'client_id' => 'required',
'network' => 'required', 'network' => 'required',
'shortcode' => 'required', 'shortcode' => 'required|numeric|digits_between:3,7',
'code_type' => 'required', 'code_type' => 'required',
'toll_free' => 'required', 'toll_free' => 'required',
'status' => 'required', 'status' => 'required',
@@ -555,7 +609,7 @@ class ClientsController extends Controller
'expiry_date' => 'required', 'expiry_date' => 'required',
'monthly_fee' => 'sometimes' 'monthly_fee' => 'sometimes'
]); ]);
$auth_user = session('current_user'); $auth_user = Auth::user();
$shortcode_arr = [ $shortcode_arr = [
'name' => $request->name, 'name' => $request->name,
@@ -565,7 +619,7 @@ class ClientsController extends Controller
'shortcode' => $request->shortcode, 'shortcode' => $request->shortcode,
'code_type' => $request->code_type, 'code_type' => $request->code_type,
'toll_free' => $request->toll_free, 'toll_free' => $request->toll_free,
'last_updated_by' => $auth_user['id'], 'last_updated_by' => $auth_user->id,
'launch_date' => $request->launch_date, 'launch_date' => $request->launch_date,
'expiry_date' => $request->expiry_date, 'expiry_date' => $request->expiry_date,
'status' => $request->status 'status' => $request->status
@@ -588,10 +642,11 @@ class ClientsController extends Controller
else{ else{
$data = ['code' => 3, 'msg' => 'Your request could not be handled at this time']; $data = ['code' => 3, 'msg' => 'Your request could not be handled at this time'];
} }
$user_id = session('current_user.id'); $user_id = $auth_user->id;
$username = session('current_user.name'); $username = $auth_user->name;
$content = "User ID : " . $user_id . " (" . $username . ") Added new short code for " . $client->name; $content = "User ID : " . $user_id . " (" . $username . ") Added new short code for " . $client->name;
$this->logUsersActivity($type = 'staff', $content); # active this later
#$this->logUsersActivity($type = 'staff', $content);
return response()->json($data, 200); return response()->json($data, 200);
} }
public function shortCodeUpdate(Request $request){ public function shortCodeUpdate(Request $request){
@@ -607,7 +662,7 @@ class ClientsController extends Controller
'launch_date' => 'required', 'launch_date' => 'required',
'expiry_date' => 'required' 'expiry_date' => 'required'
]); ]);
$auth_user = session('current_user'); $auth_user = Auth::user();
$mnoCountry = $this->getMnoCountry($request->network); $mnoCountry = $this->getMnoCountry($request->network);
if ($mnoCountry == false) { if ($mnoCountry == false) {
$data = ['code' => 3, 'msg' => 'Your request could not be handled at this time']; $data = ['code' => 3, 'msg' => 'Your request could not be handled at this time'];
@@ -641,11 +696,22 @@ class ClientsController extends Controller
else{ else{
$data = ['code' => 3, 'msg' => 'Your request could not be handled at this time']; $data = ['code' => 3, 'msg' => 'Your request could not be handled at this time'];
} }
$user_id = session('current_user.id'); $user_id = $auth_user->id;
$username = session('current_user.name'); $username = $auth_user->name;
$content = "User ID : " . $user_id . " (" . $username . ") updated short code enty for " . $request->short_code; $content = "User ID : " . $user_id . " (" . $username . ") updated short code enty for " . $request->short_code;
$this->logUsersActivity($type = 'staff', $content); #$this->logUsersActivity($type = 'staff', $content);
return response()->json($data, 200); return response()->json($data, 200);
} }
public function storeNote(Request $request)
{
$request->validate(['note' => 'required|string', 'client_id' => 'required|exists:clients,id']);
\App\Models\ClientNote::create($request->all());
return response()->json(['message' => 'Note added successfully!']);
}
} }

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
class ServicesController extends Controller
{
public function getServicesJson()
{
$services = Models\Service::select('id', 'name')->get();
return response()->json($services);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Models\Service;
class ClientPayment extends Model class ClientPayment extends Model
{ {
@@ -15,4 +16,21 @@ class ClientPayment extends Model
public function created_by_info(){ public function created_by_info(){
return $this->hasOne('App\Models\Account', 'id', 'auth_user_id'); return $this->hasOne('App\Models\Account', 'id', 'auth_user_id');
} }
// public function services(){
// return $this->hasMany('App\Models\Service', 'id', 'client_id');
// }
protected $casts = [
'services' => 'array',
];
public function getServiceNamesAttribute()
{
if (empty($this->services) || !is_array($this->services)) {
return collect();
}
return Service::whereIn('id', $this->services)->pluck('name');
}
} }

View File

@@ -46,7 +46,12 @@ return [
'throw' => false, 'throw' => false,
'report' => false, 'report' => false,
], ],
'client_files' => [
'driver' => 'local',
'root' => public_path('client_files'),
'url' => env('APP_URL').'/client_files',
'visibility' => 'public',
],
's3' => [ 's3' => [
'driver' => 's3', 'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'), 'key' => env('AWS_ACCESS_KEY_ID'),

Binary file not shown.

View File

@@ -1,14 +1,21 @@
document.addEventListener("DOMContentLoaded", function() {
if (typeof jQuery === 'undefined') {
console.error('jQuery is required for client-modals.js to work.');
return;
}
$(document).ready(function() { $(document).ready(function() {
console.log('Client modals AJAX script loaded with SweetAlert.');
// Generic AJAX handler for standard forms (Shortcodes & Payments) // Generic AJAX handler for standard forms (Shortcodes & Payments)
function submitAjaxForm(formId, modalId) { $(document).on('submit', '#shortcodeForm, #paymentForm, #noteForm', function(e) {
$(formId).on('submit', function(e) {
e.preventDefault(); e.preventDefault();
let $form = $(this); let $form = $(this);
let modalId = '#' + $form.closest('.modal').attr('id');
let $submitBtn = $form.find('button[type="submit"]'); let $submitBtn = $form.find('button[type="submit"]');
let originalBtnText = $submitBtn.text(); let originalBtnText = $submitBtn.text();
// Disable button and show loading state
$submitBtn.prop('disabled', true).text('Saving...'); $submitBtn.prop('disabled', true).text('Saving...');
$.ajax({ $.ajax({
@@ -19,12 +26,18 @@ $(document).ready(function() {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}, },
success: function(response) { success: function(response) {
// Hide modal
$(modalId).modal('hide'); $(modalId).modal('hide');
// Reset form
$form[0].reset(); $form[0].reset();
// Reload page to show fresh data (or dynamically append to table)
// Success SweetAlert
Swal.fire({
icon: 'success',
title: 'Success!',
text: response.message || 'Record saved successfully.',
confirmButtonColor: '#0d6efd'
}).then(() => {
location.reload(); location.reload();
});
}, },
error: function(xhr) { error: function(xhr) {
let errors = xhr.responseJSON?.errors; let errors = xhr.responseJSON?.errors;
@@ -32,39 +45,55 @@ $(document).ready(function() {
if (errors) { if (errors) {
errorMsg = Object.values(errors).flat().join('\n'); errorMsg = Object.values(errors).flat().join('\n');
} }
alert(errorMsg);
}, // Error SweetAlert
complete: function() { Swal.fire({
icon: 'error',
title: 'Validation Error',
text: errorMsg,
confirmButtonColor: '#dc3545'
});
$submitBtn.prop('disabled', false).text(originalBtnText); $submitBtn.prop('disabled', false).text(originalBtnText);
} }
}); });
}); });
}
// AJAX handler for Document Upload (Requires FormData for files) // AJAX handler for Document Upload (FormData)
function submitFileForm(formId, modalId) { $(document).on('submit', '#documentForm', function(e) {
$(formId).on('submit', function(e) {
e.preventDefault(); e.preventDefault();
let formElement = $(this)[0];
let formElement = this;
let $form = $(this);
let formData = new FormData(formElement); let formData = new FormData(formElement);
let $submitBtn = $(this).find('button[type="submit"]'); let modalId = '#' + $form.closest('.modal').attr('id');
let $submitBtn = $form.find('button[type="submit"]');
let originalBtnText = $submitBtn.text(); let originalBtnText = $submitBtn.text();
$submitBtn.prop('disabled', true).text('Uploading...'); $submitBtn.prop('disabled', true).text('Uploading...');
$.ajax({ $.ajax({
url: $(this).attr('action'), url: $form.attr('action'),
type: 'POST', type: 'POST',
data: formData, data: formData,
processData: false, // Essential for file uploads processData: false,
contentType: false, // Essential for file uploads contentType: false,
headers: { headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}, },
success: function(response) { success: function(response) {
$(modalId).modal('hide'); $(modalId).modal('hide');
formElement.reset(); formElement.reset();
// Success SweetAlert
Swal.fire({
icon: 'success',
title: 'Uploaded!',
text: response.message || 'Document uploaded successfully.',
confirmButtonColor: '#0d6efd'
}).then(() => {
location.reload(); location.reload();
});
}, },
error: function(xhr) { error: function(xhr) {
let errors = xhr.responseJSON?.errors; let errors = xhr.responseJSON?.errors;
@@ -72,17 +101,119 @@ $(document).ready(function() {
if (errors) { if (errors) {
errorMsg = Object.values(errors).flat().join('\n'); errorMsg = Object.values(errors).flat().join('\n');
} }
alert(errorMsg);
}, // Error SweetAlert
complete: function() { Swal.fire({
icon: 'error',
title: 'Upload Failed',
text: errorMsg,
confirmButtonColor: '#dc3545'
});
$submitBtn.prop('disabled', false).text(originalBtnText); $submitBtn.prop('disabled', false).text(originalBtnText);
} }
}); });
}); });
// Initialize Select2 for Payment Services when modal opens
$('#addPaymentModal').on('shown.bs.modal', function () {
let $select = $('#paymentServicesSelect');
// Only fetch if options haven't been loaded yet
if ($select.children('option').length === 0) {
$.ajax({
url: base_url + '/api/services', // Update route if needed
type: 'GET',
success: function(data) {
$select.empty();
data.forEach(function(service) {
$select.append(new Option(service.name, service.name, false, false));
});
// Initialize Select2 with Bootstrap 5 theme
$select.select2({
theme: 'bootstrap-5',
dropdownParent: $('#addPaymentModal'),
placeholder: 'Search and select services...'
});
},
error: function() {
console.error('Failed to load services for Select2.');
}
});
} else {
// If already loaded, just re-initialize if needed
$select.select2({
theme: 'bootstrap-5',
dropdownParent: $('#addPaymentModal'),
placeholder: 'Search and select services...'
});
}
});
// Clear Select2 values when modal is closed
$('#addPaymentModal').on('hidden.bs.modal', function () {
$('#paymentServicesSelect').val(null).trigger('change');
});
// --- DYNAMIC DOCUMENT ROW HANDLERS ---
$('#addRowBtn').on('click', function() {
let rowHtml = `
<div class="file-row card border bg-light p-3 mb-3 position-relative">
<div class="row g-2 align-items-center">
<div class="col-md-5">
<label class="form-label text-muted small fw-bold">Document Name / Title</label>
<input type="text" name="names[]" class="form-control" placeholder="e.g. Tax Clearance" required>
</div>
<div class="col-md-6">
<label class="form-label text-muted small fw-bold">Select File</label>
<input type="file" name="files[]" class="form-control" required>
</div>
<div class="col-md-1 text-end d-flex align-items-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-row-btn mt-4"><i class="bi bi-trash"></i></button>
</div>
</div>
</div>`;
$('#fileRowsContainer').append(rowHtml);
updateRemoveButtons();
});
// Remove row click handler
$(document).on('click', '.remove-row-btn', function() {
$(this).closest('.file-row').remove();
updateRemoveButtons();
});
// Hide delete button if only 1 row remains
function updateRemoveButtons() {
let totalRows = $('.file-row').length;
if (totalRows === 1) {
$('.remove-row-btn').hide();
} else {
$('.remove-row-btn').show();
}
} }
// Initialize the handlers // Reset modal fields when closed
submitAjaxForm('#shortcodeForm', '#addShortcodeModal'); $('#uploadDocumentModal').on('hidden.bs.modal', function() {
submitAjaxForm('#paymentForm', '#addPaymentModal'); $('#documentForm')[0].reset();
submitFileForm('#documentForm', '#uploadDocumentModal'); $('#fileRowsContainer').html(`
<div class="file-row card border bg-light p-3 mb-3 position-relative">
<div class="row g-2 align-items-center">
<div class="col-md-5">
<label class="form-label text-muted small fw-bold">Document Name / Title</label>
<input type="text" name="names[]" class="form-control" placeholder="e.g. Signed Service Agreement" required>
</div>
<div class="col-md-6">
<label class="form-label text-muted small fw-bold">Select File</label>
<input type="file" name="files[]" class="form-control" required>
</div>
<div class="col-md-1 text-end d-flex align-items-end">
<button type="button" class="btn btn-outline-danger btn-sm remove-row-btn mt-4" style="display: none;"><i class="bi bi-trash"></i></button>
</div>
</div>
</div>
`);
});
});
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -89,6 +89,8 @@
<option value="ussd">USSD</option> <option value="ussd">USSD</option>
<option value="airtime">Airtime</option> <option value="airtime">Airtime</option>
<option value="voice">Voice</option> <option value="voice">Voice</option>
<option value="aggregatevoice">Aggregation Voice</option>
<option value="voice">Voice</option>
</select> </select>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">

View File

@@ -16,6 +16,7 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label text-muted small fw-bold">Code Type</label> <label class="form-label text-muted small fw-bold">Code Type</label>
<select name="code_type" class="form-select" required> <select name="code_type" class="form-select" required>
<option value="" selected disabled>-- Select --</option>
<option value="sms">SMS</option> <option value="sms">SMS</option>
<option value="ussd">USSD</option> <option value="ussd">USSD</option>
<option value="voice">Voice</option> <option value="voice">Voice</option>
@@ -23,16 +24,44 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label text-muted small fw-bold">Short Code / Number</label> <label class="form-label text-muted small fw-bold">Short Code / Number</label>
<input type="text" name="shortcode" class="form-control" placeholder="e.g. 1234 or *123#" required> <input type="number" name="shortcode" class="form-control" placeholder="e.g. 5678" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label text-muted small fw-bold">Network</label> <label class="form-label text-muted small fw-bold">Network</label>
<input type="text" name="network" class="form-control" placeholder="e.g. MTN, Vodafone, AirtelTigo"> <input type="text" name="network" class="form-control" placeholder="e.g. MTN, Vodafone, AirtelTigo">
</div> </div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">Monthly Fee</label>
<input type="text" name="monthly_fee" class="form-control" placeholder="">
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">Toll Free</label>
<select name="toll_free" class="form-select" required>
<option value="" selected disabled>-- Select --</option>
<option value="yes">YES</option>
<option value="no">NO</option>
</select>
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">Status</label>
<select name="status" class="form-select" required>
<option value="" selected disabled>-- Select --</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">Launch Date</label>
<input type="date" name="launch_date" class="form-control">
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label text-muted small fw-bold">Expiry Date</label> <label class="form-label text-muted small fw-bold">Expiry Date</label>
<input type="date" name="expiry_date" class="form-control"> <input type="date" name="expiry_date" class="form-control">
</div> </div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">Remarks</label>
<textarea name="remarks" class="form-control"></textarea>
</div>
</div> </div>
<div class="modal-footer bg-light"> <div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
@@ -63,6 +92,13 @@
<label class="form-label text-muted small fw-bold">Invoice / Reference Number</label> <label class="form-label text-muted small fw-bold">Invoice / Reference Number</label>
<input type="text" name="invoice_number" class="form-control" placeholder="e.g. INV-2026-001" required> <input type="text" name="invoice_number" class="form-control" placeholder="e.g. INV-2026-001" required>
</div> </div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">Services</label>
<select name="services[]" id="paymentServicesSelect" class="form-select" multiple="multiple" required>
<!-- Options will be loaded dynamically via AJAX -->
</select>
<div class="form-text text-muted">Select one or more services associated with this payment.</div>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label text-muted small fw-bold">Amount</label> <label class="form-label text-muted small fw-bold">Amount</label>
<input type="number" step="0.01" name="invoice_amount" class="form-control" placeholder="0.00" required> <input type="number" step="0.01" name="invoice_amount" class="form-control" placeholder="0.00" required>
@@ -91,35 +127,78 @@
<!-- ========================================== --> <!-- ========================================== -->
<!-- 3. UPLOAD DOCUMENT MODAL --> <!-- 3. UPLOAD DOCUMENT MODAL (MULTI-FILE) -->
<!-- ========================================== --> <!-- ========================================== -->
<div class="modal fade" id="uploadDocumentModal" tabindex="-1" aria-labelledby="uploadDocumentModalLabel" aria-hidden="true"> <div class="modal fade" id="uploadDocumentModal" tabindex="-1" aria-labelledby="uploadDocumentModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<form id="documentForm" action="{{ route('client-files.store') }}" method="POST" enctype="multipart/form-data"> <form id="documentForm" action="{{ route('client-files.store') }}" method="POST" enctype="multipart/form-data">
@csrf @csrf
<input type="hidden" name="client_id" value="{{ $showclient->id }}"> <input type="hidden" name="client_id" value="{{ $showclient->id }}">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title fw-bold" id="uploadDocumentModalLabel"><i class="bi bi-file-earmark-arrow-up text-primary me-2"></i>Upload Client Document</h5> <h5 class="modal-title fw-bold" id="uploadDocumentModalLabel"><i class="bi bi-file-earmark-arrow-up text-primary me-2"></i>Upload Client Documents</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3">
<div id="fileRowsContainer">
<!-- Initial Row -->
<div class="file-row card border bg-light p-3 mb-3 position-relative">
<div class="row g-2 align-items-center">
<div class="col-md-5">
<label class="form-label text-muted small fw-bold">Document Name / Title</label> <label class="form-label text-muted small fw-bold">Document Name / Title</label>
<input type="text" name="name" class="form-control" placeholder="e.g. Signed Service Agreement" required> <input type="text" name="names[]" class="form-control" placeholder="e.g. Signed Service Agreement" required>
</div> </div>
<div class="mb-3"> <div class="col-md-6">
<label class="form-label text-muted small fw-bold">Select File</label> <label class="form-label text-muted small fw-bold">Select File</label>
<input type="file" name="file" class="form-control" required> <input type="file" name="files[]" class="form-control" required>
<div class="form-text text-muted">Accepted formats: PDF, DOC, DOCX, JPG, PNG (Max size depends on server config).</div>
</div> </div>
<div class="col-md-1 text-end d-flex align-items-end">
<!-- Delete button hidden for the first row -->
<button type="button" class="btn btn-outline-danger btn-sm remove-row-btn mt-4" style="display: none;"><i class="bi bi-trash"></i></button>
</div>
</div>
</div>
</div>
<!-- Add More Button -->
<button type="button" id="addRowBtn" class="btn btn-sm btn-outline-secondary fw-bold mt-1">
<i class="bi bi-plus-circle me-1"></i> Add Another Document
</button>
<div class="form-text text-muted mt-2">Accepted formats: PDF, DOC, DOCX, JPG, PNG. You can upload multiple files at once.</div>
</div> </div>
<div class="modal-footer bg-light"> <div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary btn-sm px-4">Upload Document</button> <button type="submit" class="btn btn-primary btn-sm px-4">Upload All Documents</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<!-- Client Note Modal -->
<div class="modal fade" id="addNoteModal" tabindex="-1" aria-labelledby="addNoteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form id="noteForm" action="{{ route('client-notes.store') }}" method="POST">
@csrf
<input type="hidden" name="client_id" value="{{ $showclient->id }}">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold">Add Note</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<textarea name="note" class="form-control" rows="4" placeholder="Enter notes here..." required></textarea>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary btn-sm">Save Note</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -296,7 +296,12 @@
<!-- Tab Links --> <!-- Tab Links -->
<ul class="nav nav-tabs card-header-tabs m-0" id="clientRecordTabs" role="tablist"> <ul class="nav nav-tabs card-header-tabs m-0" id="clientRecordTabs" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active fw-bold text-dark" id="shortcodes-tab" data-bs-toggle="tab" data-bs-target="#shortcodes-pane" type="button" role="tab"> <button class="nav-link active fw-bold" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-pane" type="button">
<i class="bi bi-chat-square-text me-1"></i> Notes
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link fw-bold text-dark" id="shortcodes-tab" data-bs-toggle="tab" data-bs-target="#shortcodes-pane" type="button" role="tab">
<i class="bi bi-code-square text-primary me-1"></i> Short Codes <i class="bi bi-code-square text-primary me-1"></i> Short Codes
<span class="badge bg-primary bg-opacity-10 text-primary ms-1">{{ count($voice_codes) + count($sms_codes) + count($ussd_codes) }}</span> <span class="badge bg-primary bg-opacity-10 text-primary ms-1">{{ count($voice_codes) + count($sms_codes) + count($ussd_codes) }}</span>
</button> </button>
@@ -331,9 +336,38 @@
</div> </div>
<div class="card-body bg-light tab-content" id="clientRecordTabsContent"> <div class="card-body bg-light tab-content" id="clientRecordTabsContent">
<!-- tab 0 Notes -->
<div class="tab-pane fade show active" id="notes-pane" role="tabpanel" tabindex="0">
<div class="d-flex justify-content-end mb-3">
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addNoteModal">
<i class="bi bi-plus-lg"></i> Add Note
</button>
</div>
<div class="list-group">
@forelse($show_notes as $note)
<div class="list-group-item p-3 mb-2 border rounded shadow-sm">
<p class="mb-2 text-dark">{{ $note->notes_body }}</p>
<div class="d-flex justify-content-between align-items-center text-muted" style="font-size: 0.8rem;">
<span>
<i class="bi bi-person-fill me-1 text-primary"></i>
<strong class="text-secondary">{{ $note->created_by_info->name ?? 'System User' }}</strong>
</span>
<span>
<i class="bi bi-clock me-1"></i>
{{ $note->created_at->format('d M Y, h:i A') }}
</span>
</div>
</div>
@empty
<div class="text-center py-4 text-muted small">
<i class="bi bi-chat-square-text fs-4 d-block mb-1"></i> No notes added for this client yet.
</div>
@endforelse
</div>
</div>
<!-- TAB 1: SHORT CODES --> <!-- TAB 1: SHORT CODES -->
<div class="tab-pane fade show active" id="shortcodes-pane" role="tabpanel" tabindex="0"> <div class="tab-pane fade " id="shortcodes-pane" role="tabpanel" tabindex="0">
@php @php
$allCodes = collect($voice_codes)->concat($sms_codes)->concat($ussd_codes); $allCodes = collect($voice_codes)->concat($sms_codes)->concat($ussd_codes);
@endphp @endphp
@@ -343,22 +377,29 @@
<table class="table table-hover align-middle mb-0 bg-white border rounded"> <table class="table table-hover align-middle mb-0 bg-white border rounded">
<thead class="table-light"> <thead class="table-light">
<tr style="font-size: 0.8rem;" class="text-secondary text-uppercase"> <tr style="font-size: 0.8rem;" class="text-secondary text-uppercase">
<th>Type</th> <th style="width: 10%;">Type</th>
<th>Shortcode</th> <th style="width: 15%;">Shortcode</th>
<th>Network</th> <th style="width: 20%;">Network</th>
<th>Status</th> <th style="width: 10%;">Status</th>
<th>Expiry Date</th> <th style="width: 30%;">Remarks</th>
<th style="width: 15%;">Expiry Date</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($allCodes as $code) @foreach($allCodes as $code)
<tr> <tr>
<td><span class="badge bg-secondary text-uppercase" style="font-size: 0.7rem;">{{ $code->code_type ?? 'N/A' }}</span></td> <td><span class="badge bg-secondary text-uppercase" style="font-size: 0.7rem;">{{ $code->code_type ?? 'N/A' }}</span></td>
<td class="fw-bold text-dark font-monospace">{{ $code->shortcode ?? 'N/A' }}</td> <td class="fw-bold text-dark font-monospace text-break">{{ $code->shortcode ?? 'N/A' }}</td>
<td>{{ $code->network ?? 'N/A' }}</td> <td class="text-break">{{ $code->network ?? 'N/A' }}</td>
<td> <td>
<span class="badge bg-success bg-opacity-10 text-success">{{ $code->status ?? 'Active' }}</span> <span class="badge bg-success bg-opacity-10 text-success">{{ $code->status ?? 'Active' }}</span>
</td> </td>
<!-- Styled Remarks Column with text-wrap and max-width bounds -->
<td>
<div class="text-muted text-break" style="font-size: 0.9rem; max-width: 250px; line-height: 1.3;">
{{ $code->remarks ?? '-' }}
</div>
</td>
<td class="text-muted small">{{ $code->expiry_date ? \Carbon\Carbon::parse($code->expiry_date)->format('d M Y') : 'N/A' }}</td> <td class="text-muted small">{{ $code->expiry_date ? \Carbon\Carbon::parse($code->expiry_date)->format('d M Y') : 'N/A' }}</td>
</tr> </tr>
@endforeach @endforeach
@@ -380,6 +421,7 @@
<thead class="table-light"> <thead class="table-light">
<tr style="font-size: 0.8rem;" class="text-secondary text-uppercase"> <tr style="font-size: 0.8rem;" class="text-secondary text-uppercase">
<th>Invoice #</th> <th>Invoice #</th>
<th>Services</th>
<th>Amount</th> <th>Amount</th>
<th>Date</th> <th>Date</th>
<th>Status</th> <th>Status</th>
@@ -389,7 +431,23 @@
@foreach($recent_payments as $payment) @foreach($recent_payments as $payment)
<tr> <tr>
<td class="fw-bold text-dark">{{ $payment->invoice_number ?? 'N/A' }}</td> <td class="fw-bold text-dark">{{ $payment->invoice_number ?? 'N/A' }}</td>
<td class="fw-semibold text-success">{{ number_format($payment->invoice_amount ?? 0, 2) }}</td>
<td>
@php
$serviceNames = $payment->service_names;
@endphp
@if($serviceNames->count() > 0)
<div class="d-flex flex-wrap gap-1">
@foreach($serviceNames as $serviceName)
<span class="badge bg-light text-primary border" style="font-size: 0.75rem;">
{{ $serviceName }}
</span>
@endforeach
</div>
@else
<span class="text-muted small">-</span>
@endif
</td>
<td class="text-muted small">{{ $payment->invoice_date ? \Carbon\Carbon::parse($payment->invoice_date)->format('d M Y') : 'N/A' }}</td> <td class="text-muted small">{{ $payment->invoice_date ? \Carbon\Carbon::parse($payment->invoice_date)->format('d M Y') : 'N/A' }}</td>
<td> <td>
<span class="badge bg-info bg-opacity-10 text-info text-uppercase">{{ $payment->invoice_status ?? 'Pending' }}</span> <span class="badge bg-info bg-opacity-10 text-info text-uppercase">{{ $payment->invoice_status ?? 'Pending' }}</span>
@@ -421,7 +479,8 @@
</div> </div>
</div> </div>
@if(!empty($file->file_path)) @if(!empty($file->file_path))
<a href="{{ asset('storage/client_files/' . $file->file_path) }}" target="_blank" class="btn btn-sm btn-light text-primary" title="Download File">
<a href="{{ route('client-files.download', $file->id) }}" class="btn btn-sm btn-light text-primary" target="_blank" title="{{ $file->name }}">
<i class="bi bi-download"></i> <i class="bi bi-download"></i>
</a> </a>
@endif @endif
@@ -489,6 +548,8 @@
</div> </div>
@include('clients.partials.shortcode-payment-docs-modal') @include('clients.partials.shortcode-payment-docs-modal')
@endsection @endsection
@section('scripts') @push('scripts')
<script src="{{ asset('js/client-show-modals.js') }}"></script> <script src="{{ asset('public/assets/js/client-show-modal.js') }}"></script>
@endsection <script>
</script>
@endpush

View File

@@ -10,6 +10,10 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<!-- Select2 CSS -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
<style> <style>
body { background-color: #f8f9fc; } body { background-color: #f8f9fc; }
@@ -55,7 +59,10 @@
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- SweetAlert2 CDN -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Select2 JS (Place right after jQuery) -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {
$.ajaxSetup({ $.ajaxSetup({
@@ -68,6 +75,7 @@
} }
updateClock(); updateClock();
setInterval(updateClock, 1000); setInterval(updateClock, 1000);
}); });
</script> </script>

View File

@@ -6,6 +6,8 @@ Route::get('test', function () {
return view('clients.index'); return view('clients.index');
}); });
//bd8f75cc-0f63-4707-9a2e-361800a0d94c
Auth::routes(); Auth::routes();
Route::middleware(['auth'])->group(function () { Route::middleware(['auth'])->group(function () {
@@ -20,9 +22,16 @@ Route::middleware(['auth'])->group(function () {
// {{ route('shortcodes.store') }} // {{ route('shortcodes.store') }}
// {{ route('client-payments.store') }} // {{ route('client-payments.store') }}
// {{ route('client-files.store') }} // {{ route('client-files.store') }}
Route::get('/clients/shortcodes-store', [App\Http\Controllers\ClientController::class, 'shortcodeStore'])->name('shortcodes.store'); // Route::post('/shortcodes', [App\Http\Controllers\ShortCodesController::class, 'store'])->name('shortcodes.store');
Route::get('/clients/payments-store', [App\Http\Controllers\ClientController::class, 'paymentsStore'])->name('client-payments.store'); Route::post('/clients/shortcodes-store', [App\Http\Controllers\ClientsController::class, 'shortcodeStore'])->name('shortcodes.store');
Route::get('/clients/files-store', [App\Http\Controllers\ClientController::class, 'storeFiles'])->name('client-files.store'); Route::post('/clients/payments-store', [App\Http\Controllers\ClientsController::class, 'paymentsStore'])->name('client-payments.store');
Route::post('/clients/files-store', [App\Http\Controllers\ClientsController::class, 'storeDocument'])->name('client-files.store');
Route::post('/clients/notes-store', [App\Http\Controllers\ClientsController::class, 'storeNote'])->name('client-notes.store');
Route::get('/client-files/download/{id}', [App\Http\Controllers\ClientsController::class, 'clientFiledownload'])->name('client-files.download');
Route::resource('clients', App\Http\Controllers\ClientsController::class); Route::resource('clients', App\Http\Controllers\ClientsController::class);
@@ -36,5 +45,9 @@ Route::middleware(['auth'])->group(function () {
Route::get('/senderids', [App\Http\Controllers\SenderidsController::class, 'index']); Route::get('/senderids', [App\Http\Controllers\SenderidsController::class, 'index']);
Route::get('/shortcodes', [App\Http\Controllers\ShortCodesController::class, 'index']); Route::get('/shortcodes', [App\Http\Controllers\ShortCodesController::class, 'index']);
Route::get('/api/services', [App\Http\Controllers\ServicesController::class, 'getServicesJson']);
Route::post('/logout', [App\Http\Controllers\AuthController::class, 'logout'])->name('logout'); Route::post('/logout', [App\Http\Controllers\AuthController::class, 'logout'])->name('logout');
}); });