initial: bootstrap from BukidBountyApp base
This commit is contained in:
469
.claude/plans/e8e0be1ce348a4e47641f59cdf136890-complete.md
Normal file
469
.claude/plans/e8e0be1ce348a4e47641f59cdf136890-complete.md
Normal 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 761–770), 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'])
|
||||
Reference in New Issue
Block a user