# Plan: Member Cooperative Registration From Cooperative Detail View **Plan ID:** `11a8aa601d3b45c6aa16ff2b28eb1004` **Created:** 2026-04-17 **Status:** Pending --- ## Overview Implement a **Member Cooperative Registration** flow that allows users to register (join) as members of a cooperative **after viewing its details** on the `CooperativeDetail` page. Currently, the system has: - An `EnrollFarmer` flow (admin-initiated, adds a farmer to a cooperative) - A `joinCooperative` backend method on `CooperativeController` (self-join, but no frontend) - A `useUserAdditionalDetails` composable with `joinCooperative` / `leaveCooperative` methods (settings-only sync, no full membership form) **What's missing:** A dedicated, user-facing registration page where a member can view cooperative details and then register themselves with full membership information (membership type, position, etc.). --- ## Architecture Summary ``` CooperativeDetail.vue └── "Register as Member" button └── CooperativeMemberRegister.vue └── POST /Cooperatives/Member/Register └── CooperativeController@registerMember ├── Creates cooperative_members record └── Syncs users.settings.cooperatives ``` --- ## Task 1: Backend — New `registerMember` Method in CooperativeController ### Description Create a new public method `registerMember` on `CooperativeController` that allows an authenticated user to self-register as a member of a cooperative. This differs from the existing `joinCooperative` method (which is minimal) by accepting a full membership form with all `cooperative_members` fields. ### Target File `app/Http/Controllers/Market/CooperativeController.php` ### Detailed Steps 1. **Add method signature:** ```php public function registerMember(Request $request) ``` 2. **Permission check:** Use `UserActions::JoinCooperative` for the permission gate: ```php if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::JoinCooperative)) { return ResponseHelper::returnUnauthorized(); } ``` 3. **Validate required input:** - `cooperative_hash` (required) — hashkey of the target cooperative - All other membership fields are optional 4. **Lookup the cooperative:** ```php $cooperative = Organization::where('hashkey', $cooperativeHash)->first(); if (!$cooperative) { return ResponseHelper::returnError('Cooperative not found', 404); } ``` 5. **Check duplicate membership:** ```php $existing = CooperativeMember::where('organization_id', $cooperative->id) ->where('user_id', $user->id) ->first(); if ($existing) { return ResponseHelper::returnError('You are already a member of this cooperative'); } ``` 6. **Accept full membership fields from request:** ```php $memberFields = $request->only([ 'role', 'membership_type', 'membership_level', 'officer_position', 'officer_level', 'concurrent_position', 'concurrent_level', 'cooperative_name_alt', 'cooperative_position', 'year_beginning' ]); ``` 7. **Create the `CooperativeMember` record:** ```php $member = new CooperativeMember(array_merge($memberFields, [ 'organization_id' => $cooperative->id, 'user_id' => $user->id, 'role' => $request->input('role', 'MEMBER'), 'joined_at' => now(), 'is_active' => true, ])); $member->save(); ``` 8. **Sync with `users.settings.cooperatives`:** ```php $settings = $user->settings ?? []; $cooperatives = $settings['cooperatives'] ?? []; if (!in_array($cooperativeHash, $cooperatives)) { $cooperatives[] = $cooperativeHash; $settings['cooperatives'] = $cooperatives; $user->settings = $settings; $user->save(); } ``` 9. **Return success:** ```php return ResponseHelper::returnSuccessResponse($member, $member->hashkey, 'Successfully registered as a cooperative member'); ``` ### Acceptance Criteria - [ ] Authenticated users can self-register with full membership fields - [ ] Duplicate membership is prevented - [ ] User settings are synced with the cooperative hashkey - [ ] Proper error handling for missing cooperative or unauthorized access --- ## Task 2: Backend — Register New Route ### Description Add a new POST route for the member registration endpoint. ### Target File `routes/web.php` ### Detailed Steps 1. **Add route under the Cooperative Module Routes section** (after line 543): ```php Route::post('/Cooperatives/Member/Register', [\App\Http\Controllers\Market\CooperativeController::class, 'registerMember'], ['middleware' => 'auth']); ``` ### Acceptance Criteria - [ ] Route is registered and protected by `auth` middleware - [ ] Route follows existing naming convention (`/Cooperatives/Member/Register`) --- ## Task 3: Frontend — Create `CooperativeMemberRegister.vue` Page ### Description Build a new Vue 3 Composition API page that presents a multi-step registration form for a user to join a cooperative. This page is navigated to from `CooperativeDetail.vue` and receives the cooperative `hashkey` as a `target` prop. ### Target File `resources/js/Pages/CooperativeMemberRegister.vue` ### Detailed Steps #### 3.1 — Script Setup ```javascript import { ref, onMounted, computed } from 'vue'; import axios from 'axios'; import { usePageTitle } from '../composables/Core/usePageTitle'; import { useNavigate } from '../composables/Core/useNavigate'; import { useModal } from '../composables/Core/useModal'; import CardSimple from '../Components/Core/CardSimple.vue'; const props = defineProps({ target: String }); usePageTitle('Register as Cooperative Member'); const { navigate } = useNavigate(); const modal = useModal(); ``` #### 3.2 — State Variables ```javascript const cooperative = ref(null); const loadingCoop = ref(true); const isSaving = ref(false); const alreadyMember = ref(false); const form = ref({ membership_type: '', membership_level: '', officer_position: '', officer_level: '', concurrent_position: '', concurrent_level: '', cooperative_name_alt: '', cooperative_position: '', year_beginning: '', }); ``` #### 3.3 — Fetch Cooperative Details on Mount - Call `POST /Cooperatives/Get` with `{ hashkey: props.target }` - Populate `cooperative.value` - Check `response.data.is_member` to set `alreadyMember` (depends on Task 7) #### 3.4 — Cooperative Info Header Display a read-only header card showing: - Cooperative name (bold, primary color) - Address - Registration number / CIN / Type - Member count badge - Use the same premium header style from `CooperativeDetail.vue` (bg-primary card with white text) #### 3.5 — Registration Form (using `CardSimple` component) Present the following fields organized in logical sections: **Section: Membership Information** | Field | Type | Placeholder | Notes | |---|---|---|---| | `membership_type` | `` | Select Level | Options: PRIMARY, SECONDARY, TERTIARY | | `year_beginning` | `` | e.g. 2024 | Year membership begins | **Section: Position Details (Optional)** | Field | Type | Placeholder | Notes | |---|---|---|---| | `officer_position` | `` | e.g. Board Member | Optional officer role | | `officer_level` | `` | e.g. Treasurer | Additional concurrent role | | `concurrent_level` | `` | e.g. Chairperson | Position within the cooperative | **Section: Alternative Cooperative Name (Optional)** | Field | Type | Placeholder | Notes | |---|---|---|---| | `cooperative_name_alt` | `` | Alternative name | If the cooperative is known by another name | #### 3.6 — Submit Button - Full width, `btn-premium-launch` style (reuse from `CreateCooperative.vue`) - Disabled when `isSaving` or `alreadyMember` - If `alreadyMember`, show a badge/alert: "You are already a member of this cooperative" #### 3.7 — Submit Handler ```javascript const handleRegister = async () => { isSaving.value = true; try { const response = await axios.post('/Cooperatives/Member/Register', { cooperative_hash: props.target, ...form.value, }); if (response.data.success) { modal.open({ title: 'Registration Successful', body: 'You have been registered as a member of this cooperative!', onClose: () => navigate({ page: 'CooperativeDetail', props: { target: props.target } }) }); } else { modal.open({ title: 'Error', body: response.data.message || 'Registration failed.' }); } } catch (error) { modal.open({ title: 'Error', body: error.response?.data?.message || 'Failed to register. Please try again.' }); } finally { isSaving.value = false; } }; ``` #### 3.8 — Navigation Back - Include a "Back to Cooperative" link at the top using the same pattern from `EnrollFarmer.vue`: ```html ``` #### 3.9 — Styles - Reuse premium input styles from `CreateCooperative.vue` (`.premium-input`, `.premium-input-group`, etc.) - Include dark mode scoped overrides using `:global(.dark-mode)` pattern - Use `rounded-20` cards, smooth animations (`animate-fade-in`) ### Acceptance Criteria - [ ] Cooperative header displays correctly with all key info - [ ] All membership fields are editable - [ ] Form submits to `/Cooperatives/Member/Register` - [ ] Success modal with navigation back to detail page - [ ] Already-member state is detected and prevents duplicate registration - [ ] Dark mode compatible - [ ] Mobile responsive --- ## Task 4: Frontend — Add "Register as Member" Button to `CooperativeDetail.vue` ### Description Add a prominent call-to-action button on the `CooperativeDetail.vue` page that navigates to the new `CooperativeMemberRegister` page. ### Target File `resources/js/Pages/CooperativeDetail.vue` ### Detailed Steps 1. **Add the button** in the existing action bar (near the "Enroll New Farmer" button, around line 85-89): ```html
``` 2. **Conditional visibility** (optional enhancement): Hide the "Register as Member" button if the current user is already a member of this cooperative. This requires checking `cooperative.members` for the current user's ID (available from the Pinia user store or from the page props). ### Acceptance Criteria - [ ] Button is visible and styled consistently with existing buttons - [ ] Button navigates to `CooperativeMemberRegister` with the cooperative hashkey - [ ] Optional: Button is hidden/disabled if user is already a member --- ## Task 5: Register Route in VueRouteMap ### Description Register the new `CooperativeMemberRegister` page in the VueRouteMap so it can be accessed via direct URL and through the SPA router. ### Target File `app/Http/Controllers/Support/VueRouteMap.php` ### Detailed Steps 1. **Add entry** in the `$routes` array (after the `/cooperative-detail` entry, around line 229): ```php '/cooperative-member-register' => [ 'component' => 'CooperativeMemberRegister', 'loginRequired' => true, 'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'store owner', 'store manager', 'supplier', 'user'], ], ``` 2. **Note on access:** This page should be accessible to **most authenticated users** since any user should be able to register as a cooperative member. The `allowedUserTypes` list is kept broad. The actual permission check (`JoinCooperative`) happens on the backend. ### Acceptance Criteria - [ ] Page is accessible via `/cooperative-member-register--h:HASHKEY` URL - [ ] Login is required - [ ] Route is accessible to all standard authenticated user types --- ## Task 6: RBAC — Verify JoinCooperative Permission Mapping ### Description Ensure the `UserActions::JoinCooperative` permission is properly mapped in `UserPermissions::roles()` for all user types that should be allowed to self-register as cooperative members. ### Target File `app/Http/Controllers/Helpers/Permissions/UserPermissions.php` ### Detailed Steps 1. **Check current mapping:** Search for `JoinCooperative` in the `roles()` method. 2. **Ensure it is included for these user types at minimum:** - `ULTIMATE` (automatic — gets all actions) - `SUPER_OPERATOR` - `OPERATOR` - `COORDINATOR` - `STORE_OWNER` - `STORE_MANAGER` - `SUPPLIER` - `USER` (standard user — should be able to join cooperatives) 3. **Add to `$RoleswithNoTargetUser`** if not already there, since `JoinCooperative` doesn't target another user: ```php UserActions::JoinCooperative, ``` ### Acceptance Criteria - [ ] `JoinCooperative` is permitted for all relevant user types - [ ] `JoinCooperative` is listed in `$RoleswithNoTargetUser` --- ## Task 7: Backend — Enhance `getCooperative` Response ### Description Enhance the `getCooperative` method to include additional context that the registration page needs — specifically whether the current user is already a member. ### Target File `app/Http/Controllers/Market/CooperativeController.php` ### Detailed Steps 1. **Verify current response:** The existing `getCooperative` already loads `members.user.userInfo` — confirm it returns all organization fields (registration_number, cin, cooperative_type, etc.) 2. **Add current user membership check** to the response: ```php $currentUserMembership = null; $user = Auth::user(); if ($user) { $currentUserMembership = CooperativeMember::where('organization_id', $cooperative->id) ->where('user_id', $user->id) ->first(); } return response()->json([ 'success' => true, 'data' => $cooperative, 'is_member' => $currentUserMembership !== null, 'membership' => $currentUserMembership, ]); ``` 3. This allows the frontend to: - Show/hide the "Register as Member" button - Pre-populate the registration form if the user wants to update details - Display "Already a Member" badges ### Acceptance Criteria - [ ] `getCooperative` returns all organization fields - [ ] Response includes `is_member` boolean and `membership` data for current user - [ ] No breaking changes to existing `CooperativeDetail.vue` usage --- ## Task 8: Update Dictionary ### Description Update `ai-docs/dictionary.md` with the new module information. ### Target File `ai-docs/dictionary.md` ### Detailed Steps Add the following under the **Cooperative & User Profile** section: ```markdown ## Cooperative Member Registration - **Page**: `resources/js/Pages/CooperativeMemberRegister.vue` - **Flow**: User views `CooperativeDetail` → clicks "Register as Member" → fills membership form → POST to `/Cooperatives/Member/Register` - **Backend**: `CooperativeController@registerMember` - **Route**: `/cooperative-member-register--h:COOPERATIVE_HASHKEY` - **Permission**: `UserActions::JoinCooperative` - **Key Fields**: `membership_type`, `membership_level`, `officer_position`, `officer_level`, `concurrent_position`, `concurrent_level`, `cooperative_name_alt`, `cooperative_position`, `year_beginning` - **Membership Types**: REGULAR, ASSOCIATE, HONORARY, LABORATORY - **Membership Levels**: PRIMARY, SECONDARY, TERTIARY ``` ### Acceptance Criteria - [ ] Dictionary is updated with new module documentation - [ ] Commit and push the dictionary update --- ## Implementation Order | # | Task | Dependencies | Estimated Complexity | |---|------|-------------|---------------------| | 1 | Backend `registerMember` method | None | Medium | | 2 | Register POST route | Task 1 | Low | | 3 | Create `CooperativeMemberRegister.vue` | Task 1, 2 | High | | 4 | Add button to `CooperativeDetail.vue` | Task 3, 5 | Low | | 5 | Register in `VueRouteMap.php` | Task 3 | Low | | 6 | Verify RBAC permissions | Task 1 | Low | | 7 | Enhance `getCooperative` response | None | Low | | 8 | Update dictionary | All tasks | Low | **Recommended execution order:** 7 → 1 → 2 → 6 → 3 → 5 → 4 → 8 --- ## Files Modified (Summary) | File | Action | Description | |------|--------|-------------| | `app/Http/Controllers/Market/CooperativeController.php` | **Modify** | Add `registerMember()`, enhance `getCooperative()` | | `routes/web.php` | **Modify** | Add POST route for `/Cooperatives/Member/Register` | | `resources/js/Pages/CooperativeMemberRegister.vue` | **Create** | New registration page | | `resources/js/Pages/CooperativeDetail.vue` | **Modify** | Add "Register as Member" button | | `app/Http/Controllers/Support/VueRouteMap.php` | **Modify** | Register new page route | | `app/Http/Controllers/Helpers/Permissions/UserPermissions.php` | **Modify** | Verify/add `JoinCooperative` permission mapping | | `ai-docs/dictionary.md` | **Modify** | Document new module | --- ## Post-Implementation Checklist - [ ] Run `npm run build` to compile frontend assets - [ ] Run `docker restart bukidbountyapp` to apply backend changes - [ ] Test registration flow end-to-end - [ ] Verify dark mode compatibility - [ ] Verify mobile responsiveness - [ ] Verify duplicate membership prevention - [ ] Commit and push all changes