permit and admin refactoring

This commit is contained in:
Kwesi Banson Jnr 2026-06-21 14:00:47 +00:00
parent 351c445442
commit 176cd6f6ff
47 changed files with 7609 additions and 464 deletions

View File

@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User; // adjust if you have a custom model
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Validator;
class UserController extends Controller
{
public function index()
{
return view('admin.users'); // Blade file below
}
// Returns JSON list; supports category, search, status filters
public function list(Request $request)
{
$q = $request->query('q');
$category = $request->query('category'); // District/Regional/National or null
$statuses = $request->query('status') ?: []; // array
$query = User::query();
if ($category) $query->where('category', $category);
if ($q) {
$query->where(function($sub) use ($q){
$sub->where('name','like',"%{$q}%")
->orWhere('email','like',"%{$q}%")
->orWhere('id','like',"%{$q}%");
});
}
if (!empty($statuses)) $query->whereIn('status', $statuses);
$users = $query->orderBy('last_activity_at','desc')->paginate(25);
return response()->json($users);
}
public function show($id)
{
$user = User::findOrFail($id);
return response()->json($user);
}
public function store(Request $request)
{
$v = Validator::make($request->all(), [
'name'=>'required|string|max:255',
'email'=>'required|email|unique:users,email',
'category'=>['required', Rule::in(['District','Regional','National'])],
]);
if ($v->fails()) return response()->json(['errors'=>$v->errors()], 422);
$user = User::create($request->only(['name','email','category']) + ['status'=>'Active', 'password'=>bcrypt('temporary')]);
return response()->json($user, 201);
}
public function update(Request $request, $id)
{
$user = User::findOrFail($id);
$v = Validator::make($request->all(), [
'name'=>'required|string|max:255',
'email'=>['required','email', Rule::unique('users','email')->ignore($user->id)],
'category'=>['required', Rule::in(['District','Regional','National'])],
]);
if ($v->fails()) return response()->json(['errors'=>$v->errors()], 422);
$user->update($request->only(['name','email','category']));
return response()->json($user);
}
public function move(Request $request)
{
$v = Validator::make($request->all(), [
'ids'=>'required|array',
'target'=>'required|in:District,Regional,National',
]);
if ($v->fails()) return response()->json(['errors'=>$v->errors()], 422);
User::whereIn('id', $request->ids)->update(['category'=>$request->target]);
return response()->json(['moved'=>count($request->ids)]);
}
public function deactivate(Request $request)
{
$v = Validator::make($request->all(), [
'ids'=>'required|array',
]);
if ($v->fails()) return response()->json(['errors'=>$v->errors()], 422);
User::whereIn('id', $request->ids)->update(['status'=>'Suspended']);
return response()->json(['deactivated'=>count($request->ids)]);
}
public function destroy($id)
{
$user = User::findOrFail($id);
$user->delete();
return response()->json(['deleted'=>true]);
}
}

View File

@ -9,10 +9,11 @@ use Illuminate\Support\Collection;
class AdminController extends Controller class AdminController extends Controller
{ {
public function index(){ public function index()
{
if (session('current_user.user_type') == 'district_user') { if (session('current_user.user_type') == 'district_user') {
$users_url = "user_mgt/get_all_users_by_district.php"; $users_url = "user_mgt/get_all_users_by_district.php";
$data = ['district_id' => session('district_id'), 'api_token' => env('LUPMISAPIKEY')]; $data = ['district_id' => session('current_user.district_id'), 'api_token' => env('LUPMISAPIKEY')];
} }
elseif(session('current_user.user_type') == 'regional_user'){ elseif(session('current_user.user_type') == 'regional_user'){
$users_url = "user_mgt/get_all_users_by_district.php"; $users_url = "user_mgt/get_all_users_by_district.php";
@ -22,11 +23,114 @@ class AdminController extends Controller
$users_url = "user_mgt/get_all_users.php"; $users_url = "user_mgt/get_all_users.php";
$data = ['api_token' => env('LUPMISAPIKEY') ]; $data = ['api_token' => env('LUPMISAPIKEY') ];
} }
// dd($users_url);
$result = ApiCalls::CurlPost(json_encode($data), $users_url); $result = ApiCalls::CurlPost(json_encode($data), $users_url);
$users_arr = json_decode($result, true); $users_arr = json_decode($result, true);
// dd($users_arr);
if ($users_arr == null || $users_arr['success'] == false) {
return redirect()->back()->withErrors('Your request cannot be handled at this time. Try again later');
}
$regions_url = "user_mgt/get_all_regions.php";
$data = ['api_token' => env('LUPMISAPIKEY')];
$result = ApiCalls::CurlPost(json_encode($data), $regions_url);
$regions_arr = json_decode($result, true);
if ($regions_arr == null || $regions_arr['success'] == false) {
return redirect()->back()->withErrors('Your request cannot be handled at this time. Try again later');
}
$users = collect($users_arr['data']);
// dd($users->sortBy('ua_id'));
// dump($users);
if (request()->has('search') && request()->search != '') {
$search = strtolower(request()->search);
$users = $users->filter(function($u) use ($search) {
$name = strtolower($u['full_name'] ?? '');
$email = strtolower($u['email'] ?? '');
$username = strtolower($u['username'] ?? '');
$phone = strtolower($u['phone'] ?? '');
$ua_position = strtolower($u['ua_position'] ?? '');
$allowed_apps = strtolower($u['allowed_apps'] ?? '');
return str_contains($name, $search) ||
str_contains($email, $search) ||
str_contains($username, $search) ||
str_contains($phone, $search) ||
str_contains($ua_position, $search) ||
str_contains($allowed_apps, $search);
});
}
$user_type_arr = [
'district_user' => 'District User',
'regional_user' => 'Regional User',
'national_user' => 'National User'
];
$nationalUsers = $users->filter(function($u) {
$type = trim(strtolower((string)($u['user_type'] ?? '')));
$position = trim(strtolower((string)($u['ua_position'] ?? '')));
// Check if either the type OR the position contains 'national'
return str_contains($type, 'national') || str_contains($position, 'national');
});
$regionalUsers = $users->filter(function($u) {
$type = trim(strtolower((string)($u['user_type'] ?? '')));
$position = trim(strtolower((string)($u['ua_position'] ?? '')));
return str_contains($type, 'regional') || str_contains($position, 'regional');
});
$districtUsers = $users->reject(function($u) {
$type = trim(strtolower((string)($u['user_type'] ?? '')));
$position = trim(strtolower((string)($u['ua_position'] ?? '')));
$isNational = str_contains($type, 'national') || str_contains($position, 'national');
$isRegional = str_contains($type, 'regional') || str_contains($position, 'regional');
return $isNational || $isRegional;
});
$paginatedDistrict = $this->paginateCollection($districtUsers, 10, 'district_page');
$paginatedRegional = $this->paginateCollection($regionalUsers, 10, 'regional_page');
$paginatedNational = $this->paginateCollection($nationalUsers, 10, 'national_page');
// dd($paginatedRegional);
// return view('admin.home_cats', [
// 'page_title' => 'User Management',
// 'nationalUsers' => $nationalUsers,
// 'regionalUsers' => $regionalUsers,
// 'districtUsers' => $districtUsers,
// 'totalUsers' => $users->count()
// ]);
// dd($regionalUsers->count());
return view('admin.home_cats', [
'page_title' => 'User Admin',
'regions_arr' => $regions_arr['data'],
'user_type_arr' => $user_type_arr,
'nationalUsers' => $paginatedNational,
'regionalUsers' => $paginatedRegional,
'districtUsers' => $paginatedDistrict,
'totalUsers' => $users->count()
]);
}
public function indexSingle(){
if (session('current_user.user_type') == 'district_user' || session('current_user.user_type') == 'District User' ) {
$users_url = "user_mgt/get_all_users_by_district.php";
$data = ['district_id' => session('current_user.district_id'), 'api_token' => env('LUPMISAPIKEY')];
}
elseif(session('current_user.user_type') == 'regional_user'){
$users_url = "user_mgt/get_all_users_by_district.php";
$data = ['region_id' => session('region_id'), 'api_token' => env('LUPMISAPIKEY')];
}
else{
$users_url = "user_mgt/get_all_users.php";
$data = ['api_token' => env('LUPMISAPIKEY') ];
}
$result = ApiCalls::CurlPost(json_encode($data), $users_url);
$users_arr = json_decode($result, true);
// dump($users_arr);
if ($users_arr == null || $users_arr['success'] == false) { if ($users_arr == null || $users_arr['success'] == false) {
return redirect()->back()->withErrors('Your request cannot be handled at this time. Try again later'); return redirect()->back()->withErrors('Your request cannot be handled at this time. Try again later');
} }
@ -67,6 +171,7 @@ class AdminController extends Controller
$currentPage, $currentPage,
['path' => request()->url(), 'query' => request()->query()] ['path' => request()->url(), 'query' => request()->query()]
); );
// dd($paginatedItems);
$user_type_arr = [ $user_type_arr = [
'district_user' => 'District User', 'district_user' => 'District User',
'regional_user' => 'Regional User', 'regional_user' => 'Regional User',
@ -81,6 +186,7 @@ class AdminController extends Controller
'items' => $paginatedItems 'items' => $paginatedItems
]; ];
// return view('admin.home_new', $data);
return view('admin.paginated', $data); return view('admin.paginated', $data);
// return view('admin.home', $data); // return view('admin.home', $data);
@ -182,4 +288,30 @@ class AdminController extends Controller
]; ];
return view('common.notready', $data); return view('common.notready', $data);
} }
/**
* Manually paginate a collection.
*/
private function paginateCollection(Collection $items, $perPage = 10, $pageName = 'page')
{
// 1. Get the current page from the URL (e.g., ?district_page=2)
$page = LengthAwarePaginator::resolveCurrentPage($pageName);
// 2. Slice the collection to get only the items for the current page
$currentPageItems = $items->slice(($page - 1) * $perPage, $perPage)->values();
// 3. Create the paginator instance
$paginator = new LengthAwarePaginator(
$currentPageItems,
$items->count(),
$perPage,
$page,
[
'path' => LengthAwarePaginator::resolveCurrentPath(),
'pageName' => $pageName,
]
);
// Keep any other query parameters in the URL (like search keywords)
return $paginator->withQueryString();
}
} }

View File

@ -9,4 +9,34 @@ use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController class Controller extends BaseController
{ {
use AuthorizesRequests, ValidatesRequests; use AuthorizesRequests, ValidatesRequests;
public function validateGhanaPhone($phone) {
// Remove spaces, dashes, etc.
$phone = preg_replace('/\D+/', '', $phone);
// If it starts with 0 (e.g. 0241234567), strip the leading 0
if (strpos($phone, '0') === 0) {
$phone = substr($phone, 1);
}
// If it starts with Ghana country code without + (233...), add +
if (strpos($phone, '233') === 0) {
$phone = '+'.$phone;
}
// If it doesnt start with +233, prepend it
if (strpos($phone, '+233') !== 0) {
$phone = '+233'.$phone;
}
// Now validate length: Ghana mobile numbers are 9 digits after +233
$pattern = '/^\+233\d{9}$/';
if (preg_match($pattern, $phone)) {
return $phone; // valid, normalized
} else {
return false; // invalid
}
}
} }

View File

@ -83,6 +83,8 @@ class PermitsController extends Controller
]); ]);
$result = ApiCalls::CurlPost($data, $url); $result = ApiCalls::CurlPost($data, $url);
$result = json_decode($result, true); $result = json_decode($result, true);
// dd($result);
$allowed_users_to_comment = ['PPD Head', 'Works Department Head', 'luspa-it-head']; $allowed_users_to_comment = ['PPD Head', 'Works Department Head', 'luspa-it-head'];
$data = [ $data = [
'page_title' => 'Permits Details', 'page_title' => 'Permits Details',
@ -93,6 +95,46 @@ class PermitsController extends Controller
} }
public function statusUpdate($id){ public function statusUpdate($id){
}
public function reports(){
$data = [
'page_title' => 'Permit Reports'
];
return view('common.notready', $data);
}
public function addComment(Request $request){
$url = "permit_comments/insert_application_comment.php";
$data = json_encode([
"application_code" => $request->application_code,
"client_generated_id" => "CLIENT-001",
"app_comments" => $request->comment_body,
"created_by" => session('current_user.username'),
"created_by_id" => session('current_user.user_id'),
'api_token' => env('LUPMISAPIKEY')
]);
$result = ApiCalls::CurlPost($data, $url);
$result = json_decode($result, true);
// dd($result);
return response()->json($result);
}
public function getComments(Request $request){
$url = "permit_comments/select_application_comments.php";
$data = json_encode([
"application_code" => $request->application_code,
"client_generated_id" => "CLIENT-001",
"app_comments" => $request->comment_body,
"created_by" => session('current_user.username'),
"created_by_id" => session('current_user.user_id'),
'api_token' => env('LUPMISAPIKEY')
]);
$result = ApiCalls::CurlPost($data, $url);
$result = json_decode($result, true);
return response()->json($result);
} }
public function viewPdf($filename){ public function viewPdf($filename){
$path = storage_path('app/public/site_plans/' . $filename); $path = storage_path('app/public/site_plans/' . $filename);

View File

@ -29,11 +29,12 @@ class UserloginController extends Controller
$data = ['user' => $request->username, 'pass' => $request->password, 'api_token' => env('LUPMISAPIKEY')]; $data = ['user' => $request->username, 'pass' => $request->password, 'api_token' => env('LUPMISAPIKEY')];
$check_user = ApiCalls::CurlPost(json_encode($data), $check_user_url); $check_user = ApiCalls::CurlPost(json_encode($data), $check_user_url);
// dd($check_user);
if($check_user == false){ if($check_user == false){
return redirect("user-login")->withErrors(array("System not available at the moment. Try again later!"))->withInput(); return redirect("user-login")->withErrors(array("System not available at the moment. Try again later!"))->withInput();
} }
$result = json_decode($check_user, true); $result = json_decode($check_user, true);
if($result['success'] == false){ if($result['success'] == false){
return redirect("user-login")->withErrors(array("Incorrect Email/Password. Check and try again!"))->withInput(); return redirect("user-login")->withErrors(array("Incorrect Email/Password. Check and try again!"))->withInput();
} }
@ -41,6 +42,7 @@ class UserloginController extends Controller
##return redirect("user-login")->withErrors(array("Your Account has been disabled. Contact your administrator!"))->withInput(); ##return redirect("user-login")->withErrors(array("Your Account has been disabled. Contact your administrator!"))->withInput();
} }
$logged_in = $result['data']; $logged_in = $result['data'];
// dd($logged_in);
$plainToken = Str::random(60); $plainToken = Str::random(60);
// $hashedToken = hash('sha256', $plainToken); // $hashedToken = hash('sha256', $plainToken);
@ -49,7 +51,6 @@ class UserloginController extends Controller
'token' => hash('sha256', $plainToken), 'token' => hash('sha256', $plainToken),
'created_at' => now(), 'created_at' => now(),
]); ]);
$request->session()->regenerate(true); $request->session()->regenerate(true);
$request->session()->put('current_user.ua_id', $logged_in['ua_id']); $request->session()->put('current_user.ua_id', $logged_in['ua_id']);
$request->session()->put('current_user.user_id', $logged_in['user_id']); $request->session()->put('current_user.user_id', $logged_in['user_id']);
@ -64,6 +65,7 @@ class UserloginController extends Controller
$request->session()->put('current_user.region_id', $logged_in['region_id']); $request->session()->put('current_user.region_id', $logged_in['region_id']);
$request->session()->put('current_user.is_password_changed', $logged_in['is_password_changed']); $request->session()->put('current_user.is_password_changed', $logged_in['is_password_changed']);
$request->session()->put('current_user.district_id', $logged_in['district_id']); $request->session()->put('current_user.district_id', $logged_in['district_id']);
$request->session()->put('current_user.district_name', $logged_in['vr_district_name']);
// $request->session()->put('current_user.hashedToken', $hashedToken); // $request->session()->put('current_user.hashedToken', $hashedToken);
$request->session()->put('current_user.plainToken', $plainToken); $request->session()->put('current_user.plainToken', $plainToken);

View File

@ -9,6 +9,8 @@ use Illuminate\Support\Facades\Mail;
use App\Mail\UserAccountsMail; use App\Mail\UserAccountsMail;
use App\Mail\PasswordResetMail; use App\Mail\PasswordResetMail;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use App\Rules\GhanaPhoneRule;
class UsersController extends Controller class UsersController extends Controller
@ -26,8 +28,6 @@ class UsersController extends Controller
return view('user-auth.reset', $data); return view('user-auth.reset', $data);
} }
public function check_reset_email(Request $request){ public function check_reset_email(Request $request){
// code...
// dd('foo bar');
$url = "user_mgt/get_user_by_user_id.php"; $url = "user_mgt/get_user_by_user_id.php";
$user_id = "34ba702b-18f8-4d85-948d-8c55e8500f32"; $user_id = "34ba702b-18f8-4d85-948d-8c55e8500f32";
$data = json_encode([ $data = json_encode([
@ -139,12 +139,52 @@ class UsersController extends Controller
} }
public function store(Request $request){ public function store(Request $request){
$url = "user_mgt/add_usr_user.php"; $url = "user_mgt/add_usr_user.php";
// return ['success' => true]; $this->validate($request, [
// $password = ApiCalls::generatePassword(10); 'full_name' => 'required|string|max:255',
$password = $randomString = Str::random(10); 'username' => 'required|string|max:255|unique:users,username',
'ua_position' => 'required|string',
'allowed_apps'=> 'required|array',
'user_status' => 'required|string',
'gender' => 'required|in:male,female',
'districtid' => [
Rule::requiredIf(function () use ($request) {
return in_array($request->user_type, ['district_user', 'regional_luspa']);
}),
'integer'
],
'region_id' => [
Rule::requiredIf(function () use ($request) {
return in_array($request->user_type, ['regional_luspa']);
}),
'integer'
],
'user_type' => 'required|string',
'email' => 'required|email|unique:users,email',
'phone' => ['required', new GhanaPhoneRule],
], [
'full_name.required' => 'Please provide the full name.',
'username.required' => 'A username is required.',
'username.unique' => 'This username is already taken.',
'ua_position.required' => 'Position is mandatory.',
'allowed_apps.required'=> 'Select at least one application.',
'gender.required' => 'Gender is required.',
'district_id.required' => 'District must be selected.',
'region_id.required' => 'Region is required',
'user_type.required' => 'User type is required.',
'email.required' => 'Email address is required.',
'email.email' => 'Please enter a valid email address.',
'user_status.required' => 'Please select user status.',
'email.unique' => 'This email is already registered.',
'phone.required' => 'Phone number is required.',
]);
// Generate random password
$password = Str::random(10);
$data = json_encode([ $data = json_encode([
'full_name' => $request['full_name'], 'full_name' => $request['full_name'],
'username' => $request['username'], 'username' => $request['username'],
@ -154,46 +194,90 @@ class UsersController extends Controller
'allowed_apps' => implode(", ", $request['allowed_apps']), 'allowed_apps' => implode(", ", $request['allowed_apps']),
'is_password_changed' => false, 'is_password_changed' => false,
'password_hint' => 'none', 'password_hint' => 'none',
'phone' => $request['phone'], 'phone' => str_replace('+', '',$request['phone']),
'gender' => $request['gender'], 'gender' => $request['gender'],
'user_type' => $request['user_type'], 'user_type' => $request['user_type'],
'pass' => $password, 'pass' => $password,
'is_disabled' => false,
'region_id' => $request['region_id'], 'region_id' => $request['region_id'],
'district_id' => $request['districtid'], 'district_id' => $request['districtid'],
'api_token' => env('LUPMISAPIKEY'), //'1c46538c712e9b5b' // make the API token a constant 'api_token' => env('LUPMISAPIKEY'),
]); ]);
// dd($data); // dd($data);
$result = ApiCalls::CurlPost($data, $url); $result = ApiCalls::CurlPost($data, $url);
$result = json_decode($result, true); $result = json_decode($result, true);
if ($result['success'] == false) {
return response()->json($result);
}
\Log::info("Your Password is $password"); \Log::info("Your Password is $password");
$recipientEmail = 'recipient@example.com'; Mail::to('recipient@example.com')->send(new UserAccountsMail($password, $request->username));
Mail::to($recipientEmail)->send(new UserAccountsMail($password, $request->username));
//dd('Email sent!'); $sms_message = "Hello {$request->full_name}, your LUPMIS account has been successfully created\n";
$sms_message = "Hello $request->full_name your LUPMIS account has been successfully created\n"; $sms_message .= "Username: {$request->username}\n";
$sms_message .= "Username : . $request->username \n";
$sms_message .= "Password: $password\n"; $sms_message .= "Password: $password\n";
$sms_message .= 'Login URL : https://lupmis4luspa.org'; $sms_message .= "Login URL: https://lupmis4luspa.org";
$sms_data = [ $sms_data = [
'recipient' => $request['phone'], 'recipient' => $request['phone'],
'message' => $sms_message 'message' => $sms_message
]; ];
#$sms_result = SmsLibrary::SendMnotitySms($sms_data); // will send this the API
\Log::info("SMS Body : $sms_message"); \Log::info("SMS Body : $sms_message");
#\Log::info("SMS API Response : $sms_result");
if (request()->expectsJson()) { if ($request->expectsJson()) {
return response()->json($result); return response()->json($result);
} }
} }
public function update(Request $request){ public function update(Request $request){
$url = "user_mgt/update_usr_user.php"; $url = "user_mgt/update_usr_user.php";
// return ['success' => true]; // return ['success' => true];
// dd($request->all()); // dd($request->all());
$this->validate($request, [
'full_name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:users,username',
'ua_position' => 'required|string',
'allowed_apps'=> 'required|array',
'user_status' => 'required|string',
'gender' => 'required|in:male,female',
// 'districtid' => 'required|integer',
'districtid' => [
Rule::requiredIf(function () use ($request) {
return in_array($request->user_type, ['district_user']);
}),
'integer'
],
'region_id' => [
Rule::requiredIf(function () use ($request) {
return in_array($request->user_type, ['regional_luspa', 'national_luspa']);
}),
'integer'
],
'user_type' => 'required|string',
'email' => 'required|email|unique:users,email',
'phone' => ['required', new GhanaPhoneRule],
], [
'full_name.required' => 'Please provide the full name.',
'username.required' => 'A username is required.',
'username.unique' => 'This username is already taken.',
'ua_position.required' => 'Position is mandatory.',
'allowed_apps.required'=> 'Select at least one application.',
'gender.required' => 'Gender is required.',
'district_id.required' => 'District must be selected.',
'region_id.required' => 'Region is required',
'user_type.required' => 'User type is required.',
'user_status.required' => 'Please select user status.',
'email.required' => 'Email address is required.',
'email.email' => 'Please enter a valid email address.',
'email.unique' => 'This email is already registered.',
'phone.required' => 'Phone number is required.',
]);
$is_disabled = ($request->user_status == 'active') ? 'false' : 'true';
$user_data = [ $user_data = [
'full_name' => $request['full_name'], 'full_name' => $request['full_name'],
'username' => $request['username'], 'username' => $request['username'],
@ -202,11 +286,15 @@ class UsersController extends Controller
'email' => $request['email'], 'email' => $request['email'],
'title' => $request['title'], 'title' => $request['title'],
'allowed_apps' => implode(", ", $request['allowed_apps']), 'allowed_apps' => implode(", ", $request['allowed_apps']),
'phone' => $request['phone'], 'phone' => str_replace('+', '',$request['phone']),
'gender' => $request['gender'], 'gender' => $request['gender'],
'user_type' => 'District User', 'user_type' => $request['user_type'],
'api_token' => env('LUPMISAPIKEY'), 'api_token' => env('LUPMISAPIKEY'),
'is_disabled' => $is_disabled,
// 'region_id' => $request['region_id'],
'district_id' => $request['districtid'],
]; ];
if ($request->has('expire_password')) { if ($request->has('expire_password')) {
$user_data['is_password_changed'] = 'NO'; $user_data['is_password_changed'] = 'NO';
} }

View File

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PermitApplicationComments extends Model
{
//
}

View File

@ -16,6 +16,7 @@ class AppServiceProvider extends ServiceProvider
if (config('app.env') === 'production') { if (config('app.env') === 'production') {
URL::forceScheme('https'); URL::forceScheme('https');
} }
Paginator::useBootstrap(); // Paginator::useBootstrap();
Paginator::useBootstrapFive();
} }
} }

View File

@ -0,0 +1,40 @@
<?php
// namespace App\Rules;
// use Closure;
// use Illuminate\Contracts\Validation\ValidationRule;
// class GhanaPhoneRule implements ValidationRule
// {
// /**
// * Run the validation rule.
// *
// * @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
// */
// public function validate(string $attribute, mixed $value, Closure $fail): void
// {
// //
// }
// }
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class GhanaPhoneRule implements Rule
{
public function passes($attribute, $value)
{
// Ghana phone numbers: start with 0 or +233, followed by 9 digits
// return preg_match('/^(?:\+233|0)[0-9]{9}$/', $value);
return preg_match('/^(?:\+233|233|0)[0-9]{9}$/', $value);
#/^(?:\+233|233|0)[0-9]{9}$/
}
public function message()
{
return 'Please enter a valid Ghanaian phone number (e.g., 024XXXXXXX or +23324XXXXXXX).';
}
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('permit_application_comments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->nullOnDelete();
$table->string('application_code')->index();
$table->string('status');
$table->text('comment_body')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('permit_application_comments');
}
};

View File

@ -2,6 +2,9 @@
- assiamah/win - assiamah/win
- kwesilupmis/7aa0478bce - kwesilupmis/7aa0478bce
Username: saxiquzipa/lwCDfAMaBy </li>
composer create-project laravel/laravel:^11.0 my-laravel-11-project composer create-project laravel/laravel:^11.0 my-laravel-11-project
chmod -R 0777 storage/ chmod -R 0777 storage/
chmod -R 0777 bootstrap/ chmod -R 0777 bootstrap/
@ -26,6 +29,11 @@ add measurements to the backend for works department
216.55.137.19 216.55.137.19
1c46538c712e9b5b 1c46538c712e9b5b
206.225.87.174
216.55.185.131
ua_id": 10, ua_id": 10,
"user_id": "34ba702b-18f8-4d85-948d-8c55e8500f32", "user_id": "34ba702b-18f8-4d85-948d-8c55e8500f32",
"username": "hyhix", "username": "hyhix",
@ -47,3 +55,16 @@ ua_id": 10,
"region_id": 3, "region_id": 3,
"district_id": 180, "district_id": 180,
"user_type": "District User" "user_type": "District User"
# 1. Strip the restrictive header coming from the backend server
proxy_hide_header X-Frame-Options;
# 2. Inject the correct Content-Security-Policy
# IMPORTANT: Replace the URL below with your actual LIVE Laravel domain (No trailing slash)
add_header Content-Security-Policy "frame-ancestors 'self' https://lupmis4luspa.org" always;
add_header Content-Security-Policy "frame-ancestors 'self' https://lupmis.housebanson.net" always;

55
php_code/live.env Normal file
View File

@ -0,0 +1,55 @@
cat .env
APP_NAME=lupmisBackend
APP_ENV=local
APP_KEY=base64:pmEAeuW8clKrfKKjcMWylo68exoDO/Xr2hUhXvB7dS0=
APP_DEBUG=true
APP_URL=http:https://lupmis4luspa.org
LOG_CHANNEL=daily
LUPMISAPIKEY=1c46538c712e9b5b
MNOTOFYKEY=hFsiPMAPS3sIdwYSIthRO5JtS
#DB_CONNECTION=mysql
#DB_HOST=host.docker.internal
DB_CONNECTION=sqlite
#DB_DATABASE=database/lupmis.sqlite
DB_DATABASE=/var/www/html/database/lupmis.sqlite
DB_PORT=3306
#DB_DATABASE=lupmis.sqlite
DB_USERNAME=root
DB_PASSWORD=p@ssw0rd
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_DOMAIN=.lupmis4luspa.org
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=lupmis@luspa.gov.gh
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

View File

@ -0,0 +1,108 @@
@media (max-width: 768px) {
#permit-map { height: 350px; }
}
/* Container for the entire timeline */
.timeline {
position: relative;
padding-left: 2rem; /* Creates space for the vertical line and icons */
margin-top: 1rem;
}
/* The Vertical Line */
.timeline::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0.85rem; /* Centers the line behind the 32px icons */
width: 2px;
background-color: #dee2e6; /* Standard Bootstrap gray border */
z-index: 0;
}
/* Wrapper for each timeline event */
.timeline-item {
position: relative;
margin-bottom: 1.5rem;
}
/* The Circular Icons / Dots */
.timeline-icon {
position: absolute;
top: 0;
left: -2rem; /* Pulls the icon left to sit precisely on the vertical line */
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
z-index: 1; /* Ensures icons sit above the vertical line */
}
/* The Event Content Box */
.timeline-content {
margin-left: 0.5rem; /* Pushes the card slightly away from the icon */
background-color: #fff;
transition: all 0.2s ease;
}
.timeline-content:hover {
box-shadow: 0 .5rem 1rem rgba(0,0,0,.08)!important;
}
/* Vertical Line */
.planning-stepper {
position: relative;
padding-left: 1.25rem;
}
.planning-stepper::before {
content: '';
position: absolute;
top: 6px;
bottom: 0;
left: 4px; /* Centers the line exactly under the dots */
width: 2px;
background-color: #e9ecef; /* Bootstrap light gray */
z-index: 0;
}
/* Individual Steps */
.stepper-item {
position: relative;
padding-bottom: 1.75rem;
}
.stepper-item:last-child {
padding-bottom: 0; /* Removes extra space at the bottom of the list */
}
/* The Dots */
.stepper-dot {
position: absolute;
left: -1.25rem;
top: 0.25rem;
width: 10px;
height: 10px;
border-radius: 50%;
z-index: 1;
background-color: #dee2e6; /* Default pending gray */
}
/* Dot States */
.stepper-dot.completed {
background-color: #20c997; /* Teal/Success green */
}
.stepper-dot.active {
background-color: #fd7e14; /* Orange */
box-shadow: 0 0 0 5px rgba(253, 126, 20, 0.15); /* Soft orange halo */
}
.stepper-dot.pending {
background-color: #e2e8f0; /* Muted gray */
}
/* Faded text state for pending items */
.stepper-item.pending .step-title,
.stepper-item.pending .step-desc {
opacity: 0.6;
}

View File

@ -0,0 +1,477 @@
$(document).ready(function () {
let iti;
$('#editUserModal').on('shown.bs.modal', function () {
const phoneInput = document.querySelector("#editPhone");
if (!iti) {
iti = window.intlTelInput(phoneInput, {
initialCountry: "gh",
onlyCountries: ["gh"],
nationalMode: false,
autoPlaceholder: "polite",
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
});
phoneInput.addEventListener("blur", function () {
let rawNumber = phoneInput.value.trim();
console.log(rawNumber);
if (rawNumber.startsWith("0")) {
rawNumber = rawNumber.substring(1);
phoneInput.value = "+233" + rawNumber;
}
if (iti.isValidNumber()) {
phoneInput.value = iti.getNumber(); // clean +233XXXXXXXXX
}
});
}
});
$('#addUserModal').on('shown.bs.modal', function () {
const phoneInput = document.querySelector("#addUserphone");
if (!iti) {
iti = window.intlTelInput(phoneInput, {
initialCountry: "gh",
onlyCountries: ["gh"],
nationalMode: false,
autoPlaceholder: "polite",
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
});
phoneInput.addEventListener("blur", function () {
let rawNumber = phoneInput.value.trim();
if (rawNumber.startsWith("0")) {
rawNumber = rawNumber.substring(1);
phoneInput.value = "+233" + rawNumber;
}
if (iti.isValidNumber()) {
phoneInput.value = iti.getNumber();
}
});
}
});
let lastTrigger = null;
// When edit modal closes, return focus to trigger
$('#editUserModal').on('hidden.bs.modal', function(){
if (lastTrigger && document.contains(lastTrigger)) lastTrigger.focus();
lastTrigger = null;
});
// Submit edit (updates row inline)
$('#editFormXX').on('submit', function(e){
e.preventDefault();
const id = $('#editId').val();
const name = $('#editName').val();
const email = $('#editEmail').val();
const category = $('#editCategory').val();
const row = $(`.user-row[data-id="${id}"]`);
row.data('name', name).data('email', email).data('category', category);
row.find('.fw-semibold').text(name);
row.find('td').eq(2).text(email);
row.find('.category-badge').removeClass('badge-district badge-regional badge-national');
if (category === 'District') row.find('.category-badge').addClass('badge-district').text('District');
if (category === 'Regional') row.find('.category-badge').addClass('badge-regional').text('Regional');
if (category === 'National') row.find('.category-badge').addClass('badge-national').text('National');
const targetBody = category === 'District' ? '#districtBody' : (category === 'Regional' ? '#regionalBody' : '#nationalBody');
if (!row.parent().is(targetBody)) {
row.appendTo($(targetBody));
}
updateCounts();
$('#editModal').modal('hide');
});
var selectedValue = $("input[name='user_type']:checked").val();
const $dropdown = $("#uaPostionAdd");
const optionsMap = {
district_user: [
"PPD Head", "Works Head", "MIS",
"PPD Staff", "Works Staff",
"Devt Planning Officer",
"MCE", "DCE",
"Urban Roads Department Head",
"District Fire Officer",
"District Disaster Prevention Department",
"Head of District Health Department",
"Representative of the Lands Commission",
"Representative of the Environmental Protection Agency",
"Rep from Traditional Council",
"Chairman of the Works Sub Committee",
"Chairman of Development Sub Planning Committee",
"Nominated Elected Assembly Memebers"
],
national_luspa: ["Director", "IT Head", "Staff"],
regional_luspa: ["Director", "Staff"]
};
$dropdown.empty();
$dropdown.append('<option value="">-- Select an option --</option>');
if (optionsMap['district_user']) {
$.each(optionsMap['district_user'], function(index, value) {
$dropdown.append($("<option></option>").attr("value", value.toLowerCase()).text(value));
});
}
$("input[name='user_type']").change(function() {
var userValue = $(this).val();
if (userValue == 'district_user') {
$('#regionID').prop('disabled', false);
$('#districtID').prop('disabled', false);
$('#editRegionID').prop('disabled', false);
$('#editdistrictID').prop('disabled', false);
}
if (userValue == 'national_luspa') {
$('#regionID').prop('disabled', true);
$('#districtID').prop('disabled', true);
$('#editRegionID').prop('disabled', true);
$('#editdistrictID').prop('disabled', true);
}
if (userValue == 'regional_luspa') {
$('#districtID').prop('disabled', true);
$('#regionID').prop('disabled', false);
$('#editdistrictID').prop('disabled', true);
$('#editRegionID').prop('disabled', false);
}
$dropdown.empty();
$dropdown.append('<option value="">-- Select an option --</option>');
if (optionsMap[userValue]) {
$.each(optionsMap[userValue], function(index, value) {
$dropdown.append($("<option></option>")
.attr("value", value.toLowerCase())
.text(value));
});
}
});
$('#editAllowedApps').select2({
dropdownParent: $('#editUserModal'),
placeholder : "Select options, multiple allowed"
});
$('#allowedApps').select2({
dropdownParent: $('#addUserModal'),
placeholder : "Select options, multiple allowed"
});
$('#inputPermissions').select2({
dropdownParent: $('#editUserModal'),
placeholder : "Select options, multiple allowed"
});
$('.editUserBtn').click(function(evnt){
evnt.preventDefault();
// var selectedUserId = $(this).siblings('.userIdinput').val();
const row = $(this).closest('.user-row');
var selectedUserId = row.data('id');
console.log('selectedUserId');
lastTrigger = this;
// $('#editId').val(row.data('id'));
const formData = new FormData();
formData.append('user_id', selectedUserId);
$.ajax({
url: base_url + '/users/edit/' + selectedUserId,
type: 'GET',
processData: false,
contentType: false,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'crossDomain': false
},
beforeSend: function() {
$('#editSuccessArea').text("");
$('#editErrorArea').text("Please wait ... loading user details!!");
},
success: function(data) {
$('#editErrorArea').text('');
var jason = data.data;
if(data.success == true){
var allowedAppsArray = [];
if (jason['allowed_apps']) {
allowedAppsArray = jason['allowed_apps'].split(",");
}
$('#editFullName').val(jason['full_name']);
$('#editEmail').val(jason['email']);
$('#editUsername').val(jason['username']);
$('#editGender').val(jason['gender']);
$('#editTitle').val(jason['title']);
$('#editUaPostion').val(jason['ua_position']);
$('#editPhone').val(jason['phone']);
$('#editAllowedApps').val(allowedAppsArray).trigger('change');
$('#editRegionID').val(jason['region_id']);
$('#editDistrictId').val(jason['district_id']);
$('#editUaPostion').val(jason['ua_position']);
$('#editGender').val(jason['gender']);
$("input[name='user_id']").val(jason.user_id);
}
//$('#editUserModal').modal('show');
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#errorArea').text(error);
$('#errorArea').text(error);
}
});
});
$("#editUserForm").submit(function(evt){
evt.preventDefault();
const $successArea = $("#editSuccessArea");
const $errorArea = $("#editErrorArea");
$('#editSuccessArea').addClass('d-none');
$('#editErrorArea').removeClass('d-none');
const formData = new FormData($(this)[0]);
// formData = new FormData(this);
let errors = [];
if (iti) {
let formattedPhone = iti.getNumber();
if (formattedPhone.startsWith("+2330")) {
formattedPhone = "+233" + formattedPhone.substring(5);
}
formattedPhone = formattedPhone.trim();
if (!iti.isValidNumber()) {
errors.push("001 | Please enter a valid Ghana phone number.");
} else {
formData.set("phone", formattedPhone);
}
}
const username = $("#editUsername").val().trim();
const email = $("#editEmail").val().trim();
const fullName = $("#editFullName").val().trim();
if (!fullName) errors.push("Full name is required.");
if (!username) errors.push("Username is required.");
if (!email) {
errors.push("Email is required.");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push("Invalid email format.");
}
if (errors.length > 0) {
$('#editSuccessArea').addClass("d-none").text("");
$errorArea.removeClass("d-none").html(errors.join("<br>"));
return;
}
$.ajax({
url: base_url + '/userupdate',
type: 'POST',
data: formData,
dataType: 'json',
processData: false,
contentType: false,
beforeSend: function() {
// $('#updateBtn').addClass('d-none');
// $('#uodateProgressBtn').removeClass('d-none');
// $('#updateResultsDiv').removeClass('d-none');
// $('#updateResultsParagraph').text("Processing Please wait ...");
},
success: function(data) {
console.log(data);
console.log(data['success']);
if (data['success'] == true) {
$('#editSuccessArea').removeClass('d-none');
$('#editErrorArea').addClass('d-none');
$('#editSuccessArea').text("");
$('#editSuccessArea').text("User successfully details updated!");
$.alert({
title: 'Alert!',
content: 'User successfully details updated!!',
});
setTimeout(() => location.reload(), 2000);
}
else{
$('#editErrorArea').removeClass('d-none');
$('#editErrorArea').text(data['msg']);
$.alert({
title: 'Alert!',
content: data['msg'],
});
}
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#editErrorArea').removeClass('d-none');
$('#editErrorArea').text(error);
if (xhr.status === 422) {
// Laravel validation error
let errors = xhr.responseJSON.errors;
let messages = [];
$.each(errors, function (field, msgs) {
messages.push(msgs.join("<br>"));
});
$('#editErrorArea').removeClass("d-none").html(messages.join("<br>"));
$.alert({
title: "Validation Error",
content: messages.join("<br>"),
});
} else {
$('#editErrorArea').removeClass('d-none').text("Request failed : " + error);
$.alert({
title: "Error",
content: "Request failed: " + error,
});
}
}
});
});
$('#regionID').change(function(){
console.log('change is coming');
var options = $('#districtID');
var region_id = $('#regionID').val();
$.ajax({
url: base_url + '/admin/districts/' + region_id,
type: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'crossDomain': false
},
success: function(data) {
$.each(data['districts'], function(id, row) {
$('#districtID').append($("<option />").val(row.districtid).text(row.district_name));
});
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
});
});
$("#newUserForm").on("submit", function (evt) {
evt.preventDefault();
const $successArea = $("#newUserSuccessArea");
const $errorArea = $("#newUserErrorArea");
const formData = new FormData(this);
let errors = [];
if (iti) {
let formattedPhone = iti.getNumber(); // +233XXXXXXXXX
if (formattedPhone.startsWith("+2330")) {
formattedPhone = "233" + formattedPhone.substring(5);
}
if (!iti.isValidNumber()) {
errors.push("Please enter a valid Ghana phone number.");
} else {
formData.set("phone", formattedPhone);
}
}
const username = $("#addUserUsername").val().trim();
const email = $("#addUserEmail").val().trim();
const fullName = $("#addUserFullname").val().trim();
if (!fullName) errors.push("Full name is required.");
if (!username) errors.push("Username is required.");
if (!email) {
errors.push("Email is required.");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push("Invalid email format.");
}
if (errors.length > 0) {
$successArea.addClass("d-none").text("");
$errorArea.removeClass("d-none").html(errors.join("<br>"));
return;
}
$successArea.addClass("d-none").text("");
$errorArea.addClass("d-none").text("");
$.ajax({
url: base_url + "/users",
type: "POST",
data: formData,
processData: false,
contentType: false,
headers: {
"Accept": "application/json",
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
},
beforeSend: function () {
$successArea.removeClass("d-none").text("Please wait ... user creation in progress!");
},
success: function (data) {
if (data.success) {
$successArea.removeClass("d-none").text("User successfully created!");
$errorArea.addClass("d-none");
$.alert({
title: "Success",
content: "User successfully created!",
});
setTimeout(() => location.reload(), 2000);
} else {
$successArea.addClass("d-none");
$errorArea.removeClass("d-none").text(data.msg || "An error occurred.");
$.alert({
title: "Error",
content: data.msg || "An error occurred.",
});
}
},
error: function (xhr, status, error) {
$successArea.addClass("d-none");
if (xhr.status === 422) {
// Laravel validation error
let errors = xhr.responseJSON.errors;
let messages = [];
$.each(errors, function (field, msgs) {
messages.push(msgs.join("<br>"));
});
$errorArea.removeClass("d-none").html(messages.join("<br>"));
$.alert({
title: "Validation Error",
content: messages.join("<br>"),
});
} else {
// Other errors
$errorArea.removeClass("d-none").text("Request failed: " + error);
$.alert({
title: "Error",
content: "Request failed: " + error,
});
}
},
});
});
});

View File

@ -0,0 +1,485 @@
$(document).ready(function () {
let iti;
$('#editUserModal').on('shown.bs.modal', function () {
const phoneInput = document.querySelector("#editPhone");
if (!iti) {
iti = window.intlTelInput(phoneInput, {
initialCountry: "gh",
onlyCountries: ["gh"],
nationalMode: false,
autoPlaceholder: "polite",
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
});
phoneInput.addEventListener("blur", function () {
let rawNumber = phoneInput.value.trim();
console.log(rawNumber);
if (rawNumber.startsWith("0")) {
rawNumber = rawNumber.substring(1);
phoneInput.value = "+233" + rawNumber;
}
if (iti.isValidNumber()) {
phoneInput.value = iti.getNumber(); // clean +233XXXXXXXXX
}
});
}
});
$('#addUserModal').on('shown.bs.modal', function () {
const phoneInput = document.querySelector("#addUserphone");
if (!iti) {
iti = window.intlTelInput(phoneInput, {
initialCountry: "gh",
onlyCountries: ["gh"],
nationalMode: false,
autoPlaceholder: "polite",
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
});
phoneInput.addEventListener("blur", function () {
let rawNumber = phoneInput.value.trim();
if (rawNumber.startsWith("0")) {
rawNumber = rawNumber.substring(1);
phoneInput.value = "+233" + rawNumber;
}
if (iti.isValidNumber()) {
phoneInput.value = iti.getNumber();
}
});
}
});
let lastTrigger = null;
// $(document).on('click', '.editBtnXX', function(){
// lastTrigger = this;
// const row = $(this).closest('.user-row');
// $('#editId').val(row.data('id'));
// $('#editName').val(row.data('name'));
// $('#editEmail').val(row.data('email'));
// $('#editCategory').val(row.data('category'));
// });
// When edit modal closes, return focus to trigger
$('#editUserModal').on('hidden.bs.modal', function(){
if (lastTrigger && document.contains(lastTrigger)) lastTrigger.focus();
lastTrigger = null;
});
// Submit edit (updates row inline)
$('#editFormXX').on('submit', function(e){
e.preventDefault();
const id = $('#editId').val();
const name = $('#editName').val();
const email = $('#editEmail').val();
const category = $('#editCategory').val();
const row = $(`.user-row[data-id="${id}"]`);
row.data('name', name).data('email', email).data('category', category);
row.find('.fw-semibold').text(name);
row.find('td').eq(2).text(email);
row.find('.category-badge').removeClass('badge-district badge-regional badge-national');
if (category === 'District') row.find('.category-badge').addClass('badge-district').text('District');
if (category === 'Regional') row.find('.category-badge').addClass('badge-regional').text('Regional');
if (category === 'National') row.find('.category-badge').addClass('badge-national').text('National');
const targetBody = category === 'District' ? '#districtBody' : (category === 'Regional' ? '#regionalBody' : '#nationalBody');
if (!row.parent().is(targetBody)) {
row.appendTo($(targetBody));
}
updateCounts();
$('#editModal').modal('hide');
});
var selectedValue = $("input[name='user_type']:checked").val();
const $dropdown = $("#uaPostionAdd");
const optionsMap = {
district_user: [
"PPD Head", "Works Head", "MIS",
"PPD Staff", "Works Staff",
"Devt Planning Officer",
"MCE", "DCE",
"Urban Roads Department Head",
"District Fire Officer",
"District Disaster Prevention Department",
"Head of District Health Department",
"Representative of the Lands Commission",
"Representative of the Environmental Protection Agency",
"Rep from Traditional Council",
"Chairman of the Works Sub Committee",
"Chairman of Development Sub Planning Committee",
"Nominated Elected Assembly Memebers"
],
national_luspa: ["Director", "IT Head", "Staff"],
regional_luspa: ["Director", "Staff"]
};
$dropdown.empty();
$dropdown.append('<option value="">-- Select an option --</option>');
if (optionsMap['district_user']) {
$.each(optionsMap['district_user'], function(index, value) {
$dropdown.append($("<option></option>").attr("value", value.toLowerCase()).text(value));
});
}
$("input[name='user_type']").change(function() {
var userValue = $(this).val();
if (userValue == 'district_user') {
$('#regionID').prop('disabled', false);
$('#districtID').prop('disabled', false);
$('#editRegionID').prop('disabled', false);
$('#editdistrictID').prop('disabled', false);
}
if (userValue == 'national_luspa') {
$('#regionID').prop('disabled', true);
$('#districtID').prop('disabled', true);
$('#editRegionID').prop('disabled', true);
$('#editdistrictID').prop('disabled', true);
}
if (userValue == 'regional_luspa') {
$('#districtID').prop('disabled', true);
$('#regionID').prop('disabled', false);
$('#editdistrictID').prop('disabled', true);
$('#editRegionID').prop('disabled', false);
}
$dropdown.empty();
$dropdown.append('<option value="">-- Select an option --</option>');
if (optionsMap[userValue]) {
$.each(optionsMap[userValue], function(index, value) {
$dropdown.append($("<option></option>")
.attr("value", value.toLowerCase())
.text(value));
});
}
});
$('#editAllowedApps').select2({
dropdownParent: $('#editUserModal'),
placeholder : "Select options, multiple allowed"
});
$('#allowedApps').select2({
dropdownParent: $('#addUserModal'),
placeholder : "Select options, multiple allowed"
});
$('#inputPermissions').select2({
dropdownParent: $('#editUserModal'),
placeholder : "Select options, multiple allowed"
});
$('.editUserBtn').click(function(evnt){
evnt.preventDefault();
// var selectedUserId = $(this).siblings('.userIdinput').val();
const row = $(this).closest('.user-row');
var selectedUserId = row.data('id');
console.log('selectedUserId');
lastTrigger = this;
// $('#editId').val(row.data('id'));
const formData = new FormData();
formData.append('user_id', selectedUserId);
$.ajax({
url: base_url + '/users/edit/' + selectedUserId,
type: 'GET',
processData: false,
contentType: false,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'crossDomain': false
},
beforeSend: function() {
$('#editSuccessArea').text("");
$('#editErrorArea').text("Please wait ... loading user details!!");
},
success: function(data) {
$('#editErrorArea').text('');
var jason = data.data;
if(data.success == true){
var allowedAppsArray = [];
if (jason['allowed_apps']) {
allowedAppsArray = jason['allowed_apps'].split(",");
}
$('#editFullName').val(jason['full_name']);
$('#editEmail').val(jason['email']);
$('#editUsername').val(jason['username']);
$('#editGender').val(jason['gender']);
$('#editTitle').val(jason['title']);
$('#editUaPostion').val(jason['ua_position']);
$('#editPhone').val(jason['phone']);
$('#editAllowedApps').val(allowedAppsArray).trigger('change');
$('#editRegionID').val(jason['region_id']);
$('#editDistrictId').val(jason['district_id']);
$('#editUaPostion').val(jason['ua_position']);
$('#editGender').val(jason['gender']);
$("input[name='user_id']").val(jason.user_id);
}
//$('#editUserModal').modal('show');
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#errorArea').text(error);
$('#errorArea').text(error);
}
});
});
$("#editUserForm").submit(function(evt){
evt.preventDefault();
const $successArea = $("#editSuccessArea");
const $errorArea = $("#editErrorArea");
$('#editSuccessArea').addClass('d-none');
$('#editErrorArea').removeClass('d-none');
const formData = new FormData($(this)[0]);
// formData = new FormData(this);
let errors = [];
if (iti) {
let formattedPhone = iti.getNumber();
if (formattedPhone.startsWith("+2330")) {
formattedPhone = "+233" + formattedPhone.substring(5);
}
formattedPhone = formattedPhone.trim();
if (!iti.isValidNumber()) {
errors.push("001 | Please enter a valid Ghana phone number.");
} else {
formData.set("phone", formattedPhone);
}
}
const username = $("#editUsername").val().trim();
const email = $("#editEmail").val().trim();
const fullName = $("#editFullName").val().trim();
if (!fullName) errors.push("Full name is required.");
if (!username) errors.push("Username is required.");
if (!email) {
errors.push("Email is required.");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push("Invalid email format.");
}
if (errors.length > 0) {
$('#editSuccessArea').addClass("d-none").text("");
$errorArea.removeClass("d-none").html(errors.join("<br>"));
return;
}
$.ajax({
url: base_url + '/userupdate',
type: 'POST',
data: formData,
dataType: 'json',
processData: false,
contentType: false,
beforeSend: function() {
// $('#updateBtn').addClass('d-none');
// $('#uodateProgressBtn').removeClass('d-none');
// $('#updateResultsDiv').removeClass('d-none');
// $('#updateResultsParagraph').text("Processing Please wait ...");
},
success: function(data) {
console.log(data);
console.log(data['success']);
if (data['success'] == true) {
$('#editSuccessArea').removeClass('d-none');
$('#editErrorArea').addClass('d-none');
$('#editSuccessArea').text("");
$('#editSuccessArea').text("User successfully details updated!");
$.alert({
title: 'Alert!',
content: 'User successfully details updated!!',
});
setTimeout(() => location.reload(), 2000);
}
else{
$('#editErrorArea').removeClass('d-none');
$('#editErrorArea').text(data['msg']);
$.alert({
title: 'Alert!',
content: data['msg'],
});
}
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#editErrorArea').removeClass('d-none');
$('#editErrorArea').text(error);
if (xhr.status === 422) {
// Laravel validation error
let errors = xhr.responseJSON.errors;
let messages = [];
$.each(errors, function (field, msgs) {
messages.push(msgs.join("<br>"));
});
$('#editErrorArea').removeClass("d-none").html(messages.join("<br>"));
$.alert({
title: "Validation Error",
content: messages.join("<br>"),
});
} else {
$('#editErrorArea').removeClass('d-none').text("Request failed : " + error);
$.alert({
title: "Error",
content: "Request failed: " + error,
});
}
}
});
});
$('#regionID').change(function(){
console.log('change is coming');
var options = $('#districtID');
var region_id = $('#regionID').val();
$.ajax({
url: base_url + '/admin/districts/' + region_id,
type: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'crossDomain': false
},
success: function(data) {
$.each(data['districts'], function(id, row) {
$('#districtID').append($("<option />").val(row.districtid).text(row.district_name));
});
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
});
});
$("#newUserForm").on("submit", function (evt) {
evt.preventDefault();
const $successArea = $("#newUserSuccessArea");
const $errorArea = $("#newUserErrorArea");
const formData = new FormData(this);
let errors = [];
if (iti) {
let formattedPhone = iti.getNumber(); // +233XXXXXXXXX
if (formattedPhone.startsWith("+2330")) {
formattedPhone = "233" + formattedPhone.substring(5);
}
if (!iti.isValidNumber()) {
errors.push("Please enter a valid Ghana phone number.");
} else {
formData.set("phone", formattedPhone);
}
}
const username = $("#addUserUsername").val().trim();
const email = $("#addUserEmail").val().trim();
const fullName = $("#addUserFullname").val().trim();
if (!fullName) errors.push("Full name is required.");
if (!username) errors.push("Username is required.");
if (!email) {
errors.push("Email is required.");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push("Invalid email format.");
}
if (errors.length > 0) {
$successArea.addClass("d-none").text("");
$errorArea.removeClass("d-none").html(errors.join("<br>"));
return;
}
$successArea.addClass("d-none").text("");
$errorArea.addClass("d-none").text("");
$.ajax({
url: base_url + "/users",
type: "POST",
data: formData,
processData: false,
contentType: false,
headers: {
"Accept": "application/json",
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
},
beforeSend: function () {
$successArea.removeClass("d-none").text("Please wait ... user creation in progress!");
},
success: function (data) {
if (data.success) {
$successArea.removeClass("d-none").text("User successfully created!");
$errorArea.addClass("d-none");
$.alert({
title: "Success",
content: "User successfully created!",
});
setTimeout(() => location.reload(), 2000);
} else {
$successArea.addClass("d-none");
$errorArea.removeClass("d-none").text(data.msg || "An error occurred.");
$.alert({
title: "Error",
content: data.msg || "An error occurred.",
});
}
},
error: function (xhr, status, error) {
$successArea.addClass("d-none");
if (xhr.status === 422) {
// Laravel validation error
let errors = xhr.responseJSON.errors;
let messages = [];
$.each(errors, function (field, msgs) {
messages.push(msgs.join("<br>"));
});
$errorArea.removeClass("d-none").html(messages.join("<br>"));
$.alert({
title: "Validation Error",
content: messages.join("<br>"),
});
} else {
// Other errors
$errorArea.removeClass("d-none").text("Request failed: " + error);
$.alert({
title: "Error",
content: "Request failed: " + error,
});
}
},
});
});
});

View File

@ -0,0 +1,273 @@
$(document).ready(function () {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
'Accept': 'application/json'
}
});
const phoneInstances = {};
function initPhoneInput(selector) {
const input = document.querySelector(selector);
if (!input) return null;
// If it was already initialized, don't do it again
if (phoneInstances[selector]) return phoneInstances[selector];
phoneInstances[selector] = window.intlTelInput(input, {
initialCountry: "gh",
onlyCountries: ["gh"],
nationalMode: true, // ALLOW users to type 024... natively
autoPlaceholder: "polite",
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
});
// Auto-format on blur using the plugin's built-in engine
input.addEventListener("blur", function () {
if (phoneInstances[selector].isValidNumber()) {
// Reformats to local standard (e.g., 024 123 4567) for UI
phoneInstances[selector].setNumber(phoneInstances[selector].getNumber());
}
});
return phoneInstances[selector];
}
// Initialize inputs when their respective modals open
$('#editUserModal').on('shown.bs.modal', () => initPhoneInput("#editPhone"));
$('#addUserModal').on('shown.bs.modal', () => initPhoneInput("#addUserphone"));
$('#editAllowedApps, #inputPermissions').select2({
dropdownParent: $('#editUserModal'),
placeholder: "Select options, multiple allowed"
});
$('#allowedApps').select2({
dropdownParent: $('#addUserModal'),
placeholder: "Select options, multiple allowed"
});
const optionsMap = {
district_user: [
"PPD Head", "Works Head", "MIS", "PPD Staff", "Works Staff",
"Devt Planning Officer", "MCE", "DCE", "Urban Roads Department Head",
"District Fire Officer", "District Disaster Prevention Department",
"Head of District Health Department", "Representative of the Lands Commission",
"Representative of the Environmental Protection Agency", "Rep from Traditional Council",
"Chairman of the Works Sub Committee", "Chairman of Development Sub Planning Committee",
"Nominated Elected Assembly Memebers"
],
national_luspa: ["Director", "IT Head", "Staff"],
regional_luspa: ["Director", "Staff"]
};
function populatePositions(userType, targetDropdown) {
targetDropdown.empty().append('<option value="">-- Select an option --</option>');
if (optionsMap[userType]) {
$.each(optionsMap[userType], function(index, value) {
targetDropdown.append($("<option></option>").attr("value", value.toLowerCase()).text(value));
});
}
}
let initialUserType = $("input[name='user_type']:checked").val();
if(initialUserType) populatePositions(initialUserType, $("#uaPostionAdd"));
// Handle Radio Changes
$("input[name='user_type']").change(function() {
let userType = $(this).val();
let isDistrict = (userType === 'district_user');
let isRegional = (userType === 'regional_luspa');
$('#regionID, #editRegionID').prop('disabled', !isDistrict && !isRegional);
$('#districtID, #editdistrictID').prop('disabled', !isDistrict);
populatePositions(userType, $("#uaPostionAdd"));
});
$('.regionIDD').change(function() {
let region_id = $(this).val();
let $districtSelect = $('.districtIDD');
$districtSelect.empty().append('<option value="">-- Select District --</option>');
if(!region_id) return;
$.ajax({
url: base_url + '/admin/districts/' + region_id,
type: 'GET',
success: function(data) {
if(data.districts) {
$.each(data.districts, function(id, row) {
$districtSelect.append($("<option />").val(row.districtid).text(row.district_name));
});
}
}
});
});
let lastTrigger = null;
$(document).on('click', '.editUserBtn', function(evnt) {
evnt.preventDefault();
lastTrigger = this;
const row = $(this).closest('.user-row');
let selectedUserId = row.data('id');
// Ensure phone instance exists before trying to set data into it
let phoneIti = initPhoneInput("#editPhone");
$.ajax({
url: base_url + '/users/edit/' + selectedUserId,
type: 'GET',
beforeSend: function() {
$('#editSuccessArea').addClass('d-none');
$('#editErrorArea').removeClass('d-none').text("Please wait ... loading user details!!");
},
success: function(response) {
$('#editErrorArea').addClass('d-none').text('');
if (response.success) {
let user = response.data;
let allowedAppsArray = user.allowed_apps ? user.allowed_apps.split(",") : [];
$('#editFullName').val(user.full_name);
$('#editEmail').val(user.email);
$('#editUsername').val(user.username);
$('#editGender').val(user.gender);
$('#editTitle').val(user.title);
$('#editUaPostion').val(user.ua_position);
$('#editRegionID').val(user.region_id);
$('#editDistrictId').val(user.district_id);
$("input[name='user_id']").val(user.user_id);
$('#editAllowedApps').val(allowedAppsArray).trigger('change');
if (user.user_type === 'district_user') {
$('#editUserTypeDistrict').prop('checked', true);
}
else if(user.user_type === 'regional_luspa'){
$('#editUserTypeLuspaRegion').prop('checked', true);
}
else{
$('#editUserTypeLuspaNational').prop('checked', true);
}
if (phoneIti && user.phone) {
phoneIti.setNumber(user.phone);
} else {
$('#editPhone').val(user.phone);
}
// $('#editUserModal').modal('show')
}
},
error: function(xhr, status, error) {
$('#editErrorArea').removeClass('d-none').text("Failed to load user: " + error);
}
});
});
$('#editUserModal').on('hidden.bs.modal', function(){
if (lastTrigger && document.contains(lastTrigger)) lastTrigger.focus();
lastTrigger = null;
});
function handleFormSubmission(formElement, url, successMessage, phoneInstanceSelector) {
const $form = $(formElement);
const isEdit = formElement.id === 'editUserForm';
const prefix = isEdit ? '#edit' : '#newUser';
const $successArea = $(`${prefix}SuccessArea`);
const $errorArea = $(`${prefix}ErrorArea`);
const itiInstance = phoneInstances[phoneInstanceSelector];
$successArea.addClass('d-none').text("");
$errorArea.addClass('d-none').text("");
const formData = new FormData(formElement);
let errors = [];
// Validate Phone cleanly
if (itiInstance) {
if (!itiInstance.isValidNumber()) {
errors.push("01|Please enter a valid Ghana phone number.");
} else {
// Get the clean E.164 database format (e.g. +233241234567)
formData.set("phone", itiInstance.getNumber());
}
}
// Validate standard fields
const fullName = $form.find('input[name="full_name"], [id*="FullName"], [id*="Fullname"]').val().trim();
const username = $form.find('input[name="username"], [id*="Username"]').val().trim();
const email = $form.find('input[type="email"], [id*="Email"]').val().trim();
if (!fullName) errors.push("Full name is required.");
if (!username) errors.push("Username is required.");
if (!email) {
errors.push("Email is required.");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push("Invalid email format.");
}
if (errors.length > 0) {
$errorArea.removeClass("d-none").html(errors.join("<br>"));
return;
}
$.ajax({
url: url,
type: 'POST',
data: formData,
processData: false,
contentType: false,
beforeSend: function() {
$successArea.removeClass('d-none').addClass('text-info').text("Processing request, please wait...");
},
success: function(data) {
$successArea.removeClass('text-info');
if (data.success) {
$successArea.removeClass('d-none').text(successMessage);
$.alert({ title: 'Success!', content: successMessage });
setTimeout(() => location.reload(), 2000);
} else {
$successArea.addClass('d-none');
$errorArea.removeClass('d-none').text(data.msg || "An error occurred.");
$.alert({ title: 'Error!', content: data.msg || "An error occurred." });
}
},
error: function(xhr, status, error) {
$successArea.addClass("d-none");
if (xhr.status === 422 && xhr.responseJSON && xhr.responseJSON.errors) {
let messages = [];
$.each(xhr.responseJSON.errors, function (field, msgs) {
messages.push(msgs.join("<br>"));
});
$errorArea.removeClass("d-none").html(messages.join("<br>"));
$.alert({ title: "Validation Error", content: messages.join("<br>") });
} else {
$errorArea.removeClass("d-none").text("Request failed: " + error);
$.alert({ title: "Error", content: "Request failed: " + error });
}
}
});
}
$("#editUserForm").on("submit", function (evt) {
evt.preventDefault();
handleFormSubmission(this, base_url + '/userupdate', "User details successfully updated!", "#editPhone");
});
$("#newUserForm").on("submit", function (evt) {
evt.preventDefault();
handleFormSubmission(this, base_url + '/users', "User successfully created!", "#addUserphone");
});
});

View File

@ -0,0 +1,477 @@
$(document).ready(function () {
let iti;
$('#editUserModal').on('shown.bs.modal', function () {
const phoneInput = document.querySelector("#editPhone");
if (!iti) {
iti = window.intlTelInput(phoneInput, {
initialCountry: "gh",
onlyCountries: ["gh"],
nationalMode: false,
autoPlaceholder: "polite",
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
});
phoneInput.addEventListener("blur", function () {
let rawNumber = phoneInput.value.trim();
console.log(rawNumber);
if (rawNumber.startsWith("0")) {
rawNumber = rawNumber.substring(1);
phoneInput.value = "+233" + rawNumber;
}
if (iti.isValidNumber()) {
phoneInput.value = iti.getNumber(); // clean +233XXXXXXXXX
}
});
}
});
$('#addUserModal').on('shown.bs.modal', function () {
const phoneInput = document.querySelector("#addUserphone");
if (!iti) {
iti = window.intlTelInput(phoneInput, {
initialCountry: "gh",
onlyCountries: ["gh"],
nationalMode: false,
autoPlaceholder: "polite",
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
});
phoneInput.addEventListener("blur", function () {
let rawNumber = phoneInput.value.trim();
if (rawNumber.startsWith("0")) {
rawNumber = rawNumber.substring(1);
phoneInput.value = "+233" + rawNumber;
}
if (iti.isValidNumber()) {
phoneInput.value = iti.getNumber();
}
});
}
});
let lastTrigger = null;
// When edit modal closes, return focus to trigger
$('#editUserModal').on('hidden.bs.modal', function(){
if (lastTrigger && document.contains(lastTrigger)) lastTrigger.focus();
lastTrigger = null;
});
// Submit edit (updates row inline)
$('#editFormXX').on('submit', function(e){
e.preventDefault();
const id = $('#editId').val();
const name = $('#editName').val();
const email = $('#editEmail').val();
const category = $('#editCategory').val();
const row = $(`.user-row[data-id="${id}"]`);
row.data('name', name).data('email', email).data('category', category);
row.find('.fw-semibold').text(name);
row.find('td').eq(2).text(email);
row.find('.category-badge').removeClass('badge-district badge-regional badge-national');
if (category === 'District') row.find('.category-badge').addClass('badge-district').text('District');
if (category === 'Regional') row.find('.category-badge').addClass('badge-regional').text('Regional');
if (category === 'National') row.find('.category-badge').addClass('badge-national').text('National');
const targetBody = category === 'District' ? '#districtBody' : (category === 'Regional' ? '#regionalBody' : '#nationalBody');
if (!row.parent().is(targetBody)) {
row.appendTo($(targetBody));
}
updateCounts();
$('#editModal').modal('hide');
});
var selectedValue = $("input[name='user_type']:checked").val();
const $dropdown = $("#uaPostionAdd");
const optionsMap = {
district_user: [
"PPD Head", "Works Head", "MIS",
"PPD Staff", "Works Staff",
"Devt Planning Officer",
"MCE", "DCE",
"Urban Roads Department Head",
"District Fire Officer",
"District Disaster Prevention Department",
"Head of District Health Department",
"Representative of the Lands Commission",
"Representative of the Environmental Protection Agency",
"Rep from Traditional Council",
"Chairman of the Works Sub Committee",
"Chairman of Development Sub Planning Committee",
"Nominated Elected Assembly Memebers"
],
national_luspa: ["Director", "IT Head", "Staff"],
regional_luspa: ["Director", "Staff"]
};
$dropdown.empty();
$dropdown.append('<option value="">-- Select an option --</option>');
if (optionsMap['district_user']) {
$.each(optionsMap['district_user'], function(index, value) {
$dropdown.append($("<option></option>").attr("value", value.toLowerCase()).text(value));
});
}
$("input[name='user_type']").change(function() {
var userValue = $(this).val();
if (userValue == 'district_user') {
$('#regionID').prop('disabled', false);
$('#districtID').prop('disabled', false);
$('#editRegionID').prop('disabled', false);
$('#editdistrictID').prop('disabled', false);
}
if (userValue == 'national_luspa') {
$('#regionID').prop('disabled', true);
$('#districtID').prop('disabled', true);
$('#editRegionID').prop('disabled', true);
$('#editdistrictID').prop('disabled', true);
}
if (userValue == 'regional_luspa') {
$('#districtID').prop('disabled', true);
$('#regionID').prop('disabled', false);
$('#editdistrictID').prop('disabled', true);
$('#editRegionID').prop('disabled', false);
}
$dropdown.empty();
$dropdown.append('<option value="">-- Select an option --</option>');
if (optionsMap[userValue]) {
$.each(optionsMap[userValue], function(index, value) {
$dropdown.append($("<option></option>")
.attr("value", value.toLowerCase())
.text(value));
});
}
});
$('#editAllowedApps').select2({
dropdownParent: $('#editUserModal'),
placeholder : "Select options, multiple allowed"
});
$('#allowedApps').select2({
dropdownParent: $('#addUserModal'),
placeholder : "Select options, multiple allowed"
});
$('#inputPermissions').select2({
dropdownParent: $('#editUserModal'),
placeholder : "Select options, multiple allowed"
});
$('.editUserBtn').click(function(evnt){
evnt.preventDefault();
// var selectedUserId = $(this).siblings('.userIdinput').val();
const row = $(this).closest('.user-row');
var selectedUserId = row.data('id');
console.log('selectedUserId');
lastTrigger = this;
// $('#editId').val(row.data('id'));
const formData = new FormData();
formData.append('user_id', selectedUserId);
$.ajax({
url: base_url + '/users/edit/' + selectedUserId,
type: 'GET',
processData: false,
contentType: false,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'crossDomain': false
},
beforeSend: function() {
$('#editSuccessArea').text("");
$('#editErrorArea').text("Please wait ... loading user details!!");
},
success: function(data) {
$('#editErrorArea').text('');
var jason = data.data;
if(data.success == true){
var allowedAppsArray = [];
if (jason['allowed_apps']) {
allowedAppsArray = jason['allowed_apps'].split(",");
}
$('#editFullName').val(jason['full_name']);
$('#editEmail').val(jason['email']);
$('#editUsername').val(jason['username']);
$('#editGender').val(jason['gender']);
$('#editTitle').val(jason['title']);
$('#editUaPostion').val(jason['ua_position']);
$('#editPhone').val(jason['phone']);
$('#editAllowedApps').val(allowedAppsArray).trigger('change');
$('#editRegionID').val(jason['region_id']);
$('#editDistrictId').val(jason['district_id']);
$('#editUaPostion').val(jason['ua_position']);
$('#editGender').val(jason['gender']);
$("input[name='user_id']").val(jason.user_id);
}
//$('#editUserModal').modal('show');
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#errorArea').text(error);
$('#errorArea').text(error);
}
});
});
$("#editUserForm").submit(function(evt){
evt.preventDefault();
const $successArea = $("#editSuccessArea");
const $errorArea = $("#editErrorArea");
$('#editSuccessArea').addClass('d-none');
$('#editErrorArea').removeClass('d-none');
const formData = new FormData($(this)[0]);
// formData = new FormData(this);
let errors = [];
if (iti) {
let formattedPhone = iti.getNumber();
if (formattedPhone.startsWith("+2330")) {
formattedPhone = "+233" + formattedPhone.substring(5);
}
formattedPhone = formattedPhone.trim();
if (!iti.isValidNumber()) {
errors.push("001 | Please enter a valid Ghana phone number.");
} else {
formData.set("phone", formattedPhone);
}
}
const username = $("#editUsername").val().trim();
const email = $("#editEmail").val().trim();
const fullName = $("#editFullName").val().trim();
if (!fullName) errors.push("Full name is required.");
if (!username) errors.push("Username is required.");
if (!email) {
errors.push("Email is required.");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push("Invalid email format.");
}
if (errors.length > 0) {
$('#editSuccessArea').addClass("d-none").text("");
$errorArea.removeClass("d-none").html(errors.join("<br>"));
return;
}
$.ajax({
url: base_url + '/userupdate',
type: 'POST',
data: formData,
dataType: 'json',
processData: false,
contentType: false,
beforeSend: function() {
// $('#updateBtn').addClass('d-none');
// $('#uodateProgressBtn').removeClass('d-none');
// $('#updateResultsDiv').removeClass('d-none');
// $('#updateResultsParagraph').text("Processing Please wait ...");
},
success: function(data) {
console.log(data);
console.log(data['success']);
if (data['success'] == true) {
$('#editSuccessArea').removeClass('d-none');
$('#editErrorArea').addClass('d-none');
$('#editSuccessArea').text("");
$('#editSuccessArea').text("User successfully details updated!");
$.alert({
title: 'Alert!',
content: 'User successfully details updated!!',
});
setTimeout(() => location.reload(), 2000);
}
else{
$('#editErrorArea').removeClass('d-none');
$('#editErrorArea').text(data['msg']);
$.alert({
title: 'Alert!',
content: data['msg'],
});
}
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#editErrorArea').removeClass('d-none');
$('#editErrorArea').text(error);
if (xhr.status === 422) {
// Laravel validation error
let errors = xhr.responseJSON.errors;
let messages = [];
$.each(errors, function (field, msgs) {
messages.push(msgs.join("<br>"));
});
$('#editErrorArea').removeClass("d-none").html(messages.join("<br>"));
$.alert({
title: "Validation Error",
content: messages.join("<br>"),
});
} else {
$('#editErrorArea').removeClass('d-none').text("Request failed : " + error);
$.alert({
title: "Error",
content: "Request failed: " + error,
});
}
}
});
});
$('#regionID').change(function(){
console.log('change is coming');
var options = $('#districtID');
var region_id = $('#regionID').val();
$.ajax({
url: base_url + '/admin/districts/' + region_id,
type: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'crossDomain': false
},
success: function(data) {
$.each(data['districts'], function(id, row) {
$('#districtID').append($("<option />").val(row.districtid).text(row.district_name));
});
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
});
});
$("#newUserForm").on("submit", function (evt) {
evt.preventDefault();
const $successArea = $("#newUserSuccessArea");
const $errorArea = $("#newUserErrorArea");
const formData = new FormData(this);
let errors = [];
if (iti) {
let formattedPhone = iti.getNumber(); // +233XXXXXXXXX
if (formattedPhone.startsWith("+2330")) {
formattedPhone = "233" + formattedPhone.substring(5);
}
if (!iti.isValidNumber()) {
errors.push("Please enter a valid Ghana phone number.");
} else {
formData.set("phone", formattedPhone);
}
}
const username = $("#addUserUsername").val().trim();
const email = $("#addUserEmail").val().trim();
const fullName = $("#addUserFullname").val().trim();
if (!fullName) errors.push("Full name is required.");
if (!username) errors.push("Username is required.");
if (!email) {
errors.push("Email is required.");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push("Invalid email format.");
}
if (errors.length > 0) {
$successArea.addClass("d-none").text("");
$errorArea.removeClass("d-none").html(errors.join("<br>"));
return;
}
$successArea.addClass("d-none").text("");
$errorArea.addClass("d-none").text("");
$.ajax({
url: base_url + "/users",
type: "POST",
data: formData,
processData: false,
contentType: false,
headers: {
"Accept": "application/json",
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
},
beforeSend: function () {
$successArea.removeClass("d-none").text("Please wait ... user creation in progress!");
},
success: function (data) {
if (data.success) {
$successArea.removeClass("d-none").text("User successfully created!");
$errorArea.addClass("d-none");
$.alert({
title: "Success",
content: "User successfully created!",
});
setTimeout(() => location.reload(), 2000);
} else {
$successArea.addClass("d-none");
$errorArea.removeClass("d-none").text(data.msg || "An error occurred.");
$.alert({
title: "Error",
content: data.msg || "An error occurred.",
});
}
},
error: function (xhr, status, error) {
$successArea.addClass("d-none");
if (xhr.status === 422) {
// Laravel validation error
let errors = xhr.responseJSON.errors;
let messages = [];
$.each(errors, function (field, msgs) {
messages.push(msgs.join("<br>"));
});
$errorArea.removeClass("d-none").html(messages.join("<br>"));
$.alert({
title: "Validation Error",
content: messages.join("<br>"),
});
} else {
// Other errors
$errorArea.removeClass("d-none").text("Request failed: " + error);
$.alert({
title: "Error",
content: "Request failed: " + error,
});
}
},
});
});
});

View File

@ -0,0 +1,190 @@
$(document).ready(function(){
const baseUrl = base_url + "/permits/getcomments";
fetchPermitApplicationComments();
// CREATE
$(".submitPermitCommentBtn").click(function (e) {
e.preventDefault();
// let formData = $(this).serialize();
var commentBody = $('.commentBody').val();
var applicationCode = $('.applicationCode').val();
const formData = new FormData();
formData.append('comment_body', commentBody);
formData.append('application_code', applicationCode);
$.ajax({
url: base_url + "/permits/addcomment",
type: "POST",
data: formData,
processData: false,
contentType: false,
headers: {
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
"Accept": "application/json"
},
success: function (response) {
if (response.success === true) {
$.alert("Comment successfully submitted!");
fetchPermitApplicationComments();
}
else{
$.alert(response.msg);
}
console.log(response);
},
error: function (xhr) {
alert("Error: " + xhr.responseText);
}
});
});
// READ (fetch all)
function fetchPermitApplicationComments() {
var applicationCode = $('.applicationCode').val();
const formData = new FormData();
formData.append('application_code', applicationCode);
$.ajax({
url: baseUrl,
type: "POST",
data: formData,
processData: false,
contentType: false,
headers: {
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
"Accept": "application/json"
},
success: function (data) {
console.log("Fetched Comments:", data);
// $("#permitTableBody").empty();
populateCommentsTimeline(data)
// $.each(data, function (index, app) {
// $("#permitTableBody").append(`
// <tr>
// <td>${app.id}</td>
// <td>${app.user_id}</td>
// <td>${app.application_code}</td>
// <td>${app.comment_body ?? ""}</td>
// <td>
// <button class="editBtn" data-id="${app.id}">Edit</button>
// <button class="deleteBtn" data-id="${app.id}">Delete</button>
// </td>
// </tr>
// `);
// });
},
error: function (xhr) {
alert("Failed to fetch applications.");
}
});
}
// UPDATE
$(document).on("click", ".editBtn", function () {
let id = $(this).data("id");
let updatedData = {
comment_body: prompt("Enter new comment:")
};
$.ajax({
url: `${baseUrl}/${id}`,
type: "PUT",
data: updatedData,
headers: {
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
"Accept": "application/json"
},
success: function (response) {
alert("Application Updated!");
console.log(response);
fetchPermitApplicationComments();
},
error: function (xhr) {
alert("Update failed: " + xhr.responseText);
}
});
});
// DELETE
$(document).on("click", ".deleteBtn", function () {
let id = $(this).data("id");
if (confirm("Are you sure you want to delete this application?")) {
$.ajax({
url: `${baseUrl}/${id}`,
type: "DELETE",
headers: {
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
"Accept": "application/json"
},
success: function (response) {
alert("Application Deleted!");
console.log(response);
fetchPermitApplicationComments();
},
error: function (xhr) {
alert("Delete failed: " + xhr.responseText);
}
});
}
});
// Example AJAX setup assuming your response is stored in a variable called 'response'
function populateCommentsTimeline(response) {
const $timeline = $('#commentsTimeline');
// Clear out any old data or loading spinners
$timeline.empty();
// Check if we have a successful response with data
if (response.success && response.count > 0) {
// Loop through each comment in the data array
$.each(response.data, function(index, item) {
// Format the date to look like "19 Jun 2026, 06:55 PM"
const dateObj = new Date(item.created_date);
const formattedDate = dateObj.toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
});
// Construct the Timeline Item HTML
const timelineHtml = `
<div class="timeline-item">
<div class="timeline-icon bg-secondary text-white shadow-sm">
<i class="bi bi-chat-text"></i>
</div>
<div class="timeline-content border rounded p-3 shadow-sm mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<strong class="text-dark">${item.created_by}</strong>
<small class="text-muted">${formattedDate}</small>
</div>
<p class="mb-0 text-dark small">${item.app_comments}</p>
</div>
</div>
`;
// Append it to the container
$timeline.append(timelineHtml);
});
}
else {
// Fallback UI if there are no comments yet
$timeline.append(`
<div class="text-center text-muted mt-5">
<i class="bi bi-inbox fs-1"></i>
<p class="mt-2">No history or comments found for this application.</p>
</div>
`);
}
}
});

View File

@ -0,0 +1,186 @@
(function(){
$(document).on('keydown', function(e){
if (e.key === '/' && !$(e.target).is('input, textarea')) {
e.preventDefault();
$('#globalSearch').focus().select();
}
});
// quick loading of district/regional/national rows
function updateCounts(){
// $('#count-district').text($('#districtBody .user-row:visible').length);
// $('#count-regional').text($('#regionalBody .user-row:visible').length);
// $('#count-national').text($('#nationalBody .user-row:visible').length);
}
function getVisibleRowsInActiveTab(){
const activePane = $('.tab-pane.active');
return activePane.find('.user-row:visible');
}
// Select all visible in active tab
$('#globalSelect').on('change', function(){
const checked = $(this).is(':checked');
getVisibleRowsInActiveTab().find('.row-select').prop('checked', checked).trigger('change');
});
// Row checkbox toggles bulk buttons
$(document).on('change', '.row-select', function(){
const anyChecked = $('.tab-pane.active .row-select:checked').length > 0;
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', !anyChecked);
// sync header select state
const total = getVisibleRowsInActiveTab().find('.row-select').length;
const checked = getVisibleRowsInActiveTab().find('.row-select:checked').length;
$('#globalSelect').prop('checked', total>0 && total===checked);
});
let searchTimer;
$('#globalSearch').on('keyup', function() {
clearTimeout(searchTimer);
let query = $(this).val();
// Optional: Slightly fade the tables to show it is loading
$('#districtBody, #regionalBody, #nationalBody').css('opacity', '0.5');
// Wait 500ms after the user stops typing
searchTimer = setTimeout(function() {
$.ajax({
url: window.location.pathname, // Request the same page
type: "GET",
data: { search: query },
success: function(response) {
// Turn the returned text into a searchable DOM object
let html = $(response);
// 1. Swap the Table Bodies
$('#districtBody').html(html.find('#districtBody').html());
$('#regionalBody').html(html.find('#regionalBody').html());
$('#nationalBody').html(html.find('#nationalBody').html());
// 2. Swap the Tab Counts
$('#count-district').text(html.find('#count-district').text());
$('#count-regional').text(html.find('#count-regional').text());
$('#count-national').text(html.find('#count-national').text());
// 3. Swap the Pagination links
$('#district-pagination').html(html.find('#district-pagination').html());
$('#regional-pagination').html(html.find('#regional-pagination').html());
$('#national-pagination').html(html.find('#national-pagination').html());
// Remove the fade effect
$('#districtBody, #regionalBody, #nationalBody').css('opacity', '1');
},
error: function() {
console.error("Live search failed");
$('#districtBody, #regionalBody, #nationalBody').css('opacity', '1');
}
});
}, 500); // 500ms delay
});
// $('#globalSearch').on('input', applyFilters);
// $('.filter-status').on('change', applyFilters);
// Activate tab event -> clear globalSelect and bulk buttons
$('button[data-bs-toggle="tab"]').on('shown.bs.tab', function(){
$('#globalSelect').prop('checked', false);
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', true);
});
// Edit button opens modal and populates fields
let lastTrigger = null;
$(document).on('click', '.editBtnXX', function(){
lastTrigger = this;
const row = $(this).closest('.user-row');
$('#editId').val(row.data('id'));
$('#editName').val(row.data('name'));
$('#editEmail').val(row.data('email'));
$('#editCategory').val(row.data('category'));
});
// Bulk move: gather selected rows and show count
$('#bulkMoveBtn').on('click', function(){
const selected = $('.tab-pane.active .row-select:checked');
$('#moveCount').text(selected.length);
// focus first control in modal for accessibility
$('#moveModal').on('shown.bs.modal', function(){ $('#moveTarget').focus(); });
});
// Remember trigger to restore focus after move modal
$('#moveModal').on('hidden.bs.modal', function(){ $('.tab-pane.active').find('.row-select:checked').first().closest('tr').find('.row-select').focus(); });
// Execute move
$('#moveForm').on('submit', function(e){
e.preventDefault();
const target = $('#moveTarget').val();
const selected = $('.tab-pane.active .row-select:checked').closest('.user-row');
selected.each(function(){
const $r = $(this);
$r.data('category', target);
$r.find('.category-badge').removeClass('badge-district badge-regional badge-national');
if (target === 'District') $r.find('.category-badge').addClass('badge-district').text('District');
if (target === 'Regional') $r.find('.category-badge').addClass('badge-regional').text('Regional');
if (target === 'National') $r.find('.category-badge').addClass('badge-national').text('National');
const targetBody = target === 'District' ? '#districtBody' : (target === 'Regional' ? '#regionalBody' : '#nationalBody');
$r.appendTo($(targetBody));
$r.find('.row-select').prop('checked', false);
});
$('#moveModal').modal('hide');
$('#globalSelect').prop('checked', false);
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', true);
updateCounts();
});
// Bulk deactivate demo (toggles status)
$('#bulkDeactivate').on('click', function(){
const selected = $('.tab-pane.active .row-select:checked').closest('.user-row');
selected.each(function(){
const $r = $(this);
$r.data('status', 'Suspended');
// add visual cue
$r.addClass('table-danger');
setTimeout(()=> $r.removeClass('table-danger'), 1200);
$r.find('.row-select').prop('checked', false);
});
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', true);
});
// ensure focus is moved first.
// Update counts initially
updateCounts();
// Demo: focus first edit button when pressing 'e' (example of keyboard shortcut)
$(document).on('keydown', function(e){
if (e.key === 'e' && !$(e.target).is('input, textarea')) {
e.preventDefault();
$('.editBtn').first().focus();
}
});
$('#editRegionID').change(function(){
console.log('change is coming');
var options = $('#editDistrictID');
var region_id = $('#editRegionID').val();
$.ajax({
url: base_url + '/admin/districts/' + region_id,
type: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'crossDomain': false
},
success: function(data) {
$.each(data['districts'], function(id, row) {
$('#editDistrictID').append($("<option />").val(row.districtid).text(row.district_name));
});
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
});
});
})();

View File

@ -0,0 +1,436 @@
$(document).ready(function(){
console.log('foo');
// document.addEventListener("DOMContentLoaded", function () {
// const phoneInput = document.querySelector("#newUserPhone");
// const iti = window.intlTelInput(phoneInput, {
// initialCountry: "gh",
// onlyCountries: ["gh"],
// nationalMode: false,
// utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
// });
// Optional: validate before form submit
// $("#newUserForm").on("submit", function (evt) {
// if (!iti.isValidNumber()) {
// evt.preventDefault();
// $("#newUserErrorArea")
// .removeClass("d-none")
// .text("Please enter a valid Ghana phone number.");
// }
// });
// });
$('#addUserModal').on('shown.bs.modal', function () {
const phoneInput = document.querySelector("#phone");
window.intlTelInput(phoneInput, {
initialCountry: "gh",
onlyCountries: ["gh"],
nationalMode: false,
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
});
});
var selectedValue = $("input[name='user_type']:checked").val();
const $dropdown = $("#uaPostionAdd");
const optionsMap = {
district_user: [
"PPD Head", "Works Head", "MIS",
"PPD Staff", "Works Staff",
"Devt Planning Officer",
"MCE", "DCE",
"Urban Roads Department Head",
"District Fire Officer",
"District Disaster Prevention Department",
"Head of District Health Department",
"Representative of the Lands Commission",
"Representative of the Environmental Protection Agency",
"Rep from Traditional Council",
"Chairman of the Works Sub Committee",
"Chairman of Development Sub Planning Committee",
"Nominated Elected Assembly Memebers"
],
national_luspa: ["Director", "IT Head", "Staff"],
regional_luspa: ["Director", "Staff"]
};
$dropdown.empty();
$dropdown.append('<option value="">-- Select an option --</option>');
if (optionsMap['district_user']) {
$.each(optionsMap['district_user'], function(index, value) {
$dropdown.append($("<option></option>").attr("value", value.toLowerCase()).text(value));
});
}
$("input[name='user_type']").change(function() {
var userValue = $(this).val();
if (userValue == 'district_user') {
$('#regionID').prop('disabled', false);
$('#districtID').prop('disabled', false);
}
if (userValue == 'national_luspa') {
$('#regionID').prop('disabled', true);
$('#districtID').prop('disabled', true);
}
if (userValue == 'regional_luspa') {
$('#districtID').prop('disabled', true);
$('#regionID').prop('disabled', false);
}
$dropdown.empty();
$dropdown.append('<option value="">-- Select an option --</option>');
if (optionsMap[userValue]) {
$.each(optionsMap[userValue], function(index, value) {
$dropdown.append($("<option></option>")
.attr("value", value.toLowerCase())
.text(value));
});
}
});
$('#editAllowedApps').select2({
dropdownParent: $('#editUserModal'),
placeholder : "Select options, multiple allowed"
});
$('#allowedApps').select2({
dropdownParent: $('#addUserModal'),
placeholder : "Select options, multiple allowed"
});
$('#inputPermissions').select2({
dropdownParent: $('#editUserModal'),
placeholder : "Select options, multiple allowed"
});
$('.editUserBtn').click(function(evnt){
evnt.preventDefault();
var selectedUserId = $(this).siblings('.userIdinput').val();
const formData = new FormData();
formData.append('user_id', selectedUserId);
$.ajax({
url: base_url + '/users/edit/' + selectedUserId,
type: 'GET',
processData: false,
contentType: false,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'crossDomain': false
},
beforeSend: function() {
$('#editSuccessArea').text("");
$('#editErrorArea').text("Please wait ... loading user details!!");
},
success: function(data) {
$('#editErrorArea').text('');
var jason = data.data;
if(data.success == true){
var allowedAppsArray = [];
if (jason['allowed_apps']) {
allowedAppsArray = jason['allowed_apps'].split(",");
}
$('#editFullName').val(jason['full_name']);
$('#editEmail').val(jason['email']);
$('#editUsername').val(jason['username']);
$('#editGender').val(jason['gender']);
$('#editTitle').val(jason['title']);
$('#editUaPostion').val(jason['ua_position']);
$('#editPhone').val(jason['phone']);
$('#editAllowedApps').val(allowedAppsArray).trigger('change');
$('#editRegionID').val(jason['region_id']);
$('#editDistrictId').val(jason['district_id']);
$('#editUaPostion').val(jason['ua_position']);
$('#editGender').val(jason['gender']);
$("input[name='user_id']").val(jason.user_id);
}
//$('#editUserModal').modal('show');
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#errorArea').text(error);
$('#errorArea').text(error);
}
});
});
$('.viewUserBtn').click(function(evnt){
evnt.preventDefault();
var selectedUserId = $(this).siblings('.userIdinput').val();
const formData = new FormData();
formData.append('user_id', selectedUserId);
$.ajax({
url: base_url + '/users/' + selectedUserId,
type: 'GET',
processData: false,
contentType: false,
beforeSend: function() {
$('#viewSuccessArea').text("");
$('#viewErrorArea').text("Please wait ... loading user details!");
},
success: function(data) {
var jason = data.data;
if(data.success == true){
var allowedAppsArray = [];
if (jason['allowed_apps']) {
allowedAppsArray = jason['allowed_apps'].split(",");
}
console.log(jason['full_name']);
$('#viewFullName').val(jason['full_name']);
$('#viewEmail').val(jason['email']);
$('#viewUsername').val(jason['username']);
$('#viewGender').val(jason['gender']);
$('#viewTitle').val(jason['title']);
$('#viewUaPostion').val(jason['ua_position']);
$('#viewPhone').val(jason['phone']);
$('#viewAllowedApps').val(allowedAppsArray).trigger('change');
$('#viewRegionID').val(jason['region_id']);
$('#viewDistrictId').val(jason['district_id']);
$('#viewUaPostion').val(jason['ua_position']);
$('#viewGender').val(jason['gender']);
$("input[name='user_id']").val(jason.ua_id);
}
//$('#editUserModal').modal('show');
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#errorArea').text(error);
$('#errorArea').text(error);
}
});
});
$("#newUserFormXX").submit(function(evt){
evt.preventDefault();
$('#newUserSuccessArea').addClass('d-none');
$('#newUserErrorsArea').removeClass('d-none');
var formData = new FormData($(this)[0]);
$.ajax({
url: base_url + '/users',
type: 'POST',
data: formData,
processData: false,
contentType: false,
beforeSend: function() {
$('#newUserSuccessArea').text("");
$('#newUserSuccessArea').text("Please wait ... user creation in progress!");
},
success: function(data) {
console.log(data);
if (data['success'] == true) {
$('#newUserSuccessArea').removeClass('d-none');
$('#newUserErrorsArea').addClass('d-none');
$('#newUserSuccessArea').text("");
$('#newUserSuccessArea').text("User successfully created!");
$.alert({
title: 'Alert!',
content: 'User successfully created!',
});
setTimeout(function() {
location.reload(); // Reloads the current page
}, 2000);
}
else{
$('#newUserSuccessArea').addClass('d-none');
$('#newUserErrorsArea').removeClass('d-none');
$('#newUserErrorsArea').text("");
$('#newUserErrorsArea').text(data['msg']);
$.alert({
title: 'Alert!',
content: data['msg'],
});
}
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#newUserSuccessArea').text(error);
$('#newUserSuccessArea').text(error);
}
});
});
$("#newUserForm").on("submit", function (evt) {
evt.preventDefault();
const $successArea = $("#newUserSuccessArea");
const $errorArea = $("#newUserErrorArea"); // corrected selector
const formData = new FormData(this);
// Example validation rules
const username = $("#username").val().trim();
const email = $("#email").val().trim();
const fullName = $("#fullName").val().trim();
let errors = [];
if (!fullName) {
errors.push("Full name is required.");
}
if (!username) {
errors.push("Username is required.");
}
if (!email) {
errors.push("Email is required.");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push("Invalid email format.");
}
if (errors.length > 0) {
$successArea.addClass("d-none").text("");
$errorArea.removeClass("d-none").html(errors.join("<br>"));
return; // Stop submission if validation fails
}
// Reset messages
$successArea.addClass("d-none").text("");
$errorArea.addClass("d-none").text("");
$.ajax({
url: base_url + "/users",
type: "POST",
data: formData,
processData: false,
contentType: false,
beforeSend: function () {
$successArea.removeClass("d-none").text("Please wait ... user creation in progress!");
},
success: function (data) {
if (data.success) {
$successArea.removeClass("d-none").text("User successfully created!");
$errorArea.addClass("d-none");
$.alert({
title: "Success",
content: "User successfully created!",
});
setTimeout(() => location.reload(), 2000);
} else {
$successArea.addClass("d-none");
$errorArea.removeClass("d-none").text(data.msg || "An error occurred.");
$.alert({
title: "Error",
content: data.msg || "An error occurred.",
});
}
},
error: function (xhr, status, error) {
$successArea.addClass("d-none");
$errorArea.removeClass("d-none").text("Request failed: " + error);
$.alert({
title: "Error",
content: "Request failed: " + error,
});
},
});
});
$("#editUserForm").submit(function(evt){
evt.preventDefault();
$('#successArea').addClass('d-none');
$('#errorsArea').removeClass('d-none');
var formData = new FormData($(this)[0]);
$.ajax({
url: base_url + '/userupdate',
type: 'POST',
data: formData,
dataType: 'json',
processData: false,
contentType: false,
beforeSend: function() {
// $('#updateBtn').addClass('d-none');
// $('#uodateProgressBtn').removeClass('d-none');
// $('#updateResultsDiv').removeClass('d-none');
// $('#updateResultsParagraph').text("Processing Please wait ...");
},
success: function(data) {
console.log(data);
console.log(data['success']);
if (data['success'] == true) {
$('#editSuccessArea').removeClass('d-none');
$('#editErrorArea').addClass('d-none');
$('#editSuccessArea').text("");
$('#editSuccessArea').text("User successfully details updated!");
$.alert({
title: 'Alert!',
content: 'User successfully details updated!!',
});
}
else{
$('#editErrorArea').removeClass('d-none');
$('#editErrorArea').text(data['msg']);
$.alert({
title: 'Alert!',
content: data['msg'],
});
}
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#editErrorArea').removeClass('d-none');
$('#editErrorArea').text(error);
// location.reload();
$.alert({
title: 'Alert!',
content: error,
});
}
});
});
$('#regionID').change(function(){
var options = $('#districtID');
var region_id = $('#regionID').val();
// $.get( base_url + '/admin/districts/' + region_id, function (data) {
// $('#districtID').empty();
// $.each(data['districts'], function(id, row) {
// $('#districtID').append($("<option />").val(row.districtid).text(row.district_name));
// });
// });
$.ajax({
url: base_url + '/admin/districts/' + region_id,
type: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'crossDomain': false
},
success: function(data) {
// console.log('Success:', data);
$.each(data['districts'], function(id, row) {
// console.log(row);
$('#districtID').append($("<option />").val(row.districtid).text(row.district_name));
});
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
});
});
});

View File

@ -1,5 +1,165 @@
$(document).ready(function () { $(document).ready(function () {
let iti;
$('#addUserModal').on('shown.bs.modal', function () {
const phoneInput = document.querySelector("#addUserphone");
if (!iti) {
iti = window.intlTelInput(phoneInput, {
initialCountry: "gh",
onlyCountries: ["gh"],
nationalMode: false,
autoPlaceholder: "polite",
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
});
phoneInput.addEventListener("blur", function () {
let rawNumber = phoneInput.value.trim();
if (rawNumber.startsWith("0")) {
rawNumber = rawNumber.substring(1);
phoneInput.value = "+233" + rawNumber;
}
if (iti.isValidNumber()) {
phoneInput.value = iti.getNumber(); // clean +233XXXXXXXXX
}
});
}
});
$('#editUserModal').on('shown.bs.modal', function () {
const phoneInput = document.querySelector("#editPhone");
if (!iti) {
iti = window.intlTelInput(phoneInput, {
initialCountry: "gh",
onlyCountries: ["gh"],
nationalMode: false,
autoPlaceholder: "polite",
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"
});
phoneInput.addEventListener("blur", function () {
let rawNumber = phoneInput.value.trim();
if (rawNumber.startsWith("0")) {
rawNumber = rawNumber.substring(1);
phoneInput.value = "+233" + rawNumber;
}
if (iti.isValidNumber()) {
phoneInput.value = iti.getNumber(); // clean +233XXXXXXXXX
}
});
}
});
$("#newUserForm").on("submit", function (evt) {
evt.preventDefault();
const $successArea = $("#newUserSuccessArea");
const $errorArea = $("#newUserErrorArea");
const formData = new FormData(this);
let errors = [];
if (iti) {
let formattedPhone = iti.getNumber(); // +233XXXXXXXXX
if (formattedPhone.startsWith("+2330")) {
formattedPhone = "233" + formattedPhone.substring(5);
}
if (!iti.isValidNumber()) {
errors.push("Please enter a valid Ghana phone number.");
} else {
formData.set("phone", formattedPhone);
}
}
const username = $("#addUserUsername").val().trim();
const email = $("#addUserEmail").val().trim();
const fullName = $("#addUserFullname").val().trim();
if (!fullName) errors.push("Full name is required.");
if (!username) errors.push("Username is required.");
if (!email) {
errors.push("Email is required.");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push("Invalid email format.");
}
if (errors.length > 0) {
$successArea.addClass("d-none").text("");
$errorArea.removeClass("d-none").html(errors.join("<br>"));
return;
}
$successArea.addClass("d-none").text("");
$errorArea.addClass("d-none").text("");
$.ajax({
url: base_url + "/users",
type: "POST",
data: formData,
processData: false,
contentType: false,
headers: {
"Accept": "application/json",
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
},
beforeSend: function () {
$successArea.removeClass("d-none").text("Please wait ... user creation in progress!");
},
success: function (data) {
if (data.success) {
$successArea.removeClass("d-none").text("User successfully created!");
$errorArea.addClass("d-none");
$.alert({
title: "Success",
content: "User successfully created!",
});
setTimeout(() => location.reload(), 2000);
} else {
$successArea.addClass("d-none");
$errorArea.removeClass("d-none").text(data.msg || "An error occurred.");
$.alert({
title: "Error",
content: data.msg || "An error occurred.",
});
}
},
error: function (xhr, status, error) {
$successArea.addClass("d-none");
if (xhr.status === 422) {
// Laravel validation error
let errors = xhr.responseJSON.errors;
let messages = [];
$.each(errors, function (field, msgs) {
messages.push(msgs.join("<br>"));
});
$errorArea.removeClass("d-none").html(messages.join("<br>"));
$.alert({
title: "Validation Error",
content: messages.join("<br>"),
});
} else {
// Other errors
$errorArea.removeClass("d-none").text("Request failed: " + error);
$.alert({
title: "Error",
content: "Request failed: " + error,
});
}
},
});
});
var selectedValue = $("input[name='user_type']:checked").val(); var selectedValue = $("input[name='user_type']:checked").val();
const $dropdown = $("#uaPostionAdd"); const $dropdown = $("#uaPostionAdd");
@ -39,16 +199,25 @@ $(document).ready(function(){
$('#regionID').prop('disabled', false); $('#regionID').prop('disabled', false);
$('#districtID').prop('disabled', false); $('#districtID').prop('disabled', false);
$('#editRegionID').prop('disabled', false);
$('#editdistrictID').prop('disabled', false);
} }
if (userValue == 'national_luspa') { if (userValue == 'national_luspa') {
$('#regionID').prop('disabled', true); $('#regionID').prop('disabled', true);
$('#districtID').prop('disabled', true); $('#districtID').prop('disabled', true);
$('#editRegionID').prop('disabled', true);
$('#editdistrictID').prop('disabled', true);
} }
if (userValue == 'regional_luspa') { if (userValue == 'regional_luspa') {
$('#districtID').prop('disabled', true); $('#districtID').prop('disabled', true);
$('#regionID').prop('disabled', false); $('#regionID').prop('disabled', false);
$('#editdistrictID').prop('disabled', true);
$('#editRegionID').prop('disabled', false);
} }
$dropdown.empty(); $dropdown.empty();
@ -187,62 +356,48 @@ $(document).ready(function(){
}); });
$("#newUserForm").submit(function(evt){
$("#editForm").submit(function(evt){
evt.preventDefault(); evt.preventDefault();
$('#successArea').addClass('d-none'); const $successArea = $("#editSuccessArea");
$('#errorsArea').removeClass('d-none'); const $errorArea = $("#editErrorArea");
var formData = new FormData($(this)[0]); $('#editSuccessArea').addClass('d-none');
$('#editErrorArea').removeClass('d-none');
$.ajax({ const formData = new FormData($(this)[0]);
url: base_url + '/users', // formData = new FormData(this);
type: 'POST', let errors = [];
data: formData, if (iti) {
processData: false, let formattedPhone = iti.getNumber(); // +233XXXXXXXXX
contentType: false, if (formattedPhone.startsWith("+2330")) {
beforeSend: function() { formattedPhone = "233" + formattedPhone.substring(5);
$('#successArea').text("");
$('#successArea').text("Please wait ... user creation in progress!");
},
success: function(data) {
if (data['success'] == true) {
$('#successArea').removeClass('d-none');
$('#errorsArea').addClass('d-none');
$('#successArea').text("");
$('#successArea').text("User successfully created!");
$.alert({
title: 'Alert!',
content: 'User successfully created!',
});
setTimeout(function() {
location.reload(); // Reloads the current page
}, 2000);
} }
else{
$('#successArea').addClass('d-none');
$('#errorArea').removeClass('d-none');
$('#errorArea').text("");
$('#errorArea').text("User could not be created!");
$.alert({
title: 'Alert!',
content: 'User could not be created!',
});
}
},
error: function(xhr, status, error) {
console.error('Error:', error);
$('#successArea').text(error);
$('#successArea').text(error);
}
});
});
$("#editUserForm").submit(function(evt){ if (!iti.isValidNumber()) {
evt.preventDefault(); errors.push("001 | Please enter a valid Ghana phone number.");
$('#successArea').addClass('d-none'); } else {
$('#errorsArea').removeClass('d-none'); formData.set("phone", formattedPhone);
var formData = new FormData($(this)[0]); }
}
const username = $("#editUsername").val().trim();
const email = $("#editEmail").val().trim();
const fullName = $("#editFullName").val().trim();
if (!fullName) errors.push("Full name is required.");
if (!username) errors.push("Username is required.");
if (!email) {
errors.push("Email is required.");
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.push("Invalid email format.");
}
if (errors.length > 0) {
$('#editSuccessArea').addClass("d-none").text("");
$errorArea.removeClass("d-none").html(errors.join("<br>"));
return;
}
$.ajax({ $.ajax({
url: base_url + '/userupdate', url: base_url + '/userupdate',
type: 'POST', type: 'POST',
@ -268,6 +423,7 @@ $(document).ready(function(){
title: 'Alert!', title: 'Alert!',
content: 'User successfully details updated!!', content: 'User successfully details updated!!',
}); });
setTimeout(() => location.reload(), 2000);
} }
else{ else{
$('#editErrorArea').removeClass('d-none'); $('#editErrorArea').removeClass('d-none');
@ -282,24 +438,38 @@ $(document).ready(function(){
console.error('Error:', error); console.error('Error:', error);
$('#editErrorArea').removeClass('d-none'); $('#editErrorArea').removeClass('d-none');
$('#editErrorArea').text(error); $('#editErrorArea').text(error);
// location.reload();
$.alert({ if (xhr.status === 422) {
title: 'Alert!', // Laravel validation error
content: error, let errors = xhr.responseJSON.errors;
let messages = [];
$.each(errors, function (field, msgs) {
messages.push(msgs.join("<br>"));
}); });
$('#editErrorArea').removeClass("d-none").html(messages.join("<br>"));
$.alert({
title: "Validation Error",
content: messages.join("<br>"),
});
} else {
$('#editErrorArea').removeClass('d-none').text("Request failed : " + error);
$.alert({
title: "Error",
content: "Request failed: " + error,
});
}
} }
}); });
}); });
$('#regionID').change(function(){ $('#regionID').change(function(){
console.log('change is coming');
var options = $('#districtID'); var options = $('#districtID');
var region_id = $('#regionID').val(); var region_id = $('#regionID').val();
// $.get( base_url + '/admin/districts/' + region_id, function (data) {
// $('#districtID').empty();
// $.each(data['districts'], function(id, row) {
// $('#districtID').append($("<option />").val(row.districtid).text(row.district_name));
// });
// });
$.ajax({ $.ajax({
url: base_url + '/admin/districts/' + region_id, url: base_url + '/admin/districts/' + region_id,
type: 'GET', type: 'GET',
@ -308,9 +478,7 @@ $(document).ready(function(){
'crossDomain': false 'crossDomain': false
}, },
success: function(data) { success: function(data) {
// console.log('Success:', data);
$.each(data['districts'], function(id, row) { $.each(data['districts'], function(id, row) {
// console.log(row);
$('#districtID').append($("<option />").val(row.districtid).text(row.district_name)); $('#districtID').append($("<option />").val(row.districtid).text(row.district_name));
}); });
}, },
@ -319,4 +487,7 @@ $(document).ready(function(){
} }
}); });
}); });
}); });

View File

@ -56,9 +56,7 @@
<input type="hidden" class="userIdinput" value="<?php echo $row['user_id'] ?>" name="userId"> <input type="hidden" class="userIdinput" value="<?php echo $row['user_id'] ?>" name="userId">
<!-- <a href="" data-bs-toggle="modal" data-bs-target="#viewUserModal" ><img src="{{ url('public/assets/libs/bootstrap-icons/eye.svg') }}" alt="view icon" width="16" height="16"></a> --> <!-- <a href="" data-bs-toggle="modal" data-bs-target="#viewUserModal" ><img src="{{ url('public/assets/libs/bootstrap-icons/eye.svg') }}" alt="view icon" width="16" height="16"></a> -->
<a href="" class="editUserBtn" data-bs-toggle="modal" data-bs-target="#editUserModal"> <a href="" class="editUserBtn" data-bs-toggle="modal" data-bs-target="#editUserModal"> <i class="bi bi-pen"></i> </a>
<img src="{{ url('public/assets/libs/bootstrap-icons/pencil-square.svg') }}" alt="edit icon" width="16" height="16">
</a>
</td> </td>
</tr> </tr>
<?php endforeach ?> <?php endforeach ?>
@ -81,6 +79,4 @@
<script src="{{ url('public/assets/libs/select2/dist/js/select2.full.min.js') }}" type="text/javascript" ></script> <script src="{{ url('public/assets/libs/select2/dist/js/select2.full.min.js') }}" type="text/javascript" ></script>
<script src="{{ url('public/assets/js/usermgt.js') }}" type="text/javascript" ></script> <script src="{{ url('public/assets/js/usermgt.js') }}" type="text/javascript" ></script>
@endsection @endsection

View File

@ -0,0 +1,286 @@
@extends('layouts.master')
@section('page-title')
Admin | {{ $page_title }}
@endsection
@section('page-css')
<?php //dd(session('current_user')); ?>
<link rel="stylesheet" href="{{ url('public/assets/libs/select2/dist/css/select2.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/css/intlTelInput.css"/>
<style>
/* Small visual tweaks */
.category-badge { font-weight: 600; color: #fff; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: .8rem; }
.badge-district { background:#f6a623; } /* amber */
.badge-regional { background:#17a2b8; } /* teal */
.badge-national { background:#1f6feb; } /* blue */
.user-row:hover .row-actions { visibility: visible; opacity: 1; }
.row-actions { visibility: hidden; opacity: 0; transition: .12s ease-in-out; }
.table-avatar { width:36px; height:36px; object-fit:cover; border-radius:50%; }
.toolbar { gap:.5rem; display:flex; flex-wrap:wrap; align-items:center; }
.select-all { margin-right:.5rem; }
.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; }
.status-active { background-color: #28a745; }
.status-suspended { background-color: #dc3545; }
@media (max-width: 767px) {
.toolbar { flex-direction:column; align-items:stretch; }
}
</style>
@endsection
@section('page-content')
@include('admin.partials.create-user')
@include('admin.partials.edit-user')
@include('admin.partials.edit-modal')
@include('admin.partials.move-user')
@include('admin.partials.view-user')
@include('layouts.partials.navbar')
<div class="container py-4">
<div class="d-flex align-items-center mb-3">
<h3 class="me-auto">Manage Users</h3>
<div class="text-muted">{{ \Carbon\Carbon::now()->format('F j, Y') }}</div>
</div>
<ul class="nav nav-tabs mb-3" id="userTabs" role="tablist">
@if (in_array(session('current_user.user_type'), ['district_user', 'regional_luspa', 'national_luspa']))
<li class="nav-item" role="presentation">
<button class="nav-link active" id="district-tab" data-bs-toggle="tab" data-bs-target="#district" type="button" role="tab" aria-controls="district" aria-selected="true">
District <span class="badge bg-secondary ms-1" id="count-district">{{ $districtUsers->count() }}</span>
</button>
</li>
@endif
@if (in_array(session('current_user.user_type'), ['regional_luspa', 'national_luspa']))
<li class="nav-item" role="presentation">
<button class="nav-link" id="regional-tab" data-bs-toggle="tab" data-bs-target="#regional" type="button" role="tab" aria-controls="regional" aria-selected="false">
Regional <?php //dump($regionalUsers->count()); ?> <span class="badge bg-secondary ms-1" id="count-regional">{{ $regionalUsers->count() }}</span>
</button>
</li>
@endif
@if (in_array(session('current_user.user_type'), ['national_luspa']))
<li class="nav-item" role="presentation">
<button class="nav-link" id="national-tab" data-bs-toggle="tab" data-bs-target="#national" type="button" role="tab" aria-controls="national" aria-selected="false">
National <span class="badge bg-secondary ms-1" id="count-national">{{ $nationalUsers->count() }}</span>
</button>
</li>
@endif
</ul>
<div class="card mb-3">
<div class="card-body">
<div class="toolbar">
<div class="input-group" style="max-width:420px;">
<span class="input-group-text">Search</span>
<input id="globalSearch" class="form-control" type="search" placeholder="Search name, email" aria-label="Search users">
</div>
<div class="ms-auto d-flex gap-2">
<!-- <button id="addUserBtn" class="btn btn-outline-success" disabled>Add User</button> -->
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#addUserModal">
<i class="bi bi-plus-circle"></i> Add User</button>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Filter</button>
<ul class="dropdown-menu p-3" style="min-width:220px;">
<div class="mb-2"><strong>Status</strong></div>
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Active" id="fActive"><label class="form-check-label" for="fActive">Active</label></div>
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Inactive" id="fSuspended"><label class="form-check-label" for="fSuspended">Inactive</label></div>
</ul>
</div>
<button id="bulkMoveBtn" class="btn btn-primary" disabled data-bs-toggle="modal" data-bs-target="#moveModal">Move</button>
<button id="bulkDeactivate" class="btn btn-outline-danger" disabled>Suspend</button>
</div>
</div>
</div>
</div>
<div class="tab-content" id="userTabsContent">
@if (in_array(session('current_user.user_type'), ['district_user', 'regional_luspa', 'national_luspa']))
<div class="tab-pane fade show active" id="district" role="tabpanel" aria-labelledby="district-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Allowed Apps</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="districtBody">
@forelse($districtUsers as $user)
@php $status = ($user['is_disabled'] === 'true' || $user['is_disabled'] === true) ? 'Inactive' : 'Active'; @endphp
<tr class="user-row" data-category="District" data-status="{{ $status }}" data-name="{{ $user['full_name'] }}" data-email="{{ $user['email'] }}" data-id="{{ $user['user_id'] }}">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select {{ $user['full_name'] }}"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://ui-avatars.com/api/?name={{ urlencode($user['full_name']) }}&background=random" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">{{ $user['title'] }} {{ $user['full_name'] }}</div>
<div class="text-muted small">Username: {{ $user['username'] }}</div>
</div>
</div>
</td>
<td class="align-middle">{{ $user['email'] }}</td>
<td class="align-middle"><span class="category-badge badge-district">{{ ucwords(str_replace('-', ' ', $user['ua_position'])) }}</span></td>
<td class="align-middle">
<span class="status-dot {{ $status === 'Active' ? 'status-active' : 'status-suspended' }}"></span> {{ $status }}
</td>
<td class="align-middle">{{ $user['allowed_apps'] }}</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="{{ $user['user_id'] }}"><i class="bi bi-eye"></i> View</button>
<button class="btn btn-sm btn-primary editUserBtn" data-id="{{ $user['user_id'] }}" data-bs-toggle="modal" data-bs-target="#editUserModal"><i class="bi bi-pen"></i> Edit</button>
</div>
</td>
</tr>
@empty
<tr><td colspan="7" class="text-center text-muted py-4">No district users found.</td></tr>
@endforelse
</tbody>
</table>
<div id="district-pagination" class="mt-3 px-3 pb-3">
{{ $districtUsers->links() }}
</div>
</div>
</div>
</div>
</div>
@endif
@if (in_array(session('current_user.user_type'), ['regional_luspa', 'national_luspa']))
<div class="tab-pane fade" id="regional" role="tabpanel" aria-labelledby="regional-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Person</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Allowed Apps</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="regionalBody">
@forelse($regionalUsers as $user)
<?php ?>
@php $status = ($user['is_disabled'] === 'true' || $user['is_disabled'] === true) ? 'Inactive' : 'Active'; @endphp
<tr class="user-row" data-category="Regional" data-status="{{ $status }}" data-name="{{ $user['full_name'] }}" data-email="{{ $user['email'] }}" data-id="{{ $user['user_id'] }}">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select {{ $user['full_name'] }}"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://ui-avatars.com/api/?name={{ urlencode($user['full_name']) }}&background=random" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">{{ $user['title'] }} {{ $user['full_name'] }}</div>
<div class="text-muted small">Username: {{ $user['username'] }}</div>
</div>
</div>
</td>
<td class="align-middle">{{ $user['email'] }}</td>
<td class="align-middle"><span class="category-badge badge-regional">{{ ucwords(str_replace('-', ' ', $user['ua_position'])) }}</span></td>
<td class="align-middle">
<span class="status-dot {{ $status === 'Active' ? 'status-active' : 'status-suspended' }}"></span> {{ $status }}
</td>
<td class="align-middle">{{ $user['allowed_apps'] }}</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="{{ $user['user_id'] }}"><i class="bi-eye"></i> View</button>
<button class="btn btn-sm btn-primary editUserBtn" data-id="{{ $user['user_id'] }}" data-bs-toggle="modal" data-bs-target="#editUserModal"><i class="bi-pen"></i> Edit</button>
</div>
</td>
</tr>
@empty
<tr><td colspan="7" class="text-center text-muted py-4">No regional users found.</td></tr>
@endforelse
</tbody>
</table>
<div id="regional-pagination" class="mt-3 px-3 pb-3">
{{ $regionalUsers->links() }}
</div>
</div>
</div>
</div>
</div>
@endif
@if (in_array(session('current_user.user_type'), ['district_user', 'regional_luspa', 'national_luspa']))
<div class="tab-pane fade" id="national" role="tabpanel" aria-labelledby="national-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Person</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Allowed Apps</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="nationalBody">
@forelse($nationalUsers as $user)
@php $status = ($user['is_disabled'] === 'true' || $user['is_disabled'] === true) ? 'Inactive' : 'Active'; @endphp
<tr class="user-row" data-category="National" data-status="{{ $status }}" data-name="{{ $user['full_name'] }}" data-email="{{ $user['email'] }}" data-id="{{ $user['user_id'] }}">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select {{ $user['full_name'] }}"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://ui-avatars.com/api/?name={{ urlencode($user['full_name']) }}&background=random" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">{{ $user['title'] }} {{ $user['full_name'] }}</div>
<div class="text-muted small">Username: {{ $user['username'] }}</div>
</div>
</div>
</td>
<td class="align-middle">{{ $user['email'] }}</td>
<td class="align-middle"><span class="category-badge badge-national">{{ ucwords(str_replace('-', ' ', $user['ua_position'])) }}</span></td>
<td class="align-middle">
<span class="status-dot {{ $status === 'Active' ? 'status-active' : 'status-suspended' }}"></span> {{ $status }}
</td>
<td class="align-middle">{{ $user['allowed_apps'] }}</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="{{ $user['user_id'] }}"><i class="bi bi-eye"></i> View</button>
<button class="btn btn-sm btn-primary editUserBtn" data-id="{{ $user['user_id'] }}" data-bs-toggle="modal" data-bs-target="#editUserModal"><i class="bi bi-eye"></i> Edit</button>
</div>
</td>
</tr>
@empty
<tr><td colspan="7" class="text-center text-muted py-4">No national users found.</td></tr>
@endforelse
</tbody>
</table>
<div id="national-pagination" class="mt-3 px-3 pb-3">
{{ $nationalUsers->links() }}
</div>
</div>
</div>
</div>
</div>
@endif
</div>
</div>
@endsection
@section('page-js')
<script src="{{ url('public/assets/libs/select2/dist/js/select2.full.min.js') }}" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/intlTelInput.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"></script>
<script src="{{ url('public/assets/js/user_mgt_new.js') }}" type="text/javascript"></script>
<script src="{{ url('public/assets/js/add_edit_user.js') }}" type="text/javascript"></script>
@endsection

View File

@ -0,0 +1,534 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Admin Users by Category</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
/* Small visual tweaks */
.category-badge { font-weight: 600; color: #fff; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: .8rem; }
.badge-district { background:#f6a623; } /* amber */
.badge-regional { background:#17a2b8; } /* teal */
.badge-national { background:#1f6feb; } /* blue */
.user-row:hover .row-actions { visibility: visible; opacity: 1; }
.row-actions { visibility: hidden; opacity: 0; transition: .12s ease-in-out; }
.table-avatar { width:36px; height:36px; object-fit:cover; border-radius:50%; }
.toolbar { gap:.5rem; display:flex; flex-wrap:wrap; align-items:center; }
.select-all { margin-right:.5rem; }
@media (max-width: 767px) {
.toolbar { flex-direction:column; align-items:stretch; }
}
</style>
</head>
<body class="bg-light">
<div class="container py-4">
<div class="d-flex align-items-center mb-3">
<h3 class="me-auto">Manage Users</h3>
<div class="text-muted">June 15, 2026</div>
</div>
<!-- Tabs -->
<ul class="nav nav-tabs mb-3" id="userTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="district-tab" data-bs-toggle="tab" data-bs-target="#district" type="button" role="tab" aria-controls="district" aria-selected="true">District <span class="badge bg-secondary ms-1" id="count-district">3</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="regional-tab" data-bs-toggle="tab" data-bs-target="#regional" type="button" role="tab" aria-controls="regional" aria-selected="false">Regional <span class="badge bg-secondary ms-1" id="count-regional">2</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="national-tab" data-bs-toggle="tab" data-bs-target="#national" type="button" role="tab" aria-controls="national" aria-selected="false">National <span class="badge bg-secondary ms-1" id="count-national">1</span></button>
</li>
</ul>
<!-- Shared toolbar -->
<div class="card mb-3">
<div class="card-body">
<div class="toolbar">
<div class="d-flex align-items-center">
<input type="checkbox" id="globalSelect" class="form-check-input select-all" aria-label="Select all visible users">
<div class="me-2">Select</div>
</div>
<div class="input-group" style="max-width:420px;">
<span class="input-group-text">Search</span>
<input id="globalSearch" class="form-control" type="search" placeholder="Search name, email or ID (press /)" aria-label="Search users">
</div>
<div class="ms-auto d-flex gap-2">
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Filter</button>
<ul class="dropdown-menu p-3" style="min-width:220px;">
<div class="mb-2"><strong>Status</strong></div>
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Active" id="fActive"><label class="form-check-label" for="fActive">Active</label></div>
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Pending" id="fPending"><label class="form-check-label" for="fPending">Pending</label></div>
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Suspended" id="fSuspended"><label class="form-check-label" for="fSuspended">Suspended</label></div>
</ul>
</div>
<button id="bulkMoveBtn" class="btn btn-primary" disabled data-bs-toggle="modal" data-bs-target="#moveModal">Move</button>
<button id="bulkDeactivate" class="btn btn-outline-danger" disabled>Deactivate</button>
</div>
</div>
</div>
</div>
<!-- Tab content -->
<div class="tab-content" id="userTabsContent">
<!-- District -->
<div class="tab-pane fade show active" id="district" role="tabpanel" aria-labelledby="district-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Person</th>
<th>Email</th>
<th>Role</th>
<th>Last activity</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="districtBody">
<!-- Example rows -->
<tr class="user-row" data-category="District" data-status="Active" data-name="Alice Doe" data-email="alice@example.com" data-id="D001">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Alice Doe"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=5" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Alice Doe</div>
<div class="text-muted small">ID: D001</div>
</div>
</div>
</td>
<td class="align-middle">alice@example.com</td>
<td class="align-middle"><span class="category-badge badge-district">District</span> &middot; Manager</td>
<td class="align-middle">2026-06-10</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="D001">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="D001" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
<tr class="user-row" data-category="District" data-status="Pending" data-name="Ben Kyle" data-email="ben@example.com" data-id="D002">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Ben Kyle"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=6" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Ben Kyle</div>
<div class="text-muted small">ID: D002</div>
</div>
</div>
</td>
<td class="align-middle">ben@example.com</td>
<td class="align-middle"><span class="category-badge badge-district">District</span> &middot; Staff</td>
<td class="align-middle">2026-06-12</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="D002">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="D002" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
<tr class="user-row" data-category="District" data-status="Suspended" data-name="Clara Park" data-email="clara@example.com" data-id="D003">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Clara Park"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=7" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Clara Park</div>
<div class="text-muted small">ID: D003</div>
</div>
</div>
</td>
<td class="align-middle">clara@example.com</td>
<td class="align-middle"><span class="category-badge badge-district">District</span> &middot; Supervisor</td>
<td class="align-middle">2026-05-30</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="D003">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="D003" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Regional -->
<div class="tab-pane fade" id="regional" role="tabpanel" aria-labelledby="regional-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Person</th>
<th>Email</th>
<th>Role</th>
<th>Last activity</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="regionalBody">
<tr class="user-row" data-category="Regional" data-status="Active" data-name="Derek Holt" data-email="derek@example.com" data-id="R001">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Derek Holt"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=8" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Derek Holt</div>
<div class="text-muted small">ID: R001</div>
</div>
</div>
</td>
<td class="align-middle">derek@example.com</td>
<td class="align-middle"><span class="category-badge badge-regional">Regional</span> &middot; Lead</td>
<td class="align-middle">2026-06-13</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="R001">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="R001" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
<tr class="user-row" data-category="Regional" data-status="Active" data-name="Eva Stone" data-email="eva@example.com" data-id="R002">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Eva Stone"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=9" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Eva Stone</div>
<div class="text-muted small">ID: R002</div>
</div>
</div>
</td>
<td class="align-middle">eva@example.com</td>
<td class="align-middle"><span class="category-badge badge-regional">Regional</span> &middot; Coordinator</td>
<td class="align-middle">2026-06-11</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="R002">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="R002" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- National -->
<div class="tab-pane fade" id="national" role="tabpanel" aria-labelledby="national-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Person</th>
<th>Email</th>
<th>Role</th>
<th>Last activity</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="nationalBody">
<tr class="user-row" data-category="National" data-status="Active" data-name="Frank West" data-email="frank@example.com" data-id="N001">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Frank West"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=10" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Frank West</div>
<div class="text-muted small">ID: N001</div>
</div>
</div>
</td>
<td class="align-middle">frank@example.com</td>
<td class="align-middle"><span class="category-badge badge-national">National</span> &middot; Director</td>
<td class="align-middle">2026-06-05</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="N001">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="N001" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination stub -->
<nav aria-label="User list pages" class="d-flex justify-content-end">
<ul class="pagination pagination-sm">
<li class="page-item disabled"><a class="page-link" href="#" tabindex="-1">Prev</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</nav>
</div>
<!-- Edit modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit user</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="editForm">
<div class="modal-body">
<input type="hidden" id="editId">
<div class="mb-3"><label class="form-label">Name</label><input id="editName" class="form-control"></div>
<div class="mb-3"><label class="form-label">Email</label><input id="editEmail" class="form-control" type="email"></div>
<div class="mb-3">
<label class="form-label">Category</label>
<select id="editCategory" class="form-select">
<option>District</option>
<option>Regional</option>
<option>National</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
<!-- Move modal (bulk) -->
<div class="modal fade" id="moveModal" tabindex="-1" aria-labelledby="moveModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="moveModalLabel">Move selected users</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="moveForm">
<div class="modal-body">
<p id="moveCountText">Move <span id="moveCount">0</span> users to:</p>
<div class="mb-3">
<select id="moveTarget" class="form-select" required>
<option value="">Choose category</option>
<option value="District">District</option>
<option value="Regional">Regional</option>
<option value="National">National</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Move</button>
</div>
</form>
</div>
</div>
</div>
<!-- Bootstrap 5 JS + Popper + jQuery (for DOM convenience) -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
(function(){
// keyboard shortcut to focus search
$(document).on('keydown', function(e){
if (e.key === '/' && !$(e.target).is('input, textarea')) {
e.preventDefault();
$('#globalSearch').focus().select();
}
});
// quick helpers
function updateCounts(){
$('#count-district').text($('#districtBody .user-row:visible').length);
$('#count-regional').text($('#regionalBody .user-row:visible').length);
$('#count-national').text($('#nationalBody .user-row:visible').length);
}
function getVisibleRowsInActiveTab(){
const activePane = $('.tab-pane.active');
return activePane.find('.user-row:visible');
}
// Select all visible in active tab
$('#globalSelect').on('change', function(){
const checked = $(this).is(':checked');
getVisibleRowsInActiveTab().find('.row-select').prop('checked', checked).trigger('change');
});
// Row checkbox toggles bulk buttons
$(document).on('change', '.row-select', function(){
const anyChecked = $('.tab-pane.active .row-select:checked').length > 0;
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', !anyChecked);
// sync header select state
const total = getVisibleRowsInActiveTab().find('.row-select').length;
const checked = getVisibleRowsInActiveTab().find('.row-select:checked').length;
$('#globalSelect').prop('checked', total>0 && total===checked);
});
// Search + filter (client-side demo)
function applyFilters(){
const q = $('#globalSearch').val().trim().toLowerCase();
const statuses = $('.filter-status:checked').map(function(){ return this.value; }).get();
$('.user-row').each(function(){
const $r = $(this);
const name = $r.data('name').toLowerCase();
const email = $r.data('email').toLowerCase();
const status = $r.data('status');
const matchesQuery = !q || name.includes(q) || email.includes(q) || $r.data('id').toLowerCase().includes(q);
const matchesStatus = statuses.length === 0 || statuses.indexOf(status) !== -1;
const show = matchesQuery && matchesStatus;
$r.toggle(show);
});
updateCounts();
// reset bulk buttons if nothing selected
$('.row-select').trigger('change');
}
$('#globalSearch').on('input', applyFilters);
$('.filter-status').on('change', applyFilters);
// Activate tab event -> clear globalSelect and bulk buttons
$('button[data-bs-toggle="tab"]').on('shown.bs.tab', function(){
$('#globalSelect').prop('checked', false);
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', true);
});
// Edit button opens modal and populates fields
let lastTrigger = null;
$(document).on('click', '.editBtn', function(){
lastTrigger = this;
const row = $(this).closest('.user-row');
$('#editId').val(row.data('id'));
$('#editName').val(row.data('name'));
$('#editEmail').val(row.data('email'));
$('#editCategory').val(row.data('category'));
});
// When edit modal closes, return focus to trigger
$('#editModal').on('hidden.bs.modal', function(){
if (lastTrigger && document.contains(lastTrigger)) lastTrigger.focus();
lastTrigger = null;
});
// Submit edit (demo: updates row inline)
$('#editForm').on('submit', function(e){
e.preventDefault();
const id = $('#editId').val();
const name = $('#editName').val();
const email = $('#editEmail').val();
const category = $('#editCategory').val();
const row = $(`.user-row[data-id="${id}"]`);
// Update data attrs and visible cells
row.data('name', name).data('email', email).data('category', category);
row.find('.fw-semibold').text(name);
row.find('td').eq(2).text(email);
row.find('.category-badge').removeClass('badge-district badge-regional badge-national');
if (category === 'District') row.find('.category-badge').addClass('badge-district').text('District');
if (category === 'Regional') row.find('.category-badge').addClass('badge-regional').text('Regional');
if (category === 'National') row.find('.category-badge').addClass('badge-national').text('National');
// Move row to correct table if category changed
const targetBody = category === 'District' ? '#districtBody' : (category === 'Regional' ? '#regionalBody' : '#nationalBody');
if (!row.parent().is(targetBody)) {
row.appendTo($(targetBody));
}
updateCounts();
$('#editModal').modal('hide');
});
// Bulk move: gather selected rows and show count
$('#bulkMoveBtn').on('click', function(){
const selected = $('.tab-pane.active .row-select:checked');
$('#moveCount').text(selected.length);
// focus first control in modal for accessibility
$('#moveModal').on('shown.bs.modal', function(){ $('#moveTarget').focus(); });
});
// Remember trigger to restore focus after move modal
$('#moveModal').on('hidden.bs.modal', function(){ $('.tab-pane.active').find('.row-select:checked').first().closest('tr').find('.row-select').focus(); });
// Execute move
$('#moveForm').on('submit', function(e){
e.preventDefault();
const target = $('#moveTarget').val();
const selected = $('.tab-pane.active .row-select:checked').closest('.user-row');
selected.each(function(){
const $r = $(this);
$r.data('category', target);
$r.find('.category-badge').removeClass('badge-district badge-regional badge-national');
if (target === 'District') $r.find('.category-badge').addClass('badge-district').text('District');
if (target === 'Regional') $r.find('.category-badge').addClass('badge-regional').text('Regional');
if (target === 'National') $r.find('.category-badge').addClass('badge-national').text('National');
const targetBody = target === 'District' ? '#districtBody' : (target === 'Regional' ? '#regionalBody' : '#nationalBody');
$r.appendTo($(targetBody));
$r.find('.row-select').prop('checked', false);
});
$('#moveModal').modal('hide');
$('#globalSelect').prop('checked', false);
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', true);
updateCounts();
});
// Bulk deactivate demo (toggles status)
$('#bulkDeactivate').on('click', function(){
const selected = $('.tab-pane.active .row-select:checked').closest('.user-row');
selected.each(function(){
const $r = $(this);
$r.data('status', 'Suspended');
// optionally add visual cue
$r.addClass('table-danger');
setTimeout(()=> $r.removeClass('table-danger'), 1200);
$r.find('.row-select').prop('checked', false);
});
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', true);
});
// Accessibility: avoid aria-hidden on focused elements (demo check)
// If you manually toggle aria-hidden elsewhere, ensure focus is moved first.
// Update counts initially
updateCounts();
// Demo: focus first edit button when pressing 'e' (example of keyboard shortcut)
$(document).on('keydown', function(e){
if (e.key === 'e' && !$(e.target).is('input, textarea')) {
e.preventDefault();
$('.editBtn').first().focus();
}
});
})();
</script>

View File

@ -0,0 +1,357 @@
@extends('layouts.master')
@section('page-title')
Admin | {{ $page_title }}
@endsection
@section('page-css')
<link rel="stylesheet" href="{{ url('public/assets/libs/select2/dist/css/select2.css') }}">
<style>
/* Small visual tweaks */
.category-badge { font-weight: 600; color: #fff; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: .8rem; }
.badge-district { background:#f6a623; } /* amber */
.badge-regional { background:#17a2b8; } /* teal */
.badge-national { background:#1f6feb; } /* blue */
.user-row:hover .row-actions { visibility: visible; opacity: 1; }
.row-actions { visibility: hidden; opacity: 0; transition: .12s ease-in-out; }
.table-avatar { width:36px; height:36px; object-fit:cover; border-radius:50%; }
.toolbar { gap:.5rem; display:flex; flex-wrap:wrap; align-items:center; }
.select-all { margin-right:.5rem; }
@media (max-width: 767px) {
.toolbar { flex-direction:column; align-items:stretch; }
}
</style>
@endsection
@section('page-content')
<div class="container py-4">
<div class="d-flex align-items-center mb-3">
<h3 class="me-auto">Manage Users</h3>
<div class="text-muted">June 15, 2026</div>
</div>
<!-- Tabs -->
<ul class="nav nav-tabs mb-3" id="userTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="district-tab" data-bs-toggle="tab" data-bs-target="#district" type="button" role="tab" aria-controls="district" aria-selected="true">District <span class="badge bg-secondary ms-1" id="count-district">3</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="regional-tab" data-bs-toggle="tab" data-bs-target="#regional" type="button" role="tab" aria-controls="regional" aria-selected="false">Regional <span class="badge bg-secondary ms-1" id="count-regional">2</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="national-tab" data-bs-toggle="tab" data-bs-target="#national" type="button" role="tab" aria-controls="national" aria-selected="false">National <span class="badge bg-secondary ms-1" id="count-national">1</span></button>
</li>
</ul>
<!-- Shared toolbar -->
<div class="card mb-3">
<div class="card-body">
<div class="toolbar">
<!-- <div class="d-flex align-items-center">
<input type="checkbox" id="globalSelect" class="form-check-input select-all" aria-label="Select all visible users">
<div class="me-2">Select</div>
</div> -->
<div class="input-group" style="max-width:420px;">
<span class="input-group-text">Search</span>
<input id="globalSearch" class="form-control" type="search" placeholder="Search name, email or ID (press /)" aria-label="Search users">
</div>
<div class="ms-auto d-flex gap-2">
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Filter</button>
<ul class="dropdown-menu p-3" style="min-width:220px;">
<div class="mb-2"><strong>Status</strong></div>
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Active" id="fActive"><label class="form-check-label" for="fActive">Active</label></div>
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Suspended" id="fSuspended"><label class="form-check-label" for="fSuspended">Suspended</label></div>
</ul>
</div>
<button id="bulkMoveBtn" class="btn btn-primary" disabled data-bs-toggle="modal" data-bs-target="#moveModal">Move</button>
<button id="bulkDeactivate" class="btn btn-outline-danger" disabled>Suspend</button>
</div>
</div>
</div>
</div>
<!-- Tab content -->
<div class="tab-content" id="userTabsContent">
<!-- District -->
<div class="tab-pane fade show active" id="district" role="tabpanel" aria-labelledby="district-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Person</th>
<th>Email</th>
<th>Role</th>
<th>Allowed Apps</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="districtBody">
<!-- Example rows -->
<tr class="user-row" data-category="District" data-status="Active" data-name="Alice Doe" data-email="alice@example.com" data-id="D001">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Alice Doe"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=5" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Alice Doe</div>
<div class="text-muted small">ID: D001</div>
</div>
</div>
</td>
<td class="align-middle">alice@example.com</td>
<td class="align-middle"><span class="category-badge badge-district">District</span> &middot; Manager</td>
<td class="align-middle">2026-06-10</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="D001">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="D001" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
<tr class="user-row" data-category="District" data-status="Pending" data-name="Ben Kyle" data-email="ben@example.com" data-id="D002">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Ben Kyle"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=6" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Ben Kyle</div>
<div class="text-muted small">ID: D002</div>
</div>
</div>
</td>
<td class="align-middle">ben@example.com</td>
<td class="align-middle"><span class="category-badge badge-district">District</span> &middot; Staff</td>
<td class="align-middle">2026-06-12</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="D002">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="D002" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
<tr class="user-row" data-category="District" data-status="Suspended" data-name="Clara Park" data-email="clara@example.com" data-id="D003">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Clara Park"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=7" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Clara Park</div>
<div class="text-muted small">ID: D003</div>
</div>
</div>
</td>
<td class="align-middle">clara@example.com</td>
<td class="align-middle"><span class="category-badge badge-district">District</span> &middot; Supervisor</td>
<td class="align-middle">2026-05-30</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="D003">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="D003" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Regional -->
<div class="tab-pane fade" id="regional" role="tabpanel" aria-labelledby="regional-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Person</th>
<th>Email</th>
<th>Role</th>
<th>Allowed Apps</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="regionalBody">
<tr class="user-row" data-category="Regional" data-status="Active" data-name="Derek Holt" data-email="derek@example.com" data-id="R001">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Derek Holt"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=8" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Derek Holt</div>
<div class="text-muted small">ID: R001</div>
</div>
</div>
</td>
<td class="align-middle">derek@example.com</td>
<td class="align-middle"><span class="category-badge badge-regional">Regional</span> &middot; Lead</td>
<td class="align-middle">2026-06-13</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="R001">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="R001" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
<tr class="user-row" data-category="Regional" data-status="Active" data-name="Eva Stone" data-email="eva@example.com" data-id="R002">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Eva Stone"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=9" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Eva Stone</div>
<div class="text-muted small">ID: R002</div>
</div>
</div>
</td>
<td class="align-middle">eva@example.com</td>
<td class="align-middle"><span class="category-badge badge-regional">Regional</span> &middot; Coordinator</td>
<td class="align-middle">2026-06-11</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="R002">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="R002" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- National -->
<div class="tab-pane fade" id="national" role="tabpanel" aria-labelledby="national-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Person</th>
<th>Email</th>
<th>Role</th>
<th>Allowed Apps</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="nationalBody">
<tr class="user-row" data-category="National" data-status="Active" data-name="Frank West" data-email="frank@example.com" data-id="N001">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select Frank West"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="https://i.pravatar.cc/40?img=10" alt="avatar" class="table-avatar me-2">
<div>
<div class="fw-semibold">Frank West</div>
<div class="text-muted small">ID: N001</div>
</div>
</div>
</td>
<td class="align-middle">frank@example.com</td>
<td class="align-middle"><span class="category-badge badge-national">National</span> &middot; Director</td>
<td class="align-middle">2026-06-05</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="N001">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="N001" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination stub -->
<nav aria-label="User list pages" class="d-flex justify-content-end">
<ul class="pagination pagination-sm">
<li class="page-item disabled"><a class="page-link" href="#" tabindex="-1">Prev</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</nav>
</div>
<!-- Edit modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit user</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="editForm">
<div class="modal-body">
<input type="hidden" id="editId">
<div class="mb-3"><label class="form-label">Name</label><input id="editName" class="form-control"></div>
<div class="mb-3"><label class="form-label">Email</label><input id="editEmail" class="form-control" type="email"></div>
<div class="mb-3">
<label class="form-label">Category</label>
<select id="editCategory" class="form-select">
<option>District</option>
<option>Regional</option>
<option>National</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
<!-- Move modal (bulk) -->
<div class="modal fade" id="moveModal" tabindex="-1" aria-labelledby="moveModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="moveModalLabel">Move selected users</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="moveForm">
<div class="modal-body">
<p id="moveCountText">Move <span id="moveCount">0</span> users to:</p>
<div class="mb-3">
<select id="moveTarget" class="form-select" required>
<option value="">Choose category</option>
<option value="District">District</option>
<option value="Regional">Regional</option>
<option value="National">National</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Move</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('page-js')
<script src="{{ url('public/assets/libs/select2/dist/js/select2.full.min.js') }}" type="text/javascript" ></script>
<script src="{{ url('public/assets/js/user_mgt_new.js') }}" type="text/javascript" ></script>
@endsection

View File

@ -0,0 +1,186 @@
@extends('layouts.master')
@section('content')
<div class="container py-4">
<div class="d-flex align-items-center mb-3">
<h3 class="me-auto">Manage Users</h3>
<div class="text-muted">June 15, 2026</div>
</div>
<!-- same HTML UI as before but with minor Blade AJAX integration -->
<!-- ...copy the HTML from the previous snippet but remove static example rows and use empty tbody for JS to populate... -->
<!-- For brevity include only key changes shown below -->
<ul class="nav nav-tabs mb-3" id="userTabs" role="tablist">
<li class="nav-item"><button class="nav-link active" id="district-tab" data-bs-toggle="tab" data-bs-target="#district" type="button" role="tab">District <span class="badge bg-secondary ms-1" id="count-district">0</span></button></li>
<li class="nav-item"><button class="nav-link" id="regional-tab" data-bs-toggle="tab" data-bs-target="#regional" type="button" role="tab">Regional <span class="badge bg-secondary ms-1" id="count-regional">0</span></button></li>
<li class="nav-item"><button class="nav-link" id="national-tab" data-bs-toggle="tab" data-bs-target="#national" type="button" role="tab">National <span class="badge bg-secondary ms-1" id="count-national">0</span></button></li>
</ul>
<!-- toolbar (same as earlier) -->
<!-- tab content: empty bodies to be filled by JS -->
<div class="tab-content" id="userTabsContent">
<div class="tab-pane fade show active" id="district" role="tabpanel" aria-labelledby="district-tab">
<div class="card mb-4"><div class="card-body p-0"><div class="table-responsive"><table class="table table-hover mb-0"><thead class="table-light"><tr><th style="width:42px;"></th><th>Person</th><th>Email</th><th>Role</th><th>Last activity</th><th style="width:130px;">Actions</th></tr></thead><tbody id="districtBody"></tbody></table></div></div></div>
</div>
<div class="tab-pane fade" id="regional" role="tabpanel" aria-labelledby="regional-tab">
<div class="card mb-4"><div class="card-body p-0"><div class="table-responsive"><table class="table table-hover mb-0"><thead class="table-light"><tr><th style="width:42px;"></th><th>Person</th><th>Email</th><th>Role</th><th>Last activity</th><th style="width:130px;">Actions</th></tr></thead><tbody id="regionalBody"></tbody></table></div></div></div>
</div>
<div class="tab-pane fade" id="national" role="tabpanel" aria-labelledby="national-tab">
<div class="card mb-4"><div class="card-body p-0"><div class="table-responsive"><table class="table table-hover mb-0"><thead class="table-light"><tr><th style="width:42px;"></th><th>Person</th><th>Email</th><th>Role</th><th>Last activity</th><th style="width:130px;">Actions</th></tr></thead><tbody id="nationalBody"></tbody></table></div></div></div>
</div>
</div>
<!-- include modals from earlier (editModal, moveModal) unchanged -->
</div>
@endsection
@push('scripts')
<script>
const routes = {
list: "{{ route('admin.users.list') }}",
show: (id)=> "{{ url('admin/users') }}/"+id,
update: (id)=> "{{ url('admin/users') }}/"+id,
move: "{{ route('admin.users.move') }}",
deactivate: "{{ route('admin.users.deactivate') }}"
};
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' } });
(function(){
let currentCategory = 'District';
function fetchCategory(category){
currentCategory = category;
const params = { category, q: $('#globalSearch').val(), status: $('.filter-status:checked').map(function(){return this.value}).get() };
$.get(routes.list, params, function(data){
// data is a Laravel paginator; use data.data for items
const items = data.data || data;
const mapToTbody = { District: '#districtBody', Regional: '#regionalBody', National: '#nationalBody' };
// clear all then populate respective bodies
$('#districtBody,#regionalBody,#nationalBody').empty();
items.forEach(renderRow);
// if API returns mixed categories (when no category filter) you can instead append based on user.category
// update counts using paginator meta if available, otherwise compute
$('#count-district').text(items.filter(u=>u.category==='District').length);
$('#count-regional').text(items.filter(u=>u.category==='Regional').length);
$('#count-national').text(items.filter(u=>u.category==='National').length);
});
}
function renderRow(u){
const badgeClass = u.category === 'District' ? 'badge-district' : (u.category === 'Regional' ? 'badge-regional' : 'badge-national');
const row = $(`
<tr class="user-row" data-category="${u.category}" data-status="${u.status}" data-name="${u.name}" data-email="${u.email}" data-id="${u.id}">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select ${u.name}"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="${u.avatar_url || 'https://i.pravatar.cc/40'}" alt="avatar" class="table-avatar me-2">
<div><div class="fw-semibold">${u.name}</div><div class="text-muted small">ID: ${u.id}</div></div>
</div>
</td>
<td class="align-middle">${u.email}</td>
<td class="align-middle"><span class="category-badge ${badgeClass}">${u.category}</span> &middot; ${u.role || ''}</td>
<td class="align-middle">${u.last_activity_at || ''}</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="${u.id}">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="${u.id}" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>`);
const body = u.category === 'District' ? '#districtBody' : (u.category === 'Regional' ? '#regionalBody' : '#nationalBody');
$(body).append(row);
}
// Initial load for all categories (no filter) — call list without category to fetch many, but for simplicity fetch per category:
['District','Regional','National'].forEach(c => {
$.get(routes.list, { category: c }, function(data){
(data.data || data).forEach(renderRow);
$('#count-'+c.toLowerCase()).text((data.meta?.total) ? data.meta.total : ( (data.data || data).length ));
});
});
// Search + filter debounce
let t=null;
$('#globalSearch, .filter-status').on('input change', function(){
clearTimeout(t);
t = setTimeout(()=> { fetchCategory(currentCategory); }, 300);
});
// Edit: fetch details and populate modal
let lastTrigger = null;
$(document).on('click', '.editBtn', function(){
lastTrigger = this;
const id = $(this).data('id');
$.get(routes.show(id), function(user){
$('#editId').val(user.id);
$('#editName').val(user.name);
$('#editEmail').val(user.email);
$('#editCategory').val(user.category);
});
});
// Update via AJAX
$('#editForm').on('submit', function(e){
e.preventDefault();
const id = $('#editId').val();
const payload = { name: $('#editName').val(), email: $('#editEmail').val(), category: $('#editCategory').val() };
$.ajax({
url: routes.update(id),
method: 'PUT',
data: payload,
success: function(updated){
// update DOM row if present
const row = $(`.user-row[data-id="${updated.id}"]`);
if (row.length){
row.data('name', updated.name).data('email', updated.email).data('category', updated.category);
row.find('.fw-semibold').text(updated.name);
row.find('td').eq(2).text(updated.email);
row.find('.category-badge').removeClass('badge-district badge-regional badge-national');
if (updated.category === 'District') row.find('.category-badge').addClass('badge-district').text('District');
if (updated.category === 'Regional') row.find('.category-badge').addClass('badge-regional').text('Regional');
if (updated.category === 'National') row.find('.category-badge').addClass('badge-national').text('National');
// move row if category changed
const target = updated.category === 'District' ? '#districtBody' : (updated.category === 'Regional' ? '#regionalBody' : '#nationalBody');
if (!row.parent().is(target)) row.appendTo($(target));
} else {
// if row wasn't loaded, re-fetch current category
fetchCategory(currentCategory);
}
$('#editModal').modal('hide');
},
error: function(xhr){
if (xhr.status === 422) {
// show validation briefly (extend as needed)
alert('Validation error');
} else alert('Update failed');
}
});
});
// Bulk move
$('#moveForm').on('submit', function(e){
e.preventDefault();
const ids = $('.tab-pane.active .row-select:checked').closest('.user-row').map(function(){ return $(this).data('id'); }).get();
const target = $('#moveTarget').val();
$.post(routes.move, { ids, target }, function(res){
// refresh current category view
fetchCategory(currentCategory);
$('#moveModal').modal('hide');
}).fail(function(){ alert('Move failed'); });
});
// Bulk deactivate
$('#bulkDeactivate').on('click', function(){
const ids = $('.tab-pane.active .row-select:checked').closest('.user-row').map(function(){ return $(this).data('id'); }).get();
if (!ids.length) return;
$.post(routes.deactivate, { ids }, function(res){
fetchCategory(currentCategory);
}).fail(function(){ alert('Deactivate failed'); });
});
// Additional UX: pagination, error handling, loading states can be added.
})();
</script>
@endpush

View File

@ -0,0 +1,427 @@
@extends('layouts.master_two')
@section('content')
<div class="container py-4">
<div class="d-flex align-items-center mb-3">
<h3 class="me-auto">Manage Users</h3>
<div class="text-muted">June 15, 2026</div>
</div>
<!-- Tabs -->
<ul class="nav nav-tabs mb-3" id="userTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="district-tab" data-bs-toggle="tab" data-bs-target="#district" type="button" role="tab" aria-controls="district" aria-selected="true">District <span class="badge bg-secondary ms-1" id="count-district">0</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="regional-tab" data-bs-toggle="tab" data-bs-target="#regional" type="button" role="tab" aria-controls="regional" aria-selected="false">Regional <span class="badge bg-secondary ms-1" id="count-regional">0</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="national-tab" data-bs-toggle="tab" data-bs-target="#national" type="button" role="tab" aria-controls="national" aria-selected="false">National <span class="badge bg-secondary ms-1" id="count-national">0</span></button>
</li>
</ul>
<!-- Shared toolbar -->
<div class="card mb-3">
<div class="card-body">
<div class="toolbar">
<div class="d-flex align-items-center">
<input type="checkbox" id="globalSelect" class="form-check-input select-all" aria-label="Select all visible users">
<div class="me-2">Select</div>
</div>
<div class="input-group" style="max-width:420px;">
<span class="input-group-text">Search</span>
<input id="globalSearch" class="form-control" type="search" placeholder="Search name, email or ID (press /)" aria-label="Search users">
</div>
<div class="ms-auto d-flex gap-2">
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Filter</button>
<ul class="dropdown-menu p-3" style="min-width:220px;">
<div class="mb-2"><strong>Status</strong></div>
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Active" id="fActive"><label class="form-check-label" for="fActive">Active</label></div>
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Pending" id="fPending"><label class="form-check-label" for="fPending">Pending</label></div>
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Suspended" id="fSuspended"><label class="form-check-label" for="fSuspended">Suspended</label></div>
</ul>
</div>
<button id="bulkMoveBtn" class="btn btn-primary" disabled data-bs-toggle="modal" data-bs-target="#moveModal">Move</button>
<button id="bulkDeactivate" class="btn btn-outline-danger" disabled>Deactivate</button>
</div>
</div>
</div>
</div>
<!-- Tab content -->
<div class="tab-content" id="userTabsContent">
<!-- District -->
<div class="tab-pane fade show active" id="district" role="tabpanel" aria-labelledby="district-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Person</th>
<th>Email</th>
<th>Role</th>
<th>Last activity</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="districtBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Regional -->
<div class="tab-pane fade" id="regional" role="tabpanel" aria-labelledby="regional-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Person</th>
<th>Email</th>
<th>Role</th>
<th>Last activity</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="regionalBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- National -->
<div class="tab-pane fade" id="national" role="tabpanel" aria-labelledby="national-tab">
<div class="card mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width:42px;"></th>
<th>Person</th>
<th>Email</th>
<th>Role</th>
<th>Last activity</th>
<th style="width:130px;">Actions</th>
</tr>
</thead>
<tbody id="nationalBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination stub -->
<nav aria-label="User list pages" class="d-flex justify-content-end">
<ul class="pagination pagination-sm" id="paginationContainer">
<li class="page-item disabled"><a class="page-link" href="#" tabindex="-1">Prev</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</nav>
</div>
<!-- Edit modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit user</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="editForm">
<div class="modal-body">
<input type="hidden" id="editId">
<div class="mb-3"><label class="form-label">Name</label><input id="editName" class="form-control"></div>
<div class="mb-3"><label class="form-label">Email</label><input id="editEmail" class="form-control" type="email"></div>
<div class="mb-3">
<label class="form-label">Category</label>
<select id="editCategory" class="form-select">
<option>District</option>
<option>Regional</option>
<option>National</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
<!-- Move modal (bulk) -->
<div class="modal fade" id="moveModal" tabindex="-1" aria-labelledby="moveModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="moveModalLabel">Move selected users</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="moveForm">
<div class="modal-body">
<p id="moveCountText">Move <span id="moveCount">0</span> users to:</p>
<div class="mb-3">
<select id="moveTarget" class="form-select" required>
<option value="">Choose category</option>
<option value="District">District</option>
<option value="Regional">Regional</option>
<option value="National">National</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Move</button>
</div>
</form>
</div>
</div>
</div>
@push('styles')
<style>
/* Small visual tweaks */
.category-badge { font-weight: 600; color: #fff; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: .8rem; }
.badge-district { background:#f6a623; } /* amber */
.badge-regional { background:#17a2b8; } /* teal */
.badge-national { background:#1f6feb; } /* blue */
.user-row:hover .row-actions { visibility: visible; opacity: 1; }
.row-actions { visibility: hidden; opacity: 0; transition: .12s ease-in-out; }
.table-avatar { width:36px; height:36px; object-fit:cover; border-radius:50%; }
.toolbar { gap:.5rem; display:flex; flex-wrap:wrap; align-items:center; }
.select-all { margin-right:.5rem; }
@media (max-width: 767px) {
.toolbar { flex-direction:column; align-items:stretch; }
}
</style>
@endpush
@push('scripts')
<!-- jQuery + Bootstrap bundle -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
const routes = {
list: "{{ route('admin.users.list') }}",
show: (id)=> "{{ url('admin/users') }}/"+id,
update: (id)=> "{{ url('admin/users') }}/"+id,
move: "{{ route('admin.users.move') }}",
deactivate: "{{ route('admin.users.deactivate') }}"
};
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' } });
(function(){
// keyboard shortcut to focus search
$(document).on('keydown', function(e){
if (e.key === '/' && !$(e.target).is('input, textarea')) {
e.preventDefault();
$('#globalSearch').focus().select();
}
});
let currentCategory = 'District';
function renderRow(u){
const badgeClass = u.category === 'District' ? 'badge-district' : (u.category === 'Regional' ? 'badge-regional' : 'badge-national');
const avatar = u.avatar_url || 'https://i.pravatar.cc/40';
const role = u.role || '';
const lastAct = u.last_activity_at || '';
const row = $(`
<tr class="user-row" data-category="${u.category}" data-status="${u.status}" data-name="${u.name}" data-email="${u.email}" data-id="${u.id}">
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select ${u.name}"></td>
<td class="align-middle">
<div class="d-flex align-items-center">
<img src="${avatar}" alt="avatar" class="table-avatar me-2">
<div><div class="fw-semibold">${u.name}</div><div class="text-muted small">ID: ${u.id}</div></div>
</div>
</td>
<td class="align-middle">${u.email}</td>
<td class="align-middle"><span class="category-badge ${badgeClass}">${u.category}</span> &middot; ${role}</td>
<td class="align-middle">${lastAct}</td>
<td class="align-middle">
<div class="row-actions d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="${u.id}">View</button>
<button class="btn btn-sm btn-primary editBtn" data-id="${u.id}" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
</div>
</td>
</tr>`);
const body = u.category === 'District' ? '#districtBody' : (u.category === 'Regional' ? '#regionalBody' : '#nationalBody');
$(body).append(row);
}
function clearBodies(){ $('#districtBody,#regionalBody,#nationalBody').empty(); }
function fetchCategory(category){
currentCategory = category;
const params = { category, q: $('#globalSearch').val(), status: $('.filter-status:checked').map(function(){return this.value}).get() };
$.get(routes.list, params, function(data){
clearBodies();
const items = data.data || data;
items.forEach(renderRow);
// Try to use paginator meta for counts, fall back to counting rendered rows
$('#count-district').text(data.meta?.total_by_category?.District ?? $('#districtBody .user-row').length);
$('#count-regional').text(data.meta?.total_by_category?.Regional ?? $('#regionalBody .user-row').length);
$('#count-national').text(data.meta?.total_by_category?.National ?? $('#nationalBody .user-row').length);
});
}
// Initial load per category
['District','Regional','National'].forEach(c => {
$.get(routes.list, { category: c }, function(data){
(data.data || data).forEach(renderRow);
$('#count-'+c.toLowerCase()).text(data.meta?.total ?? (data.data || data).length);
});
});
// Select all visible in active tab
function getVisibleRowsInActiveTab(){
const activePane = $('.tab-pane.active');
return activePane.find('.user-row:visible');
}
$('#globalSelect').on('change', function(){
const checked = $(this).is(':checked');
getVisibleRowsInActiveTab().find('.row-select').prop('checked', checked).trigger('change');
});
// Row checkbox toggles bulk buttons
$(document).on('change', '.row-select', function(){
const anyChecked = $('.tab-pane.active .row-select:checked').length > 0;
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', !anyChecked);
const total = getVisibleRowsInActiveTab().find('.row-select').length;
const checked = getVisibleRowsInActiveTab().find('.row-select:checked').length;
$('#globalSelect').prop('checked', total>0 && total===checked);
});
// Search + filter debounce
let t=null;
$('#globalSearch, .filter-status').on('input change', function(){
clearTimeout(t);
t = setTimeout(()=> { fetchCategory(currentCategory); }, 300);
});
// Activate tab event -> set currentCategory and reset selects
$('button[data-bs-toggle="tab"]').on('shown.bs.tab', function(e){
const target = $(e.target).data('bsTarget') || $(e.target).data('bs-target');
currentCategory = target.replace('#','');
$('#globalSelect').prop('checked', false);
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', true);
});
// Edit: fetch details and populate modal
let lastTrigger = null;
$(document).on('click', '.editBtn', function(){
lastTrigger = this;
const id = $(this).data('id');
$.get(routes.show(id), function(user){
$('#editId').val(user.id);
$('#editName').val(user.name);
$('#editEmail').val(user.email);
$('#editCategory').val(user.category);
});
});
// When edit modal closes, return focus to trigger
$('#editModal').on('hidden.bs.modal', function(){
if (lastTrigger && document.contains(lastTrigger)) lastTrigger.focus();
lastTrigger = null;
});
// Update via AJAX
$('#editForm').on('submit', function(e){
e.preventDefault();
const id = $('#editId').val();
const payload = { name: $('#editName').val(), email: $('#editEmail').val(), category: $('#editCategory').val() };
$.ajax({
url: routes.update(id),
method: 'PUT',
data: payload,
success: function(updated){
const row = $(`.user-row[data-id="${updated.id}"]`);
if (row.length){
row.data('name', updated.name).data('email', updated.email).data('category', updated.category);
row.find('.fw-semibold').text(updated.name);
row.find('td').eq(2).text(updated.email);
row.find('.category-badge').removeClass('badge-district badge-regional badge-national');
if (updated.category === 'District') row.find('.category-badge').addClass('badge-district').text('District');
if (updated.category === 'Regional') row.find('.category-badge').addClass('badge-regional').text('Regional');
if (updated.category === 'National') row.find('.category-badge').addClass('badge-national').text('National');
const target = updated.category === 'District' ? '#districtBody' : (updated.category === 'Regional' ? '#regionalBody' : '#nationalBody');
if (!row.parent().is(target)) row.appendTo($(target));
} else {
fetchCategory(currentCategory);
}
$('#editModal').modal('hide');
},
error: function(xhr){
if (xhr.status === 422) {
alert('Validation error');
} else alert('Update failed');
}
});
});
// Bulk move: prepare modal count
$('#bulkMoveBtn').on('click', function(){
const selected = $('.tab-pane.active .row-select:checked');
$('#moveCount').text(selected.length);
$('#moveTarget').val('');
$('#moveModal').on('shown.bs.modal', function(){ $('#moveTarget').focus(); });
});
// Execute move
$('#moveForm').on('submit', function(e){
e.preventDefault();
const ids = $('.tab-pane.active .row-select:checked').closest('.user-row').map(function(){ return $(this).data('id'); }).get();
const target = $('#moveTarget').val();
$.post(routes.move, { ids, target }, function(res){
fetchCategory(currentCategory);
$('#moveModal').modal('hide');
}).fail(function(){ alert('Move failed'); });
});
// Bulk deactivate
$('#bulkDeactivate').on('click', function(){
const ids = $('.tab-pane.active .row-select:checked').closest('.user-row').map(function(){ return $(this).data('id'); }).get();
if (!ids.length) return;
$.post(routes.deactivate, { ids }, function(res){
fetchCategory(currentCategory);
}).fail(function(){ alert('Deactivate failed'); });
});
// init counts if none loaded
setTimeout(()=> {
$('#count-district').text($('#districtBody .user-row').length);
$('#count-regional').text($('#regionalBody .user-row').length);
$('#count-national').text($('#nationalBody .user-row').length);
}, 400);
// accessibility shortcut example
$(document).on('keydown', function(e){
if (e.key === 'e' && !$(e.target).is('input, textarea')) {
e.preventDefault();
$('.editBtn').first().focus();
}
});
})();
</script>
@endpush

View File

@ -4,7 +4,7 @@
@endsection @endsection
@section('page-css') @section('page-css')
<link rel="stylesheet" href="{{ url('public/assets/libs/select2/dist/css/select2.css') }}"> <link rel="stylesheet" href="{{ url('public/assets/libs/select2/dist/css/select2.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/css/intlTelInput.css"/>
@endsection @endsection
@section('page-content') @section('page-content')
@include('admin.partials.create-user') @include('admin.partials.create-user')
@ -12,7 +12,8 @@
@include('admin.partials.view-user') @include('admin.partials.view-user')
@include('layouts.partials.navbar') @include('layouts.partials.navbar')
<?php //dump(session('current_user'));
?>
<div class="container py-4"> <div class="container py-4">
<div class="main"> <div class="main">
<div class="row"> <div class="row">
@ -85,10 +86,7 @@
</tbody> </tbody>
</table> </table>
<?php if (count($items) > 1): ?> <?php if (count($items) > 1): ?>
<div class="float-end pb-2" > <div class=" mt-3 px-3 pb-3">
Showing {{ $items->currentPage() }} of {{ $items->lastPage() }} pages | {{ $items->total() }} records
</div>
<div class="d-flex justify-content-centerw">
{{ $items->links() }} {{ $items->links() }}
</div> </div>
<?php endif; ?> <?php endif; ?>
@ -105,9 +103,13 @@
@endsection @endsection
@section('page-js') @section('page-js')
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/intlTelInput.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"></script>
<script src="{{ url('public/assets/libs/select2/dist/js/select2.full.min.js') }}" type="text/javascript" ></script> <script src="{{ url('public/assets/libs/select2/dist/js/select2.full.min.js') }}" type="text/javascript" ></script>
<script src="{{ url('public/assets/js/usermgt.js') }}" type="text/javascript" ></script> <script src="{{ url('public/assets/js/usermgt.js') }}" type="text/javascript" ></script>
@endsection @endsection

View File

@ -1,5 +1,5 @@
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModalLabel" aria-hidden="true"> <div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModalLabel">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -7,15 +7,29 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="alert alert-success d-none" id="successArea">Heere at the wall</div> <div class="alert alert-success d-none" id="newUserSuccessArea"></div>
<div class="alert alert-danger d-none" id="errorArea">Heere at the wall</div> <p class="alert alert-danger d-none" id="newUserErrorArea"></p>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<form class="row g-3" action="" method="POST" id="newUserForm"> <form class="row g-3" action="" method="POST" id="newUserForm">
@csrf @csrf
<input type="hidden" name="action" value="newuser"> <input type="hidden" name="action" value="newuser">
<div class="col-md-12"> <div class="col-md-12">
@if (session('current_user.user_type') == 'district_user')
<div class="form-check">
<input class="form-check-input" type="radio" name="user_type" value="district_user" id="userTypeDistrict" checked>
<label class="form-check-label" for="userTypeDistrict">District User</label>
</div>
@elseif(session('current_user.user_type') == 'regional_user')
<div class="form-check">
<input class="form-check-input" type="radio" name="user_type" value="district_user" id="userTypeDistrict" checked>
<label class="form-check-label" for="userTypeDistrict">District User</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="user_type" value="regional_luspa" id="userTypeLuspaRegion">
<label class="form-check-label" for="userTypeLuspaRegion">Regional LUSPA</label>
</div>
@else
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="user_type" value="district_user" id="userTypeDistrict" checked> <input class="form-check-input" type="radio" name="user_type" value="district_user" id="userTypeDistrict" checked>
<label class="form-check-label" for="userTypeDistrict">District User</label> <label class="form-check-label" for="userTypeDistrict">District User</label>
@ -28,12 +42,14 @@
<input class="form-check-input" type="radio" name="user_type" value="national_luspa" id="userTypeLuspaNational"> <input class="form-check-input" type="radio" name="user_type" value="national_luspa" id="userTypeLuspaNational">
<label class="form-check-label" for="userTypeLuspaNational">National LUSPA</label> <label class="form-check-label" for="userTypeLuspaNational">National LUSPA</label>
</div> </div>
@endif
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label for="titleSelecton" class="form-label">Title</label> <label for="titleSelecton" class="form-label">Title</label>
<select id="titleSelecton" name="title" class="form-select" required> <select id="titleSelecton" name="title" class="form-select" required>
<option selected disabled>Choose...</option> <option selected disabled>Choose...</option>
<option value="Mr">Mr</option> <option value="Mr">Mr</option>
<option value="Mr">Miss</option>
<option value="Mrs">Mrs</option> <option value="Mrs">Mrs</option>
<option value="Dr">Dr</option> <option value="Dr">Dr</option>
<option value="Proff">Proff</option> <option value="Proff">Proff</option>
@ -41,31 +57,31 @@
</div> </div>
<div class="col-md-5"> <div class="col-md-5">
<label for="fullName" class="form-label">Fullname</label> <label for="addUserFullname" class="form-label">Fullname*</label>
<input type="text" name="full_name" class="form-control" id="fullName" required> <input type="text" name="full_name" class="form-control" id="addUserFullname" required>
</div> </div>
<div class="col-md-5"> <div class="col-md-5">
<label for="email" class="form-label">Email</label> <label for="addUserEmail" class="form-label">Email*</label>
<input type="email" name="email" class="form-control" id="email" required> <input type="email" name="email" class="form-control" id="addUserEmail" required>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label for="username" class="form-label">Username</label> <label for="addUserUsername" class="form-label">Username*</label>
<input type="username" name="username" class="form-control" id="username" required> <input type="username" name="username" class="form-control" id="addUserUsername" required>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label for="gender" class="form-label">Gender</label> <label for="addUserGender" class="form-label">Gender*</label>
<select id="gender" name="gender" class="form-select" required> <select id="addUserGender" name="gender" class="form-select" required>
<option selected>Choose...</option> <option selected disabled>Choose...</option>
<option value="female">Female</option> <option value="female">Female</option>
<option value="male">Male</option> <option value="male">Male</option>
</select> </select>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label for="phone" class="form-label">Phone</label> <label for="phone" class="form-label">Phone*</label>
<input type="text" name="phone" class="form-control" id="phone" required> <input type="tel" name="phone" class="form-control" id="addUserphone" required>
</div> </div>
<div class="col-md-4"> <div class="col-md-<?php echo (session('current_user.user_type')) == 'district_user' ? '12' : '4'; ?>">
<label for="uaPostionAdd" class="form-label">Position</label> <label for="uaPostionAdd" class="form-label">Position</label>
<select id="uaPostionAdd" name="ua_position" class="form-select" required> <select id="uaPostionAdd" name="ua_position" class="form-select" required>
<option selected disabled>Choose...</option> <option selected disabled>Choose...</option>
@ -77,6 +93,9 @@
<option value="district-works-staff">Works Department Staff</option> <option value="district-works-staff">Works Department Staff</option>
</select> </select>
</div> </div>
@if (session('current_user.user_type') == 'district_user' )
<input type="hidden" name="districtid" value="{{ session('current_user.district_id') }}">
@elseif(session('current_user.user_type') == 'regional_user')
<div class="col-md-4"> <div class="col-md-4">
<label for="regionID" class="form-label">Region</label> <label for="regionID" class="form-label">Region</label>
<select name="region_id" id="regionID" class="form-select"> <select name="region_id" id="regionID" class="form-select">
@ -86,14 +105,33 @@
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
@else
<div class="col-md-4"> <div class="col-md-4">
<label for="districtID" class="form-label">District</label> <label for="regionID" class="form-label">Region*</label>
<select id="districtID" name="districtid" class="form-select"> <select name="region_id" id="regionID" class="form-select regionIDD">
<option selected disabled>Choose...</option>
<?php foreach ($regions_arr as $row): ?>
<option value="<?php echo $row['regionid']; ?>"><?php echo $row['region_name']; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label for="districtID" class="form-label">District*</label>
<select id="districtID" name="districtid" class="form-select districtIDD">
<option selected disabled>Choose...</option> <option selected disabled>Choose...</option>
</select> </select>
</div> </div>
@endif
<div class="col-md-6">
<label for="addUserUserStatus" class="form-label">Status *</label>
<select id="addUserUserStatus" name="user_status" class="form-select"required style="width:100%">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div class="col-md-12"> <div class="col-md-12">
<label for="allowedApps" class="form-label">Allowed Apps</label> <label for="allowedApps" class="form-label">Allowed Apps*</label>
<select id="allowedApps" name="allowed_apps[]"class="form-select" multiple required style="width:100%"> <select id="allowedApps" name="allowed_apps[]"class="form-select" multiple required style="width:100%">
<option value="drawing-tools">Drawing Tools</option> <option value="drawing-tools">Drawing Tools</option>
<option value="permit-tools">Permit Tools</option> <option value="permit-tools">Permit Tools</option>
@ -117,3 +155,8 @@
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,29 @@
<div class="modal fade" id="editModalSimple" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit user</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="editForm">
<div class="modal-body">
<input type="hidden" id="editId">
<div class="mb-3"><label class="form-label">Name</label><input id="editName" class="form-control"></div>
<div class="mb-3"><label class="form-label">Email</label><input id="editEmail" class="form-control" type="email"></div>
<div class="mb-3">
<label class="form-label">Category</label>
<select id="editCategory" class="form-select">
<option>District</option>
<option>Regional</option>
<option>National</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true"> <div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="exampleModalLabel">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@ -15,6 +15,38 @@
<form class="row g-3" action="" method="POST" id="editUserForm"> <form class="row g-3" action="" method="POST" id="editUserForm">
@csrf @csrf
<input type="hidden" name="user_id" value=""> <input type="hidden" name="user_id" value="">
<input type="hidden" name="districtid" value="{{ session('current_user.district_id') }}">
<!-- <input type="hidden" name="user_type" value="district_user"> -->
<div class="col-md-12">
@if (session('current_user.user_type') == 'district_user')
<div class="form-check">
<input class="form-check-input" type="radio" name="user_type" value="district_user" id="editUserTypeDistrict" >
<label class="form-check-label" for="editUserTypeDistrict">District User</label>
</div>
@elseif(session('current_user.user_type') == 'regional_user')
<div class="form-check">
<input class="form-check-input" type="radio" name="user_type" value="district_user" id="editUserTypeDistrict" >
<label class="form-check-label" for="editUserTypeDistrict">District User</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="user_type" value="regional_luspa" id="editUserTypeLuspaRegion">
<label class="form-check-label" for="editUserTypeLuspaRegion">Regional LUSPA</label>
</div>
@else
<div class="form-check">
<input class="form-check-input" type="radio" name="user_type" value="district_user" id="editUserTypeDistrict" >
<label class="form-check-label" for="editUserTypeDistrict">District User</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="user_type" value="regional_luspa" id="editUserTypeLuspaRegion">
<label class="form-check-label" for="editUserTypeLuspaRegion">Regional LUSPA</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="user_type" value="national_luspa" id="editUserTypeLuspaNational">
<label class="form-check-label" for="editUserTypeLuspaNational">National LUSPA</label>
</div>
@endif
</div>
<div class="col-md-2"> <div class="col-md-2">
<label for="editTitle" class="form-label">Title</label> <label for="editTitle" class="form-label">Title</label>
<select id="editTitle" name="title" class="form-select" required> <select id="editTitle" name="title" class="form-select" required>
@ -27,11 +59,11 @@
</div> </div>
<div class="col-md-5"> <div class="col-md-5">
<label for="editFullName" class="form-label">Fullname MM</label> <label for="editFullName" class="form-label">Fullname *</label>
<input type="text" name="full_name" value="" class="form-control" id="editFullName" required> <input type="text" name="full_name" value="" class="form-control" id="editFullName" required>
</div> </div>
<div class="col-md-5"> <div class="col-md-5">
<label for="editEmail" class="form-label">Email</label> <label for="editEmail" class="form-label">Email *</label>
<input type="email" name="email" value="" class="form-control" id="editEmail" required> <input type="email" name="email" value="" class="form-control" id="editEmail" required>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
@ -47,11 +79,11 @@
</select> </select>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label for="editPhone" class="form-label">Phone</label> <label for="editPhone" class="form-label">Phone *</label>
<input type="text" name="phone" value="" class="form-control" id="editPhone" required> <input type="text" name="phone" value="" class="form-control" id="editPhone" required>
</div> </div>
<div class="col-md-6"> <div class="col-md-4">
<label for="editUaPostion" class="form-label">Position</label> <label for="editUaPostion" class="form-label">Position *</label>
<select id="editUaPostion" name="ua_position" class="form-select" required> <select id="editUaPostion" name="ua_position" class="form-select" required>
<option value="district-mis">District MIS</option> <option value="district-mis">District MIS</option>
<option value="district-ppd-head">PPD Head</option> <option value="district-ppd-head">PPD Head</option>
@ -61,8 +93,43 @@
<option value="district-works-staff">Works Department Staff</option> <option value="district-works-staff">Works Department Staff</option>
</select> </select>
</div> </div>
@if (session('current_user.user_type') == 'district_user' )
<input type="hidden" name="districtid" value="{{ session('current_user.district_id') }}">
@elseif(session('current_user.user_type') == 'regional_user')
<div class="col-md-4">
<label for="editRegionID" class="form-label">Region *</label>
<select name="region_id" id="editRegionID" class="form-select">
<option selected disabled>Choose...</option>
<?php foreach ($regions_arr as $row): ?>
<option value="<?php echo $row['regionid']; ?>" ><?php echo $row['region_name']; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label for="editDistrictID" class="form-label">District *</label>
<select id="editDistrictID" name="districtid" class="form-select">
<option selected disabled>Choose...</option>
</select>
</div>
@else
<div class="col-md-4">
<label for="editRegionID" class="form-label">Region *</label>
<select name="region_id" id="editRegionID" class="form-select regionIDD">
<option selected disabled>Choose...</option>
<?php foreach ($regions_arr as $row): ?>
<option value="<?php echo $row['regionid']; ?>"><?php echo $row['region_name']; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label for="editDistrictID" class="form-label">District *</label>
<select id="editDistrictID" name="districtid" class="form-select districtIDD">
<option selected disabled>Choose...</option>
</select>
</div>
@endif
<div class="col-md-6"> <div class="col-md-6">
<label for="editAllowedApps" class="form-label">Allowed Apps</label> <label for="editAllowedApps" class="form-label">Allowed Apps *</label>
<select id="editAllowedApps" name="allowed_apps[]" class="form-select" multiple required style="width:100%"> <select id="editAllowedApps" name="allowed_apps[]" class="form-select" multiple required style="width:100%">
<!-- <option selected >Choose...</option> --> <!-- <option selected >Choose...</option> -->
<option value="drawing-tools">Drawing Tools</option> <option value="drawing-tools">Drawing Tools</option>
@ -70,21 +137,19 @@
<option value="admin-gui">Admin GUI</option> <option value="admin-gui">Admin GUI</option>
</select> </select>
</div> </div>
<div class="col-md-6">
<label for="userStatus" class="form-label">Status *</label>
<select id="userStatus" name="user_status" class="form-select"required style="width:100%">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div class="col-md-12"> <div class="col-md-12">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="expire_password" value="yes" id="expirePassword" checked> <input class="form-check-input" type="checkbox" name="expire_password" value="yes" id="expirePassword" checked>
<label class="form-check-label" for="expirePassword">Expire Password</label> <label class="form-check-label" for="expirePassword">Expire Password</label>
</div> </div>
</div> </div>
<div class="col-md-12">
<label for="inputPermissions" class="form-label">Permissions</label>
<select name="permission" id="inputPermissions" multiple class="form-select" style="width: 100%">
<option value="1">Permission 1</option>
<option value="2">Permission 2</option>
<option value="3">Permission 3</option>
<option value="4">Permission 4</option>
</select>
</div>
<div class="col-md-12"> <div class="col-md-12">
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>

View File

@ -0,0 +1,27 @@
<div class="modal fade" id="moveModal" tabindex="-1" aria-labelledby="moveModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="moveModalLabel">Move selected users</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="moveForm">
<div class="modal-body">
<p id="moveCountText">Move <span id="moveCount">0</span> users to:</p>
<div class="mb-3">
<select id="moveTarget" class="form-select" required>
<option value="">Choose category</option>
<option value="District">District</option>
<option value="Regional">Regional</option>
<option value="National">National</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Move</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,84 @@
@extends('layouts.master')
@section('page-title')
Admin | {{ $page_title }}
@endsection
@section('page-css')
<link rel="stylesheet" href="{{ url('public/assets/libs/select2/dist/css/select2.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/css/intlTelInput.css"/>
@endsection
@section('page-content')
@include('admin.partials.create-user')
<div class="container py-4">
<div class="main">
<div class="row">
<div class="col-md-12">
<h3>Users</h3>
<!-- <a href="" class="float-end">Add User</a> -->
<div class="float-end pb-2" >
<button type="button" class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#addUserModal">
<img src="{{ url('public/assets/libs/bootstrap-icons/person-plus.svg') }}" alt="person-plus" width="16" height="16"> Add User</button>
</div>
<table class="table table-hover table-bordered table-condensed">
<thead class="">
<tr class="table-active">
<th scope="col">Fullname</th>
<th scope="col">Username</th>
<th scope="col">Email</th>
<th scope="col">Phone</th>
<th scope="col">Position</th>
<th scope="col">Allowed Apps</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<?php if (count($users_arr) < 1): ?>
<tr>
<td colspan="7">No users found</td>
</tr>
<?php else: ?>
<?php foreach ($users_arr as $row): ?>
<?php $ua_id = $row['ua_id']; ?>
<tr>
<td><?php echo $row['full_name'] ?></td>
<td><?php echo $row['username'] ?></td>
<td><?php echo $row['email'] ?></td>
<td><?php echo $row['phone'] ?></td>
<td><?php echo $row['ua_position'] ?></td>
<td><?php echo $row['allowed_apps'] ?></td>
<td>
@csrf
<input type="hidden" class="userIdinput" value="<?php echo $row['user_id'] ?>" name="userId">
<!-- <a href="" data-bs-toggle="modal" data-bs-target="#viewUserModal" ><img src="{{ url('public/assets/libs/bootstrap-icons/eye.svg') }}" alt="view icon" width="16" height="16"></a> -->
<a href="" class="editUserBtn" data-bs-toggle="modal" data-bs-target="#editUserModal">
<img src="{{ url('public/assets/libs/bootstrap-icons/pencil-square.svg') }}" alt="edit icon" width="16" height="16">
</a>
</td>
</tr>
<?php endforeach ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<footer class="py-5">
<div class="row">
</div>
</footer>
</div>
@endsection
@section('page-js')
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/intlTelInput.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js"></script>
<script src="{{ url('public/assets/libs/select2/dist/js/select2.full.min.js') }}" type="text/javascript" ></script>
<script src="{{ url('public/assets/js/usermgt.js') }}" type="text/javascript" ></script>
@endsection

View File

@ -70,7 +70,58 @@
</div> </div>
</div> </div>
</nav> </nav>
<!-- Welcome / User Context Panel -->
<?php
//dump(session('current_user'));
?>
<div class="container mt-4 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body py-3">
<div class="row align-items-center">
<!-- User Info -->
<div class="col-md-8">
<h5 class="mb-1">
Welcome, <strong>{{ ucfirst(session('current_user.username')) }}</strong>
</h5>
<p class="text-muted mb-0">
You are currently signed in as a
<span class="badge bg-primary">
{{ session('current_user.ua_position') }}
</span>
</p>
</div>
<!-- District Info -->
<div class="col-md-4 text-md-end mt-3 mt-md-0">
<div class="small text-muted">
<?php
$district_name = '';
if (session('current_user.user_type') == 'district_user') {
echo "Current District";
$district_name = session('current_user.district_name');
}
elseif (session('current_user.user_type') == 'regional_luspa') {
echo "Current User";
$district_name = 'Regional LUSPA';
}
elseif (session('current_user.user_type') == 'national_luspa') {
echo "Current User";
$district_name = 'National LUSPA';
}
?>
</div>
<div class="fw-semibold fs-5">
{{ $district_name }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container py-5"> <div class="container py-5">
@include('common.notifications') @include('common.notifications')
<div class="row g-4"> <div class="row g-4">

View File

@ -8,6 +8,7 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content=""> <meta name="author" content="">
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('page-title')</title> <title>@yield('page-title')</title>
<!-- Custom fonts for this template--> <!-- Custom fonts for this template-->

View File

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('page-title')</title>
<!-- Custom fonts for this template-->
<link href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i" rel="stylesheet">
<!-- Custom styles for this template-->
<!-- <link rel="stylesheet" href="../assets/css/bootstrap.css"> -->
<link rel="stylesheet" href="{{ url('public/assets/libs/bootstrap/css/bootstrap5.3.2.css') }}">
<link rel="stylesheet" href="{{ url('public/assets/libs/ol/ol.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url('public/assets/libs/ol/ol-ext.css') }}" />
<link rel="stylesheet" href="{{ url('public/assets/css/l4l.css') }}" rel="stylesheet">
<link rel="stylesheet" href="{{ url('public/assets/css/styles.css') }}" rel="stylesheet">
<link rel="stylesheet" href="{{ url('public/assets/libs/fontawesome-free-7.1.0-web/css/all.min.css') }}" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="{{ url('public/assets/libs/jquery-confirm/jquery-confirm.min.css') }}" type="text/css">
@yield('page-css')
<script type="text/javascript">
var base_url = "{!! url('/') !!}";
</script>
</head>
@include('admin.partials.profile')
<!-- <body class="bg-gradient-primary"> -->
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary shadow-sm">
<div class="container">
<a class="navbar-brand" href="/landing">LUPMIS4LUSPA</a>
<div class="ms-auto">
<div class="dropdown">
<button class="btn btn-light dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown">
<span class="me-2"><?php echo ucfirst(session('current_user.username')); ?></span>
<!-- <small class="text-muted">Municipality</small> -->
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item profileLink" >Profile</a></li>
<li><a class="dropdown-item" href="/landing">Landing Page</a></li>
<li><a class="dropdown-item" href="#">Settings</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/user-logout">Logout</a></li>
</ul>
</div>
</div>
</div>
</nav>
@yield('page-content')
<script src="{{ url('public/assets/libs/jquery-3.2.1.min.js') }}" type="text/javascript"></script>
<script src="{{ url('public/assets/libs/bootstrap/js/bootstrap5.3.2.js') }}"></script>
<script src="{{ url('public/assets/libs/ol/ol.js') }}" type="text/javascript" ></script> <!-- ol6.15.1 -->
<script src="{{ url('public/assets/libs/ol/ol-ext.js') }}" type="text/javascript" ></script>
<script src="{{ url('public/assets/libs/fontawesome-free-7.1.0-web/js/all.min.js') }}"></script>
<script src="{{ url('public/assets/libs/jquery-confirm/jquery-confirm.min.js') }}"></script>
<script src="{{ url('public/assets/js/all_pages.js') }}"></script>
@yield('page-js')
</body>
</html>

View File

@ -0,0 +1,77 @@
<button class="btn btn-outline-secondary w-100" type="button" data-bs-toggle="offcanvas" data-bs-target="#historyOffcanvas" aria-controls="historyOffcanvas">
<i class="bi bi-clock-history"></i> View Full History & Comments
</button>
<div class="offcanvas offcanvas-end shadow-lg" tabindex="-1" id="historyOffcanvas" aria-labelledby="historyOffcanvasLabel" style="width: 450px;">
<div class="offcanvas-header border-bottom bg-light sticky-top z-3">
<h5 class="offcanvas-title fw-bold" id="historyOffcanvasLabel">Application History</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="timeline" id="commentsTimeline">
<div class="timeline-item">
<div class="timeline-icon bg-primary text-white shadow-sm">
<i class="bi bi-file-earmark-arrow-up"></i>
</div>
<div class="timeline-content border rounded p-3 shadow-sm">
<div class="d-flex justify-content-between align-items-center mb-1">
<strong class="text-primary">Application Submitted</strong>
<small class="text-muted">16th May, 2026 - 2:15 PM</small>
</div>
<p class="mb-0 small text-muted">by Applicant Olivia Sampson</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-icon bg-secondary text-white shadow-sm">
<i class="bi bi-chat-text"></i>
</div>
<div class="timeline-content border rounded p-3 shadow-sm">
<div class="d-flex justify-content-between align-items-center mb-1">
<strong>Applicatant to review</strong>
<small class="text-muted">17th May, 2026 - 3:15 PM</small>
</div>
<p class="mb-0 text-dark small">Please ensure the architectural drawings include the updated drainage plan for the region.</p>
<p class="mb-0 small text-muted">Updated by Kafui Amuzu (Works, Head )</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-icon bg-success text-white shadow-sm">
<i class="bi bi-check-lg"></i>
</div>
<div class="timeline-content border rounded p-3 shadow-sm">
<div class="d-flex justify-content-between align-items-center mb-1">
<strong class="text-success">Application Received</strong>
<small class="text-muted">18th May, 2026 - 10:30 AM</small>
</div>
<p class="mb-2 small">All documents are in order</p>
<div>
<p class="mb-0 small text-muted">Updated by Luke Okyere (PPD Head)</p>
</div>
</div>
</div>
</div>
</div>
<div class="offcanvas-footer border-top p-3 bg-light z-3">
<form>
<div class="mb-2">
<select class="form-select form-select-sm" aria-label="Update Status">
<option selected>Update status (Optional)...</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
</select>
</div>
<textarea class="form-control mb-2" rows="2" placeholder="Write a comment..."></textarea>
<button class="btn btn-primary w-100 btn-sm" type="button">
<i class="bi bi-send"></i> Submit Update
</button>
</form>
</div>
</div>

View File

@ -0,0 +1,66 @@
<div class="offcanvas offcanvas-end shadow-lg" tabindex="-1" id="historyOffcanvas" aria-labelledby="historyOffcanvasLabel" style="width: 450px; z-index: 1065;">
<div class="offcanvas-header border-bottom bg-light sticky-top z-3">
<h5 class="offcanvas-title fw-bold" id="historyOffcanvasLabel">Comments History</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="timeline" id="commentsTimeline">
<div class="timeline-item">
<div class="timeline-icon bg-primary text-white shadow-sm">
<i class="bi bi-file-earmark-arrow-up"></i>
</div>
<div class="timeline-content border rounded p-3 shadow-sm">
<div class="d-flex justify-content-between align-items-center mb-1">
<strong class="text-primary">Application Submitted</strong>
<small class="text-muted">16th May, 2026 - 2:15 PM</small>
</div>
<p class="mb-0 small text-muted">by Applicant Olivia Sampson</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-icon bg-secondary text-white shadow-sm">
<i class="bi bi-chat-text"></i>
</div>
<div class="timeline-content border rounded p-3 shadow-sm">
<div class="d-flex justify-content-between align-items-center mb-1">
<strong>Applicatant to review</strong>
<small class="text-muted">17th May, 2026 - 3:15 PM</small>
</div>
<p class="mb-0 text-dark small">Please ensure the architectural drawings include the updated drainage plan for the region.</p>
<p class="mb-0 small text-muted">Updated by Kafui Amuzu (Works, Head )</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-icon bg-success text-white shadow-sm">
<i class="bi bi-check-lg"></i>
</div>
<div class="timeline-content border rounded p-3 shadow-sm">
<div class="d-flex justify-content-between align-items-center mb-1">
<strong class="text-success">Application Received</strong>
<small class="text-muted">18th May, 2026 - 10:30 AM</small>
</div>
<p class="mb-2 small">All documents are in order</p>
<div>
<p class="mb-0 small text-muted">Updated by Luke Okyere (PPD Head)</p>
</div>
</div>
</div>
</div>
</div>
<div class="offcanvas-footer border-top p-3 bg-light z-3">
<textarea class="form-control mb-2 commentBody" rows="2" placeholder="Write a comment..."></textarea>
<button class="btn btn-primary w-100 btn-sm submitPermitCommentBtn" type="button"><i class="bi bi-send"></i> Submit Comment
</button>
</div>
</div>

View File

@ -0,0 +1,118 @@
<div class="modal fade" id="inspectionReportModal" tabindex="-1" aria-labelledby="inspectionReportModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-light border-bottom">
<h5 class="modal-title fw-bold" id="inspectionReportModalLabel">Site Inspection Report</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4" id="report-pdf-content">
<h6 class="text-primary fw-bold border-bottom pb-2 mb-3">1. Project Information</h6>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label text-muted small mb-1">Applicant Name</label> <input type="text" class="form-control form-control-sm" value="Jedidiah Madjanor">
</div>
<div class="col-md-6">
<label class="form-label text-muted small mb-1">Site Location</label> <input type="text" class="form-control form-control-sm" value="2nd 3rdlink Fertilizer Road, Accra">
</div>
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Plot/House Number (Ghana Post GPS)</label> <input type="text" class="form-control form-control-sm">
</div>
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Proposed Development</label> <input type="text" class="form-control form-control-sm" value="Development Permit">
</div>
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Date of Inspection</label> <input type="date" class="form-control form-control-sm">
</div>
</div>
<h6 class="text-primary fw-bold border-bottom pb-2 mb-3">2. Site Identification & Boundaries</h6>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Boundary Verification (Clear/Match Plan?)</label> <select class="form-select form-select-sm"><option>Yes</option><option>No</option></select>
</div>
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Beacon Check (Fixed and visible?)</label> <select class="form-select form-select-sm"><option>Yes</option><option>No</option></select>
</div>
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Land Ownership Document Verified?</label> <select class="form-select form-select-sm"><option>Yes</option><option>No</option></select>
</div>
</div>
<h6 class="text-primary fw-bold border-bottom pb-2 mb-3">3. Zoning & Planning Compliance</h6>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Zoning Status Conformity</label> <input type="text" class="form-control form-control-sm" placeholder="e.g., residential, commercial">
</div>
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Setbacks Met?</label> <input type="text" class="form-control form-control-sm" placeholder="Distance from roads, etc.">
</div>
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Access to Site (Right of way)</label> <input type="text" class="form-control form-control-sm" placeholder="Is there an accessible road?">
</div>
</div>
<h6 class="text-primary fw-bold border-bottom pb-2 mb-3">4. Site Suitability & Environmental Factors</h6>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Topography (Flooding/Erosion)</label> <input type="text" class="form-control form-control-sm">
</div>
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Proximity to Hazards</label> <input type="text" class="form-control form-control-sm" placeholder="Distance to high-tension, gutters">
</div>
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Drainage/Waste Management</label> <input type="text" class="form-control form-control-sm" placeholder="Stormwater plan">
</div>
</div>
<div class="html2pdf__page-break"></div>
<h6 class="text-primary fw-bold border-bottom pb-2 mb-3">5. Technical & Structural Observations</h6>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Foundation Status</label> <input type="text" class="form-control form-control-sm" placeholder="Suitability of soil capacity">
</div>
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Structural Integrity</label> <input type="text" class="form-control form-control-sm" placeholder="Quality of materials">
</div>
<div class="col-md-4">
<label class="form-label text-muted small mb-1">Workmanship Supervised?</label> <select class="form-select form-select-sm"><option>N/A</option><option>Yes</option><option>No</option></select>
</div>
</div>
<h6 class="text-primary fw-bold border-bottom pb-2 mb-3">6. Official Recommendations & Sign-Off</h6>
<div class="row g-3 mb-4">
<div class="col-12">
<label class="form-label text-muted small mb-1">Inspector's Findings</label> <textarea class="form-control form-control-sm" rows="2" placeholder="e.g., Site boundaries are correct..."></textarea>
</div>
<div class="col-12">
<label class="form-label text-muted small mb-1">Recommendations</label> <select class="form-select form-select-sm w-50">
<option>Approve</option>
<option>Query</option>
<option>Reject</option>
</select>
</div>
<div class="col-md-6 mt-4">
<label class="form-label text-muted small mb-1">Physical Planning Officer</label> <input type="text" class="form-control form-control-sm mb-2" placeholder="Name">
<input type="text" class="form-control form-control-sm" placeholder="Signature (Digital)">
</div>
<div class="col-md-6 mt-4">
<label class="form-label text-muted small mb-1">Works Department Engineer</label> <input type="text" class="form-control form-control-sm mb-2" placeholder="Name">
<input type="text" class="form-control form-control-sm" placeholder="Signature (Digital)">
</div>
</div>
</div>
<div class="modal-footer bg-light border-top">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<!-- <button type="button" class="btn btn-success" id="downloadPdfBtn"><i class="bi bi-download"></i> Download PDF
</button> -->
<button type="button" class="btn btn-success" onclick="generateReportPDF()">
<i class="bi bi-download"></i> Download PDF
</button>
<button type="button" class="btn btn-primary">Save Report</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,466 @@
@extends('layouts.master')
@section('page-title')
Permits | {{ $page_title }}
@endsection
@section('page-css')
<link rel="stylesheet" type="text/css" href="{{ url('public/assets/css/permit_show.css') }}">
@endsection
@section('page-content')
@include('permits.partials.pdf-modal')
@include('layouts.partials.permits-navbar')
@include('permits.partials.site-inspection')
<div class="container">
<div class="row">
<div class="col-md-7">
<div class="d-flex justify-content-between align-items-center mb-4 mt-2">
<h1 class="display-6 fw-bold text-dark">{{ $permit_arr['application_code'] }} <span class="badge bg-warning text-dark">{{ $permit_arr['status'] }}</span></h1>
</div>
<!-- Map Area -->
<!-- <div class="map-container mb-4"> -->
<!-- <div id="applicationmap" class="applicationmap" style="width: 100%; height: 100vH;"></div> -->
<!-- <div><img src="{{ url('public/assets/images/grey-map-with-location-pin.jpg') }}" height="500px" width="801px"></div> -->
<!-- <img src="https://placeholdit.com/900x200/dddddd/999999?text=Project+Location" alt="Project Location" class="img-fluid" /> -->
<!-- </div> -->
<div class="map-container mb-4">
<iframe id="permit-map"
src="https://pwa.lupmis4luspa.org/embed.php?mode=permit&application_code={{ urlencode($permit_arr['application_code']) }}{{ !empty($permit_arr['lon']) ? '&lon='.urlencode($permit_arr['lon']).'&lat='.urlencode($permit_arr['lat']) : '' }}{{ !empty($permit_arr['upn']) ? '&upn='.urlencode($permit_arr['upn']) : '' }}"
style="width:100%;height:500px;border:0;border-radius:8px;"
allow="geolocation; clipboard-write"
referrerpolicy="strict-origin-when-cross-origin"
loading="lazy"
title="Permit location map">
</iframe>
</div>
<!-- Applicant & Property Details -->
<div class="remarks-section">
<h3 class="section-title">Actual Project Location</h3>
<div class="row mb-3">
<label for="permitLocation" class="form-label fw-bold text-primary">Coordinates *</label>
<input type="text" name="permit_location" id="permitLocation" class="form-control">
</div>
<div class="row mb-3">
<label for="permitUPN" class="form-label fw-bold text-primary">UPN</label>
<input type="text" name="permit_upn" id="permitUPN" class="form-control">
</div>
<div class="row mb-3">
<div class="d-flex gap-1">
<button class="btn btn-warning btn-sm flex-fill" id="checkComplianceBtn"><i class="fa-solid fa-map"></i> Check Compliance</button>
</div>
</div>
<div class="row mb-3">
<label for="processingFee" class="form-label fw-bold text-primary">Permit Fee *</label>
<input type="text" name="processing_fees" id="processingFee" class="form-control">
</div>
<div class="row mb-3">
<div class="d-flex gap-1">
<button class="btn btn-primary flex-fill"><i class="fa-solid fa-paper-plane"></i> Submit</button>
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#inspectionReportModal">
<i class="bi bi-file-earmark-text"></i> Site Inspection Report
</button>
</div>
</div>
</div>
</div>
<div class="col-md-5">
<!-- Remarks -->
<div class="details-section mt-2">
<h2 class="section-title text-primary">
Applicant & Property Details
</h2>
<div class="row">
<div class="col-md-12">
<!-- <h6 class="detail-label">Project Description</h6> -->
<p class="detail-value"></p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Name</h6>
<p class="detail-value">{{ $permit_arr['applicant_name'] }}</p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Nationality</h6>
<p class="detail-value">{{ $permit_arr['nationality'] }}</p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Email</h6>
<p class="detail-value">{{ $permit_arr['email'] }}</p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Telephone Number</h6>
<p class="detail-value">{{ $permit_arr['phone'] }}</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h6 class="detail-label">Address</h6>
<p class="detail-value">{{ $permit_arr['address'] }}, {{ $permit_arr['city'] }}, {{ $permit_arr['region'] }}</p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Project Location</h6>
<p class="detail-value">{{ $permit_arr['project_location'] }}</p>
</div>
<!-- <div class="col-md-4">
<h6 class="detail-label">Parcel Size</h6>
<p class="detail-value">0.25 Acre</p>
</div> -->
<div class="col-md-6">
<h6 class="detail-label">Permit Type</h6>
<p class="detail-value">{{ $permit_arr['permit_type'] }}</p>
</div>
<div class="col-md-12">
<h6 class="detail-label">Description</h6>
<p class="detail-value">{{ $permit_arr['project_description'] }}</p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Purpose</h6>
<p class="detail-value">{{ $permit_arr['land_request_use'] }}</p>
</div>
</div>
</div>
<!-- Planning Permit Application -->
<div class="details-section mt-4">
<h2 class="section-title text-primary">Documents</h2>
<div class="row">
<div class="col-md-6">
<!-- <h6 class="detail-label">Architectural Drawings</h6> -->
<p class="detail-value">
<a href="#" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#pdfModal" data-pdf-url="{{ route('view.pdf', ['filename' => 'site_plan.pdf']) }}">
<i class="fa-solid fa-paperclip"></i> Site Plan
</a>
<!-- <a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Site Plan</a> -->
</p>
<p class="detail-value">
<!-- <a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Architectural Drawings</a> -->
<a href="#" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#pdfModal" data-pdf-url="{{ route('view.pdf', ['filename' => 'architectural_drawing.pdf']) }}">
<i class="fa-solid fa-paperclip"></i> Architectural Drawings
</a>
</p>
<!-- <p class="detail-value"><a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Working Drawing</a></p> -->
</div>
<div class="col-md-6">
<!-- <h6 class="detail-label">Others</h6> -->
<p class="detail-value">
<!-- <a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Block Plan</a> -->
<a href="#" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#pdfModal" data-pdf-url="{{ route('view.pdf', ['filename' => 'block_plan.pdf']) }}">
<i class="fa-solid fa-paperclip"></i> Block Plan
</a>
</p>
</div>
<div class="col-md-6">
<!-- <h6 class="detail-label">Architectural Drawings</h6> -->
<!-- <p class="detail-value"><a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Zoning Report</a></p> -->
<!-- <p class="detail-value"><a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Working Drawing</a></p> -->
</div>
<!-- <div class="col-md-6">
<p class="detail-value"><a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Site Plan</a></p>
<p class="detail-value"><a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Title Certificate</a></p>
</div> -->
</div>
</div>
<div class="details-section mt-4">
<h2 class="section-title text-primary">Duration</h2>
<div class="row">
<div class="col-md-6">
<h6 class="detail-label">Date of Application</h6>
<p class="detail-value">
<?php
$originalDate = "2023-05-31 01:16:06";
$unixTime = strtotime($permit_arr['submitted_at']);
$normal_datetime = date("jS F, Y @ g:i A", $unixTime);
echo $normal_datetime;
?>
</p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Processing Days Remaining</h6>
<p class="detail-value">
<span class="badge bg-warning text-dark">
<?php
if ($permit_arr['status'] == 'submitted') {
echo "N/A";
}
?>
</span>
</p>
</div>
</div>
</div>
<div class="remarks-section">
<h3 class="section-title">Status & Comments</h3>
<div class="row">
<div class="col-md-12">
@include('permits.partials.comments-timeline')
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<label for="permitCurrentStatus" class="form-label fw-bold text-primary"><i class="fa-solid fa-temperature-three-quarters"></i> Status Update</label>
{{-- @if(in_array(session('current_user.ua_position'), $allowed_users_to_comment)) --}}
<select class="form-select form-select-lgdd" id="permitCurrentStatus">
<option value="" selected disabled>Select status ...</option>
<option value="status_five">Applicant to Review</option>
<option value="status_one">Application Received</option>
<option value="status_two" disabled>Application Accepted</option>
<option value="status_three" disabled>Permit Pending TSC Review</option>
<option value="status_four" disabled>Permit Pending SPC Decision</option>
<option value="status_five" disabled>Permit Approved (In Principle)</option>
<option value="status_size" disabled>Permit Approved</option>
<option value="status_seven" disabled>Permit Refused</option>
<option value="status_eight" disabled>Permit Deferred</option>
</select>
{{-- @else --}}
<!-- <p class="badge bg-warning text-dark">You don't have the right permission to update the status.</p> -->
{{-- @endif-- }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
{{-- @if(in_array(session('current_user.ua_position'), $allowed_users_to_comment)) --}}
<textarea class="form-control mb-3 commentBody" rows="4" name="comment_body" placeholder="Enter new comment here" ></textarea>
<input type="hidden" class="applicationCode" name="application_code" value="{{ $permit_arr['application_code'] }}">
{{-- @else --}}
<!-- <p class="badge bg-warning text-dark">You don't have the right permission to add comments.</p> -->
{{-- @endif --}}
</div>
</div>
<div class="row mb-3">
<div class="d-flex gap-1">
<button class="btn btn-primary flex-fill submitPermitCommentBtn"><i class="fa-solid fa-paper-plane"></i> Submit</button>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('page-js')
<!-- <script src="{{ url('public/assets/libs/select2/dist/js/select2.full.min.js') }}" type="text/javascript" ></script> -->
<!-- <script src="{{ url('public/assets/js/permit_tools.js') }}" type="text/javascript" ></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script src="{{ url('public/assets/js/permit_comments.js') }}" type="text/javascript" ></script>
<script type="text/javascript">
$(document).ready(function(){
// Initial load
var pdfModal = document.getElementById('pdfModal');
pdfModal.addEventListener('show.bs.modal', function (event) {
// Button that triggered the modal
var button = event.relatedTarget;
// Extract info from data-bs-* attributes
var pdfUrl = button.getAttribute('data-pdf-url');
// Update the modal's iframe src
var pdfIframe = document.getElementById('pdfIframe');
pdfIframe.src = pdfUrl;
});
// Optional: Clear the iframe src when the modal is closed to stop the stream
pdfModal.addEventListener('hidden.bs.modal', function () {
var pdfIframe = document.getElementById('pdfIframe');
pdfIframe.src = '';
});
const MAP_ORIGIN = 'https://pwa.lupmis4luspa.org';
const iframe = document.getElementById('permit-map');
const upnInput = document.getElementById('permitUPN');
const locInput = document.getElementById('permitLocation');
window.addEventListener('message', function (event) {
if (event.origin !== MAP_ORIGIN) return;
const msg = event.data;
if (!msg || typeof msg !== 'object') return;
switch (msg.type) {
case 'ready':
@if(!empty($permit_arr['upn']))
iframe.contentWindow.postMessage(
{ type: 'set:selected', upn: @json($permit_arr['upn']) },
MAP_ORIGIN);
@endif
break;
case 'parcel:select':
upnInput.value = msg.upn || '';
locInput.value = (msg.lon != null && msg.lat != null)
? msg.lat.toFixed(6) + ', ' + msg.lon.toFixed(6)
: '';
upnInput.dataset.parcelId = msg.parcel_id || '';
upnInput.dataset.zoneCode = msg.zone_code || '';
break;
case 'parcel:cleared':
upnInput.value = '';
locInput.value = '';
delete upnInput.dataset.parcelId;
delete upnInput.dataset.zoneCode;
break;
case 'error':
console.warn('[permit-map]', msg.code, msg.message);
break;
}
$('#checkComplianceBtn').on('click', function (e) {
e.preventDefault();
const payload = {
application_code: @json($permit_arr['application_code']),
upn: upnInput.value,
parcel_id: upnInput.dataset.parcelId || null,
coordinates: locInput.value,
zone_code: upnInput.dataset.zoneCode || null,
};
$.ajax({ url: '{{ route("permits.checkCompliance") }}',
method: 'POST', data: payload })
.done(function (res) { /* render compliance result */ });
});
});
document.addEventListener('DOMContentLoaded', function() {
const downloadBtn = document.getElementById('downloadPdfBtn');
if(downloadBtn) {
downloadBtn.addEventListener('click', function() {
const element = document.getElementById('report-pdf-content');
const inputs = element.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
if (input.tagName === 'TEXTAREA') {
input.innerHTML = input.value;
} else if (input.tagName === 'SELECT') {
const selectedOption = input.options[input.selectedIndex];
if (selectedOption) selectedOption.setAttribute('selected', 'selected');
} else {
input.setAttribute('value', input.value);
}
});
const originalText = downloadBtn.innerHTML;
downloadBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Generating...';
downloadBtn.disabled = true;
// Configure and generate PDF
const opt = {
margin: 0.5,
filename: 'Site_Inspection_Report.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, logging: false },
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' }
};
html2pdf().set(opt).from(element).save().then(() => {
downloadBtn.innerHTML = originalText;
downloadBtn.disabled = false;
});
});
}
});
window.generateReportPDF = function() {
console.log("1. Button clicked. Starting PDF process...");
if (typeof html2pdf === 'undefined') {
alert("ERROR: The html2pdf library is not loaded.");
return;
}
const element = document.getElementById('report-pdf-content');
if (!element) {
alert("ERROR: Could not find the div with id='report-pdf-content'.");
return;
}
try {
const inputs = element.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
if (input.tagName === 'TEXTAREA') {
input.innerHTML = input.value;
} else if (input.tagName === 'SELECT') {
const selectedOption = input.options[input.selectedIndex];
if (selectedOption) selectedOption.setAttribute('selected', 'selected');
input.style.backgroundImage = 'none';
input.style.appearance = 'none'; // Removes native browser arrows
} else {
input.setAttribute('value', input.value);
}
});
console.log("3. Form data locked and SVGs removed. Generating file...");
const opt = {
margin: 0.5,
filename: 'Site_Inspection_Report.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
logging: false,
scrollY: 0,
windowY: 0
},
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' },
pagebreak: {
mode: 'css',
avoid: ['.row', 'h6']
}
};
// Generate PDF
html2pdf().set(opt).from(element).save().then(() => {
console.log("Success: PDF downloaded.");
// THE RESTORE: Put the arrows back on the screen after download finishes
inputs.forEach(input => {
if (input.tagName === 'SELECT') {
input.style.backgroundImage = '';
input.style.appearance = '';
}
});
}).catch(err => {
console.error("PDF Generation Error:", err);
alert("Failed to build the PDF. Check the browser console.");
});
} catch (error) {
console.error("Critical Execution Error:", error);
alert("A javascript error occurred: " + error.message);
}
}
});
</script>
@endsection

View File

@ -0,0 +1,545 @@
@extends('layouts.master')
@section('page-title')
Permits | {{ $page_title }}
@endsection
@section('page-css')
<link rel="stylesheet" type="text/css" href="{{ url('public/assets/css/permit_show.css') }}">
@endsection
@section('page-content')
@include('permits.partials.pdf-modal')
@include('layouts.partials.permits-navbar')
@include('permits.partials.site-inspection')
<div class="container">
<div class="row">
<div class="col-md-7">
<div class="d-flex justify-content-between align-items-center mb-4 mt-2">
<h1 class="display-6 fw-bold text-dark">{{ $permit_arr['application_code'] }} <span class="badge bg-warning text-dark">{{ $permit_arr['status'] }}</span></h1>
</div>
<!-- Map Area -->
<!-- <div class="map-container mb-4"> -->
<!-- <div id="applicationmap" class="applicationmap" style="width: 100%; height: 100vH;"></div> -->
<!-- <div><img src="{{ url('public/assets/images/grey-map-with-location-pin.jpg') }}" height="500px" width="801px"></div> -->
<!-- <img src="https://placeholdit.com/900x200/dddddd/999999?text=Project+Location" alt="Project Location" class="img-fluid" /> -->
<!-- </div> -->
<div class="map-container mb-4">
<iframe id="permit-map"
src="https://pwa.lupmis4luspa.org/embed.php?mode=permit&application_code={{ urlencode($permit_arr['application_code']) }}{{ !empty($permit_arr['lon']) ? '&lon='.urlencode($permit_arr['lon']).'&lat='.urlencode($permit_arr['lat']) : '' }}{{ !empty($permit_arr['upn']) ? '&upn='.urlencode($permit_arr['upn']) : '' }}"
style="width:100%;height:500px;border:0;border-radius:8px;"
allow="geolocation; clipboard-write"
referrerpolicy="strict-origin-when-cross-origin"
loading="lazy"
title="Permit location map">
</iframe>
</div>
<!-- Applicant & Property Details -->
<div class="remarks-section">
<h3 class="section-title">Actual Project Location & Fees</h3>
<div class="card border border-light-subtle shadow-sm mb-4" style="background-color: #f8f9fc;">
<div class="card-body p-4">
<h6 class="fw-bold text-uppercase text-secondary mb-4" style="letter-spacing: 0.5px; font-size: 0.85rem;">
<!-- Actual Project Location -->
</h6>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label fw-bold text-secondary small mb-1">
Coordinates <br class="d-none d-md-block"> (Latitude, Longitude) <span class="text-danger">*</span>
</label>
<div class="input-group shadow-sm">
<span class="input-group-text bg-white border-end-0 text-muted font-monospace small pe-1"></span>
<input type="text" class="form-control border-start-0 ps-1 font-monospace" placeholder="" value="">
</div>
</div>
<div class="col-md-4">
<label class="form-label fw-bold text-secondary small mb-1">
<br class="d-none d-md-block">UPN (Unique Parcel Number)
</label>
<input type="text" class="form-control shadow-sm" value="">
</div>
<div class="col-md-4">
<label class="form-label fw-bold text-secondary small mb-1">
Permit <br class="d-none d-md-block"> Fee (GH₵) <span class="text-danger"></span>
</label>
<input type="number" class="form-control shadow-sm fw-bold" value="1250">
</div>
</div>
<div class="d-flex flex-wrap gap-2">
<!-- <button type="button" class="btn fw-bold px-4 text-dark shadow-sm" style="background-color: #ffb400; border-color: #ffb400;">
Submit
</button> -->
<button type="button" class="btn btn-primary fw-bold px-4 shadow-sm">
<i class="bi bi-map me-1"></i> Submit
</button>
</div>
</div>
</div>
</div>
<div class="remarks-section card border border-light-subtle shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex align-items-center border-bottom pb-3 mb-4">
<i class="fa-solid fa-comments text-primary fs-5 me-2"></i>
<h6 class="fw-bold mb-0 text-dark" style="font-size: 1.1rem;">Status & Comments</h6>
</div>
<div class="mb-4">
<button class="btn btn-outline-secondary w-100" type="button" data-bs-toggle="offcanvas" data-bs-target="#historyOffcanvas" aria-controls="historyOffcanvas">
<i class="bi bi-clock-history"></i> View Existing Comments
</button>
</div>
<div class="mb-4">
<label for="permitCurrentStatus" class="form-label fw-bold text-secondary small mb-2" style="letter-spacing: 0.5px;">
<i class="bi bi-option text-primary me-1"></i> Update Status Here
</label>
<select class="form-select shadow-sm" id="permitCurrentStatus">
<option value="" selected disabled>Select status ...</option>
<option value="status_zero">Applicant to Review</option>
<option value="status_one">Application Received</option>
<option value="status_two" disabled>Application Accepted</option>
<option value="status_three" disabled>Permit Pending TSC Review</option>
<option value="status_four" disabled>Permit Pending SPC Decision</option>
<option value="status_five" disabled>Permit Approved (In Principle)</option>
<option value="status_six" disabled>Permit Approved</option>
<option value="status_seven" disabled>Permit Refused</option>
<option value="status_eight" disabled>Permit Deferred</option>
</select>
</div>
<div class="mb-4">
<textarea class="form-control shadow-sm commentBody" rows="4" name="comment_body" placeholder="Enter new comment here"></textarea>
<input type="hidden" class="applicationCode" name="application_code" value="{{ $permit_arr['application_code'] }}">
</div>
<button class="btn btn-primary w-100 fw-bold shadow-sm submitPermitCommentBtn" type="button">
<i class="fa-solid fa-paper-plane me-1"></i> Submit
</button>
</div>
</div>
</div>
<div class="col-md-5">
<!-- Remarks -->
<div class="details-section mt-2">
<div class="card border border-light-subtle shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex align-items-center border-bottom pb-3 mb-4">
<i class="bi bi-person text-primary fs-5 me-2"></i>
<h6 class="fw-bold mb-0 text-dark" style="font-size: 1.1rem;">Applicant & Property Details</h6>
</div>
<div class="row gy-4">
<div class="col-12">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">
Applicant Name:
</div>
<div class="fw-bold text-dark fs-5">{{ $permit_arr['applicant_name'] }}</div>
</div>
<div class="col-md-6">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">
Nationality:
</div>
<div class="text-dark fw-medium">{{ $permit_arr['nationality'] }}</div>
</div>
<div class="col-md-6">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">
Email Address:
</div>
<div>
<a href="" class="text-decoration-none text-primary">
{{ $permit_arr['email'] }}
</a>
</div>
</div>
<div class="col-md-6">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">
Telephone Number:
</div>
<div class="text-dark">{{ $permit_arr['phone'] }}</div>
</div>
<div class="col-md-6">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">
Project Location:
</div>
<div class="text-dark fw-bold">{{ $permit_arr['project_location'] }}</div>
</div>
<div class="col-12">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">
Mailing Address:
</div>
<div class="text-dark">{{ $permit_arr['address'] }}, {{ $permit_arr['city'] }}, {{ $permit_arr['region'] }}</div>
</div>
<div class="col-md-6">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">
Permit Type Requested
</div>
<div class="text-dark fw-bold">{{ $permit_arr['permit_type'] }}</div>
</div>
<div class="col-md-6">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">
Purpose
</div>
<div class="fw-bold" style="color: #e67e22;">{{ $permit_arr['land_request_use'] }}</div>
</div>
<div class="col-12 mt-2">
<div class="border border-dark-subtle rounded p-3">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">
Project Scope:
</div>
<div class="text-dark fst-italic" style="font-size: 0.95rem;">{{ $permit_arr['project_description'] }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="card border border-light-subtle shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex align-items-center border-bottom pb-3 mb-4">
<i class="bi bi-file-earmark-text text-primary fs-5 me-2"></i>
<h6 class="fw-bold mb-0 text-dark" style="font-size: 1.1rem;">Attached CAD Drawings & Documents</h6>
</div>
<div class="d-flex flex-column gap-3">
<div class="border border-light-subtle rounded p-3 d-flex align-items-center justify-content-between bg-light shadow-sm transition-all">
<div class="d-flex align-items-center text-truncate pe-3">
<i class="bi bi-file-earmark text-primary fs-4 me-3"></i>
<div class="text-truncate">
<div class="text-dark fw-medium" style="font-size: 0.95rem;">Site Plan Drawing</div>
<div class="text-secondary font-monospace text-uppercase" style="font-size: 0.75rem; letter-spacing: 0.5px;">
LUSPA_TAIFA_SP3_DRAFT.jpg
</div>
</div>
</div>
<a href="#" class="text-decoration-none fw-semibold text-nowrap" style="font-size: 0.85rem;">
Inspect CAD <i class="bi bi-arrow-up-right ms-1"></i>
</a>
</div>
<div class="border border-light-subtle rounded p-3 d-flex align-items-center justify-content-between bg-light shadow-sm transition-all">
<div class="d-flex align-items-center text-truncate pe-3">
<i class="bi bi-file-earmark text-primary fs-4 me-3"></i>
<div class="text-truncate">
<div class="text-dark fw-medium" style="font-size: 0.95rem;">Block Plan Drawing</div>
<div class="text-secondary font-monospace text-uppercase" style="font-size: 0.75rem; letter-spacing: 0.5px;">
LUSPA_TAIFA_BLOCK2_A.jpg
</div>
</div>
</div>
<a href="#" class="text-decoration-none fw-semibold text-nowrap" style="font-size: 0.85rem;">Inspect CAD <i class="bi bi-arrow-up-right ms-1"></i>
</a>
</div>
<div class="border border-light-subtle rounded p-3 d-flex align-items-center justify-content-between bg-light shadow-sm transition-all">
<div class="d-flex align-items-center text-truncate pe-3">
<i class="bi bi-file-earmark text-primary fs-4 me-3"></i>
<div class="text-truncate">
<div class="text-dark fw-medium" style="font-size: 0.95rem;">Architectural Drawings</div>
<div class="text-secondary font-monospace text-uppercase" style="font-size: 0.75rem; letter-spacing: 0.5px;">
TAIFA_RESID_ELEVATION.jpg
</div>
</div>
</div>
<a href="#" class="text-decoration-none fw-semibold text-nowrap" style="font-size: 0.85rem;">
Inspect CAD <i class="bi bi-arrow-up-right ms-1"></i>
</a>
</div>
</div>
</div>
</div>
<div class="card border border-light-subtle shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start border-bottom pb-3 mb-4">
<div class="d-flex align-items-center">
<i class="bi bi-clock text-primary fs-5 me-2"></i>
<h6 class="fw-bold mb-0 text-dark" style="font-size: 1.1rem;">Processing & Duration</h6>
</div>
<div class="text-end">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">
Remaining Days
</div>
<span class="badge text-dark fs-6 px-3 py-1" style="background-color: #fce4b4;">N/A</span>
</div>
</div>
<div class="overflow-auto pe-2" style="max-height: 450px;">
<div class="planning-stepper">
<div class="stepper-item">
<div class="stepper-dot active"></div>
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">
Applicant to Review
</div>
<h6 class="fw-bold mb-1" style="color: #e67e22;">Queried</h6>
<p class="mb-2 small text-secondary">
Applicant was prompted to review their application and update.
</p>
<span class="badge bg-light text-secondary border fw-medium px-2 py-1">Updated by: PPD Head</span>
</div>
<div class="stepper-item">
<div class="stepper-dot completed"></div>
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">
<?php
$unixTime = strtotime($permit_arr['submitted_at']);
$normal_datetime = date("jS F, Y @ g:i A", $unixTime);
?>
</div>
<h6 class="fw-bold mb-1 text-dark">Application Submitted</h6>
<p class="mb-2 small text-secondary">
Application successfully submitted on LUPMIS Portal
</p>
<!-- <span class="badge bg-light text-secondary border fw-medium px-2 py-1">Verified by: Applicant Portal</span> -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@include('permits.partials.comments-timeline')
@endsection
@section('page-js')
<!-- <script src="{{ url('public/assets/libs/select2/dist/js/select2.full.min.js') }}" type="text/javascript" ></script> -->
<!-- <script src="{{ url('public/assets/js/permit_tools.js') }}" type="text/javascript" ></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script src="{{ url('public/assets/js/permit_comments.js') }}" type="text/javascript" ></script>
<script type="text/javascript">
$(document).ready(function(){
// Initial load
var pdfModal = document.getElementById('pdfModal');
pdfModal.addEventListener('show.bs.modal', function (event) {
// Button that triggered the modal
var button = event.relatedTarget;
// Extract info from data-bs-* attributes
var pdfUrl = button.getAttribute('data-pdf-url');
// Update the modal's iframe src
var pdfIframe = document.getElementById('pdfIframe');
pdfIframe.src = pdfUrl;
});
// Optional: Clear the iframe src when the modal is closed to stop the stream
pdfModal.addEventListener('hidden.bs.modal', function () {
var pdfIframe = document.getElementById('pdfIframe');
pdfIframe.src = '';
});
const MAP_ORIGIN = 'https://pwa.lupmis4luspa.org';
const iframe = document.getElementById('permit-map');
const upnInput = document.getElementById('permitUPN');
const locInput = document.getElementById('permitLocation');
window.addEventListener('message', function (event) {
if (event.origin !== MAP_ORIGIN) return;
const msg = event.data;
if (!msg || typeof msg !== 'object') return;
switch (msg.type) {
case 'ready':
@if(!empty($permit_arr['upn']))
iframe.contentWindow.postMessage(
{ type: 'set:selected', upn: @json($permit_arr['upn']) },
MAP_ORIGIN);
@endif
break;
case 'parcel:select':
upnInput.value = msg.upn || '';
locInput.value = (msg.lon != null && msg.lat != null)
? msg.lat.toFixed(6) + ', ' + msg.lon.toFixed(6)
: '';
upnInput.dataset.parcelId = msg.parcel_id || '';
upnInput.dataset.zoneCode = msg.zone_code || '';
break;
case 'parcel:cleared':
upnInput.value = '';
locInput.value = '';
delete upnInput.dataset.parcelId;
delete upnInput.dataset.zoneCode;
break;
case 'error':
console.warn('[permit-map]', msg.code, msg.message);
break;
}
$('#checkComplianceBtn').on('click', function (e) {
e.preventDefault();
const payload = {
application_code: @json($permit_arr['application_code']),
upn: upnInput.value,
parcel_id: upnInput.dataset.parcelId || null,
coordinates: locInput.value,
zone_code: upnInput.dataset.zoneCode || null,
};
$.ajax({ url: '{{ route("permits.checkCompliance") }}',
method: 'POST', data: payload })
.done(function (res) { /* render compliance result */ });
});
});
document.addEventListener('DOMContentLoaded', function() {
const downloadBtn = document.getElementById('downloadPdfBtn');
if(downloadBtn) {
downloadBtn.addEventListener('click', function() {
const element = document.getElementById('report-pdf-content');
const inputs = element.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
if (input.tagName === 'TEXTAREA') {
input.innerHTML = input.value;
} else if (input.tagName === 'SELECT') {
const selectedOption = input.options[input.selectedIndex];
if (selectedOption) selectedOption.setAttribute('selected', 'selected');
} else {
input.setAttribute('value', input.value);
}
});
const originalText = downloadBtn.innerHTML;
downloadBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Generating...';
downloadBtn.disabled = true;
// Configure and generate PDF
const opt = {
margin: 0.5,
filename: 'Site_Inspection_Report.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, logging: false },
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' }
};
html2pdf().set(opt).from(element).save().then(() => {
downloadBtn.innerHTML = originalText;
downloadBtn.disabled = false;
});
});
}
});
window.generateReportPDF = function() {
console.log("1. Button clicked. Starting PDF process...");
if (typeof html2pdf === 'undefined') {
alert("ERROR: The html2pdf library is not loaded.");
return;
}
const element = document.getElementById('report-pdf-content');
if (!element) {
alert("ERROR: Could not find the div with id='report-pdf-content'.");
return;
}
try {
const inputs = element.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
if (input.tagName === 'TEXTAREA') {
input.innerHTML = input.value;
} else if (input.tagName === 'SELECT') {
const selectedOption = input.options[input.selectedIndex];
if (selectedOption) selectedOption.setAttribute('selected', 'selected');
input.style.backgroundImage = 'none';
input.style.appearance = 'none'; // Removes native browser arrows
} else {
input.setAttribute('value', input.value);
}
});
console.log("3. Form data locked and SVGs removed. Generating file...");
const opt = {
margin: 0.5,
filename: 'Site_Inspection_Report.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
logging: false,
scrollY: 0,
windowY: 0
},
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' },
pagebreak: {
mode: 'css',
avoid: ['.row', 'h6']
}
};
// Generate PDF
html2pdf().set(opt).from(element).save().then(() => {
console.log("Success: PDF downloaded.");
// THE RESTORE: Put the arrows back on the screen after download finishes
inputs.forEach(input => {
if (input.tagName === 'SELECT') {
input.style.backgroundImage = '';
input.style.appearance = '';
}
});
}).catch(err => {
console.error("PDF Generation Error:", err);
alert("Failed to build the PDF. Check the browser console.");
});
} catch (error) {
console.error("Critical Execution Error:", error);
alert("A javascript error occurred: " + error.message);
}
}
});
</script>
@endsection

View File

@ -1,283 +1,310 @@
@extends('layouts.master') @extends('layouts.master')
@section('page-title') @section('page-title')
Permits | {{ $page_title }} Permits | {{ $page_title }}
@endsection @endsection
@section('page-css')
<style>
@media (max-width: 768px) {
#permit-map { height: 350px; }
}
</style>
@section('page-css')
<link rel="stylesheet" type="text/css" href="{{ url('public/assets/css/permit_show.css') }}">
<style>
/* TIMELINE & STEPPER CSS */
/* Vertical Stepper Line */
.planning-stepper { position: relative; padding-left: 1.25rem; }
.planning-stepper::before { content: ''; position: absolute; top: 6px; bottom: 0; left: 4px; width: 2px; background-color: #e9ecef; z-index: 0; }
.stepper-item { position: relative; padding-bottom: 1.75rem; }
.stepper-item:last-child { padding-bottom: 0; }
.stepper-dot { position: absolute; left: -1.25rem; top: 0.25rem; width: 10px; height: 10px; border-radius: 50%; z-index: 1; background-color: #dee2e6; }
.stepper-dot.completed { background-color: #20c997; }
.stepper-dot.active { background-color: #fd7e14; box-shadow: 0 0 0 5px rgba(253, 126, 20, 0.15); }
.stepper-dot.pending { background-color: #e2e8f0; }
.stepper-item.pending .step-title, .stepper-item.pending .step-desc { opacity: 0.6; }
/* Offcanvas Timeline CSS */
.timeline { position: relative; padding-left: 2rem; margin-top: 1rem; }
.timeline::before { content: ''; position: absolute; top: 0; bottom: 0; left: 0.85rem; width: 2px; background-color: #dee2e6; z-index: 0; }
.timeline-item { position: relative; margin-bottom: 1.5rem; }
.timeline-icon { position: absolute; top: 0; left: -2rem; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1rem; z-index: 1; }
.timeline-content { margin-left: 0.5rem; background-color: #fff; transition: all 0.2s ease; }
.timeline-content:hover { box-shadow: 0 .5rem 1rem rgba(0,0,0,.08)!important; }
</style>
@endsection @endsection
@section('page-content') @section('page-content')
@include('permits.partials.pdf-modal') @include('permits.partials.pdf-modal')
@include('layouts.partials.permits-navbar') @include('layouts.partials.permits-navbar')
<div class="container"> @include('permits.partials.site-inspection')
<div class="row">
<div class="col-md-7">
<div class="d-flex justify-content-between align-items-center mb-4 mt-2">
<h1 class="display-6 fw-bold text-dark">{{ $permit_arr['application_code'] }} <span class="badge bg-warning text-dark">{{ $permit_arr['status'] }}</span></h1>
</div>
<!-- Map Area --> <div class="container py-4">
<!-- <div class="map-container mb-4"> --> <div class="row g-4">
<!-- <div id="applicationmap" class="applicationmap" style="width: 100%; height: 100vH;"></div> -->
<!-- <div><img src="{{ url('public/assets/images/grey-map-with-location-pin.jpg') }}" height="500px" width="801px"></div> --> <div class="col-lg-7 col-md-12">
<!-- <img src="https://placeholdit.com/900x200/dddddd/999999?text=Project+Location" alt="Project Location" class="img-fluid" /> -->
<!-- </div> --> <div class="d-flex align-items-center mb-4 mt-2 gap-3">
<div class="map-container mb-4"> <h1 class="display-6 fw-bold text-dark mb-0">{{ $permit_arr['application_code'] }}</h1>
<span class="badge bg-warning text-dark fs-5 shadow-sm align-middle">{{ $permit_arr['status'] }}</span>
</div>
<div class="map-container mb-4 border border-light-subtle shadow-sm rounded overflow-hidden" style="height: 450px; background-color: #f8f9fc;">
<iframe id="permit-map" <iframe id="permit-map"
src="https://pwa.lupmis4luspa.org/embed?mode=permit&application_code={{ urlencode($permit_arr['application_code']) }}{{ !empty($permit_arr['lon']) ? '&lon='.urlencode($permit_arr['lon']).'&lat='.urlencode($permit_arr['lat']) : '' }}{{ !empty($permit_arr['upn']) ? '&upn='.urlencode($permit_arr['upn']) : '' }}" src="https://pwa.lupmis4luspa.org/embed.php?mode=permit&application_code={{ urlencode($permit_arr['application_code']) }}{{ !empty($permit_arr['lon']) ? '&lon='.urlencode($permit_arr['lon']).'&lat='.urlencode($permit_arr['lat']) : '' }}{{ !empty($permit_arr['upn']) ? '&upn='.urlencode($permit_arr['upn']) : '' }}"
style="width:100%;height:500px;border:0;border-radius:8px;" style="width:100%;height:500px;border:0;border-radius:8px;"
allow="geolocation; clipboard-write" allow="geolocation; clipboard-write"
referrerpolicy="strict-origin-when-cross-origin" referrerpolicy="strict-origin-when-cross-origin"
loading="lazy" loading="lazy"
title="Permit location map"></iframe> title="Permit location map">
</div> </iframe>
<!-- <div class="map-container mb-4 border border-light-subtle shadow-sm rounded p-5 text-center" style="background-color: #f8f9fc;">
<i class="bi bi-map text-secondary" style="font-size: 3rem;"></i>
<!-- Applicant & Property Details --> <h5 class="mt-3 text-dark">Map View Available</h5>
<div class="remarks-section"> <p class="text-muted">For security reasons, the interactive map must be opened in a secure window.</p>
<a href="https://pwa.lupmis4luspa.org/embed.php?mode=permit&application_code={{ urlencode($permit_arr['application_code']) }}..." target="_blank" class="btn btn-primary mt-2">
<i class="bi bi-box-arrow-up-right me-1"></i> Open Spatial Map
<h3 class="section-title">Actual Project Location</h3>
<div class="row mb-3">
<label for="permitLocation" class="form-label fw-bold text-primary">Coordinates *</label>
<input type="text" name="permit_location" id="permitLocation" class="form-control">
</div>
<div class="row mb-3">
<label for="permitUPN" class="form-label fw-bold text-primary">UPN</label>
<input type="text" name="permit_upn" id="permitUPN" class="form-control">
</div>
<div class="row mb-3">
<div class="d-flex gap-1">
<button class="btn btn-warning btn-sm flex-fill" id="checkComplianceBtn"><i class="fa-solid fa-map"></i> Check Compliance</button>
</div>
</div>
<div class="row mb-3">
<label for="processingFee" class="form-label fw-bold text-primary">Permit Fee *</label>
<input type="text" name="processing_fees" id="processingFee" class="form-control">
</div>
<div class="row mb-3">
<div class="d-flex gap-1">
<button class="btn btn-primary flex-fill"><i class="fa-solid fa-paper-plane"></i> Submit</button>
</div>
</div>
</div>
</div>
<div class="col-md-5">
<!-- Remarks -->
<div class="details-section mt-2">
<h2 class="section-title text-primary">
Applicant & Property Details
</h2>
<div class="row">
<div class="col-md-12">
<!-- <h6 class="detail-label">Project Description</h6> -->
<p class="detail-value"></p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Name</h6>
<p class="detail-value">{{ $permit_arr['applicant_name'] }}</p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Nationality</h6>
<p class="detail-value">{{ $permit_arr['nationality'] }}</p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Email</h6>
<p class="detail-value">{{ $permit_arr['email'] }}</p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Telephone Number</h6>
<p class="detail-value">{{ $permit_arr['phone'] }}</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h6 class="detail-label">Address</h6>
<p class="detail-value">{{ $permit_arr['address'] }}, {{ $permit_arr['city'] }}, {{ $permit_arr['region'] }}</p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Project Location</h6>
<p class="detail-value">{{ $permit_arr['project_location'] }}</p>
</div>
<!-- <div class="col-md-4">
<h6 class="detail-label">Parcel Size</h6>
<p class="detail-value">0.25 Acre</p>
</div> -->
<div class="col-md-6">
<h6 class="detail-label">Permit Type</h6>
<p class="detail-value">{{ $permit_arr['permit_type'] }}</p>
</div>
<div class="col-md-12">
<h6 class="detail-label">Description</h6>
<p class="detail-value">{{ $permit_arr['project_description'] }}</p>
</div>
<div class="col-md-6">
<h6 class="detail-label">Purpose</h6>
<p class="detail-value">{{ $permit_arr['land_request_use'] }}</p>
</div>
</div>
</div>
<!-- Planning Permit Application -->
<div class="details-section mt-4">
<h2 class="section-title text-primary">Documents</h2>
<div class="row">
<div class="col-md-6">
<!-- <h6 class="detail-label">Architectural Drawings</h6> -->
<p class="detail-value">
<a href="#" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#pdfModal" data-pdf-url="{{ route('view.pdf', ['filename' => 'site_plan.pdf']) }}">
<i class="fa-solid fa-paperclip"></i> Site Plan
</a> </a>
<!-- <a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Site Plan</a> -->
</p>
<p class="detail-value">
<!-- <a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Architectural Drawings</a> -->
<a href="#" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#pdfModal" data-pdf-url="{{ route('view.pdf', ['filename' => 'architectural_drawing.pdf']) }}">
<i class="fa-solid fa-paperclip"></i> Architectural Drawings
</a>
</p>
<!-- <p class="detail-value"><a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Working Drawing</a></p> -->
</div>
<div class="col-md-6">
<!-- <h6 class="detail-label">Others</h6> -->
<p class="detail-value">
<!-- <a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Block Plan</a> -->
<a href="#" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#pdfModal" data-pdf-url="{{ route('view.pdf', ['filename' => 'block_plan.pdf']) }}">
<i class="fa-solid fa-paperclip"></i> Block Plan
</a>
</p>
</div>
<div class="col-md-6">
<!-- <h6 class="detail-label">Architectural Drawings</h6> -->
<!-- <p class="detail-value"><a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Zoning Report</a></p> -->
<!-- <p class="detail-value"><a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Working Drawing</a></p> -->
</div>
<!-- <div class="col-md-6">
<p class="detail-value"><a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Site Plan</a></p>
<p class="detail-value"><a href="" class="btn btn-link"><i class="fa-solid fa-paperclip"></i> Title Certificate</a></p>
</div> --> </div> -->
</div> </div>
<div class="card border border-light-subtle shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex align-items-center border-bottom pb-3 mb-4">
<i class="bi bi-person text-primary fs-5 me-2"></i>
<h6 class="fw-bold mb-0 text-dark" style="font-size: 1.1rem;">Applicant & Property Details</h6>
</div> </div>
<div class="details-section mt-4"> <div class="row gy-4">
<h2 class="section-title text-primary">Duration</h2> <div class="col-12">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">Applicant Name:</div>
<div class="row"> <div class="fw-bold text-dark fs-5">FRANCIS BULLEN GAVOR</div>
<div class="col-md-6">
<h6 class="detail-label">Date of Application</h6>
<p class="detail-value">
<?php
$originalDate = "2023-05-31 01:16:06";
$unixTime = strtotime($permit_arr['submitted_at']);
$normal_datetime = date("jS F, Y @ g:i A", $unixTime);
echo $normal_datetime;
?>
</p>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h6 class="detail-label">Processing Days Remaining</h6> <div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">Nationality:</div>
<p class="detail-value"> <div class="text-dark fw-medium">Motswana</div>
<span class="badge bg-warning text-dark"> </div>
<?php <div class="col-md-6">
if ($permit_arr['status'] == 'submitted') { <div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">Email Address:</div>
echo "N/A"; <div><a href="mailto:jmadjanor6@gmail.com" class="text-decoration-none text-primary">jmadjanor6@gmail.com</a></div>
} </div>
?> <div class="col-md-6">
</span> <div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">Telephone Number:</div>
</p> <div class="text-dark">0554116836</div>
</div>
<div class="col-md-6">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">Zonal Location:</div>
<div class="text-dark fw-bold">TAIFA</div>
</div>
<div class="col-12">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">Mailing Address:</div>
<div class="text-dark">1 Tomatoes St, Hse 404/12, Accra, EASTERN REGION</div>
</div>
<div class="col-md-6">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">Permit Type Requested</div>
<div class="text-dark fw-bold">Development</div>
</div>
<div class="col-md-6">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">Zoned Classification</div>
<div class="fw-bold" style="color: #e67e22;">Mixed-Use Low Density Residential</div>
</div>
<div class="col-12 mt-2">
<div class="border border-dark-subtle rounded p-3">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.75rem; letter-spacing: 0.5px;">Project Scope:</div>
<div class="text-dark fst-italic" style="font-size: 0.95rem;">Proposed construction of multi-family residential building.</div>
</div> </div>
</div> </div>
</div> </div>
<div class="remarks-section"> </div>
<h3 class="section-title">Remarks</h3> </div>
<div class="row mb-3">
<label for="permitCurrentStatus" class="form-label fw-bold text-primary"><i class="fa-solid fa-temperature-three-quarters"></i> Status Update</label> <div class="card border border-light-subtle shadow-sm mb-4" style="background-color: #f8f9fc;">
@if(in_array(session('current_user.ua_position'), $allowed_users_to_comment)) <div class="card-body p-4">
<select class="form-select form-select-lg" id="permitCurrentStatus"> <h6 class="fw-bold text-uppercase text-secondary mb-4" style="letter-spacing: 0.5px; font-size: 0.85rem;">
<option value="" selected disabled>First select status ...</option> Actual Project Location & Cadastral Registration
<option value="status_five">Applicant to Review</option> </h6>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label fw-bold text-secondary small mb-1">
Coordinates <br class="d-none d-md-block"> (Latitude, Longitude) <span class="text-danger">*</span>
</label>
<div class="input-group shadow-sm">
<span class="input-group-text bg-white border-end-0 text-muted font-monospace small pe-1">GPS:</span>
<input type="text" class="form-control border-start-0 ps-1 font-monospace" placeholder="e.g., 5.66240, -0.18055" value="5.66240,">
</div>
</div>
<div class="col-md-4">
<label class="form-label fw-bold text-secondary small mb-1">
<br class="d-none d-md-block">UPN (Unique Parcel Number)
</label>
<input type="text" class="form-control shadow-sm" value="04-098-032-012">
</div>
<div class="col-md-4">
<label class="form-label fw-bold text-secondary small mb-1">
Permit Municipal <br class="d-none d-md-block">Review Fee (GH₵) <span class="text-danger">*</span>
</label>
<input type="number" class="form-control shadow-sm fw-bold" value="1250">
</div>
</div>
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn fw-bold px-4 text-dark shadow-sm" style="background-color: #ffb400; border-color: #ffb400;">
Check Spatial Buffers
</button>
<button type="button" class="btn btn-primary fw-bold px-4 shadow-sm">
<i class="bi bi-map me-1"></i> Save Geospatial Bounds
</button>
</div>
</div>
</div>
<div class="card border border-light-subtle shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex align-items-center border-bottom pb-3 mb-4">
<i class="bi bi-file-earmark-text text-primary fs-5 me-2"></i>
<h6 class="fw-bold mb-0 text-dark" style="font-size: 1.1rem;">Attached CAD Drawings & Documents</h6>
</div>
<div class="d-flex flex-column gap-3">
<div class="border border-light-subtle rounded p-3 d-flex align-items-center justify-content-between bg-light shadow-sm transition-all">
<div class="d-flex align-items-center text-truncate pe-3">
<i class="bi bi-file-earmark text-primary fs-4 me-3"></i>
<div class="text-truncate">
<div class="text-dark fw-medium" style="font-size: 0.95rem;">Site Plan Drawing</div>
<div class="text-secondary font-monospace text-uppercase" style="font-size: 0.75rem; letter-spacing: 0.5px;">LUSPA_TAIFA_SP3_DRAFT.jpg</div>
</div>
</div>
<a href="#" class="text-decoration-none fw-semibold text-nowrap" style="font-size: 0.85rem;">
Inspect CAD <i class="bi bi-arrow-up-right ms-1"></i>
</a>
</div>
<div class="border border-light-subtle rounded p-3 d-flex align-items-center justify-content-between bg-light shadow-sm transition-all">
<div class="d-flex align-items-center text-truncate pe-3">
<i class="bi bi-file-earmark text-primary fs-4 me-3"></i>
<div class="text-truncate">
<div class="text-dark fw-medium" style="font-size: 0.95rem;">Block Plan Drawing</div>
<div class="text-secondary font-monospace text-uppercase" style="font-size: 0.75rem; letter-spacing: 0.5px;">LUSPA_TAIFA_BLOCK2_A.jpg</div>
</div>
</div>
<a href="#" class="text-decoration-none fw-semibold text-nowrap" style="font-size: 0.85rem;">
Inspect CAD <i class="bi bi-arrow-up-right ms-1"></i>
</a>
</div>
<div class="border border-light-subtle rounded p-3 d-flex align-items-center justify-content-between bg-light shadow-sm transition-all">
<div class="d-flex align-items-center text-truncate pe-3">
<i class="bi bi-file-earmark text-primary fs-4 me-3"></i>
<div class="text-truncate">
<div class="text-dark fw-medium" style="font-size: 0.95rem;">Architectural Drawings</div>
<div class="text-secondary font-monospace text-uppercase" style="font-size: 0.75rem; letter-spacing: 0.5px;">TAIFA_RESID_ELEVATION.jpg</div>
</div>
</div>
<a href="#" class="text-decoration-none fw-semibold text-nowrap" style="font-size: 0.85rem;">
Inspect CAD <i class="bi bi-arrow-up-right ms-1"></i>
</a>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-5 col-md-12">
<div class="sticky-top" style="top: 1.5rem; z-index: 1020;">
<div class="card border border-light-subtle shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start border-bottom pb-3 mb-4">
<div class="d-flex align-items-center">
<i class="bi bi-clock text-primary fs-5 me-2"></i>
<h6 class="fw-bold mb-0 text-dark" style="font-size: 1.1rem;">Planning Stepper & Duration</h6>
</div>
<div class="text-end">
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">Remaining Days</div>
<span class="badge text-dark fs-6 px-3 py-1" style="background-color: #fce4b4;">N/A</span>
</div>
</div>
<div class="overflow-auto pe-2" style="max-height: 450px;">
<div class="planning-stepper">
<div class="stepper-item">
<div class="stepper-dot completed"></div>
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">28th Feb 2026, 11:59 PM LUPMIS ENGINE</div>
<h6 class="fw-bold mb-1 text-dark">Submission Received</h6>
<p class="mb-2 small text-secondary">Application successfully uploaded to LUPMIS database by applicant.</p>
<span class="badge bg-light text-secondary border fw-medium px-2 py-1">Verified by: Applicant Portal</span>
</div>
<div class="stepper-item">
<div class="stepper-dot active"></div>
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">PENDING PROCESS SCHEDULE LUPMIS ENGINE</div>
<h6 class="fw-bold mb-1" style="color: #e67e22;">Document Verification</h6>
<p class="mb-2 small text-secondary">Reviewing attached Site Plan, Block Plan, and Structural drawings consistency.</p>
<span class="badge bg-light text-secondary border fw-medium px-2 py-1">Verified by: Planning Officer</span>
</div>
<div class="stepper-item pending">
<div class="stepper-dot pending"></div>
<div class="text-uppercase text-secondary fw-semibold mb-1" style="font-size: 0.7rem; letter-spacing: 0.5px;">PENDING PROCESS SCHEDULE LUPMIS ENGINE</div>
<h6 class="fw-bold mb-1 step-title text-secondary">Spatial GIS Alignment</h6>
<p class="mb-2 small text-secondary step-desc">Verification of parcel boundaries against Taifa master plan & greenbelt buffers.</p>
</div>
</div>
</div>
</div>
</div>
<div class="remarks-section card border border-light-subtle shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex align-items-center border-bottom pb-3 mb-4">
<i class="fa-solid fa-comments text-primary fs-5 me-2"></i>
<h6 class="fw-bold mb-0 text-dark" style="font-size: 1.1rem;">Status & Comments</h6>
</div>
<div class="mb-4">
<button class="btn btn-outline-secondary w-100 fw-bold" type="button" data-bs-toggle="offcanvas" data-bs-target="#historyOffcanvas" aria-controls="historyOffcanvas">
<i class="bi bi-clock-history me-1"></i> View Full History & Comments
</button>
</div>
<div class="mb-4">
<label for="permitCurrentStatus" class="form-label fw-bold text-secondary small mb-2" style="letter-spacing: 0.5px;">
<i class="fa-solid fa-temperature-three-quarters text-primary me-1"></i> Status Update
</label>
<select class="form-select shadow-sm" id="permitCurrentStatus">
<option value="" selected disabled>Select status ...</option>
<option value="status_zero">Applicant to Review</option>
<option value="status_one">Application Received</option> <option value="status_one">Application Received</option>
<option value="status_two" disabled>Application Accepted</option> <option value="status_two" disabled>Application Accepted</option>
<option value="status_three" disabled>Permit Pending TSC Review</option> <option value="status_three" disabled>Permit Pending TSC Review</option>
<option value="status_four" disabled>Permit Pending SPC Decision</option>
<option value="status_five" disabled>Permit Approved (In Principle)</option>
<option value="status_size" disabled>Permit Approved</option>
<option value="status_seven" disabled>Permit Refused</option>
<option value="status_eight" disabled>Permit Deferred</option>
</select> </select>
@else
<p class="badge bg-success text-dark">You don't have the right permission to update the status.</p>
@endif
</div>
<div class="row mb-3">
@if(in_array(session('current_user.ua_position'), $allowed_users_to_comment))
<textarea class="form-control mb-3" rows="4" placeholder="Write a message" ></textarea>
@else
<p class="badge bg-info text-dark">You don't have the right permission to add comments.</p>
@endif
</div>
<div class="row mb-3">
<div class="d-flex gap-1">
<button class="btn btn-primary flex-fill"><i class="fa-solid fa-paper-plane"></i> Submit</button>
</div>
</div>
</div> </div>
<div class="mb-4">
<textarea class="form-control shadow-sm commentBody" rows="4" name="comment_body" placeholder="Enter new comment here"></textarea>
<input type="hidden" class="applicationCode" name="application_code" value="{{ $permit_arr['application_code'] }}">
</div> </div>
<button class="btn btn-primary w-100 fw-bold shadow-sm submitPermitCommentBtn" type="button">
<i class="fa-solid fa-paper-plane me-1"></i> Submit
</button>
</div> </div>
</div> </div>
</div> </div> </div>
</div>
<div class="offcanvas offcanvas-end shadow-lg" tabindex="-1" id="historyOffcanvas" aria-labelledby="historyOffcanvasLabel" style="width: 450px;">
<div class="offcanvas-header border-bottom bg-light sticky-top z-3">
<h5 class="offcanvas-title fw-bold" id="historyOffcanvasLabel">Application History</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="timeline" id="historyTimeline">
<div class="text-center text-muted mt-5">
<i class="bi bi-arrow-clockwise fs-1"></i>
<p class="mt-2">Loading application history...</p>
</div>
</div>
</div>
</div>
@endsection @endsection
@section('page-js')
<!-- <script src="{{ url('public/assets/libs/select2/dist/js/select2.full.min.js') }}" type="text/javascript" ></script> -->
@section('page-scripts')
<!-- <script src="{{ url('public/assets/js/permit_tools.js') }}" type="text/javascript" ></script> --> <script>
<script type="text/javascript">
$(document).ready(function(){ $(document).ready(function(){
var pdfModal = document.getElementById('pdfModal');
pdfModal.addEventListener('show.bs.modal', function (event) {
// Button that triggered the modal
var button = event.relatedTarget;
// Extract info from data-bs-* attributes
var pdfUrl = button.getAttribute('data-pdf-url');
// Update the modal's iframe src
var pdfIframe = document.getElementById('pdfIframe');
pdfIframe.src = pdfUrl;
});
// Optional: Clear the iframe src when the modal is closed to stop the stream
pdfModal.addEventListener('hidden.bs.modal', function () {
var pdfIframe = document.getElementById('pdfIframe');
pdfIframe.src = '';
});
const MAP_ORIGIN = 'https://pwa.lupmis4luspa.org'; const MAP_ORIGIN = 'https://pwa.lupmis4luspa.org';
const iframe = document.getElementById('permit-map'); const iframe = document.getElementById('permit-map');
const upnInput = document.getElementById('permitUPN'); const upnInput = document.getElementById('permitUPN');
const locInput = document.getElementById('permitLocation'); const locInput = document.getElementById('permitLocation');
// --- Receive events FROM the map ---
window.addEventListener('message', function (event) { window.addEventListener('message', function (event) {
if (event.origin !== MAP_ORIGIN) return; // SECURITY if (event.origin !== MAP_ORIGIN) return;
const msg = event.data; const msg = event.data;
if (!msg || typeof msg !== 'object') return; if (!msg || typeof msg !== 'object') return;
@ -310,10 +337,6 @@
console.warn('[permit-map]', msg.code, msg.message); console.warn('[permit-map]', msg.code, msg.message);
break; break;
} }
});
// --- Wire "Check Compliance" to use what the map gave us ---
// $('button:contains("Check Compliance")').on('click', function (e) {
$('#checkComplianceBtn').on('click', function (e) { $('#checkComplianceBtn').on('click', function (e) {
e.preventDefault(); e.preventDefault();
@ -330,7 +353,87 @@
}); });
}); });
document.addEventListener('DOMContentLoaded', function() {
// === PDF GENERATION LOGIC ===
window.generateReportPDF = function() {
console.log("1. Button clicked. Starting PDF process...");
if (typeof html2pdf === 'undefined') {
alert("ERROR: The html2pdf library is not loaded.");
return;
}
const element = document.getElementById('report-pdf-content');
if (!element) {
alert("ERROR: Could not find the div with id='report-pdf-content'.");
return;
}
try {
// Lock in the form values AND remove problem images
const inputs = element.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
if (input.tagName === 'TEXTAREA') {
input.innerHTML = input.value;
} else if (input.tagName === 'SELECT') {
// Lock the selected value
const selectedOption = input.options[input.selectedIndex];
if (selectedOption) selectedOption.setAttribute('selected', 'selected');
// THE FIX: Remove the SVG background arrow that crashes html2canvas
input.style.backgroundImage = 'none';
input.style.appearance = 'none'; // Removes native browser arrows
} else {
input.setAttribute('value', input.value);
}
});
console.log("3. Form data locked and SVGs removed. Generating file...");
const opt = {
margin: 0.5,
filename: 'Site_Inspection_Report.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
logging: false,
scrollY: 0,
windowY: 0
},
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' },
pagebreak: {
mode: 'css',
avoid: ['.row', 'h6']
}
};
// Generate PDF
html2pdf().set(opt).from(element).save().then(() => {
console.log("Success: PDF downloaded.");
// THE RESTORE: Put the arrows back on the screen after download finishes
inputs.forEach(input => {
if (input.tagName === 'SELECT') {
input.style.backgroundImage = '';
input.style.appearance = '';
}
});
}).catch(err => {
console.error("PDF Generation Error:", err);
alert("Failed to build the PDF. Check the browser console.");
});
} catch (error) {
console.error("Critical Execution Error:", error);
alert("A javascript error occurred: " + error.message);
}
}
});
});
</script> </script>
@endsection @endsection

File diff suppressed because one or more lines are too long

View File

@ -4,13 +4,18 @@ use Illuminate\Support\Facades\Route;
use App\Http\Middleware\CheckBackendSession; use App\Http\Middleware\CheckBackendSession;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use App\Mail\UserAccountsMail; use App\Mail\UserAccountsMail;
use App\Http\Controllers\Admin\UserController;
Route::get('/send-test-email', function () { Route::get('/send-test-email', function () {
$recipientEmail = 'recipient@example.com'; $recipientEmail = 'recipient@example.com';
Mail::to($recipientEmail)->send(new UserAccountsMail()); Mail::to($recipientEmail)->send(new UserAccountsMail());
dd('Email sent!'); dd('Email sent!');
}); });
Route::get('/test', function () {
// return view('admin.hometwo');
return view('admin.home_new');
});
#Auth::routes(); #Auth::routes();
Auth::routes([ Auth::routes([
@ -19,6 +24,21 @@ Auth::routes([
'verify' => false, // Disables the email verification routes 'verify' => false, // Disables the email verification routes
]); ]);
Route::middleware(['auth','can:manage-users'])->prefix('admin')->name('admin.')->group(function(){
Route::get('users', [UserController::class,'index'])->name('users.index'); // shows Blade
Route::get('users/list', [UserController::class,'list'])->name('users.list'); // returns JSON filtered list
Route::get('users/{id}', [UserController::class,'show'])->name('users.show'); // user details
Route::post('users', [UserController::class,'store'])->name('users.store'); // create
Route::put('users/{id}', [UserController::class,'update'])->name('users.update'); // update
Route::post('users/move', [UserController::class,'move'])->name('users.move'); // move multiple
Route::post('users/deactivate', [UserController::class,'deactivate'])->name('users.deactivate'); // bulk deactivate
Route::delete('users/{id}', [UserController::class,'destroy'])->name('users.destroy'); // delete
});
Route::get('/sso/validate', [App\Http\Controllers\SsoController::class, 'validateToken']); Route::get('/sso/validate', [App\Http\Controllers\SsoController::class, 'validateToken']);
@ -72,8 +92,11 @@ Route::middleware([CheckBackendSession::class])->group(function () {
Route::get('/users/edit/{user_id}', [App\Http\Controllers\UsersController::class, 'edit']); Route::get('/users/edit/{user_id}', [App\Http\Controllers\UsersController::class, 'edit']);
Route::post('/permits/addcomment', [App\Http\Controllers\PermitsController::class, 'addComment']);
Route::post('/permits/getcomments', [App\Http\Controllers\PermitsController::class, 'getComments']);
Route::get('/permits/home', [App\Http\Controllers\PermitsController::class, 'index']); Route::get('/permits/home', [App\Http\Controllers\PermitsController::class, 'index']);
Route::get('/permits/reports', [App\Http\Controllers\PermitsController::class, 'reports']);
Route::post('/permits/checkcompliance', [App\Http\Controllers\PermitsController::class, 'checkCompliance'])->name('permits.checkCompliance'); Route::post('/permits/checkcompliance', [App\Http\Controllers\PermitsController::class, 'checkCompliance'])->name('permits.checkCompliance');
Route::get('/permits/districtsettings', [App\Http\Controllers\PermitsController::class, 'settings']); Route::get('/permits/districtsettings', [App\Http\Controllers\PermitsController::class, 'settings']);
@ -82,4 +105,7 @@ Route::middleware([CheckBackendSession::class])->group(function () {
Route::get('/permits/status/{name}', [App\Http\Controllers\PermitsController::class, 'statusIndex']); Route::get('/permits/status/{name}', [App\Http\Controllers\PermitsController::class, 'statusIndex']);
}); });

View File

@ -0,0 +1,50 @@
Great—time-accurate historical data is exactly what the “effective-dated assignment” approach is for.
## Recommended flow (historical accuracy)
### 1) Model “which branch the user belonged to when”
Use a history table, e.g. `user_branch_assignments`:
- `user_id`
- `branch_id`
- `starts_at`
- `ends_at` (nullable; `null` = current)
- timestamps
Your rule for history:
- For any `user_id`, at a given timestamp, exactly one assignment row is “active” (`starts_at <= t` and (`ends_at` is null or `t < ends_at`)).
### 2) Model branch-specific activities against the assignment
For any activity that should be tied to the users branch “at the time it happened”, store:
- `user_branch_assignment_id` (FK)
- `occurred_at` (the activity time)
- activity fields
When creating an activity at time `occurred_at`, you must pick the assignment row that was active at that time.
## Transfer logic (what happens at transfer time)
At transfer time `T`:
1. Close current assignment (branch A):
- set `ends_at = T`
2. Create new assignment (branch B):
- set `starts_at = T`
- `ends_at = null`
From then on:
- old activities remain linked to the old assignment
- new activities link to the new assignment
## Query pattern youll use
- “Current branch for a user”:
- assignment where `ends_at is null`
- “Activities for a user in branch X at time range” (historical):
- join activities → `user_branch_assignments` and filter by `branch_id`
## Laravel-specific implementation note (important)
When inserting an activity with `occurred_at`, do this in a transaction:
- read the assignment active at `occurred_at`
- create the activity pointing to `user_branch_assignment_id`
Optionally lock the users assignment rows to avoid edge-case races around the transfer timestamp.
If you share your current activity table name + the column you use for the activity time (e.g. `created_at` vs `occurred_at`), I can sketch the exact Laravel query/transaction structure.