initial: bootstrap from BukidBountyApp base
This commit is contained in:
490
plans/11a8aa601d3b45c6aa16ff2b28eb1004.md
Normal file
490
plans/11a8aa601d3b45c6aa16ff2b28eb1004.md
Normal file
@@ -0,0 +1,490 @@
|
||||
# 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>` | 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-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
|
||||
<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-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
|
||||
<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>
|
||||
```
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user