diff --git a/app/Http/Controllers/ClientsController.php b/app/Http/Controllers/ClientsController.php index 703bbc0..5fa3806 100644 --- a/app/Http/Controllers/ClientsController.php +++ b/app/Http/Controllers/ClientsController.php @@ -13,6 +13,9 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; +use Barryvdh\DomPDF\Facade\Pdf; // Ensure you have dompdf installed if exporting PDFs + + class ClientsController extends Controller { @@ -199,14 +202,18 @@ class ClientsController extends Controller private function buildFilteredQuery(Request $request) { - $query = Models\Client::query(); + // $query = Models\Client::query(); + $query = Models\Client::with('account_manager'); if ($request->filled('search')) { $searchTerm = $request->search; $query->where(function ($q) use ($searchTerm) { $q->where('name', 'like', "%{$searchTerm}%") - ->orWhere('email', 'like', "%{$searchTerm}%") - ->orWhere('contact_person', 'like', "%{$searchTerm}%"); + ->orWhere('email', '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 */ + + public function export(Request $request) { - // Get the filtered data but execute get() instead of paginate() - $clients = $this->buildFilteredQuery($request)->get(); - $format = $request->input('format', 'csv'); + // Build query using the exact same filters as your data table + // $query = Models\Client::query(); + $query = Models\Client::with('account_manager'); - if ($format === 'pdf') { - return $this->exportToPdf($clients); + if ($request->filled('search')) { + $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 ]); } - $user_id = $auth_user->id; + $user_id = Auth::user()->id; Models\UserActivity::create([ 'type' => 'staff', diff --git a/app/Http/Controllers/HelperController.php b/app/Http/Controllers/HelperController.php new file mode 100644 index 0000000..f74a6b4 --- /dev/null +++ b/app/Http/Controllers/HelperController.php @@ -0,0 +1,19 @@ +orderBy('en_short_name', 'asc') + ->select('en_short_name') + ->get(); + + return response()->json($countries); + } +} diff --git a/app/Http/Controllers/code-1784661420553.php b/app/Http/Controllers/code-1784661420553.php deleted file mode 100644 index cbc1356..0000000 --- a/app/Http/Controllers/code-1784661420553.php +++ /dev/null @@ -1,424 +0,0 @@ - '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. -} \ No newline at end of file diff --git a/app/Models/Client.php b/app/Models/Client.php index 1b818a8..290a2f8 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -29,13 +29,16 @@ class Client extends Model return $this->hasMany('App\Models\MeetingReport', 'client', 'id'); } 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(){ - return $this->hasOne('App\Models\SystemUser', 'id', 'created_by'); + return $this->hasOne('App\Models\StaffMember', 'id', 'created_by'); } 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(){ return $this->hasMany('App\Models\ShortCode', 'client_id', 'id'); diff --git a/countries_new.sql b/countries_new.sql new file mode 100644 index 0000000..9c1fe41 --- /dev/null +++ b/countries_new.sql @@ -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'); diff --git a/public/assets/js/client-index.js b/public/assets/js/client-index.js new file mode 100644 index 0000000..eb14476 --- /dev/null +++ b/public/assets/js/client-index.js @@ -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('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(` + + `); + 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(` + + `); + $.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('
' + value[0] + '
'); + } + }); + } else { + $alertBox.html(` + + `); + } + }, + 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(` + + +
+
Loading clients...
+ + + `); + + $.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('Failed to load data. Please try again.'); + } + }); + } + + // 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('No clients found matching your criteria.'); + return; + } + + clients.forEach(client => { + let initials = client.name.substring(0, 2).toUpperCase(); + + let servicesHtml = 'N/A'; + 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 => + `${service}` + ).join(''); + } + } catch (e) { + console.error("Could not parse services for client: " + client.name); + } + } + + let statusBadge = client.status === 'Live' || client.status === 'active' + ? 'Active' + : '' + client.status + ''; + + 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 += ` + + +
+
${initials}
+
+
${client.name}
+
${client.country || 'N/A'}
+
+
+ + +
${client.contact_person || 'N/A'}
+ + + ${amName} + + +
${client.email || 'N/A'}
+ + + ${servicesHtml} + + ${client.pay_mode || 'N/A'} + ${formattedDate} + ${statusBadge} + +
+ + +
+ + + `; + }); + + $('#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 += `
  • Previous
  • `; + + 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 += `
  • ${i}
  • `; + } + + let nextDisabled = response.current_page === response.last_page ? 'disabled' : ''; + paginationHtml += `
  • Next
  • `; + } + + $('#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(''); + + 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(); +}); \ No newline at end of file diff --git a/public/assets/js/client-show-modal.js b/public/assets/js/client-show-modal.js index 490457f..861a1a7 100644 --- a/public/assets/js/client-show-modal.js +++ b/public/assets/js/client-show-modal.js @@ -343,7 +343,7 @@ document.addEventListener("DOMContentLoaded", function() { $.ajax({ url: $form.attr('action'), - type: 'POST', // Spoofed as PUT via hidden input + type: 'POST', data: $form.serialize(), headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') diff --git a/public/favicon/android-chrome-192x192.png b/public/favicon/android-chrome-192x192.png new file mode 100644 index 0000000..55746fb Binary files /dev/null and b/public/favicon/android-chrome-192x192.png differ diff --git a/public/favicon/android-chrome-512x512.png b/public/favicon/android-chrome-512x512.png new file mode 100644 index 0000000..f6d76d4 Binary files /dev/null and b/public/favicon/android-chrome-512x512.png differ diff --git a/public/favicon/apple-touch-icon.png b/public/favicon/apple-touch-icon.png new file mode 100644 index 0000000..dfa337d Binary files /dev/null and b/public/favicon/apple-touch-icon.png differ diff --git a/public/favicon/favicon-16x16.png b/public/favicon/favicon-16x16.png new file mode 100644 index 0000000..951a9bd Binary files /dev/null and b/public/favicon/favicon-16x16.png differ diff --git a/public/favicon/favicon-32x32.png b/public/favicon/favicon-32x32.png new file mode 100644 index 0000000..791cb3f Binary files /dev/null and b/public/favicon/favicon-32x32.png differ diff --git a/public/favicon/favicon.ico b/public/favicon/favicon.ico new file mode 100644 index 0000000..ea99d59 Binary files /dev/null and b/public/favicon/favicon.ico differ diff --git a/public/favicon/site.webmanifest b/public/favicon/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/public/favicon/site.webmanifest @@ -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"} \ No newline at end of file diff --git a/resources/views/clients/export-pdf.blade.php b/resources/views/clients/export-pdf.blade.php new file mode 100644 index 0000000..8b36de6 --- /dev/null +++ b/resources/views/clients/export-pdf.blade.php @@ -0,0 +1,125 @@ + + + + + Clients Report + + + + + +
    +

    Click ERP - Client Master Report

    +

    Generated on: {{ date('d M Y, h:i A') }} • Total Records: {{ count($clients) }}

    +
    + + + + + + + + + + + + + + + + @forelse($clients as $index => $client) + + + + + + + + + + @empty + + + + @endforelse + +
    #Client NameContact PersonEmailPay ModeCountryStatus
    {{ $index + 1 }}{{ $client->name }}{{ $client->contact_person ?? 'N/A' }}{{ $client->email ?? 'N/A' }}{{ $client->pay_mode ?? 'N/A' }}{{ $client->country ?? 'N/A' }} + @php + $isActive = $client->status === 'Live' || $client->status === 'active'; + @endphp + + {{ $client->status ?? 'N/A' }} + +
    No client records found matching the criteria.
    + + + + + + \ No newline at end of file diff --git a/resources/views/clients/index.blade.php b/resources/views/clients/index.blade.php index 628183c..f93b6ae 100644 --- a/resources/views/clients/index.blade.php +++ b/resources/views/clients/index.blade.php @@ -32,6 +32,13 @@ 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; } + /* Ensure all Select2 containers expand to fill their grid column width */ + .select2-container { + width: 100% !important; + } + .select2-selection--multiple { + min-height: 38px !important; + } @endpush @@ -46,12 +53,10 @@

    Client Management

    -

    Manage clients, payment modes, services etc.

    +

    Manage clients, payment modes, contact person, services etc.