Compare commits

...

12 Commits

Author SHA1 Message Date
Kwesi Banson Jnr
d0f50a8549 fixed a bug on the sms units/charge display 2026-04-27 17:37:32 +00:00
Kwesi Banson Jnr
757f908404 added User management 2026-04-24 12:09:36 +00:00
Kwesi Banson Jnr
16f2dbbdb6 removed login email placeholder and added datepicker on date filter on sms list 2026-04-22 19:15:46 +00:00
Kwesi Banson Jnr
d42764a89d added SMS balance and Sent SMS 2026-04-22 13:46:57 +00:00
Kwesi Banson Jnr
431dbc7da6 minor updates in master and index-test 2026-04-10 12:13:34 +00:00
Kwesi Banson Jnr
db00b8e2ca tabulator fixes 2026-04-10 12:05:45 +00:00
Kwesi Banson Jnr
eb8ef2ee8e removed some comments from the traffic page 2026-04-10 11:59:31 +00:00
Kwesi Banson Jnr
a459e35982 update master.blade.php with hardcoded base URL 2026-04-10 11:56:39 +00:00
Kwesi Banson Jnr
759c516714 removed send sms and apps link 2026-04-10 11:47:03 +00:00
Kwesi Banson Jnr
814761ef83 a little update on the account.md 2026-04-10 11:20:42 +00:00
Kwesi Banson Jnr
fc6f5e8300 increase initial page load to 1000 and removed message status 2026-04-09 20:00:16 +00:00
Kwesi Banson Jnr
ffde370ed3 add URL class in Service Provider 2026-04-09 11:42:45 +00:00
19 changed files with 1388 additions and 461 deletions

View File

@@ -8,3 +8,15 @@
### client ### client
- hixu@mailinator.com/frantic@300 - hixu@mailinator.com/frantic@300
## Sunking
- https://smsportal.clickmlapps.com/
- info@sunking.com
- 847SIcgO9sUX
### add
- change pagination to 1000 from 20
- remove delivery receipt
http://10.199.0.11/botswana/bpc/web

View File

@@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models;
use App\Utilities\ApiCalls;
use Session;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Hash;
class ClientUsersController extends Controller
{
public function indexBak(){
$user_list = Models\ClientSession::get();
$data = [
'page_title' => 'Users',
];
return view('client-users.index', $data);
}
public function index()
{
$data = [
'page_title' => 'Users',
];
return view('client-users.index', $data);
}
// Read: Fetch data for the jQuery table
// public function fetch()
// {
// $sessions = Models\ClientSession::orderBy('id', 'desc')->paginate(5);
// return response()->json(['sessions' => $sessions]);
// }
public function fetch(Request $request)
{
$query = Models\ClientSession::query();
// Check if the search parameter has a value
if ($request->has('search') && !empty($request->search)) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('email', 'LIKE', "%{$search}%")
->orWhere('role', 'LIKE', "%{$search}%");
});
}
$sessions = $query->orderBy('id', 'desc')->paginate(5);
return response()->json(['sessions' => $sessions]);
}
public function store(Request $request)
{
$request->validate([
'email' => 'required|email|unique:client_sessions,email',
'role' => 'required|string',
'password' => 'required|string'
]);
$session = Models\ClientSession::create([
'email' => $request->email,
'role' => $request->role,
'client_id' => session('current_user.org_id'),
'password' => Hash::make($request->password)
]);
return response()->json(['status' => 'success', 'message' => 'User created successfully!']);
}
public function edit($id)
{
$session = Models\ClientSession::findOrFail($id);
return response()->json(['session' => $session]);
}
public function update(Request $request, $id)
{
$request->validate([
'email' => 'required|email|unique:client_sessions,email,' . $id,
'role' => 'required|string',
'password' => 'required|string'
]);
$session = Models\ClientSession::findOrFail($id);
$session->update([
'email' => $request->email,
'role' => $request->role,
'client_id' => session('current_user.org_id'),
'password' => Hash::make($request->password)
]);
return response()->json(['status' => 'success', 'message' => 'User updated successfully!']);
}
// Delete: Remove record
public function destroy($id)
{
Models\ClientSession::findOrFail($id)->delete();
return response()->json(['status' => 'success', 'message' => 'User deleted successfully!']);
}
}

View File

@@ -83,20 +83,22 @@ class ClientsLoginController extends Controller
$logged_in = ''; $logged_in = '';
$client = Models\ClientSession::where('email', $request->email)->first(); $client = Models\ClientSession::where('email', $request->email)->first();
if ($client == false) { if ($client == false) {
return redirect()->back()->withErrors(['Invalid credentials']); return redirect()->back()->withErrors(['Invalid credentials']);
} }
$client_url = "clients/" . $client->client_id; $client_url = "clients/" . $client->client_id;
$result = ApiCalls::CurlGet($client_url); $result = ApiCalls::CurlGet($client_url);
$result_arr = json_decode($result, true); $result_arr = json_decode($result, true);
$logged_in = $result_arr; $logged_in = $result_arr;
// dd($logged_in);
$request->session()->regenerate(true); $request->session()->regenerate(true);
$request->session()->put('current_user.user_id', $logged_in['id']); $request->session()->put('current_user.user_id', $logged_in['id']);
$request->session()->put('current_user.org_id', $logged_in['id']); $request->session()->put('current_user.org_id', $logged_in['id']);
$request->session()->put('current_user.name', $logged_in['name']); $request->session()->put('current_user.name', $logged_in['name']);
$request->session()->put('current_user.email', $logged_in['email']); $request->session()->put('current_user.email', $logged_in['email']);
$request->session()->put('current_user.role', $client['role']);
$request->session()->put('current_user.phoneNumber', $logged_in['phoneNumber']); $request->session()->put('current_user.phoneNumber', $logged_in['phoneNumber']);
$request->session()->put('current_user.status', $logged_in['status']); $request->session()->put('current_user.status', $logged_in['status']);
$request->session()->put('current_user.createdAt', $logged_in['createdAt']); $request->session()->put('current_user.createdAt', $logged_in['createdAt']);

View File

@@ -8,6 +8,7 @@ use App\Utilities\ApiCalls;
use Session; use Session;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use DateTime;
class ClientsTrafficController extends Controller class ClientsTrafficController extends Controller
{ {
@@ -38,17 +39,36 @@ class ClientsTrafficController extends Controller
); );
} }
public function index(){ public function index(){
//dump(session('current_user'));
$client_id = session('current_user.org_id'); $client_id = session('current_user.org_id');
$clients_url = "messages/client/$client_id?page=0&size=20&sort=createdAt,desc"; $clients_url = "messages/client/$client_id?page=0&size=1000&sort=createdAt,desc";
$result = ApiCalls::CurlGet($clients_url); $result = ApiCalls::CurlGet($clients_url);
$result_arr = json_decode($result); $result_arr = json_decode($result);
// dump($result_arr);
$current_date = new DateTime();
$start_date = $current_date->modify('-10 days');
$start_date = $start_date->format('Y-m-d');
$end_date = date('Y-m-d');
$daily_smsunits_url = "ucm/reports/daily-sms-units?from=$start_date&to=$end_date";
$result = ApiCalls::CurlGet($daily_smsunits_url);
$sms_units_arr = json_decode($result, true);
$orgId = session('current_user.org_id');
$sms_balance_url = "clients/$orgId/balance";
$result = ApiCalls::CurlGet($sms_balance_url);
$balance_arr = json_decode($result, true);
// dump($sms_units_arr);
$data = [ $data = [
'page_title' => 'SMS Traffic', 'page_title' => 'SMS Traffic',
'traffic_arr' => $result_arr 'traffic_arr' => $result_arr,
'sms_units_arr' => $sms_units_arr,
'balance_arr' => $balance_arr
]; ];
return view('client-traffic.index-test', $data); return view('client-traffic.index-main', $data);
} }
public function indexTabulator(Request $request){ public function indexTabulator(Request $request){
$client = new Client(); $client = new Client();
@@ -155,6 +175,22 @@ class ClientsTrafficController extends Controller
} }
Session::flash('success_message', 'Message successfully submitted for delivery!'); Session::flash('success_message', 'Message successfully submitted for delivery!');
return redirect("send-sms"); return redirect("send-sms");
}
public function dailySmsUnits(Request $request){
$this->validate($request, [
'start_date' => 'required|date',
'end_date' => 'required|date',
]);
$start_date = $request->start_date;
$end_date = $request->end_date;
$daily_smsunits_url = "ucm/reports/daily-sms-units?from=$start_date&to=$end_date";
$result = ApiCalls::CurlGet($daily_smsunits_url);
\Log::info('SMS Units Response ' . $result);
$data = json_decode($result, true);
return response()->json($data);
} }
public function newAppStore(Request $request){ public function newAppStore(Request $request){

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class RoleMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
#public function handle(Request $request, Closure $next): Response
public function handle(Request $request, Closure $next, string $role): Response
{
if (session('current_user.role') !== $role) {
abort(403, 'You do not have access to this page.');
}
return $next($request);
}
}

View File

@@ -3,10 +3,9 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\Traits\HasRoles;
class ClientSession extends Model class ClientSession extends Model
{ {
protected $guarded = [ protected $guarded = ['id'];
'id'
];
} }

View File

@@ -3,6 +3,8 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\URL;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {

View File

@@ -4,6 +4,7 @@ use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\CheckClientSession; use App\Http\Middleware\CheckClientSession;
use App\Http\Middleware\RoleMiddleware;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
@@ -15,6 +16,7 @@ return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
$middleware->alias([ $middleware->alias([
'checksession' => CheckClientSession::class, 'checksession' => CheckClientSession::class,
'checkrole' => RoleMiddleware::class,
]); ]);
}) })
->withExceptions(function (Exceptions $exceptions) { ->withExceptions(function (Exceptions $exceptions) {

206
config/permission.php Normal file
View File

@@ -0,0 +1,206 @@
<?php
use Spatie\Permission\DefaultTeamResolver;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
return [
'models' => [
/*
* When using the "HasPermissions" trait from this package, we need to know which
* Eloquent model should be used to retrieve your permissions. Of course, it
* is often just the "Permission" model but you may use whatever you like.
*
* The model you want to use as a Permission model needs to implement the
* `Spatie\Permission\Contracts\Permission` contract.
*/
'permission' => Permission::class,
/*
* When using the "HasRoles" trait from this package, we need to know which
* Eloquent model should be used to retrieve your roles. Of course, it
* is often just the "Role" model but you may use whatever you like.
*
* The model you want to use as a Role model needs to implement the
* `Spatie\Permission\Contracts\Role` contract.
*/
'role' => Role::class,
],
'table_names' => [
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'roles' => 'roles',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your permissions. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'permissions' => 'permissions',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your models permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_permissions' => 'model_has_permissions',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your models roles. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_roles' => 'model_has_roles',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'role_has_permissions' => 'role_has_permissions',
],
'column_names' => [
/*
* Change this if you want to name the related pivots other than defaults
*/
'role_pivot_key' => null, // default 'role_id',
'permission_pivot_key' => null, // default 'permission_id',
/*
* Change this if you want to name the related model primary key other than
* `model_id`.
*
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/
'model_morph_key' => 'model_id',
/*
* Change this if you want to use the teams feature and your related model's
* foreign key is other than `team_id`.
*/
'team_foreign_key' => 'team_id',
],
/*
* When set to true, the method for checking permissions will be registered on the gate.
* Set this to false if you want to implement custom logic for checking permissions.
*/
'register_permission_check_method' => true,
/*
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
*/
'register_octane_reset_listener' => false,
/*
* Events will fire when a role or permission is assigned/unassigned:
* \Spatie\Permission\Events\RoleAttached
* \Spatie\Permission\Events\RoleDetached
* \Spatie\Permission\Events\PermissionAttached
* \Spatie\Permission\Events\PermissionDetached
*
* To enable, set to true, and then create listeners to watch these events.
*/
'events_enabled' => false,
/*
* Teams Feature.
* When set to true the package implements teams using the 'team_foreign_key'.
* If you want the migrations to register the 'team_foreign_key', you must
* set this to true before doing the migration.
* If you already did the migration then you must make a new migration to also
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
* (view the latest version of this package's migration file)
*/
'teams' => false,
/*
* The class to use to resolve the permissions team id
*/
'team_resolver' => DefaultTeamResolver::class,
/*
* Passport Client Credentials Grant
* When set to true the package will use Passports Client to check permissions
*/
'use_passport_client_credentials' => false,
/*
* When set to true, the required permission names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
/*
* When set to true, the required role names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_role_in_exception' => false,
/*
* By default wildcard permission lookups are disabled.
* See documentation to understand supported syntax.
*/
'enable_wildcard_permission' => false,
/*
* The class to use for interpreting wildcard permissions.
* If you need to modify delimiters, override the class and specify its name here.
*/
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
/* Cache-specific settings */
'cache' => [
/*
* By default all permissions are cached for 24 hours to speed up performance.
* When permissions or roles are updated the cache is flushed automatically.
*/
'expiration_time' => DateInterval::createFromDateString('24 hours'),
/*
* The cache key used to store all permissions.
*/
'key' => 'spatie.permission.cache',
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
* file. Using 'default' here means to use the `default` set in cache.php.
*/
'store' => 'default',
],
];

View File

@@ -0,0 +1,134 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), Exception::class, 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // permission id
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
};

35
info.md Normal file
View File

@@ -0,0 +1,35 @@
Messaging Notes- meeting with Victor on messaging with focus on the dashboard
They would like a Finance feature that shows the account balances
App to include option for report that shows balances , consumption and balances
They do not require CDRs for the report but volumes of the messages sent
Messaging report for call centre usage that enables them to extract OTP
Message access for last 3-6 hours
Sam added that we can provide functionality for the App to be useable by multiple departments
Victor will be able to share feedback on the endpoint change by 24th April.
They have Mozambique and Madagascar offices that we could potentially onboard and Victor will loop us in but mentioned they would require the dashboard functionality
User Management
Ability to have multiple user logins with different roles. The roles will have different functions and accessibility as shown below;
Administrator
Overall permissions with access to the following;
- SMS messages
- Finance related views ie Current account balance, SMS charges
- Create additional users
- View/dowload reports
- SMS sents
- SMS usage balance over a period
Finance
- Current balance
- SMS usage over a period
- Consumption/Usage reports
Customer Care
- SMS messages
Kindly provide a way to view or extract a report for the following
- Periodic SMS usage (Financial)
- Periodic SMS sent
administrator, finance, customercare

View File

@@ -0,0 +1,123 @@
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const startDateElement = document.getElementById('startDate');
const endDateElement = document.getElementById('endDate');
const startDatepicker = new Datepicker(startDateElement, {
autohide: true,
format: 'yyyy-mm-dd'
});
const endDatepicker = new Datepicker(endDateElement, {
autohide: true,
format: 'yyyy-mm-dd'
});
function sendDailySmsUnits() {
document.getElementById('loadingOverlay').style.display = 'flex';
const endpoint = "client-dailysmsunits";
const startDate = startDateElement.value;
const endDate = endDateElement.value;
fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-TOKEN': token },
body: 'start_date=' + encodeURIComponent(startDate) + '&end_date=' + encodeURIComponent(endDate)
})
.then(response => response.json())
.then(data => {
// document.getElementById('loadingSpinner').style.display = 'none';
document.getElementById('loadingOverlay').style.display = 'none';
const theReportRange = document.getElementById('reportRange');
const theSmsUnitsValue = document.getElementById('smsUnitsValue');
theReportRange.innerHTML = data.reportDate;
theSmsUnitsValue.innerHTML = "SMS Units : " + data.smsUnits + "| Charge : " + data.clientChargeTotal.toFixed(2);
})
.catch(error => console.error('Error:', error));
}
endDateElement.addEventListener('changeDate', sendDailySmsUnits);
function statusDesign (cell, formatterParams){
var value = cell.getValue();
if (value !== null) {
if(value.includes('SENT')){
return "<span style='color:#3FB449; font-weight:bold;'>" + value + "</span>";
}
else{
return "<span style='color:#E4A11B;'>" + value + "</span>";
}
}
}
var table = new Tabulator("#message-table", {
ajaxURL: base_url = "client-traffic-tabulator",
ajaxConfig: {
method: "GET",
headers: {
"Content-type": "application/json; charset=utf-8",
},
},
ajaxParams: {size: 1000},
pagination: "remote",
paginationSize: 20,
paginationDataSent: {
"page": "page",
"size": "size"
},
paginationDataReceived: {
"last_page": "totalPages",
"data": "content",
"current_page": "number",
"total": "totalElements"
},
ajaxResponse: function(url, params, response) {
return response.content;
},
columns: [
{title: "Sender", field: "from", width:150, headerFilter:"input"},
{title: "Msisdn", field: "to", width:150, headerFilter:"input"},
{title:"Message", field:"message", width:650, formatter:"textarea", headerFilter:"input"},
{
title:"Date Created",
field:"createdAt",
width:200,
formatter:"datetime",
formatterParams:{
inputFormat:"iso",
outputFormat:"yyyy-MM-dd",
invalidPlaceholder:"(invalid date)"
},
headerFilter:function(cell, onRendered, success, cancel){
var input = document.createElement("input");
input.type = "date";
input.addEventListener("change", function(){
console.log(input.value);
success(input.value);
});
return input;
},
headerFilterFunc:function(headerValue, rowValue){
if(!headerValue){ return true; } // no filter
if(!rowValue){ return false; }
const rowDate = new Date(rowValue);
const formatted = rowDate.toISOString().split("T")[0]; // yyyy-MM-dd
return formatted === headerValue;
}
}
],
});
document.getElementById("download-pdf").addEventListener("click", function(){
table.download("pdf", "messages.pdf", {
orientation:"portrait", // portrait or landscape
title:"Messages Export", // document title
});
});
document.getElementById("download-xlsx").addEventListener("click", function(){
table.download("xlsx", "messages.xlsx", {sheetName:"Messages"});
});

View File

@@ -1,217 +1,192 @@
$(document).ready(function(){ $(document).ready(function() {
// $('.editUserBtn').click(function(evnt){ $.ajaxSetup({
headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') }
// });
//
console.log('foo bar');
$('#editAllowedApps').select2({
// width: "resolve",
dropdownParent: $('#editUserModal'),
placeholder : "Select options, multiple allowed"
}); });
$('#allowedApps').select2({ const sessionModal = new bootstrap.Modal(document.getElementById('sessionModal'));
// width: "resolve",
dropdownParent: $('#addUserModal'), let currentPage = 1;
placeholder : "Select options, multiple allowed" let searchQuery = '';
let searchTimer;
fetchSessions(currentPage, searchQuery);
function fetchSessions(page, search = '') {
$.ajax({
type: "GET",
url: base_url + `/fetch-client-users?page=${page}&search=${encodeURIComponent(search)}`,
dataType: "json",
success: function(response) {
$('#clientUsersTableBody').html("");
if (response.sessions.data && response.sessions.data.length > 0) {
$.each(response.sessions.data, function(key, item) {
$('#clientUsersTableBody').append(`
<tr>
<td>${item.id}</td>
<td>${item.email}</td>
<td>${item.role}</td>
<td>
<button class="btn btn-success btn-sm editBtn" value="${item.id}">Edit</button>
<button class="btn btn-danger btn-sm deleteBtn" value="${item.id}">Delete</button>
</td>
</tr>
`);
}); });
$('#inputPermissions').select2({ let from = response.sessions.from;
// width: "resolve", let to = response.sessions.to;
dropdownParent: $('#editUserModal'), let total = response.sessions.total;
placeholder : "Select options, multiple allowed" $('#paginationCounter').html(`Showing ${from} to ${to} of ${total} results`);
} else {
$('#clientUsersTableBody').html("<tr><td colspan='4' class='text-center'>No users found</td></tr>");
$('#paginationCounter').html('Showing 0 to 0 of 0 results');
}
renderPagination(response);
}
});
}
$('#searchInput').on('keyup', function() {
clearTimeout(searchTimer);
searchQuery = $(this).val();
// Wait 500ms after the user stops typing to trigger the request
searchTimer = setTimeout(function() {
currentPage = 1; // Reset to page 1 for a brand new search
fetchSessions(currentPage, searchQuery);
}, 500);
}); });
$('.editUserBtn').click(function(evnt){ $(document).on('click', '.page-link', function(e) {
evnt.preventDefault(); e.preventDefault();
var selectedUserId = $(this).siblings('.userIdinput').val();
const formData = new FormData(); if ($(this).parent().hasClass('disabled')) {
formData.append('user_id', selectedUserId); return false;
}
let page = $(this).data('page');
if (page > 0) {
currentPage = page;
fetchSessions(currentPage, searchQuery);
}
});
function renderPagination(response) {
let linksHtml = '';
currentPage = response.sessions.current_page;
let lastPage = response.sessions.last_page;
let prevDisabled = currentPage === 1 ? 'disabled' : '';
let prevTabIndex = currentPage === 1 ? 'tabindex="-1"' : '';
linksHtml += `
<li class="page-item ${prevDisabled}">
<a class="page-link" href="#" data-page="${currentPage - 1}" ${prevTabIndex}>Previous</a>
</li>`;
for (let i = 1; i <= lastPage; i++) {
let activeClass = currentPage === i ? 'active' : '';
let activeAria = currentPage === i ? 'aria-current="page"' : '';
linksHtml += `
<li class="page-item ${activeClass}" ${activeAria}>
<a class="page-link" href="#" data-page="${i}">${i}</a>
</li>`;
}
let nextDisabled = currentPage === lastPage ? 'disabled' : '';
let nextTabIndex = currentPage === lastPage ? 'tabindex="-1"' : '';
linksHtml += `
<li class="page-item ${nextDisabled}">
<a class="page-link" href="#" data-page="${currentPage + 1}" ${nextTabIndex}>Next</a>
</li>`;
$('#paginationLinks').html(linksHtml);
}
$(document).on('click', '.page-link', function(e) {
e.preventDefault();
let page = $(this).data('page');
if (page > 0) {
fetchSessions(page);
}
});
$('#addNewBtn').click(function() {
$('#sessionForm')[0].reset();
$('#sessionId').val('');
$('#modalTitle').text('Add Session');
sessionModal.show();
});
$('#sessionForm').submit(function(e) {
e.preventDefault();
let id = $('#sessionId').val();
let data = {
email: $('#email').val(),
role: $('#role').val(),
password: $('#password').val()
};
let url = id ? base_url + `/client-users/${id}` : base_url + '/client-users';
let type = id ? "PUT" : "POST";
$.ajax({ $.ajax({
url: base_url + '/users/edit/' + selectedUserId, type: type,
type: 'GET', url: url,
processData: false, data: data,
contentType: false, dataType: "json",
beforeSend: function() { success: function(response) {
$('#editSuccessArea').text(""); sessionModal.hide();
$('#editErrorArea').text("Please wait ... loading user details!"); showAlert(response.message, 'success');
fetchSessions(currentPage);
}, },
success: function(data) { error: function(xhr) {
var jason = data.data; let errors = xhr.responseJSON.errors;
if(data.success == true){ if(errors.email) showAlert(errors.email, 'danger');
var allowedAppsArray = []; if(errors.role) showAlert(errors.role, 'danger');
if (jason['allowed_apps']) {
allowedAppsArray = jason['allowed_apps'].split(",");
}
console.log(jason['full_name']);
$('#editFullName').val(jason['full_name']);
$('#editEmail').val(jason['email']);
$('#editUsername').val(jason['username']);
$('#editGender').val(jason['gender']);
$('#editTitle').val(jason['title']);
$('#editUaPostion').val(jason['ua_position']);
$('#editPhone').val(jason['phone']);
$('#editAllowedApps').val(allowedAppsArray).trigger('change');
$('#editRegionID').val(jason['region_id']);
$('#editDistrictId').val(jason['district_id']);
$('#editUaPostion').val(jason['ua_position']);
$('#editGender').val(jason['gender']);
$("input[name='user_id']").val(jason.ua_id);
}
//$('#editUserModal').modal('show');
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#errorArea').text(error);
$('#errorArea').text(error);
} }
}); });
}); });
$('.viewUserBtn').click(function(evnt){ $(document).on('click', '.editBtn', function() {
evnt.preventDefault(); let id = $(this).val();
var selectedUserId = $(this).siblings('.userIdinput').val(); $.get(base_url + `/client-users/${id}/edit`, function(response) {
$('#sessionId').val(response.session.id);
const formData = new FormData(); $('#email').val(response.session.email);
formData.append('user_id', selectedUserId); $('#role').val(response.session.role);
$('#modalTitle').text('Edit Session');
sessionModal.show();
});
});
$(document).on('click', '.deleteBtn', function() {
let id = $(this).val();
if(confirm('Are you sure you want to delete this session?')) {
$.ajax({ $.ajax({
url: base_url + '/users/' + selectedUserId, type: "DELETE",
type: 'GET', url: base_url + `/client-users/${id}`,
processData: false, success: function(response) {
contentType: false, showAlert(response.message, 'success');
beforeSend: function() { fetchSessions(currentPage); // Stay on current page
$('#viewSuccessArea').text("");
$('#viewErrorArea').text("Please wait ... loading user details!");
},
success: function(data) {
var jason = data.data;
if(data.success == true){
var allowedAppsArray = [];
if (jason['allowed_apps']) {
allowedAppsArray = jason['allowed_apps'].split(",");
} }
console.log(jason['full_name']); });
$('#viewFullName').val(jason['full_name']);
$('#viewEmail').val(jason['email']);
$('#viewUsername').val(jason['username']);
$('#viewGender').val(jason['gender']);
$('#viewTitle').val(jason['title']);
$('#viewUaPostion').val(jason['ua_position']);
$('#viewPhone').val(jason['phone']);
$('#viewAllowedApps').val(allowedAppsArray).trigger('change');
$('#viewRegionID').val(jason['region_id']);
$('#viewDistrictId').val(jason['district_id']);
$('#viewUaPostion').val(jason['ua_position']);
$('#viewGender').val(jason['gender']);
$("input[name='user_id']").val(jason.ua_id);
}
//$('#editUserModal').modal('show');
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#errorArea').text(error);
$('#errorArea').text(error);
} }
}); });
}); function showAlert(message, type) {
$('#alertArea').html(`
$("#newUserForm").submit(function(evt){ <div class="alert alert-${type} alert-dismissible fade show" role="alert">
evt.preventDefault(); ${message}
$('#successArea').addClass('d-none'); <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
$('#errorsArea').removeClass('d-none'); </div>
var formData = new FormData($(this)[0]); `);
$.ajax({
url: base_url + '/users',
type: 'POST',
data: formData,
processData: false,
contentType: false,
beforeSend: function() {
$('#successArea').text("");
$('#successArea').text("Please wait ... user creation in progress!");
},
success: function(data) {
if (data['success'] == true) {
$('#successArea').removeClass('d-none');
$('#errorsArea').addClass('d-none');
$('#successArea').text("");
$('#successArea').text("User successfully created!");
// location.reload();
setTimeout(function() {
location.reload(); // Reloads the current page
}, 15000);
} }
else{
$('#successArea').addClass('d-none');
$('#errorArea').removeClass('d-none');
$('#errorArea').text("");
$('#errorArea').text("User could not be created!");
}
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#successArea').text(error);
$('#successArea').text(error);
}
});
});
$("#editUserForm").submit(function(evt){
evt.preventDefault();
$('#successArea').addClass('d-none');
$('#errorsArea').removeClass('d-none');
var formData = new FormData($(this)[0]);
$.ajax({
url: base_url + '/users/update/',
type: 'POST',
data: formData,
processData: false,
contentType: false,
beforeSend: function() {
// $('#updateBtn').addClass('d-none');
// $('#uodateProgressBtn').removeClass('d-none');
// $('#updateResultsDiv').removeClass('d-none');
// $('#updateResultsParagraph').text("Processing Please wait ...");
},
success: function(data) {
console.log(data);
$('#editSuccessArea').removeClass('d-none');
$('#editErrorArea').addClass('d-none');
$('#editSuccessArea').text("");
$('#editSuccessArea').text("User successfully details updated!");
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#editSuccessArea').text(error);
$('#editErrorArea').text(error);
location.reload();
}
});
});
$('#regionID').change(function(){
var options = $('#districtID');
var region_id = $('#regionID').val();
$.get( base_url + '/admin/districts/' + region_id, function (data) {
$('#districtID').empty();
$.each(data['districts'], function(id, row) {
$('#districtID').append($("<option />").val(row.districtid).text(row.district_name));
});
});
});
}); });

View File

@@ -1,4 +1,5 @@
<!doctype html> <!doctype html>
<!-- -->
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@@ -80,7 +81,13 @@
@csrf @csrf
<div class="mb-3"> <div class="mb-3">
<label for="companyEmail" class="form-label fw-semibold">Email</label> <label for="companyEmail" class="form-label fw-semibold">Email</label>
<input type="email" name="email" class="form-control" id="companyEmail" placeholder="client@click-mobile.com" value="" autocomplete=""> <div class="input-group">
<span class="input-group-text bg-white border-end-0 rounded-start-4">
<i class="bi bi-envelope-at"></i>
</span>
<input type="email" name="email" class="form-control" id="companyEmail" placeholder="" value="" autocomplete="">
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">

View File

@@ -0,0 +1,158 @@
@extends('layouts.master')
@section('page-title')
{{ $page_title }}
@endsection
@section('page-css')
<!-- <link href="https://unpkg.com/tabulator-tables@6.4.0/dist/css/tabulator.min.css" rel="stylesheet"> -->
<link href="{{ url('public/libs/tabulator-master/dist/css/tabulator_bootstrap5.css') }}" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.1.4/dist/css/datepicker.min.css">
@endsection
@section('content')
<div id="loadingOverlay"
style="display:none; position:fixed; top:0; left:0; width:100%; height:100%;
background:rgba(255,255,255,0.8); z-index:9999;
align-items:center; justify-content:center;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<section class="traffic-hero mb-4">
<div class="row g-4 align-items-end">
<div class="col-lg-8">
<div class="text-uppercase small fw-semibold opacity-75 mb-2">SMS Traffic Monitoring</div>
<!-- <h1 class="display-6 fw-semibold mb-3">Track every message your team sends.</h1>
<p class="fs-5 opacity-75 mb-0">
Search sent SMS, review delivery outcomes, inspect failed attempts, and give clients a fast audit trail for every campaign.
</p> -->
</div>
<div class="col-lg-4">
<div class="row g-3">
<!-- <div class="col-6">
<div class="rounded-4 p-3 bg-white bg-opacity-10">
<div class="small opacity-75">Campaigns</div>
<div class="h3 mb-0">18</div>
</div>
</div> -->
<div class="col-12">
@if(in_array(session('current_user.role'), ['administrator', 'finance']))
<div class="rounded-4 p-3 bg-white bg-opacity-10">
<div class="small opacity-75">SMS Account Balance</div>
<div class="h3 mb-0" id="mainSmsBalance">{{ number_format($balance_arr['balance']) }}</div>
</div>
@endif
</div>
</div>
</div>
</div>
</section>
<section class="row g-4 mb-4">
<div class="col-md-6 col-xl-6">
<article class="traffic-card">
<div class="d-flex align-items-start justify-content-between mb-3">
<!-- <div class="icon-wrap"><i class="bi bi-send-check"></i></div> -->
<!-- <span class="small text-success fw-semibold">+12.6%</span> -->
</div>
<div class="muted-label mb-2">
<!-- Messages sent from -->
<span id="reportRange">{{ $sms_units_arr['reportDate'] }}</span> </div>
<div class="row">
<div class="col-md-6">
<input id="startDate" name="start_date" type="text" class="form-control" placeholder="Select Start Date">
</div>
<div class="col-md-6">
<input id="endDate" name="end_date" type="text" class="form-control" placeholder="Select End Date">
</div>
<!-- <div id="loadingSpinner" class="spinner-border text-primary" role="status" style="display:none;">
<span class="visually-hidden">Loading...</span>
</div> -->
</div>
<!-- <div class="h3 mb-2 pt-1" id="smsUnitsValue">SMS Units : {{ $sms_units_arr['smsUnits'] }}</div> -->
<!-- <div class="mini-chart"><span style="width: 52%;"></span></div> -->
</article>
</div>
<div class="col-md-6 col-xl-6">
<article class="traffic-card">
<div class="d-flex align-items-start justify-content-between mb-3">
<div class="icon-wrap"><i class="bi bi-check2-circle"></i></div>
<!-- <span class="small text-success fw-semibold">98.4%</span> -->
</div>
<div class="muted-label mb-2">Sent Messages</div>
<!-- <div class="h3 mb-2">183,372</div> -->
<div class="h3 mb-2 pt-1" id="smsUnitsValue">SMS Units : {{ $sms_units_arr['smsUnits'] }} | Charge : {{ number_format($sms_units_arr['clientChargeTotal'], 2) }}</div>
<div class="mini-chart"><span style="width: 57%;"></span></div>
</article>
</div>
</section>
<section class="row g-4">
<div class="col-xl-12">
<div class="filter-card mb-4">
<div class="d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3 mb-3">
<div>
<h2 class="h4 mb-1">Sent SMS list</h2>
<!-- <p class="text-secondary mb-0">Filter messages by date, sender, recipient, or delivery state.</p> -->
</div>
<!-- <div class="d-flex flex-wrap gap-2">
<button class="btn btn-ghost px-4"><i class="bi bi-download me-2"></i>Export CSV</button>
<button class="btn btn-click px-4"><i class="bi bi-funnel me-2"></i>Apply filters</button>
</div> -->
</div>
<div class="row g-3">
<div class="col-md-12">
<div class="float-end">
<!-- <label for="search" class="form-label fw-semibold">Search</label> -->
<!-- <input id="search" type="text" class="form-control" placeholder="Phone, sender ID, text"> -->
</div>
</div>
</div>
</div>
<div class="traffic-table-card">
<h2 class="h4 mb-1">Sent SMS list</h2>
<div class="p-1">
<button id="download-pdf" class="btn btn-danger"><i class="bi bi-file-pdf-fill me-2"></i> Export PDF</button>
<button id="download-xlsx" class="btn btn-success"><i class="bi bi-file-excel-fill me-2"></i> Export Excel</button>
</div>
<div id="message-table"></div>
</div>
</div>
<!-- <div class="col-xl-2"> -->
<!-- <aside class="detail-card"> -->
<!-- <h2 class="h5 mb-3">Recent activity</h2> -->
<!-- <div class="timeline-item pt-0 mt-0 border-0">
<div class="fw-semibold">Sender ID approved</div>
<div class="muted-label">CLICKINFO added </div>
<div class="small text-secondary mt-1">09:04</div>
</div> -->
<!-- <div class="timeline-item">
<div class="fw-semibold">Campaign completed</div>
<div class="muted-label">[campaign name] batch finished</div>
<div class="small text-secondary mt-1">08:41</div>
</div> -->
<!-- </aside> -->
<!-- </div> -->
</section>
@endsection
@section('page-js')
<!-- <script src="https://unpkg.com/tabulator-tables@6.4.0/dist/js/tabulator.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.1.4/dist/js/datepicker.min.js"></script>
<script src="{{ url('public/libs/tabulator-master/dist/js/tabulator.min.js') }}"></script>
<script src="https://unpkg.com/jspdf@latest/dist/jspdf.umd.min.js"></script>
<script type="text/javascript" src="https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/luxon@2.3.1/build/global/luxon.min.js"></script>
<script src="{{ url('public/libs/tabulator-master/dist/js/autotable.min.js') }}"></script>
<script src="{{ url('public/assets/js/traffic-mgt.js') }}"></script>
@endsection

View File

@@ -1,238 +0,0 @@
@extends('layouts.master')
@section('page-title')
{{ $page_title }}
@endsection
@section('page-css')
<!-- <link href="https://unpkg.com/tabulator-tables@6.4.0/dist/css/tabulator.min.css" rel="stylesheet"> -->
<link href="{{ url('public/libs/tabulator-master/dist/css/tabulator_bootstrap5.css') }}" rel="stylesheet">
@endsection
@section('content')
<section class="traffic-hero mb-4">
<div class="row g-4 align-items-end">
<div class="col-lg-8">
<div class="text-uppercase small fw-semibold opacity-75 mb-2">SMS Traffic Monitoring</div>
<!-- <h1 class="display-6 fw-semibold mb-3">Track every message your team sends.</h1>
<p class="fs-5 opacity-75 mb-0">
Search sent SMS, review delivery outcomes, inspect failed attempts, and give clients a fast audit trail for every campaign.
</p> -->
</div>
<div class="col-lg-4"></div>
</div>
</section>
<section class="row g-4">
<div class="col-xl-10">
<div class="filter-card mb-4">
<div class="d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3 mb-3">
<div>
<h2 class="h4 mb-1">Sent SMS list</h2>
<!-- <p class="text-secondary mb-0">Filter messages by date, sender, recipient, or delivery state.</p> -->
</div>
<!-- <div class="d-flex flex-wrap gap-2">
<button class="btn btn-ghost px-4"><i class="bi bi-download me-2"></i>Export CSV</button>
<button class="btn btn-click px-4"><i class="bi bi-funnel me-2"></i>Apply filters</button>
</div> -->
</div>
<div class="row g-3">
<div class="col-md-12">
<div class="float-end">
<!-- <label for="search" class="form-label fw-semibold">Search</label> -->
<!-- <input id="search" type="text" class="form-control" placeholder="Phone, sender ID, text"> -->
</div>
</div>
</div>
</div>
<div class="traffic-table-card">
<div class="p-1">
<button id="download-pdf" class="btn btn-danger"><i class="bi bi-file-pdf-fill me-2"></i> Export PDF</button>
<button id="download-xlsx" class="btn btn-success"><i class="bi bi-file-excel-fill me-2"></i> Export Excel</button>
</div>
<div id="message-table"></div>
</div>
</div>
<div class="col-xl-2">
<aside class="detail-card">
<h2 class="h5 mb-3">Recent activity</h2>
<!-- <div class="timeline-item pt-0 mt-0 border-0">
<div class="fw-semibold">Sender ID approved</div>
<div class="muted-label">CLICKINFO added </div>
<div class="small text-secondary mt-1">09:04</div>
</div> -->
<!-- <div class="timeline-item">
<div class="fw-semibold">Campaign completed</div>
<div class="muted-label">[campaign name] batch finished</div>
<div class="small text-secondary mt-1">08:41</div>
</div> -->
</aside>
</div>
</section>
@endsection
@section('page-js')
<!-- <script src="https://unpkg.com/tabulator-tables@6.4.0/dist/js/tabulator.min.js"></script> -->
<script src="{{ url('public/libs/tabulator-master/dist/js/tabulator.min.js') }}"></script>
<script src="https://unpkg.com/jspdf@latest/dist/jspdf.umd.min.js"></script>
<script type="text/javascript" src="https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/luxon@2.3.1/build/global/luxon.min.js"></script>
<script src="{{ url('public/libs/tabulator-master/dist/js/autotable.min.js') }}"></script>
<script>
// var table = new Tabulator("#message-table", {
// ajaxURL: base_url + "/client-traffic-tabulator/",
// ajaxResponse: function(url, params, response) {
// return response.content;
// },
// columns: [
// {title: "Sender ID", field: "from"},
// {title: "Msisdn", field: "to"},
// {title: "Message", field: "message"},
// {title: "Status", field: "status"},
// {title: "Delivery Status", field: "deliveryStatus"},
// // {title: "Created At", field: "createdAt"},
// {title:"Date Created ", field:"createdAt", formatter:"datetime", formatterParams:{
// inputFormat: "iso",
// outputFormat: "dd-MM-yyyy HH:mm:ss",
// invalidPlaceholder: "(invalid date)"
// }}
// ],
// });
// document.getElementById("download-pdf").addEventListener("click", function(){
// table.download("pdf", "messages.pdf", {
// orientation:"portrait", // portrait or landscape
// title:"Messages Export", // document title
// });
// });
// document.getElementById("download-xlsx").addEventListener("click", function(){
// table.download("xlsx", "messages.xlsx", {sheetName:"Messages"});
// });
</script>
<script>
function statusDesign (cell, formatterParams){
var value = cell.getValue();
// if(value === 'Approved'){
// console.log(value !== null);
if (value !== null) {
if(value.includes('SENT')){
return "<span style='color:#3FB449; font-weight:bold;'>" + value + "</span>";
}
// else if(value.includes('Active')){
// return "<span style='color:#3FB449; font-weight:bold;'>" + value + "</span>";
// }
else{
return "<span style='color:#E4A11B;'>" + value + "</span>";
}
}
}
var table = new Tabulator("#message-table", {
ajaxURL: base_url + "/client-traffic-tabulator/",
ajaxParams: {size: 20},
pagination: "remote",
paginationSize: 20,
paginationDataSent: {
"page": "page",
"size": "size"
},
paginationDataReceived: {
"last_page": "totalPages",
"data": "content",
"current_page": "number",
"total": "totalElements"
},
ajaxResponse: function(url, params, response) {
return response.content;
},
columns: [
{title: "Sender", field: "from", width:150, headerFilter:"input"},
{title: "Msisdn", field: "to", width:150, headerFilter:"input"},
// {title: "Message", field: "message"},
{title:"Message", field:"message", width:350, formatter:"textarea", headerFilter:"input"},
// {title: "Status", field: "status"},
{title: "Delivery Status", field: "deliveryStatus", width:100, headerFilter:"input", formatter: statusDesign},
// {title: "Created At", field: "createdAt"},
{title:"Date Created ", field:"createdAt", width:200, formatter:"datetime", headerFilter:"input", formatterParams:{
inputFormat: "iso",
outputFormat: "dd-MM-yyyy HH:mm:ss",
invalidPlaceholder: "(invalid date)"
}}
],
});
document.getElementById("download-pdf").addEventListener("click", function(){
table.download("pdf", "messages.pdf", {
orientation:"portrait", // portrait or landscape
title:"Messages Export", // document title
});
});
document.getElementById("download-xlsx").addEventListener("click", function(){
table.download("xlsx", "messages.xlsx", {sheetName:"Messages"});
});
</script>
<script>
// var table = new Tabulator("#message-table", {
// ajaxURL: base_url + "/client-traffic-tabulator/",
// pagination: "remote",
// paginationSize: 20,
// paginationDataSent: {
// "page": "page",
// "size": "size"
// },
// paginationDataReceived: {
// "last_page": "totalPages",
// "data": "content",
// "current_page": "number",
// "total": "totalElements"
// },
// // Sorting configuration
// ajaxSorting: true,
// sortMode: "remote",
// ajaxRequestFunc: function(url, config, params) {
// let query = {
// page: params.page - 1,
// size: params.size,
// };
// if (params.sorters && params.sorters.length > 0) {
// let sorter = params.sorters[0];
// query.sort = sorter.field + "," + sorter.dir;
// // Laravel/Spring style: ?sort=createdAt,asc
// }
// // Build query string
// let qs = Object.keys(query)
// .map(k => encodeURIComponent(k) + "=" + encodeURIComponent(query[k]))
// .join("&");
// return url + "?" + qs;
// },
// ajaxResponse: function(url, params, response) {
// return response.content;
// },
// columns: [
// {title: "ID", field: "id", sorter: "number"},
// {title: "Client ID", field: "clientId", sorter: "number"},
// {title: "From", field: "from", sorter: "string"},
// {title: "To", field: "to", sorter: "string"},
// {title: "Message", field: "message", sorter: "string"},
// {title: "Status", field: "status", sorter: "string"},
// {title: "Delivery Status", field: "deliveryStatus", sorter: "string"},
// {title: "Created At", field: "createdAt", sorter: "datetime"},
// ],
// });
</script>
@endsection

View File

@@ -0,0 +1,327 @@
@extends('layouts.master')
@section('page-title')
{{ $page_title }}
@endsection
@section('page-css')
@endsection
@section('content')
<section class="row g-4">
<div class="col-xl-10">
<div class="filter-card mb-4">
<div class="d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3 mb-3">
<div>
<h2 class="h4 mb-1">User Management</h2>
</div>
</div>
<div class="row g-3">
<div class="col-md-6 col-lg-3">
<!-- <label for="search" class="form-label fw-semibold">Search</label> -->
<input id="searchInput" type="text" class="form-control" placeholder="search email, role">
</div>
</div>
</div>
<div class="traffic-table-card">
<div class="table-responsive">
<div class="float-end">
<button class="btn btn-ghost px-4" id="addNewBtn"><i class="bi bi-person me-2 text-danger"></i>Add New User</button>
</div>
<table class="table align-middle mb-0">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Role</th>
<th scope="col">Date Created</th>
</tr>
</thead>
<tbody id="clientUsersTableBody"></tbody>
</table>
<!-- <div class="d-flex justify-content-center mt-4">
<nav aria-label="Page navigation">
<ul class="pagination" id="paginationLinks"></ul>
</nav>
</div> -->
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted" id="paginationCounter"></div>
<nav aria-label="Page navigation">
<ul class="pagination mb-0" id="paginationLinks"></ul>
</nav>
</div>
</div>
</div>
</div>
<div class="col-xl-3">
<!-- <aside class="detail-card">
<h2 class="h5 mb-3">Recent activity</h2>
<div class="timeline-item pt-0 mt-0 border-0">
<div class="fw-semibold">Sender ID approved</div>
<div class="muted-label">CLICKINFO added </div>
<div class="small text-secondary mt-1">09:04</div>
</div>
<div class="timeline-item">
<div class="fw-semibold">Retry queue triggered</div>
<div class="muted-label">42 Zambia messages re-routed after timeout</div>
<div class="small text-secondary mt-1">08:57</div>
</div>
<div class="timeline-item">
<div class="fw-semibold">Campaign completed</div>
<div class="muted-label">[campaign name] batch finished</div>
<div class="small text-secondary mt-1">08:41</div>
</div>
</aside> -->
</div>
</section>
<div class="modal fade" id="sessionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Add Session</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="sessionForm">
<div class="modal-body">
<input type="hidden" id="sessionId">
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" id="email" required>
</div>
<div class="mb-3">
<label for="role" class="form-label">Role</label>
<select class="form-control" name="role" id="role">
<option value="">--Select--</option>
<option value="administrator">Administrator</option>
<option value="customercare">Customer Care</option>
<option value="finance">Finance</option>
</select>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" name="password" class="form-control" id="password" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" id="saveBtn">Save changes</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('page-js')
<script src="{{ url('public/assets/js/usermgt.js') }}"></script>
<script>
// $(document).ready(function() {
// $.ajaxSetup({
// headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') }
// });
// const sessionModal = new bootstrap.Modal(document.getElementById('sessionModal'));
// let currentPage = 1;
// let searchQuery = '';
// let searchTimer; // For debouncing
// fetchSessions(currentPage, searchQuery);
// // 1. UPDATED FETCH SESSIONS METHOD
// function fetchSessions(page, search = '') {
// $.ajax({
// type: "GET",
// // Pass both page and search queries to the URL
// url: base_url + `/fetch-client-users?page=${page}&search=${encodeURIComponent(search)}`,
// dataType: "json",
// success: function(response) {
// $('#clientUsersTableBody').html("");
// if (response.sessions.data && response.sessions.data.length > 0) {
// $.each(response.sessions.data, function(key, item) {
// $('#clientUsersTableBody').append(`
// <tr>
// <td>${item.id}</td>
// <td>${item.email}</td>
// <td>${item.role}</td>
// <td>
// <button class="btn btn-success btn-sm editBtn" value="${item.id}">Edit</button>
// <button class="btn btn-danger btn-sm deleteBtn" value="${item.id}">Delete</button>
// </td>
// </tr>
// `);
// });
// let from = response.sessions.from;
// let to = response.sessions.to;
// let total = response.sessions.total;
// $('#paginationCounter').html(`Showing ${from} to ${to} of ${total} results`);
// } else {
// $('#clientUsersTableBody').html("<tr><td colspan='4' class='text-center'>No users found</td></tr>");
// $('#paginationCounter').html('Showing 0 to 0 of 0 results');
// }
// renderPagination(response);
// }
// });
// }
// // 2. LIVE SEARCH EVENT (WITH DEBOUNCE)
// $('#searchInput').on('keyup', function() {
// clearTimeout(searchTimer);
// searchQuery = $(this).val();
// // Wait 500ms after the user stops typing to trigger the request
// searchTimer = setTimeout(function() {
// currentPage = 1; // Reset to page 1 for a brand new search
// fetchSessions(currentPage, searchQuery);
// }, 500);
// });
// // 3. UPDATED PAGINATION CLICK LISTENER
// $(document).on('click', '.page-link', function(e) {
// e.preventDefault();
// if ($(this).parent().hasClass('disabled')) {
// return false;
// }
// let page = $(this).data('page');
// if (page > 0) {
// currentPage = page;
// // Pass both the target page and the current typed search string
// fetchSessions(currentPage, searchQuery);
// }
// });
// function renderPagination(response) {
// let linksHtml = '';
// currentPage = response.sessions.current_page;
// let lastPage = response.sessions.last_page;
// let prevDisabled = currentPage === 1 ? 'disabled' : '';
// let prevTabIndex = currentPage === 1 ? 'tabindex="-1"' : '';
// linksHtml += `
// <li class="page-item ${prevDisabled}">
// <a class="page-link" href="#" data-page="${currentPage - 1}" ${prevTabIndex}>Previous</a>
// </li>`;
// for (let i = 1; i <= lastPage; i++) {
// let activeClass = currentPage === i ? 'active' : '';
// let activeAria = currentPage === i ? 'aria-current="page"' : '';
// linksHtml += `
// <li class="page-item ${activeClass}" ${activeAria}>
// <a class="page-link" href="#" data-page="${i}">${i}</a>
// </li>`;
// }
// let nextDisabled = currentPage === lastPage ? 'disabled' : '';
// let nextTabIndex = currentPage === lastPage ? 'tabindex="-1"' : '';
// linksHtml += `
// <li class="page-item ${nextDisabled}">
// <a class="page-link" href="#" data-page="${currentPage + 1}" ${nextTabIndex}>Next</a>
// </li>`;
// $('#paginationLinks').html(linksHtml);
// }
// $(document).on('click', '.page-link', function(e) {
// e.preventDefault();
// let page = $(this).data('page');
// // Prevent clicking disabled boundaries
// if (page > 0) {
// fetchSessions(page);
// }
// });
// // Reset forms and trigger reloads to remain on active pages
// $('#addNewBtn').click(function() {
// $('#sessionForm')[0].reset();
// $('#sessionId').val('');
// $('#modalTitle').text('Add Session');
// sessionModal.show();
// });
// $('#sessionForm').submit(function(e) {
// e.preventDefault();
// let id = $('#sessionId').val();
// let data = {
// email: $('#email').val(),
// role: $('#role').val()
// };
// let url = id ? base_url + `/client-users/${id}` : base_url + '/client-users';
// let type = id ? "PUT" : "POST";
// $.ajax({
// type: type,
// url: url,
// data: data,
// dataType: "json",
// success: function(response) {
// sessionModal.hide();
// showAlert(response.message, 'success');
// fetchSessions(currentPage); // Stay on current page
// },
// error: function(xhr) {
// let errors = xhr.responseJSON.errors;
// if(errors.email) showAlert(errors.email, 'danger');
// if(errors.role) showAlert(errors.role, 'danger');
// }
// });
// });
// $(document).on('click', '.editBtn', function() {
// let id = $(this).val();
// $.get(base_url + `/client-users/${id}/edit`, function(response) {
// $('#sessionId').val(response.session.id);
// $('#email').val(response.session.email);
// $('#role').val(response.session.role);
// $('#modalTitle').text('Edit Session');
// sessionModal.show();
// });
// });
// $(document).on('click', '.deleteBtn', function() {
// let id = $(this).val();
// if(confirm('Are you sure you want to delete this session?')) {
// $.ajax({
// type: "DELETE",
// url: base_url + `/client-users/${id}`,
// success: function(response) {
// showAlert(response.message, 'success');
// fetchSessions(currentPage); // Stay on current page
// }
// });
// }
// });
// function showAlert(message, type) {
// $('#alertArea').html(`
// <div class="alert alert-${type} alert-dismissible fade show" role="alert">
// ${message}
// <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
// </div>
// `);
// }
// });
</script>
@endsection

View File

@@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- <title>Click Client Portal | SMS Traffic</title> --> <!-- <title>Click Client Portal | SMS Traffic</title> -->
<title>@yield('page-title') | Client Portal </title> <title>@yield('page-title') | Client Portal </title>
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="icon" type="image/png" href="{{ url('public/assets/images/click-logo.png') }}"> <link rel="icon" type="image/png" href="{{ url('public/assets/images/click-logo.png') }}">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -16,7 +18,12 @@
@yield('page-css') @yield('page-css')
<script type="text/javascript"> <script type="text/javascript">
var base_url = "{!! url('/') !!}"; @if(env('APP_ENV') == 'production')
var base_url = "https://smsportal.clickmlapps.com";
@else
var base_url = "{{ url('/') }}";
@endif
//"{!! url('/') !!}";
</script> </script>
</head> </head>
<body> <body>
@@ -31,19 +38,22 @@
<div class="fw-semibold">{{ session('current_user.name') }}</div> <div class="fw-semibold">{{ session('current_user.name') }}</div>
</div> </div>
</div> </div>
<div class="d-flex flex-wrap align-items-center gap-2"> <div class="d-flex flex-wrap align-items-center gap-2">
<span class="badge rounded-pill text-bg-success px-3 py-2">{{ session('current_user.role') }}</span>
<!-- <span class="badge rounded-pill text-bg-light px-3 py-2">{{ session('current_user.name') }}</span> --> <!-- <span class="badge rounded-pill text-bg-light px-3 py-2">{{ session('current_user.name') }}</span> -->
<!-- <span class="badge rounded-pill text-bg-light px-3 py-2"><i class="bi bi-clock-history me-1"></i> Last sync 09:12</span> --> <span class="badge rounded-pill text-bg-light px-3 py-2"><i class="bi bi-clock-history me-1"></i> </span>
@if($page_title == 'SMS Traffic') @if(session('current_user.role') == 'administrator')
<a href="{{ url('send-sms') }}" class="btn btn-click px-4"><i class="bi bi-plus-circle me-2"></i>New SMS</a> <a href="{{ url('client-users') }}" class="btn btn-click px-4"><i class="bi bi-people me-2"></i>Users</a>
@else @endif
@if(in_array(session('current_user.role'), ['administrator', 'finance']))
<a href="{{ url('client-traffic') }}" class="btn btn-click px-4"><i class="bi bi-chat-right-text me-2"></i>Messages</a> <a href="{{ url('client-traffic') }}" class="btn btn-click px-4"><i class="bi bi-chat-right-text me-2"></i>Messages</a>
@endif @endif
@if($page_title == 'Applications') @if($page_title == 'Applications')
<a href="{{ url('new-app') }}" class="btn btn-clicksuccess px-4" data-bs-toggle="modal" data-bs-target="#newApplicationModal"><i class="bi bi-plus-square me-2"></i>New App</a> <!-- <a href="{{ url('new-app') }}" class="btn btn-clicksuccess px-4" data-bs-toggle="modal" data-bs-target="#newApplicationModal"><i class="bi bi-plus-square me-2"></i>New App</a> -->
@else @else
<a href="{{ url('client-apps') }}" class="btn btn-clicksuccess px-4"><i class="bi bi-list-ul me-2"></i>App List</a> <!-- <a href="{{ url('client-apps') }}" class="btn btn-clicksuccess px-4"><i class="bi bi-list-ul me-2"></i>App List</a> -->
@endif @endif
<a href="{{ url('client-logout') }} " class="btn btn-ghost px-4">Log out</a> <a href="{{ url('client-logout') }} " class="btn btn-ghost px-4">Log out</a>
</div> </div>

View File

@@ -49,7 +49,7 @@ Route::post('/client-activation', [App\Http\Controllers\ClientsLoginController::
#Route::resource('posts', AdminController::class); #Route::resource('posts', AdminController::class);
}); });
Route::middleware(['checksession'])->group(function () { Route::middleware(['checksession', 'checkrole:administrator'])->group(function () {
Route::get('/', [App\Http\Controllers\ClientsTrafficController::class, 'index']); Route::get('/', [App\Http\Controllers\ClientsTrafficController::class, 'index']);
Route::get('client-traffic', [App\Http\Controllers\ClientsTrafficController::class, 'index']); Route::get('client-traffic', [App\Http\Controllers\ClientsTrafficController::class, 'index']);
Route::get('client-traffic-tabulator', [App\Http\Controllers\ClientsTrafficController::class, 'indexTabulator']); Route::get('client-traffic-tabulator', [App\Http\Controllers\ClientsTrafficController::class, 'indexTabulator']);
@@ -60,6 +60,16 @@ Route::post('/client-activation', [App\Http\Controllers\ClientsLoginController::
Route::post('send-sms', [App\Http\Controllers\ClientsTrafficController::class, 'store'])->name('client.sendsms'); Route::post('send-sms', [App\Http\Controllers\ClientsTrafficController::class, 'store'])->name('client.sendsms');
Route::post('client-newapp', [App\Http\Controllers\ClientsTrafficController::class, 'newAppStore'])->name('client.sendsms'); Route::post('client-newapp', [App\Http\Controllers\ClientsTrafficController::class, 'newAppStore'])->name('client.sendsms');
Route::post('client-dailysmsunits', [App\Http\Controllers\ClientsTrafficController::class, 'dailySmsUnits'])->name('client.dailysmsunits');
Route::get('/client-users', [App\Http\Controllers\ClientUsersController::class, 'index']);
Route::get('/fetch-client-users', [App\Http\Controllers\ClientUsersController::class, 'fetch']);
Route::post('/client-users', [App\Http\Controllers\ClientUsersController::class, 'store']);
Route::get('/client-users/{id}/edit', [App\Http\Controllers\ClientUsersController::class, 'edit']);
Route::put('/client-users/{id}', [App\Http\Controllers\ClientUsersController::class, 'update']);
Route::delete('/client-users/{id}', [App\Http\Controllers\ClientUsersController::class, 'destroy']);
// Route::post('send-sms', [App\Http\Controllers\ClientsTrafficController::class, 'store'])->name('client.sendsms'); // Route::post('send-sms', [App\Http\Controllers\ClientsTrafficController::class, 'store'])->name('client.sendsms');