From 1b2dbd06c5cc0b96700a3d4264a0ff18625d1a53 Mon Sep 17 00:00:00 2001 From: Angel de la Torre Date: Tue, 8 Jul 2025 11:02:53 -0700 Subject: [PATCH 1/9] feat(admin): add Groups Management functionality and middleware - Introduced GroupsController for managing groups via API. - Added AdminMiddleware to restrict access to admin routes. - Created GroupsManagement Vue component for the admin interface. - Updated AdminController to include a route for groups management. - Added corresponding Blade view for groups management. - Updated navigation to include a link to Groups Management. All this is boilerplate and we will be adding more functionality to it. That said, we needed to wrap the web route with the AdminMiddleware as any regular user could access /stats and /admin/groups. --- app/Http/Controllers/API/GroupsController.php | 78 +++++++++++++++++++ app/Http/Controllers/AdminController.php | 5 ++ app/Http/Middleware/AdminMiddleware.php | 45 +++++++++++ bootstrap/app.php | 1 + resources/js/app.js | 2 + .../js/components/admin/GroupsManagement.vue | 19 +++++ resources/views/admin/groups.blade.php | 23 ++++++ resources/views/layouts/navbar.blade.php | 1 + routes/api.php | 5 ++ routes/web.php | 3 +- 10 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/API/GroupsController.php create mode 100644 app/Http/Middleware/AdminMiddleware.php create mode 100644 resources/js/components/admin/GroupsManagement.vue create mode 100644 resources/views/admin/groups.blade.php diff --git a/app/Http/Controllers/API/GroupsController.php b/app/Http/Controllers/API/GroupsController.php new file mode 100644 index 0000000000..0e61ef4e3f --- /dev/null +++ b/app/Http/Controllers/API/GroupsController.php @@ -0,0 +1,78 @@ +withCount(['allConfirmedHosts', 'allConfirmedRestarters']); + + $perPage = min($request->input('per_page', 100), 500); + $groups = $query->paginate($perPage); + + // Transform data for frontend + $transformedGroups = $groups->getCollection()->map(function ($group) { + return $this->transformGroup($group); + }); + + return response()->json([ + 'success' => true, + 'data' => $transformedGroups, + 'pagination' => [ + 'current_page' => $groups->currentPage(), + 'last_page' => $groups->lastPage(), + 'per_page' => $groups->perPage(), + 'total' => $groups->total(), + 'from' => $groups->firstItem(), + 'to' => $groups->lastItem(), + ] + ]); + + } catch (\Exception $e) { + Log::error('Error fetching groups: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch groups', + 'error' => config('app.debug') ? $e->getMessage() : null + ], 500); + } + } + + /** + * Transform group data for frontend + */ + private function transformGroup($group): array + { + // Get country name from country code + $countryDisplay = null; + if ($group->country_code) { + $countryDisplay = \App\Helpers\Fixometer::getCountryFromCountryCode($group->country_code); + } + + return [ + 'idgroups' => $group->idgroups, + 'name' => $group->name, + 'location' => $group->location, + 'postcode' => $group->postcode, + 'area' => $group->area, + 'country_code' => $group->country_code, + 'country_display' => $countryDisplay, + 'approved' => (bool) $group->approved, + 'archived_at' => $group->archived_at, + 'created_at' => $group->created_at, + 'networks' => $group->networks, + 'group_tags' => $group->group_tags, + 'all_confirmed_hosts_count' => $group->all_confirmed_hosts_count, + 'all_confirmed_restarters_count' => $group->all_confirmed_restarters_count, + ]; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 3c88e6fd53..92971eb5d5 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -105,4 +105,9 @@ public static function getStats2() 'wasteTotal' => $stats[0]->powered_waste, // + $stats[0]->unpowered_waste, ]; } + + public static function groups() + { + return view('admin.groups'); + } } diff --git a/app/Http/Middleware/AdminMiddleware.php b/app/Http/Middleware/AdminMiddleware.php new file mode 100644 index 0000000000..fd60bb58df --- /dev/null +++ b/app/Http/Middleware/AdminMiddleware.php @@ -0,0 +1,45 @@ +expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Authentication required' + ], 401); + } + return redirect()->guest(route('login')); + } + + if (!Fixometer::hasRole($user, 'Administrator')) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Administrator access required' + ], 403); + } + abort(403, 'Administrator access required'); + } + + return $next($request); + } +} \ No newline at end of file diff --git a/bootstrap/app.php b/bootstrap/app.php index c1f11a764c..a4bef9cce8 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -83,6 +83,7 @@ 'AcceptUserInvites' => \App\Http\Middleware\AcceptUserInvites::class, 'ensureAPIToken' => \App\Http\Middleware\EnsureAPIToken::class, 'customApiAuth' => \App\Http\Middleware\CustomApiTokenAuth::class, + 'admin' => \App\Http\Middleware\AdminMiddleware::class, 'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class, 'localeViewPath' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class, 'localizationRedirect' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class, diff --git a/resources/js/app.js b/resources/js/app.js index 01e88c3914..addd49756c 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -40,6 +40,7 @@ import EventAddEdit from './components/EventAddEdit.vue' import EventsRequiringModeration from './components/EventsRequiringModeration' import EventPage from './components/EventPage.vue' import FixometerPage from './components/FixometerPage' +import GroupsManagement from './components/admin/GroupsManagement.vue' import GroupsPage from './components/GroupsPage.vue' import GroupPage from './components/GroupPage.vue' import GroupAddEditPage from './components/GroupAddEditPage.vue' @@ -1309,6 +1310,7 @@ jQuery(document).ready(function () { 'eventsrequiringmoderation': EventsRequiringModeration, 'eventpage': EventPage, 'fixometerpage': FixometerPage, + 'groupsmanagement': GroupsManagement, 'groupspage': GroupsPage, 'grouppage': GroupPage, 'groupaddeditpage': GroupAddEditPage, diff --git a/resources/js/components/admin/GroupsManagement.vue b/resources/js/components/admin/GroupsManagement.vue new file mode 100644 index 0000000000..a5d10c3763 --- /dev/null +++ b/resources/js/components/admin/GroupsManagement.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/resources/views/admin/groups.blade.php b/resources/views/admin/groups.blade.php new file mode 100644 index 0000000000..ce6eb50a72 --- /dev/null +++ b/resources/views/admin/groups.blade.php @@ -0,0 +1,23 @@ +@extends('layouts.app') + +@section('title') +Groups Management +@endsection + +@section('content') +
+
+
+
+ +
+
+
+
+@endsection + +@section('scripts') + +@endsection \ No newline at end of file diff --git a/resources/views/layouts/navbar.blade.php b/resources/views/layouts/navbar.blade.php index 211a614e21..bdb0a342f8 100644 --- a/resources/views/layouts/navbar.blade.php +++ b/resources/views/layouts/navbar.blade.php @@ -133,6 +133,7 @@
  • Brands
  • Skills
  • Group tags
  • +
  • Groups Management
  • Categories
  • Users
  • Roles
  • diff --git a/routes/api.php b/routes/api.php index 75d5db32f8..374c20b762 100644 --- a/routes/api.php +++ b/routes/api.php @@ -109,6 +109,11 @@ }); }); + // Admin Groups Management API + Route::prefix('/admin/groups')->middleware(['auth:api', App\Http\Middleware\AdminMiddleware::class])->group(function() { + Route::get('/', [App\Http\Controllers\API\GroupsController::class, 'index']); + }); + Route::get('/items', [API\ItemController::class, 'listItemsv2']); Route::prefix('/alerts')->group(function() { diff --git a/routes/web.php b/routes/web.php index 08106ca78d..c5b41dd8ae 100644 --- a/routes/web.php +++ b/routes/web.php @@ -405,8 +405,9 @@ }); //Admin Controller - Route::prefix('admin')->group(function () { + Route::prefix('admin')->middleware(App\Http\Middleware\AdminMiddleware::class)->group(function () { Route::get('/stats', [AdminController::class, 'stats']); + Route::get('/groups', [AdminController::class, 'groups'])->name('admin.groups'); }); //Category Controller From 99415299a97fe3d2d05d0b4e0298e3289a70c4f1 Mon Sep 17 00:00:00 2001 From: Angel de la Torre Date: Tue, 8 Jul 2025 13:26:17 -0700 Subject: [PATCH 2/9] feat(groups): groups table component - Enhanced GroupsController to support sorting by various fields. - Added GroupsTable component for displaying groups with pagination and sorting. - Integrated API call for fetching groups in GroupsManagement component. The group actions are not implemented yet, but the table view is working. --- app/Http/Controllers/API/GroupsController.php | 42 +- resources/js/api/groups.js | 11 + .../js/components/admin/GroupsManagement.vue | 97 ++++ resources/js/components/admin/GroupsTable.vue | 415 ++++++++++++++++++ resources/views/admin/groups.blade.php | 6 - 5 files changed, 555 insertions(+), 16 deletions(-) create mode 100644 resources/js/api/groups.js create mode 100644 resources/js/components/admin/GroupsTable.vue diff --git a/app/Http/Controllers/API/GroupsController.php b/app/Http/Controllers/API/GroupsController.php index 0e61ef4e3f..03999a4c73 100644 --- a/app/Http/Controllers/API/GroupsController.php +++ b/app/Http/Controllers/API/GroupsController.php @@ -16,6 +16,30 @@ public function index(Request $request): JsonResponse $query = Group::with(['networks', 'group_tags']) ->withCount(['allConfirmedHosts', 'allConfirmedRestarters']); + // Handle sorting + $sortBy = $request->input('sort_by', 'name'); + $sortDirection = $request->input('sort_direction', 'asc'); + + // Validate sort direction + if (!in_array(strtolower($sortDirection), ['asc', 'desc'])) { + $sortDirection = 'asc'; + } + + // Map frontend column names to database columns + $sortableColumns = [ + 'name' => 'name', + 'location' => 'location', + 'confirmed_hosts_count' => 'all_confirmed_hosts_count', + 'confirmed_restarters_count' => 'all_confirmed_restarters_count', + 'approved' => 'approved', + 'created_at' => 'created_at', + ]; + + // Default to name if invalid sort field + $sortColumn = $sortableColumns[$sortBy] ?? 'name'; + + $query->orderBy($sortColumn, $sortDirection); + $perPage = min($request->input('per_page', 100), 500); $groups = $query->paginate($perPage); @@ -27,14 +51,12 @@ public function index(Request $request): JsonResponse return response()->json([ 'success' => true, 'data' => $transformedGroups, - 'pagination' => [ - 'current_page' => $groups->currentPage(), - 'last_page' => $groups->lastPage(), - 'per_page' => $groups->perPage(), - 'total' => $groups->total(), - 'from' => $groups->firstItem(), - 'to' => $groups->lastItem(), - ] + 'current_page' => $groups->currentPage(), + 'last_page' => $groups->lastPage(), + 'per_page' => $groups->perPage(), + 'total' => $groups->total(), + 'from' => $groups->firstItem(), + 'to' => $groups->lastItem(), ]); } catch (\Exception $e) { @@ -71,8 +93,8 @@ private function transformGroup($group): array 'created_at' => $group->created_at, 'networks' => $group->networks, 'group_tags' => $group->group_tags, - 'all_confirmed_hosts_count' => $group->all_confirmed_hosts_count, - 'all_confirmed_restarters_count' => $group->all_confirmed_restarters_count, + 'confirmed_hosts_count' => $group->all_confirmed_hosts_count, + 'confirmed_restarters_count' => $group->all_confirmed_restarters_count, ]; } } \ No newline at end of file diff --git a/resources/js/api/groups.js b/resources/js/api/groups.js new file mode 100644 index 0000000000..df9d66a256 --- /dev/null +++ b/resources/js/api/groups.js @@ -0,0 +1,11 @@ +import axios from "axios"; + +const API_BASE = "/api/v2/admin/groups"; + +export default { + // Fetch groups with pagination and filtering + async fetchGroups(params = {}) { + const response = await axios.get(API_BASE, { params }); + return response.data; + }, +}; diff --git a/resources/js/components/admin/GroupsManagement.vue b/resources/js/components/admin/GroupsManagement.vue index a5d10c3763..a02a5653d6 100644 --- a/resources/js/components/admin/GroupsManagement.vue +++ b/resources/js/components/admin/GroupsManagement.vue @@ -3,12 +3,109 @@

    Groups Management

    + + + diff --git a/resources/js/components/admin/GroupsTable.vue b/resources/js/components/admin/GroupsTable.vue new file mode 100644 index 0000000000..eb2c393720 --- /dev/null +++ b/resources/js/components/admin/GroupsTable.vue @@ -0,0 +1,415 @@ + + + + + \ No newline at end of file diff --git a/resources/views/admin/groups.blade.php b/resources/views/admin/groups.blade.php index ce6eb50a72..a3aec884bf 100644 --- a/resources/views/admin/groups.blade.php +++ b/resources/views/admin/groups.blade.php @@ -15,9 +15,3 @@ @endsection - -@section('scripts') - -@endsection \ No newline at end of file From b8fd0b56deaa887b58b0d39293c5d047d3813e84 Mon Sep 17 00:00:00 2001 From: Angel de la Torre Date: Tue, 8 Jul 2025 14:42:35 -0700 Subject: [PATCH 3/9] feat(groups): implement group action handling and confirmation modal - Added performAction method in GroupsController to handle group actions such as approve, unapprove, archive, unarchive, and delete. - Introduced ConfirmationModal component for user confirmation before performing actions on groups. - Updated GroupsManagement component to integrate the confirmation modal and handle group actions. - Enhanced GroupsTable component to trigger actions via the modal. - Added API routes for performing a group action. This update provides a complete flow for managing group actions in the admin interface. --- app/Http/Controllers/API/GroupsController.php | 71 +++++ resources/js/api/groups.js | 6 + .../js/components/admin/ConfirmationModal.vue | 240 +++++++++++++++++ .../js/components/admin/GroupsManagement.vue | 247 ++++++++++-------- resources/js/components/admin/GroupsTable.vue | 16 +- routes/api.php | 1 + 6 files changed, 470 insertions(+), 111 deletions(-) create mode 100644 resources/js/components/admin/ConfirmationModal.vue diff --git a/app/Http/Controllers/API/GroupsController.php b/app/Http/Controllers/API/GroupsController.php index 03999a4c73..e63002030d 100644 --- a/app/Http/Controllers/API/GroupsController.php +++ b/app/Http/Controllers/API/GroupsController.php @@ -69,6 +69,77 @@ public function index(Request $request): JsonResponse } } + public static function performAction(int $group_id, string $action): JsonResponse + { + try { + $group = Group::findOrFail($group_id); + + switch ($action) { + case 'approve': + $group->approved = true; + $group->save(); + $message = "Group '{$group->name}' has been approved successfully."; + break; + + case 'unapprove': + $group->approved = false; + $group->save(); + $message = "Group '{$group->name}' has been unapproved successfully."; + break; + + case 'archive': + $group->archived_at = now(); + $group->save(); + $message = "Group '{$group->name}' has been archived successfully."; + break; + + case 'unarchive': + $group->archived_at = null; + $group->save(); + $message = "Group '{$group->name}' has been unarchived successfully."; + break; + + case 'delete': + $groupName = $group->name; + if (!$group->canDelete()) { + throw new \Exception("Group '{$groupName}' cannot be deleted because it has events with devices."); + } + $group->delete(); + $message = "Group '{$groupName}' has been deleted successfully."; + break; + + default: + throw new \Exception("Invalid action: {$action}"); + } + + $result = [ + 'message' => $message, + 'group' => $action === 'delete' ? + ['id' => $group->idgroups, 'deleted' => true] : + [ + 'id' => $group->idgroups, + 'name' => $group->name, + 'approved' => (bool) $group->approved, + 'archived' => $group->archived_at !== null, + 'deleted' => false + ] + ]; + + return response()->json([ + 'success' => true, + 'message' => $result['message'], + 'group' => $result['group'] + ]); + + } catch (\Exception $e) { + Log::error('Error performing action: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Failed to perform action' + ], 500); + } + } + /** * Transform group data for frontend */ diff --git a/resources/js/api/groups.js b/resources/js/api/groups.js index df9d66a256..de2c59be8c 100644 --- a/resources/js/api/groups.js +++ b/resources/js/api/groups.js @@ -8,4 +8,10 @@ export default { const response = await axios.get(API_BASE, { params }); return response.data; }, + + // Perform single action on a group + async performAction(groupId, action) { + const response = await axios.post(`${API_BASE}/${groupId}/${action}`); + return response.data; + }, }; diff --git a/resources/js/components/admin/ConfirmationModal.vue b/resources/js/components/admin/ConfirmationModal.vue new file mode 100644 index 0000000000..4bd9ab9436 --- /dev/null +++ b/resources/js/components/admin/ConfirmationModal.vue @@ -0,0 +1,240 @@ + + + + + \ No newline at end of file diff --git a/resources/js/components/admin/GroupsManagement.vue b/resources/js/components/admin/GroupsManagement.vue index a02a5653d6..fee67a32b3 100644 --- a/resources/js/components/admin/GroupsManagement.vue +++ b/resources/js/components/admin/GroupsManagement.vue @@ -1,116 +1,153 @@ - + - + +.groups-management { + padding: 20px; +} + diff --git a/resources/js/components/admin/GroupsTable.vue b/resources/js/components/admin/GroupsTable.vue index eb2c393720..c049713074 100644 --- a/resources/js/components/admin/GroupsTable.vue +++ b/resources/js/components/admin/GroupsTable.vue @@ -95,32 +95,32 @@