bug fix in clients controller

This commit is contained in:
Kwesi Banson Jnr
2026-08-13 20:12:24 +00:00
parent e4a417ec92
commit a797d9587c
20 changed files with 989 additions and 852 deletions

View File

@@ -13,6 +13,9 @@ use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Barryvdh\DomPDF\Facade\Pdf; // Ensure you have dompdf installed if exporting PDFs
class ClientsController extends Controller class ClientsController extends Controller
{ {
@@ -199,14 +202,18 @@ class ClientsController extends Controller
private function buildFilteredQuery(Request $request) private function buildFilteredQuery(Request $request)
{ {
$query = Models\Client::query(); // $query = Models\Client::query();
$query = Models\Client::with('account_manager');
if ($request->filled('search')) { if ($request->filled('search')) {
$searchTerm = $request->search; $searchTerm = $request->search;
$query->where(function ($q) use ($searchTerm) { $query->where(function ($q) use ($searchTerm) {
$q->where('name', 'like', "%{$searchTerm}%") $q->where('name', 'like', "%{$searchTerm}%")
->orWhere('email', 'like', "%{$searchTerm}%") ->orWhere('email', 'like', "%{$searchTerm}%")
->orWhere('contact_person', 'like', "%{$searchTerm}%"); ->orWhere('contact_person', 'like', "%{$searchTerm}%")
->orWhereHas('account_manager', function($amQuery) use ($searchTerm) {
$amQuery->where('name', 'like', "%{$searchTerm}%");
});
}); });
} }
@@ -240,17 +247,85 @@ class ClientsController extends Controller
/** /**
* Handle the Export Request * Handle the Export Request
*/ */
public function export(Request $request) public function export(Request $request)
{ {
// Get the filtered data but execute get() instead of paginate() // Build query using the exact same filters as your data table
$clients = $this->buildFilteredQuery($request)->get(); // $query = Models\Client::query();
$format = $request->input('format', 'csv'); $query = Models\Client::with('account_manager');
if ($format === 'pdf') { if ($request->filled('search')) {
return $this->exportToPdf($clients); $search = $request->search;
$query->where(function($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%")
->orWhere('contact_person', 'like', "%{$search}%")
->orWhereHas('account_manager', function($amQuery) use ($search) {
$amQuery->where('name', 'like', "%{$search}%");
});
});
} }
return $this->exportToCsv($clients); if ($request->filled('service')) {
$query->whereJsonContains('services', $request->service);
}
if ($request->filled('billing')) {
$query->where('pay_mode', $request->billing);
}
if ($request->filled('status')) {
$query->where('status', $request->status);
}
$clients = $query->get();
$format = $request->get('format', 'csv');
// Handle CSV Export
if ($format === 'csv') {
$filename = 'clients_export_' . date('Y-m-d') . '.csv';
$headers = [
"Content-Type" => "text/csv",
"Content-Disposition" => "attachment; filename=\"$filename\"",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
];
$callback = function() use ($clients) {
$file = fopen('php://output', 'w');
// CSV Header Row
fputcsv($file, ['ID', 'Client Name', 'Primary Email', 'Phone', 'Contact Person', 'Account Manager', 'Pay Mode', 'Status', 'Country', 'Date Added']);
foreach ($clients as $client) {
fputcsv($file, [
$client->id,
$client->name,
$client->email,
$client->phone,
$client->contact_person,
$client->account_manager->name,
$client->pay_mode,
$client->status,
$client->country,
$client->created_at,
]);
}
fclose($file);
};
return response()->stream($callback, 200, $headers);
}
// Handle PDF Export (Optional: requires barryvdh/laravel-dompdf)
if ($format === 'pdf') {
$pdf = Pdf::loadView('clients.export-pdf', compact('clients'));
return $pdf->download('clients_export_' . date('Y-m-d') . '.pdf');
}
return redirect()->back();
} }
/** /**
@@ -370,7 +445,7 @@ class ClientsController extends Controller
'last_modified_by_id' => Auth::user()->id 'last_modified_by_id' => Auth::user()->id
]); ]);
} }
$user_id = $auth_user->id; $user_id = Auth::user()->id;
Models\UserActivity::create([ Models\UserActivity::create([
'type' => 'staff', 'type' => 'staff',

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models;
class HelperController extends Controller
{
public function getCountriesJson()
{
$countries = \DB::table('countries_new')
->orderBy('en_short_name', 'asc')
->select('en_short_name')
->get();
return response()->json($countries);
}
}

View File

@@ -1,424 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Carbon\Carbon;
use App\Models;
use App\Http\Requests;
use App\Libs\PaperLessNgx;
use App\Jobs\SendNewUssdClientEmail;
use App\Jobs\SendOnboardingCompletedEmailAlert;
use App\Jobs\SendShortCodeListToFinance;
use App\Jobs\SendNewNotesEmailAlert;
use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Facades\Config;
class ClientsController extends Controller
{
public function index(): View
{
return view('client.index-tabulator', [
'page_title' => 'Clients',
'current_user' => session('current_user')
]);
}
public function indexInactive(): View
{
return view('client.index-inactive', [
'page_title' => 'Inactive Clients',
'current_user' => session('current_user')
]);
}
public function indexStatus($status): View
{
return view('client.index-status', [
'status' => $status,
'page_title' => strtoupper($status) . ' Clients',
'current_user' => session('current_user')
]);
}
public function indexCancelled(): View
{
return view('client.index-prospective', [
'page_title' => 'Prospective Clients',
'current_user' => session('current_user')
]);
}
public function indexDiscussion(): View
{
return view('client.index-prospective', [
'page_title' => 'In Discussion Clients',
'current_user' => session('current_user')
]);
}
public function getClientJson(Request $request): JsonResponse
{
$query = DB::table('clients')
->join('auth_users AS aumngr', 'aumngr.id', '=', 'clients.auth_user_id')
->join('auth_users AS aumodify', 'aumodify.id', '=', 'clients.last_modified_by')
->join('flags AS flags', 'flags.country', '=', 'clients.country')
->select('clients.id', 'clients.name', 'clients.status', 'clients.progress_indicator_score', 'clients.country', 'aumngr.name As accountMgr', 'aumodify.name AS modifiedBy', 'flags.url AS theflag')
->whereNotIn('status', ['inactive', 'Cancelled']);
if ($request->filled('keyword')) {
$keyword = $request->keyword;
$query->where(function ($q) use ($keyword) {
$q->where('clients.name', 'LIKE', "%{$keyword}%")
->orWhere('clients.country', 'LIKE', "%{$keyword}%")
->orWhere('aumngr.name', 'LIKE', "%{$keyword}%")
->orWhere('aumodify.name', 'LIKE', "%{$keyword}%");
});
}
return response()->json($query->orderBy('name', 'ASC')->get());
}
public function getInactiveClientJson(Request $request): JsonResponse
{
$query = DB::table('clients')
->join('auth_users AS aumngr', 'aumngr.id', '=', 'clients.auth_user_id')
->join('auth_users AS aumodify', 'aumodify.id', '=', 'clients.last_modified_by')
->join('flags AS flags', 'flags.country', '=', 'clients.country')
->select('clients.id', 'clients.name', 'clients.status', 'clients.progress_indicator_score', 'clients.country', 'aumngr.name As accountMgr', 'aumodify.name AS modifiedBy', 'flags.url AS theflag')
->whereIn('status', ['inactive', 'Cancelled']);
if ($request->filled('keyword')) {
$keyword = $request->keyword;
$query->where(function ($q) use ($keyword) {
$q->where('clients.name', 'LIKE', "%{$keyword}%")
->orWhere('clients.status', 'LIKE', "%{$keyword}%")
->orWhere('clients.country', 'LIKE', "%{$keyword}%")
->orWhere('aumngr.name', 'LIKE', "%{$keyword}%")
->orWhere('aumodify.name', 'LIKE', "%{$keyword}%")
->orWhere('clients.progress_indicator_score', 'LIKE', "%{$keyword}%");
});
}
return response()->json($query->orderBy('name', 'ASC')->get());
}
public function create(): View
{
return view('client.create', [
'page_title' => 'Create Client',
'countries' => Models\Country::pluck('en_short_name', 'en_short_name'),
'service_type' => Models\Service::pluck('name', 'name'),
'status' => ['Live' => 'Live', 'Inactive' => 'Inactive', 'Prospective' => 'Prospective', 'Cancelled' => 'Cancelled'],
'currency' => Models\Currency::pluck('name', 'name'),
'auth_users' => Models\SystemUser::pluck('name', 'id'),
'payment_type' => ['Prepaid' => 'Prepaid', 'Postpaid' => 'Postpaid'],
'company_types' => ['Aggregator/Supplier' => 'Aggregator/Supplier', 'Enterprise' => 'Enterprise', 'Hybrid' => 'Hybrid'],
'industries' => Models\Industry::orderBy('name', 'ASC')->pluck('name', 'name')
]);
}
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => 'required|unique:clients,name',
'email' => 'required|email',
'services' => 'required|array',
'country' => 'required',
'status' => 'required',
'payment_mode' => 'required',
'currency' => 'required',
'company_type' => 'required',
'industry' => 'required',
'auth_user_id' => 'required',
]);
$onboarding_stages = Models\ClientOnboardingMainStage::orderBy('stage_id')->get();
$client_current_stages = [];
foreach ($onboarding_stages as $value) {
$client_current_stages[$value->stage] = "PENDING";
}
$client_arr = [
'name' => $request->name,
'email' => $request->email,
'country' => $request->country,
'status' => $request->status,
'pay_mode' => $request->payment_mode,
'currency' => $request->currency,
'auth_user_id' => $request->auth_user_id,
'created_by' => session('current_user.id'),
'last_modified_by' => session('current_user.id'),
'progress_indicator_score' => 10,
'progress_indicator' => $onboarding_stages[0]->stage,
'onboarding_progress_stage' => json_encode($client_current_stages),
'notes' => $request->notes,
'services' => json_encode($request->services),
'phone' => $request->phone,
'skype_name' => $request->skype_name,
'linkedin_name' => $request->linkedin_name,
'contact_person' => $request->contact_person,
'company_type' => $request->company_type,
'industry' => $request->industry,
];
$result = Models\Client::create(array_filter($client_arr)); // Remove nulls automatically
$get_stage_subs_items = Models\ClientOnboardingSubItem::get();
foreach ($get_stage_subs_items as $value) {
Models\ClientOnboardingProgress::create([
'stage_id' => $value->stage_id,
'client_id' => $result->id,
'name' => $value->name,
'status' => 'PENDING'
]);
}
if (in_array('USSD', $request->services)) {
Log::info('ussd client detected');
Models\UssdClientPayment::create([
'client_id' => $result->id,
'last_modified_by_id' => session('current_user.id')
]);
}
Models\UserActivity::create([
'type' => 'staff',
'content' => session('current_user.name') . " added a new client ({$result->name}) successfully!",
'user_id' => session('current_user.id'),
'ip_address' => request()->ip(),
'device' => $request->header('User-Agent')
]);
Session::flash('success_message', 'Client successfully added');
return redirect('clients');
}
public function show($id): View
{
$showclient = Models\Client::with('service_info', 'country_flag_info', 'auth_user_info', 'short_code_info')->findOrFail($id);
$networks_raw = [
'AirtelTigo GH', 'MTN GH', 'Airtel MW', 'Airtel Zambia', 'TNM MW',
'Safaricom Kenya', 'Airtel Kenya', 'Telkom Kenya', 'Orange Kenya'
];
sort($networks_raw);
$renewal_due = 'N/A';
$highlight_colour = 'none';
if (!empty($showclient->contract_validity)) {
$expiry_date = Carbon::parse($showclient->contract_validity);
$current_date = Carbon::today();
$days = $current_date->diffInDays($expiry_date, false); // False allows negative values for past dates
if ($days < 0) {
$highlight_colour = 'warning';
$days_abs = abs($days);
if ($days_abs > 365) $renewal_due = "Contract expired " . floor($days_abs / 365) . " year(s) ago";
elseif ($days_abs > 31) $renewal_due = "Contract expired " . floor($days_abs / 31) . " month(s) ago";
else $renewal_due = "Contract expired {$days_abs} days(s) ago";
} else {
if ($days > 365) $renewal_due = "In " . floor($days / 365) . " year(s)";
elseif ($days > 31) $renewal_due = "In " . floor($days / 31) . " months";
else $renewal_due = "In {$days} day(s)";
}
}
$change_account_mgr_permission = Config::get('permissions.CHANGE_ACCOUNT_MANAGERS');
$has_permission = $this->hasAnyAccess([$change_account_mgr_permission]) ? 'YES' : 'NO';
return view('client.show_accordion', [
'page_title' => 'Client Profile',
'showclient' => $showclient,
'show_services' => Models\ClientCategory::where('client_id', $id)->get(),
'service_type' => Models\Service::pluck('name', 'id'),
'service_type_names' => Models\Service::pluck('name', 'name'),
'show_notes' => Models\ClientNote::with('created_by_info', 'client_info')->where('client_id', $id)->latest()->take(20)->get(),
'show_notes_highlight' => Models\ClientNote::with('created_by_info', 'client_info')->where('client_id', $id)->where('highlight', 'YES')->latest()->take(1)->get(),
'status_bg' => $showclient->status == 'Live' ? 'info' : ($showclient->status == 'Prospective' ? 'warning' : 'danger'),
'progress_status_bg' => $showclient->progress_indicator_score >= 70 ? 'success' : ($showclient->progress_indicator_score >= 50 ? 'warning' : 'danger'),
'voice_codes' => Models\ClientShortCode::where('client_id', $id)->where('code_type', 'voice')->get(),
'sms_codes' => Models\ClientShortCode::where('client_id', $id)->where('code_type', 'sms')->get(),
'ussd_codes' => Models\ClientShortCode::where('client_id', $id)->where('code_type', 'ussd')->get(),
'countries' => Models\Country::pluck('en_short_name', 'en_short_name'),
'networks' => Models\NetworkOps::pluck('name', 'id'),
'progress_indicators' => Models\ClientIndicator::pluck('name', 'name'),
'networks_raw' => array_combine($networks_raw, $networks_raw),
'renewal_due' => $renewal_due,
'recent_payments' => Models\ClientPayment::where('client_id', $id)->latest()->get(),
'highlight_colour' => $highlight_colour,
'showdocuments' => Models\ClientFile::where('client_id', $id)->get(),
'support_fees' => Models\ClientSupportFees::where('client_id', $id)->latest()->get(),
'recurring_arr' => ['NO' => 'NO', 'Monthly' => 'Monthly', 'Quarterly' => 'Quarterly', 'Semiannual' => 'Semiannual', 'Yearly' => 'Yearly'],
'change_account_mgr_permisson' => $has_permission,
'am_list_arr' => Models\SystemUser::orderBy('name', 'ASC')->pluck('name', 'id'),
'country_network_arr' => DB::table('network_operators')->selectRaw('id, concat(name, " (", country, ")") AS network')->orderBy('network')->pluck('network', 'network'),
'mnos_arr' => ['' => '-- Select Country first --']
]);
}
public function update(Requests\UpdateClientRequest $request, $id): RedirectResponse
{
$client_update = Models\Client::findOrFail($id);
$paperless = new PaperLessNgx();
if ($client_update->progress_indicator != 'COMPLETED') {
$current_pending_stage_details = Models\ClientOnboardingMainStage::where('stage', $request->current_pending_stage)->first();
$get_stage_subs_items = Models\ClientOnboardingSubItem::where('stage_id', $current_pending_stage_details->stage_id)->get();
if ($request->onboarding_sub_items_progress) {
foreach ($request->onboarding_sub_items_progress as $value) {
Models\ClientOnboardingProgress::updateOrCreate(
['stage_id' => $current_pending_stage_details->stage_id, 'client_id' => $id, 'name' => $value],
['status' => 'COMPLETED']
);
}
}
$get_stage_onboarding_status = Models\ClientOnboardingProgress::where('client_id', $id)
->where('stage_id', $current_pending_stage_details->stage_id)
->where('status', 'COMPLETED')
->get();
$onboarding_progress_stage = json_decode($client_update->onboarding_progress_stage, true);
if (count($get_stage_subs_items) == count($get_stage_onboarding_status)) {
$onboarding_progress_stage[$current_pending_stage_details->stage] = 'COMPLETED';
}
$pending_stage = Arr::where($onboarding_progress_stage, fn($value) => $value == "PENDING");
if (!empty($pending_stage)) {
$client_update->progress_indicator = array_key_first($pending_stage);
} else {
$client_update->progress_indicator = 'COMPLETED';
dispatch(new SendOnboardingCompletedEmailAlert(Models\Client::with('auth_user_info')->find($id)));
}
}
// File handling logic encapsulated safely
$fileFields = [
'document_one' => 'document_one_name',
'document_two' => 'document_two_name',
'document_three' => 'document_three_name',
'other_document' => 'other_document_name'
];
foreach ($fileFields as $fileInput => $nameInput) {
if ($request->hasFile($fileInput) && $request->file($fileInput)->isValid() && $request->filled($nameInput)) {
$filename = "erp_" . time() . Str::random(4) . "." . $request->file($fileInput)->extension();
$request->file($fileInput)->storeAs('client_files', $filename, 'public');
$document_name = $fileInput === 'other_document' && !$request->filled($nameInput)
? 'Other Document'
: $request->input($nameInput);
Models\ClientFile::updateOrCreate(
['client_id' => $id, 'name' => $document_name],
[
'file_path' => $filename,
'file_extension' => $request->file($fileInput)->extension(),
'file_reff' => time() . uniqid(),
'created_by' => session('current_user.id'),
]
);
}
}
$client_update->name = $request->name;
$client_update->email = $request->email;
$client_update->phone = $request->phone ?? "";
$client_update->contact_person = $request->contact_person;
$client_update->status = $request->status;
$client_update->pay_mode = $request->payment_mode;
$client_update->country = $request->country;
$client_update->currency = $request->currency;
$client_update->notes = $request->notes;
$client_update->industry = $request->industry;
if ($client_update->progress_indicator != 'COMPLETED') {
$client_update->onboarding_progress_stage = json_encode($onboarding_progress_stage);
$progress_breakdown = array_count_values($onboarding_progress_stage);
$indicator_score = Arr::has($progress_breakdown, 'COMPLETED')
? ($progress_breakdown['COMPLETED'] / count($onboarding_progress_stage)) * 100
: 0;
$client_update->progress_indicator_score = number_format($indicator_score);
} else {
$client_update->progress_indicator_score = 100;
}
$client_update->skype_name = $request->skype_name ?? "";
$client_update->linkedin_name = $request->linkedin_name ?? "";
$client_update->smpp_username = $request->smpp_username ?? "";
$client_update->company_type = $request->company_type ?? "";
$client_update->auth_user_id = $request->auth_user_id ?? "";
$client_update->contract_type = $request->contract_type ?? "";
$client_update->contract_validity = $request->contract_validity ?? "";
$client_update->contract_auto_renew = $request->contract_auto_renew ?? "";
$client_update->sender_ids = $request->sender_ids ? json_encode($request->sender_ids) : "";
$client_update->connections = $request->connections ? json_encode($request->connections) : "";
$client_update->services = $request->services ? json_encode($request->services) : "";
$client_update->message_types = $request->message_types ? json_encode($request->message_types) : "";
$client_update->finance_email = $request->finance_email ? json_encode($request->finance_email) : "";
$client_update->support_emails = $request->support_emails ? json_encode($request->support_emails) : "";
$client_update->rate_emails = $request->rate_emails ? json_encode($request->rate_emails) : "";
$client_update->support_phones = $request->support_phones ? json_encode($request->support_phones) : "";
$client_update->support_skype = $request->support_skype ? json_encode($request->support_skype) : "";
if ($request->has('how_we_got_client')) {
$client_update->how_we_got_client = $request->how_we_got_client == 'Other' ? ($request->how_we_got_client_other ?? "") : $request->how_we_got_client;
}
$client_update->last_modified_by = session('current_user.id');
$client_update->save();
Models\UserActivity::create([
'type' => 'staff',
'content' => session('current_user.name') . " updated ({$client_update->name}) details successfully!",
'user_id' => session('current_user.id'),
'ip_address' => request()->ip(),
'device' => $request->header('User-Agent')
]);
Session::flash('success_message', 'Client successfully Updated');
return redirect(url('clients', $id));
}
public function getClientFile($id)
{
$client_file = Models\ClientFile::with('client_info')->findOrFail($id);
// Use proper storage path logic
$file = storage_path('app/public/client_files/' . $client_file->file_path);
if (!file_exists($file)) {
abort(404, 'File not found on the server.');
}
$filename = Str::slug($client_file->client_info->name . "_" . $client_file->name, '_');
$filename = $filename . "." . $client_file->file_extension;
return Response::download($file, $filename);
}
public function cleanStr($string): string
{
// Replaced custom regex block with Laravel's native slug helper
return Str::slug($string, '_');
}
// ... Additional unchanged methods (getMnoCountry, etc.) follow below.
}

View File

@@ -29,13 +29,16 @@ class Client extends Model
return $this->hasMany('App\Models\MeetingReport', 'client', 'id'); return $this->hasMany('App\Models\MeetingReport', 'client', 'id');
} }
public function auth_user_info(){ public function auth_user_info(){
return $this->hasOne('App\Models\SystemUser', 'id', 'auth_user_id'); return $this->hasOne('App\Models\StaffMember', 'id', 'auth_user_id');
}
public function account_manager(){
return $this->hasOne('App\Models\StaffMember', 'id', 'auth_user_id');
} }
public function created_by_info(){ public function created_by_info(){
return $this->hasOne('App\Models\SystemUser', 'id', 'created_by'); return $this->hasOne('App\Models\StaffMember', 'id', 'created_by');
} }
public function modified_by_info(){ public function modified_by_info(){
return $this->hasOne('App\Models\SystemUser', 'id', 'last_modified_by'); return $this->hasOne('App\Models\StaffMember', 'id', 'last_modified_by');
} }
public function short_code_info(){ public function short_code_info(){
return $this->hasMany('App\Models\ShortCode', 'client_id', 'id'); return $this->hasMany('App\Models\ShortCode', 'client_id', 'id');

283
countries_new.sql Normal file
View File

@@ -0,0 +1,283 @@
-- Create table
CREATE TABLE countries_new (
num_code INT PRIMARY KEY,
alpha_2_code CHAR(2) NOT NULL,
alpha_3_code CHAR(3) NOT NULL,
en_short_name VARCHAR(100) NOT NULL,
nationality VARCHAR(100) NOT NULL
);
-- Insert countries_new (chunk 1: 150)
INSERT INTO countries_new (num_code, alpha_2_code, alpha_3_code, en_short_name, nationality) VALUES
(4, 'AF', 'AFG', 'Afghanistan', 'Afghan'),
(8, 'AL', 'ALB', 'Albania', 'Albanian'),
(10, 'AQ', 'ATA', 'Antarctica', 'Antarctic'),
(12, 'DZ', 'DZA', 'Algeria', 'Algerian'),
(16, 'AS', 'ASM', 'American Samoa', 'American Samoan'),
(20, 'AD', 'AND', 'Andorra', 'Andorran'),
(24, 'AO', 'AGO', 'Angola', 'Angolan'),
(28, 'AG', 'ATG', 'Antigua and Barbuda', 'Antiguan'),
(31, 'AZ', 'AZE', 'Azerbaijan', 'Azerbaijani'),
(32, 'AR', 'ARG', 'Argentina', 'Argentine'),
(36, 'AU', 'AUS', 'Australia', 'Australian'),
(40, 'AT', 'AUT', 'Austria', 'Austrian'),
(44, 'BS', 'BHS', 'Bahamas', 'Bahamian'),
(48, 'BH', 'BHR', 'Bahrain', 'Bahraini'),
(50, 'BD', 'BGD', 'Bangladesh', 'Bangladeshi'),
(51, 'AM', 'ARM', 'Armenia', 'Armenian'),
(52, 'BB', 'BRB', 'Barbados', 'Barbadian'),
(56, 'BE', 'BEL', 'Belgium', 'Belgian'),
(60, 'BM', 'BMU', 'Bermuda', 'Bermudian'),
(64, 'BT', 'BTN', 'Bhutan', 'Bhutanese'),
(68, 'BO', 'BOL', 'Bolivia', 'Bolivian'),
(70, 'BA', 'BIH', 'Bosnia and Herzegovina', 'Bosnian'),
(72, 'BW', 'BWA', 'Botswana', 'Botswanan'),
(74, 'BV', 'BVT', 'Bouvet Island', 'Bouvet Islander'),
(76, 'BR', 'BRA', 'Brazil', 'Brazilian'),
(84, 'BZ', 'BLZ', 'Belize', 'Belizean'),
(86, 'IO', 'IOT', 'British Indian Ocean Territory', 'BIOT'),
(90, 'SB', 'SLB', 'Solomon Islands', 'Solomon Islander'),
(92, 'VG', 'VGB', 'British Virgin Islands', 'British Virgin Islander'),
(96, 'BN', 'BRN', 'Brunei Darussalam', 'Bruneian'),
(100, 'BG', 'BGR', 'Bulgaria', 'Bulgarian'),
(104, 'MM', 'MMR', 'Myanmar', 'Burmese'),
(108, 'BI', 'BDI', 'Burundi', 'Burundian'),
(112, 'BY', 'BLR', 'Belarus', 'Belarusian'),
(116, 'KH', 'KHM', 'Cambodia', 'Cambodian'),
(120, 'CM', 'CMR', 'Cameroon', 'Cameroonian'),
(124, 'CA', 'CAN', 'Canada', 'Canadian'),
(132, 'CV', 'CPV', 'Cabo Verde', 'Cape Verdean'),
(136, 'KY', 'CYM', 'Cayman Islands', 'Caymanian'),
(140, 'CF', 'CAF', 'Central African Republic', 'Central African'),
(144, 'LK', 'LKA', 'Sri Lanka', 'Sri Lankan'),
(148, 'TD', 'TCD', 'Chad', 'Chadian'),
(152, 'CL', 'CHL', 'Chile', 'Chilean'),
(156, 'CN', 'CHN', 'China', 'Chinese'),
(158, 'TW', 'TWN', 'Taiwan', 'Taiwanese'),
(162, 'CX', 'CXR', 'Christmas Island', 'Christmas Islander'),
(166, 'CC', 'CCK', 'Cocos Islands', 'Cocos Islander');
-- Insert countries_new (chunk 2: 51100)
INSERT INTO countries_new (num_code, alpha_2_code, alpha_3_code, en_short_name, nationality) VALUES
(170, 'CO', 'COL', 'Colombia', 'Colombian'),
(174, 'KM', 'COM', 'Comoros', 'Comorian'),
(175, 'YT', 'MYT', 'Mayotte', 'Mahoran'),
(178, 'CG', 'COG', 'Congo', 'Congolese'),
(180, 'CD', 'COD', 'Democratic Republic of the Congo', 'Congolese'),
(184, 'CK', 'COK', 'Cook Islands', 'Cook Islander'),
(188, 'CR', 'CRI', 'Costa Rica', 'Costa Rican'),
(191, 'HR', 'HRV', 'Croatia', 'Croatian'),
(192, 'CU', 'CUB', 'Cuba', 'Cuban'),
(196, 'CY', 'CYP', 'Cyprus', 'Cypriot'),
(203, 'CZ', 'CZE', 'Czech Republic', 'Czech'),
(204, 'BJ', 'BEN', 'Benin', 'Beninese'),
(208, 'DK', 'DNK', 'Denmark', 'Danish'),
(212, 'DM', 'DMA', 'Dominica', 'Dominican'),
(214, 'DO', 'DOM', 'Dominican Republic', 'Dominican'),
(218, 'EC', 'ECU', 'Ecuador', 'Ecuadorian'),
(222, 'SV', 'SLV', 'El Salvador', 'Salvadoran'),
(226, 'GQ', 'GNQ', 'Equatorial Guinea', 'Equatoguinean'),
(231, 'ET', 'ETH', 'Ethiopia', 'Ethiopian'),
(232, 'ER', 'ERI', 'Eritrea', 'Eritrean'),
(233, 'EE', 'EST', 'Estonia', 'Estonian'),
(234, 'FO', 'FRO', 'Faroe Islands', 'Faroese'),
(238, 'FK', 'FLK', 'Falkland Islands', 'Falkland Islander'),
(239, 'GS', 'SGS', 'South Georgia and the South Sandwich Islands', 'South Georgian'),
(242, 'FJ', 'FJI', 'Fiji', 'Fijian'),
(246, 'FI', 'FIN', 'Finland', 'Finnish'),
(248, 'AX', 'ALA', 'Åland Islands', 'Åland Islander'),
(250, 'FR', 'FRA', 'France', 'French'),
(254, 'GF', 'GUF', 'French Guiana', 'French Guianese'),
(258, 'PF', 'PYF', 'French Polynesia', 'French Polynesian'),
(260, 'TF', 'ATF', 'French Southern Territories', 'French Southern Territories'),
(262, 'DJ', 'DJI', 'Djibouti', 'Djiboutian'),
(266, 'GA', 'GAB', 'Gabon', 'Gabonese'),
(268, 'GE', 'GEO', 'Georgia', 'Georgian'),
(270, 'GM', 'GMB', 'Gambia', 'Gambian'),
(275, 'PS', 'PSE', 'Palestine', 'Palestinian'),
(276, 'DE', 'DEU', 'Germany', 'German'),
(288, 'GH', 'GHA', 'Ghana', 'Ghanaian'),
(292, 'GI', 'GIB', 'Gibraltar', 'Gibraltarian'),
(296, 'KI', 'KIR', 'Kiribati', 'I-Kiribati'),
(300, 'GR', 'GRC', 'Greece', 'Greek'),
(304, 'GL', 'GRL', 'Greenland', 'Greenlandic'),
(308, 'GD', 'GRD', 'Grenada', 'Grenadian'),
(312, 'GP', 'GLP', 'Guadeloupe', 'Guadeloupean'),
(316, 'GU', 'GUM', 'Guam', 'Guamanian'),
(320, 'GT', 'GTM', 'Guatemala', 'Guatemalan');
-- Insert countries_new (chunk 3: 101150)
INSERT INTO countries_new (num_code, alpha_2_code, alpha_3_code, en_short_name, nationality) VALUES
(324, 'GN', 'GIN', 'Guinea', 'Guinean'),
(328, 'GY', 'GUY', 'Guyana', 'Guyanese'),
(332, 'HT', 'HTI', 'Haiti', 'Haitian'),
(334, 'HM', 'HMD', 'Heard Island and McDonald Islands', 'Heard Islander'),
(336, 'VA', 'VAT', 'Vatican City', 'Vatican'),
(340, 'HN', 'HND', 'Honduras', 'Honduran'),
(344, 'HK', 'HKG', 'Hong Kong', 'Hong Konger'),
(348, 'HU', 'HUN', 'Hungary', 'Hungarian'),
(352, 'IS', 'ISL', 'Iceland', 'Icelandic'),
(356, 'IN', 'IND', 'India', 'Indian'),
(360, 'ID', 'IDN', 'Indonesia', 'Indonesian'),
(364, 'IR', 'IRN', 'Iran', 'Iranian'),
(368, 'IQ', 'IRQ', 'Iraq', 'Iraqi'),
(372, 'IE', 'IRL', 'Ireland', 'Irish'),
(376, 'IL', 'ISR', 'Israel', 'Israeli'),
(380, 'IT', 'ITA', 'Italy', 'Italian'),
(384, 'CI', 'CIV', 'Côte d''Ivoire', 'Ivorian'),
(388, 'JM', 'JAM', 'Jamaica', 'Jamaican'),
(392, 'JP', 'JPN', 'Japan', 'Japanese'),
(398, 'KZ', 'KAZ', 'Kazakhstan', 'Kazakhstani'),
(400, 'JO', 'JOR', 'Jordan', 'Jordanian'),
(404, 'KE', 'KEN', 'Kenya', 'Kenyan'),
(408, 'KP', 'PRK', 'North Korea', 'North Korean'),
(410, 'KR', 'KOR', 'South Korea', 'South Korean'),
(414, 'KW', 'KWT', 'Kuwait', 'Kuwaiti'),
(417, 'KG', 'KGZ', 'Kyrgyzstan', 'Kyrgyzstani'),
(418, 'LA', 'LAO', 'Laos', 'Laotian'),
(422, 'LB', 'LBN', 'Lebanon', 'Lebanese'),
(426, 'LS', 'LSO', 'Lesotho', 'Basotho'),
(428, 'LV', 'LVA', 'Latvia', 'Latvian'),
(430, 'LR', 'LBR', 'Liberia', 'Liberian'),
(434, 'LY', 'LBY', 'Libya', 'Libyan'),
(438, 'LI', 'LIE', 'Liechtenstein', 'Liechtensteiner'),
(440, 'LT', 'LTU', 'Lithuania', 'Lithuanian'),
(442, 'LU', 'LUX', 'Luxembourg', 'Luxembourger'),
(446, 'MO', 'MAC', 'Macao', 'Macanese'),
(450, 'MG', 'MDG', 'Madagascar', 'Malagasy'),
(454, 'MW', 'MWI', 'Malawi', 'Malawian'),
(458, 'MY', 'MYS', 'Malaysia', 'Malaysian'),
(462, 'MV', 'MDV', 'Maldives', 'Maldivian'),
(466, 'ML', 'MLI', 'Mali', 'Malian'),
(470, 'MT', 'MLT', 'Malta', 'Maltese'),
(474, 'MQ', 'MTQ', 'Martinique', 'Martiniquais'),
(478, 'MR', 'MRT', 'Mauritania', 'Mauritanian'),
(480, 'MU', 'MUS', 'Mauritius', 'Mauritian'),
(484, 'MX', 'MEX', 'Mexico', 'Mexican'),
(492, 'MC', 'MCO', 'Monaco', 'Monégasque'),
(496, 'MN', 'MNG', 'Mongolia', 'Mongolian');
-- Insert countries_new (chunk 4: 151200)
INSERT INTO countries_new (num_code, alpha_2_code, alpha_3_code, en_short_name, nationality) VALUES
(498, 'MD', 'MDA', 'Moldova', 'Moldovan'),
(499, 'ME', 'MNE', 'Montenegro', 'Montenegrin'),
(500, 'MS', 'MSR', 'Montserrat', 'Montserratian'),
(504, 'MA', 'MAR', 'Morocco', 'Moroccan'),
(508, 'MZ', 'MOZ', 'Mozambique', 'Mozambican'),
(512, 'OM', 'OMN', 'Oman', 'Omani'),
(516, 'NA', 'NAM', 'Namibia', 'Namibian'),
(520, 'NR', 'NRU', 'Nauru', 'Nauruan'),
(524, 'NP', 'NPL', 'Nepal', 'Nepali'),
(528, 'NL', 'NLD', 'Netherlands', 'Dutch'),
(531, 'CW', 'CUW', 'Curaçao', 'Curaçaoan'),
(533, 'AW', 'ABW', 'Aruba', 'Aruban'),
(534, 'SX', 'SXM', 'Sint Maarten', 'Sint Maartener'),
(535, 'BQ', 'BES', 'Bonaire, Sint Eustatius and Saba', 'Bonairean'),
(540, 'NC', 'NCL', 'New Caledonia', 'New Caledonian'),
(548, 'VU', 'VUT', 'Vanuatu', 'Ni-Vanuatu'),
(554, 'NZ', 'NZL', 'New Zealand', 'New Zealander'),
(558, 'NI', 'NIC', 'Nicaragua', 'Nicaraguan'),
(562, 'NE', 'NER', 'Niger', 'Nigerien'),
(566, 'NG', 'NGA', 'Nigeria', 'Nigerian'),
(570, 'NU', 'NIU', 'Niue', 'Niuean'),
(574, 'NF', 'NFK', 'Norfolk Island', 'Norfolk Islander'),
(578, 'NO', 'NOR', 'Norway', 'Norwegian'),
(580, 'MP', 'MNP', 'Northern Mariana Islands', 'Northern Marianan'),
(581, 'UM', 'UMI', 'United States Minor Outlying Islands', 'American'),
(583, 'FM', 'FSM', 'Micronesia', 'Micronesian'),
(584, 'MH', 'MHL', 'Marshall Islands', 'Marshallese'),
(585, 'PW', 'PLW', 'Palau', 'Palauan'),
(586, 'PK', 'PAK', 'Pakistan', 'Pakistani'),
(591, 'PA', 'PAN', 'Panama', 'Panamanian'),
(598, 'PG', 'PNG', 'Papua New Guinea', 'Papua New Guinean'),
(600, 'PY', 'PRY', 'Paraguay', 'Paraguayan'),
(604, 'PE', 'PER', 'Peru', 'Peruvian'),
(608, 'PH', 'PHL', 'Philippines', 'Filipino'),
(612, 'PN', 'PCN', 'Pitcairn Islands', 'Pitcairn Islander'),
(616, 'PL', 'POL', 'Poland', 'Polish'),
(620, 'PT', 'PRT', 'Portugal', 'Portuguese'),
(624, 'GW', 'GNB', 'Guinea-Bissau', 'Bissau-Guinean'),
(626, 'TL', 'TLS', 'Timor-Leste', 'Timorese'),
(630, 'PR', 'PRI', 'Puerto Rico', 'Puerto Rican'),
(634, 'QA', 'QAT', 'Qatar', 'Qatari'),
(638, 'RE', 'REU', 'Réunion', 'Réunionese'),
(642, 'RO', 'ROU', 'Romania', 'Romanian'),
(643, 'RU', 'RUS', 'Russia', 'Russian'),
(646, 'RW', 'RWA', 'Rwanda', 'Rwandan');
-- Insert countries_new (chunk 5: 201250)
INSERT INTO countries_new (num_code, alpha_2_code, alpha_3_code, en_short_name, nationality) VALUES
(652, 'BL', 'BLM', 'Saint Barthélemy', 'Barthélemois'),
(654, 'SH', 'SHN', 'Saint Helena, Ascension and Tristan da Cunha', 'Saint Helenian'),
(659, 'KN', 'KNA', 'Saint Kitts and Nevis', 'Kittitian'),
(660, 'AI', 'AIA', 'Anguilla', 'Anguillan'),
(662, 'LC', 'LCA', 'Saint Lucia', 'Saint Lucian'),
(663, 'MF', 'MAF', 'Saint Martin', 'Saint-Martinoise'),
(666, 'PM', 'SPM', 'Saint Pierre and Miquelon', 'Saint-Pierrais'),
(670, 'VC', 'VCT', 'Saint Vincent and the Grenadines', 'Vincentian'),
(674, 'SM', 'SMR', 'San Marino', 'Sammarinese'),
(678, 'ST', 'STP', 'Sao Tome and Principe', 'São Toméan'),
(682, 'SA', 'SAU', 'Saudi Arabia', 'Saudi'),
(686, 'SN', 'SEN', 'Senegal', 'Senegalese'),
(688, 'RS', 'SRB', 'Serbia', 'Serbian'),
(690, 'SC', 'SYC', 'Seychelles', 'Seychellois'),
(694, 'SL', 'SLE', 'Sierra Leone', 'Sierra Leonean'),
(702, 'SG', 'SGP', 'Singapore', 'Singaporean'),
(703, 'SK', 'SVK', 'Slovakia', 'Slovak'),
(704, 'VN', 'VNM', 'Vietnam', 'Vietnamese'),
(705, 'SI', 'SVN', 'Slovenia', 'Slovenian'),
(706, 'SO', 'SOM', 'Somalia', 'Somali'),
(710, 'ZA', 'ZAF', 'South Africa', 'South African'),
(716, 'ZW', 'ZWE', 'Zimbabwe', 'Zimbabwean'),
(724, 'ES', 'ESP', 'Spain', 'Spanish'),
(728, 'SS', 'SSD', 'South Sudan', 'South Sudanese'),
(729, 'SD', 'SDN', 'Sudan', 'Sudanese'),
(732, 'EH', 'ESH', 'Western Sahara', 'Sahrawi'),
(740, 'SR', 'SUR', 'Suriname', 'Surinamese'),
(744, 'SJ', 'SJM', 'Svalbard and Jan Mayen', 'Svalbardian'),
(748, 'SZ', 'SWZ', 'Eswatini', 'Swazi'),
(752, 'SE', 'SWE', 'Sweden', 'Swedish'),
(756, 'CH', 'CHE', 'Switzerland', 'Swiss'),
(760, 'SY', 'SYR', 'Syria', 'Syrian'),
(762, 'TJ', 'TJK', 'Tajikistan', 'Tajikistani'),
(764, 'TH', 'THA', 'Thailand', 'Thai'),
(768, 'TG', 'TGO', 'Togo', 'Togolese'),
(772, 'TK', 'TKL', 'Tokelau', 'Tokelauan'),
(776, 'TO', 'TON', 'Tonga', 'Tongan'),
(780, 'TT', 'TTO', 'Trinidad and Tobago', 'Trinidadian'),
(784, 'AE', 'ARE', 'United Arab Emirates', 'Emirati'),
(788, 'TN', 'TUN', 'Tunisia', 'Tunisian'),
(792, 'TR', 'TUR', 'Turkey', 'Turkish'),
(795, 'TM', 'TKM', 'Turkmenistan', 'Turkmen'),
(796, 'TC', 'TCA', 'Turks and Caicos Islands', 'Turks and Caicos Islander'),
(798, 'TV', 'TUV', 'Tuvalu', 'Tuvaluan'),
(800, 'UG', 'UGA', 'Uganda', 'Ugandan'),
(804, 'UA', 'UKR', 'Ukraine', 'Ukrainian'),
(807, 'MK', 'MKD', 'North Macedonia', 'Macedonian'),
(818, 'EG', 'EGY', 'Egypt', 'Egyptian');
-- Insert countries_new (chunk 6: 251end)
INSERT INTO countries_new (num_code, alpha_2_code, alpha_3_code, en_short_name, nationality) VALUES
(826, 'GB', 'GBR', 'United Kingdom', 'British'),
(831, 'GG', 'GGY', 'Guernsey', 'Channel Islander'),
(832, 'JE', 'JEY', 'Jersey', 'Channel Islander'),
(833, 'IM', 'IMN', 'Isle of Man', 'Manx'),
(834, 'TZ', 'TZA', 'Tanzania', 'Tanzanian'),
(840, 'US', 'USA', 'United States of America', 'American'),
(850, 'VI', 'VIR', 'United States Virgin Islands', 'U.S. Virgin Islander'),
(854, 'BF', 'BFA', 'Burkina Faso', 'Burkinabé'),
(858, 'UY', 'URY', 'Uruguay', 'Uruguayan'),
(860, 'UZ', 'UZB', 'Uzbekistan', 'Uzbekistani'),
(862, 'VE', 'VEN', 'Venezuela', 'Venezuelan'),
(876, 'WF', 'WLF', 'Wallis and Futuna', 'Wallisian'),
(882, 'WS', 'WSM', 'Samoa', 'Samoan'),
(887, 'YE', 'YEM', 'Yemen', 'Yemeni'),
(894, 'ZM', 'ZMB', 'Zambia', 'Zambian');

View File

@@ -0,0 +1,416 @@
// public/js/client-index.js
$(document).ready(function() {
$('#clientService').select2({
placeholder: "-- Select Services --",
allowClear: true,
dropdownParent: $('#createClientModal')
});
let searchTimer;
// Initialize the table on page load
fetchClients();
// Event Listeners for Search and Filters
$('#searchClient').on('keyup', function() {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => fetchClients(1), 400);
});
$('#filterService, #filterBilling').on('change', function() {
fetchClients(1);
});
$('#filterService, #filterStatus').on('change', function() {
fetchClients(1);
});
// Handle Pagination Clicks dynamically
$(document).on('click', '.page-link-ajax', function(e) {
e.preventDefault();
let page = $(this).data('page');
if (page) fetchClients(page);
});
$('#btnOpenClientModal').on('click', function(e) {
e.preventDefault();
$('#createClientModal').modal('show');
});
$(document).on('click', '.btn-view-client', function(e) {
e.preventDefault();
let clientId = $(this).data('id');
window.location.href = base_url + "/clients/" + clientId;
});
// ---------------------------------------------------------
// EDIT CLIENT MODAL TRIGGER & POPULATION
// ---------------------------------------------------------
$(document).on('click', '.btn-edit-client', function(e) {
e.preventDefault();
let clientId = $(this).data('id');
let $form = $('#editClientForm');
// Set dynamic action URL for update
$form.attr('action', base_url + '/clients/' + clientId);
// Fetch Services first, then Client data to populate the edit modal
$.ajax({
url: base_url + '/api/services',
type: 'GET',
success: function(services) {
let $servicesSelect = $('#edit_services');
$servicesSelect.empty();
if (services && Array.isArray(services)) {
services.forEach(function(s) {
$servicesSelect.append(new Option(s.name, s.id));
});
}
// Fetch client details JSON
$.ajax({
url: base_url + '/clients/' + clientId + '/json',
type: 'GET',
success: function(client) {
$('#edit_name').val(client.name);
$('#edit_email').val(client.email);
$('#edit_phone').val(client.phone);
$('#edit_contact_person').val(client.contact_person);
$('#edit_company_type').val(client.company_type);
$('#edit_contract_type').val(client.contract_type);
$('#edit_industry').val(client.industry);
$('#edit_status').val(client.status);
$('#edit_currency').val(client.currency);
// $('#edit_country').val(client.country);
loadCountries(client.country);
// Helper for multi-select Select2 fields
function setSelect2Values(selector, values) {
let $el = $(selector);
$el.val(null).trigger('change');
if (values && Array.isArray(values)) {
values.forEach(function(val) {
if ($el.find("option[value='" + val + "']").length === 0) {
$el.append(new Option(val, val, true, true));
}
});
$el.val(values).trigger('change');
}
}
// Parse services if stored as JSON string
let clientServices = client.services;
if (typeof clientServices === 'string') {
try { clientServices = JSON.parse(clientServices); } catch(err) {}
}
setSelect2Values('#edit_services', clientServices);
setSelect2Values('#edit_message_types', client.message_types);
setSelect2Values('#edit_connections', client.connections);
setSelect2Values('#edit_support_phones', client.support_phones);
setSelect2Values('#edit_support_emails', client.support_emails);
setSelect2Values('#edit_rate_emails', client.rate_emails);
setSelect2Values('#edit_support_skype', client.support_skype);
// Show the modal after populating
$('#editClientModal select[multiple]').each(function() {
if (!$(this).hasClass("select2-hidden-accessible")) {
$(this).select2({
theme: 'bootstrap-5',
dropdownParent: $('#editClientModal'),
tags: true, // Allows typing custom entries for emails, phones, etc.
tokenSeparators: [',', ' '],
placeholder: 'Select or type and hit enter...'
});
}
});
$('#editClientModal').modal('show');
},
error: function() {
Swal.fire('Error', 'Could not fetch client details.', 'error');
}
});
}
});
});
$('#createClientModal, #editClientModal').on('hidden.bs.modal', function () {
let $form = $(this).find('form');
if ($form.length) {
$form[0].reset();
// Reset any select elements or select2 tags back to blank
$form.find('select').val(null).trigger('change');
}
});
// ---------------------------------------------------------
// AJAX FORM SUBMISSION (Create Client)
// ---------------------------------------------------------
$('#createClientForm').on('submit', function(e) {
e.preventDefault();
let $form = $(this);
let $submitBtn = $('#btnSubmitClient');
let originalText = $submitBtn.html();
let $alertBox = $('#clientModalAlert');
$submitBtn.html('<span class="spinner-border spinner-border-sm me-2"></span>Saving...').prop('disabled', true);
$alertBox.html('');
$form.find('.is-invalid').removeClass('is-invalid');
$form.find('.invalid-feedback').remove();
$.ajax({
url: $form.attr('action'),
method: 'POST',
data: $form.serialize(),
success: function(response) {
if(response.success) {
$alertBox.html(`
<div class="alert alert-success d-flex align-items-center" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>
<div>${response.message || 'Client added successfully!'}</div>
</div>
`);
fetchClients(1);
setTimeout(() => {
$('#createClientModal').modal('hide');
$form[0].reset();
$alertBox.html('');
}, 2000);
}
},
error: function(xhr) {
if (xhr.status === 401) {
window.location.href = base_url + '/login';
}
if (xhr.status === 422) {
let errors = xhr.responseJSON.errors;
$alertBox.html(`
<div class="alert alert-danger d-flex align-items-center" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<div>Please fix the errors highlighted below.</div>
</div>
`);
$.each(errors, function(key, value) {
let fieldName = key;
if(key === 'services') fieldName = 'services[]';
let $input = $form.find('[name="' + fieldName + '"]');
if ($input.length) {
$input.addClass('is-invalid');
$input.parent().append('<div class="invalid-feedback d-block fw-medium">' + value[0] + '</div>');
}
});
} else {
$alertBox.html(`
<div class="alert alert-danger d-flex align-items-center" role="alert">
<i class="bi bi-x-circle-fill me-2"></i>
<div>An unexpected server error occurred. Please try again.</div>
</div>
`);
}
},
complete: function() {
$submitBtn.html(originalText).prop('disabled', false);
}
});
});
$('#createClientModal').on('hidden.bs.modal', function () {
$('#createClientForm')[0].reset();
$('#clientService').val(null).trigger('change');
$('#clientModalAlert').html('');
$('#createClientForm').find('.is-invalid').removeClass('is-invalid');
$('#createClientForm').find('.invalid-feedback').remove();
});
// AJAX Fetch Function
function fetchClients(page = 1) {
const search = $('#searchClient').val();
const service = $('#filterService').val();
const billing = $('#filterBilling').val();
const status = $('#filterStatus').val();
$('#clientTableBody').html(`
<tr>
<td colspan="7" class="text-center py-5">
<div class="spinner-border text-primary" role="status" style="color: #5c4df0 !important;"></div>
<div class="mt-2 text-secondary" style="font-size: 0.85rem;">Loading clients...</div>
</td>
</tr>
`);
$.ajax({
url: base_url + "/clients/data",
type: "GET",
data: { page: page, search: search, service: service, billing: billing, status: status },
success: function(response) {
renderTable(response.data);
renderPagination(response);
},
error: function() {
$('#clientTableBody').html('<tr><td colspan="7" class="text-center text-danger py-4">Failed to load data. Please try again.</td></tr>');
}
});
}
// Handle Export Actions
$('.btn-export').on('click', function(e) {
e.preventDefault();
const search = $('#searchClient').val();
const service = $('#filterService').val();
const billing = $('#filterBilling').val();
const status = $('#filterStatus').val();
const format = $(this).data('format');
const queryParams = $.param({
search: search,
service: service,
billing: billing,
status: status,
format: format
});
window.location.href = base_url + "/clients/export?" + queryParams;
});
// Render Table Rows
function renderTable(clients) {
let html = '';
if (clients.length === 0) {
$('#clientTableBody').html('<tr><td colspan="7" class="text-center text-secondary py-4">No clients found matching your criteria.</td></tr>');
return;
}
clients.forEach(client => {
let initials = client.name.substring(0, 2).toUpperCase();
let servicesHtml = '<span class="text-secondary" style="font-size: 0.8rem;">N/A</span>';
if (client.services) {
try {
let servicesArray = typeof client.services === 'string' ? JSON.parse(client.services) : client.services;
if (Array.isArray(servicesArray) && servicesArray.length > 0) {
servicesHtml = servicesArray.map(service =>
`<span class="badge bg-secondary bg-opacity-10 text-secondary border me-1 mb-1" style="font-size: 0.7rem;">${service}</span>`
).join('');
}
} catch (e) {
console.error("Could not parse services for client: " + client.name);
}
}
let statusBadge = client.status === 'Live' || client.status === 'active'
? '<span class="badge bg-success bg-opacity-10 text-success px-2 py-1"><i class="bi bi-check-circle me-1"></i>Active</span>'
: '<span class="badge bg-warning bg-opacity-10 text-warning px-2 py-1"><i class="bi bi-clock me-1"></i>' + client.status + '</span>';
let formattedDate = 'N/A';
if (client.created_at) {
const dateObj = new Date(client.created_at);
formattedDate = dateObj.toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric'
});
}
// Extract Account Manager name safely
let amName = 'Unassigned';
if (client.account_manager && client.account_manager.name) {
amName = client.account_manager.name;
}
html += `
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-circle bg-primary bg-opacity-10 text-primary me-3">${initials}</div>
<div>
<div class="fw-bold text-dark">${client.name}<div>
<div class="text-secondary" style="font-size: 0.8rem;"><i class="bi bi-geo-alt me-1"></i>${client.country || 'N/A'}</div>
</div>
</div>
</td>
<td>
<div class="fw-semibold">${client.contact_person || 'N/A'}</div>
<td>
<span class="text-dark fw-medium" style="font-size: 0.85rem;">
<i class="bi bi-person-badge text-secondary me-1"></i>${amName}
</span>
</td>
<div class="text-secondary" style="font-size: 0.8rem;">${client.email || 'N/A'}</div>
</td>
<td style="max-width: 200px; white-space: normal;">
${servicesHtml}
</td>
<td><span class="badge bg-info bg-opacity-10 text-info text-uppercase">${client.pay_mode || 'N/A'}</span></td>
<td><span class="fw-medium text-primary">${formattedDate}</span></td>
<td>${statusBadge}</td>
<td class="text-end">
<div class="d-flex gap-1 justify-content-end align-items-center">
<button class="btn btn-sm btn-light text-primary me-1 btn-view-client" data-id="${client.id}" title="View Details">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-light text-primary btn-edit-client" data-id="${client.id}" title="Edit Client">
<i class="bi bi-pencil"></i>
</button>
</div>
</td>
</tr>
`;
});
$('#clientTableBody').html(html);
}
// Render Pagination Links
function renderPagination(response) {
$('#paginationInfo').text(`Showing ${response.from || 0} to ${response.to || 0} of ${response.total} entries`);
let paginationHtml = '';
if (response.last_page > 1) {
let prevDisabled = response.current_page === 1 ? 'disabled' : '';
paginationHtml += `<li class="page-item ${prevDisabled}"><a class="page-link page-link-ajax" href="#" data-page="${response.current_page - 1}">Previous</a></li>`;
for (let i = 1; i <= response.last_page; i++) {
let activeClass = response.current_page === i ? 'active' : '';
let style = response.current_page === i ? 'style="background-color: #5c4df0; border-color: #5c4df0;"' : 'class="page-link text-dark"';
paginationHtml += `<li class="page-item ${activeClass}"><a class="page-link page-link-ajax" href="#" ${style} data-page="${i}">${i}</a></li>`;
}
let nextDisabled = response.current_page === response.last_page ? 'disabled' : '';
paginationHtml += `<li class="page-item ${nextDisabled}"><a class="page-link page-link-ajax" href="#" data-page="${response.current_page + 1}">Next</a></li>`;
}
$('#paginationLinks').html(paginationHtml);
}
// Helper function to populate country dropdowns dynamically
function loadCountries(selectedCountry = '') {
$.ajax({
url: base_url + '/api/countries',
type: 'GET',
success: function(countries) {
// Target both create and edit country dropdowns if they exist
let $selectors = $('#create_country, #edit_country');
$selectors.each(function() {
let $select = $(this);
let currentVal = $select.val() || selectedCountry;
$select.empty().append('<option value="">Select Country</option>');
if (countries && Array.isArray(countries)) {
countries.forEach(function(c) {
$select.append(new Option(c.en_short_name, c.en_short_name));
});
}
if (currentVal) {
$select.val(currentVal);
}
});
}
});
}
// Call loadCountries on page load so it's ready for the create modal
loadCountries();
});

View File

@@ -343,7 +343,7 @@ document.addEventListener("DOMContentLoaded", function() {
$.ajax({ $.ajax({
url: $form.attr('action'), url: $form.attr('action'),
type: 'POST', // Spoofed as PUT via hidden input type: 'POST',
data: $form.serialize(), data: $form.serialize(),
headers: { headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 B

BIN
public/favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Clients Report</title>
<style>
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #333;
font-size: 12px;
line-height: 1.4;
margin: 0;
padding: 20px;
}
.header {
margin-bottom: 30px;
border-bottom: 2px solid #5c4df0;
padding-bottom: 10px;
}
.header h2 {
margin: 0;
color: #5c4df0;
font-size: 22px;
}
.header p {
margin: 5px 0 0;
color: #666;
font-size: 11px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
padding: 8px 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f8f9fa;
color: #333;
font-weight: bold;
font-size: 11px;
text-transform: uppercase;
}
tbody tr:nth-child(even) {
background-color: #fdfdfd;
}
.badge {
padding: 3px 6px;
font-size: 10px;
font-weight: bold;
border-radius: 4px;
text-transform: uppercase;
}
.badge-active { background-color: #d1e7dd; color: #0f5132; }
.badge-other { background-color: #fff3cd; color: #664d03; }
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 10px;
color: #aaa;
border-top: 1px solid #eee;
padding-top: 5px;
}
</style>
</head>
<body>
<!-- Report Header -->
<div class="header">
<h2>Click ERP - Client Master Report</h2>
<p>Generated on: {{ date('d M Y, h:i A') }} &bull; Total Records: {{ count($clients) }}</p>
</div>
<!-- Data Table -->
<table>
<thead>
<tr>
<th>#</th>
<th>Client Name</th>
<th>Contact Person</th>
<th>Email</th>
<th>Pay Mode</th>
<th>Country</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@forelse($clients as $index => $client)
<tr>
<td>{{ $index + 1 }}</td>
<td><strong>{{ $client->name }}</strong></td>
<td>{{ $client->contact_person ?? 'N/A' }}</td>
<td>{{ $client->email ?? 'N/A' }}</td>
<td><span style="text-transform: uppercase;">{{ $client->pay_mode ?? 'N/A' }}</span></td>
<td>{{ $client->country ?? 'N/A' }}</td>
<td>
@php
$isActive = $client->status === 'Live' || $client->status === 'active';
@endphp
<span class="badge {{ $isActive ? 'badge-active' : 'badge-other' }}">
{{ $client->status ?? 'N/A' }}
</span>
</td>
</tr>
@empty
<tr>
<td colspan="7" style="text-align: center; color: #777; padding: 20px;">No client records found matching the criteria.</td>
</tr>
@endforelse
</tbody>
</table>
<!-- Footer -->
<div class="footer">
Click ERP &bull; Confidential Business Report
</div>
</body>
</html>

View File

@@ -32,6 +32,13 @@
border-color: #dc3545; border-color: #dc3545;
} }
.avatar-circle { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; font-weight: bold; font-size: 0.85rem; } .avatar-circle { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; font-weight: bold; font-size: 0.85rem; }
/* Ensure all Select2 containers expand to fill their grid column width */
.select2-container {
width: 100% !important;
}
.select2-selection--multiple {
min-height: 38px !important;
}
</style> </style>
@endpush @endpush
@@ -46,12 +53,10 @@
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div> <div>
<h4 class="fw-bold mb-1">Client Management</h4> <h4 class="fw-bold mb-1">Client Management</h4>
<p class="text-secondary mb-0" style="font-size: 0.9rem;">Manage clients, payment modes, services etc.</p> <p class="text-secondary mb-0" style="font-size: 0.9rem;">Manage clients, payment modes, contact person, services etc.</p>
</div> </div>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-outline-secondary d-flex align-items-center dropdown-toggle" type="button" data-bs-toggle="dropdown" style="border-radius: 8px;">
<i class="bi bi-download me-2"></i> Export
</button>
<ul class="dropdown-menu shadow-sm border-0"> <ul class="dropdown-menu shadow-sm border-0">
<li> <li>
<a class="dropdown-item btn-export d-flex align-items-center" href="#" data-format="csv"> <a class="dropdown-item btn-export d-flex align-items-center" href="#" data-format="csv">
@@ -73,9 +78,9 @@
<!-- Data Table Card --> <!-- Data Table Card -->
<div class="content-card"> <div class="content-card">
<!-- Filters --> <!-- Filters & Export Header -->
<div class="p-3 border-bottom bg-light bg-opacity-50"> <div class="p-3 border-bottom bg-light bg-opacity-50">
<div class="row g-3"> <div class="row g-3 align-items-center">
<div class="col-md-4"> <div class="col-md-4">
<div class="input-group"> <div class="input-group">
<span class="input-group-text bg-white border-end-0 text-secondary"><i class="bi bi-search"></i></span> <span class="input-group-text bg-white border-end-0 text-secondary"><i class="bi bi-search"></i></span>
@@ -89,8 +94,6 @@
<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">
@@ -107,9 +110,28 @@
<option value="Prospective">Prospective</option> <option value="Prospective">Prospective</option>
<option value="Cancelled">Cancelled</option> <option value="Cancelled">Cancelled</option>
<option value="Inactive">Inactive</option> <option value="Inactive">Inactive</option>
<option value="Prospective">Prospective</option>
</select> </select>
</div> </div>
<!-- Moved Export Button Here -->
<div class="col-md-2 text-end">
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle w-100 bg-white" type="button" data-bs-toggle="dropdown">
<i class="bi bi-download me-1"></i> Export
</button>
<ul class="dropdown-menu shadow-sm border-0 w-100">
<li>
<a class="dropdown-item btn-export d-flex align-items-center" href="#" data-format="csv">
<i class="bi bi-filetype-csv me-2 text-success"></i> Export CSV
</a>
</li>
<li>
<a class="dropdown-item btn-export d-flex align-items-center" href="#" data-format="pdf">
<i class="bi bi-filetype-pdf me-2 text-danger"></i> Export PDF
</a>
</li>
</ul>
</div>
</div>
</div> </div>
</div> </div>
@@ -120,6 +142,7 @@
<tr> <tr>
<th>Client Details</th> <th>Client Details</th>
<th>Primary Contact</th> <th>Primary Contact</th>
<th>Account Manager</th>
<th>Services</th> <th>Services</th>
<th>Billing</th> <th>Billing</th>
<th>Date Added</th> <th>Date Added</th>
@@ -147,405 +170,11 @@
@push('modals') @push('modals')
@include('clients.partials.create') @include('clients.partials.create')
@include('clients.partials.edit-modal')
@endpush @endpush
@push('scripts') @push('scripts')
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script> <script src="{{ asset('public/assets/js/client-index.js') }}"></script>
$(document).ready(function() {
$('#clientService').select2({
placeholder: "-- Select Services --",
allowClear: true,
dropdownParent: $('#createClientModal')
});
let searchTimer;
// Initialize the table on page load
fetchClients();
// Event Listeners for Search and Filters
$('#searchClient').on('keyup', function() {
clearTimeout(searchTimer);
// Debounce the search so we don't spam the server on every keystroke
searchTimer = setTimeout(() => fetchClients(1), 400);
});
$('#filterService, #filterBilling').on('change', function() {
fetchClients(1);
});
$('#filterService, #filterStatus').on('change', function() {
fetchClients(1);
});
// Handle Pagination Clicks dynamically
$(document).on('click', '.page-link-ajax', function(e) {
e.preventDefault();
let page = $(this).data('page');
if (page) fetchClients(page);
});
$('#btnOpenClientModal').on('click', function(e) {
e.preventDefault();
// Replace '#createClientModal' with the actual ID of the modal in your clients.partials.create file
$('#createClientModal').modal('show');
});
$(document).on('click', '.btn-view-client', function(e) {
e.preventDefault();
let clientId = $(this).data('id');
// OPTION A: Redirect to a view page
window.location.href = base_url + "/clients/" + clientId;
// OPTION B: If you are using a View Modal instead, uncomment this:
// fetchClientDetailsForView(clientId);
// $('#viewClientModal').modal('show');
});
$(document).on('click', '.btn-edit-client', function(e) {
e.preventDefault();
let clientId = $(this).data('id');
// OPTION A: Redirect to an edit page
window.location.href = base_url + "/clients/" + clientId + "/edit";
// OPTION B: If you are using an Edit Modal instead, uncomment this:
// fetchClientDetailsForEdit(clientId);
// $('#editClientModal').modal('show');
});
// ---------------------------------------------------------
// AJAX FORM SUBMISSION (Create Client)
// ---------------------------------------------------------
$('#createClientForm').on('submit', function(e) {
e.preventDefault();
let $form = $(this);
let $submitBtn = $('#btnSubmitClient');
let originalText = $submitBtn.html();
let $alertBox = $('#clientModalAlert');
// 1. Show loading state & reset previous alerts/errors
$submitBtn.html('<span class="spinner-border spinner-border-sm me-2"></span>Saving...').prop('disabled', true);
$alertBox.html('');
$form.find('.is-invalid').removeClass('is-invalid');
$form.find('.invalid-feedback').remove();
$.ajax({
url: $form.attr('action'),
method: 'POST',
data: $form.serialize(),
success: function(response) {
if(response.success) {
// Show success message inside the modal
$alertBox.html(`
<div class="alert alert-success d-flex align-items-center" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>
<div>${response.message || 'Client added successfully!'}</div>
</div>
`);
// Refresh the background datatable
fetchClients(1);
// Optional: Automatically close the modal after 2 seconds
setTimeout(() => {
$('#createClientModal').modal('hide');
$form[0].reset();
$alertBox.html('');
}, 2000);
}
},
error: function(xhr) {
if (xhr.status === 401) {
window.location.href = base_url + '/login';
}
// Handle Laravel Validation Errors (Status 422)
if (xhr.status === 422) {
let errors = xhr.responseJSON.errors;
// Show general warning alert at the top
$alertBox.html(`
<div class="alert alert-danger d-flex align-items-center" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<div>Please fix the errors highlighted below.</div>
</div>
`);
// Highlight specific fields
$.each(errors, function(key, value) {
// Account for array names like services[]
let fieldName = key;
if(key === 'services') fieldName = 'services[]';
let $input = $form.find('[name="' + fieldName + '"]');
if ($input.length) {
$input.addClass('is-invalid');
// Append the specific error message right below the field
$input.parent().append('<div class="invalid-feedback d-block fw-medium">' + value[0] + '</div>');
}
});
} else {
// Handle standard 500 server errors
$alertBox.html(`
<div class="alert alert-danger d-flex align-items-center" role="alert">
<i class="bi bi-x-circle-fill me-2"></i>
<div>An unexpected server error occurred. Please try again.</div>
</div>
`);
}
},
complete: function() {
// Restore the button to its original state
$submitBtn.html(originalText).prop('disabled', false);
}
});
});
// Clear alerts and errors when the modal is closed manually
$('#createClientModal').on('hidden.bs.modal', function () {
$('#createClientForm')[0].reset();
$('#clientService').val(null).trigger('change');
$('#clientModalAlert').html('');
$('#createClientForm').find('.is-invalid').removeClass('is-invalid');
$('#createClientForm').find('.invalid-feedback').remove();
});
// AJAX Fetch Function
function fetchClients(page = 1) {
const search = $('#searchClient').val();
const service = $('#filterService').val();
const billing = $('#filterBilling').val();
const status = $('#filterStatus').val();
// Show loading state
$('#clientTableBody').html(`
<tr>
<td colspan="7" class="text-center py-5">
<div class="spinner-border text-primary" role="status" style="color: #5c4df0 !important;"></div>
<div class="mt-2 text-secondary" style="font-size: 0.85rem;">Loading clients...</div>
</td>
</tr>
`);
$.ajax({
url: base_url + "/clients/data",
type: "GET",
data: { page: page, search: search, service: service, billing: billing, status:status },
success: function(response) {
renderTable(response.data);
renderPagination(response);
},
error: function() {
$('#clientTableBody').html('<tr><td colspan="6" class="text-center text-danger py-4">Failed to load data. Please try again.</td></tr>');
}
});
}
// Handle Export Actions
$('.btn-export').on('click', function(e) {
e.preventDefault();
// Get current filter values
const search = $('#searchClient').val();
const service = $('#filterService').val();
const billing = $('#filterBilling').val();
const status = $('#filterStatus').val();
const format = $(this).data('format'); // 'csv' or 'pdf'
// Build the query string
const queryParams = $.param({
search: search,
service: service,
billing: billing,
status : billing,
format: format
});
window.location.href = "{{ route('clients.export') }}?" + queryParams;
});
// Render Table Rows
function renderTableOld(clients) {
let html = '';
if (clients.length === 0) {
$('#clientTableBody').html('<tr><td colspan="6" class="text-center text-secondary py-4">No clients found matching your criteria.</td></tr>');
return;
}
clients.forEach(client => {
// Extract initials for the avatar
let initials = client.name.substring(0, 2).toUpperCase();
let servicesHtml = '<span class="text-secondary" style="font-size: 0.8rem;">N/A</span>';
if (client.services) {
try {
let servicesArray = JSON.parse(client.services);
// 2. Map each service into a Bootstrap badge
if (Array.isArray(servicesArray) && servicesArray.length > 0) {
servicesHtml = servicesArray.map(service =>
`<span class="badge bg-secondary bg-opacity-10 text-secondary border me-1 mb-1" style="font-size: 0.7rem;">${service}</span>`
).join('');
}
} catch (e) {
console.error("Could not parse services for client: " + client.name);
}
}
// Status Badge Logic
let statusBadge = client.status === 'active'
? '<span class="badge bg-success bg-opacity-10 text-success px-2 py-1"><i class="bi bi-check-circle me-1"></i>Active</span>'
: '<span class="badge bg-warning bg-opacity-10 text-warning px-2 py-1"><i class="bi bi-clock me-1"></i>' + client.status + '</span>';
html += `
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-circle bg-primary bg-opacity-10 text-primary me-3">${initials}</div>
<div>
<div class="fw-bold text-dark">${client.name}</div>
<div class="text-secondary" style="font-size: 0.8rem;"><i class="bi bi-geo-alt me-1"></i>${client.country || 'N/A'}</div>
</div>
</div>
</td>
<td>
<div class="fw-semibold">${client.contact_person || 'N/A'}</div>
<div class="text-secondary" style="font-size: 0.8rem;">${client.email || 'N/A'}</div>
</td>
<td style="max-width: 200px; white-space: normal;">
${servicesHtml}
</td>
<td><span class="badge bg-info bg-opacity-10 text-info text-uppercase">${client.pay_mode || 'N/A'}</span></td>
<td><span class="fw-medium text-primary">${client.created_at}</span></td>
<td>${statusBadge}</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-view-client"
data-id="${client.id}">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-client"
data-id="${client.id}">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
`;
});
$('#clientTableBody').html(html);
}
function renderTable(clients) {
let html = '';
if (clients.length === 0) {
$('#clientTableBody').html('<tr><td colspan="7" class="text-center text-secondary py-4">No clients found matching your criteria.</td></tr>');
return;
}
clients.forEach(client => {
// 1. Extract initials for the avatar
let initials = client.name.substring(0, 2).toUpperCase();
// 2. Handle Services
let servicesHtml = '<span class="text-secondary" style="font-size: 0.8rem;">N/A</span>';
if (client.services) {
try {
let servicesArray = JSON.parse(client.services);
if (Array.isArray(servicesArray) && servicesArray.length > 0) {
servicesHtml = servicesArray.map(service =>
`<span class="badge bg-secondary bg-opacity-10 text-secondary border me-1 mb-1" style="font-size: 0.7rem;">${service}</span>`
).join('');
}
} catch (e) {
console.error("Could not parse services for client: " + client.name);
}
}
// 3. Status Badge Logic
let statusBadge = client.status === 'Live' || client.status === 'active'
? '<span class="badge bg-success bg-opacity-10 text-success px-2 py-1"><i class="bi bi-check-circle me-1"></i>Active</span>'
: '<span class="badge bg-warning bg-opacity-10 text-warning px-2 py-1"><i class="bi bi-clock me-1"></i>' + client.status + '</span>';
// 4. Format the Date (Human Friendly)
let formattedDate = 'N/A';
if (client.created_at) {
const dateObj = new Date(client.created_at);
// Formats to: "23 Jul 2026"
formattedDate = dateObj.toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric'
});
}
html += `
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-circle bg-primary bg-opacity-10 text-primary me-3">${initials}</div>
<div>
<div class="fw-bold text-dark">${client.name}</div>
<div class="text-secondary" style="font-size: 0.8rem;"><i class="bi bi-geo-alt me-1"></i>${client.country || 'N/A'}</div>
</div>
</div>
</td>
<td>
<div class="fw-semibold">${client.contact_person || 'N/A'}</div>
<div class="text-secondary" style="font-size: 0.8rem;">${client.email || 'N/A'}</div>
</td>
<td style="max-width: 200px; white-space: normal;">
${servicesHtml}
</td>
<td><span class="badge bg-info bg-opacity-10 text-info text-uppercase">${client.pay_mode || 'N/A'}</span></td>
<!-- Updated Date Column -->
<td><span class="fw-medium text-primary">${formattedDate}</span></td>
<td>${statusBadge}</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary me-1 btn-view-client"
data-id="${client.id}">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-light text-primary me-1 btn-edit-client"
data-id="${client.id}">
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
`;
});
$('#clientTableBody').html(html);
}
// Render Pagination Links
function renderPagination(response) {
// Update "Showing X to Y of Z entries"
$('#paginationInfo').text(`Showing ${response.from || 0} to ${response.to || 0} of ${response.total} entries`);
let paginationHtml = '';
// Only show pagination if there is more than 1 page
if (response.last_page > 1) {
// Previous Button
let prevDisabled = response.current_page === 1 ? 'disabled' : '';
paginationHtml += `<li class="page-item ${prevDisabled}"><a class="page-link page-link-ajax" href="#" data-page="${response.current_page - 1}">Previous</a></li>`;
// Page Numbers
for (let i = 1; i <= response.last_page; i++) {
let activeClass = response.current_page === i ? 'active' : '';
let style = response.current_page === i ? 'style="background-color: #5c4df0; border-color: #5c4df0;"' : 'class="page-link text-dark"';
paginationHtml += `<li class="page-item ${activeClass}"><a class="page-link page-link-ajax" href="#" ${style} data-page="${i}">${i}</a></li>`;
}
// Next Button
let nextDisabled = response.current_page === response.last_page ? 'disabled' : '';
paginationHtml += `<li class="page-item ${nextDisabled}"><a class="page-link page-link-ajax" href="#" data-page="${response.current_page + 1}">Next</a></li>`;
}
$('#paginationLinks').html(paginationHtml);
}
});
</script>
@endpush @endpush

View File

@@ -22,7 +22,13 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Location / Country *</label> <label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Location / Country *</label>
<!-- Changed name from 'location' to 'country' --> <!-- Changed name from 'location' to 'country' -->
<input type="text" class="form-control" id="country" name="country" required placeholder="e.g. Ghana"> <!-- <input type="text" class="form-control" id="country" name="country" required placeholder="e.g. Ghana"> -->
<!-- <div class="col-md-4"> -->
<label class="form-label text-muted small fw-bold">Country</label>
<select name="country" id="create_country" class="form-select w-100">
<option value="">Select Country</option>
</select>
<!-- </div> -->
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Company Type *</label> <label class="form-label text-secondary fw-semibold" style="font-size: 0.85rem;">Company Type *</label>

View File

@@ -51,10 +51,8 @@
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label text-muted small fw-bold">Message Types</label> <label class="form-label text-muted small fw-bold">Message Types</label>
<select name="message_types[]" id="edit_message_types" class="form-select select2-tags" multiple="multiple"> <select name="message_types[]" id="edit_message_types" class="form-select select2-tags" multiple="multiple">
<option value="SMS">SMS</option> <option value="International">International</option>
<option value="USSD">USSD</option> <option value="Local">Local</option>
<option value="Voice">Voice</option>
<option value="Email">Email</option>
</select> </select>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@@ -89,7 +87,9 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label text-muted small fw-bold">Country</label> <label class="form-label text-muted small fw-bold">Country</label>
<input type="text" name="country" id="edit_country" class="form-control"> <select name="country" id="edit_country" class="form-select w-100">
<option value="">Select Country</option>
</select>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -14,7 +14,10 @@
<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@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" /> <link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('public/favicon/apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('public/favicon/favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('public/favicon/favicon-16x16.png') }}">
<link rel="manifest" href="/site.webmanifest">
<style> <style>
body { background-color: #f8f9fc; } body { background-color: #f8f9fc; }

View File

@@ -16,7 +16,7 @@ Route::middleware(['auth'])->group(function () {
Route::get('home', [App\Http\Controllers\DashboardController::class, 'index'])->name('home'); Route::get('home', [App\Http\Controllers\DashboardController::class, 'index'])->name('home');
#Clients #Clients
Route::get('/clients/data', [App\Http\Controllers\ClientsController::class, 'fetchData'])->name('clients.data'); Route::get('/clients/data', [App\Http\Controllers\ClientsController::class, 'fetchData'])->name('clients.data');
Route::get('/clients/export', [App\Http\Controllers\ClientController::class, 'export'])->name('clients.export'); Route::get('/clients/export', [App\Http\Controllers\ClientsController::class, 'export'])->name('clients.export');
Route::get('/clients/{id}/json', [App\Http\Controllers\ClientsController::class, 'getClientJson']); Route::get('/clients/{id}/json', [App\Http\Controllers\ClientsController::class, 'getClientJson']);
Route::put('/clients/{id}', [App\Http\Controllers\ClientsController::class, 'update'])->name('clients.update'); Route::put('/clients/{id}', [App\Http\Controllers\ClientsController::class, 'update'])->name('clients.update');
Route::post('/clients/shortcodes-store', [App\Http\Controllers\ClientsController::class, 'shortcodeStore'])->name('shortcodes.store'); Route::post('/clients/shortcodes-store', [App\Http\Controllers\ClientsController::class, 'shortcodeStore'])->name('shortcodes.store');
@@ -43,6 +43,7 @@ Route::middleware(['auth'])->group(function () {
Route::get('/api/services', [App\Http\Controllers\ServicesController::class, 'getServicesJson']); Route::get('/api/services', [App\Http\Controllers\ServicesController::class, 'getServicesJson']);
Route::get('/api/countries', [App\Http\Controllers\HelperController::class, 'getCountriesJson']);
Route::post('/logout', [App\Http\Controllers\AuthController::class, 'logout'])->name('logout'); Route::post('/logout', [App\Http\Controllers\AuthController::class, 'logout'])->name('logout');