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

View File

@@ -0,0 +1,97 @@
## Final Implementation Plan for Hashkey/Payload URL Encoding System
### Understanding the Requirement
Based on your clarification:
- **Current navigation pattern:** `navigate({ page: 'EditUser', props: { target_user: hashkey } })`
- **URL format should be:** `/page-name--h:HASHKEY` or `/page-name--e:PAYLOAD`
- **Hashkeys are passed as values** (not prop names)
- **Encrypted payloads needed** for sensitive data
- **Universal resolver** - same mechanism for users, products, stores, etc.
### Revised Implementation Plan
#### 1. Frontend - New Composables
**File: `resources/js/composables/useUrlArgument.js`**
```javascript
export function useUrlArgument() {
// Extract hashkey/payload from URL path segments
// Parse format like "/edituser--h:HASHKEY" or "/product--e:ENCODED_PAYLOAD"
// Return: { slug, type, hashkey, payload }
}
```
**File: `resources/js/composables/useUrlEncoder.js`**
```javascript
export function useUrlEncoder() {
const encodeHash = (hashkey) => `h:${hashkey}`;
const encodePayload = (payload) => `e:${base64Encode(JSON.stringify(payload))}`;
// Returns URL-friendly encoded strings
}
```
#### 2. Frontend - Updated useNavigate
Modify existing `resources/js/composables/Core/useNavigate.js` to:
- Detect hashkey/payload in props and encode them into URL
- Support format: `/page-name--h:HASHKEY` or `/page-name--e:PAYLOAD`
- Maintain backward compatibility with current navigation
- Extract from current URL on page load
#### 3. Backend - PHP Helpers
**File: `app/Support/RouteArgumentParser.php`**
```php
class RouteArgumentParser {
public function parseArgument($argument)
// Parse format: "page-name--h:HASHKEY" or "page-name--e:ENCODED_PAYLOAD"
// Returns structured data for the resource
}
```
**File: `app/Support/HashkeyResolver.php`**
```php
class HashkeyResolver {
public function resolveByHashkey($hashkey, $modelClass)
// Generic resolver that accepts any model class (User, Product, Store, etc.)
}
```
#### 4. Backend - Middleware
**File: `app/Middleware/DecodeRouteArgumentMiddleware.php`**
```php
class DecodeRouteArgumentMiddleware {
public function handle($request, Closure $next)
// Automatically parses URL arguments and attaches to request
// Makes decoded data available as $request->decodedRoute
}
```
### URL Format Examples
- **User route:** `/edituser--h:USER_HASHKEY123`
- **Product route:** `/product/edit--h:PRODUCT_HASH456`
- **Store route:** `/store/view--h:STORE_HASH789`
- **Payload route (encrypted):** `/data/review--e:ENCODED_PAYLOAD`
### Current Navigation to URL Mapping
| Current Nav | New URL Format |
|-------------|----------------|
| `navigate({ page: 'EditUser', props: { target_user: hashkey } })` | `/edituser--h:HASHKEY` |
### Migration Strategy
1. **Phase 1: Composables** - Create `useUrlArgument.js` and `useUrlEncoder.js`
2. **Phase 2: Frontend Integration** - Update `useNavigate.js`
3. **Phase 3: Backend Parsing** - Create PHP helpers
4. **Phase 4: Middleware** - Add middleware for automatic parsing
5. **Phase 5: Testing** - Verify navigation works with both old and new formats
### Key Features
- ✅ Backward compatible (existing navigation still works)
- ✅ Universal hashkey resolver for all resource types
- ✅ Encrypted payload support for sensitive data
- ✅ Standard URL format across all routes

View File

@@ -0,0 +1,82 @@
## Full Implementation Plan
### Phase 1: Service Worker Implementation
**Files to create/modify:**
- `resources/js/service-worker.js` - Main service worker file
- `resources/js/app.js` - Register service worker
- `public/sw.js` - Entry point for SW registration (if needed)
**Service Worker Features:**
1. **Precaching static assets** using workbox precacheAndRoute
2. **Stale-while-revalidate strategy** for pages with cache name `page-stale-while-revalidate-cache`
3. **Cache-first strategy** for `/RequestData/File/` blobs with cache name `request-data-cache`
4. **Background sync** with `BackgroundSyncPlugin`
5. **Message passing** to clients using `clients.postMessage()`
### Phase 2: OPFS Implementation
**Files to create:**
- `resources/js/composables/useOPFS.js` - OPFS file management
- Update `resources/js/composables/useFileBlobCache.js` to use OPFS
**OPFS Features:**
1. **Save blobs to OPFS** using `navigator.storage.getDirectory().getFileHandle()`
2. **Load blobs from OPFS** for faster access
3. **Handle hashkey-to-blob mapping**
4. **Fallback to server fetch** if blob not in OPFS
### Phase 3: Pinia + SSE Sync (Replace Web Workers)
**Files to create/modify:**
- `resources/js/stores/syncState.js` - New store for sync state
- Update `resources/js/composables/useSyncData.js`
- Implement Server-Sent Events listener
**Features:**
1. **Server-Sent Events (SSE)** for real-time updates
2. **Pinia state management** for cache status
3. **Interval-based polling** as fallback if SSE not available
4. **Cache update notifications** via Pinia
### Phase 4: Hashkey-Based URL Caching
**Files to create:**
- `resources/js/composables/useHashKeyCache.js`
**Features:**
1. **URL format support**: `/page--h:HASHKEY` and `/page--e:PAYLOAD`
2. **Cache lookup by hashkey** in URL
3. **Hashkey-to-data mapping** with proper cache invalidation
### Phase 5: RequestData Pattern Migration
**Files to create:**
- `resources/js/utils/RequestData.js`
**Features:**
1. **Method chaining**: `.url().type().data().success().error().go()`
2. **Hashkey support** for caching
3. **`fromVarCache` option** for cache-first behavior
### Implementation Steps:
1. **Create service worker file** with workbox strategies
2. **Register SW in app.js**
3. **Test basic caching** works
4. **Implement OPFS composable**
5. **Update file blob cache** to use OPFS
6. **Add SSE listener** for real-time updates
7. **Create hashkey URL pattern** support
8. **Migrate RequestData pattern** to new fetch wrapper
### Estimated Timeline:
- Phase 1 (Service Worker): 2-3 hours
- Phase 2 (OPFS): 1-2 hours
- Phase 3 (Pinia + SSE): 2-3 hours
- Phase 4 (Hashkey URL): 1 hour
- Phase 5 (RequestData): 1-2 hours
Total: ~8-10 hours of implementation time
Are you ready to proceed with the implementation? Please toggle to Act mode and I'll start building these features.