initial: bootstrap from BukidBountyApp base

This commit is contained in:
Jonathan Sykes
2026-06-06 18:43:00 +08:00
commit eb4a5731fb
5674 changed files with 160857 additions and 0 deletions

View File

@@ -0,0 +1,469 @@
---
task: Cooperative officer/member homepages, chapter org chart, member search, create user, assign officer to child chapter, create chapter, share registration link
cycles: 5
context: true
private: false
started: 2026-05-29T00:00:00Z
finished: 2026-05-29T00:01:00Z
---
## prerequisite
This plan DEPENDS on plan `4fc3b455fb62b15dfb06790a1f421c96` (COOP_MEMBER/COOP_OFFICER types + migrations). Execute that plan first. Assumes the following already exist after that plan runs:
- `users.acct_type` values `'coop officer'` and `'coop member'` (UserTypes enum)
- `chapters.cooperative_id` column + `chapters.level` enum includes `'municipal'`
- `chapter_members.role` column
- `UserActions::ViewChapterOrgChart`, `ManageChapterMembers`, `ViewScopedMemberReports`, `AssignChapterOfficer`
- `UserTypes::COOP_OFFICER` and `UserTypes::COOP_MEMBER` in UserPermissions::roles()
- `isCoopOfficer` and `isCoopMember` in `useAuth.js`
- `Home.vue` fragments wired for COOP_OFFICER and COOP_MEMBER
## files
- `app/Http/Controllers/Support/ChapterController.php` — add orgChart(), officerAssign(), createChapter(), memberSearch() methods
- `app/Http/Controllers/Market/CooperativeController.php` [line 410] — extend publicRegisterMember to accept chapter_hash; auto-assign to chapter
- `routes/web.php` [lines 761-770] — register new chapter endpoints + public chapter route
- `app/Http/Controllers/Support/VueRouteMap.php` [lines 278-311] — register new SPA pages; add coop officer/member to existing coop allowedUserTypes
- `resources/js/Pages/Fragments/Home/HomeCoopOfficer.vue` — NEW: level-aware officer dashboard
- `resources/js/Pages/Fragments/Home/HomeCoopMember.vue` — NEW: read-only member card
- `resources/js/Pages/ChapterOrgChart.vue` — NEW: org chart with child chapter toggle
- `resources/js/Pages/CoopMemberSearch.vue` — NEW: name-only member search for officers
- `resources/js/Pages/CreateCoopUser.vue` — NEW: full user creation with coop+chapter auto-assign
- `resources/js/Pages/AssignChapterOfficer.vue` — NEW: move member to child chapter as officer
- `resources/js/Pages/CreateChapter.vue` — NEW: create chapter one level below current officer's chapter
- `resources/js/composables/useChapters.js` — add fetchOfficerScope(), fetchOrgChart(), searchMembers(), assignOfficer(), createChapter()
- `routes/web.php` [line ~754] — add public chapter info + registration endpoints
## level hierarchy constants
```
LEVEL_ORDER = ['national','region','province','city','municipal','barangay']
CHILD_LEVELS = {
'national' => ['region'],
'region' => ['province'],
'province' => ['city','municipal'],
'city' => ['barangay'],
'municipal' => ['barangay'],
'barangay' => [],
}
```
All PHP code using level must use `in_array($level, ['city','municipal'])` never equality check.
## steps
### 1. ChapterController — getOrgChart()
Add `public function getOrgChart(Request $request)` to `ChapterController.php`:
- Auth: caller must be logged in
- Determine caller's chapter: `ChapterMember::with('chapter')->where('user_id', Auth::id())->where('is_active', true)->orderByRaw("FIELD(chapters.level, 'region','province','city','municipal','barangay')")->join('chapters','chapters.id','chapter_members.chapter_id')->first()`
- For COOP_OFFICER: load own chapter + its officers (chapter_members WHERE role IS NOT NULL AND role != 'MEMBER') + direct child chapters (WHERE parent_id = own chapter id AND cooperative_id = own cooperative_id) each with `active_members_count` and their own officers (collapsed by default)
- For COOP_MEMBER: load own chapter name + officers only (names + roles) — do NOT return member list
- For Big3/COORDINATOR: accept optional `cooperative_id` param, return full coop chapter tree
- Response shape:
```json
{
"own_chapter": { "id", "hashkey", "name", "level", "location_key", "officers": [{"name","role","position"}] },
"children": [{ "id", "hashkey", "name", "level", "member_count": 42, "officers": [] }]
}
```
Children `officers` array starts empty (populated by frontend on toggle via second call to same endpoint with `chapter_id` param pointing to that child).
### 2. ChapterController — assignOfficer()
Add `public function assignOfficer(Request $request)`:
- Validate: `member_user_hashkey` (string, required), `child_chapter_id` (integer, required), `role` (string, required: PRESIDENT/VICE_PRESIDENT/SECRETARY/TREASURER/AUDITOR/BOARD_MEMBER)
- Permission check: caller must be COOP_OFFICER (or Big3/COORDINATOR)
- Eligibility check A — the selected user: must have a chapter_members row in caller's OWN chapter (`is_active = true`), AND role IS NULL OR role = 'MEMBER' (not already an officer there)
- Eligibility check B — child_chapter_id: must have `parent_id = caller's own chapter id` AND `cooperative_id = caller's cooperative_id`
- Effect:
1. Delete the member's existing chapter_members row in the parent chapter
2. Insert/update chapter_members row: `{user_id, chapter_id: child_chapter_id, role, position: role label, is_manual_override: true, assigned_by: Auth::id(), is_active: true, created_by, updated_by, hashkey}`
3. Upgrade `users.acct_type = 'coop officer'` if currently `'coop member'`
- Return `{ success: true, message: '...' }`
### 3. ChapterController — createChapter()
Add `public function createChapter(Request $request)`:
- Validate: `name` (string, required), `location_key` (string, required), `lat` (nullable decimal), `lng` (nullable decimal)
- Permission check: caller must be COOP_OFFICER (or Big3/COORDINATOR)
- Determine caller's chapter level → compute allowed child level using CHILD_LEVELS map
- If caller is at BARANGAY level, return 422 "Cannot create sub-chapters at barangay level"
- Determine caller's cooperative_id from their chapter's cooperative_id
- Create Chapter: `{ name, level: childLevel, parent_id: callerChapterId, cooperative_id: callerCoopId, location_key: strtolower(trim()), lat, lng, is_active: true, created_by, updated_by, hashkey }`
- Return `{ success: true, chapter: { hashkey, name, level } }`
### 4. ChapterController — memberSearch()
Add `public function memberSearch(Request $request)`:
- Validate: `query` (string, required, min 2)
- Permission check: caller must have `ManageChapterMembers` action (COOP_OFFICER, COORDINATOR, Big3)
- Scope: collect all chapter IDs in caller's subtree (own chapter + all descendants recursively via `parent_id` chain, scoped to same `cooperative_id`)
- Query: `User::join('chapter_members','users.id','chapter_members.user_id')->whereIn('chapter_members.chapter_id', $scopeIds)->where('chapter_members.is_active', true)->where('users.name', 'LIKE', "%{$query}%")->select('users.name', 'chapter_members.role', 'chapters.name as chapter_name')->join('chapters','chapters.id','chapter_members.chapter_id')->distinct()->limit(30)->get()`
- Return ONLY: `[{ name, role, chapter_name }]` — NO hashkey, NO mobile_number, NO username, NO other fields
### 5. ChapterController — getOfficerScope()
Add `public function getOfficerScope(Request $request)`:
- Returns caller's own chapter info + list of direct child chapters (for dropdowns in AssignChapterOfficer and CreateChapter pages)
- Response: `{ own_chapter: {id, hashkey, name, level}, child_chapters: [{id, hashkey, name, level, active_members_count}], cooperative: {hashkey, name}, eligible_members: [{user_hashkey, name, role}] }`
- `eligible_members`: chapter_members WHERE chapter_id = own chapter AND is_active = true AND (role IS NULL OR role = 'MEMBER') → join users → return {user_hashkey: users.hashkey, name: users.name OR users.fullname, role}
### 6. Public chapter registration endpoint
In `routes/web.php` add after existing public coop routes (~line 755):
```php
Route::get('/api/public/chapter/{hkey}', [\App\Http\Controllers\Support\ChapterController::class, 'publicGetChapter']);
Route::post('/api/public/chapter/register', [\App\Http\Controllers\Support\ChapterController::class, 'publicRegisterToChapter']);
```
Add `publicGetChapter(Request $request, string $hkey)` to ChapterController:
- No auth required
- Return chapter name, level, cooperative name — nothing else
- Response: `{ success: true, chapter: { name, level }, cooperative: { name } }`
Add `publicRegisterToChapter(Request $request)` to ChapterController:
- No auth required
- Params: `chapter_hash` (string), `name`, `username`, `mobile_number`, `password`
- Validate same as publicRegisterMember
- Load chapter by hashkey; load cooperative from chapter.cooperative_id
- Create user: acct_type = 'coop member', parentuid = chapter.created_by (fallback: first COORDINATOR)
- Insert cooperative_members row linking user to cooperative
- Insert chapter_members row: `{user_id, chapter_id: chapter.id, is_manual_override: false, is_active: true, created_by: user.id, updated_by: user.id, hashkey}`
- Update user.settings.cooperatives = [cooperative.hashkey]
- Return `{ success: true, user_hashkey, message }`
### 7. routes/web.php — register new chapter endpoints
In the Chapter routes block (lines 761770), add:
```php
Route::post('/Chapters/OrgChart', [\App\Http\Controllers\Support\ChapterController::class, 'getOrgChart'], ['middleware' => 'auth']);
Route::post('/Chapters/Officer/Assign', [\App\Http\Controllers\Support\ChapterController::class, 'assignOfficer'], ['middleware' => 'auth']);
Route::post('/Chapters/Create', [\App\Http\Controllers\Support\ChapterController::class, 'createChapter'], ['middleware' => 'auth']);
Route::post('/Chapters/Members/Search', [\App\Http\Controllers\Support\ChapterController::class, 'memberSearch'], ['middleware' => 'auth']);
Route::post('/Chapters/Officer/Scope', [\App\Http\Controllers\Support\ChapterController::class, 'getOfficerScope'], ['middleware' => 'auth']);
```
### 8. home-data — COOP_OFFICER and COOP_MEMBER branches
In the `/home-data` closure in `routes/web.php` (after the COORDINATOR block, ~line 153):
```php
if ($acctType === \App\Enums\UserTypes::COOP_OFFICER && $user) {
$myChapterMember = \App\Models\ChapterMember::join('chapters','chapters.id','chapter_members.chapter_id')
->where('chapter_members.user_id', $user->id)->where('chapter_members.is_active', true)
->orderByRaw("FIELD(chapters.level,'region','province','city','municipal','barangay')")
->select('chapter_members.*','chapters.name as chapter_name','chapters.level as chapter_level','chapters.hashkey as chapter_hashkey','chapters.cooperative_id')
->first();
if ($myChapterMember) {
$myChapterId = $myChapterMember->chapter_id;
$chapterMemberCount = \App\Models\ChapterMember::where('chapter_id', $myChapterId)->where('is_active', true)->count();
$childChapterCount = \App\Models\Chapter::where('parent_id', $myChapterId)->where('is_active', true)->count();
$newMembersCount = \App\Models\ChapterMember::where('chapter_id', $myChapterId)->where('is_active', true)->where('created_at', '>=', now()->subDays(7))->count();
$cooperativeHash = \App\Models\Market\Organization::where('id', $myChapterMember->cooperative_id)->value('hashkey');
$props['props']['chapter_info'] = [
'chapter_name' => $myChapterMember->chapter_name,
'chapter_level' => $myChapterMember->chapter_level,
'chapter_hashkey' => $myChapterMember->chapter_hashkey,
'cooperative_hash' => $cooperativeHash,
];
$props['props']['stats']['chapter_member_count'] = $chapterMemberCount;
$props['props']['stats']['child_chapter_count'] = $childChapterCount;
$props['props']['stats']['new_members_7d'] = $newMembersCount;
}
}
if ($acctType === \App\Enums\UserTypes::COOP_MEMBER && $user) {
$myChapterMember = \App\Models\ChapterMember::join('chapters','chapters.id','chapter_members.chapter_id')
->where('chapter_members.user_id', $user->id)->where('chapter_members.is_active', true)
->orderByRaw("FIELD(chapters.level,'barangay','city','municipal','province','region','national')")
->select('chapter_members.*','chapters.name as chapter_name','chapters.level as chapter_level','chapters.id as chapter_id','chapters.cooperative_id')
->first();
if ($myChapterMember) {
$chapterMemberCount = \App\Models\ChapterMember::where('chapter_id', $myChapterMember->chapter_id)->where('is_active', true)->count();
$cooperativeName = \App\Models\Market\Organization::where('id', $myChapterMember->cooperative_id)->value('name');
$props['props']['chapter_info'] = [
'chapter_name' => $myChapterMember->chapter_name,
'chapter_level' => $myChapterMember->chapter_level,
'cooperative_name' => $cooperativeName,
'member_count' => $chapterMemberCount,
];
}
}
```
### 9. VueRouteMap.php — register new pages + update existing allowedUserTypes
Add to the routes array (near the cooperatives block, ~line 308):
```php
'/chapter-org-chart' => [
'component' => 'ChapterOrgChart',
'loginRequired' => true,
'allowedUserTypes'=> ['ult','super operator','operator','coordinator','coop officer','coop member'],
'module' => 'cooperatives',
],
'/coop-member-search' => [
'component' => 'CoopMemberSearch',
'loginRequired' => true,
'allowedUserTypes'=> ['ult','super operator','operator','coordinator','coop officer'],
'module' => 'cooperatives',
],
'/create-coop-user' => [
'component' => 'CreateCoopUser',
'loginRequired' => true,
'allowedUserTypes'=> ['ult','super operator','operator','coordinator','coop officer'],
'module' => 'cooperatives',
],
'/assign-chapter-officer' => [
'component' => 'AssignChapterOfficer',
'loginRequired' => true,
'allowedUserTypes'=> ['ult','super operator','operator','coordinator','coop officer'],
'module' => 'cooperatives',
],
'/create-chapter' => [
'component' => 'CreateChapter',
'loginRequired' => true,
'allowedUserTypes'=> ['ult','super operator','operator','coordinator','coop officer'],
'module' => 'cooperatives',
],
'/register-chapter' => [
'component' => 'RegisterChapter',
'loginRequired' => false,
'allowedUserTypes'=> ['*'],
'module' => 'cooperatives',
],
```
Also update existing `/cooperative-list`, `/cooperative-detail`, `/cooperative-member-register` entries to include `'coop officer'` and `'coop member'` in their `allowedUserTypes`.
### 10. useChapters.js — add new methods
Append to the `useChapters()` composable return object:
```js
const fetchOrgChart = async ({ chapterId = null } = {}) => { /* POST /Chapters/OrgChart */ }
const fetchOfficerScope = async () => { /* POST /Chapters/Officer/Scope */ }
const searchMembers = async (query) => { /* POST /Chapters/Members/Search */ }
const assignOfficer = async ({ memberUserHashkey, childChapterId, role }) => { /* POST /Chapters/Officer/Assign */ }
const createChapter = async ({ name, locationKey, lat = null, lng = null }) => { /* POST /Chapters/Create */ }
```
Follow same loading/error pattern as existing methods.
### 11. HomeCoopOfficer.vue — NEW
`resources/js/Pages/Fragments/Home/HomeCoopOfficer.vue`
Pattern: mirror `HomeCooperative.vue` structure (imports: usePageData, useNavigate, useAuth, useModal, BalanceBox, ServiceButtonGrid, SideTextButtonList, HomeSkeleton).
`onMounted`: call `fetchPageData('/home-data')`. Extract `data.value?.chapter_info` for chapter name/level/hashkey/cooperative_hash. Extract `data.value?.stats` for member count, child chapters, new members 7d.
Stats cards:
```js
[
{ title: 'Members', number: chapter_member_count, unit: 'In Chapter' },
{ title: 'Sub-Chapters', number: child_chapter_count, unit: 'Direct' },
{ title: 'New (7d)', number: new_members_7d, unit: 'Members' },
]
```
Chapter badge: show chapter level (capitalize) + chapter name. e.g. "REGIONAL — Bukidnon Chapter".
Services grid (icon: use FontAwesome `fas fa-*`, NOT micons for new components):
```js
[
{ icon: 'fas fa-sitemap', title: 'Org Chart', pagename: 'ChapterOrgChart' },
{ icon: 'fas fa-search', title: 'Search Members', pagename: 'CoopMemberSearch' },
{ icon: 'fas fa-user-plus', title: 'Create User', pagename: 'CreateCoopUser' },
{ icon: 'fas fa-user-tie', title: 'Assign Officer', pagename: 'AssignChapterOfficer' },
{ icon: 'fas fa-map-marker-alt', title: 'Create Chapter', pagename: 'CreateChapter' },
{ icon: 'fas fa-share-alt', title: 'Share Invite', action: 'shareChapterLink' },
]
```
SideTextButtonList:
```js
[
{ text: 'My Cooperative', action: 'viewMyCoop' },
{ text: 'My Profile', pagename: 'UserInfoEdit' },
{ text: 'Member Ledger', pagename: 'AccountingDashboard' },
]
```
`shareChapterLink()` handler:
```js
const shareChapterLink = () => {
const coopHash = chapterInfo.value?.cooperative_hash;
const chapterHash = chapterInfo.value?.chapter_hashkey;
if (!coopHash || !chapterHash) return;
const encoded = btoa(JSON.stringify({ coop_hash: coopHash, chapter_hash: chapterHash }));
const url = `${window.location.origin}/register-chapter--e:${encoded}`;
if (navigator.share) {
navigator.share({ title: 'Join our cooperative chapter', url }).catch(() => {});
} else {
navigator.clipboard?.writeText(url);
modal.quickDismiss({ title: 'Link Copied', body: 'Registration link copied to clipboard.' });
}
};
```
No `bg-white`, `bg-light`, or `text-dark` hardcoded. Use `var(--bg-card)`, `var(--text-primary)`. Sticky header: `top: 66px; z-index: 1020;` if used.
### 12. HomeCoopMember.vue — NEW
`resources/js/Pages/Fragments/Home/HomeCoopMember.vue`
Fetch `/home-data`. Extract `chapter_info` (chapter_name, chapter_level, cooperative_name, member_count).
Display:
- Chapter name card: chapter_level (capitalized) + chapter_name + cooperative_name
- Member count badge: "X members in this chapter" — count only, NO names list
- Org chart strip: fetch `/Chapters/OrgChart` (own chapter's officers) → display name + role, e.g. "Pres. Juan dela Cruz". Do NOT display member names.
Services grid:
```js
[
{ icon: 'fas fa-sitemap', title: 'Org Chart', pagename: 'ChapterOrgChart' },
{ icon: 'fas fa-handshake', title: 'My Cooperative', action: 'viewMyCoop' },
{ icon: 'fas fa-user-circle', title: 'My Profile', pagename: 'UserInfoEdit' },
{ icon: 'fas fa-wallet', title: 'My Wallet', pagename: 'MyWallet' },
]
```
No search button, no create user, no assign officer.
### 13. ChapterOrgChart.vue — NEW
Route: `/chapter-org-chart` (no hash needed — scoped server-side to caller).
`onMounted`: call `POST /Chapters/OrgChart` → get `{ own_chapter, children }`.
Render:
1. **Header card**: own_chapter.name + level badge
2. **Officers section** (own chapter): flat list of `officer.name + officer.role` badges. If no officers: "No officers assigned yet."
3. **Child chapters list**: for each child in `children`:
- Row: `[level badge] Chapter Name ——— X members [▶ toggle icon]`
- Toggle (`showOfficers[child.id]` ref): on click, if `child.officers` is empty, call `POST /Chapters/OrgChart` with `{ chapter_id: child.id }` to fetch and populate `child.officers`. Show officer names + roles below the row.
4. COOP_MEMBER: show only own_chapter.name + officer list. No children drill-down. No member names anywhere.
No `bg-white`/`bg-light`. Use theme variables.
### 14. CoopMemberSearch.vue — NEW
Route: `/coop-member-search`
Simple search page:
- Input: text field with `v-model="query"`, debounced 400ms, min 2 chars
- On each debounced change: call `POST /Chapters/Members/Search { query }``[{ name, role, chapter_name }]`
- Results: list of cards showing `name`, `chapter_name`, role badge (if officer). NO mobile, NO hashkey link, NO other fields.
- Empty state if < 2 chars: "Type at least 2 characters to search."
- No results: "No members found matching '{{query}}'."
### 15. CreateCoopUser.vue — NEW
Route: `/create-coop-user`
`onMounted`: call `POST /Chapters/Officer/Scope` to get `{ own_chapter, cooperative, cooperative_list (if multiple) }`.
If officer has multiple cooperatives show cooperative picker dropdown first.
Form fields (full user creation):
- `name` (display name), `username`, `mobile_number` (format 09XXXXXXXXX), `password`, `fullname`, `firstname`, `middlename`, `lastname` (optional extras for UserInfo)
- Real-time duplicate check via existing `/admin/check/mobile` and `/admin/check/username` endpoints
- Chapter assignment: auto-set to officer's own chapter (read-only display: "Will be added to: [chapter_name]")
- Cooperative: auto-set (read-only if single, picker if multiple)
On submit: `POST /api/public/chapter/register` with `{ chapter_hash, name, username, mobile_number, password }`. On success show confirmation.
Cooperative picker: use `user.value?.settings?.cooperatives` array resolve names via `POST /Cooperatives/Get` for each hashkey show select modal. Selected cooperative must have a chapter that matches.
### 16. AssignChapterOfficer.vue — NEW
Route: `/assign-chapter-officer`
`onMounted`: call `POST /Chapters/Officer/Scope` get `eligible_members` (members of own chapter, not yet officers) + `child_chapters`.
Step 1 pick member: searchable list of `eligible_members` (name only). Tap to select.
Step 2 pick child chapter: list of `child_chapters` from scope. Tap to select.
Step 3 pick role: dropdown `[PRESIDENT, VICE_PRESIDENT, SECRETARY, TREASURER, AUDITOR, BOARD_MEMBER]`
Step 4 confirm: "Assign [name] as [role] to [child_chapter_name]? This will MOVE them from [own_chapter_name] to [child_chapter_name]."
On confirm: `POST /Chapters/Officer/Assign { member_user_hashkey, child_chapter_id, role }`.
On success: navigate back to home.
### 17. CreateChapter.vue — NEW
Route: `/create-chapter`
`onMounted`: call `POST /Chapters/Officer/Scope` get own_chapter (name, level). Compute `childLevel` from CHILD_LEVELS map. If own_chapter.level === 'barangay' show error "Cannot create sub-chapters at barangay level."
Form:
- `name` (string, required)
- `location_key` (auto-lowercase from name, editable)
- `lat`, `lng` (optional)
- Read-only: "Level: [childLevel]", "Parent: [own_chapter.name]", "Cooperative: [cooperative.name]"
On submit: `POST /Chapters/Create { name, location_key, lat, lng }`.
On success: show confirmation with new chapter name + level badge.
### 18. RegisterChapter.vue — NEW (public page)
Route: `/register-chapter--e:<encoded>` (no auth required `loginRequired: false`)
`onMounted`:
1. Decode `target` prop (base64 JSON: `{ coop_hash, chapter_hash }`)
2. Call `GET /api/public/chapter/{chapter_hash}` show chapter name, level, cooperative name
Form:
- `name`, `username`, `mobile_number`, `password`
- Real-time duplicate checks via existing public endpoints
- On submit: `POST /api/public/chapter/register { chapter_hash, name, username, mobile_number, password }`
- On success: show "Welcome to [cooperative name] [chapter name]! Your account has been created. Please login." with Login button.
No auth required. No sidebar/header full-page public layout (mirror `RegisterCoop.vue` structure).
## context
### home-data closure location (routes/web.php lines 101-199)
COORDINATOR branch at line 147. Insert COOP_OFFICER and COOP_MEMBER blocks after line 153 (after end of coordinator if block).
`$acctType` already resolved at line 117. Use `\App\Enums\UserTypes::COOP_OFFICER` and `\App\Enums\UserTypes::COOP_MEMBER`.
### ChapterController existing methods (lines 27, 75, 108, 141, 194)
- `hierarchy()` line 27 GET chapters by parent
- `mapData()` line 75 lat/lng dots
- `members()` line 108 members for chapter_id (returns full user info DO NOT reuse for search, too much data)
- `assignMember()` line 141 generic assign (does NOT move, does NOT upgrade acct_type new `assignOfficer()` must handle move + upgrade)
- `removeMember()` line 194 remove by hashkey
### CooperativeController::publicRegisterMember (lines 410-460)
Creates user with `acct_type = 'user'`, no chapter assignment. The new `publicRegisterToChapter` in ChapterController is the chapter-aware equivalent.
### useChapters.js existing methods (lines 8-127)
Returns: fetchHierarchy, fetchMapData, fetchOrgHierarchy, fetchOrgMapData, fetchMembers, assignMember, removeMember, fetchPositions, syncAutoAssignments.
ADD: fetchOrgChart (POST /Chapters/OrgChart), fetchOfficerScope (POST /Chapters/Officer/Scope), searchMembers (POST /Chapters/Members/Search), assignOfficer (POST /Chapters/Officer/Assign), createChapter (POST /Chapters/Create).
### RegisterCoop.vue (existing public page — mirror for RegisterChapter.vue)
File: `resources/js/Pages/RegisterCoop.vue`. Route: `/register-coop--h:HASHKEY`. No auth. Calls `GET /api/public/cooperative/{hkey}` then `POST /api/public/cooperative/register`. Mirror this structure for RegisterChapter.vue.
### Share link encoding
Payload: `btoa(JSON.stringify({ coop_hash, chapter_hash }))`. Frontend decodes with `JSON.parse(atob(target))` where `target` is the prop injected by VueRouteMap from the `--e:` URL segment. This follows the existing encoded payload pattern already used in VueRouteMap.
### Web Share API pattern (already in CooperativeDetail.vue)
```js
if (navigator.share) {
navigator.share({ title, url }).catch(() => {});
} else {
navigator.clipboard?.writeText(url);
// show copied notice
}
```
### LEVEL_ORDER for PHP childLevel lookup
```php
const CHILD_LEVELS = [
'national' => ['region'],
'region' => ['province'],
'province' => ['city', 'municipal'],
'city' => ['barangay'],
'municipal' => ['barangay'],
'barangay' => [],
];
```
Helper: `$childLevels = CHILD_LEVELS[$chapter->level] ?? []; if (empty($childLevels)) { return 422; }`
### Hypervel migration imports (for any new migration if needed)
```php
use Hyperf\Database\Schema\Blueprint;
use Hypervel\Database\Migrations\Migration;
use Hypervel\Support\Facades\Schema;
```
NO Illuminate classes.
### UserActions in $RoleswithNoTargetUser
`ViewChapterOrgChart`, `ManageChapterMembers`, `ViewScopedMemberReports`, `AssignChapterOfficer` must be in `UserPermissions::$RoleswithNoTargetUser` (added by prerequisite plan). Verify before adding routes.
## notes
- dictionary: ai-docs/dictionary.md
- linters: none detected
- constraints:
- PREREQUISITE: execute plan 4fc3b455fb62b15dfb06790a1f421c96 first (adds COOP_OFFICER/COOP_MEMBER types, migrations, permissions, Home.vue fragments)
- All chapter level checks use in_array(level, ['city','municipal']) not equality city and municipal are peers
- memberSearch NEVER returns mobile_number, hashkey, username or any field other than name/role/chapter_name
- HomeCoopMember NEVER shows fellow member names member count only; officer names ARE shown in org chart strip
- assignOfficer MOVES the member (delete parent row, insert child row) not a copy
- createChapter links to officer's cooperative via cooperative_id chapters are coop-scoped
- share link uses encoded payload (btoa JSON) not hashkey decode in RegisterChapter.vue onMounted
- No bg-white, bg-light, text-dark in any new Vue component use var(--bg-card), var(--text-primary)
- Sticky headers must use top: 66px; z-index: 1020
- Raw DB queries must include created_by/updated_by (Auth::id()) and timestamps (now())
- All new routes use Hypervel-style Route::post(..., ..., ['middleware' => 'auth'])