From 8428d94abd7e116e29bb3e18030c9c3b6d7ae4e4 Mon Sep 17 00:00:00 2001 From: Kwesi Banson Date: Tue, 13 Oct 2026 14:29:40 +0000 Subject: [PATCH] user roles, bug fixes --- app/Http/Controllers/ClientsController.php | 3 +- .../NetworkOperatorsController.php | 42 +++++++++- .../Controllers/StaffMembersController.php | 16 +++- app/Http/Middleware/CheckRole.php | 28 +++++++ app/Models/ErpSystemRole.php | 17 ++++ app/Models/Mnonote.php | 9 ++ app/Models/NetworkOperator.php | 4 + app/Models/StaffMember.php | 17 ++++ bootstrap/app.php | 3 + public/assets/js/mno-show.js | 84 +++++++++++++++++++ .../partials/modals.blade.php | 38 +++++++++ .../views/network_operators/show.blade.php | 65 +++++++++++++- resources/views/staff/index.blade.php | 46 +++++++++- routes/web.php | 3 + 14 files changed, 366 insertions(+), 9 deletions(-) create mode 100644 app/Http/Middleware/CheckRole.php create mode 100644 app/Models/ErpSystemRole.php diff --git a/app/Http/Controllers/ClientsController.php b/app/Http/Controllers/ClientsController.php index 38a9733..e5c054d 100644 --- a/app/Http/Controllers/ClientsController.php +++ b/app/Http/Controllers/ClientsController.php @@ -16,7 +16,6 @@ use Illuminate\Support\Str; use Barryvdh\DomPDF\Facade\Pdf; - class ClientsController extends Controller { public function index(){ @@ -890,7 +889,7 @@ class ClientsController extends Controller 'invoice_amount' => 'required|numeric', 'invoice_date' => 'required|date', 'invoice_status' => 'required|string', - 'remarks' => 'sometimes|string' + 'remarks' => 'nullable|string' ]); // Find the record and update it diff --git a/app/Http/Controllers/NetworkOperatorsController.php b/app/Http/Controllers/NetworkOperatorsController.php index b675bb1..1dbe6cb 100644 --- a/app/Http/Controllers/NetworkOperatorsController.php +++ b/app/Http/Controllers/NetworkOperatorsController.php @@ -118,8 +118,12 @@ class NetworkOperatorsController extends Controller public function show($id) { $operator = NetworkOperator::findOrFail($id); + + // Fetch all services for the dropdown + $allServices = \App\Models\Service::orderBy('name')->get(); + $staffMembers = \App\Models\StaffMember::orderBy('name')->get(); - return view('network_operators.show', compact('operator', 'staffMembers')); + return view('network_operators.show', compact('operator', 'staffMembers', 'allServices')); } public function storeConnection(Request $request, $operatorId) @@ -169,6 +173,42 @@ class NetworkOperatorsController extends Controller 'connection' => $connection ]); } + public function storeNote(Request $request, $operatorId) + { + $request->validate([ + 'notes_body' => 'required|string', + 'services' => 'nullable|string|max:200' + ]); + + $serviceName = null; + if ($request->filled('services')) { + // This will find the service if it exists, or create a new one in the database if the user typed a new tag + $service = \App\Models\Service::firstOrCreate([ + 'name' => trim($request->services) + ]); + $serviceName = $service->name; + } + + \App\Models\MnoNote::create([ + 'mno_id' => $operatorId, + 'services' => $request->services ?? '', + 'user_id' => auth()->id(), // Automatically assign the logged-in user + 'notes_body' => $request->notes_body, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Note added successfully!' + ]); + } + + public function destroyNote($id) + { + $note = \App\Models\MnoNote::findOrFail($id); + $note->delete(); + + return redirect()->back()->with('success', 'Note deleted successfully.'); + } public function destroyConnection($id) { diff --git a/app/Http/Controllers/StaffMembersController.php b/app/Http/Controllers/StaffMembersController.php index ff64c52..3c97b90 100644 --- a/app/Http/Controllers/StaffMembersController.php +++ b/app/Http/Controllers/StaffMembersController.php @@ -45,11 +45,16 @@ class StaffMembersController extends Controller // dd($staffMembers); // dd(\Auth::user()->designation); + + $systemRoles = \App\Models\ErpSystemRole::orderBy('name')->get(); + + $data = [ 'staffMembers' => $staffMembers, 'departments' => $departments, 'allstaffMembers' => $allstaffMembers, 'countries' => $countries, + 'systemRoles' => $systemRoles ]; @@ -73,12 +78,17 @@ class StaffMembersController extends Controller $validated['profile_pic'] = $request->file('photo')->store('staff_profiles', 'public'); } + // $user->roles()->syncWithoutDetaching([$roleId]); // Add without removing existing roles + // $user->roles()->sync($roleIds); // Replace the user's role set + $validated['created_by'] = auth()->id() ?? 1; $validated['password'] = Hash::make('default_password'); $validated['hire_date'] = now(); StaffMember::create($validated); - + if ($request->has('roles')) { + $staff->roles()->sync($request->roles); + } return response()->json(['success' => true, 'message' => 'Staff member added successfully!']); } @@ -107,6 +117,8 @@ class StaffMembersController extends Controller $validated['modified_by'] = auth()->id() ?? 1; $staff->update($validated); + $staff->roles()->sync($request->roles ?? []); + return response()->json(['success' => true, 'message' => 'Staff member updated successfully!']); } @@ -152,7 +164,7 @@ class StaffMembersController extends Controller } public function showDetails($id) { - $staff = StaffMember::with('dependents')->findOrFail($id); + $staff = StaffMember::with(['dependents', 'roles'])->findOrFail($id); return response()->json([ 'success' => true, diff --git a/app/Http/Middleware/CheckRole.php b/app/Http/Middleware/CheckRole.php new file mode 100644 index 0000000..5fedf7c --- /dev/null +++ b/app/Http/Middleware/CheckRole.php @@ -0,0 +1,28 @@ +check()) { + return redirect('login'); + } + + $user = auth()->user(); // Assuming StaffMember acts as your Authenticatable user + + // Check if the user has any of the allowed roles + foreach ($roles as $role) { + if ($user->hasRole($role)) { + return $next($request); + } + } + + abort(403, 'Unauthorized action.'); + } +} \ No newline at end of file diff --git a/app/Models/ErpSystemRole.php b/app/Models/ErpSystemRole.php new file mode 100644 index 0000000..35fb16b --- /dev/null +++ b/app/Models/ErpSystemRole.php @@ -0,0 +1,17 @@ +belongsToMany(StaffMember::class, 'user_roles', 'system_role_id', 'staff_member_id'); + } +} diff --git a/app/Models/Mnonote.php b/app/Models/Mnonote.php index caf6dd7..7d04d06 100644 --- a/app/Models/Mnonote.php +++ b/app/Models/Mnonote.php @@ -18,4 +18,13 @@ class Mnonote extends Model public function created_by_info(){ return $this->hasOne('App\Models\StaffMember', 'id', 'user_id'); } + public function operator() + { + return $this->belongsTo(NetworkOperator::class, 'mno_id', 'id'); + } + + public function user() + { + return $this->belongsTo(User::class, 'user_id', 'id'); + } } diff --git a/app/Models/NetworkOperator.php b/app/Models/NetworkOperator.php index 29eeb87..b7a0b1f 100755 --- a/app/Models/NetworkOperator.php +++ b/app/Models/NetworkOperator.php @@ -31,6 +31,10 @@ class NetworkOperator extends Model { return $this->hasMany(NetworkOperatorDocument::class); } + public function notes() + { + return $this->hasMany(\App\Models\Mnonote::class, 'mno_id', 'id')->latest(); + } /* public function setBuyingRateAttribute($value) { diff --git a/app/Models/StaffMember.php b/app/Models/StaffMember.php index cbc07a8..27bda70 100644 --- a/app/Models/StaffMember.php +++ b/app/Models/StaffMember.php @@ -6,6 +6,8 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Database\Eloquent\Factories\HasFactory; use App\Traits\LogsActivity; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use App\Models\ErpSystemRole; class StaffMember extends Authenticatable { @@ -35,7 +37,22 @@ class StaffMember extends Authenticatable public function department(){ return $this->hasOne('App\Models\Department', 'id', 'department_id'); } + // public function designation(){ + // return $this->hasOne('App\Models\Department', 'id', 'department_id'); + // } + public function roles(): BelongsToMany + { + // Parameter order: Related Model, Pivot Table Name, Foreign Key on Pivot, Related Key on Pivot + return $this->belongsToMany(ErpSystemRole::class, 'user_roles', 'staff_member_id', 'system_role_id'); + } + + public function hasRole(string $role): bool + { + return $this->roles()->where('name', $role)->exists(); + } + + // 4. Cast dates correctly protected $casts = [ // 'dob' => 'date', diff --git a/bootstrap/app.php b/bootstrap/app.php index bdb67b2..e4ee53c 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -14,6 +14,9 @@ return Application::configure(basePath: dirname(__DIR__)) ) ->withMiddleware(function (Middleware $middleware) { // + $middleware->alias([ + 'role' => \App\Http\Middleware\CheckRole::class, + ]); }) ->withExceptions(function (Exceptions $exceptions) { $exceptions->render(function (HttpException $e, Request $request) { diff --git a/public/assets/js/mno-show.js b/public/assets/js/mno-show.js index 4e67236..16f3aac 100644 --- a/public/assets/js/mno-show.js +++ b/public/assets/js/mno-show.js @@ -271,4 +271,88 @@ $(document).ready(function() { } }); }); + + + + // ========================================== + // 4. NOTES LOGIC + // ========================================== + + // Initialize Select2 with the 'tags' feature enabled + $('#note_services').select2({ + placeholder: "Select or type a new service...", + allowClear: true, + width: '100%', + dropdownParent: $('#addNoteModal'), + tags: true // This allows users to type and submit custom entries + }); + + $('#addNoteForm').on('submit', function(e) { + // ... (keep your existing ajax submit logic here) ... + }); + + $('#addNoteModal').on('hidden.bs.modal', function () { + $('#addNoteForm')[0].reset(); + $('#noteModalAlert').html(''); + $('#addNoteForm').find('.is-invalid').removeClass('is-invalid'); + $('#addNoteForm').find('.invalid-feedback').remove(); + + // Clear the Select2 dropdown properly + $('#note_services').val(null).trigger('change'); + }); + $('#addNoteForm').on('submit', function(e) { + e.preventDefault(); + + let $form =$(this); + let $submitBtn =$('#btnSubmitNote'); + let originalText = $submitBtn.html(); + let $alertBox =$('#noteModalAlert'); + + $submitBtn.html('Saving...').prop('disabled', true); + $alertBox.html('');$form.find('.is-invalid').removeClass('is-invalid'); + + $.ajax({ + url: $form.attr('action'), + type: 'POST', + data: $form.serialize(), + success: function(response) { + if (response.success) { + $('#addNoteModal').modal('hide'); + Swal.fire({ + icon: 'success', + title: 'Saved!', + text: response.message, + confirmButtonColor: '#5c4df0' + }).then(() => location.reload()); + } + }, + error: function(xhr) { + if (xhr.status === 422) { + let errors = xhr.responseJSON.errors; + $alertBox.html(` + + `); + $.each(errors, function(key, value) { + let $input =$form.find('[name="' + key + '"]'); + if ($input.length) { + $input.addClass('is-invalid');$input.parent().append('
' + value[0] + '
'); + } + }); + } + }, + complete: function() { + $submitBtn.html(originalText).prop('disabled', false); + } + }); + }); + + $('#addNoteModal').on('hidden.bs.modal', function () { + $('#addNoteForm')[0].reset(); + $('#noteModalAlert').html(''); + $('#addNoteForm').find('.is-invalid').removeClass('is-invalid'); + $('#addNoteForm').find('.invalid-feedback').remove(); + }); }); \ No newline at end of file diff --git a/resources/views/network_operators/partials/modals.blade.php b/resources/views/network_operators/partials/modals.blade.php index 446af1d..8cfc366 100644 --- a/resources/views/network_operators/partials/modals.blade.php +++ b/resources/views/network_operators/partials/modals.blade.php @@ -255,4 +255,42 @@ + + + + + \ No newline at end of file diff --git a/resources/views/network_operators/show.blade.php b/resources/views/network_operators/show.blade.php index 07ae89e..a7a9673 100644 --- a/resources/views/network_operators/show.blade.php +++ b/resources/views/network_operators/show.blade.php @@ -1,7 +1,31 @@ @extends('layouts.masterbeta') @section('title', 'Cick ERP - ' . $operator->name) - +@push('styles') + + +@endpush @section('breadcrumbs') @@ -303,6 +327,44 @@ + +
+
+
+ Internal Notes & Activity +
+ +
+
+ +
+ @forelse($operator->notes as $note) +
+
+
+ {{ $note->user->name ?? 'System User' }} + {{ $note->created_at->format('d M Y, h:i A') }} + @if($note->services) + {{ $note->services }} + @endif +
+
+ @csrf + @method('DELETE') + +
+
+
{{ $note->notes_body }}
+
+ @empty +
No notes added yet.
+ @endforelse +
+
@@ -317,6 +379,7 @@ const baseUrl = "{{ url('/') }}"; const operatorId = "{{ $operator->id }}"; + diff --git a/resources/views/staff/index.blade.php b/resources/views/staff/index.blade.php index c1e685a..f1bc556 100644 --- a/resources/views/staff/index.blade.php +++ b/resources/views/staff/index.blade.php @@ -149,7 +149,8 @@ data-department_id="{{ $staff->department_id }}" data-location_country="{{ $staff->location_country }}" data-status="{{ $staff->status }}" - data-pic="{{ $staff->profile_pic ? asset('public/storage/' . $staff->profile_pic) : '' }}"> + data-pic="{{ $staff->profile_pic ? asset('public/storage/' . $staff->profile_pic) : '' }}" + data-roles="{{ $staff->roles->pluck('id')->toJson() }}"> Edit @@ -258,6 +259,16 @@ + +
+ + +
Leave blank for standard employee access.
+
+
+
ANNUAL LEAVE BALANCE
+
0 Days
+
+ + +
+
SYSTEM ROLES
+
+ Loading... +
+
+ @@ -423,6 +447,12 @@ width: '100%', dropdownParent: $('#staffModal') }); + $('#staffRoles').select2({ + placeholder: "Select applicable roles...", + allowClear: true, + width: '100%', + dropdownParent: $('#staffModal') + }); // Handle Photo Preview $('#staffPhoto').on('change', function(e) { @@ -441,7 +471,7 @@ $('#staffId').val(''); $('#avatarPreview').html(''); $('#staffCountry').val(null).trigger('change'); - + $('#staffRoles').val(null).trigger('change'); $('#staffModalTitle').text('Add New Employee'); $('#btnSubmitStaff').html('Save Employee'); staffModal.show(); @@ -460,6 +490,7 @@ $('#staffPhone').val(data.phone); $('#staffDept').val(data.department_id); $('#staffCountry').val(data.location_country).trigger('change'); + $('#staffRoles').val(data.roles).trigger('change'); $('#staffStatus').val(data.status); if (data.pic) { @@ -559,7 +590,7 @@ $('#viewStaffLocation').text(''); $('#viewStaffLeaveBalance').text(''); $('#viewStaffDependentsList').html('Loading...'); - + $('#viewStaffRoles').html('Loading...'); viewModal.show(); // Fetch staff details via AJAX @@ -590,6 +621,15 @@ let initials = staff.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase(); $('#viewStaffAvatarContainer').html(`
${initials}
`); } + let rolesHtml = ''; + if (staff.roles && staff.roles.length > 0) { + staff.roles.forEach(role => { + rolesHtml += `${role.label}`; + }); + } else { + rolesHtml = `Standard Employee Access`; + } + $('#viewStaffRoles').html(rolesHtml); // Dependents Table population let dependentsHtml = ''; diff --git a/routes/web.php b/routes/web.php index ba93ee7..9d12b61 100644 --- a/routes/web.php +++ b/routes/web.php @@ -159,6 +159,9 @@ Route::middleware(['auth'])->group(function () { Route::post('/vpns/download', [App\Http\Controllers\VpnConfigController::class, 'download'])->name('vpns.download'); + Route::post('/mnos/{operatorId}/notes', [App\Http\Controllers\NetworkOperatorsController::class, 'storeNote'])->name('mnos.notes.store'); + Route::delete('/mnos/notes/{id}', [App\Http\Controllers\NetworkOperatorsController::class, 'destroyNote'])->name('mnos.notes.destroy'); + Route::get('/activity-logs', [App\Http\Controllers\ActivityLogController::class, 'index'])->name('activity-logs.index');