bug fix in clients controller
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
19
app/Http/Controllers/HelperController.php
Normal file
19
app/Http/Controllers/HelperController.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
|
||||||
}
|
|
||||||
@@ -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
283
countries_new.sql
Normal 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: 1–50)
|
||||||
|
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: 51–100)
|
||||||
|
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: 101–150)
|
||||||
|
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: 151–200)
|
||||||
|
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: 201–250)
|
||||||
|
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: 251–end)
|
||||||
|
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');
|
||||||
416
public/assets/js/client-index.js
Normal file
416
public/assets/js/client-index.js
Normal 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();
|
||||||
|
});
|
||||||
@@ -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')
|
||||||
|
|||||||
BIN
public/favicon/android-chrome-192x192.png
Normal file
BIN
public/favicon/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
BIN
public/favicon/android-chrome-512x512.png
Normal file
BIN
public/favicon/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
public/favicon/apple-touch-icon.png
Normal file
BIN
public/favicon/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/favicon/favicon-16x16.png
Normal file
BIN
public/favicon/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 350 B |
BIN
public/favicon/favicon-32x32.png
Normal file
BIN
public/favicon/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 728 B |
BIN
public/favicon/favicon.ico
Normal file
BIN
public/favicon/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
public/favicon/site.webmanifest
Normal file
1
public/favicon/site.webmanifest
Normal 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"}
|
||||||
125
resources/views/clients/export-pdf.blade.php
Normal file
125
resources/views/clients/export-pdf.blade.php
Normal 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') }} • 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 • Confidential Business Report
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user