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,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