['region'], 'region' => ['province'], 'province' => ['city', 'municipal'], 'city' => ['barangay'], 'municipal' => ['barangay'], 'barangay' => [], ]; private const OFFICER_ROLES = ['PRESIDENT', 'VICE_PRESIDENT', 'SECRETARY', 'TREASURER', 'AUDITOR', 'BOARD_MEMBER']; /** Big3 / Coordinator may act org-wide. */ private function isAdminCaller($acctType): bool { return in_array($acctType, [ UserTypes::SUPER_ADMIN, UserTypes::PUNONG_BARANGAY, UserTypes::KAGAWAD, UserTypes::SECRETARY, ], true); } /** * Resolve the caller's primary (highest) active chapter membership row, * joined with chapter columns. Returns null if none. */ private function resolveCallerChapter(int $userId) { return ChapterMember::join('chapters', 'chapters.id', 'chapter_members.chapter_id') ->where('chapter_members.user_id', $userId) ->where('chapter_members.is_active', true) ->orderByRaw("FIELD(chapters.level,'national','region','province','city','municipal','barangay')") ->select( 'chapter_members.*', 'chapters.id as c_id', 'chapters.name as c_name', 'chapters.level as c_level', 'chapters.hashkey as c_hashkey', 'chapters.location_key as c_location_key', 'chapters.cooperative_id as c_cooperative_id' ) ->first(); } private function chapterOfficers(int $chapterId): array { return ChapterMember::join('users', 'users.id', 'chapter_members.user_id') ->where('chapter_members.chapter_id', $chapterId) ->where('chapter_members.is_active', true) ->whereNotNull('chapter_members.role') ->where('chapter_members.role', '!=', 'MEMBER') ->select('users.name as name', 'users.fullname as fullname', 'chapter_members.role as role', 'chapter_members.position as position') ->get() ->map(fn ($r) => [ 'name' => $r->fullname ?: $r->name, 'role' => $r->role, 'position' => $r->position, ]) ->values() ->all(); } /** * Collect all chapter IDs in the subtree rooted at $rootId, scoped to $cooperativeId. */ private function subtreeChapterIds(int $rootId, ?int $cooperativeId): array { $ids = [$rootId]; $frontier = [$rootId]; while (!empty($frontier)) { $q = Chapter::whereIn('parent_id', $frontier)->where('is_active', true); if ($cooperativeId !== null) { $q->where('cooperative_id', $cooperativeId); } $children = $q->pluck('id')->map(fn ($v) => (int) $v)->all(); $children = array_values(array_diff($children, $ids)); if (empty($children)) { break; } $ids = array_merge($ids, $children); $frontier = $children; } return $ids; } /** * Get hierarchy data for a given chapter level. * If chapter_id is null, returns national-level (all regions). * Otherwise returns children of the given chapter with member counts and leaders. */ public function hierarchy(Request $request) { $chapterId = $request->input('chapter_id'); if (!$chapterId) { // Return all regions under national $national = Chapter::where('level', 'national')->where('location_key', 'philippines')->first(); if (!$national) { return response()->json(['chapters' => [], 'current' => null, 'breadcrumb' => []]); } $chapters = $national->children() ->where('is_active', true) ->withCount('activeMembers') ->with(['leaders.user']) ->get() ->map(fn($c) => $this->formatChapter($c)) ->values(); return response()->json([ 'chapters' => $chapters, 'current' => null, 'breadcrumb' => [], ]); } $current = Chapter::withCount('activeMembers')->with(['leaders.user'])->find($chapterId); if (!$current) { return ResponseHelper::returnError('Chapter not found', 404); } $chapters = $current->children() ->where('is_active', true) ->withCount('activeMembers') ->with(['leaders.user', 'children' => fn($q) => $q->where('is_active', true)]) ->get() ->map(fn($c) => $this->formatChapter($c)) ->values(); return response()->json([ 'chapters' => $chapters, 'current' => $this->formatChapter($current), 'breadcrumb' => $this->buildBreadcrumb($current), ]); } /** * Get member dots for the map — returns lat/lng counts per chapter at requested level. */ public function mapData(Request $request) { $level = $request->input('level', 'region'); $parentId = $request->input('parent_id'); $query = Chapter::where('level', $level) ->where('is_active', true) ->withCount('activeMembers'); if ($parentId) { $query->where('parent_id', $parentId); } $chapters = $query->get() ->filter(fn($c) => $c->lat && $c->lng) ->map(function ($chapter) { return [ 'id' => $chapter->id, 'name' => $chapter->name, 'level' => $chapter->level, 'lat' => $chapter->lat, 'lng' => $chapter->lng, 'count' => $chapter->active_members_count, ]; }) ->values(); return response()->json(['chapters' => $chapters]); } /** * Get members for a specific chapter (paginated). */ public function members(Request $request) { $chapterId = $request->input('chapter_id'); if (!$chapterId) { return ResponseHelper::returnError('chapter_id required', 400); } $members = ChapterMember::with(['user', 'user.userInfo']) ->where('chapter_id', $chapterId) ->where('is_active', true) ->get() ->map(function ($cm) { $user = $cm->user; $info = $user?->userInfo; return [ 'hashkey' => $user?->hashkey, 'name' => $user?->fullname ?? $user?->name, 'position' => $cm->position, 'photo' => $user?->photourl, 'region' => $info?->region, 'province' => $info?->province, 'city' => $info?->city, 'barangay' => $info?->barangay, 'is_manual' => $cm->is_manual_override, ]; }); return response()->json(['members' => $members]); } /** * Assign or update a member's position / chapter assignment. */ public function assignMember(Request $request) { $validator = Validator::make($request->all(), [ 'user_hashkey' => 'required|string', 'chapter_id' => 'required|integer', 'position' => 'nullable|string', 'is_manual' => 'boolean', ]); if ($validator->fails()) { return ResponseHelper::returnError('Validation failed', 422, $validator->errors()); } $user = User::where('hashkey', $request->user_hashkey)->first(); if (!$user) { return ResponseHelper::returnError('User not found', 404); } $chapter = Chapter::find($request->chapter_id); if (!$chapter) { return ResponseHelper::returnError('Chapter not found', 404); } $existing = ChapterMember::where('user_id', $user->id)->where('chapter_id', $chapter->id)->first(); if ($existing) { $existing->update([ 'position' => $request->position, 'is_manual_override' => $request->input('is_manual', true), 'assigned_by' => Auth::id(), 'assigned_at' => now(), 'updated_by' => Auth::id(), ]); } else { ChapterMember::create([ 'hashkey' => (string) \Hypervel\Support\Str::uuid(), 'user_id' => $user->id, 'chapter_id' => $chapter->id, 'position' => $request->position, 'is_manual_override' => $request->input('is_manual', true), 'is_active' => true, 'assigned_by' => Auth::id(), 'assigned_at' => now(), 'created_by' => Auth::id(), ]); } return response()->json(['success' => true]); } /** * Remove a member from a chapter assignment. */ public function removeMember(Request $request) { $chapterMember = ChapterMember::where('hashkey', $request->input('hashkey'))->first(); if (!$chapterMember) { return ResponseHelper::returnError('Member assignment not found', 404); } $chapterMember->update(['is_active' => false, 'updated_by' => Auth::id()]); return response()->json(['success' => true]); } /** * Trigger auto-assignment for all users based on their current addresses. */ public function syncAllAutoAssignments() { $count = 0; UserInfo::whereNotNull('region')->chunk(100, function ($infos) use (&$count) { foreach ($infos as $info) { Chapter::autoAssignUser($info->user_id); $count++; } }); return response()->json(['synced' => $count]); } /** * Get available positions from system settings. */ public function positions() { $positions = SystemSetting::getValue('chapter_positions', json_encode(['Director', 'Coordinator', 'Officer', 'Secretary', 'Treasurer', 'Auditor', 'Member'])); if (is_string($positions)) { $positions = json_decode($positions, true) ?? []; } return response()->json(['positions' => $positions]); } /** * Hierarchy scoped to members of the main organization, with a synthetic * "island_group" level above region (Luzon / Visayas / Mindanao). * * Inputs: * - island: 'luzon'|'visayas'|'mindanao' to drill into an island group * - chapter_id: real chapter id to drill into a region/province/city * If neither is provided, returns the 3 island groups with member counts. */ public function orgHierarchy(Request $request) { $org = SystemSettingsHelper::mainOrganization(); if (!$org) { return response()->json([ 'chapters' => [], 'current' => null, 'breadcrumb' => [], 'error' => 'No main organization configured.', ]); } $orgUserIds = $this->orgUserIds((int) $org->id); $island = $request->input('island'); $chapterId = $request->input('chapter_id'); // Top level: 3 island groups if (!$chapterId && !$island) { $regionsByIsland = $this->regionIdsByIsland(); $chapters = []; foreach (IslandGroupHelper::islands() as $isl) { $regionIds = $regionsByIsland[$isl] ?? []; $count = empty($regionIds) || empty($orgUserIds) ? 0 : ChapterMember::whereIn('chapter_id', $regionIds) ->whereIn('user_id', $orgUserIds) ->where('is_active', true) ->distinct('user_id') ->count('user_id'); $center = IslandGroupHelper::center($isl); $chapters[] = [ 'id' => null, 'island' => $isl, 'hashkey' => 'island:' . $isl, 'name' => IslandGroupHelper::label($isl), 'level' => 'island_group', 'location_key' => $isl, 'lat' => $center['lat'], 'lng' => $center['lng'], 'member_count' => (int) $count, 'leaders' => [], 'has_children' => !empty($regionIds), ]; } return response()->json([ 'chapters' => $chapters, 'current' => null, 'breadcrumb' => [], ]); } // Drilling into an island group: list its regions if ($island && !$chapterId) { $regionIds = $this->regionIdsByIsland()[$island] ?? []; if (empty($regionIds)) { return response()->json([ 'chapters' => [], 'current' => $this->islandPseudoChapter($island), 'breadcrumb' => [], ]); } $chapters = Chapter::whereIn('id', $regionIds) ->where('is_active', true) ->with(['leaders.user', 'children' => fn($q) => $q->where('is_active', true)]) ->get() ->map(fn($c) => $this->formatChapterScoped($c, $orgUserIds)) ->values(); return response()->json([ 'chapters' => $chapters, 'current' => $this->islandPseudoChapter($island), 'breadcrumb' => [], ]); } // Drilling into a real chapter (region/province/city) — return its children $current = Chapter::with(['leaders.user'])->find($chapterId); if (!$current) { return ResponseHelper::returnError('Chapter not found', 404); } $chapters = $current->children() ->where('is_active', true) ->with(['leaders.user', 'children' => fn($q) => $q->where('is_active', true)]) ->get() ->map(fn($c) => $this->formatChapterScoped($c, $orgUserIds)) ->values(); return response()->json([ 'chapters' => $chapters, 'current' => $this->formatChapterScoped($current, $orgUserIds), 'breadcrumb' => $this->buildOrgBreadcrumb($current), ]); } /** * Map dots scoped to members of the main organization. * Levels: 'island_group' (3 dots), 'region', 'province', 'city', 'barangay'. */ public function orgMapData(Request $request) { $org = SystemSettingsHelper::mainOrganization(); if (!$org) { return response()->json(['chapters' => []]); } $orgUserIds = $this->orgUserIds((int) $org->id); $level = $request->input('level', 'island_group'); $parentId = $request->input('parent_id'); $island = $request->input('island'); if ($level === 'island_group') { $regionsByIsland = $this->regionIdsByIsland(); $dots = []; foreach (IslandGroupHelper::islands() as $isl) { $regionIds = $regionsByIsland[$isl] ?? []; $count = empty($regionIds) || empty($orgUserIds) ? 0 : ChapterMember::whereIn('chapter_id', $regionIds) ->whereIn('user_id', $orgUserIds) ->where('is_active', true) ->distinct('user_id') ->count('user_id'); $center = IslandGroupHelper::center($isl); $dots[] = [ 'id' => null, 'island' => $isl, 'name' => IslandGroupHelper::label($isl), 'level' => 'island_group', 'lat' => $center['lat'], 'lng' => $center['lng'], 'count' => (int) $count, ]; } return response()->json(['chapters' => $dots]); } $query = Chapter::where('level', $level)->where('is_active', true); if ($parentId) { $query->where('parent_id', $parentId); } elseif ($level === 'region' && $island) { $regionIds = $this->regionIdsByIsland()[$island] ?? []; if (empty($regionIds)) return response()->json(['chapters' => []]); $query->whereIn('id', $regionIds); } $chapters = $query->get() ->filter(fn($c) => $c->lat && $c->lng) ->map(function ($chapter) use ($orgUserIds) { return [ 'id' => $chapter->id, 'name' => $chapter->name, 'level' => $chapter->level, 'lat' => $chapter->lat, 'lng' => $chapter->lng, 'count' => $this->scopedMemberCount($chapter->id, $orgUserIds), ]; }) ->values(); return response()->json(['chapters' => $chapters]); } /** * Org chart for the caller's chapter (officers + direct children), * or — when a chapter_id is given — the officers of that specific chapter. */ public function getOrgChart(Request $request) { $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $acctType = $user->acct_type; // Drill request: officers of a specific child chapter. $requestedChapterId = $request->input('chapter_id'); if ($requestedChapterId) { $chapter = Chapter::find($requestedChapterId); if (!$chapter) { return ResponseHelper::returnError('Chapter not found', 404); } return response()->json([ 'chapter' => [ 'id' => $chapter->id, 'hashkey' => $chapter->hashkey, 'name' => $chapter->name, 'level' => $chapter->level, ], 'officers' => $this->chapterOfficers((int) $chapter->id), ]); } // Admin / coordinator: optional cooperative_id to view a coop's top chapter. if ($this->isAdminCaller($acctType)) { $cooperativeId = $request->input('cooperative_id'); $rootQuery = Chapter::where('is_active', true) ->orderByRaw("FIELD(level,'national','region','province','city','municipal','barangay')"); if ($cooperativeId) { $rootQuery->where('cooperative_id', $cooperativeId); } $root = $rootQuery->first(); if (!$root) { return response()->json(['own_chapter' => null, 'children' => []]); } return response()->json([ 'own_chapter' => [ 'id' => $root->id, 'hashkey' => $root->hashkey, 'name' => $root->name, 'level' => $root->level, 'location_key' => $root->location_key, 'officers' => $this->chapterOfficers((int) $root->id), ], 'children' => $this->childChapterRows((int) $root->id, $root->cooperative_id), ]); } $cm = $this->resolveCallerChapter((int) $user->id); if (!$cm) { return response()->json(['own_chapter' => null, 'children' => []]); } $ownChapter = [ 'id' => (int) $cm->c_id, 'hashkey' => $cm->c_hashkey, 'name' => $cm->c_name, 'level' => $cm->c_level, 'location_key' => $cm->c_location_key, 'officers' => $this->chapterOfficers((int) $cm->c_id), ]; // COOP_MEMBER: own chapter + officers only, no children/member lists. if ($acctType === UserTypes::RESIDENT) { return response()->json(['own_chapter' => $ownChapter, 'children' => []]); } return response()->json([ 'own_chapter' => $ownChapter, 'children' => $this->childChapterRows((int) $cm->c_id, $cm->c_cooperative_id), ]); } /** * Direct child chapters with active member counts (officers collapsed/empty). */ private function childChapterRows(int $parentId, $cooperativeId): array { $query = Chapter::where('parent_id', $parentId)->where('is_active', true); if ($cooperativeId !== null) { $query->where('cooperative_id', $cooperativeId); } return $query->withCount('activeMembers')->get()->map(fn ($c) => [ 'id' => (int) $c->id, 'hashkey' => $c->hashkey, 'name' => $c->name, 'level' => $c->level, 'member_count' => (int) $c->active_members_count, 'officers' => [], ])->values()->all(); } /** * Caller's own chapter + direct children + cooperative + eligible (non-officer) members. */ public function getOfficerScope(Request $request) { $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } $cm = $this->resolveCallerChapter((int) $user->id); if (!$cm) { return response()->json([ 'own_chapter' => null, 'child_chapters' => [], 'cooperative' => null, 'eligible_members' => [], ]); } $coop = $cm->c_cooperative_id ? Organization::where('id', $cm->c_cooperative_id)->first(['hashkey', 'name']) : null; $childChapters = Chapter::where('parent_id', $cm->c_id) ->where('is_active', true) ->when($cm->c_cooperative_id !== null, fn ($q) => $q->where('cooperative_id', $cm->c_cooperative_id)) ->withCount('activeMembers') ->get() ->map(fn ($c) => [ 'id' => (int) $c->id, 'hashkey' => $c->hashkey, 'name' => $c->name, 'level' => $c->level, 'active_members_count' => (int) $c->active_members_count, ])->values()->all(); $eligible = ChapterMember::join('users', 'users.id', 'chapter_members.user_id') ->where('chapter_members.chapter_id', $cm->c_id) ->where('chapter_members.is_active', true) ->where(function ($q) { $q->whereNull('chapter_members.role')->orWhere('chapter_members.role', 'MEMBER'); }) ->select('users.hashkey as user_hashkey', 'users.name as name', 'users.fullname as fullname', 'chapter_members.role as role') ->get() ->map(fn ($r) => [ 'user_hashkey' => $r->user_hashkey, 'name' => $r->fullname ?: $r->name, 'role' => $r->role, ])->values()->all(); return response()->json([ 'own_chapter' => [ 'id' => (int) $cm->c_id, 'hashkey' => $cm->c_hashkey, 'name' => $cm->c_name, 'level' => $cm->c_level, ], 'child_chapters' => $childChapters, 'cooperative' => $coop ? ['hashkey' => $coop->hashkey, 'name' => $coop->name] : null, 'eligible_members' => $eligible, ]); } /** * Name-only member search across the caller's chapter subtree. * Returns ONLY name, role, chapter_name — no contact/identifying fields. */ public function memberSearch(Request $request) { $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } if (!UserPermissions::isActionPermitted($user->acct_type, UserActions::ManageChapterMembers)) { return ResponseHelper::returnUnauthorized(); } $validator = Validator::make($request->all(), [ 'query' => 'required|string|min:2', ]); if ($validator->fails()) { return ResponseHelper::returnError('Validation failed', 422, $validator->errors()); } $query = trim((string) $request->input('query')); $scopeIds = []; if ($this->isAdminCaller($user->acct_type)) { // Admins: search all active chapters. $scopeIds = Chapter::where('is_active', true)->pluck('id')->map(fn ($v) => (int) $v)->all(); } else { $cm = $this->resolveCallerChapter((int) $user->id); if (!$cm) { return response()->json(['members' => []]); } $scopeIds = $this->subtreeChapterIds((int) $cm->c_id, $cm->c_cooperative_id); } if (empty($scopeIds)) { return response()->json(['members' => []]); } $members = User::join('chapter_members', 'users.id', 'chapter_members.user_id') ->join('chapters', 'chapters.id', 'chapter_members.chapter_id') ->whereIn('chapter_members.chapter_id', $scopeIds) ->where('chapter_members.is_active', true) ->where('users.name', 'LIKE', "%{$query}%") ->select('users.name as name', 'chapter_members.role as role', 'chapters.name as chapter_name') ->distinct() ->limit(30) ->get() ->map(fn ($r) => [ 'name' => $r->name, 'role' => $r->role, 'chapter_name' => $r->chapter_name, ])->values()->all(); return response()->json(['members' => $members]); } /** * Move an eligible member of the caller's chapter to a direct child chapter as an officer. */ public function assignOfficer(Request $request) { $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } if (!UserPermissions::isActionPermitted($user->acct_type, UserActions::AssignChapterOfficer)) { return ResponseHelper::returnUnauthorized(); } $validator = Validator::make($request->all(), [ 'member_user_hashkey' => 'required|string', 'child_chapter_id' => 'required|integer', 'role' => 'required|string|in:' . implode(',', self::OFFICER_ROLES), ]); if ($validator->fails()) { return ResponseHelper::returnError('Validation failed', 422, $validator->errors()); } $cm = $this->resolveCallerChapter((int) $user->id); if (!$cm && !$this->isAdminCaller($user->acct_type)) { return ResponseHelper::returnError('You are not assigned to a chapter.', 422); } $member = User::where('hashkey', $request->input('member_user_hashkey'))->first(); if (!$member) { return ResponseHelper::returnError('Member not found', 404); } $childChapter = Chapter::find($request->input('child_chapter_id')); if (!$childChapter) { return ResponseHelper::returnError('Child chapter not found', 404); } // Eligibility B — child must be a direct child of caller's chapter (admins exempt). if (!$this->isAdminCaller($user->acct_type)) { if ((int) $childChapter->parent_id !== (int) $cm->c_id || (int) $childChapter->cooperative_id !== (int) $cm->c_cooperative_id) { return ResponseHelper::returnError('Selected chapter is not a direct sub-chapter of your chapter.', 422); } } // Eligibility A — member must be an active non-officer in the caller's chapter. $parentChapterId = $this->isAdminCaller($user->acct_type) ? (int) $childChapter->parent_id : (int) $cm->c_id; $parentMembership = ChapterMember::where('user_id', $member->id) ->where('chapter_id', $parentChapterId) ->where('is_active', true) ->first(); if (!$parentMembership) { return ResponseHelper::returnError('Member is not part of the parent chapter.', 422); } if (!empty($parentMembership->role) && $parentMembership->role !== 'MEMBER') { return ResponseHelper::returnError('Member is already an officer.', 422); } $role = $request->input('role'); // 1. Remove existing parent-chapter membership (MOVE, not copy). $parentMembership->update(['is_active' => false, 'updated_by' => Auth::id()]); // 2. Upsert child-chapter officer membership. $existingChild = ChapterMember::where('user_id', $member->id) ->where('chapter_id', $childChapter->id) ->first(); if ($existingChild) { $existingChild->update([ 'role' => $role, 'position' => $role, 'is_manual_override' => true, 'assigned_by' => Auth::id(), 'assigned_at' => now(), 'is_active' => true, 'updated_by' => Auth::id(), ]); } else { ChapterMember::create([ 'hashkey' => Str::random(64), 'user_id' => $member->id, 'chapter_id' => $childChapter->id, 'role' => $role, 'position' => $role, 'is_manual_override' => true, 'assigned_by' => Auth::id(), 'assigned_at' => now(), 'is_active' => true, 'created_by' => Auth::id(), 'updated_by' => Auth::id(), ]); } // 3. Upgrade acct_type if currently a plain coop member. if ($member->acct_type === UserTypes::RESIDENT) { $member->acct_type = UserTypes::KAGAWAD; $member->save(); } return response()->json([ 'success' => true, 'message' => "Assigned {$member->name} as {$role} to {$childChapter->name}.", ]); } /** * Create a chapter one level below the caller's chapter, scoped to its cooperative. */ public function createChapter(Request $request) { $user = Auth::user(); if (!$user) { return ResponseHelper::returnUnauthorized(); } if (!UserPermissions::isActionPermitted($user->acct_type, UserActions::ManageChapterMembers)) { return ResponseHelper::returnUnauthorized(); } $validator = Validator::make($request->all(), [ 'name' => 'required|string|max:255', 'location_key' => 'required|string|max:255', 'lat' => 'nullable|numeric', 'lng' => 'nullable|numeric', ]); if ($validator->fails()) { return ResponseHelper::returnError('Validation failed', 422, $validator->errors()); } $cm = $this->resolveCallerChapter((int) $user->id); if (!$cm) { return ResponseHelper::returnError('You are not assigned to a chapter.', 422); } $childLevels = self::CHILD_LEVELS[$cm->c_level] ?? []; if (empty($childLevels)) { return ResponseHelper::returnError('Cannot create sub-chapters at barangay level.', 422); } $childLevel = $childLevels[0]; $chapter = Chapter::create([ 'hashkey' => \Ramsey\Uuid\Uuid::uuid4()->toString(), 'name' => trim((string) $request->input('name')), 'level' => $childLevel, 'parent_id' => (int) $cm->c_id, 'cooperative_id' => $cm->c_cooperative_id, 'location_key' => strtolower(trim((string) $request->input('location_key'))), 'lat' => $request->input('lat'), 'lng' => $request->input('lng'), 'is_active' => true, 'created_by' => Auth::id(), 'updated_by' => Auth::id(), ]); return response()->json([ 'success' => true, 'chapter' => [ 'hashkey' => $chapter->hashkey, 'name' => $chapter->name, 'level' => $chapter->level, ], ]); } /** * Public: get minimal chapter + cooperative info for a registration link. */ public function publicGetChapter(Request $request, string $hkey) { $chapter = Chapter::where('hashkey', $hkey)->where('is_active', true)->first(); if (!$chapter) { return response()->json(['success' => false, 'message' => 'Chapter not found'], 404); } $coop = $chapter->cooperative_id ? Organization::where('id', $chapter->cooperative_id)->first(['name']) : null; return response()->json([ 'success' => true, 'chapter' => ['name' => $chapter->name, 'level' => $chapter->level], 'cooperative' => ['name' => $coop?->name], ]); } /** * Public: register a new coop member directly into a chapter via a shared link. */ public function publicRegisterToChapter(Request $request) { $hkey = $request->input('chapter_hash'); if (!$hkey) { return ResponseHelper::returnIncorrectDetails(); } $chapter = Chapter::where('hashkey', $hkey)->where('is_active', true)->first(); if (!$chapter) { return response()->json(['success' => false, 'message' => 'Chapter not found'], 404); } $cooperative = $chapter->cooperative_id ? Organization::where('id', $chapter->cooperative_id)->where('type', 'COOPERATIVE')->first() : null; $validator = Validator::make($request->all(), [ 'name' => 'required|string|max:255', 'username' => 'required|string|max:255|unique:users,username', 'mobile_number' => ['required', 'string', 'max:20', 'unique:users,mobile_number', 'regex:/^(09|\+639)\d{9}$/'], 'password' => 'required|string|min:6', ]); if ($validator->fails()) { return response()->json(['success' => false, 'errors' => $validator->errors()], 422); } $validated = $validator->validated(); $parentUser = User::where('id', $chapter->created_by)->first() ?? User::where('acct_type', UserTypes::SECRETARY->value)->first() ?? User::orderBy('id')->first(); if (!$parentUser) { return response()->json(['success' => false, 'message' => 'No valid parent user found'], 500); } $user = new User(); $user->username = $validated['username']; $user->name = $validated['name']; $user->mobile_number = $validated['mobile_number']; $user->password = Hash::make($validated['password']); $user->parentuid = $parentUser->id; $user->acct_type = UserTypes::RESIDENT; $user->active = true; if ($cooperative) { $settings = $user->settings ?? []; if (!is_array($settings)) { $settings = []; } $settings['cooperatives'] = [$cooperative->hashkey]; $user->settings = $settings; } $user->save(); // Link to cooperative. if ($cooperative) { CooperativeMember::create([ 'hashkey' => Str::random(64), 'organization_id' => $cooperative->id, 'user_id' => $user->id, 'is_active' => true, 'created_by' => $user->id, 'updated_by' => $user->id, ]); } // Link to chapter. ChapterMember::create([ 'hashkey' => Str::random(64), 'user_id' => $user->id, 'chapter_id' => $chapter->id, 'is_manual_override' => false, 'is_active' => true, 'created_by' => $user->id, 'updated_by' => $user->id, ]); return response()->json([ 'success' => true, 'user_hashkey' => $user->hashkey, 'message' => 'Account created. You may now log in.', ], 201); } // --- Private helpers --- private function formatChapter(Chapter $chapter): array { $memberCount = $chapter->active_members_count ?? $chapter->activeMembers()->count(); $leaders = $chapter->leaders->map(fn($cm) => [ 'name' => $cm->user?->fullname ?? $cm->user?->name, 'position' => $cm->position, 'photo' => $cm->user?->photourl, 'hashkey' => $cm->user?->hashkey, ])->values()->toArray(); // Check if children already loaded, otherwise count $hasChildren = $chapter->relationLoaded('children') ? $chapter->children->isNotEmpty() : $chapter->children()->where('is_active', true)->exists(); return [ 'id' => $chapter->id, 'hashkey' => $chapter->hashkey, 'name' => $chapter->name, 'level' => $chapter->level, 'location_key' => $chapter->location_key, 'lat' => $chapter->lat, 'lng' => $chapter->lng, 'member_count' => (int) $memberCount, 'leaders' => $leaders, 'has_children' => (bool) $hasChildren, ]; } /** * IDs of users who are active CooperativeMember rows of the given organization. * Returns [] if none — callers should treat that as "no scoped members". */ private function orgUserIds(int $organizationId): array { return CooperativeMember::where('organization_id', $organizationId) ->pluck('user_id') ->filter() ->unique() ->values() ->all(); } private function scopedMemberCount(int $chapterId, array $orgUserIds): int { if (empty($orgUserIds)) return 0; return (int) ChapterMember::where('chapter_id', $chapterId) ->where('is_active', true) ->whereIn('user_id', $orgUserIds) ->count(); } /** * Build a map of island_group → [region chapter ids] using IslandGroupHelper. */ private function regionIdsByIsland(): array { $map = [ IslandGroupHelper::LUZON => [], IslandGroupHelper::VISAYAS => [], IslandGroupHelper::MINDANAO => [], ]; $regions = Chapter::where('level', 'region')->where('is_active', true)->get(['id', 'name', 'location_key']); foreach ($regions as $r) { $island = IslandGroupHelper::fromRegion($r->location_key) ?? IslandGroupHelper::fromRegion($r->name); if ($island && isset($map[$island])) { $map[$island][] = (int) $r->id; } } return $map; } private function islandPseudoChapter(string $island): array { $center = IslandGroupHelper::center($island); return [ 'id' => null, 'island' => $island, 'hashkey' => 'island:' . $island, 'name' => IslandGroupHelper::label($island), 'level' => 'island_group', 'location_key' => $island, 'lat' => $center['lat'], 'lng' => $center['lng'], 'member_count' => 0, 'leaders' => [], 'has_children' => true, ]; } /** * Same as formatChapter but member_count is scoped to org user ids. * Leaders are filtered to those whose user is in the org. */ private function formatChapterScoped(Chapter $chapter, array $orgUserIds): array { $memberCount = $this->scopedMemberCount((int) $chapter->id, $orgUserIds); $leaders = $chapter->leaders ->filter(fn($cm) => empty($orgUserIds) ? false : in_array((int) $cm->user_id, $orgUserIds, true)) ->map(fn($cm) => [ 'name' => $cm->user?->fullname ?? $cm->user?->name, 'position' => $cm->position, 'photo' => $cm->user?->photourl, 'hashkey' => $cm->user?->hashkey, ])->values()->toArray(); $hasChildren = $chapter->relationLoaded('children') ? $chapter->children->isNotEmpty() : $chapter->children()->where('is_active', true)->exists(); return [ 'id' => $chapter->id, 'hashkey' => $chapter->hashkey, 'name' => $chapter->name, 'level' => $chapter->level, 'location_key' => $chapter->location_key, 'lat' => $chapter->lat, 'lng' => $chapter->lng, 'member_count' => (int) $memberCount, 'leaders' => $leaders, 'has_children' => (bool) $hasChildren, ]; } /** * Breadcrumb walking up to the region, then prepending the island group. */ private function buildOrgBreadcrumb(Chapter $chapter): array { $crumbs = []; $current = $chapter; while ($current->parent_id) { $current = $current->parent; if (!$current) break; if ($current->level === 'national') break; array_unshift($crumbs, ['id' => $current->id, 'name' => $current->name, 'level' => $current->level]); } // Determine island from the topmost region in the trail (or the chapter itself). $regionRow = collect($crumbs)->first(fn($c) => $c['level'] === 'region'); $regionName = $regionRow['name'] ?? ($chapter->level === 'region' ? $chapter->name : null); $island = IslandGroupHelper::fromRegion($regionName); if ($island) { array_unshift($crumbs, [ 'id' => null, 'island' => $island, 'name' => IslandGroupHelper::label($island), 'level' => 'island_group', ]); } return $crumbs; } private function buildBreadcrumb(Chapter $chapter): array { $breadcrumb = []; $current = $chapter; while ($current->parent_id) { $current = $current->parent; if (!$current) break; array_unshift($breadcrumb, ['id' => $current->id, 'name' => $current->name, 'level' => $current->level]); } return $breadcrumb; } }