18 KiB
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
EnrollFarmerflow (admin-initiated, adds a farmer to a cooperative) - A
joinCooperativebackend method onCooperativeController(self-join, but no frontend) - A
useUserAdditionalDetailscomposable withjoinCooperative/leaveCooperativemethods (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
-
Add method signature:
public function registerMember(Request $request) -
Permission check: Use
UserActions::JoinCooperativefor the permission gate:if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::JoinCooperative)) { return ResponseHelper::returnUnauthorized(); } -
Validate required input:
cooperative_hash(required) — hashkey of the target cooperative- All other membership fields are optional
-
Lookup the cooperative:
$cooperative = Organization::where('hashkey', $cooperativeHash)->first(); if (!$cooperative) { return ResponseHelper::returnError('Cooperative not found', 404); } -
Check duplicate membership:
$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'); } -
Accept full membership fields from request:
$memberFields = $request->only([ 'role', 'membership_type', 'membership_level', 'officer_position', 'officer_level', 'concurrent_position', 'concurrent_level', 'cooperative_name_alt', 'cooperative_position', 'year_beginning' ]); -
Create the
CooperativeMemberrecord:$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(); -
Sync with
users.settings.cooperatives:$settings = $user->settings ?? []; $cooperatives = $settings['cooperatives'] ?? []; if (!in_array($cooperativeHash, $cooperatives)) { $cooperatives[] = $cooperativeHash; $settings['cooperatives'] = $cooperatives; $user->settings = $settings; $user->save(); } -
Return success:
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
- Add route under the Cooperative Module Routes section (after line 543):
Route::post('/Cooperatives/Member/Register', [\App\Http\Controllers\Market\CooperativeController::class, 'registerMember'], ['middleware' => 'auth']);
Acceptance Criteria
- Route is registered and protected by
authmiddleware - 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
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
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/Getwith{ hashkey: props.target } - Populate
cooperative.value - Check
response.data.is_memberto setalreadyMember(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> |
Select Type | Options: REGULAR, ASSOCIATE, HONORARY, LABORATORY |
membership_level |
<select> |
Select Level | Options: PRIMARY, SECONDARY, TERTIARY |
year_beginning |
<input type="number"> |
e.g. 2024 | Year membership begins |
Section: Position Details (Optional)
| Field | Type | Placeholder | Notes |
|---|---|---|---|
officer_position |
<input type="text"> |
e.g. Board Member | Optional officer role |
officer_level |
<select> |
Select Level | Options: PRIMARY, SECONDARY, TERTIARY |
concurrent_position |
<input type="text"> |
e.g. Treasurer | Additional concurrent role |
concurrent_level |
<select> |
Select Level | Same as officer_level |
cooperative_position |
<input type="text"> |
e.g. Chairperson | Position within the cooperative |
Section: Alternative Cooperative Name (Optional)
| Field | Type | Placeholder | Notes |
|---|---|---|---|
cooperative_name_alt |
<input type="text"> |
Alternative name | If the cooperative is known by another name |
3.6 — Submit Button
- Full width,
btn-premium-launchstyle (reuse fromCreateCooperative.vue) - Disabled when
isSavingoralreadyMember - If
alreadyMember, show a badge/alert: "You are already a member of this cooperative"
3.7 — Submit Handler
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:<button @click="navigate({ page: 'CooperativeDetail', target: target })" class="btn btn-link ..."> <i class="fas fa-arrow-left me-1"></i> Back to Cooperative </button>
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-20cards, 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
-
Add the button in the existing action bar (near the "Enroll New Farmer" button, around line 85-89):
<div class="mb-4 d-flex justify-content-end gap-2 flex-wrap"> <button @click="navigate({ page: 'CooperativeMemberRegister', props: { target: props.target } })" class="btn btn-success rounded-pill px-4 py-2 shadow-sm"> <i class="fas fa-id-card me-2"></i> Register as Member </button> <button @click="navigate({ page: 'EnrollFarmer', target: props.target })" class="btn btn-primary rounded-pill px-4 py-2 shadow-sm"> <i class="fas fa-user-plus me-2"></i> Enroll New Farmer </button> </div> -
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.membersfor 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
CooperativeMemberRegisterwith 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
-
Add entry in the
$routesarray (after the/cooperative-detailentry, around line 229):'/cooperative-member-register' => [ 'component' => 'CooperativeMemberRegister', 'loginRequired' => true, 'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'store owner', 'store manager', 'supplier', 'user'], ], -
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
allowedUserTypeslist is kept broad. The actual permission check (JoinCooperative) happens on the backend.
Acceptance Criteria
- Page is accessible via
/cooperative-member-register--h:HASHKEYURL - 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
- Check current mapping: Search for
JoinCooperativein theroles()method. - Ensure it is included for these user types at minimum:
ULTIMATE(automatic — gets all actions)SUPER_OPERATOROPERATORCOORDINATORSTORE_OWNERSTORE_MANAGERSUPPLIERUSER(standard user — should be able to join cooperatives)
- Add to
$RoleswithNoTargetUserif not already there, sinceJoinCooperativedoesn't target another user:UserActions::JoinCooperative,
Acceptance Criteria
JoinCooperativeis permitted for all relevant user typesJoinCooperativeis 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
-
Verify current response: The existing
getCooperativealready loadsmembers.user.userInfo— confirm it returns all organization fields (registration_number, cin, cooperative_type, etc.) -
Add current user membership check to the response:
$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, ]); -
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
getCooperativereturns all organization fields- Response includes
is_memberboolean andmembershipdata for current user - No breaking changes to existing
CooperativeDetail.vueusage
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:
## 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 buildto compile frontend assets - Run
docker restart bukidbountyappto 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