diff --git a/app/Http/Controllers/API/NetworkController.php b/app/Http/Controllers/API/NetworkController.php
index 4a022c5ba3..a4f410a0cd 100644
--- a/app/Http/Controllers/API/NetworkController.php
+++ b/app/Http/Controllers/API/NetworkController.php
@@ -139,6 +139,38 @@ public function getNetworkv2($id)
return \App\Http\Resources\Network::make($network);
}
+ /**
+ * @OA\Get(
+ * path="/api/v2/networks/{id}/viewer",
+ * operationId="getNetworkViewer",
+ * tags={"Networks"},
+ * summary="Get what the caller can do on a network page",
+ * description="Returns whether the caller can moderate the network, and the groups nearest to the caller's own groups in it. 403 unless the caller is an administrator, a coordinator of the network, or a member of one of its groups.",
+ * @OA\Parameter(name="id", required=true, in="path", @OA\Schema(type="integer")),
+ * @OA\Response(response=200, description="Successful operation"),
+ * @OA\Response(response=403, description="Forbidden"),
+ * @OA\Response(response=404, ref="#/components/responses/NotFound"),
+ * )
+ */
+
+ #[UserStory('As a group volunteer or host, I can open my network\'s page and see the groups nearest to mine', persona: 'Restarter', theme: 'Network groups & events')]
+ public function getNetworkViewerv2($id): JsonResponse
+ {
+ $network = Network::findOrFail($id);
+ $user = Auth::user();
+
+ if (! $user || ! $user->can('viewPublic', $network)) {
+ abort(403, 'You do not have access to this network');
+ }
+
+ return response()->json([
+ 'data' => [
+ 'can_moderate' => $user->can('view', $network),
+ 'nearby_groups' => $network->nearbyGroupsFor($user),
+ ],
+ ]);
+ }
+
/**
* @OA\Get(
* path="/api/v2/networks/{id}/groups",
diff --git a/app/Http/Resources/Network.php b/app/Http/Resources/Network.php
index 036223576a..15b80b3e55 100644
--- a/app/Http/Resources/Network.php
+++ b/app/Http/Resources/Network.php
@@ -222,13 +222,18 @@ class Network extends JsonResource
*/
public function toArray(Request $request): array
{
+ $user = \Illuminate\Support\Facades\Auth::user()
+ ?? auth('sanctum')->user()
+ ?? auth('api')->user();
+
return [
'id' => $this->id,
'name' => $this->name,
'logo' => $this->logo ? ($request->root() . '/uploads/' . $this->logo) : null,
'description' => $this->description,
'website' => $this->website,
- 'reporting_url' => $this->reporting_url,
+ // The reporting dashboard is for the network's coordinators and administrators only.
+ 'reporting_url' => ($user && $user->can('view', $this->resource)) ? $this->reporting_url : null,
'shortname' => $this->shortname,
'default_language' => $this->default_language,
'stats' => $this->resource->stats(),
diff --git a/app/Network.php b/app/Network.php
index c0ea3840a1..155df1437f 100644
--- a/app/Network.php
+++ b/app/Network.php
@@ -101,6 +101,80 @@ public function groupsNotIn()
return $query->get();
}
+ /**
+ * The approved, active groups in this network nearest to $from (not including it), with their distance in km.
+ */
+ public function nearestGroups(Group $from, int $count = 5)
+ {
+ if (is_null($from->latitude) || is_null($from->longitude)) {
+ return collect();
+ }
+
+ return $this->groups()
+ ->select('groups.idgroups', 'groups.name', 'groups.location', 'groups.latitude', 'groups.longitude')
+ ->selectRaw(
+ '( 6371 * acos( cos( radians(?) ) * cos( radians( latitude ) ) * cos( radians( longitude ) - radians(?) ) + sin( radians(?) ) * sin( radians( latitude ) ) ) ) AS dist',
+ [$from->latitude, $from->longitude, $from->latitude]
+ )
+ ->where('groups.idgroups', '!=', $from->idgroups)
+ ->where('groups.approved', true)
+ ->whereNull('groups.archived_at')
+ ->whereNotNull('groups.latitude')
+ ->whereNotNull('groups.longitude')
+ ->orderBy('dist')
+ ->take($count)
+ ->get();
+ }
+
+ /**
+ * The groups in the network nearest to any of the user's own located groups in it, nearest first, with each
+ * group listed once at its smallest distance. The user's own groups are left out. Null if they have no
+ * located group in the network.
+ */
+ public function nearbyGroupsFor(User $user): ?array
+ {
+ $mine = Group::join('users_groups', 'users_groups.group', '=', 'groups.idgroups')
+ ->join('group_network', 'group_network.group_id', '=', 'groups.idgroups')
+ ->where('group_network.network_id', $this->id)
+ ->where('users_groups.user', $user->id)
+ ->whereNull('users_groups.deleted_at')
+ ->whereNotNull('groups.latitude')
+ ->whereNotNull('groups.longitude')
+ ->orderBy('groups.name')
+ ->select('groups.*')
+ ->get();
+
+ if ($mine->isEmpty()) {
+ return null;
+ }
+
+ $mineIds = $mine->pluck('idgroups')->all();
+ $nearest = [];
+
+ foreach ($mine as $group) {
+ // Ask for extra so that skipping the user's other groups still leaves enough.
+ foreach ($this->nearestGroups($group, 5 + count($mineIds)) as $g) {
+ if (in_array($g->idgroups, $mineIds)) {
+ continue;
+ }
+
+ if (! isset($nearest[$g->idgroups]) || $g->dist < $nearest[$g->idgroups]->dist) {
+ $nearest[$g->idgroups] = $g;
+ }
+ }
+ }
+
+ return [
+ 'from' => $mine->pluck('name')->implode(', '),
+ 'groups' => collect($nearest)->sortBy('dist')->take(5)->map(fn ($g) => [
+ 'id' => $g->idgroups,
+ 'name' => $g->name,
+ 'location' => $g->location,
+ 'distance' => round($g->dist, 1),
+ ])->values()->all(),
+ ];
+ }
+
public function stats()
{
$eEmissionRatio = \App\Helpers\LcaStats::getEmissionRatioPowered();
diff --git a/app/Policies/NetworkPolicy.php b/app/Policies/NetworkPolicy.php
index 4aee713fb0..5eb05d5b60 100644
--- a/app/Policies/NetworkPolicy.php
+++ b/app/Policies/NetworkPolicy.php
@@ -26,6 +26,22 @@ public function view(User $user, Network $network): bool
return false;
}
+ /**
+ * Determine whether the user can view the network page with its coordinator-only parts hidden. That is
+ * anyone who can fully view it, plus the volunteers and hosts of a group in the network.
+ */
+ public function viewPublic(User $user, Network $network): bool
+ {
+ if ($this->view($user, $network)) {
+ return true;
+ }
+
+ return \App\UserGroups::where('user', $user->id)
+ ->where(fn ($q) => $q->where('status', 1)->orWhereNull('status'))
+ ->whereIn('group', \DB::table('group_network')->where('network_id', $network->id)->select('group_id'))
+ ->exists();
+ }
+
/**
* Determine whether the user can create networks.
*/
diff --git a/client/app/api/NetworkAPI.js b/client/app/api/NetworkAPI.js
index 56c78b2b67..f5d6705fc0 100644
--- a/client/app/api/NetworkAPI.js
+++ b/client/app/api/NetworkAPI.js
@@ -19,6 +19,13 @@ export default class NetworkAPI extends BaseAPI {
return this.$get(`/api/v2/networks/${id}`)
}
+ // GET /api/v2/networks/{id}/viewer - what the caller may do on the network
+ // page: {can_moderate, nearby_groups}. Requires auth; 403 unless they are an
+ // administrator, a coordinator of the network or a member of one of its groups.
+ viewer(id) {
+ return this.$get(`/api/v2/networks/${id}/viewer`)
+ }
+
// includeNextEvent/includeDetails/includeStats/includeArchived/group_tag
// query params (getNetworkGroupsv2). Also honours includeCounts (read
// directly off the request by GroupSummary#toArray, not documented in
diff --git a/client/app/components/networks/NetworkNearbyGroups.vue b/client/app/components/networks/NetworkNearbyGroups.vue
new file mode 100644
index 0000000000..3c8920d6b2
--- /dev/null
+++ b/client/app/components/networks/NetworkNearbyGroups.vue
@@ -0,0 +1,37 @@
+
+
+
+
+